The rules cards get zones: boxed rows, threat colour only for the threat, the blood pool as pips

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Fable 5 committed 2026-08-30 16:54:05 +02:00
1 parent 00f121bec7
commit bfb3631ec3
4 files changed
+66 -4

No files matched your search

+9
View File
@@ -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 `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. 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") ## 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 The opponent intro carried six kinds of information as one modal line stack. It wears designated
+1 -1
View File
@@ -871,6 +871,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div> </div>
</div> </div>
</div> </div>
<script type="module" src="src/main.js?v=337"></script> <script type="module" src="src/main.js?v=338"></script>
</body> </body>
</html> </html>
+11 -3
View File
@@ -390,19 +390,27 @@ export function startMatch(cup = false, draw = null, train = 0) {
G.seen.train = true; G.seen.train = true;
showModal({ title: 'THE TRAINING COURT', color: UI.hot, showModal({ title: 'THE TRAINING COURT', color: UI.hot,
body: ['ONE BODY, ONE GAME AT A TIME', 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) { } else if (cup && !G.seen.cup) {
G.seen.cup = true; G.seen.cup = true;
showModal({ title: 'THE TOURNAMENT', color: UI.hot, showModal({ title: 'THE TOURNAMENT', color: UI.hot,
body: ["UP YOUR LEAGUE'S DEAD HIERARCHY — THE FINAL IS SET", 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) { } else if (!cup && !train && !G.seen.match) {
G.seen.match = true; G.seen.match = true;
// Oliver's copy direction: name the stake, count the blood. TUNE.matchPlayerHp is the real // 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. // number, so the modal can never drift from the pool it describes.
showModal({ title: 'DEATH MATCH', color: UI.hot, showModal({ title: 'DEATH MATCH', color: UI.hot,
body: ['WIN THE POINT — WOUND THE ZOMBIE', 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(); matchOpponent();
matchReady(); matchReady();
+45
View File
@@ -2341,6 +2341,50 @@ function drawFlickHint() {
const BACK_C = 1.70158; const BACK_C = 1.70158;
const easeBack = p => 1 + (BACK_C + 1) * (p - 1) ** 3 + BACK_C * (p - 1) ** 2; 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 // 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 // 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 // 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.fillRect(0, 0, W, H);
ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
if (m.foe) return drawFoeModal(m, t, dx); 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]; 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 // 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 // portrait-sized icon pushed the TAP prompt down onto the weapon rack