Spaces:
Paused
Paused
Commit History
-
Odyssey: bound the living galaxy to 3 seed-drawn background worlds
Claude +1: Claude Opus 5Unverified signaturecreateGalaxy instantiated and simulated all 11 worlds from turn one. It now brings up the start seat plus a pseudo-random draw of BACKGROUND_WORLDS (3) others — backgroundWorldIds, keyed on the galaxy seed and the start world from its own distinctly-seeded stream, returned in fixed roster order so the background scheduler and every galaxy-wide scan read exactly as before. Which neighbours are alive is now part of what a seed means; same seed still replays the same galaxy. The rest of the roster stays DORMANT: still on the starmap, still jumpable, still a full skirmish map — built in on arrival (jumpCapital already did this for a never-visited destination) and simulating from then on like any world you leave behind. addPlanet now honours a standing claim when it builds a world in, so a dormant world the faction-spread sweep claimed flies those colours when the player finally lands on it. Three paths assumed every world's state existed, and are fixed here: - initiateJump treated a mis…b88cc9c
-
Phase 5 (replay + season): re-run any recorded match, and close a ladder
Claude +1: Claude Opus 4.8Unverified signaturedocs/competitions-and-elo.md Phase 5's remaining two bullets, which completes the plan — all six phases (0-5) have now landed, noted in a new closing section. REPLAY IS A RE-RUN, NOT A RECORDING. A stored history row already carries its world, its exact seed, its swapAsym half, its pinned difficulty and both entrants' strategies; the roster carries each entrant's archetype. That is the complete input to createSelfPlayState, so buildReplayConfig(row, ledger) rebuilds the match and startSpectatedMatch runs it through the spectator the previous stage built — same speed control, same Observer Mode, nothing captured, nothing stored. What gets rebuilt is the SEATING, not the labelling: a row is always reported A-relative but was played in one of the worker's two directions, so buildReplayConfig un-does flipRow's relabelling and restates the recorded outcome seat-relative, which is what lets spectatedMatchOutcome be compared to it directly. DETERMINISM HELD — BUT ONLY AFTER A REAL BUG, AND …090c049 -
Phase 5 (spectate): watch an AI-vs-AI match live, at 1x-8x
Claude +1: Claude Opus 4.8Unverified signaturedocs/competitions-and-elo.md Phase 5, the spectate half. Replay-from-the-ledger and Season stay open. Observer Mode was one `if` away from supporting this, and that is all it took: enterObserverMode's `if (!game.galaxy) return` became `if (!game.galaxy && !game.spectateMatch) return`, with spectateId falling back to the state's own planetId. The camera path, input.js's seven delegation points, observedState()'s existing game.state fallback and boot.js's render swap were already general and are untouched — the Odyssey observer tests are the safety net that the galaxy path did not move. Where Observer Mode is OFFERED stays exactly two places: an Odyssey, and a match the human is not playing. An ordinary skirmish is still refused; revealing its fog would just be a cheat. A WATCHED MATCH IS EXHIBITION ONLY, deliberately. Every rated result in this system is a pairing — side-swapped, multi-seed, replicate-parity swapAsym — because a single unswapped match is confounded by the seat edge se…450f6c1 -
Phase 4 follow-ups: keep a simulated match from moving a human rating
Claude +1: Claude Opus 5Unverified signatureThree findings from Phase 4's review lenses, fixed before merge. The one that mattered: the human's roster entry was selectable as an ordinary AI entrant in Quick Duel and the Tournament field builder (the Gauntlet already excluded it). Both of those are SIMULATED by the Worker, so picking the human would have played their name with a scripted controller and folded the result into their rating -- a rating changing from a match they never played, earned by a strategy that isn't them. That breaks the property the whole feature rests on (D1: a rating means one thing). Both pickers now exclude the human, with the reasoning stated where each exclusion lives. Opening Standings from the tab landed on whatever bracket was last viewed (Medium by default), so finishing a Hard gauntlet and clicking the tab showed "Nobody has played a rated match at this difficulty yet" with the rated games one bracket away and nothing saying so. A new pure mostRelevantBracket() picks a bracket that actually has…6fec449 -
Phase 4 (part 2): the playable Gauntlet — play a real match, rate it, resume it
Claude +1: Claude Opus 4.8Unverified signatureThe human-inclusive half of docs/competitions-and-elo.md Phase 4, on top of part 1's persisted ledger support: a fifth Competition tab where you play ONE live match against each AI entrant in a field, in order, and every result lands on the same bracketed ladder the AI entrants are rated on. One live match per opponent is the format's defining decision, not an economy measure: an AI-vs-AI pairing is worlds x seeds x 2 sides of simulated matches (seconds of compute), but a person cannot play 40 real matches. That is why Gauntlet is the human-inclusive format that ships first, and why the config screen prices a run in HOURS OF REAL PLAY before Start is clickable ("4 live matches — one per opponent — up to about 1 h 20 min"), defaulting to Quick (20 min), since the same field at Standard is over three hours. - boot.js startCompetitionMatch(fixture): a near-copy of startGame, deliberately — same resolveSeed/difficultyDials/createGameState/bootState chain, same loop, same HUD. A gaunt…1ff6f70
-
Tier 1, step 5: close the coverage gaps and the doc drift (C6-C16, E1, E2)
Claude +1: Claude Opus 5Unverified signatureThe mutation-proven gaps first. Each of these was verified by re-introducing the exact defect and confirming the new test goes red. C8 — drawBuildingBars and drawBuildGhost were covered only by assert.doesNotThrow, so replacing either body with `return;` survived the full suite: every health bar, build-progress bar, output gauge and paused/stalled badge in the game could vanish with CI green on both Node versions. The recording ctx this file already had is now used to assert real output, that a damaged building gets a bar, and that a paused producer is drawn differently from a running one. (Not "draws more" — a running-but-throttled factory draws the busier badge of the two.) C9 — dropping aiColonyTarget, aiLastThreatAt, or the entire playerAi block from persist.js also survived. Re-checked by mutation: both single-field drops now fail. The failure mode is "the AI behaves differently after reload", the hardest class of bug to report. C14 — commit ac44339's ore-surplus army-cap featu…cce54f5
-
Add a configurable population cap (200/250/300/Max) for both sides
ClaudeUnverified signatureNo hard ceiling existed before this — supplyCap (engine/supply.js) was purely additive from completed buildings (Command Center +10, Habitat +8), unbounded above. Added a new setup.js option, Population cap, alongside the existing economy dials (Faction/Resources), offered in both Skirmish and Odyssey (the two modes with a real economy) — Max (the default) is byte-identical to today's always-uncapped behavior. - engine/supply.js: split the old supplyCap into buildingSupplyCap (the raw, unclamped additive total) and a new supplyCap that clamps it to state.popCap when set. Every existing caller (production.js's enforcement gate, hud.js's display) keeps calling supplyCap unchanged and transparently gets the clamped behavior. - state.popCap threads through createGameState, boot.js's startGame AND startOdyssey, and engine/galaxy.js's createGalaxy/addPlanet — applied unconditionally to every world (active seat and background neighbours alike, exactly like sizeMult/resourceMult…dadc0b2 -
Add Observer Mode: a free-look spectator over the Odyssey galaxy
Claude +1: Claude Sonnet 5Unverified signatureLets the player watch how AI archetypes develop and interact, like a scientist running the sim and just watching, instead of needing a CLI tool (tools/ailab.js) to get that view. O (or the topbar Observe button) toggles it: fog is revealed on whichever world is being spectated, the Galaxy map becomes a free camera jump to any world (discovered or not, no fuel/Spaceport), and Space cycles every base on that world regardless of owner. A stats panel surfaces the spectated world's archetype, strategy, diplomatic stance/hostility, army composition, buildings/development, supply, and resources. Orders are disabled while observing. Deliberately never touches game.state, galaxy.activeId, or the real camera - observer.js only changes what's drawn (every world already ticks regardless, via stepGalaxy) - so normal play resumes exactly where it was left on exit. Fog bypass is a pure render-time flag on drawFrame/drawUnits/drawBuildings/drawNodes/drawMinimap, never a mutation of state.fog. First…6bd2c72
-
P8 Combat Depth: veterancy chevrons + tiered destruction visuals and audio
Claude +1: Claude Sonnet 5Unverified signaturePresentation half of the veterancy/tiered-destruction proposals (the sim half landed in the previous commit): - renderUnits.js's drawUnits overlay pass stamps a small chevron stack above the health bar for any unit at rankMults(unit).rank > 0 — either owner (an enemy veteran's rank is intel, same as its hostile pip), and independent of drawHealthBar's own "hide at full hp" rule since a rank is a permanent trait, not a damage state. - effects.js's addDeathFlash now carries the killed entity's radius/kind (defaulting to a small-hull baseline for back-compat), and scales the death flash's own lifetime by both: a bigger hull lingers longer (clamped 0.7x-2.2x), and a building's slower collapse gets an extra 1.6x on top. renderEffects.js's death-ring draw scales its radius/width off the same baseline, flings a few outward debris shards for a hull at or above the debris threshold (radius 10 — Breacher and up), and draws a building as a slower two-ring collapse (a laggi…d213ade -
Starmap live colony ledger: alert badges + garrison line
Claude +1: Claude Sonnet 5Unverified signatureMulti-colony Odyssey play routed all trouble through transient toasts (boot.js notifyColony, 5-9s, max 3 on screen); the starmap itself showed only status/income/stance/industry, so a world under attack, recently fallen, or undefended looked identical to a healthy one. - boot.js's notifyColony now records every attack/hostile/lost notification onto the session as game.colonyAlerts[planetId] = {type, at} (session.js), in addition to its existing toast. Written unconditionally for all three types, independent of the toast's own 9s throttle (which still only compares a world's own prior ATTACKED timestamp, unchanged). focusActivePlanet clears exactly the one world's entry when the player actually arrives there; bootState resets the whole ledger for a fresh game. Transient session state, like lastAttackAt/groups next to it — never persisted. - starmap.js's renderStarmap reads the ledger per world when building each node: a CSS badge class + sub-label reading "under atta…25e82b6 -
Colossus splash: def-driven area damage as the T3 anti-mass verb
Claude +1: Claude Sonnet 5Unverified signatureAdds a generic def.splash = {radius, frac} mechanic and puts it on the Colossus (radius 26, 50% falloff damage), giving T3 a real answer to "spread out vs artillery" that punishes separation.js's own same-owner unit packing — until now every T3 unit was single-target, so clumped deathballs were never punished by anything. engine/combat.js: performAttack's new applySplash helper runs after the primary hit lands. It mirrors acquireTarget's own grid/full-scan idiom (queryNeighbors(state.unitGrid, ...) when a grid exists, else a plain scan over state.units.values()) and, by construction, walks only state.unitGrid/state.units — never state.buildings — so splash can never reach a building or the attacker's own side, however close either stands to the impact point. The primary target itself is excluded from its own splash (it already took the full direct hit). HP is subtracted for every splash victim in one pass before any death is resolved, so the outcome never depends on queryNeighbors' it…8f2921c -
HUD alerts + selection UX: minimap attack pings, base cycling, idle-production chip, Alt-subtract
Claude +1: Claude Sonnet 5Unverified signatureThree related input.js proposals landed as one coherent pass, per the roadmap's Phase 3 UX grouping (docs/improvement-proposals.md lines 351-372, 563-572): - Attack pings on the minimap plus a jump-to-last-alert key: effects.js exports activePings() (the same under-attack pings addUnderAttackPing already feeds, kept alive far longer via a new MINIMAP_PING_LIFETIME_MS so the minimap can still show them once the world-space ping and the banner have faded); minimap.js draws pulsing red rings for them; boot.js threads activePings() into its drawMinimap call and moves the remembered attack site onto game.lastAttackAt (session.js, next to supplyBlockedUntil, which already crosses this exact module boundary) so input.js's new Backspace binding can jump to it too, not just the banner's own click handler. - Space cycles bases and an idle-production topbar chip: centerOnBase() used to always land on the first completed Command Center a .find() happened to hit; repeated presses wi…18ec8e7 -
Reactive opening checklist + an honest, configurable endgame clock
Claude +1: Claude Sonnet 5Unverified signatureReactive opening checklist (replaces the static objectives strip): overlays.js's showObjectives() used to print one static sentence that auto-dismissed after 30s regardless of what the player had actually done. renderObjectives(state, galaxy) now walks a small per-mode predicate table (5 items for skirmish, 4 for Odyssey) and paints a live checklist; items check off with a flash as their predicate is satisfied. updateObjectives(game), called from hud.js's renderHUD every tick behind a cheap signature guard, keeps it reactive and retires the strip (remembering the completion via a per-mode localStorage flag) once every item is done, instead of a blind 30-second timeout. The strip is suppressed for a scripted scenario, whose own scenario bar already states that mode's objective. Make the clock endgame visible, honest, and configurable: - engine/victory.js's finish() now records state.winReason ("elimination" | "mutual-wipe-score" | "timeout-score") at each of checkWinCondition's …5bffdf0 -
Wire the start-world picker and asym-swap toggle into setup/boot
Claude +1: Claude Sonnet 5Unverified signaturesetup.js's Odyssey branch now renders a compact 11-world card row (the skirmish nine plus the two Odyssey-only extras) plus a default "Random" card, replacing the old single "land on a random world" button — reusing the skirmish card's own data (name, deposits tag, archetype doctrine, industry/tech). One click both picks the world (or Random) and begins, mirroring the skirmish cards below it. boot.js's startOdyssey threads the pick through as createGalaxy's opts.startId. The skirmish card grid gains a per-card "swap sides" toggle on any world with an asym block (Oort, Nimbus) — a plain `<span>`, not a nested `<button>` (invalid: a button can't contain another interactive control), with its own local pick so toggling one card can never leak onto a different card you click instead. boot.js's startGame threads the choice through as createGameState's opts.swapAsym. Not offered on scenario cards (setupEscort/Raider/Bounty don't consume swapAsym). setup.css companion styling: a narrower .m…2fa2e12 -
Telegraph counter-triangle hits: bonus flag, hotter tracers, HUD counter text
Claude +1: Claude Sonnet 5Unverified signatureMerges "Counter-triangle telegraphs" and "Surface the counter-triangle on unit buttons and selection rows" into one readability feature, per the roadmap. - engine/combat.js: performAttack stamps bonus:true on the attackHit event whenever the attacker's bonusVs counters the target's type (scoped to the per-type hard counter only, distinct from the pre-existing heavy flag's class-wide siege bonus). Zero sim effect - UI-only field. - effects.js/boot.js: addTracer takes the bonus flag through from the event. - renderEffects.js: a bonus tracer renders hotter (shade()'d toward white) and thicker, with a bigger/hotter impact spark. - hudSelection.js: unitTip() appends "strong vs / falls to" lines derived from UNITS[].bonusVs (+ "structures" for bonusVsBuildings) via a hoisted reverse index (COUNTERED_BY), same idiom as ALL_COSTS and aiMilitary.js's own COUNTER_OF. The aggregated multi-type selection rows get the same "falls to" suffix (shared summaryRowLabel() feeds both the …0056365
-
Move DIFFICULTY_OPTIONS into engine/aiDifficulty.js, add state.ai.difficulty
Claude +1: Claude Sonnet 5Unverified signatureScaffold for tiered difficulty economics: the Easy/Medium/Hard table now lives in engine/ (mirroring how aiStrategy.js already holds STRATEGIES) with a difficultyFor(state) resolver alongside it, and every state.ai now carries the picked difficulty as a plain string, threaded through boot.js's skirmish/Odyssey paths and galaxy.js's per-planet seeding, with save/load round-trip support. setup.js re-exports DIFFICULTY_OPTIONS so boot.js and existing tests are unaffected. No economic field is added to any difficulty tier yet — every new/updated test proves this lands as a pure no-op. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G73oK4p3bnreizCcMUHVEy53ce1fb
-
Add a player-picked AI Strategy (Aggressive/Economic/Force Parity) and Helium Bomb use
Claude +1: Claude Sonnet 5Unverified signatureThe AI never touched the Helium Bomb (built at the Star Dock alongside the Leviathan) despite having full logic for the Plasma Rig. engine/aiSuperweapon.js gives every strategy a use for one: armed as a home-defense trap the instant the base is attacked, or — Aggressive only — walked to the attack target and triggered there, mirroring the player's own "Detonate Now". engine/aiStrategy.js adds a new player-selectable stance, orthogonal to the planet-driven archetype (which stays purely a flavor pick): Aggressive presses the attack earlier/smaller and forces the issue in Odyssey instead of waiting out diplomacy; Economic keeps a minimal standing army until it's actually attacked, then surges, and leans into the deep industry chain (Plasma Rig included) regardless of archetype; Force Parity continuously mirrors whatever it's seen of the enemy's army and never picks the fight itself. The default strategy is a byte-identical no-op, so every existing archetype/resolve guarantee is untouched…be1f1f9
-
Implement Phase 3 of battle wreckage: rendering and event polish
ClaudeUnverified signatureWreck nodes now render as jagged grey/rust debris instead of the warm-gold asteroid look every natural deposit shares, so a battlefield's mineable aftermath reads at a glance as wreckage. The "wreckMatured" event gets a sound + toast in boot.js, mirroring the Helium Bomb crater's own craterMatured callout, listing every commodity the site settled into. Verified in a live browser (dev server + Playwright): injected synthetic wreck/normal nodes side by side to confirm the visual distinction, and fired a real wreckMatured event to confirm the toast/sound fire cleanly with no console errors.6e6bac2
-
Fix stray move order during jump landing pick; mark player assets on the picker
Claude +1: Claude Sonnet 5Unverified signatureA right-click-drag (or box-select, or an armed attack-move) begun on the main canvas arms its state via a canvas-scoped mousedown, but resolves via a window-scoped mouseup (intentional, so a drag ending off-canvas still completes). If the landing-site picker or starmap opened mid-gesture, that stale drag was still "live": releasing anywhere over the new modal — including the landing picker's own chart — read as a move order for the currently selected units on the ORIGIN planet, using the drag's original start point. input.js now exposes cancelGesture() to clear all in-flight pointer state; boot.js's initiateJump and starmap.js's openStarmap call it before showing their modal. Also: the landing picker no longer treats every destination as a total unknown. It only ever opens when there's no COMPLETED player Spaceport there, which includes a colony you already hold (a pad still under construction, or lost). On those worlds the chart now marks the player's own known assets — a bigger circ…7aab638 -
Add a fuse delay to the Helium Bomb and slow the shockwave further (#12)
Unverified signatureProximity and the player's own "Detonate Now" command now light a 4-second fuse (BOMB_FUSE_DELAY) instead of detonating on the spot; the actual blast follows once the timer comes due, via a new updateBombFuse tick check. Being attacked stays an instant, unrelated trigger. Disarming a bomb also cuts a lit fuse. The proximity trigger distance (BOMB_DETECT_RANGE) is now tied to BOMB_CORE_RADIUS (1.5x the bomb unit's own radius) instead of a distance past the blast radius, so only a point-blank enemy sets it off. Adds a pulsing amber warning ring + a distinct beep cue while the fuse burns, and slows the shockwave ring's travel by a further 20% for more dramatic pacing. Updates the HUD panel text and persistence (fuseUntil survives save/load, sanitized like armed) accordingly. Co-authored-by: Claude <noreply@anthropic.com>c1ba8ce -
Give the Helium Bomb distance-based damage falloff and a slow shockwave (#11)
Unverified signatureReplace the flat "erase everything within blast radius" kill with a distance-squared decay of HP loss: entities within BOMB_CORE_RADIUS (1.5x the bomb's own radius) still take a guaranteed-kill hit, but damage tapers off with the square of distance beyond that, reaching zero at BOMB_BLAST_RADIUS. Blast-killed entities still grant no salvage, same as before. Also slows and reworks the detonation VFX into a dramatic, decelerating shockwave ring that crawls out to the true blast radius and cools from a hot core color to ash-grey at the tapering edge, echoing the new damage curve. Updates the HUD panel text and test suite to match. Co-authored-by: Claude <noreply@anthropic.com>33fe16b -
Land at your last-used Spaceport; pick blind on a fogged minimap otherwise
ClaudeUnverified signatureA jump's landing zone was always a world's fixed, generation-time anchor point (map.bases.player) — the same spot on every visit, with no memory of anything the player built there. Now: - If the destination already has a player Spaceport standing (built on an earlier visit), the fleet lands right at it — whichever pad was most recently used to land there (engine/galaxy.js's new `lastLanding` timestamp on the building, tie-broken by id for a never-used pad). - Otherwise there's no beacon to home in on, so the player picks an approximate landing site themselves on a new "blind minimap" modal (landingPicker.js) before the jump executes — fully fogged (no terrain, fog, nodes or entities drawn), reusing minimap.js's coordinate mapping. The pick is deliberately imprecise: engine/galaxy.js's snapLandingPoint rounds it onto a fixed grid, previewed as a dashed ring around the marker. - A Spaceport-less origin (the existing "control switch" fallback that moves no units) never …d1cc6d6
-
Helium Bomb: a detonation also terraforms - the crater matures into ore
Claude +1: Claude Sonnet 5Unverified signatureA detonation now schedules a crater (state.craters) at the blast center in addition to erasing everything in range. CRATER_SPAWN_DELAY (60s of sim time) later, engine/sim.js's per-tick updateCraters call turns it into a real ResourceNode - deterministically picked (hashStr off the crater's own id, no engine randomness) from every Raw-tier commodity, not just what this planet's own deposit table naturally rolled. It's a plain node once it exists: gather.js, fog/discovery, and rendering need zero special-casing, since node type is already derived purely from COM[com].extract. The one real complication is persistence. A normal map-generated node is regenerated fresh from the seed on load and only needs its `amount` saved - but a crater node doesn't exist in that regeneration at all. serPlanet now saves a crater node's whole shape (tagged `crater: true`) instead of just {id, amount}, and rehydratePlanet re-adds it to the regenerated map after the fact, sanitized the same way a tampered en…5330c74 -
Add the Helium Bomb: a mobile, arm-then-detonate doomsday unit
Claude +1: Claude Sonnet 5Unverified signatureA new Odyssey unit (engine/entities.js UNITS.heliumbomb), built at the Star Dock alongside the Leviathan, costed entirely in the three named materials: gas (Helium-3), antimatter, and plasmatorp (Plasma Torpedoes) - all three already-real commodities, no new economy pieces needed. Lifecycle: built UNARMED (safe to move anywhere, no different from any other unit) - the player ARMS it via a HUD toggle (hudSelection.js, same direct-mutation pattern as the Mender's auto-repair switch) - once armed, it detonates the instant any of three triggers fires: - it's attacked at all (combat.js's performAttack calls engine/bomb.js's detonateIfAttacked before applying normal damage - any hit sets it off instead of chipping its hp) - a live enemy unit or building comes within BOMB_DETECT_RANGE (checked once per tick from sim.js for every role:"bomb" unit) - the player clicks "Detonate Now" (calls detonateBomb directly) All three funnel through the one detonateBomb() function in the …53d4247 -
Fix core architecture drift: duplicate FACTIONS export, difficulty lists, seed dedup
Claude +1: Claude Sonnet 5Unverified signatureThree independent findings from the critical review: - data.js and engine/factions.js both exported a binding named FACTIONS (different shapes: lore/UI flavor vs. real gameplay traits), coexisting only because every importer happened to grab the right one. Renamed data.js's to LORE_FACTIONS. (starmap.js, its one importer, is updated in the next commit alongside its other starmap fix, since both land in that same file.) - boot.js kept its own hardcoded easy/medium/hard -> {aiApm, aiMicro} map, entirely separate from setup.js's DIFFICULTY_OPTIONS (which drives the picker). A drift between the two would silently downgrade an unrecognised difficulty to Medium instead of erroring. setup.js's DIFFICULTY_OPTIONS is now the single source for both the picker and the AI dials; boot.js derives from it. Values are byte-identical to before. - The seed-resolution expression was copy-pasted at five call sites in boot.js. Extracted into one resolveSeed() helper. Added regression …ed2403c
-
Tier 4: render interpolation — smooth movement on high-refresh displays
Claude +1: Claude Opus 4.8Unverified signatureThe sim ticks at a fixed 20 Hz but the screen paints at 60–144 Hz, so drawing raw sim positions made every unit teleport in 50 ms steps (a Skiff jumps ~11 screen px/step at max zoom). The fixed-timestep loop already hands the renderer the leftover fraction (engine/loop.js render(acc/dtFixed)); boot.js was ignoring it. render.js now keeps a prevPos baseline per unit (snapshotPositions, called by boot.js immediately before each tick) and draws each unit at prev + (cur - prev) * alpha — the hull (via a reused scratch so updateFacing sees the smoothed coords too), its health bar / cargo dot / enemy pip, and its selection ring. A unit that jumped this step (relief spawn, world relocation) exceeds a teleport threshold and snaps instead of sliding. While paused, alpha is pinned to 1 so units sit still. Pure render-side — the sim state is never mutated (drawFrame's contract holds), so replay determinism is untouched (549 tests green, determinism guard included). Zero per-frame allocation (pr…f894a44 -
Tier 3 review fixes: 5 defects from the adversarial pass
Claude +1: Claude Opus 4.8Unverified signatureAn adversarial review of the Tier-3 diff surfaced five real UI defects the happy-path browser probe couldn't catch: - Manual pause was keyboard-only (medium): added a topbar ⏸ Pause / ▶ Resume button (togglePause), visible in every mode, so touch players can pause too; the PAUSED banner text is now mode-neutral ("⏸ PAUSED"). - Group chip captured the input controller at build time (low): after an Odyssey jump with an unchanged chip signature the row isn't rebuilt, so a captured handler would recall on the destroyed pre-jump controller. Now reads game.input live in the handler. - togglePause ignored game-over (low): pressing P on the finished-match screen painted a spurious PAUSED banner. Now guarded with `game.state.over`. - Space's global preventDefault swallowed keyboard activation of focused controls (low): a focused HUD/modal button couldn't be Space-activated during play. The keydown handler now skips the Space/production hotkeys when a focusable control (button/inp…a3867b9 -
Tier 3 UX: pause, toast stack, control groups, hotkeys, touch & colour cues
Claude +1: Claude Opus 4.8Unverified signatureSeven UI/controls fixes from the review (all UI-side — no engine changes, so replay determinism is untouched and the 549-test engine suite is unaffected). - Pause (boot.js/overlays.js/saveload.js/style.css): a refcounted pause gates the update() callback (render keeps drawing the frozen frame). Opening Help or the Home-confirm modal pauses the sim; a P key toggles a manual pause with a centered PAUSED banner. The accumulator self-bounds (loop's backlog-drop), so resume never spirals and the fixed-dt sequence is intact. - Toast stack (overlays.js/style.css/index.html): #galaxyToast is now a column of up to 3 toasts, each with its own timer/onclick — a routine "Researched…" can no longer overwrite a one-shot clickable "colony has fallen — click to retake"; a plain toast never evicts a clickable one. Same showGalaxyToast signature. - Control groups (session.js/input.js/hud.js/dom.js/index.html/style.css): moved onto game.groups keyed per planet, so a group survives an Ody…1ad681c -
Tier 2 Odyssey meta: colony re-alerts, roster repair, war notices, full conquest
Claude +1: Claude Opus 4.8Unverified signatureFive meta-layer gaps the review surfaced (the relief-clock and activeId-structural throws were already fixed in Tier 1): - sweepColonies (galaxy.js): a standing colony resets the colonyLost latch, so retaking and rebuilding a world re-arms its alerts. Before, a world lost once was muted forever — a second razing never re-announced and the rebuilt colony's under-attack pings stayed suppressed. - Background war declaration (galaxy.js + boot.js): a neighbourHostile event on a background world used to be silently drained (no consumer), so the first warning was the colony already dying. sweepColonies now surfaces it as a "hostile" note and boot.js shows a clickable "neighbour turned hostile — click to reinforce" toast. - Roster repair on load (persist.js): deserializeGalaxy appends any ODYSSEY_WORLDS missing from a save's frozen roster. A pre-Phase-4 save froze `worlds` at nine, permanently hiding the newer worlds from the starmap AND — since the background scheduler keys…e692ca2 -
Tier 1 review fixes: save-load hardening, engine guards, UX bugs
Claude +1: Claude Opus 4.8Unverified signatureAddresses the Tier-1 (correctness & safety) findings from the codebase review. Save/load safety (engine/persist.js, saveload.js): - Validate values on load, not just JSON shape: drop entities of unknown type (an undefined def threw on the first tick, freezing the game AFTER load's try/catch returned), coerce numeric fields to finite/in-range (a string "time" concatenated; a NaN coord poisoned the spatial hash), and recompute the entity-id counter past every loaded id so a low/missing nextEntityId can't mint a colliding id. All identity for a valid save, so determinism is untouched. - Persist aiWaveCount — omitting it reset the AI economy-raid cadence on every reload, breaking continue-identically for Tactical games. - deserializeGalaxy now throws on structural nonsense (no worlds/planets, or an activeId with no planet) BEFORE the caller tears down the live game, and filters the world roster to real world ids. - autoSave returns success; "Save & Exit" falls back to a …a4bbde9
-
Odyssey: never lose — a wipeout sends relief; surrender is the only end
Claude +1: Claude Opus 4.8Unverified signaturePer follow-up: as long as the player hasn't surrendered, life goes on. - No defeat at all. checkGalaxyLoss is gone; checkGalaxyRescue replaces it: if you ever hold no foothold anywhere (every Command Center razed, no colony ship left), a RELIEF colony ship is dispatched to your active world's landing zone so you can re-found and fight on — instead of a game over. A sim-time cooldown (RELIEF_COOLDOWN) bounds the churn if relief keeps getting farmed, and its arrival raises a toast. Deterministic (sim-time gate, no clock/RNG). - Surrender is the one terminal state. surrenderGalaxy ends the run on the player's explicit choice; a new "🏳 Surrender Odyssey" button in the galaxy map (two-click confirm, arms red) drives it via boot.surrenderOdyssey. The game-over screen shows surrender copy ("you lay down your flag"); the old win/defeat Odyssey copy is retired (the sandbox has neither). - victory.js: the per-world checkEndlessLoss comment updated — a galaxy never loses, it…76e5854 -
Odyssey: 3-tier Spaceport (supply-capacity jumps), no wins, milestone fireworks
Claude +1: Claude Opus 4.8Unverified signatureThree changes to the open-world Odyssey, per request. 3-tier Spaceport with population-limited jumps: - A Spaceport now has a tier (1..3), upgradeable in place for escalating ore (upgradeSpaceport), like the Capital fortification. Its jump capacity — measured in ship POPULATION (supply), not head-count — scales 12 → 24 → 40. - A jump lifts only up to the pad's capacity (jumpManifest, closest-to-the-pad first, skip-not-break fill); a fleet larger than that crosses over several jumps, the overflow waiting at the origin pad. jumpCapital carries the manifest and reports what it left behind. - HUD: tier + capacity line, a live staged-fleet manifest (what lifts, what waits), and an upgrade button. render.js draws tier pips on the pad. No win conditions — play forever: - The Antimatter Gate and Domination no longer END the run. checkGalaxyProgress and checkDomination now emit one-time progress milestones instead of a victory; checkEndlessWin is suppressed for galaxy states (…947d9db -
Odyssey: make the colony alert toast clickable — click to defend/retake
Claude +1: Claude Opus 4.8Unverified signatureA background colony coming under attack (or falling) now raises an actionable toast: clicking it jumps you straight to that world to defend or retake it — a free hop, since it's a world you've held. Closes the loop on "how do I get back there to defend it" without hunting through the galaxy map. - overlays.js: showGalaxyToast takes an optional onClick — it flips on pointer events, dismisses on click, and lingers a bit longer (8s vs 5s) so there's time to act. Assigned via .onclick so a following plain toast clears the handler (no accumulation). - style.css: a .clickable variant re-enables pointer events (the base toast is pointer-events:none) and cues it with a pointer cursor + hover. - boot.js: notifyColony wires both alerts to jump to the colony; performJump now returns its result, so a jump that can't launch (no Spaceport on your current world) falls back to a toast explaining why instead of doing nothing. Browser-verified: the alert is clickable (pointer-events auto…ba43c95
-
Add a Home button that confirms saving before returning to the menu
Claude +1: Claude Opus 4.8Unverified signatureA topbar ⌂ Home button (beside Save/Load) returns to the setup screen, first asking whether to save. A skirmish/Odyssey gets a "Return to the menu?" modal with Save & Exit / Exit without Saving / Cancel; a scenario (which can't be resumed) gets a plain "Leave the mission?" with Leave / Cancel. Cancel, the backdrop, and Esc all dismiss it. - index.html/dom.js: the button. - boot.js: export restartToMapSelect and stop the loop on the way out (idempotent — game over already stopped it), so the game freezes behind the menu. - saveload.js: goHome() builds the confirm modal, routing Save & Exit through the existing mode-aware save() (galaxy vs single-state). - style.css: modal + .btn primary/ghost variants. UI only; 468 tests pass. Browser-verified all paths: Cancel keeps playing, Exit writes no save, Save & Exit writes one, and a scenario offers no save option. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEQUVetLk31Jj7niDQA…cf3fc9b -
Odyssey Tier-3 "make it legible": factory panels, iconed topbar, grouped build menu, hostile heads-up
Claude +1: Claude Opus 4.8Unverified signatureThe chain and the economy were running but invisible. This is the UX pass that surfaces them — no sim changes to the win paths, only what the HUD shows. #8 Factory selection panels: clicking a Smelter/Assembly Plant/… used to read only "420/420 hp". It now shows the recipe (2 ore → 2 Metals · ⚡2) and a colour- coded status answering "why isn't my antimatter rising?": Running (with live output rate), Throttled N% — low Power, Starved — needs <input>, or Stalled — no Power. A Reactor gets its own line (Grants ⚡20 Power) since it runs no recipe. The status mirrors updateProduction's own limits exactly and rides the panel signature so it stays live without a per-tick rebuild. #10 Topbar + build menu legibility: resource readouts are now iconed and empty stockpiles are suppressed (a fresh Odyssey no longer shows "ai: 0, antimatter: 0" across a dozen unmade goods) — ore always stays visible. The ~18-building Odyssey build menu is grouped Economy / Military / Endgame / Travel, keeping the e…820fa12 -
Odyssey Tier-2: the Domination victory — a military win path + the Gate/fleet fork
Claude +1: Claude Opus 4.8Unverified signatureThe review found the open-world meta-layer (jump, colonies, cargo, army) was optional scenery: you could win the single-world Antimatter Gate without ever building a Spaceport, and the Leviathan/Star Dock had no purpose (it costs the same strategic goods as the Gate but there was no military win). Razing a neighbour's Command Center did nothing — a free "decapitate then turtle" exploit. This adds a second, galaxy-wide win: DOMINATION. Raze the neighbour's Command Center on DOMINATION_TARGET (4) worlds and you conquer the galaxy. A world is "pacified" the moment its AI capital falls; pacification is sticky (a rebuilt capital can't un-take it) and win-progress. This creates the game's central strategic fork — the tall, single-world economic win (the Gate) vs the wide, multi-world military campaign — and makes the jump, a mobile army, and the Leviathan base-cracker all load-bearing. - engine/galaxy.js: galaxy.pacified (a Set), checkDomination() in the stepGalaxy tail (twin of checkGal…7acc706 -
Odyssey Tier-1 "make it playable": fix the threat curve + cheap wins
Claude +1: Claude Opus 4.8Unverified signatureA gameplay review (instrumented headless runs + a critique team) found Odyssey's threat curve and win curve ~6x out of sync: war reliably began at ~minute 4 (two economies mine the shared world to the old 23%-depletion war threshold that fast, and "peace" only gated the AI's ATTACKING, not its arming), so a turtling economy player was dead by minute 6-12 — while the Antimatter Gate win needs ~25-35 min. The whole economy was, in practice, unreachable. - engine/diplomacy.js (#1, the critical fix): a 7-minute GRACE window floors the scarcity target at cordial so a fresh economy can establish, and a much gentler souring curve (war at ~47% depletion, full hostility only near total depletion, slower drift) moves the war into the mid/late game. Empirically: war onset moves from ~min 4 to ~min 8-21, and a base with a reasonable defensive core now SURVIVES the full 35-min win window across seeds (previously the CC fell by min 6-12). #2 ("pressure, not exterminate") falls out for f…e46bcce -
Odyssey Phase 3: the Strategic tier + the endgame win (Antimatter Gate)
Claude +1: Claude Opus 4.8Unverified signatureOdyssey has been endless and loss-only. Phase 3 gives it a destination: complete the chain to the Strategic tier and fire the Antimatter Gate to win the galaxy. Reconciled from a three-way brainstorm toward the skeptic's "ship the win, one clean vertical slice" — one strategic good, the wonder, and the win machinery. - engine/wonder.js (new): updateWonder — the Antimatter Gate charges by consuming antimatter over a power-throttled chargeTime (competes with your factories for the grid), clamped to stock so it stalls when starved and never goes negative. Goods are spent as fed, so a razed Gate loses the whole investment. Odyssey-only, a no-op off a `wonder` building — skirmish tick untouched. - engine/victory.js: checkEndlessWin — the twin of checkEndlessLoss; a full-charge player Gate wins. Wired into sim.js's endless branch (win-first) and the building loop (updateWonder). - engine/galaxy.js: checkGalaxyWin in the stepGalaxy tail — a Gate left charging on a background CO…15e011a -
Odyssey: perf throttling, galaxy save/load, aggression ramp, cleanups
Claude +1: Claude Opus 4.8Unverified signatureImplements the three remaining review items (each brainstormed by a subagent, then reconciled into one plan). Perf — background-world throttling (engine/galaxy.js, boot.js): - stepGalaxy() advances the whole galaxy per frame: the active world at full cadence, background colonies on a coarser fixed step (BG_STEP×dt) spread round-robin by roster index. Keyed to an integer galaxy.tick, so it's deterministic (no wall-clock, no Map-order dependence) and conserves each colony's sim time. boot.js's loop calls it instead of ticking every world every frame. Galaxy save/load (engine/persist.js, saveload.js, boot.js, setup.js, hud.js): - serializeGalaxy/deserializeGalaxy extend the existing seed+delta persistence: a shared serPlanet/rehydratePlanet core (maps regenerate from seed; entities, node amounts, fog memory, AI bookkeeping restored), plus per-world market pressure + diplomacy stance and the galaxy meta (credits, active world, tick, entity counters, the shared global id…7e2a606 -
Odyssey improvements: felt diplomacy, colony income, honest starmap
Claude +1: Claude Opus 4.8Unverified signatureFollow-up on a review of the Odyssey mode — the three highest-impact fixes, so the living-galaxy systems actually manifest in play. Diplomacy now lands within a session (engine/diplomacy.js): - Sharper scarcity curve (full → +0.45, ~30% mined out → hostile) and a faster drift, so a neighbour measurably sours as a world depletes — reaching Wary/war around the mid-game instead of effectively never (measured: still Cordial at 10 min before; Wary by ~10 min now, and faster once the player is also mining). - Player aggression is a second driver: every enemy ship you destroy on a world drops the stance at once, read from the change in the neighbour's unit count so catch-up substeps can't double-count. You can now break the peace yourself, and forgiveness comes only from the drift back. Colonies are assets, not husks (engine/galaxy.js): - A held background colony sends home passive income each tick (COLONY_INCOME_PER_BUILDING per surviving player building), so the worlds y…b1db01d
-
Odyssey Phase 4: living galaxy — scarcity diplomacy + colony alerts
Claude +1: Claude Opus 4.8Unverified signatureThe neighbours are no longer just obstacles: a world can be shared in harmony and then turn on you as its ground runs out, and the colonies you leave behind report in when they come under fire. Engine: - diplomacy.js (new): each Odyssey world's AI holds a stance (-1 hostile .. +1 allied) that drifts toward a target set by how depleted the world's deposits are — cordial while rich, hostile once mined out. - ai.js: the offense commit is gated on the stance — at peace the neighbour keeps building economy and army and still DEFENDS itself, it just launches no attacks on you until scarcity turns it hostile. A skirmish has no diplomacy, so the gate is a no-op there (the resolves-to-a-winner guarantee is untouched). - galaxy.js: every world gets a diplomacy stance; sweepColonies() watches the background colonies — reporting one coming under attack or being lost — and is the single place their sim events are drained (a colony isn't rendered, so nothing else consumes them). -…11037b3 -
Odyssey Phase 2: the galaxy + the interplanetary jump
Claude +1: Claude Opus 4.8Unverified signatureThe signature open-world loop. You now build a Spaceport, relocate your single Command Center (and the units staged by the pad) to another world, and the planet you leave keeps evolving as a background colony while you develop the new one. Engine: - galaxy.js: the meta-layer grows a real galaxy. addPlanet() gains an `unsettled` option that strips the auto-seeded player base — a jump destination has only its neighbour until your capital arrives. jumpCapital() moves the Command Center + player units within JUMP_LOAD_RADIUS of the Spaceport to the destination's landing zone (assigning fresh galaxy-scoped ids so nothing collides across states), flips the origin to a background colony, and makes the destination the active seat. canJump() gates it on a finished Spaceport. - sim.js: a background colony (no capital, by design) is never "over" — only your active seat runs the capital-loss check. - entities.js: the Spaceport building (Odyssey's launch pad, gated behind the Fou…5df8767 -
Add Odyssey open-world mode (Phase 1): endless single-planet foundation
Claude +1: Claude Opus 4.8Unverified signatureThe first slice of the "Space Exploration" meta-game — the open-world campaign the project is named for. Phase 1 stands up the galaxy scaffold and a playable, endless single-planet sandbox with one relocatable Command Center; the multi-planet jump, market, and diplomacy build on this in later phases. Engine: - state.js: new `endless` flag on a game state. - victory.js: checkEndlessLoss() — an Odyssey never ends by conquest or clock; it only ends in defeat when the player loses their single Command Center (their capital). Razing a neighbour's CC just leaves that world running. - sim.js: tick() routes the terminal check three ways now — scenarios settle themselves, endless uses checkEndlessLoss, a normal match uses checkWinCondition. - engine/galaxy.js (new): the meta-layer. createGalaxy() picks a random start world from the galaxy seed and builds its planet state (a normal createGameState flagged endless); holds universal `credits`, the active planet id, the world roste…cae0952 -
Offer all map sizes (Small–Gigantic) in the three scenarios
Claude +1: Claude Opus 4.8Unverified signatureThe Map size dial was skirmish-only; the scenarios always ran on the base (Small) map. Enable Small/Standard/Large/Gigantic for Convoy Escort, Pirate Raider, and Bounty Marshal too. - setup.js moves the Map size row out of the skirmish-only block so it renders in every mode; Faction and Resources stay skirmish-only (a scenario has no economy). SCENARIO_START already maps each mode to its boot function. - boot.js threads setup.sizeMult into startScenario/startRaider/ startBounty. - engine/scenarios.js: setupEscort/setupRaider/setupBounty take a sizeMult, pass it to createGameState (the map generates at that scale), and scale the mission clock by it. This is the crux: the route and the camp scatter already use width/height fractions, so they grow with the map on their own, but the fixed time limits would make a big map unwinnable — a 4x map is a 4x-longer convoy route. Since a convoy scenario ends when the convoy arrives (the clock is only the failure deadline) and…8c580c4 -
Add Bounty Marshal scenario: hunt scattered pirate camps on the clock
Claude +1: Claude Opus 4.8Unverified signatureThe third scenario, a seek-and-destroy hunt rather than a convoy run. Pirate camps are scattered across the sector, each marked with its bounty; the player leads a marshal's posse and must clear a quota of camps before the clock runs out. Scenario framework (engine/scenarios.js): - updateScenario() dispatches to updateBounty() on scenario.type. There is no route or convoy: the camps sit and defend (AI combat units auto-acquire anything that strays into range), so the player picks their targets and their order. With no input the clock simply expires with nothing cleared, which also guarantees the mission terminates. - setupBounty() musters the posse at the map centre and scatters the camps around it via a seeded, well-separated placement (scatterPoint), each camp's pirates tagged with a packId. BOUNTY_DIFFICULTY scales the posse, camp count/composition, clear quota, and clock. - updateBounty() recomputes each camp's live centroid every tick (so its marker tracks the pac…3a48732 -
Add Pirate Raider scenario: raid the AI convoy before it escapes
Claude +1: Claude Opus 4.8Unverified signatureThe mirror of Convoy Escort, built on the same scenario framework. The roles flip: an AI convoy (four freighters + an escort) runs a multi-leg route toward the destination gate under its own control, and the player commands a raider fleet that lies in wait off the lane. Sink a difficulty-scaled kill quota before the convoy escapes or the mission clock runs out. Scenario framework generalization (engine/scenarios.js): - updateScenario() is now a dispatcher on scenario.type; the existing escort logic moves into updateEscort(), the new updateRaider() drives the AI convoy and settles the raider objective. - liveFreighters() reads scenario.freighterOwner ("player" in Escort, "ai" in Raider) so the shared route/arrival/delivery logic serves both. - endScenario() branches its score by type (computeEscortScore vs the new computeRaiderScore, which rewards freighters sunk, escorts destroyed, raiders left alive, and a win bonus). Raider specifics: - setupRaider() lays out the AI convo…11d7a22 -
Add Convoy Escort scenario: shepherd freighters across a raided route
Claude +1: Claude Opus 4.8Unverified signatureIntroduces a scenario layer over the existing skirmish engine. The Convoy Escort scenario tasks the player with shepherding four freighters along a multi-leg route through pirate territory to a destination gate, against a global mission clock. Scenario framework (engine/scenarios.js, new): - setupEscort() reuses createGameState for map/fog/players, then clears units+buildings and lays out an evenly-spaced left-to-right route of stations (one leg per configured risk value), the four freighters, and a difficulty-scaled escort fleet. - updateScenario() drives a prep -> travel -> docked phase machine: freighters advance leg by leg, pirate waves spawn per-leg by risk and steer at the lead freighter, and the player docks between legs to repair from a budget. Win on delivering >=1 freighter before the clock; loss on losing them all or the clock expiring undelivered. - Determinism preserved: scenario randomness flows through a seeded mulberry32 on state.scenario.rng, decoupled…dcbdeac -
Refactor: split the main.js god-file into focused modules
Claude +1: Claude Opus 4.8Unverified signaturePure code-health, behavior-preserving: main.js goes from 1001 lines to a 117-line composition root, with its concerns extracted into cohesive modules: session.js — the live { state, input, supplyBlockedUntil } holder that boot reassigns and every UI module reads (was the main.js closure vars). dom.js — every getElementById handle + MINIMAP_W/H + isTouchMode(), once. setup.js — the splash / map-select screen and the shared `setup` config. boot.js — startGame, bootState, the fixed-timestep loop, the frame-event sound/effects pump, and the under-attack alert. hud.js — renderHUD, the selection panel (rebuild + live patch), the button / tooltip factories and formatters. overlays.js — seed + faction chips, the objectives strip, the help overlay, and the game-over screen. saveload.js — localStorage save / load. main.js — imports the graph (each module self-wires its own listeners), owns …d32ddf2