diff --git a/PROJECT.md b/PROJECT.md index a64be1b..e5a5c28 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -491,6 +491,15 @@ zombie (now an occasional opponent, `TUNE.racketEvery`, not the every-game fixtu `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 rules cards get zones too (2026-08-29, Oliver: "wichtige Infos, schwer zu erfassen") + +Same disease as the walk-on, same medicine: the three mode modals (THE TOURNAMENT, DEATH MATCH, +THE TRAINING COURT) carried compound em-dash sentences edge to edge. `render.drawRulesModal` +(driven by `m.rules: [{lead, sub, hot, pips}]`): each rule is its own boxed row — the statement +in cream, the consequence beneath it DIM for information and the modal's hot colour only when it +threatens (`hot`), and a rule about blood shows the pool as the HUD's own pips instead of a naked +number. Entrance lanes, freeze and tap rules unchanged; title/body still carry the words. + ## Session log: the walk-on gets its own face (2026-08-29, Oliver: "buried in a wall of text") The opponent intro carried six kinds of information as one modal line stack. It wears designated diff --git a/index.html b/index.html index ad355f5..ec2de3d 100644 --- a/index.html +++ b/index.html @@ -871,6 +871,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat - + diff --git a/src/beats.js b/src/beats.js index e45c750..0b5f0a6 100644 --- a/src/beats.js +++ b/src/beats.js @@ -390,19 +390,27 @@ export function startMatch(cup = false, draw = null, train = 0) { G.seen.train = true; showModal({ title: 'THE TRAINING COURT', color: UI.hot, body: ['ONE BODY, ONE GAME AT A TIME', - 'FELL IT — OR BLEED OUT — AND IT COMES BACK', 'NO RUN IS BANKED — REPS PAY QUARTER XP'] }); + 'FELL IT — OR BLEED OUT — AND IT COMES BACK', 'NO RUN IS BANKED — REPS PAY QUARTER XP'], + rules: [{ lead: 'ONE BODY, ONE GAME AT A TIME', sub: 'FELL IT OR BLEED OUT — IT COMES BACK' }, + { lead: 'NO RUN IS BANKED', sub: 'REPS PAY QUARTER XP' }] }); } else if (cup && !G.seen.cup) { G.seen.cup = true; showModal({ title: 'THE TOURNAMENT', color: UI.hot, body: ["UP YOUR LEAGUE'S DEAD HIERARCHY — THE FINAL IS SET", - 'ITS POINTS DRAW BLOOD — ' + TUNE.matchPlayerHp + ' AND YOU ARE EATEN'] }); + 'ITS POINTS DRAW BLOOD — ' + TUNE.matchPlayerHp + ' AND YOU ARE EATEN'], + rules: [{ lead: "UP YOUR LEAGUE'S DEAD HIERARCHY", sub: 'THE FINAL IS SET' }, + { lead: 'ITS POINTS DRAW BLOOD', sub: 'AT ZERO YOU ARE EATEN', hot: true, + pips: TUNE.matchPlayerHp }] }); } else if (!cup && !train && !G.seen.match) { G.seen.match = true; // Oliver's copy direction: name the stake, count the blood. TUNE.matchPlayerHp is the real // number, so the modal can never drift from the pool it describes. showModal({ title: 'DEATH MATCH', color: UI.hot, body: ['WIN THE POINT — WOUND THE ZOMBIE', - 'ITS POINTS DRAW BLOOD — ' + TUNE.matchPlayerHp + ' AND YOU ARE EATEN'] }); + 'ITS POINTS DRAW BLOOD — ' + TUNE.matchPlayerHp + ' AND YOU ARE EATEN'], + rules: [{ lead: 'WIN THE POINT', sub: 'WOUND THE ZOMBIE' }, + { lead: 'ITS POINTS DRAW BLOOD', sub: 'AT ZERO YOU ARE EATEN', hot: true, + pips: TUNE.matchPlayerHp }] }); } matchOpponent(); matchReady(); diff --git a/src/render.js b/src/render.js index cfc0588..50579f6 100644 --- a/src/render.js +++ b/src/render.js @@ -2341,6 +2341,50 @@ function drawFlickHint() { const BACK_C = 1.70158; const easeBack = p => 1 + (BACK_C + 1) * (p - 1) ** 3 + BACK_C * (p - 1) ** 2; +// THE RULES CARD (2026-08-29, Oliver: the mode modals were walls of text too). Each rule is its +// own boxed row — the statement in cream over its consequence in the modal's colour — and a rule +// about BLOOD shows the pool as the game's own pips instead of a naked number. Driven by +// m.rules: [{ lead, sub, pips }]; title/body keep carrying the words for the suite. +function drawRulesModal(m, t, dx) { + const boxW = Math.min(W - 28, 240), bx = Math.round(W / 2 - boxW / 2); + const rowH = r => (r.sub ? 34 : 22) + (r.pips ? 12 : 0); + const gap = 8; + const blockH = 30 + m.rules.reduce((a, r) => a + rowH(r) + gap, 0) - gap; + let y = Math.round(H / 2 - blockH / 2); + const hx = dx(0, 1); + ctx.font = B(24); ctx.fillStyle = m.color || UI.warm; + if (hx) for (const [f, a] of [[0.7, 0.12], [0.35, 0.26]]) { + ctx.save(); ctx.globalAlpha = a; + ctx.fillText(m.title, W / 2 + Math.round(hx * (1 + f)), y + 8); + ctx.restore(); + } + ctx.fillText(m.title, W / 2 + hx, y + 8); + y += 30; + m.rules.forEach((r, i) => { + const h = rowH(r), rx = dx(i + 1, -1); + panel(bx + rx, y, boxW, h, 3, 'rgba(8,26,18,0.85)', 'rgba(120,150,130,0.4)'); + ctx.font = F(8, 700); + shadowText(r.lead, W / 2 + rx, y + 12, UI.cream); + // the consequence line: DIM for information, the modal's hot colour only when it threatens + if (r.sub) { ctx.font = F(8, 0); shadowText(r.sub, W / 2 + rx, y + 25, r.hot ? (m.color || UI.warm) : UI.dim); } + if (r.pips) { // the pool, in the HUD's own pip language + const pg = 9, pw = (r.pips - 1) * pg; + for (let k = 0; k < r.pips; k++) { + ctx.beginPath(); ctx.arc(W / 2 + rx - pw / 2 + k * pg, y + h - 9, 2.6, 0, 7); + ctx.fillStyle = '#8a1a12'; ctx.fill(); + ctx.strokeStyle = 'rgba(0,0,0,0.5)'; ctx.lineWidth = 1; ctx.stroke(); + } + } + y += h + gap; + }); + if (t >= 1 && Math.floor(G.idleT * 2) % 2) { + ctx.font = F(8); + shadowText(IS_TOUCH ? 'TAP TO CONTINUE' : 'PRESS ANY KEY', W / 2, + Math.round(H / 2 + blockH / 2 + 24), UI.warm); + } + ctx.textBaseline = 'top'; +} + // THE WALK-ON'S OWN FACE (2026-08-29, Oliver: the opponent card was "a wall of text" — six kinds // of information in one line stack). Designated zones instead: the MASTHEAD (mugshot beside the // name with the slogan under it), THE MEASURE (its level boxed against yours — the number wears @@ -2417,6 +2461,7 @@ function drawModal(m) { ctx.fillRect(0, 0, W, H); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; if (m.foe) return drawFoeModal(m, t, dx); + if (m.rules) return drawRulesModal(m, t, dx); const body = m.body || [], img = m.icon && SPR[m.icon]; // cap the illustration against the viewport, not just the asset: at 270px tall (landscape) a // portrait-sized icon pushed the TAP prompt down onto the weapon rack