Spaces:
Paused
Paused
Commit History
-
Fix the three engine defects the 3D client reported (#92, #93, #94)
Claude +1: Claude Opus 5Unverified signatureAll three were found from outside — alma92350/3D-SpaceExploration-RTS vendors engine/ byte-for-byte — and filed unverified against this repo's suite, because that session could not clone it. All three reproduce here. The full suite is green (2519), typecheck clean, and each fix carries a test that fails without it. #92 — a world fought over did not round-trip its market prices. createMarket derives every raw commodity's price from its share of ALL nodes, and runs exactly twice: at creation, and on load. Wreck/crater nodes are pushed onto the map DURING play and the loader restores them before recomputing, so the reload derived the book from a bigger node set than the live one. Since the share is a fraction of the total, every raw commodity moved, not just the debris's own. Skip wreck and crater nodes — the same exclusion worldHasCommodity already applies. No live behaviour change (there is no debris at creation) and no save-format change. #93 — galaxyStatus reported income 0 for a co…96c6d29
-
Get CI green, and make the AI's opponent belief fade the way it says it does
Claude +1: Claude Opus 4.8Unverified signatureActs on the first three findings of docs/code-review-2026-08.md. CI has been red since 2026-08-05 and two PRs merged through it. Two causes, both fixed here: - competitionLedger.js had two wrong JSDoc signatures. addRosterEntry never declared the `genome` field its body reads and writes (the RosterEntry typedef omitted it too), and recordCompetition typed its match rows as bare `object`, so every row.aName / row.winner / row.margin read in the file was an error the moment a checker looked. Adds a real CompetitionResultRow typedef and narrows the one ternary TypeScript was widening to `string`. - CI installed TypeScript unpinned via `npx --package typescript`, so the same tree reported 2 errors under 5.7 and 21 under 7.0.2 — a green commit could go red overnight with no code change. The invocation is pinned to 5.7.2; the compiler still stays out of package.json, it is just no longer a moving target. Verified clean on both 5.7.2 and latest. The third is a real behavi…15f3a03 -
Phases 2-3: the AI answers what it reads
Claude +1: Claude Opus 5Unverified signaturePHASE 2 -- the raid stops being a metronome. It fired on `waveCount % RAID_EVERY` and only on Hard: harassment on a schedule, regardless of whether harassment was the right answer. It now ALSO fires when the read says the enemy is visibly investing in economy over defence and enough of them has been scouted to act on it. That is harassment as a response: punish greed, and specifically punish the hoarder docs/ai-evolution-design.md found winning by never committing its army. An opponent that can be SEEN to be undefended should not be safe -- which removes that degenerate strategy structurally instead of penalising turtling inside a fitness function, where evolution would just route around it. The read is deliberately not gated on controller.micro. Reacting to what you scouted is ordinary competence, not the unit-level micro Hard buys, and an Easy/Medium AI that never once punishes a greedy opening is the passivity the bench keeps measuring. PHASE 3 -- a damped stance. posture is far t…5d60e36 -
Phase 1: the AI gets a belief about its opponent
Claude +1: Claude Opus 5Unverified signatureengine/aiIntel.js. Every other reactive read in the AI answers a question about right now -- are there enemies at my base, how many can I see, what do they field most -- and none of them survives losing sight of the enemy, because entity sighting goes through the LIVE fog layer. Kill the scout and the AI's picture of its opponent is empty, and it has no way to know it. This is the other kind of knowledge: what sort of game the opponent is playing. It persists between sightings and decays when unrefreshed. It is the first piece of AI state that can be WRONG -- everything else is a fact about the current frame -- and that is the point. An opponent model you can deceive is what makes scouting worth its cost and killing the scout a real counter-play. Three numbers, and they are three for a reason: mil / eco ore-value of what has been SEEN, split by whether it shoots posture mil / (mil + eco): 0 is a pure economy game, 1 is a pure war game confidence how much has been see…2991d58
-
Tier 2b: four correctness fixes with a wider blast radius
Claude +1: Claude Opus 5Unverified signatureA yield-multiplied deposit could overfill a collection-point hold. The clamp was applied to the RAW quantity moved and the bonus to the CREDITED quantity, so a PARTIAL deposit landed more than fits — reproduced at 251 into a 250 hold at the Logistics Network's 1.25x. It breaks the hold's capacity and "a save round-trips identically" at once, because persist.js truncates the excess on load: continuing a game stopped being the same as saving and continuing it. That second half is invisible to every replay test, since both runs agree until someone saves. haul.js's own depositToFreighter never had the asymmetry — it moves and credits the same number — and gather.js now does the same. garrisonSlots' corrective pull-back never converged. Its header claims it "keeps the WHOLE parked group, not just its middle, inside the radius", but the anchor is pulled back RADIALLY while the farthest slot is an off-axis grid CORNER, so max|slot - cc| is not a linear function of the ring radius and one pas…2b50b54 -
Tier 1, step 3: harden the save load path past the entity boundary
Claude +1: Claude Opus 5Unverified signaturecleanEntity coerces ~20 per-field cases well, and that discipline stopped exactly at the entity boundary. Three whole sub-objects reachable from an Odyssey save were written into live state verbatim, two lane invariants the runtime enforces were dropped on load, and one field was never persisted at all. Sixteen new tests, every one verified red first. A5 — galaxy.rivalAscended, the idempotency latch the code names as such, was never written to the payload (zero hits in persist.js, zero across test/). Continuing a campaign silently undid a Rival Gate ascension: with the Gate razed the "permanent" stance ceiling could never be re-applied, because checkRivalGate can only re-latch by finding a completed Gate; with it standing, the ascension event re-fired on every Continue. Autosave runs every 12 s. Additive, so no GALAXY_SAVE_VERSION bump. B1 — the diplomacy block was `{ ...createDiplomacy(), ...P.diplomacy }`, the untrusted object winning every key. engine/diplomacy.js's clamp() is the…0974cb1 -
Tier 1, step 2b: expand // @ts-check coverage from 10 files to 20 (D3)
Claude +1: Claude Opus 5Unverified signatureAdopted in the order docs/code-improvement-tiers.md sets out, cheapest and highest bug-class value first: supply, aiCommon, colliders, victory, production, scout, the three AI leaves (aiStrategy, aiDifficulty, aiArchetypes), and finally persist.js — the prize, because every save field is a State/Unit/Building field read by name, so annotating it makes save-shape drift a check-time error. Each file's exports are annotated with the shared typedefs, not just given the pragma: per the guard added in the previous commit, a pragma without annotations checks nothing. Doing it surfaced three more genuine drifts in engine/types.js, all found by the checker rather than by reading: - Building.rally was typed {x, y} but carries nodeId — the rally-to-resource feature, where a rally dropped on a live node makes a produced unit spawn already mining (engine/production.js). - Building.queue entries were typed {unitType, progress} but carry `alt`, which records that a job was charged the unit's…7e764b0
-
Make the AI controller owner-parametric: true two-sided self-play
Claude +1: Claude Sonnet 5Unverified signaturerunAI(state, dt, owner = "ai") and every AI phase module now resolve their controller, opponent, and fog grid from `owner` instead of hardcoding "ai"/ state.fogAI — 100% backward compatible (every pre-existing call site keeps the default and behaves byte-for-byte as before). A new, independently configured controller slot (state.playerAi, null by default) lets a second real AI drive the "player" seat in a skirmish, headless-only (tools/ selfplay.js) — nothing in setup.js/boot.js/the shipped game changed. Independent adversarial review (fairness lens + correctness lens) caught a real bug before this was accepted: Hard difficulty's economic edge (hardEdge) was seeded onto state.players.ai only, never a self-play "player" controller configured for Hard. Fixed via seedDifficultyEdge (engine/state.js), called for "player" once state.playerAi exists. engine/persist.js's save/load extension is purely additive (no SAVE_VERSION bump) — old saves round-trip unaffected. Co-Authored-By: Claude …916c3ad
-
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
-
P8 Combat Depth: uphill concealment (real LOS), veterancy ranks, tiered-kill event fields
Claude +1: Claude Sonnet 5Unverified signatureThree combat.js-anchored proposals from Phase 8 of the improvement roadmap, landed as one coherent pass through engine/fog.js and engine/combat.js since a later team's fog-render-perf work depends on this cell/fog-grid contract: 1. Real LOS pass (docs/improvement-proposals.md lines 831-839): fog.js's reveal() now takes the source's own terrain (one sampleTerrain call per source) and caps how far a NON-high-ground source reveals a high-ground CELL at UPHILL_FRAC (0.55) of its normal sight radius — a mesa is a stat pad AND a genuine blind spot for anyone still in the lowland. combat.js's nearestEnemy/spreadEnemy apply the identical rule to acquisition via a shared highGroundCap helper, so a unit can't shell a held ridge blind just because its raw aggro happens to reach that far. Both fog grids (state.fog, state.fogAI) inherit this through the same updateFog/reveal path — no AI-side code needed, verified with a dedicated symmetry test. 2. Veterancy ranks (line…8f89fac -
Domination with teeth + Faction memory, tuned as one stance-pipeline system
Claude +1: Claude Sonnet 5Unverified signatureTwo proposals that rewrite the same checkDomination/updateDiplomacy pipeline, shipped together per the roadmap's explicit call ("ship together as one stance-pipeline change"). Domination with teeth (docs/improvement-proposals.md 657-665): checkDomination now stamps state.diplomacy.pacified = true on a freshly-razed world (it already holds that state in its loop). updateDiplomacy floors that world's drift target at APPEASE_FLOOR (Neutral) permanently, placed AFTER the existing Gate-finale clause so pacifying a world is specifically what exempts it from finale mobilization — conquest becomes the military counter to your own Gate's provocation. deserializeGalaxy re-stamps the flag on load for saves that already carry galaxy.pacified from before this flag existed. Faction memory (docs/improvement-proposals.md 519-527): pacifying a world now echoes a bounded, one-time stance penalty (FACTION_ECHO_PENALTY) onto every OTHER unpacified world of the same faction (matched by claim or AI factio…051d7f7 -
Revert accidental inclusion of concurrent team's in-progress work
Claude +1: Claude Sonnet 5Unverified signatureThe previous commit's `git add` swept in engine/diplomacy.js, engine/galaxy.js (the domination/faction-memory hunks), engine/persist.js, engine/types.js, test/diplomacy.test.js, and test/domination.test.js — all in-progress, uncommitted work from the concurrently-running P7-Domination-Faction-Memory team, not mine to commit. This restores those six files to their prior (pre-that-commit) content; their own working-tree changes are reapplied on top afterward, unaffected, for that team to commit under its own history. No functional change to the rival-Gate feature itself. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>40dc0e6 -
Extend determinism coverage to the rival Gate's charge
Claude +1: Claude Sonnet 5Unverified signaturetest/_helpers.js's entitySnapshot now folds a wonder's `charge` into its per-building fingerprint tuple, so a charge-path float divergence (player's or a rival's) actually surfaces in a determinism comparison instead of only in the coarser rounded-count galaxy-persist checks. New determinism-roster case: two galaxies from the same seed, each with an AI-owned rival Gate mid-charge on a background world, replay byte-identical through stepGalaxy's real background-world schedule. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>2f96159 -
Colony economy rework: Freight Lanes + Colony standing orders
Claude +1: Claude Sonnet 5Unverified signatureThe roadmap calls these "two halves of a single colony-economy rework" and they share the same background-scheduler plumbing in stepGalaxy/persist.js, so they land together as designed, even though each is its own mechanic: Freight Lanes (engine/galaxy.js): a galaxy.lanes array. At a Spaceport, assign parked freighters to a LANE (source -> destination, commodity filter) via createLane/assignShipToLane. Every LANE_PERIOD galaxy ticks, runLanes moves min(assigned cargoHold, filtered stock) straight from the source world's treasury to the destination's — a standing investment, not free teleportation: assigned ships are revalidated every cycle (still alive, still parked within JUMP_LOAD_RADIUS of a source Spaceport) and are excluded from stagedRiders/jumpManifestAll so they never get swept off on a jump. UI: a lane-management panel on the Spaceport selection panel (hudSelection.js) plus a read-only summary on the starmap. Colony standing orders (new engine/colonyPolicy.js): a per-planet …84220dc -
Doctrine depth redesign: Assault tempo verbs + Bulwark regen (Tier 2 & 3, merged)
Claude +1: Claude Sonnet 5Unverified signatureMerges the two related doctrine-depth proposals (docs/improvement-proposals.md lines 217-225 and 433-441) into one coherent redesign per Phase 5 of docs/improvement-roadmap.md, instead of shipping them as separate tickets. Assault: overchargedCore (Tier 2) now also grants chaseSpeedMult 1.1 ("combat drive" - closes on an acquired target ~10% faster, applied only in updateCombat's chase branch). A new Arsenal-gated capstone, Overdrive Actuators (Tier 3, requires ["overchargedCore","arsenal"]), grants attackCooldownMult 0.9 - a distinct tempo identity, applied at both cooldown- reset sites (updateCombat's main attack path and updateWorkerCombat), but deliberately NOT at updateBuildingCombat's - this is the army's tempo upgrade, not a base-defense buff. Bulwark: THE RECONCILIATION - proposal 1's Tier-2 grant and proposal 2's Tier-3 capstone both independently proposed "out-of-combat hp regen" as Bulwark's identity. Rather than ship two separately-tracked regen fields, this is one mechan…f0783f6 -
Validate a loaded favor request's shape, like cargo/resources already are
Claude +1: Claude Sonnet 5Unverified signaturedip.request (engine/diplomacy.js, previous commit) carries a commodity id and three numbers straight off untrusted save data. This file already has a standing rule for exactly that shape — cargo.com, per-building resource maps — validated against the real commodity table before anything reads it dynamically (`res[req.com] -= req.qty` in fulfillRequest). Extend the same rule here: an invalid or corrupt request (unknown commodity, a non-positive/non-finite qty, a non-finite reward or deadline) is dropped on load rather than trusted verbatim, exactly like a malformed cargo entry is nulled a few lines above. A well-formed request round-trips untouched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>e4f4a6b -
Range-layered formation ranks and per-building logistics priority
Claude +1: Claude Sonnet 5Unverified signatureTwo P3-Combat-Logistics proposals sharing engine/commands.js, landed together per the roadmap's own grouping. Range-layered formation ranks (brawlers screen, artillery trails): dispatchFormation now re-pairs a shaped (line/wedge/circle) formation's followers with their slots by weapon range instead of raw selection-array order — shortest-ranged units (Bastion, Skiff) take the most forward slots, longest-ranged (Lancer, Breacher, Colossus) the rearmost, via a new rankSlotsByRange helper. Ranking uses each slot's forwardness (projection onto the formation's own travel heading), computed by a new resolveHeading export in engine/formation.js — factored out of formationSlots' existing heading-resolution logic (including its near-zero-heading fallback to facing +x, which matters for issueHoldFormation's origin===destination case) so the two never drift apart. The legacy grid shape and the AI's plain (no-formation) path are both untouched — reordering only ever runs inside the player leader/…b383918 -
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 -
Let the player pick their side of asymmetric matchups (Oort, Nimbus)
Claude +1: Claude Sonnet 5Unverified signaturegenerateMap accepts opts.swapAsym: when set and the world carries an asym block, it attaches a shallow COPY of modifiers with the player/ai asym halves exchanged. Copying (not mutating) matters because generateMap otherwise attaches the shared PLANET_MODIFIERS[planetId] object by reference — every game on that world reads it, so mutating it in place would corrupt the next game. Swapping consumes no rng draw, so map layout/node generation is byte-identical either way. Threaded through createGameState (a new opts.swapAsym, defaulting false) and persisted as an additive field next to sizeMult/ resourceMult in engine/persist.js's serPlanet/rehydratePlanet — no SAVE_VERSION bump, an old save without the field loads as unswapped. engine/types.js's State typedef gained the new field. Tests: engine/map.js's opts.swapAsym behavior (exchange, no mutation of the shared table, no-op on a world with no asym block, no rng/ node-layout perturbation) in test/map.test.js; the flag's persistence + reg…8338353 -
Doctrine research develops over time instead of landing instantly
Claude +1: Claude Sonnet 5Unverified signatureGeneralizes engine/techtree.js's updateResearch to resolve its node table by building type (datacenter -> TECHS, refinery -> UPGRADES) instead of being hardcoded to the Datacenter, so the Refinery's doctrine upgrades now run through the same queued/timed development loop as the Odyssey tech tree. production.js's researchUpgrade becomes an enqueue: it pays up front, keeps the committedDoctrine lock and prereqsMet check, and adds a queued-dupe guard, instead of flagging the upgrade researched the instant it's paid for. Each UPGRADES entry gets a ~25-40s `time` field, scaled by researchTimeScale like every TECHS node, so a skirmish world's own tech rating finally matters for doctrine research too. Because the upgrade no longer lands in player.upgrades until it finishes developing, entities.js's committedDoctrine now also commits from a upgrade that's merely queued (paid for, still developing) at a Refinery — otherwise both doctrines could sit queued side-by-side during the development wi…4b95948
-
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 -
Add player-assigned home base override and worker repair of wounded units
Claude +1: Claude Sonnet 5Unverified signatureCloses the two items flagged as future work in the previous zone-affinity commit: a home-base override, and repairing mobile units. engine/gather.js zoneFirst now takes an optional homeId that, when it resolves to a live owned Command Center, wins over the usual nearest-CC distance guess. Right-clicking a completed own CC with eligible units selected (worker, Mender, freighter) pins unit.homeCC to it (issueSetHomeBase) — a passive assignment that only changes the unit's next idle job search, never its current order. A stale override (its CC destroyed) is ignored and falls back to the distance guess. Threaded through every zone-first call site in haul.js and repair.js's pickRepairTarget, so it covers haul/service/ferry job search and both the worker repair job and the Mender's own autoRepairRoam. assignRepair no longer restricts itself to buildings: a worker can now be assigned to chase down and patch a wounded mobile unit the same way it patches a turret, walking toward the target's …06de929 -
Add Command Center zone affinity for haulers, plus a worker repair job
Claude +1: Claude Sonnet 5Unverified signatureHaulers/servers/ferriers used to pick jobs by raw nearest-distance across the whole empire, with no notion of "which base do I belong to." Once a busy base's own ≤2-per-target caps filled up, idle labour got routinely poached across the map by another base's job. engine/gather.js zoneFirst now restricts each search to the worker's own Command Center zone first, only widening to the whole empire when the home zone has nothing to offer (byte-identical to before on any single-CC game). The auto-repair Mender's roam target-pick adopts the same zone-first logic via a shared pickRepairTarget helper (engine/repair.js). Buildings could previously only be healed passively by a Mender in range. Workers can now be assigned a repair job (auto, zone-first, after ferry/haul/service in priority; or manual via right-click, issueRepairBuilding), patching a damaged own building at a flat rate, capped per building like the existing haul/service caps. Both changes are pure functions of state and reused …32d2559
-
Implement Phase 4 of battle wreckage: growing battles + deposit gating
ClaudeUnverified signatureA new contribution to an already-pending wreck site now refreshes its spawnAt (a raging fight over the same ground keeps enriching the eventual deposit), capped at WRECK_MAX_DELAY total from the site's first death so it can't defer maturation forever. The 80% base return is now gated to commodities this world actually has (isNaturalDeposit) -- a genuine map-generated node, never a wreck or crater node itself, so one slipped-through foreign commodity can't unlock itself for every kill after it. Battle-intensity value still counts everything regardless of local availability, and the Phase-2 bonus is deliberately left ungated (it's meant to synthesize something that wouldn't otherwise be here). This protects aiWorkers.js's "surface commodity set is constant for the match" assumption, and as a real consequence, Strategic-tier costs (ai/antimatter/plasmatorp) are now only ever recoverable through the capped bonus roll, never the base return, on any world.865aea3 -
Implement Phase 2 of battle wreckage: battle-intensity bonus materials
ClaudeUnverified signatureA wreck site now tracks the un-scaled sum of every contributing death's cost (site.value). Once that clears WRECK_BONUS_THRESHOLD, the site forges a modest, capped amount of one higher-tier commodity (metals, electronics, or relics) on top of its normal 80% base return -- bigger consolidated battles have a real shot at denser materials a raw unit's own cost never pays in. Deterministic (same hashStr technique the crater's own commodity roll uses), and persisted so a save/load mid-battle can't reset a site's bonus-eligibility progress.b4afb1c -
Implement Phase 1 of battle wreckage: kills leave minable wreck sites
ClaudeUnverified signatureEvery unit, worker, or building destroyed in combat (engine/combat.js) or by a Helium Bomb blast (engine/bomb.js) now deposits ~80% of its own construction cost into a nearby wreck site (engine/wreckage.js), which matures into real ResourceNodes after a delay -- one node per commodity, reusing the crater's pending-timer -> mature-into-node pipeline. Nearby deaths consolidate into a single site instead of scattering tiny deposits. This replaces the old instant, combat-unit-only 25% salvage refund (grantSalvage/SALVAGE_FRAC), which gave workers and buildings nothing. Wreckage is neutral, like every other resource node -- no ownership gate. See docs/battle-wreckage-design.md for the full design and remaining phases.3620efb
-
Give each factory input commodity its own larder slice instead of one shared pool
ClaudeUnverified signatureA multi-input recipe (Chip Fab: crystals+metals; Torpedo Works: antimatter+ alloys+radioactives; ...) shared ONE pooled input buffer across every real input commodity. Workers over-delivering whichever commodity the treasury happened to have plenty of could fill that whole pool on their own, leaving zero room for whichever OTHER input the recipe was actually starved of — haul.js's inputRoom check always read 0 for it, so it could never be hauled in and the factory stalled forever on an input it could never receive. inputCapOf now splits the larder evenly across a recipe's real (non-energy) inputs, so each gets its own guaranteed slice regardless of how much of another has been delivered. Updated every inputRoom call site to check room per-commodity, the HUD's Larder readout to show each commodity against its own slice, and the save-load clamp to clamp each commodity independently instead of sum-clamping the whole buffer.18d5122 -
Let the player recycle a completed building or unit for part of its cost back
ClaudeUnverified signatureSelect any owned, completed building or unit and Recycle it: it stands down in place over roughly half its build time (freely cancellable, no penalty) and refunds 40% of its cost, or 50% for pricier industrial/capital assets (a factory, the Plasma Rig, a Reactor/Generator, a freighter, a Mender) — boosted to 70%/80% (capped) by a new research node in either tech tree (Field Recycling, Logistics doctrine tier 3 in skirmish; Reclamation Engineering at the Datacenter in Odyssey). A Command Center can't be recycled. Persists across save/load, with save-hardening against a tampered file.79c7af7 -
Let idle workers auto-discover a collection-point freighter, and stop ferrying it home themselves
ClaudeUnverified signatureIdle workers now offer to ferry any nearby COLLECTION-POINT freighter with room and a producer worth fetching from (assignFerry), checked ahead of haul/service so a busy rig's hauler slots don't all go to CC-bound workers first. A ferry worker on a collection-point freighter only tops it up now — the freighter's own shuttle run owns the trip home, instead of the worker carrying cargo back to the CC itself. Also fixes nearestBacklogProducer's minFraction=0 path (used by assignShuttle and a ferry worker's replan) treating a fully-drained producer as still "worth waiting for", since storeTotal(b) < cap*0 is never true at exactly zero — this silently blocked a collection-point freighter's last partial load from ever shipping once its feeding producer ran dry.5e0b657 -
Let workers use a freighter as a collection point, with self-shuttling when full
Claude +1: Claude Sonnet 5Unverified signatureWorkers can already ferry cargo to/from a landed freighter. This adds a Collection Point toggle (no research needed): once a freighter's hold is full, it drives itself to the nearest Command Center, banks its whole hold, and returns to the spot it was anchored at to keep collecting -- so a freighter parked at a remote claim can act as a self-emptying forward depot instead of needing a worker to carry its load home too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdsYZQyGk32n8x3UBiPGup7a847ac -
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 -
Redefine formation leader as first-selected unit, add persistent follow, speed cap, and drag-to-face
Claude +1: Claude Sonnet 5Unverified signatureReworks the formation system per feedback on PR #9: - Leader is now whichever unit the player selected first (the one others get added to), not a stat-based pick. pickLeader() and layout() in engine/formation.js simply use array order instead of scanning for highest maxHp. - Selecting and moving just the leader now drags along everyone previously grouped with it, with no need to reselect the group. A new persistent "follow-leader" order (engine/commands.js, engine/movement.js, engine/sim.js, engine/combat.js) mirrors the existing escort-order pattern: followers re-seek the leader's live position every tick via object references (unit.squadLeader / leader.squadFollowers), set up by dispatchFormation() and torn down by any direct order that bypasses it (stop, hold, scout, build). - A formation's travel speed is capped to its slowest member so a fast leader can't stretch the group out of shape. dispatchFormation() computes the cap once at issue time and stores i…6648f62 -
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 -
Persistence: fix docs contradicting the code, document a hazard, add dialog a11y
Claude +1: Claude Sonnet 5Unverified signature- CONTRIBUTING.md told contributors to make the loader tolerant of older saves; README.md had two passages that disagreed with each other, one correctly saying an incompatible save is rejected, the other claiming loaders stay "backward-tolerant." The actual code (deserializeGame/deserializeGalaxy) does an exact version-match throw, no migration, no tolerance at all. Rewrote all three passages to describe what the code actually does, and to explain when a version bump is (and isn't) needed. - peekEntityId/restoreEntityId touch a module-global counter in engine/state.js from four call sites in persist.js, with no comment in persist.js itself flagging the shared-mutable-state hazard - the only place this was documented was a test comment elsewhere. Added a clear comment at the import site. - saveload.js's goHome() hand-built confirmation modal (gates real data loss) had no role="dialog", no aria-modal, and no focus management. Added both, plus focus-into-dialog-o…b6109dc -
Harden save/load: sanitize sizeMult/resourceMult and player economy
Claude +1: Claude Sonnet 5Unverified signatureTwo load-time gaps in the otherwise-paranoid persist.js: rehydratePlanet passed a save's sizeMult/resourceMult straight into generateMap() with no sanitization (map.js's `opts.sizeMult || 1` only catches falsy values, so a NaN or numeric string sails through and produces a NaN-sized map, which then defeats every coordinate clamp cleanEntity performs elsewhere), and player resources/upgrades round-tripped with no validation at all, unlike every entity buffer. Sanitize sizeMult/resourceMult once (finite, positive, floored at 0.1, defaulting to 1) and reuse the same value for both map generation and the state assignment, so the two can never disagree. Add cleanPlayer(), mirroring coerceBuffer's style, to keep only real commodity/tech/upgrade ids with sane values. Both fixes live in rehydratePlanet, so skirmish and Odyssey saves get them for free. New adversarial tests in save-hardening.test.js cover both gaps plus identity round-trips for valid values; the existing byte-identical round-t…e738400 -
Two-owner scaffold: structural generalization (owner-generic, 1v1 byte-identical)
Claude +1: Claude Opus 4.8Unverified signatureThe `state.players = {player, ai}` scaffold was hardcoded across the engine — two named literals for the player map, two `createFog` calls, two `seedPlayer` calls, two `updateFog` calls, a two-branch victory check, and the save serialize/rehydrate. Combat, movement, separation and the spatial grid were already owner-generic (`owner !== self`); only the scaffold named the two sides by hand, so adding a side meant a sweep across ~15 files. Generalize the scaffold to ITERATE `state.owners` instead of naming the pair: - state.js: an `ownerDefs` list builds `state.owners`, `state.players` and a per-owner `state.fogs` map by loop; `state.fog` / `state.fogAI` become aliases into `state.fogs` so the many existing fog consumers keep working unchanged; seeding and initial fog run as loops over `owners`. - victory.js: `checkWinCondition` reads last-side-standing over `state.owners` (one side left ⇒ it wins; none ⇒ score); `scoreLeader` picks the highest score with a first-listed tie-b…b2772b8 -
Address remaining review defects: B3–B6 + load-hardening + static checks
Claude +1: Claude Opus 4.8Unverified signatureA fanned-out team of agents fixed the remaining SAFE items from the architecture review, partitioned by file so they never collided; verified and committed centrally. All identity for valid data (guarded on field presence/range), so determinism + round-trip guarantees hold — behavior changes only for corrupt/tampered/NaN input. Load hardening (engine/persist.js, test/save-hardening.test.js): - B3: clamp a wonder's `charge` to [0,1] on load — a hand-edited charge:1 no longer trips an instant standalone-endless win (victory.js). - B4: recompute the galaxy `g`-id counter (`entitySeq`) from live g-ids across all worlds on load, mirroring maxOwnEntityId for the u/b counter — a low/missing entitySeq can no longer mint a colliding g-id over an in-flight colony ship / freighter / relief ship. - B5: coerce untrusted order/orderQueue coords (finite, in-map) and worker cargo (finite qty, real commodity) so a tampered save can't NaN-poison the movement/gather math. - Non-array `researchQu…fdea120 -
Fix B1 + B2 from the architecture review
Claude +1: Claude Opus 4.8Unverified signatureB1 — faction expansion could flip the AI faction of the world the player is ON. checkExpansion picks the nearest UNCLAIMED world and sets its AI's faction to the expander's; faction feeds live combat/economy multipliers (factions.js → sideMod, read on every hit and haul), and there was no activeId exclusion — so a distant faction "expanding" into the player's current world silently changed the enemy's damage/sight/ speed/gather mid-battle, with only a starmap toast. Now expansion skips the active world and any world the player holds a foothold on; it only reaches the unclaimed, unheld fringe. (engine/galaxy.js checkExpansion.) B2 — a corrupt production queue bricked the game. updateProductionQueue did `UNITS[job.unitType].buildTime` with no guard, so a bogus unitType (localStorage bit-rot or a hand-edited save) threw on the first tick — inside the rAF loop, past load's try/catch — and since it's the autosave, every reload re-crashed. Two-layer fix: • runtime guard in updateProductio…785c4de
-
Living galaxy (7/N): factions expand across planets + starmap territory
Claude +1: Claude Opus 4.8Unverified signatureThe payoff of the living galaxy: AI factions now spread between worlds. A new galaxy-level faction-territory model (checkExpansion, on the throttled galaxy scan): - SELF-CLAIM: a world whose AI has developed past CLAIM_DEV flies its own faction's flag. - EXPAND: the single most-developed claimed world (past EXPAND_DEV) colonises the NEAREST still-unclaimed world, claiming it AND flipping that world's AI to its colours — so a thriving faction absorbs the fringe and its territory grows across the starmap over a long game. One cross-planet claim per scan keeps the spread gradual; it's paced by how fast worlds develop (minutes) and converges once every world is claimed. Deterministic: development is an entity count, distance is fixed planet-x, order is the fixed roster, ties break by world id — no clock, no RNG. Reads/writes only galaxy.claims (persisted) and the AI's faction (persisted), so it's save/load-safe. Surfaced for the player: galaxyStatus reports each world's controll…bd7958c -
Living galaxy (6/N): every world simulates from the start
Claude +1: Claude Opus 4.8Unverified signaturecreateGalaxy now instantiates ALL worlds (unsettled, background) at galaxy creation, not just the player's start seat — so each world's AI faction founds a base and develops via aiIndustry, its economy growing and diplomacy drifting, before the player ever visits. The galaxy is alive from turn one. A probe puts the whole 11-world galaxy at well under 1 ms/ frame (the BG scheduler already spreads the work; background armies stay bounded), so this is perf-safe. Because every world now exists, "the state object exists" can no longer mean "the player has been here." A new `galaxy.discovered` set tracks worlds the player has actually REACHED: - jumpCost is free to a discovered world (or one you hold a foothold on), distance-scaled to an unreached one — so exploration still costs fuel; - galaxyStatus reads a world "unexplored" (and hides its stance) until it's discovered, and reports discovered.size as "visited" — the starmap looks exactly as before until you travel; - jumpCapital marks…ad2fc1c -
Auto-repair: one Mender per building — drones spread out instead of dogpiling
Claude +1: Claude Opus 4.8Unverified signatureEvery auto-repair Mender independently picked the single most-worn building, so they all converged on the same target. Now a per-tick claim tally caps it at one auto-repair Mender per building: countMenderTargets (sim.js, frozen at tick start like the hauler counts) tallies each building's committed drones, and autoRepairRoam skips a building already claimed and claims the one it takes for the rest of the tick. So with several worn buildings the menders fan out one-to-one; with fewer buildings than menders the extras stay off rather than piling on. Buildings only — a wounded UNIT can still draw several menders. Deterministic, player-only (AI menders don't auto-repair), transient menderClaims stripped from saves. Tests: maintenance.test.js gains "menders spread one-per-building" and "extra mender doesn't pile on the lone worn building". Full suite 607 green, tsc clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AEQUVetLk31J…a485a5f -
Fix auto-repair ping-pong: hysteresis, most-worn priority, slower wear
Claude +1: Claude Opus 4.8Unverified signatureThe auto-repair Mender oscillated: it targeted ANY building below max HP, so the instant gentle decay nicked a freshly-healed hull the drone was yanked back — heal, leave, decay, return, forever. - Hysteresis: the Mender is only ATTRACTED to a friendly worn past NEEDS_REPAIR (85%), and once committed it STAYS on that target until it's topped back above HEALED (98.5%) — a hair of decay on a full building no longer re-grabs it, and it can't flip between two similar targets mid-approach (a transient repairTargetId holds the commitment). - Priority: it now goes to the MOST worn friendly first (lowest HP fraction), distance only breaking ties — not merely the nearest. - Slower wear: DECAY_FRAC 0.002 → 0.0006 (~18 min to the floor), so maintenance is a gentle long-game pressure rather than a constant chore, and a parked Mender's passive heal trivially out-paces it (the hull stays full → never re-attracts the drone). Net: a Mender heals the worst-off building, tops it up, then P…56dcb34 -
Round-trip worker logistics + manual worker→building assignment
Claude +1: Claude Opus 4.8Unverified signatureWorkers no longer run empty legs, and the player can put a worker on a specific building. The old separate supply/haul-for-factories jobs become one SERVICE round trip: in a single loop a worker carries a needed INPUT from the treasury to a factory AND carries its finished OUTPUT back — neither leg wasted. Pure producers (the Rig, the forward drop-offs) keep the one-way HAUL. - haul.js: rewritten around updateService/assignService (round trip) + updateHaul/ assignHaul (pure producers only, recipe buildings excluded). Adaptive legs — with no input to fetch it goes straight for the output; with no output it just delivers. - commands.js: issueServiceBuilding — assign workers a standing `manual` service order. - input.js: right-clicking a completed friendly factory / Rig / drop-off with selected workers assigns them to service it (they keep serving it until re-ordered). - sim.js: idle worker → haul (pure producers) else service (factories); dispatch service. - persist.js/types.js: `…d443e60 -
Combustion Generator: cheap gas/biomass power on a tight grid
Claude +1: Claude Opus 4.8Unverified signatureA fuel-burning alternative to the Reactor. It grants half the Power (energyGrants 10 vs 20) over a much smaller grid (powerRange 0.55 shrinks its efficiency zones), and only while it's fed — it burns gas OR biomass (whichever the stockpile has more of) at combust.rate/sec from the treasury, the mirror of the Reactor's free-but-pricey grid: cheap to raise, ongoing fuel bill. Out of fuel or paused, it grants nothing. - entities.js: `combustor` def (energyGrants 10, powerRange 0.55, combust spec); the Reactor gains powerRange 1 as the reference reach. - industry.js: powerEfficiency now scales each source's distance by its powerRange (nearestReactorDist → bestGridDist), so a Generator's zones sit tight and a consumer must huddle close; powerCap and the grid only count ACTIVE sources; updateCombustors burns fuel each tick and sets the transient `powered`/`fuel`. - sim.js: settle Generators before any consumer reads powerCap. - persist.js: strip the transient powered/fuel from saves…08cde58 -
Logistics Phase B: factory input larders + a real supply network
Claude +1: Claude Opus 4.8Unverified signatureFactories stop sipping from an infinite global pool: each now draws its inputs from a FINITE local larder (building.input) and banks output into a finite buffer (building.store), both capped at 80 by default for any building with a recipe (storeCapOf/inputCapOf). updateProduction runs a factory only while it's been supplied AND has output room — it stalls starved or backed-up, exactly like the Plasma Rig. Worker haulage gains a supply leg (engine/haul.js): an idle player worker picks the nearest own factory low on an input the treasury can provide, carries a cargo from the Command Center into its larder. Output is cleared by the Phase A haul path, which now covers factories automatically. Idle workers try haul first (clear backlogs, refill the treasury) then supply (feed the starving). Both capped at 2 per building, deterministic nearest-by-id, player-only. The industry chain is now a genuine network routed through the treasury: raws → CC → supply → smelter → metals → haul → CC → sup…832f494 -
Logistics Phase A: finite rig output buffer + worker haulage
Claude +1: Claude Opus 4.8Unverified signatureStorage stops being an infinite sink and workers get a job that lasts to the end of the game. The Plasma Rig (a leaf producer of raw goods — the safest surface to prove the mechanic, since its output feeds no other factory) now banks each dig into a FINITE local buffer (building.store, capped at def.storeCap 120) and stalls the moment it's full. Those goods aren't spendable until hauled to a Command Center. New worker haulage (engine/haul.js): an idle PLAYER worker auto-assigns to the nearest own producer whose buffer is ≥34% full (≤2 haulers per producer, ties broken by id), walks there, loads a cargo, carries it to the nearest drop-off, and banks it into the treasury 1:1 — no gather multiplier, since the rig already extracted it. Run short on haulers and the buffer fills and the rig stalls: logistics is now a live demand on labour, exactly as intended. - entities.js: storeCapOf / storeTotal / storeRoom helpers; storeCap on the rig def. - rig.js: bank to store (top off the final dig…91ad84b
-
Plasma Rig: deep-core extraction — unlimited raws at a probabilistic yield tier
Claude +1: Claude Opus 4.8Unverified signatureA new Odyssey building (engine/rig.js) that mines a raw commodity forever instead of from a finite node. Each dig cycle strikes a probabilistic tier — low, medium, good, or overwhelming — whose odds rise with the rig's spot AND the planet, so placement matters. It's costed as an engine, not free money: - "plasma to dig" = a heavy Power draw routed through industry.js's grid, so it competes with factories/the Gate and its digs slow when power is short. - "nuclear to exploit" = radioactives burned per dig; out of them, it stalls. - "AI to pilot" + manufactured materials = an expensive build cost ({ ore: 500, machinery: 8, electronics: 5, ai: 3 }), gated behind aifoundry + reactor. Which raw a rig mines (its "vein") is fixed by where you build it. Determinism-safe: every roll is a hashStr of the rig id + its dig counter (no Math.random, no clock), so a same-seed galaxy strikes the same seams on replay; the engine-purity + determinism guards stay green. dig state persists on the bui…db667bc