Story: the cup's tableau between rounds, a brighter act card (v388)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
wuwiandClaude Opus 5.5 committed 2026-10-16 15:32:35 +02:00
1 parent 0576f24614
commit 0c67d32519
5 files changed
+67 -5

No files matched your search

+15
View File
@@ -636,6 +636,21 @@ Each phase ships to staging and gets a phone verdict, and tests ride along with
5. **LATER.** The metal bed (when Oliver has a track: start at the transformation, gore only) and Members'
Night (option C).
## Session log: the cup's tableau returns, the act card shows its court (2026-10-16, v388)
Oliver after a full replay: "das im Storyboard benannte 03 Turnier-Tableau sehe ich nie" — right, phases
1-4 never built it (the old #bracket stage was retired 2026-08-29 for the arcade cup and stays retired).
Now `main.storyScreen('draw', {act, round})`: the act's rounds as rows (head crop, round name, body name;
the final framed gold), WON stamped on what is behind you, NEXT blinking on the next face, the court tile
dim underneath. It shows (a) straight after the act card — `storyUI.show('act')` chains it, labelled
⚑ SAVED because the cup start is the checkpoint — and (b) after every non-final round, from
`beats.cupGameWon`'s afterFall right after `cupDraw`. Auto-closes after 3.2 s, a tap skips; a pock per
row, a boom on the stamp. Story only — the arcade cup is untouched. Test: one tableau per won round.
Act card: "das farbige Bild ruhig ein bisschen deutlicher durchscheinen lassen" — `.bgc` lost its
opacity .55 and the veil went from #0009→#000d to #0006→#0004→#000c (the storyboard's own look); the
small Silkscreen labels over a picture got a black halo so they still read on clay.
## Session log: the ladder is flatter — the width cliff (2026-10-15, v387)
Oliver: *"ab Runner ist das Zombie-Game schon sehr schwer — können wir die Schwierigkeitsleiter ein wenig
abflachen?"* **Measured first**: cup mode, the ring bot, 6-8 seeds (`tools/rally-sim.mjs`, `T_<knob>` sweeps).
+13 -3
View File
@@ -131,9 +131,11 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
#sty[hidden] { display: none; }
#sty .col { width: min(420px, 100%); display: grid; gap: 14px; justify-items: center; text-align: center; }
#sty .bgc { position: absolute; inset: 0; background-size: cover; background-position: center 30%;
image-rendering: pixelated; opacity: .55; }
#sty .bgc::after { content: ''; position: absolute; inset: 0; background: linear-gradient(#0009, #000d 80%); }
image-rendering: pixelated; }
#sty .bgc::after { content: ''; position: absolute; inset: 0; background: linear-gradient(#0006, #0004 35%, #000c 85%); }
#sty .col { position: relative; }
#sty .bgc ~ .col .lab { text-shadow: 1px 1px 0 #000, 0 0 8px #000, 0 0 3px #000; }
#sty .bgc.dim::after { background: linear-gradient(#000b, #000e 70%); }
#sty .lab { font: 400 10px Silkscreen, monospace; letter-spacing: 2px; color: #8fa0b8; text-transform: uppercase; }
#sty .gold { color: #e0cc72; } #sty .hot { color: #d9705f; } #sty .pink { color: #ff5cf0; }
#sty .huge { font: 400 92px/0.9 Bangers, Impact, sans-serif; text-shadow: 4px 4px 0 #000; margin: 0; }
@@ -157,6 +159,14 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
#sty .next { width: 100%; height: 92px; border-radius: 4px; background-size: cover; background-position: center;
image-rendering: pixelated; display: flex; align-items: flex-end; padding: 8px; text-align: left; box-sizing: border-box; }
#sty .next .lab { text-shadow: 1px 1px 0 #000, 0 0 6px #000; }
#sty .draw { width: 100%; display: grid; gap: 8px; }
#sty .draw > div { display: flex; align-items: center; gap: 12px; height: 64px; padding: 0 10px; text-align: left;
border: 1px solid #ffffff22; border-radius: 4px; background: #0006; }
#sty .draw > div.fin { border-color: #e0cc72; }
#sty .draw > div.done img, #sty .draw > div.done .mid { opacity: .5; }
#sty .draw img { height: 52px; width: 52px; }
#sty .draw .st { margin-left: auto; }
#sty .draw .won { color: #9fe0a8; display: inline-block; animation: stySlam .5s cubic-bezier(.2,.9,.3,1.2) both .5s; }
#sty .btn { max-width: 320px; }
#sty .tap { animation: styBlink 1.2s steps(2) infinite; }
#sty .r { animation: styRise .5s ease-out both; } #sty .s { animation: stySlam .55s cubic-bezier(.2,.9,.3,1.2) both; }
@@ -1201,6 +1211,6 @@ if (location.hostname !== 'splatter-tennis.wuwi.one' && !window.Capacitor?.isNat
</div>
</div>
<span id="recLamp" hidden>REC</span>
<script type="module" src="src/main.js?v=387"></script>
<script type="module" src="src/main.js?v=388"></script>
</body>
</html>
+1
View File
@@ -940,6 +940,7 @@ function cupGameWon(zb, banked = false) {
else d.mopT = 0.6;
G.interlude = { introT: d.mopT + 1.6 };
cupDraw(); // the next round's rung -> d.opp
if (G.story && storyUI.show) storyUI.show('draw', { act: G.story.act, round: cup.round }); // the tableau, stamped
score.server = other(score.server);
G.phase = 'over'; G.overT = d.mopT + 2.2; // d.ending stays true until the walk-on tick
};
+22 -1
View File
@@ -3,7 +3,7 @@
// Module map: config (tuning/constants) · state (mutable game state) · gameplay (logic/update)
// render (camera/drawing) · input (keys/touch) · audio (sfx/umpire) · panel (T tuning UI)
import { TUNE, COURTS, TOURS, KITS, SKINS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt, LEAGUES, leagueOf, cupRoundsFor, cupFixture, cupRoundName, matchFoe, LADDER , storyFixture, storyAct , STORY, ROMAN, storyBossArt , STORY_HELP, STORE } from './config.js';
import { TUNE, COURTS, TOURS, KITS, SKINS, MODES, MENU, DIFFS, EATEN_RESTART, DONE_RESTART, WEAPONS, bossArt, LEAGUES, leagueOf, cupRoundsFor, cupFixture, cupRoundName, matchFoe, LADDER , storyFixture, storyRounds, storyAct , STORY, ROMAN, storyBossArt , STORY_HELP, STORE } from './config.js';
import { P, O, ball, score, G, setupPoint, sendBoy, selectCourt, selectTour, selectKit, selectSkin } from './state.js';
import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, startGore, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, flickTarget, dragServe, toggleTestMode } from './gameplay.js';
import { startDuel, resolveInterlude, startHorde, startBossFight, endBossFight, startDrill, endDrill, startMatch, endMatch, startBench, endBench, FALL, benchFall, startNetVs, endNetVs , startStory , storyUI } from './beats.js';
@@ -784,6 +784,25 @@ function storyScreen(kind, d) {
<div class="lab tap">TAP</div>
</div>`;
whoosh(0.3); at(0.3, () => boom(0.45)); // the court flies in, ACT n lands
} else if (kind === 'draw') {
// THE TOURNAMENT TABLEAU (storyboard 03): the cup's rounds up the league, the final the act boss's
// body. After the act card it is the checkpoint (⚑ — a loss throws you back here); between rounds
// it stamps the round just won and points at the next face.
auto = 3.2;
glow = '#1a2030';
const R = storyRounds(act), last = R.length - 1;
html = `<div class="bgc dim" style="background-image:url(${art(court.tile)})"></div><div class="col">
<div class="lab gold r">ACT ${ROMAN[act]} · THE CUP${d.round ? '' : ' · ⚑ SAVED'}</div>
<div class="big r" ${delay(1)}>${['', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE'][R.length]} ROUNDS</div>
<div class="draw">${R.map((r, i) => { const f = matchFoe(r.band[0]);
const st = i < d.round ? '<span class="won">WON</span>' : i === d.round ? '<span class="gold tap">NEXT</span>' : '';
return `<div class="r${i === last ? ' fin' : ''}${i < d.round ? ' done' : ''}" ${delay(2 + i)}>
<img src="${foeArt(f)}" alt=""><div><div class="lab${i === last ? ' gold' : ''}">${r.name}</div>
<div class="mid">${f.name}</div></div><div class="lab st">${st}</div></div>`; }).join('')}</div>
<div class="lab tap">TAP</div>
</div>`;
R.forEach((_, i) => at(0.3 + (2 + i) * 0.15, () => pock(0.4, 600 + i * 120)));
if (d.round) at(0.3 + (1 + d.round) * 0.15 + 0.3, () => boom(0.3)); // the WON stamp
} else if (kind === 'loot') {
const xd = profile.runDelta(), mid = storyBossArt(act, 'mid').name;
html = `<div class="col">
@@ -852,6 +871,8 @@ sty.addEventListener('click', e => {
if (b) styClose(b.dataset.door); else if (!styDoors) styClose('tap');
});
storyUI.show = (kind, d) => storyScreen(kind, d).then(door => {
// the act card opens onto the cup's tableau — the checkpoint the cup starts from
if (kind === 'act') return storyScreen('draw', { act: d.act, round: 0 });
if (kind !== 'clear') return;
// the act card IS the act's ending: the run closes here, and the story picks up where it now stands
restartRun(); profile.push(); refreshSplash();
+16 -1
View File
@@ -90,7 +90,7 @@ globalThis.getComputedStyle = () => ({ paddingTop: '0' });
const state = await import('./src/state.js');
const { G, P, O, score, ball, setupPoint } = state;
const { ringCue, predictLanding, solveShot, update, forceZombies, startGore, splatPoint, restartRun, leaguePace, duelSkill, tinLadder, deliverTin, peerK: _peerK, legLevel: _legLevel, winLevel: _winLevel, assistK: _assistK, flickTarget, flatCount, swayOf, keyPressed, classicGame, serveApexY, assistK, rallyFeel, barrelGap, machCap, hitServe, cheer, inCourt } = await import('./src/gameplay.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs, startStory, startStoryChapter, startHorde } = await import('./src/beats.js');
const { startDuel, resolveInterlude, startBossFight, endBossFight, startDrill: _startDrill, endDrill, startMatch: _startMatch, endMatch, FALL_LEN, startNetVs, endNetVs, startStory, startStoryChapter, startHorde, storyUI } = await import('./src/beats.js');
const { mirror, netMsg, netTest, netTick, netAgain } = await import('./src/net.js');
// the drill's first-sight modal freezes the world (that is what modals do) — dismiss it the way a
// player would, so the tests step a live court
@@ -3720,6 +3720,21 @@ test('THE STORY: the act\'s cup is won into its chapter — no promotion, the st
assert.equal(G.runGames, cfg.STORY.base(1), 'the chapter starts at its own game 1');
});
test('THE STORY: every round won shows the cup\'s tableau, stamped up to the next face', () => {
// the storyboard's screen 03 (Oliver, 2026-10-16: "das Turnier-Tableau sehe ich nie")
const shown = [], was = storyUI.show;
storyUI.show = (kind, d) => { shown.push(kind + ':' + (d.round ?? '')); return Promise.resolve(); };
try {
intoStory(1, 'cup');
winStoryCup();
} finally { storyUI.show = was; }
const R = cfg.storyRounds(1).length;
assert.deepEqual(shown.filter(k => k.startsWith('draw')), Array.from({ length: R - 1 }, (_, i) => 'draw:' + (i + 1)),
'one tableau per won round before the final (the act card opens round 0\'s)');
assert.equal(shown[0], 'act:', 'the act card comes first');
restartRun();
});
test('THE STORY: the beaten finalist grows into the act boss, runs off, and game 1 begins without a cut', () => {
intoStory(1, 'cup');
const cup = G.match.cup;