The walk-on gets its own face: mugshot masthead, the measure, the round strip — no more wall of text
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7fb595ca22
commit
00f121bec7
5 files changed
+87
-10
No files matched your search
+12
@@ -491,6 +491,18 @@ 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 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
|
||||
zones now (`render.drawFoeModal`, driven by `m.foe` from matchOpponent): the MASTHEAD (mugshot
|
||||
large on the left, the name in the announcement face, the slogan wrapped beneath — the shared
|
||||
wrapText, measured against the space beside the mug), THE MEASURE (a split .figs-language panel:
|
||||
ITS LEVEL boxed against YOURS, the number in the modal's hot red while the body outranks you and
|
||||
the pick green once it does not), the round/lesson strip, and the blinking tap. Same modal
|
||||
machinery throughout — entrance lanes (masthead from the right, the measure from the left), the
|
||||
frozen court, dismiss rules. m.title/m.body still carry the same words, so the suite and any
|
||||
legacy reader are untouched; the one icon-shaped assertion moved to m.foe.head.
|
||||
|
||||
## Session log: the look stops lying (2026-08-29, Oliver: "the man with a pink shirt")
|
||||
|
||||
After a reload with the women's tour picked, the live-player preview showed THE MAN from behind
|
||||
|
||||
+1
-1
@@ -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=336"></script>
|
||||
<script type="module" src="src/main.js?v=337"></script>
|
||||
</body>
|
||||
</html>
|
||||
+12
-8
@@ -649,15 +649,19 @@ function matchOpponent() {
|
||||
// 1.6 s intro beat until the tap, and the head wears the same z_<fam>head crop the card wore.
|
||||
// In the cup the round names itself on the second line.
|
||||
const cup = G.match.cup;
|
||||
// the "am I its peer yet?" line (Oliver, 2026-08-29): the body's ladder position spread over
|
||||
// the profile's own level scale, beside yours — display only, nothing scales off it
|
||||
const vs = 'LEVEL ' + foeLevel(G.match.opp) + ' — YOURS: ' + levelAt(prof.xp);
|
||||
showModal({ title: zb.foe.name, color: UI.hot, icon: 'z_' + zb.fam + 'head', iconH: 64,
|
||||
body: [zb.foe.line, vs].concat(cup ? [cup.rounds[cup.round].name + (cup.rounds[cup.round].need > 1
|
||||
// THE WALK-ON wears its own zoned layout (render.drawFoeModal — Oliver: the generic line
|
||||
// stack was "a wall of text"). `foe` drives the drawing; title/body still carry the same
|
||||
// words for the suite and any legacy reader. The level pair is display only.
|
||||
const sub = cup ? [cup.rounds[cup.round].name + (cup.rounds[cup.round].need > 1
|
||||
? ' · FIRST TO ' + cup.rounds[cup.round].need + ' GAMES' : '')]
|
||||
// the top of the band walking BACK on is the arcade's natural "what now?" moment — the
|
||||
// intro answers it in place (THE LEAGUES: the cup is the only promotion)
|
||||
: G.match.again ? ['NOBODY STRONGER IS LEFT', 'THE CUP IS THE WAY UP'] : []) });
|
||||
// the top of the band walking BACK on is the arcade's natural "what now?" moment — the
|
||||
// intro answers it in place (THE LEAGUES: the cup is the only promotion)
|
||||
: G.match.again ? ['NOBODY STRONGER IS LEFT', 'THE CUP IS THE WAY UP'] : [];
|
||||
const lvl = foeLevel(G.match.opp), mine = levelAt(prof.xp);
|
||||
showModal({ title: zb.foe.name, color: UI.hot,
|
||||
body: [zb.foe.line, 'LEVEL ' + lvl + ' — YOURS: ' + mine].concat(sub),
|
||||
foe: { head: 'z_' + zb.fam + 'head', name: zb.foe.name, line: zb.foe.line,
|
||||
lvl, mine, sub } });
|
||||
// ...and a fresh body has read NOTHING yet: the habit tracker starts clean (see THE READ, config)
|
||||
G.match.laneRun = 0; G.match.lastLane = undefined; G.match.surprised = G.match.expected = false;
|
||||
// a FLAT pool per body: with regicide gone the body is pure scoreboard (matchPoint takes a
|
||||
|
||||
@@ -2341,6 +2341,65 @@ function drawFlickHint() {
|
||||
const BACK_C = 1.70158;
|
||||
const easeBack = p => 1 + (BACK_C + 1) * (p - 1) ** 3 + BACK_C * (p - 1) ** 2;
|
||||
|
||||
// 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
|
||||
// the modal's hot red while the body outranks you, the pick green once it does not), and the
|
||||
// round/lesson strip. Same machinery as every modal otherwise: the entrance lanes, the frozen
|
||||
// court, the blinking tap (drawModal below). m.title/m.body still carry the same words for the
|
||||
// suite and any legacy reader — this is a LAYOUT, not new state.
|
||||
function drawFoeModal(m, t, dx) {
|
||||
const f = m.foe, img = f.head && SPR[f.head];
|
||||
const boxW = Math.min(W - 24, 250), bx = Math.round(W / 2 - boxW / 2);
|
||||
const mugH = Math.round(Math.min(56, H * 0.22));
|
||||
const mugW = img ? Math.round(mugH * img.width / img.height) : 0;
|
||||
const slogan = wrapText(f.line, F(8, 0), boxW - (mugW ? mugW + 12 : 0)); // the shared wrapper sets the font
|
||||
const headH = Math.max(mugH, 26 + slogan.length * 12);
|
||||
const measureH = 36, gap = 14;
|
||||
const subH = f.sub.length ? f.sub.length * 12 + 4 : 0;
|
||||
const blockH = headH + gap + measureH + (subH ? gap - 4 + subH : 0);
|
||||
let y = Math.round(H / 2 - blockH / 2);
|
||||
// THE MASTHEAD flies from the right as one piece
|
||||
const hx = dx(0, 1);
|
||||
const nx = bx + (mugW ? mugW + 12 : 0) + hx;
|
||||
if (img) { ctx.imageSmoothingEnabled = false; ctx.drawImage(img, bx + hx, y, mugW, mugH); }
|
||||
ctx.textAlign = 'left';
|
||||
ctx.font = B(20);
|
||||
ctx.fillStyle = m.color || UI.hot;
|
||||
ctx.fillText(f.name, nx, y + 10);
|
||||
ctx.font = F(8, 0);
|
||||
slogan.forEach((l, i) => shadowText(l, nx, y + 28 + i * 12, UI.cream));
|
||||
y += headH + gap;
|
||||
// THE MEASURE flies from the left: two boxed figures sharing one frame (the .figs language)
|
||||
const mx = dx(1, -1);
|
||||
const half = Math.round(boxW / 2);
|
||||
panel(bx + mx, y, boxW, measureH, 3, 'rgba(8,26,18,0.85)', 'rgba(120,150,130,0.4)');
|
||||
ctx.strokeStyle = 'rgba(120,150,130,0.4)'; ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(bx + mx + half + 0.5, y + 4); ctx.lineTo(bx + mx + half + 0.5, y + measureH - 4);
|
||||
ctx.stroke();
|
||||
ctx.textAlign = 'center';
|
||||
ctx.font = F(7, 0);
|
||||
shadowText('ITS LEVEL', bx + mx + half / 2, y + 10, UI.dim);
|
||||
shadowText('YOURS', bx + mx + half * 1.5, y + 10, UI.dim);
|
||||
ctx.font = F(13, 700);
|
||||
shadowText(String(f.lvl), bx + mx + half / 2, y + 25, f.lvl > f.mine ? (m.color || UI.hot) : UI.pick);
|
||||
shadowText(String(f.mine), bx + mx + half * 1.5, y + 25, UI.cream);
|
||||
y += measureH;
|
||||
// the round / the lesson, from the left a beat later
|
||||
if (f.sub.length) {
|
||||
y += gap - 4;
|
||||
ctx.font = F(8, 0);
|
||||
f.sub.forEach((l, i) => shadowText(l, W / 2 + dx(2, -1), y + 6 + i * 12, UI.dim));
|
||||
}
|
||||
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';
|
||||
}
|
||||
|
||||
function drawModal(m) {
|
||||
// The entrance (m.in, 0..1, ticked in gameplay's update behind the world freeze): the icon and
|
||||
// title fly in from the RIGHT, the body lines cascade in from the LEFT one after another, each
|
||||
@@ -2357,6 +2416,7 @@ function drawModal(m) {
|
||||
ctx.fillStyle = `rgba(8,6,10,${(0.82 * Math.min(1, t / 0.35)).toFixed(3)})`; // the dim washes in first
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
if (m.foe) return drawFoeModal(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
|
||||
|
||||
@@ -2058,7 +2058,8 @@ test('the walk-on is a MODAL the player taps away, and it holds the world', () =
|
||||
assert.ok(_startMatch());
|
||||
assert.ok(G.modal, 'the opponent announces as a modal, not a band card');
|
||||
assert.equal(G.modal.title, cfg.matchFoe(1).name, 'it names the body');
|
||||
assert.equal(G.modal.icon, 'z_' + cfg.matchFoe(1).fam + 'head', '...and wears its head');
|
||||
assert.equal(G.modal.foe.head, 'z_' + cfg.matchFoe(1).fam + 'head', '...and wears its head (the zoned foe layout)');
|
||||
assert.ok(G.modal.foe.lvl >= 1 && G.modal.foe.mine >= 1, '...and carries the level pair');
|
||||
const z0 = oppOf().z, intro0 = G.interlude.introT;
|
||||
for (let i = 0; i < 60; i++) update(DT);
|
||||
assert.equal(oppOf().z, z0, 'the frozen court holds while it is read');
|
||||
|
||||
Reference in new issue
Block a user