⚽ Play MECH CUP 2026How to play · 玩法指南
Every pull request that shaped the game, newest work first. The project began on 2026-06-12 as The Electric Dream Cup and was rebranded to MECH CUP 2026 (the first space robot football final, on the Moon). It ships as one self-contained index.html and deploys to mechcup.com (Cloudflare Pages).
Every PR went through the same pipeline before going live: build (minify) → headless Chrome smoke-test gate → Cloudflare Pages deploy → verify (/version.json == commit). The smoke suite grew from a handful of checks to 142 assertions, one (or more) added per feature/fix so bugs can't silently return.
Each entry: #PR · merge date · merge-sha — what changed.
Note on ordering: PR #17 carries a lower number but merged after #18–#26 (it was rebased on top of them, commit
c28685b); PRs #27+ landed later still — see the top section.
CFG.replayBtn). The pause screen gains "⟲ Replay the last goal" — the most recent goal plays back through the HILITE engine on demand, mid-match, under a bilingual "⟲ LAST GOAL · 22′" banner, instead of waiting for the full-time reel. The frozen match is snapshotted (ball, spin, all ten robots' positions and headings) and restored to the exact frame when the replay ends — you resume precisely where you paused. The button appears only once a goal exists. Smoke 141 → 142 — hidden pre-goal · replays under the banner · returns to pause with transforms restored · resume works · flag-off is a hard no-op; validated to FAIL with the flag off. (One self-inflicted bug caught by the suite mid-build: an sfx.init() in the new path flipped audio on headless — the audio-off gate refused it.) Live-verified with a real click.CFG.skillMoves). Hands-on depth for the crowd the Legend tier invites. Double-tap a move key for a stepover burst — half a second of +35% acceleration where tackles slide off (odds ×0.3), on a 3-second cooldown, and only with the ball at your boots. Hold both mouse buttons (or LT+RT on a pad) while carrying to shield — planted at 0.55× pace, but challenges bite half as hard. Both moves read through the same speed and tackle chains the AI already uses — no parallel physics. Smoke 140 → 141 — double-tap arms (a single tap doesn't) · the burst is a real speed jump in the movement chain · cooldown holds · shield slows + halves tackle bite · pad LT+RT reads as shield · flag-off inert; validated to FAIL with the flag off. Live-verified on real frames: the same 400 ms window covered 1.89× the ground under a burst.CFG.rivalry). The career now carries a grudge: an elimination brands the nation that ended your run (stored by stable id, so roster changes can't mislabel it). Meet them again and the walkout reads ⚔ REVENGE MATCH / 复仇之战 and the house treats it like a final — stakes floor 0.85, so the stands pack and the pre-kick fireworks fly. Beat them and the score is settled: the result screen says so, the brand clears, and the Trophy Room gains a "⚔ Revenge wins × N" honour. Hands-on matches only — Watch mode can neither brand nor settle. Story built entirely from data the game already keeps. Smoke 139 → 140 — brand on elimination · rematch framing (tag + stakes) · settle (honour + cleared brand) · Watch-safe · stale-id normalization · flag-off inert; validated to FAIL with the flag off. Live-verified (FRA–PAR: "Round of 16 · ⚔ REVENGE MATCH" on the walkout card).CFG.legend). A fourth tier above Fever, for the veterans: faster reads (vision 28 → 34), tighter marks (tackle odds +31%), cleaner passes (skill .9 → .97), a livelier line (+7% pace, full send) — but the keeper keeps Fever's calibrated numbers: the wall is the team, not a psychic GK, so the shot-balance gates hold untouched. Legend is explicit-pick only — the cup's round-climb still tops out at Fever for every lower base — and shared links accept diff=legend (clamping to Hard if the tier is off). The menu gains a bilingual Legend / 传奇 pill; compact-landscape menus tightened so four pills still clear the fold (the responsive gate caught the wrap). Smoke 138 → 139 — tier shape · Fever-GK identity · pill picks + persists · explicit-only climb · GK gates re-run at Legend (corners score, weak shots saved) · flag-off caps at Hard; validated to FAIL with the flag off. Live-verified.CFG.goldenBoot). Your goals now build a career: every open-play goal your team scores hands-on (Watch mode can't farm it, opponent goals don't count) is credited to its scorer's shirt number in the persistent career store — number-keyed, so roster reshuffles can't mislabel it. The Trophy Room gains a Career goals stat and a 👢 Golden Boot honour line naming your top scorer ("№10 · 23 goals" / "10号 · 23 球") — shown even before your first cup, because a scorer's record is an honour too. Old saves normalize cleanly on load. Smoke 137 → 138 — credit + persistence · opponent/Watch ignored · Trophy Room line (cup-less included) · pre-boot save normalization · flag-off silent; validated to FAIL with the flag off. Live-verified ("👢 Golden Boot — №10 · 1 goal" rendered after a real goal).057f52c · 2026-07-05 — Gamepad support (CFG.gamepad). (shipped as a direct commit — no PR) Plug in a controller and play: left stick moves (pin it to the rim to sprint, same as the touch stick), A passes / holds to shoot, B plays the through ball, Y the lofted pass, X holds the finesse curler, LB switches player, LT holds the jockey stance, RB/RT sprint, Start pauses. The pad is polled and translated into the same verbs the keyboard fires — no parallel input path to drift. A bilingual "🎮 GAMEPAD READY" toast names the mapping on connect. Smoke 136 → 137 via a stubbed getGamepads pad — stick/deadzone/rim-sprint · A arms/releases the kick on button edges · watching + stick takes over · flag-off makes the pad invisible; validated to FAIL with the flag off. Live-verified (a stubbed stick ran the №10 6.9 m across real frames).CFG.crowdID). The floating ring is no longer a neutral galaxy: each nation's fans take an end — the crowd souls tint to the two flags' band colors, with neutrals on the sides — and the house fills as the cup deepens (R16 ~70% seated → the Final packed, the murmur up to +35% louder). Semi-final and Final nights open under pre-kick fireworks in both nations' colors during the countdown. Quitting to the menu (or the flag) restores the exact neutral house. Smoke 135 → 136 — data-driven end mapping · density rises R16→Final · fireworks only at stakes ≥ SF · exact neutral restore; validated to FAIL with the flag off. Live-verified (FRA–BRA Final: tinted ring + bursts on screen).AAA loop restarted at the owner's direction ("make this a top-notch AAA game — keep the loop going"); item 11 of the new queue (docs/plans/aaa-loop-queue.md).
CFG.commentary). A broadcast lower third now voices the big beats: goals with the real minute ("36′ — FRANCE strike. The arena erupts."), big saves, woodwork, counter-attacks, clutch time, the golden goal, and the shootout — each from a small bilingual variant pool so lines don't repeat verbatim. A 6-second cooldown keeps it a voice, not a log — and a goal always speaks (the hook zeroes the cooldown). The pill sits above the touch zone, never competing with the center splash, and a fresh match silences the booth. Smoke 134 → 135 — deterministic variant · cooldown holds · goal overrides it · zh pool switches with the language · flag-off is silent; validated to FAIL with the flag off. Live-verified in WebGL (the real-minute goal line on screen mid-celebration)..credit hiding prevented). The links now sit in flow after the button stack — they can never overlap it; at tight folds they slide below (auxiliary links, the scrollable panel is the backstop) while the CTAs stay above the fold (gates re-verified). The visibility assertion now also requires no overlap (links.top ≥ trophyBtn.bottom). Smoke stays 134..credit class — which the menu-fold tiers display:none at short windows and on all phones, and whose position:absolute;bottom:3.5vh made the two footer lines overlap. Caught by the post-deploy live check ("is it actually on the home page?"): the links existed in the DOM but rendered nowhere for most real users. They now have their own .infolinks slot — absolute (zero fold cost), below the credit line, visible at every size except ultra-short landscape (≤480px tall). Smoke 133 → 134 — the links must be visible (non-zero rects) at desktop AND a 390px phone; this assertion fails against the #145 markup./guide.html + /changelog.html. The site gains a bilingual playing guide — quick start, full controls (keyboard + touch), the defending/finishing verbs, shootouts + your keeper, set pieces, the three gravity worlds, the cup/career/sharing loop — as one standalone brand-styled page with an English/中文 toggle that shares the game's language preference (dreamcup-lang), so it opens in your language. And this changelog is now published: build.mjs converts CHANGELOG.md → changelog.html on every build (a small purpose-built markdown converter — paragraphs and list items are buffered whole so bold/code pairs survive line wraps), meaning the public page can never go stale. Both pages are linked from the game's menu footer (bilingual), added to the sitemap, and gated in CI: the build test now asserts the guide ships bilingual and the changelog is generated, converted, and current (5 → 7 build checks).AAA loop item 10 (docs/plans/aaa-loop-queue.md) — the roadmap's #1 priority (the game is polished but invisible) gets everything code-side; the submissions themselves are owner actions.
npm run build:zip + a dormant portal-SDK hook + the handoff doc. build:zip produces mechcup-portal.zip — the standard HTML5-game upload (minified game + self-hosted Three.js vendor/ + meta files, index.html at the zip root as itch/CrazyGames expect; ~1.3MB, gitignored). A new MONETIZE.portalSDK slot + loadPortalSDK() give CrazyGames/Poki their SDK seam: wired at boot, dormant unless configured — the game never phones a portal on its own (track() was already designed as the event seam). docs/DISTRIBUTION.md hands the owner the itch → CrazyGames → Poki steps, including the strict-portal note (flip the CDN order to vendor-first for no-external builds). Smoke 132 → 133 — the hook is dormant by default (no tag, empty config), injects exactly one tagged script when explicitly configured, and an unconfigured call is a no-op; validated to FAIL with the dormancy guard stripped. Live-proven the way a portal would run it: the game boots fully from the extracted zip (FRANCE menu, 16-team cup, hook dormant).AAA loop item 9 (docs/plans/aaa-loop-queue.md).
CFG.photo). The pause screen's free orbit camera (drag to frame, scroll to zoom) gains a 📸 Save a photo: one clean frame rendered on demand and captured to PNG — the WebGL canvas carries no HUD, so the shot is already poster-clean — then Web Share with files on mobile, a plain download elsewhere, with a "Photo saved" toast. Render + capture happen in one task (the drawing buffer isn't preserved between frames). The button is .endlink-sized (fold-safe) and hidden when the flag is off. Smoke 131 → 132 — the pause 📸 yields a real image/png blob >10KB (an actual rendered frame, not a blank) with the download side-effect stubbed; validated to FAIL with the flag off. Live-verified in WebGL: a 2MB PNG from a real paused match, toast shown, share-sheet route taken.AAA loop item 8 (docs/plans/aaa-loop-queue.md) — closing the trophy cinema's noted v1 gap.
setTeam re-kit path) for the length of the playback, and hands the slots their real kits back on exit — so a title won as France against Brazil replays in blue and gold, whatever you're wearing at the menu today. If either id no longer resolves (a future roster drop), the swap is skipped and the clip plays in current kits as before. Smoke: the cinema assertion now proves the swap (menu re-kitted to two OTHER nations → playback wears the clip's ids → exit restores), validated to FAIL with the swap stripped (131/131 held). Live-proven end-to-end through the real ▶ click: CRO/AUT at the menu → FRA/BRA during the clip → CRO/AUT after.AAA loop item 7 — the first loop-proposed item after the approved queue completed (docs/plans/aaa-loop-queue.md).
CFG.intro). The walkout was already half a broadcast open (anthem motif, matchup card, geography fact); this adds the other half. A slow camera dolly sweeps across the pitch for the length of the walkout (skipped under prefers-reduced-motion), and the card gains a lineup strip — both fives as number chips with each side's №10 star flagged ★ in gold, teaching the star system at a glance. Tap or Space skips straight to the countdown (the intro must never cost a returning player time). With the FULL TIME reel (#132), a match now opens and closes like a broadcast. Smoke 130 → 131 — lineup strip (10 chips, exactly 2 stars) · the dolly moves the camera through the walkout inside its height band · Space skips to COUNT · flag-off leaves the card bare; validated to FAIL with the flag off. Live-verified in WebGL (🇫🇷 1 4 5 8 10★ · 🇵🇾 1 4 5 8 10★ on the card, dolly mid-sweep).AAA loop item 6 — the last of the user-approved queue (docs/plans/aaa-loop-queue.md).
CFG.setPieces). Both are staged TABLEAUS inside PLAY (whistle → place → hold via settleT → release) — no new state machine, and the dream walls still keep open play flowing (no throw-ins, no midfield fouls). Corners: a defender turning the ball over his own byline concedes one — "CORNER!", the taker walks to the flag (you take your own; the AI's crosses lofted into the box after a beat, where the aerial system can meet it), runners attack the spot, markers drop in. Penalties: a committed lunge that misses in the box is a spot kick, symmetric by design — deep in its own box the AI sometimes risks a desperate lunge (+15% win bonus, penalty if it misses, ~1 a match by construction), and your own whiffed box tackle concedes one too (the spot kick replaces the stumble — the penalty IS the price). "PENALTY!" stages ball-on-spot, taker vs keeper, everyone else outside the box; the goal/save resolves through the normal engine (swept trigger, GK dive) — no special resolution code. A goal or restart voids any staged piece. One knock-on absorbed: the wall-pocket scrum can now legitimately resolve into a corner, so that gate's bound moved 6s → 9s (it's a staged self-clear now). Smoke 129 → 130 — defender-byline stages the corner (ball at the flag, you take your own, the user's strike releases it) · attacker exits stay normal bounces · the AI corner crosses lofted · both penalty directions stage on pinned randoms (ball on the spot) · flag-off keeps plain bounces; validated to FAIL with the flag off. Live-verified in WebGL (the CORNER! tableau with №10 at the arc).AAA loop item 5 (docs/plans/aaa-loop-queue.md).
CFG.momentum). Fall 2+ goals behind and the crowd rallies — "🔥 THE CROWD RALLIES BEHIND YOU" — while the opponent eases: challenge odds ×0.85, decisions 20% slower. Go 2+ up and "😤 THEY DIG IN": ×1.15 / 20% quicker. Deliberately honest and bounded: a step at ±2 (no hidden gradient), announced once per swing, consumed at exactly two opponent-only sites (aiTackle odds · aiDwell tempo — the same consume-a-scalar pattern as knockout caginess), the GK untouched (balance gates stay green), and zeroed in Watch mode (no human side to tilt toward). Smoke 128 → 129 — score-step + swing-announcement + consumer mapping + a deterministic integration proof (a random pinned between the eased 0.17 and base 0.20 thresholds flips a real aiTackle outcome) + demo neutrality; validated to FAIL with the flag off. Live-verified in WebGL (0–2 down → the rally callout with ×0.85/×1.2 live).AAA loop item 4 (docs/plans/aaa-loop-queue.md).
CFG.userKeeper). On the opponent's shootout kicks you're no longer a spectator: a "🧤 YOU'RE IN GOAL — aim, SPACE dives" sting opens the kick, your keeper pre-leans live with your aim (mouse/stick, screen-mapped to the keeper's left/right), and Space/click COMMITS a full-reach dive (reach ≈ the Fever AI's 2.6) — commit before the strike and it's a real guess, exactly like penalties. Stay passive and all you get is a late central reflex stab (reach 1.2, tt<0.3) — corners beat a keeper who doesn't play. Your own kicks, Watch mode, and the flag-off path keep today's behavior; the dive slow-mo only plays on mid-flight commits (pre-kick guesses stay real-time). Found & fixed a real lifecycle bug the feature exposed: _soKeeperCommitted only reset when a kick fired, so it sat stale-true through every wind-up — pre-kick reads were impossible; it now resets per soSetup. Smoke 127 → 128 — pre-lean · committed-dive (full reach, no charge) · passive-reflex (late + short) · own-kick-charges · flag-off-inert; validated to FAIL with the flag off. Live-verified in WebGL (hint splash, lean −0.94 → committed dive −2.21 with the ball mid-flight, dive slow-mo engaged).AAA loop item 3 (docs/plans/aaa-loop-queue.md).
SFX.MOTIFS). All 16 live R16 qualifiers now carry a hand-composed signature theme — original 8-note micro-motifs with a regional accent (FRA lydian elegance · BRA syncopated pentatonic · ARG clipped tango minor · ESP phrygian flourish · EGY hijaz color · SUI alphorn leaps · NOR northern-morning minor · MEX paired thirds · POR fado lift · …), never a quote of a real anthem (licensing + taste). Same contract as the generator (tonic start, octave resolve), so the synth voices and the goal-reprise slice work unchanged; every non-override id (and any future roster) still gets its deterministic hash motif — the table is pure data + a two-line lookup in motifNotes. Smoke: the motif assertion now proves all 16 overrides are sane, mutually distinct, and table-sourced (FRA's exact note row), with the hash fallback exercised via an eliminated nation; validated to FAIL with the lookup stripped (127/127 held). Live: the walkout scheduled the composed FRA theme frequency-for-frequency (16/16 against an independent recompute).AAA loop item 2 (docs/plans/aaa-loop-queue.md).
CFG.weather). Each Gravity Lab world now has ambient presence: Mars drifts warm dust with a periodic gust front sweeping the pitch (+ a soft whoosh); Earth plays in a gentle drizzle — and on a wet pitch: ground friction ×0.72, so rollers run ~30% longer (the one gameplay touch, read live by the ball from game._wet); the Moon gets slow-rising dust motes in the low-g stillness. All of it rides the existing pooled ParticleSystem (a few particles per tick — the pool's idle-skip keeps it ~free) and is skipped entirely under prefers-reduced-motion. GK-balance gates untouched (they run on the default Moon; the wet flag is Earth-only). Smoke 126 → 127 — wet-roll distance (Earth > dry ×1.12), _wet wiring per world, per-world particles in the air via the isolated weather path, and flag-off stillness; validated to FAIL with the flag off. Live-verified in WebGL on Mars (dust + a forced gust, 46 live particles) and Earth (drizzle, 75 live, wet:true).First feature out of the AAA improvement loop (docs/plans/aaa-loop-queue.md — idea → code → test → merge → live-verify, repeat).
CFG.cinema). Winning a cup now banks the title-winning goal with the trophy: the last clip YOUR side scored in the final — the 30Hz sample recording (~18KB), not video — is stored in localStorage (capped at the 8 newest championships) keyed to the trophy's new unique k. In the Trophy Room, rows with footage grow a gold ▶: it replays the goal through the HILITE playback engine over the live arena — "🏆 TROPHY CINEMA · 🇫🇷 v 🇧🇷 · 1′" banner, tap/Space to finish — and returns to the menu. A pens-only final with no user goal simply banks nothing (no broken button). v1 plays with current kits (slot re-skin noted as a follow-up); the caption carries the real teams by stable id, so a roster reorder can't mislabel it. Smoke 125 → 126 — a full-lifecycle assertion (championship banks the clip · the ▶ renders · the real click plays it over the menu with sample-driven motion · skip returns to the MENU · storage caps at 8), validated to FAIL with the flag off. Live-verified in WebGL (the ▶ click path, the banner, the goal replaying in period kits) — which also caught a splash-lingering parity gap with the FT reel, fixed (playCinema clears any in-flight message).Verifying #133 on the live site surfaced a real failure mode: attaching an audio track from a suspended AudioContext (no user gesture yet, backgrounded tab, portal embeds) starves MediaRecorder's muxer — the recorder finishes with zero chunks and the entire highlights clip is silently lost, not just its sound.
_clipRecStart now checks sfx.c.state==='running' before wiring the synth bus into the stream — a silent clip beats no clip. The full matrix was proven in a real browser against the built game: running context → 569KB vp9,opus (sound in the file) · suspended context, fixed → 490KB vp9 video-only that decodes (1980×1193) · suspended context, old code → no clip (the bug) · fully hidden window → no painted frames, correctly discarded by the ≥1.5s gate (the tab-switch edge was already graceful). The smoke's share-clip assertion now also checks that a suspended context contributes no track (conditional — the Playwright context may start running). Found because the verification decoded the blob in a <video> element instead of trusting its size.Round four of the AAA push, stacked on the #132 reel. Virality + audio identity.
CFG.shareClip) · nation motifs (CFG.motifs). Share clip — while the FULL TIME reel plays, MediaRecorder captures the canvas stream (+ the WebAudio synth bus, so the clip has crowd sound) into a webm/mp4 video; the result screen then offers a compact "🎬 Save the highlights clip" link (Web Share with files on mobile share sheets, else a download). Feature-detected end to end — no MediaRecorder/captureStream, no button; an instant skip (<1.5s) records nothing worth sharing and correctly discards. Live-proofed: a real 2.4s reel produced a 582KB VP9 webm and the button lit up. The button is .endlink-sized so the result screen's fold budget is untouched. Nation motifs — every nation id derives a deterministic signature leitmotif (mode · contour · rhythm from an FNV hash; always tonic→octave, so it stays triumphant): the walkout anthem sting now plays YOUR nation's motif, and when a nation scores, ITS motif reprises bright under the roar — "that's their tune". Pure function (sfx.motifNotes) so it's headless-testable without an audio context; the old arpeggio remains the flag-off fallback. Smoke 123 → 125 — a recorder-lifecycle assertion (recording during HILITE · stopped after · button only with a clip · resets per match; validated to FAIL flag-off) and a motif assertion (deterministic per id · distinct across GER/BRA/FRA · sane tonic→octave shape).Round three of the AAA push. Audio identity was already deep (anthem sting, terrace chant, roar, groan, excitement-driven crowd bed), so this round ships the other broadcast staple: match highlights.
CFG.highlights). During play a rolling ring records the whole stage at 30Hz — ball (x,y,z) + all 10 robots (x,z,yaw), 33 floats/sample, a 6s window. Every goal freezes the last ~4.5s into a clip (capped at 5). At FULL TIME the reel plays back before the result screen: a gold ⚽ MATCH HIGHLIGHTS banner (clip counter when >1), a low side-on broadcast camera glued to the recorded ball, robots animated straight from the samples (their velocity feeds the existing leg-swing rig — no physics, no goal-trigger re-fires), 0.9× broadcast pace, a flash + crowd swell as each strike lands. Tap / Space / Enter skips a clip; a goalless match (or prefers-reduced-motion) goes straight to the result as before. Two edges caught in live QA and locked by the assertion: a last-second goal's splash could linger over the reel (cleared on reel start), and the banner reuses the in-match GOAL-replay node — its text is handed back on reel end so the next match's replay label can't say "HIGHLIGHTS". Smoke 122 → 123 — a full lifecycle assertion (ring fills in play · a goal freezes a clip · FULL TIME routes into the reel with the result screen held back · samples drive the ball with physics off · skip reveals the result + restores the label · a rematch resets the reel), validated to FAIL with the flag off. Live-verified in WebGL (banner + broadcast camera + sample-driven robots + skip tag on screen; a saved-shot goalless match goes straight to the result).Round two of the AAA push — after the human-feel verbs (#130), the remaining "left for later" items that read on screen: the tackle's body language, per-team identity, and last-minutes drama.
defTools) — the tackle verb finally LOOKS like one: a 0.42s slide pose (sit back, both legs thrust at the ball, arms trailing) on every committed challenge — the human's attempt AND the AI's won aiTackle — plus turf-dust particles. The lean pivots about the body centre (the #126 bicycle-kick lesson — live QA caught the first cut sinking the torso under the pitch, exactly the feet-origin mistake again; the smoke assertion now locks the pivot geometry). Stars (CFG.stars) — each team's №10/ST is its star: +7% pace (+5% under user control), strikes carry +6% power (applied before the shot gate, so a star's borderline effort still counts as a shot), and a gold boot-trail marks the danger man at full sprint. Symmetric — the opponent's star is a real matchup, and yours is worth protecting. GK-balance gate verified green with the boost live. Clutch (CFG.clutch) — the last 20s of a LEVEL (≤1 goal) regulation match fire ONE "FINAL MOMENTS!" sting: the crowd swells and the clock pulses gold (the global reduced-motion rule kills the pulse for prefers-reduced-motion). Golden goal + the shootout keep their own existing drama; a 2+ goal blowout stays calm. Smoke 121 → 122 — a slide/stars/clutch assertion (pose plays with the centre-pivot z-shift + self-recovers · both №10s star + the same player's identical strike measurably faster · clutch fires once, re-arms per match, respects the blowout gap), validated to FAIL with the flags/pose off. Live-verified in WebGL (the fixed pose sits ABOVE the turf; the gold clutch clock renders).Every prior gameplay phase upgraded the AI/team half (teamAI passing, counters, aerial, knockout caginess) — the HUMAN's verbs hadn't grown since the through-ball. Biggest gap: the player literally had no way to win the ball (only the AI's aiTackle could dispossess a carrier). This ships the planned PES P2/P3.
CFG.defTools) — Space near an opposing carrier = a committed TACKLE lunge (win ≈ tackleUser, the same loose-ball pop + hit-stop beat as the AI's tackle; whiff = a stumble — risk buys the reward; a cooldown swallows repeat presses so a duel can't accidentally start a shot charge; touch ⚡ inherits the context, out-of-range Space still charges as always). Hold right-mouse = JOCKEY (contain stance: ×0.62 speed, tackles bite +jockeyBonus; right-button-up can't fire a mid-charge shot). Finish (CFG.finesse) — hold E = the finesse curler: power capped at 0.78, aim bent toward the corner the keeper ISN'T, and a big Magnus bend does the rest (spin sign locked by a fly-the-ball smoke assertion); a light shot at a RUSHED keeper auto-DINKS over him (no new key — the natural answer to the sweeper-keeper), and the AI makes the same read in Watch mode. Weight (CFG.ballFeel) — sprint turns carry momentum (the run direction blends over ~⅓s instead of snapping; walking stays instant close control) and overcharging a shot (>1.35s) snatches at it (power bleeds, aim skews) so release timing matters. Bilingual PES legend rows for all three verbs + TACKLE!/Dinked!/Curler! action flashes. Smoke 119 → 121 (a defend/feel assertion + a finishing assertion incl. the curl-direction fly-test; both validated to FAIL with the flags off; the GK-balance/teamAI/counters gates all stay green). Live-verified in a WebGL browser (tackle clears the carrier + flashes, the curler bends to the far corner, the dink lofts at vy≈11, both legends). Fixes folded: two latent smoke-harness leaks (a stubbed input.axis and a stuck touch-joystick state bled into later tests). Behind an adversarial diff review (releaseKick/pass-path/SHOOT isolation/spin math all confirmed clean; two real finds fixed: dinks were invisible to shot/save stats — kicked below the 0.45 shot gate, now 0.5 — and the whiff-stumble was dodgeable via Q-switch — it now lives on the Player and decays in Player.update; + a frame-rate-independent momentum blend and a jockey-unstick on window blur). Design: docs/plans/2026-06-15-pes-controls-design.md (P2/P3 now marked shipped).The Round of 32 was played (2026-07-04), so the cup advances to the real Round of 16: the 16 winners — including the upsets (Paraguay over Germany, Canada over South Africa, Norway over Ivory Coast, Egypt over Australia) — with Germany, the old default team, eliminated.
TEAMS reordered so slots 0–15 are the 16 R32 winners in real bracket-slot order; CUP_SIZE 32 → 16 and CUP_SEED16 becomes the identity (replacing the old thematic seeding), so drawCup's pairs are the actual R16 ties: PAR–FRA · CAN–MAR · POR–ESP · USA–BEL · BRA–NOR · MEX–ENG · ARG–EGY · SUI–COL. The 16 eliminated sides stay in TEAMS (slots 16–31) for geography facts + career/trophy history but sit outside the cup — unpickable. New default team: FRANCE (a new DEFAULT_TEAM const drives every fallback; resolveTeamPref now also rejects a saved pick that got knocked out — a persisted GER resolves but is outside CUP_SIZE → default, no picker/draw desync). Static placeholders + the cupFormat line follow (16-team · win 4). ROSTER_SIG changes with the reorder, so stale cups are rejected and careers re-anchor by id automatically (the #119 machinery doing its job on its first real roster change). Live-verified in a WebGL browser: menu FRANCE vs PARAGUAY · Round of 16, picker = 16 flags (no Germany), the full tree renders the exact 8-tie sheet above, zh CTA 出战16强赛. Smoke stays 119 — five 32-team assertions retargeted to the R16 world (cup size/rounds, roster + identity pairs + unpickable losers + default-France, bracket path QF-NEXT, pref fallbacks incl. the new knocked-out case, trophy badge ·N/16); the identity-seeding and default-France checks validated to FAIL against the old values.Three low/nit findings from the deep review, grouped as the "hardcoded strings & year coupling" theme.
MECH CUP 2026 title with a 机甲杯 · Chinese subtitle for all players → an English share mixed scripts. The title is now localized (机甲杯 2026 in zh, MECH CUP 2026 in en — both build-stamped) and the subtitle carries only the world + gravity, so the brand appears once, in the reader's language. (#9) The smoke copy-check asserted a literal MECH CUP 2026; a documented CUP_YEAR bump would have red-gated a correct build. Now MECH CUP \d{4}. (#11) build.mjs guarded only the English brand before the year-stamp replace; added a symmetric 机甲杯 \d{4} guard so a dropped zh brand can't silently no-op the zh year stamp. Smoke → +1 — a canvas-fillText-spy assertion (English card all-Latin, no 机甲杯; Chinese card 机甲杯 title), validated to FAIL against the old hardcoded brand.Three medium findings from the deep review, grouped as the "renders/computes wrong" cluster.
p.x (the clamp fires when a fast ball over-travels the net plane in one frame) while interpolating z/y on the full step — a mismatched fraction that pushed the computed crossing z outside the post, ghost-saving a real near-post shot at low FPS (the exact class the swept trigger exists to prevent). Now the raw stepped position is captured before any ground/wall/net clamp and the trigger interpolates x and z/y on that one consistent segment. (#4) During a penalty shootout the regulation scoreboard #hud (score + comp tag, top-center) was never hidden, so it bled through the shootout tally (same anchor); startShootout now hides it (it stays hidden through the result screen; the next startMatch re-shows it). (#5) At 320px (iPhone SE) the setup rows (label + 3 pills) overflowed one flex line and clipped with no h-scroll; a @media (max-width:340px) tier wraps them and tightens the pills (the 320px-portrait Start CTA then relies on the panel's overflow-y:auto — documented). Smoke → +3: a 20-FPS diagonal-shot-scores assertion, a shootout-#hud-hidden assertion, and a 320px no-clip/no-overflow assertion; all three validated to FAIL without their fix.The deep review found the global keydown handler called e.preventDefault() on Space / Tab / arrows in every state, before any state check — so a keyboard-only user could never Tab off <body> to a control, nor Space-activate a focused button, on the menu, bracket, nation picker, Trophy Room, pause overlay, or end screen. The entire DOM UI was keyboard-dead (mouse/touch were unaffected, which is why it went unnoticed).
{PLAY,COUNT,WALKOUT,GOAL,SHOOT} set togglePause engages from. Outside it (menu · pause overlay · end screen) those keys keep their native behavior, so Tab traverses and Space/Enter activate the DOM buttons. Pause (P/Esc) and mute (M) stay live in every state, so a match is still pausable and resumable from its own overlay. The shootout keeps its Space kick (SHOOT is in the in-play set). Smoke 100 → 101 — a synthetic-keydown assertion (defaultPrevented false for Tab/Space/arrows on the menu, true in-match), validated to FAIL with the old unconditional preventDefault.A deep multi-lens review found the highest-leverage latent bug: cup, career, and team-pick state were all keyed by raw TEAMS index with no fingerprint. The #117 roster reorder therefore silently relabeled returning players' state — a Korea pick became South Africa, a resumed cup rendered a scrambled bracket, and the Trophy Room lit the wrong flags — because every stale index still passed validCup's range check.
ROSTER_SIG (FNV-1a hash of TEAMS.map(t=>t.id)) is stamped into every drawn cup and career record. validCup now requires a matching sig — a stale or pre-fingerprint save is rejected and startCup draws fresh, instead of resuming into a mislabeled bracket. The team pick persists by stable id (dreamcup-team stores "GER", not 5), so a reorder or a dropped nation resolves to the right team or cleanly defaults (resolveTeamPref; a legacy numeric index is adopted when in range so the id-migration doesn't gratuitously reset a returning player's pick — a regression-review fix). loadCareer re-anchors trophies by id on a detected roster change (dropping the unmappable, rebuilding the per-nation map, keeping index-independent totals — cups/streak/worlds); a career predating the fingerprint adopts the current roster as its baseline. Also (#8 from the review) validCup now rejects non-numeric results, closing a tampered-save → raw-string-into-innerHTML path (self-XSS, defense-in-depth). Smoke 100 → 101 — a persistence assertion (stale/absent sig rejected · non-numeric results rejected · team pick id-keyed · career trophies re-anchored by id, aggregates kept), validated to FAIL without the sig check.The 32-team bracket already borrowed almost everything portable from the published real-tournament bracket (the round tree + connectors, score/pens cards, progressive TBD reveal, a gold path highlight). The one real gap — flagged by a Codex note and the project's own live review — was the phone: a 16-tie Round-of-32 column is dense even zoomed-to-fit. This is the deferred "v2 your-path view".
max-width:760px) and Full Bracket on desktop. A NEXT marker now flags your immediate match in both views — a gold corner badge on the full tree (an ::after, so the 16-tie column height is unchanged) and a gold card + NEXT badge in My Path — so your next game pops out of a wall of nations. The path view honours the same progressive-reveal ceiling as the full tree (ceiling=(done&&!champ)?played:played+1) — unreached rounds stay TBD, never a fabricated foe. No engine/data changes — pure presentation over the existing cup model; cupPathHTML + a showBracket→_renderBracket refactor. Live-verified in a WebGL browser on the real roster (GER 2–1 PAR won → R16 NEXT GER vs FRA → QF/SF/Final/Champion TBD; the full tree's one gold NEXT tie; mobile 390px with no overflow). Smoke 100 → 102: a toggle/NEXT/reveal-ceiling assertion (path stacks 6 rounds, marks exactly one NEXT tie in each view — validated to FAIL when the NEXT flag is neutralized) and a 390px phone-overflow guard. Explicitly NOT done (decided with the user): a center-converging two-halves layout (denser + worse on mobile), per-match venues (the whole cup is on one world), a neon-green path (gold ties to the trophy). Behind a 2-lens review (correctness/i18n · CSS/responsive/test): both ship. Design: docs/plans/2026-06-28-bracket-path-view-design.md. A follow-up deep review then caught two defects in this PR's own new code, fixed here: the My Path "vs" separator was a hardcoded English literal (now T('vsLabel') → zh 对阵), and the My Path | Full Bracket tabs never set aria-selected (now mirrored to the visual .sel in _renderBracket, seeded in the static markup + aria-controls). Smoke 102 → 103.The group stage locked (2026-06-28), so the provisional 32 became the actual 2026 tournament Round-of-32 field. Per a pure-authenticity call, China and Korea are out (they didn't qualify); the default team is now Germany, the top-left seed.
TEAMS is now stored in real bracket-slot order, so CUP_SEED32 is the identity [0..31] and drawCup's pairs are the actual R32 ties: GER–PAR · FRA–SWE · RSA–CAN · NED–MAR · POR–CRO · ESP–AUT · USA–BIH · BEL–SEN · BRA–JPN · CIV–NOR · MEX–ECU · ENG–COD · ARG–Cape Verde · AUS–EGY · SUI–ALG · COL–GHA. Live-verified in a WebGL browser (default GER vs PAR; the picker shows the 32 real flags + zh names; the bracket renders the real tie sheet). No engine code touched — exactly the data swap the design doc planned. Also: the walkout "Did you know?" card now names the country (Did you know? PARAGUAY — …) so it's clear which of the two flags on screen the fun fact is about (en + zh); and stale China references were cleaned up (the dead flagTexture China branch, the HUD + default-card placeholders → Germany, unused USA/JPN bands). Smoke 97 → 99: a "Roster = real 2026 Round of 32" assertion (full 16-pair identity bracket + China/Korea/Italy absent + default Germany) and a walkout-fact-names-the-country assertion — both validated to FAIL pre-change. Behind a 2-lens review (content accuracy · reorder/engine): content zero defects (all capitals · endonyms · zh · facts · kits web-verified), engine ship (no index-hardcoding broke; saves stay structurally valid).A self-driven live end-to-end review (build 061bf03) found the game polished and bug-free across the menu, picker, a full match to a golden-goal result, share, bracket, Daily Challenge, Trophy Room, accessibility, and en/zh parity — with one real usability issue: on a 1280×720 browser window (~633px usable after chrome) and landscape phones, the retention CTAs (Watch · Daily Challenge · Trophy Room) sat below the fold — only the gold START was visible.
@media (max-height:700px) tier drops the two most expendable menu lines (the gravity "did you know" fact + the controls legend — both covered by the in-field hint and the visible YOU card) and tightens the vertical rhythm, so the full START · Watch · Daily · Trophy stack clears a ~630px fold without scrolling — live-verified at a real 1280×720 window (all four visible, menuScrolls:false, brand intact). The shared .bigBtn shrink is safe: the result screen is protected by #endScreen .bigBtn specificity (always co-active since 700≤840) and pause is content-light (confirmed by a focused diff review). Smoke 97 → 98 — a realistic-window check at 1280×640 (the existing 720 check uses a true viewport with no browser chrome, which is exactly why the live below-the-fold CTAs slipped the gate); validated to FAIL without the fix. Follow-up noted: the champion result screen has the same realistic-height fold (its last action drops below 640) — a separate, pre-existing layout pass.An 8.0/10 review on the 32-team build. Most of its findings verified as artifacts (hidden controls are inert from load via the panel observer; the landscape clip is covered by a passing fits test) — but two were real, one of them twice-flagged.
togglePause was the only writer of #pauseBtn's aria, so quitting, restarting, or advancing to the next round left a stale "Resume match" / aria-pressed=true into the menu or the next match. Extracted _syncPauseBtn() (keyed off the overlay's visibility, robust to call timing) and called it from resetToMenu + startMatch, which also now hide a lingering pause overlay. (real, a 32-team regression) the nation picker's Close is reachable again — 32 nations made it 7 rows tall and pushed Close below the fold at 1280×720; the grid now caps at 56vh and scrolls internally so the title + Close stay put (live-verified). Smoke 93 → 95 (pause aria + overlay clear after quit AND restart — validated to FAIL on the pre-fix restart path; + picker Close in-viewport at 1280×720). Copy nits (PLAY ROUND OF 32, 开始32强赛, BACK TO SETUP) stay parked, and a proposed 🏆 Trophy icon was dropped — the diff review caught that the button already renders a mech-cup SVG (PR #111), so an emoji would double it. The Continue-vs-Change-Setup redesign is tracked separately.Expands the cup from 16 teams (R16 → Final, 4 rounds) to 32 teams (Round of 32 → Final, 5 rounds), themed on the real 2026 knockout stage (48 → a 32-team R32). The engine was already parameterized (CUP_ROUNDS = log2(CUP_SIZE)), so this is mostly content + a responsive bracket. The roster is provisional — a plausible 32 now, swapped for the real qualifiers when the group stage locks (≈ 2026-06-28); the swap procedure (edit three marked blocks, no engine changes) is in the design note.
CUP_SIZE 16 → 32, CUP_ROUNDS 5; roundName gains Round of 32 / 32强 (the CTA renders 出战32强赛 via the #102 强-rule); cupFormat auto-reads "win 5 matches"; a swappable CUP_SEED32 fixes the R32 pairings. Responsive bracket (.b32): 6 columns, the 16-tie R32 column caps at 62vh and scrolls internally so the caption + Close never overlap — live-verified in a WebGL browser at desktop + the path highlight intact. Saved 16-team cups migrate cleanly (rejected by validCup → fresh draw). Behind a 3-lens review (content accuracy · engine regressions · bracket/tests), all ship: capitals/endonyms/zh/facts fact-checked (Mexico's chocolate line softened for precision; NZ flag gets its Union-flag note). Smoke 82 → 83 (a 32-team assertion that also guards the swap from silently downsizing the cup if a nation is dropped). Design + swap steps: docs/plans/2026-06-25-32-team-cup-design.md.The primary "begin this match" button read Enter the Round of 16 / 进入16强, which can be misread as qualifying for the stage (especially zh 进入16强 = "reached the last 16," a result you achieved) rather than playing it now. Switched to an unambiguous action verb, with a native-Chinese review pass.
enterRound → en ✦ Start the ${r} ✦; zh ✦ 出战${r}${r 以"强"结尾→补"赛"} ✦, rendering 出战16强赛 / 出战四分之一决赛 / 出战半决赛 / 出战决赛 — all parallel, all ending in 赛. The 赛 is appended only on the CTA for 强-ending rounds; the round name itself stays 16强 in bracket headers / scoreline / result labels (a stage name vs a verb+match object — the correct linguistic split, confirmed by the zh-native review). One change fixes the menu start button and the end-screen "next round" button (shared enterRound), plus the static/fallback … the Knockout. Smoke 81 → 82: a new assertion drives the live #startBtn through a real en↔zh language switch and asserts Start the Round of 16/出战16强赛 (no Enter/进入) — validated to FAIL on the pre-change wording; also retired a stale fallback sentinel left toothless by the rename.A tenth Codex review (8.5/10) on build 43221d9 — which confirmed #100's Change-Setup fix held. Its one High finding (third review in a row to flag it): tapping Share copied a bare setup link (?team&world&diff), so a paste shared "play this scenario," not "I won 1–0." The result lived only in the downloaded card image, which isn't what lands in a paste. Earlier rounds under-weighted this as an automation artifact; this round fixed the payload. Shipped behind a 4-lens diff review (all ship) that upgraded the share text to name the outcome and broadened the test across result shapes.
_shareText(R) builds one line — CHN 1–0 USA · Won the Round of 16 · 🌙 Moon (Out·/CHAMPION 🏆 and a (5–4 pens) clause when relevant, via the existing _resultLabel) — now used for both the clipboard and the native-share text, with the playable ?team= URL appended so a paste both brags and links. Previously the clipboard got only challengeURL(). Copy: Share result card… → Share result; "… link copied" → "… result copied"; Opening share card… → Sharing result… (en + zh). The native-share image/text/url payload is unchanged in shape (it already carried the card image). Smoke 80 → 81 — one assertion spies the clipboard across win / loss / shootout / champion and asserts scoreline + outcome word + a playable link; validated to FAIL on the pre-fix bare URL (and on a regression that drops the outcome word).A ninth Codex review (8.4/10) on the live build 20e51a7. One real HIGH bug (verified against the code) plus a cheap clarity win; the share-flow flag was confirmed an automation artifact and dropped. Shipped behind an adversarial 4-lens review of the diff, which caught — and forced the rewrite of — a vacuous first draft of the new test.
Change Setup / Quit now re-sync the menu. resetToMenu() never re-ran syncTeamSel(), so after a win the matchup card + start button stayed frozen on the round you just played while the saved cup had already advanced — the menu disagreed with what a reload restored. It now repaints (syncTeamSel();syncChallengeUI();), so the card shows the round you'll enter NEXT, matching the persisted run (an active daily challenge is preserved across the repaint, not cleared). (low) a muted "Rival results are simulated." caption (en + zh) under the bracket, shown only once you've played a round, so the known AI scorelines don't read as a pre-scripted outcome. The share-flow "promised a card, copied a link" finding was an automation artifact — the native-share toast already fires only on the genuine OS-sheet path — so no change there. Smoke 78 → 80 (the Change-Setup re-sync assertion was validated to FAIL on the pre-fix build before being accepted, after the diff review flagged a first draft that passed vacuously).From the live QA review's "make it better" follow-up — the two cheapest, universal, client-side wins. Investigation found the game was already juicy (goal slow-mo/shake/flash/roar, shot FOV-kick, crowd bed) and already had a 3-step first-match tutorial + a first-match opponent slowdown, so these ENHANCE rather than rebuild.
ball.pos.y>1.1 && vel.lengthSq()>90, reuses sparkles.spawn), leaning into the low-gravity hang; plus a crowd swell on a shot at goal (bumps the existing sfx.excite). (onboarding) a gentler first kickoff — opponents ease to 70% for the opening 5s of match #1 (then the existing 85%) via the updateAI speed multiplier, so a newcomer isn't scored on while finding their striker (the opening prompt was already the tut0/1/2 tutorial). No physics touched; smoke 78/78 (GK-balance + ball-moves green). Design note: docs/plans/2026-06-20-juice-onboarding-design.md.A Codex-review round plus polish and a CI guard landed between #93 and the game-feel work above but weren't logged at the time; reconstructed here from the PRs. Together they took the smoke suite 73 → 78.
main don't race the Cloudflare Pages deploy.langFromNav (zh* any region/script → Chinese, everything else incl. empty/null → English; the PRIMARY navigator.language only) used by the LANG init and exposed on DreamCupTest; a smoke assertion locks the full mapping + a fresh en-US boot → English + the boot landing on a valid I18N key, so a silent refactor of the detector now trips the gate. Design + rationale (primary-only signal, a remembered toggle override that wins, validity guard) captured as a reusable pattern in docs/language-detection.md. No behavior change — the detection logic is unchanged, just made testable. Smoke 72 → 73.A self-driven end-to-end live review of build 8b149e7 across five lenses (QA · gameplay · UI · localization · marketing), then an adversarial multi-agent pass over the fix diff before shipping. Most of the build verified sound — low-gravity ball physics (Moon apex 2.31 m / 0.87 s hang vs Earth 1.25 m / 0.40 s), fair GK balance (corners score, central saved), progressive bracket reveal, en/zh parity, the watched-shootout, and the 1200×630 share card all checked out. The fixes below close the first-impression and reach gaps the review surfaced. (Several plausible "bugs" were ruled out as harness artifacts before reaching the report — a shootout "hang", a share card "NaN", a validCup scare.)
-2.5vh, viewport-relative so it struck through the cards + "YOU · №10" at every width, worst on mobile) is neutralised to 0; the 1280×720 button-fold still fits (11px headroom). (high) a <link rel="canonical"> → mechcup.com so the four mirror domains stop splitting duplicate-content signals. (med) Daily Challenge applies in place — no full page reload / "booting the lunar arena" splash; it sets the seed, syncs the pills/cards/banner, and history.replaceStates the shareable ?daily= URL (changing a setting still opts out + strips it). (med) real robots.txt + sitemap.xml (copied into the build) so those paths stop soft-404ing to the SPA and crawlers get the canonical URL. (low) case-insensitive endonym dedup — "ARGENTINA (Argentina)" → "ARGENTINA" (also France/England/Portugal), while "CHINA (中国)" / "USA (United States)" keep the informative endonym. (low) a denser scrim (.92) scoped to the overlay modals (bracket/trophy/nation-picker) so the busy menu doesn't bleed through where backdrop-filter is unsupported — the base menu/pause/end panels keep the lighter .62 atmosphere. Copy: daily hint "…gold button to start" → "…gold button below to start". Smoke 68 → 71 (helper-overlap geometry, in-place daily no-reload, endonym dedup).An eighth Codex review (7.5/10) on build a4ea098. Two real bugs + one hardening + one layout; the recurring trio (pause logic, hidden-controls focusable, landscape "no scroll") stays declined as verified artifacts of the reviewer's throttled/automation session.
canFiles branch now also copies the link so the clipboard is never empty if the native sheet doesn't surface; clearChallenge() strips ?daily=/?team= from the URL so changing a setting + reloading no longer snaps the daily scenario back. (hardening) #pauseOverlay hides instantly on resume (no fade) — retires the recurring "stuck overlay" artifact + crisper resume. (layout) a compact landscape: a #setupRows wrapper (display:contents → 2-up grid at ≤520px) + tighter CTAs + dropped world-fact line so the full menu stack fits 844×390 without scrolling. Copy: "press the gold button to play" → "…to start". Smoke 67 → 68.A seventh Codex review (7.2/10) on build ff90591. Each claim verified live: 3 real bugs (2 reproduced) + a share-robustness gap + a stale toast; the pause-toggle, hidden-controls, and landscape-scroll claims were re-confirmed non-issues (throttled/automation session).
ceiling = (done&&!champ) ? played : played+1).c.champ, which left the Final-loss case showing a decided Final (your loss) over a blank Champion. The Champion now shows iff the Final was played (champId = (played===ROUNDS-1) ? level[0] : null) and is its real winner — you if you lifted it, the side that beat you if you lost it; still TBD if you went out before the final.share() hangs) and only claims "link copied" once the clipboard write resolves. (low) the stale "SWITCHED" action toast is cleared on match-end + quit. Smoke 65 → 67.A sixth Codex review (7.0/10) on build debd150, run in a heavily-throttled session with un-clearable stale state (which manufactured several symptoms). Each claim was verified live: 2 real bugs (1 critical) + 1 real share gap + 2 polish; the pause-toggle and hidden-controls claims were re-confirmed non-issues.
_clearGoalFx() (called from startMatch + resetToMenu, reused at celebration end) tears it all down. (real) the in-match HUD scorebug + corner camera/pause/mute controls now retire on the result screen (they sat behind the translucent panel, showing through + staying focusable); a successful Web-Share now shows a "Shared!" toast (the canFiles path was a silent no-op when the OS sheet didn't surface). (polish) the advance CTA names the round ("✦ Enter the Quarter-final ✦"); zh "Advanced" 实况 → 进阶. Smoke 63 → 65.A fifth Codex review (7.0/10) on build 037a8a4. Every claim was verified against the live code first: 2 were real, 2 fair polish, and 2 didn't hold up (the pause toggle is a correct single-source toggle — the "stuck overlay" was a throttled-CSS-fade automation artifact; hidden panel controls are already inert via #50).
endMatch clears the live "WATCHING" badge + take-over hint so a watched match no longer leaves them on the result screen; the setup option groups expose aria-pressed (mirrors .sel, tracks clicks) so a screen reader announces the active language/difficulty/length/control/world. (polish) a @media(max-height:780px) squeeze so the full menu stack (Enter · Watch · Daily · Trophy) clears the 1280×720 fold without scrolling; a prominent, announced share toast (role=status/aria-live, gold top banner) alongside the synchronous button relabel. Smoke 60 → 63..panel inert/fade machinery, role="dialog", en+zh. Smoke 59 → 60.CUP_SIZE_SETTING, one of 2 | 4 | 8 | 16 (≤ the nation pool, invalid → 8), shipped at 16 — not a user/menu toggle; everything (draw, seeding, bracket, roundName, daily seed, Trophy Room grid) derives from it. All 16 nations are freely selectable (no unlock gating); the 8 added (Argentina, Netherlands, Croatia, England, Portugal, Morocco, Senegal, Poland) already had full colours/flags + native/capital/facts. The bracket now scrolls horizontally (reads R16 → Final) instead of wrapping on narrow screens. Old 8-team saves self-invalidate and redraw. Smoke 58 → 59 (cup-size config contract).A multi-agent adversarial review of the two new features confirmed 10 issues (3 refuted).
loadCareer sanitizes the trophy log like validCup sanitizes a saved cup, so a hand-edited / migrated record can't crash the Trophy Room; a daily/friend link now starts its fixed scenario fresh (resume is gated on !CHALLENGE) instead of silently resuming a coincidentally-matching saved cup mid-bracket; the challenge banner re-localizes on a language switch (applyLang → syncChallengeUI). (P2) Daily + Trophy Room share one centered, wrap-friendly row so the menu stack is shorter. (P3) dropped the write-only career.diffs map and the unused challengeCta key. Smoke 54 → 58 (trophy sanitization, the recordCup cup.done gate, world-aware champion copy, and a ?daily=DATE reload).The retention pillar: a persistent record of every cup, so there's a reason to come back. localStorage only — no backend, accounts, or payments.
dreamcup-career record tracks cups won/played, the current + best win streak, cups won per nation / world / difficulty, and a trophy log. A pure core (applyCupResult) folds each finished cup in — a win adds a trophy and extends the streak, a loss ends it — and returns the new honours earned (first champion on a world, first cup with a nation, Triple Crown across Moon + Mars + Earth, a new best streak), surfaced as a gold end-screen callout. A 🏆 Trophy Room menu panel shows the stats, three-world medals (+ Triple Crown badge), a nation flag grid (X / 8), and recent trophies. Champion copy is now world-aware (names Moon / Mars / Earth, not a hardcoded "Moon"). en+zh. Smoke 53 → 54.The first "make players return / make results worth sharing" bundle. Both pieces lean on what already exists — the deterministic cup draw + the share card — so neither needs a backend, accounts, or payments (localStorage only).
?daily=YYYY-MM-DD seeds a fixed {nation, world, difficulty} (FNV-hash of the date) so everyone playing today gets the same scenario; ?team=BRA&world=mars&diff=hard presets the menu from any link — and the in-game share card now points here, so "beat this?" lands a friend on the exact same matchup. A gold banner shows the active challenge; changing any setting opts out into a custom game (and the share URL follows). A 🎯 Daily Challenge menu button loads today's seed. Resume-aware: a challenge never wipes a saved cup. en+zh. Smoke 51 → 53.og:image). Shared links now render as a card, not bare text: scripts/gen-og.mjs renders a brand-matched 1200×630 og.png (committed, copied into build/) and the <head> carries absolute og:image/twitter:image + summary_large_image. A smoke assertion checks the tags point at og.png and the file is served.A fourth review (8.1/10) on build e32c7ec. Each concrete claim was verified on the live build before fixing; "Cup"/"nation" and the round-framed CTA were kept as deliberate calls.
952c127 — Possession readability. The ball-locator ring glows gold when you control the ball (cyan otherwise), tying the ball to your player in a cluster; the selected-player overhead marker gains a lock-on scale pulse.08f047a — Responsive fit. Beyond #54's scroll-safety, the primary actions now sit above the fold without scrolling: a width-based mobile compaction (the ≤840px-height rule missed 844px phones) brings the portrait CTA above the fold; the result screen fits all four actions at 720 even for the champion layout; landscape drops the menu title + shrinks so the Enter CTA fits.86da71e — Fix: Share button silent no-op. The whole share action (even the desktop download) was nested in canvas.toBlob, whose callback doesn't fire in throttled tabs / where navigator.share is absent → clicking did nothing. Now file-share is probed synchronously; otherwise the card downloads via the synchronous toDataURL + link-copy, always landing on a visible state ("Image saved · link copied"). Label → "Share result card…".A third review (8.0/10) on the post-fix build — all polish, no broken gameplay. Two PRs; its three concrete HIGH claims were each verified against the live build before fixing.
f8bec37 — UX/copy. End-screen primary "New Cup" → "Play Again" (the deterministic draw means it replays the same bracket, not a new cup); "Share result…" (the ellipsis signals it opens the OS share sheet / a download); and brief action-feedback toasts — Switched / Through ball / Lofted pass — for the explicit verbs that had no text confirmation.27b76f3 — Responsiveness + a11y. Full-screen panels are now justify-content:safe center + overflow-y:auto (centered when they fit, top-aligned + scrollable when they overflow), and the end screen is compacted at ≤840px height — so all four result buttons fit at 1280×720 and mobile-landscape menu cards no longer clip. Hidden panels also drop to display:none after their fade-out (no ghost layout bounds), completing the inert a11y fix from #50.A follow-up review (on the copy/UX build) found polish-level issues — no broken gameplay, no console errors. Four PRs (the share hook + the "scripted bracket" were already shipped):
536c769 — Ball-locator ring + persistent camera chip. A cyan ring tracks the ball on the pitch so it stays findable when robots cluster; a small "📷 Follow / Ball / Wide" label under the corner buttons shows the current camera mode at all times (not just the switch toast).b9f1193 — EN localization + mobile CTA. English mode no longer mixes scripts on the geography line — the endonym rides the name in parens ("JAPAN (日本)" + "Tokyo"), the capital line is always the UI language. The start CTA's wide tracking is tightened on phones so it no longer wraps to a tall block at 390px.ff2baab — A11y: inert inactive panels. inert now mirrors the .hidden class on every full-screen panel (menu / pause / end / bracket), so hidden panels are out of the focus + accessibility tree; an open modal also inerts the screen behind it.991ea5f — Bracket: future rounds stay TBD. Follow-up to #42 — a round's team slots now appear only once the round feeding it has been played, so a fresh bracket shows just round 1 (later rounds are TBD), not predicted advancers.A Codex playtest review flagged control clarity + pause/tournament-state reliability. The "score changed while paused" report turned out not to be a simulation leak (pause fully freezes the sim) — it was a pause that silently never engaged. Five PRs:
5353422 — Copy/UX pass. Brand line consolidated to "…Football Cup" everywhere (right on Mars/Earth, not just the Moon); controls "PES" → Advanced; bracket kicker → "The Road to the Cup"; format line "win N matches"; an informative pause subtitle ("Time, AI and ball physics are frozen…"). Presentation: helper text is sentence-case now (titles + CTAs keep caps), decorative ✦ trimmed from secondary text, and the in-field hint regrouped into Move / Attack / Match (with the C camera key). A11y: icon-button tooltips.95b24e4 — Camera options. Three in-match modes via a 🎥 HUD button + the C key: Follow (default), Ball (keeps the ball framed near the box), and Wide (a high, pulled-back tactical view of the half-pitch + goal). Persisted; default Follow leaves the existing feel unchanged.62055d6 — Progressive bracket. Scores + a decided winner now appear only for rounds you've played; future rounds show the pairing alone (no pre-filled AI scores, no pre-crowned champion), so the tournament reads live instead of scripted.e3e79e7 — Control clarity. Stronger selected-player ring + larger overhead marker with a high opacity floor; a possession cue (the ring brightens + swells when the ball's in range); and the first-match control prompt auto-dismisses so it never lingers over the field.1759e2a — Pause + in-match menu. Pause now engages from any in-match state (countdown, goal celebration, shootout) instead of being a silent no-op outside PLAY, and the pause overlay gained Resume · Restart match · Quit to Setup.2e1af69 — Gravity Lab P2: per-world environment grade. Each world now looks as distinct as it plays — DreamEnvironment.applyWorld() re-grades fog, the two lights, the sky tint, and the regolith, and hides the Earthrise on Earth. Mars warms to a dusty rust, Earth cools to a deep-blue "home" sky; Moon is a pixel-identity (defaults unchanged, GK-balance tests untouched). Colours/visibility only — no geometry, no recompile.5699acb — Shareable result card (P1). A Share button on the end screen composes a dependency-free 1200×630 PNG on an offscreen canvas (title · world + gravity · scoreline · Champion 🏆/round result · best-goal minute · mechcup.com), then routes to navigator.share({files}) on mobile or download + copy-link on desktop. Client-side only — the game uploads nothing. (Goal clip P2 + challenge-link P3 stay phased.)622af3d — Geography "Did you know?" cards. All 16 nations gained a bilingual facts{capital, did, flag} + a native endonym; the pre-match walkout now shows the endonym · capital under each flag and a fun fact about your opponent. Ambient, no quiz. (A smoke assertion caught a merge that would have clobbered the team's emoji flag — fixed.)f6f9065 — Gravity Lab. A 🌕 Moon / 🔴 Mars / 🌍 Earth WORLD selector flips the game's lone gravity constant (CFG.gravity) per match — lower g = higher, hangier arcs. Gameplay g is compressed for playability (Moon stays the baseline, so live play + the GK-balance tests are unchanged) while the fact card carries the real m/s². The "Moon 2026 · Mars 2030" tagline, made playable.ecf6870 — Docs: land the gravity/geography/shareability design trio on main (design-only markdown + a CLAUDE.md pointer), verified against the live code.d5bf54d — Bracket viewable from home + your first opponent on the right card. The bracket view existed but only the end-screen opened it; now the Cup card and the gold format line both open it from the menu (a pure entryCup() previews the deterministic draw without touching a saved run). Because the draw is deterministic, the right card now shows your actual first-round opponent (flag · name · round, e.g. Quarter-final) instead of a generic 🏆, updating live as you cycle nation — the menu "vs" is a real matchup, with the full draw one tap away. New smoke: cardB shows an opponent (not 🏆), updates on nation change, and opens the bracket.heading vector, trophy-dust + dream-mote scratch reuse, mote drift staggered to ~30 Hz, particle pools sweep/draw only a live high-water span, HUD clock writes only on change); fire-and-forget timers (end-screen reveal + goal chant) now tracked and cleared on a fast restart; i18n/a11y (translate the through-ball aria-label + the menu "vs", drop untranslated icon tooltips, de-interactive the inert Cup card); test/build hardening (smoke wraps cleanup in try/finally + checks console errors across the whole run with a tightened benign-analytics filter; build.mjs throws if the version-stamp marker is missing; the Lighthouse job drops its unused checkout + blind sleep); and removed the dead shootout intro phase. No behaviour change for players.9e78950 — Google Analytics 4 (build-time gtag injection). Injects the gtag loader at a <!--GA4--> marker only when GA_MEASUREMENT_ID is set (unset = no tag); track() forwards engagement events to window.gtag. Coexists with Cloudflare Web Analytics + the localStorage counters. New test/build-ga.mjs regression.f8f224f — Fix the code-review high + medium findings. Sudden death no longer ends a round early (the lead guards are regulation-only); validCup range-checks stored nation indices; the RAF loop stops + surfaces fatal() on a persistent crash instead of spamming forever; the tautological pass/tackle smoke assertion now flickers cross-team so it genuinely tests the debounce.e223298 — Docs: add CHANGELOG.md (every PR, #1–#26) + a README link.c28685b — Configurable knockout (4 / 8 / 16) + a WC-style results bracket + menu framing. CUP_SIZE now drives a power-of-two bracket with fixed (deterministic, seed-based) draws; added nations 8–15 for a 16-team option (dormant at the live size 8). The end-screen View Bracket shows every tie with scorelines, penalty scores in parens, the winner bolded and your path lit gold. The menu now frames the knockout up front: a gold format line that sizes with CUP_SIZE and a start button that names the entry round. (Browser-session review caught + fixed a smoke typo — T.n→T.nationCount — before merge; game code was correct.)94de619 — Extra-time countdown + a discoverable "Watch" toggle. Golden-goal extra time now shows a gold countdown to the shootout in the clock (90'+ · ET 0:29) instead of an invisible deadline. Added a persistent 👁 Watch button (+ V key), shown only while hands-on, that hands the match back to the AI — the inverse of demo-mode takeover, so watching ↔ playing is a clear round-trip both ways.9a150e9 — Watch / auto-play demo mode. A ✦ Watch ✦ menu button lets the AI play both sides while you watch (a "● WATCHING" chip + hint, controls hidden); any input instantly takes over into the player nearest the ball. Demo-only liveliness tuning keeps it from stalemating.2d2ca30 — Pre-match walkout + anthem. A new WALKOUT beat before the 3·2·1: a glassy card (both flags · "vs" · the round) over a short synth anthem, the crowd hushing then swelling on the kickoff whistle.0033c0e — Crowd goal chant + near-miss groan + build-up swell. Synth-only additions to the existing crowd bed: a pitched terrace chant a beat after the goal roar, an "ooooh" on a near-miss, and a murmur that rises into a lined-up shot.8d7ec2c — Goal lower-third. A TV-style scoreline band slides in on a goal (🇧🇷 BRA 2 – 1 USA 🇺🇸 + minute + scorer), riding the existing replay/slow-mo path; distinct from the centered GOAL splash, which keeps its no-paintable-background guard.f5d31f9 — Mobile "switch player" button (⇄). A touch button (above the joystick) for switchPlayer(), which was keyboard-only (Q/Tab) — touch players were stuck on the kickoff striker.38422ba — Make penalties obvious. Added an on-field aim guide (arrow + goal-line target ring), touch aiming via the movement stick, a real charge that builds power during the shootout (it previously fired at min power), and on-screen instructions + a corner-aim tip. Fixed a keeper so well-placed corners actually score.a4c7411 — Penalty shootout (#4B). A tied knockout goes to 30s golden-goal extra time, then a self-contained best-of-five (then sudden-death) shootout with a read-and-commit keeper, a bilingual tally HUD, and a (4–3 pens) end-screen scoreline.c7bd686 — Bump deploy actions off the deprecated Node 20 runtime (checkout@v7, setup-node@v6, wrangler-action@v4, build Node 22) — clears GitHub's Node-20 deprecation warnings.ebcdf6f — Fix the build's $& footgun. Inlining the minified script used String.replace(str, str), so a $& in minified JS was treated as a match-substitution pattern and corrupted the bundle ("Unexpected token '<'" on boot). Now uses a function replacement.e1232e7 — Analytics: point the Cloudflare Web Analytics beacon at the mechcup.com site.de1e992 — Docs sweep to MECH CUP 2026, preserving the load-bearing internal identifiers (window.DreamCup/DreamCupPerf, dreamCupStats(), DreamEnvironment, the dream-cup Pages project + dream-cup.pages.dev).ca2dff3 — Lunar visual pass: cratered regolith ground, a bigger Earth in the sky, cooler crowd / fainter aurora.d702aaa — Rebrand: The Electric Dream Cup → MECH CUP 2026 / 机甲杯 2026 — the first space robot football final, set on the Moon (copy, theme, head metadata).34803cd — Integrate the parallel session's work into the Cup: trademark-safe naming (no tournament-brand names), Easy/Medium/Hard difficulty labels, dormant monetization hooks, and PES-style controls (through ball / lofted pass / Simple⇄PES toggle).7c4dae7 — #4C: full 8-team bracket view on the end screen (rebased onto the MECH CUP main).9fb390b — Smoke: skip the WebGL draw in the harness (__norender) — fixes a post-gameplay page wedge that stalled later assertions.09a48ab — Fix the deploy gate with a per-step smoke timeout (a job-level limit wrongly counted variable Chrome-install time against the smoke budget).c60842e — Cup persistence (#4A): resume an in-progress run across reloads.e61928e — Re-add Cup test coverage via the pure drawCup function (asserts bracket invariants without wedging the headless harness).aef19e2 — Remove a Cup smoke block that wedged the headless harness, to green the gate (coverage restored in #6).b835ddc — Speed up the smoke gate: cache the number textures + lighten the Cup test.8f0c58a — Fix a smoke-test hang from a stale .team.attack (→ NaN) + add a CI timeout.0505ab8 — Fix a setTeam crash: a number-mesh skin-key mismatch in the per-round re-kit path.75fcaf9 — Add The Cup: an 8-nation single-elimination knockout (Quarter-final → Semi-final → Final) with a per-round difficulty ramp — pick a nation and play through to the title.init — Initial release of The Electric Dream Cup (pre-PR, direct commit): the single-file Three.js arcade robot-football match that everything above builds on.