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
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
+1 -1
View File
@@ -871,6 +871,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div>
</div>
</div>
<script type="module" src="src/main.js?v=337"></script>
<script type="module" src="src/main.js?v=338"></script>
</body>
</html>
+11 -3
View File
@@ -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();
+45
View File
@@ -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