diff --git a/PROJECT.md b/PROJECT.md index 48f91d3..1eb9487 100644 --- a/PROJECT.md +++ b/PROJECT.md @@ -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_` sweeps). diff --git a/index.html b/index.html index a1cdbaa..faedf1a 100644 --- a/index.html +++ b/index.html @@ -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 - + diff --git a/src/beats.js b/src/beats.js index e135a5b..ab00947 100644 --- a/src/beats.js +++ b/src/beats.js @@ -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 }; diff --git a/src/main.js b/src/main.js index 75be2d4..4d205f3 100644 --- a/src/main.js +++ b/src/main.js @@ -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) {
TAP
`; 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 = `
+
ACT ${ROMAN[act]} · THE CUP${d.round ? '' : ' · ⚑ SAVED'}
+
${['', 'ONE', 'TWO', 'THREE', 'FOUR', 'FIVE'][R.length]} ROUNDS
+
${R.map((r, i) => { const f = matchFoe(r.band[0]); + const st = i < d.round ? 'WON' : i === d.round ? 'NEXT' : ''; + return `
+
${r.name}
+
${f.name}
${st}
`; }).join('')}
+
TAP
+
`; + 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 = `
@@ -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(); diff --git a/test.js b/test.js index 0346d76..2e00651 100644 --- a/test.js +++ b/test.js @@ -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;