diff --git a/HANDOFF.md b/HANDOFF.md index 33b6207..520857d 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -343,7 +343,13 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu `perfectMs` window, the slingshot earns it by STOPPING in `pullFull..pullFull*perfectBand`. The draw clicks brighter when you enter that band and goes quiet when you overshoot — without that audible target it would be invisible skill. The word itself rotates through `PRAISE` (cycled, not - random: the same word twice running reads as a stuck HUD element, not as praise). + random: the same word twice running reads as a stuck HUD element, not as praise) and sits behind + `TUNE.praiseEvery`, the same shared-cooldown model the umpire's asides use — you LEARN to hit the + band, and then a per-serve word is noise again whatever it says. The click is the feedback for a + good pull; the word is garnish. +- **The free-floating slot is 18px, not card-sized.** Half of what lands in it (the praise words, + `RETURNED`, `DOUBLE KILL`) is a footnote to something you already watched happen. Sharing the loud + FACE with the milestone cards is right; sharing their SIZE made every footnote read as a milestone. ## Session log: the sidestep, and two type registers (2026-08-03) diff --git a/config.js b/config.js index e808c4d..3ac4231 100644 --- a/config.js +++ b/config.js @@ -195,6 +195,10 @@ export const TUNE = { // needs the pull to STOP in the band pullFull..pullFull*this (130-166 px), which // is a target you can miss. The pull clicks when you enter it (see input.js) and // goes quiet when you overshoot, so the window is learnable by feel. + praiseEvery: 14, // s between praise words, on the umpire's-asides model (asideEvery below). The + // band alone was not enough: you learn to hit it, and then the HUD talks on + // every other serve again. The CLICK is the reliable feedback for a good pull — + // the word is garnish, and garnish on every plate is just food. aimCone: 0.10, // lane half-width added per meter of range. This is what makes the pull's ANGLE // mean something: at 0.45 the cone was ±10 m at the far baseline — the whole // court — so every downward pull picked the same body and the angle felt inert. diff --git a/gameplay.js b/gameplay.js index dd74e9c..73da091 100644 --- a/gameplay.js +++ b/gameplay.js @@ -43,7 +43,14 @@ export const pullPerfect = mag => mag >= TUNE.pullFull && mag <= TUNE.pullFull * // running is what made one word read as a stuck HUD element rather than as praise. const PRAISE = ['PERFECT', 'TEXTBOOK', 'IMMACULATE', 'OH, LOVELY', 'UNPLAYABLE', 'SHOT OF THE DAY']; let praiseN = 0; -const praise = () => { G.flash = { text: PRAISE[praiseN++ % PRAISE.length], t: 0.7 }; }; +// ...and he does not say it every time. Same cooldown model as the asides (aside()): once you have +// learned the band you hit it constantly, and a HUD that talks on every other serve is noise +// whatever the word is. The bright click in the pull is the reliable feedback; this is the garnish. +function praise() { + if (G.praiseT > 0) return; + G.praiseT = TUNE.praiseEvery; + G.flash = { text: PRAISE[praiseN++ % PRAISE.length], t: 0.7 }; +} export function flickTarget(ch, aim) { // exported: render draws the live reticle with it const q = pullQuality(aim.mag), f = pullFrac(aim.mag); @@ -1213,6 +1220,7 @@ export function update(dt) { crowd.cheer = Math.max(0, crowd.cheer - dt); G.idleT += dt; if (G.asideT > 0) G.asideT -= dt; // the asides' shared cooldown (see aside()) + if (G.praiseT > 0) G.praiseT -= dt; // ...and the praise words' (see praise()) // the fired band unravels behind the ball. It also goes the moment the ball does — a shambler // eating the shot ends the band with the splat, which is a cut you want. if (G.trail && ((G.trail.t += dt) >= G.trail.T || !ball.active)) G.trail = null; diff --git a/index.html b/index.html index ac0fbd8..ff69d58 100644 --- a/index.html +++ b/index.html @@ -184,6 +184,6 @@ - + diff --git a/render.js b/render.js index 60586f1..bd08869 100644 --- a/render.js +++ b/render.js @@ -1578,7 +1578,10 @@ export function render() { // RETURNED, CHANGEOVER, weapon labels — everything that used to be plain white 12px text sitting // directly on the crowd. It keeps its 1px jitter; the shadow is what makes it legible anywhere. if (G.flash) { - ctx.font = B(24); + // 18, not 24: at the card's near-size a transient one-liner read as a milestone announcement, + // and half of what lands in this slot (PERFECT, RETURNED, DOUBLE KILL) is only ever a footnote + // to something you already watched happen. The register is right; the volume was not. + ctx.font = B(18); shadowText(G.flash.text, W / 2, FLASH_Y + Math.sin(G.flash.t * 20) * 1, '#ffffff'); } // the SET milestone card (the duel's own announcement is a real modal now, not a timed card) diff --git a/state.js b/state.js index ae14cac..d81468a 100644 --- a/state.js +++ b/state.js @@ -81,6 +81,7 @@ export const G = { hurtT: 0, // s of post-hit grace left; also drives the red hit flash hurtFlash: 0, // s of the impact wash left asideT: 14, // s until the umpire's next unsolicited remark (TUNE.asideEvery) + praiseT: 0, // s until the next praise word is allowed (TUNE.praiseEvery) bossDue: 0, // chapter number the schedule owes you — fired at the next hand-cleared court bossFight: 0, // chapter number of the boss currently on court, 0 = none. Blocks all other spawns. slowT: 0, // s of REAL time left running the world in slow motion (a chapter boss dying)