diff --git a/gameplay.js b/gameplay.js index 32cf1d5..13efeee 100644 --- a/gameplay.js +++ b/gameplay.js @@ -683,26 +683,29 @@ export function deliverTin() { icon: WEAPONS[id].icon, color: UI.warm, t: TUNE.tinCardT }; } -// ---- TEST MODE. Triple-tap the scoreboard. A sandbox for trying the new content without playing -// twenty games to reach it: a steady medium wave and every barrel weapon on the rack, topped back up -// as you spend it. It does NOT stop the run — games, duels and bosses all still happen underneath; -// it only overrides the difficulty dials and keeps the rack stocked. +// ---- TEST MODE, the back room's WEAPON TEST (five taps on the splash logo — it used to be a +// triple-tap on the scoreboard). A sandbox for trying the new content without playing twenty games +// to reach it: a steady medium wave and EVERY weapon on the rack, topped back up as you spend it. +// It does NOT stop the run — games, duels and bosses all still happen underneath; it only overrides +// the difficulty dials and keeps the rack stocked. export function toggleTestMode() { G.testMode = !G.testMode; if (!G.testMode) { G.flash = { text: 'TEST MODE OFF', t: 1.4 }; return false; } if (G.mode === 'tennis') forceZombies(); // no zombies to test against in a tennis match stockTestRack(); G.flash = { text: 'TEST MODE', t: 1.6 }; - showModal({ title: 'TEST MODE', color: UI.pink, - body: ['A STEADY CROWD, MEDIUM SPEED', 'EVERY BARREL WEAPON, KEPT FULL', - '', 'TRIPLE-TAP THE SCOREBOARD AGAIN TO STOP'] }); + showModal({ title: 'WEAPON TEST', color: UI.pink, + body: ['A STEADY CROWD, MEDIUM SPEED', 'EVERY WEAPON, KEPT FULL', + '', 'IT RUNS UNTIL THE RUN DOES'] }); return true; } -// every barrel weapon on the rack at full stock. Called on entry and then kept topped up in update, -// so a burst you commit (which spends the whole tin) is available again a moment later. +// EVERY weapon on the rack at full stock — the four tins as well as the four barrel weapons, which +// is eight of the nine slots. It stocked only the barrel weapons before, on a screen called the +// weapon test. Called on entry and then kept topped up in update, so a burst you commit (which +// spends the whole tin) is available again a moment later. function stockTestRack() { - for (const id of Object.keys(BARREL_WEAPONS)) { + for (const id of Object.keys(WEAPONS)) { const have = G.rack.find(e => e.id === id); if (have) have.n = Math.max(have.n, WEAPONS[id].tin); else if (G.rack.length < RACK_MAX) G.rack.push({ id, n: WEAPONS[id].tin }); diff --git a/index.html b/index.html index 70ec242..df2f806 100644 --- a/index.html +++ b/index.html @@ -118,13 +118,15 @@ /* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame alive across every screen is the point — a screen that unmounts the whole overlay flashes the court through between menus. */ - #splash, #setup, #stats, #ranks, #newbest { display: none; } + #splash, #setup, #stats, #ranks, #newbest, #secret, #bosses { display: none; } #intro[data-stage="splash"] #splash { display: flex; flex-direction: column; flex: 1; } /* flex COLUMN, not block: it is what lets the footer's margin-top:auto reach the bottom of a screen whose content does not fill it */ #intro[data-stage="setup"] #setup, #intro[data-stage="stats"] #stats, #intro[data-stage="ranks"] #ranks, + #intro[data-stage="secret"] #secret, + #intro[data-stage="bosses"] #bosses, #intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1; animation: drop 0.3s ease-out; } /* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the @@ -180,6 +182,19 @@ #tours button.on { border-color: var(--acid); background: rgba(188,212,85,0.09); } #tours img { height: 88px; width: auto; display: block; image-rendering: pixelated; } #tours b { font: 400 11px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; } + /* THE BOSS PICKER (back room). Two columns of portraits, because you pick a boss by looking at + him — the tile is mostly art. Same plate as the tour tiles above; the grid is what makes ten + of them fit on a phone. */ + #bossList { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding-bottom: 14px; } + #bossList button { display: flex; flex-direction: column; align-items: center; gap: 5px; + padding: 8px 6px 10px; background: var(--panel); border: 2px solid var(--line); + color: inherit; font: inherit; cursor: pointer; + -webkit-tap-highlight-color: transparent; } + #bossList img { height: 78px; width: auto; display: block; image-rendering: pixelated; } + #bossList b { font: 400 9px Silkscreen, monospace; color: var(--cream); letter-spacing: 0.5px; + text-align: center; line-height: 1.35; } + #bossList i { font: 400 8px Silkscreen, monospace; color: var(--dim); font-style: normal; + letter-spacing: 1px; } /* a row, not a stacked tile: three 3:2 tiles stacked fill a phone twice over, and the art still reads at this size. -webkit-tap-highlight-color kills iOS's blue flash over the pixel art. */ #courtList button.court { display: flex; align-items: center; gap: 11px; width: 100%; @@ -343,6 +358,23 @@

BEST SINGLE RUNKILLS · GAMES

+ +
+

BACK ROOM

+
+ + +
+
+ +
+

BOSS TEST

+

PICK A CHAPTERFULL ARSENAL

+
+
@@ -359,6 +391,6 @@
- + diff --git a/main.js b/main.js index 9a51458..28176ed 100644 --- a/main.js +++ b/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, EATEN_RESTART } from './config.js'; +import { TUNE, COURTS, TOURS, EATEN_RESTART, WEAPONS, bossArt } from './config.js'; import { P, O, ball, score, G, setupPoint, selectCourt, selectTour } from './state.js'; import { update, restartRun, keyPressed, solveShot, inServeBox, inCourt, forceZombies, splatPoint, deliverTin, selectWeapon, grantWeapon, showModal, dismissModal, startDuel, resolveInterlude, flickTarget, startBossFight, endBossFight , toggleTestMode } from './gameplay.js'; import { fitViewport, render, proj, RES, SPRV, LOAD } from './render.js'; @@ -186,8 +186,55 @@ document.getElementById('nbSend').addEventListener('click', async () => { }); nbName.addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('nbSend').click(); }); document.getElementById('cStart').addEventListener('click', start); -for (const id of ['sBack', 'stBack', 'rkBack']) +for (const id of ['sBack', 'stBack', 'rkBack', 'scBack']) document.getElementById(id).addEventListener('click', () => go('splash')); +document.getElementById('bsBack').addEventListener('click', () => go('secret')); + +// ---- THE BACK ROOM: five taps on the logo, and nothing on the screen says so. It replaces the +// triple-tap on the in-play scoreboard, which sat on top of the miss-log tap and which the +// testteam kept opening by accident — a cheat you can trip mid-run is a bug. +let logoTaps = []; +document.querySelector('#splash .mid').addEventListener('pointerdown', () => { + const now = performance.now(); + logoTaps = logoTaps.filter(t => now - t < 2000); + logoTaps.push(now); + if (logoTaps.length >= 5) { logoTaps = []; go('secret'); } +}); + +// Both entries drop you into a REAL run rather than a special mode — same update loop, same +// scoring, same death — which is the only way a test tells you anything about the game. +// This is laserScene's trick: force the turn and then step the transition out in one go, because +// the cinematic is ten seconds you did not come here to watch. +function intoSplatter() { + if (G.mode === 'tennis') forceZombies(); + for (let i = 0; i < 900 && G.mode !== 'splatter'; i++) update(1 / 60); + G.modal = null; G.horde = null; G.interlude = false; G.changeover = 0; G.clearing = false; + G.mgT = 0; G.gunId = null; G.beam = null; G.hp = G.hpMax = TUNE.playerHp; + zombies.length = 0; shots.length = 0; pellets.length = 0; + score.server = 'P'; setupPoint(); +} +document.getElementById('scWeapons').addEventListener('click', () => { + start(); intoSplatter(); toggleTestMode(); +}); +document.getElementById('scBosses').addEventListener('click', () => go('bosses')); + +// The list is built by ASKING bossArt which design each chapter gets, so it can never drift from +// what the run actually spawns — including the champion sitting on chapter 10 rather than 7. +const bossList = document.getElementById('bossList'); +for (let ch = 1; ch <= 10; ch++) { + const art = bossArt(ch), b = document.createElement('button'); + b.innerHTML = `` + + `${art.name}CHAPTER ${ch} · GAME ${ch * TUNE.bossEvery}`; + b.addEventListener('click', () => { + start(); intoSplatter(); + G.runGames = ch * TUNE.bossEvery; // the roster, the HUD and the spawn ramp all read this one + G.bossDue = 0; // ...and the schedule must not queue a SECOND fight behind it + for (const id of Object.keys(WEAPONS)) grantWeapon(id, TUNE.wCap); // full arsenal, every tin + G.weapon = 0; + startBossFight(ch); + }); + bossList.appendChild(b); +} // the three period tabs are one listener on the strip — the button carries the period in data-p document.getElementById('stTabs').addEventListener('click', e => { const b = e.target.closest('button'); if (!b) return; @@ -278,27 +325,14 @@ const maybeRestart = () => { cv.addEventListener('pointerdown', maybeRestart); addEventListener('keydown', maybeRestart); -// hidden shortcut: three taps on the SCOREBOARD opens test mode (a medium wave plus every barrel -// weapon — see gameplay.toggleTestMode). The umpire triple-tap (skip to zombie mode) is REMOVED: -// the testteam found it, and a cheat a player can trip by accident is a bug. T.forceZombies() -// still exists for the console. -let boardTaps = []; +// A tap on the SCOREBOARD toggles the miss-log overlay, and that is now all it does. The triple-tap +// that opened test mode has moved to the back room on the splash (see below): a cheat you can trip +// by accident mid-run is a bug, and this one sat on the one thing you tap to read the log. cv.addEventListener('pointerdown', e => { const r = cv.getBoundingClientRect(); const x = (e.clientX - r.left) / r.width * (cv.width / RES); // logical coords const y = (e.clientY - r.top) / r.height * (cv.height / RES); - if (x >= 6 && x <= 138 && y >= 6 && y <= 36) { // the scoreboard - const now = performance.now(); - boardTaps = boardTaps.filter(t => now - t < 1600); - boardTaps.push(now); - if (boardTaps.length >= 3) { - boardTaps = []; - // taps one and two already toggled the miss-log twice, so it is back where it started — - // this third one must NOT toggle it again, or every trip through test mode leaves the - // overlay on behind you - toggleTestMode(); - } else G.logShow = !G.logShow; // a single tap is still the miss-log overlay - } + if (x >= 6 && x <= 138 && y >= 6 && y <= 36) G.logShow = !G.logShow; // the scoreboard });