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
-
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
-
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
-
Disable jump-time cargo auto-fill: an empty ship carries nothing
Claude +1: Claude Sonnet 5Unverified signatureAn empty freighter used to auto-fill from the origin stockpile most-valuable-first (machinery, then electronics, alloys, spice, metals) on every jump - a "produce here, sell there" convenience default. It actively worked against a deliberate there-and-back haul: load goods, jump, unload at the destination, then jump straight back before reloading, and the now-empty ship would eagerly scoop up whatever was just delivered and carry it right back - a delivery silently undone, easily mistaken for a duplication bug. loadCargo (engine/galaxy.js) now delivers exactly what was hand-loaded per ship (loadFreighter/unloadFreighter) and nothing else. Freight Lanes (runLanes) and the freighter panel's own "Auto-load" button are untouched - both are separate, still-explicit uses of cargoManifest's most-valuable-first pick, not the silent jump-time trigger this removes. Updated the Spaceport panel's cargo preview to show what's actually aboard the staged ships rather than a hypothetical auto-fill p…f6a4da9 -
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
-
Conquest overrides a stale rival-Gate ascension, not the other way around
Claude +1: Claude Sonnet 5Unverified signatureThe Domination-Faction-Memory team flagged this cross-team interaction in their own report rather than guessing at a fix outside their file scope: Rival Gate ascension ceils a world's stance permanently at PEACE_THRESHOLD via a direct mutation reapplied every scan, while Domination with teeth floors a pacified world's stance target at Neutral inside updateDiplomacy's slower drift. On a world that both ascends and is later personally pacified by the player, the ceiling's every-scan reassertion always wins the raw stance value between the floor's drift ticks — so razing an ascended neighbour's Command Center would have no visible effect on its stance, forever. That reads as broken: conquest is the more direct, more recent player action, and should win. Confirmed the bug with a red test first (200 interleaved updateDiplomacy/checkRivalGate scans after pacifying an already- ascended world never lift its stance past the ceiling), then fixed it by skipping the ceiling's application and reap…9300cbe -
Domination occupation dividend: pacified worlds pay a small credits/sec
Claude +1: Claude Sonnet 5Unverified signatureOptional half of docs/improvement-proposals.md 657-665 ("Domination with teeth"): a pacified world now contributes PACIFIED_INCOME (0.1 credits/sec, ~6/min) in sweepColonies, additive on top of any ordinary building-based colony income and independent of whether the player ever founded a colony there — conquest alone is worth a small, modest annuity, deliberately far below what a single well-built colony can pay at its cap (COLONY_INCOME_PER_BUILDING * COLONY_INCOME_CAP = 1.8/s). Tests in test/domination.test.js: a pacified world with zero player buildings still banks credits, and an unpacified background world pays nothing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>3ac784d -
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 -
Add the rival Gate: a neighbour races its own Antimatter Gate
Claude +1: Claude Sonnet 5Unverified signatureMerges two independently-written proposals describing the same feature (Defense twin "the galaxy's strongest faction races its own wonder" + AI twin "a fully-teched neighbour races its own Antimatter Gate") into one mechanism. Reconciled trigger (engine/aiIndustry.js rivalGateEligible/RIVAL_GATE_BUFFER): the Strategic tier standing (Antimatter Forge + AI Foundry + Torpedo Works, antimatter_gate's own `requires`) AND a banked strategic-goods buffer, gated to Hard difficulty OR a wantsDeepIndustry temperament. Every qualifying AI world raises its own Gate independently, self-contained in its own think cycle. Explicitly re-checks the Easy strategicCeiling flag so the invariant holds by construction, not just by the accident of the chain never reaching that far. engine/wonder.js generalizes chargingPlayerWonder into chargingWonderOf(state, owner), with chargingPlayerWonder now a thin delegate (existing diplomacy.js/aiMilitary.js callers unaffected) — updateWonder needed no changes at all…f75beff -
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 -
Spaceport tiers discount new-world jump fuel
Claude +1: Claude Sonnet 5Unverified signatureThe pad's tier no longer only lifts more fleet per jump (SPACEPORT_CAPACITY) — it now cuts new-world fuel too, via a FUEL_DISCOUNT_BY_TIER table (x1.0 / x0.85 / x0.7) applied in jumpCost's distance-scaled branch only. Free return jumps to a world you already hold are untouched. Both existing cost-preview call sites (hudSelection.js's Spaceport panel, starmap.js's jump hint) already call jumpCost so they pick up the discount automatically; extended their copy text to say so. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>8624f8e -
Odyssey: let the player choose (or reroll) their starting world
Claude +1: Claude Sonnet 5Unverified signaturecreateGalaxy now accepts opts.startId, validated against ODYSSEY_WORLDS. The seed's own random draw still always runs (and is used whenever startId is absent or unrecognised) so downstream RNG consumption is byte-identical regardless of whether the player picked a world or left it to chance — the same determinism guarantee every other seeded draw in this file already keeps. The resolved choice is recorded on galaxy.settings (which persists whole, so no persist.js change is needed and no GALAXY_SAVE_VERSION bump). Tests added to test/odyssey.test.js: an explicit startId is honored and recorded; an unrecognised startId falls back to the same draw as omitting it; choosing a different world doesn't perturb any other world's own per-planet RNG stream; a startId matching the natural draw reproduces the identical galaxy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>0735d65
-
Vary neighbour worlds' AI difficulty and personality across the galaxy
Claude +1: Claude Sonnet 5Unverified signatureToday the player's single splash-screen Difficulty + AI Strategy pick applies to every world in the galaxy uniformly — once you pick Hard, every world you ever visit plays Hard, with no variety to discover. neighbourAiProfile(seed, planetId) (engine/galaxy.js) resolves an independent, deterministic difficulty (with matching aiApm/aiMicro, never a mismatched pair) and AI Strategy for a world, derived from the galaxy seed + planet id the same way per-world map generation already is — no new randomness source, fully replay-safe. addPlanet now uses the player's actual settings only for the start seat (planetId === galaxy.activeId); every other world resolves its own profile instead. Nothing new is needed for "the player must discover it by playing" — galaxyStatus already hides everything about a world until galaxy.discovered has it. Written test-driven, per the new process (see CONTRIBUTING.md): the tests in test/livingGalaxy.test.js were written first, against neighbourAiProfile before …9f4b083 -
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
-
Let workers load/unload freighters and add an AI-logistics freighter mode
Claude +1: Claude Sonnet 5Unverified signatureWorkers can now physically ferry cargo to and from a landed freighter (right-click it, like assigning a worker to service a building) — the freighter becomes a mobile collection point for nearby production instead of every load detouring through the Command Center. Also adds a new Datacenter research, Autonomous Freight AI, that lets a freighter join the local haul/service logistics chain on its own, like a worker but at its own much larger cargo capacity. Running autonomously burns AI Cores from the treasury while active; out of AI Cores it just pauses in place instead of losing its job or cargo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdsYZQyGk32n8x3UBiPGup6f39bc5 -
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
-
Fix jump only ever using the first of multiple Spaceports
Claude +1: Claude Sonnet 5Unverified signatureplayerSpaceport() picked exactly one completed Spaceport via Array.find() - whichever one Map iteration happened to return first. jumpCapital, jumpVessel, and canJump all routed through it. A second (or third) completed Spaceport's staged units were silently invisible to every jump, forever: not included in the manifest, not counted in leftBehind, just never considered - and a colony ship staged only near that second pad was never reported as loaded either. Added jumpManifestAll(), which combines every completed player Spaceport on the current world into one manifest: each staged unit boards through its nearest pad (deduped so a unit in two overlapping catchment radii can't be double-counted), each pad fills its own capacity closest-first exactly like the existing single-pad jumpManifest (byte-identical output when there's only one pad - verified against the full existing test suite unchanged). jumpCapital now uses it; jumpVessel and canJump now check every pad instead of just the fir…f3ed241 -
Galaxy/starmap: pause the sim while it's open, unhide toast alerts, cache index lookups
Claude +1: Claude Sonnet 5Unverified signature- openStarmap/closeStarmap never paused/resumed the sim, unlike every other modal in this codebase - the AI, diplomacy, and economy kept ticking behind an opaque snapshot. Now calls pauseLoop("starmap")/resumeLoop("starmap") on every open/close path (including Escape and backdrop click). Separately, .starmap-overlay's z-index (30) sat above .galaxy-toast-stack's (20), so a "colony under attack/lost" alert rendered invisibly behind the starmap. Bumped the toast stack to 35. - galaxy.js's background-world scheduler ran galaxy.worlds.indexOf(id) every frame for every background world, an O(n) scan on an array that never changes shape after creation. Replaced with a WeakMap-cached id->index Map, keyed on the array's own identity (self-invalidating if a new array is ever assigned). Verified the one place that mutates a worlds array in place (persist.js's roster-backfill loop) does so before the array is ever exposed as a live galaxy.worlds, so the cache can't go stal…003c805 -
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
-
Odyssey freighters: a player-managed cargo hold (load/unload), shipped on a jump
Claude +1: Claude Opus 4.8Unverified signatureCargo ships used to be capacity-only — a jump auto-picked the most valuable goods and the player had no say in what shipped. Each freighter now carries a `freight` hold (commodity → qty, capped at its cargoHold) that the player fills and empties by hand: - loadFreighter/unloadFreighter (engine/galaxy.js) move goods between the current world's stockpile and a freighter's hold, clamped to room and stock; freightUsed / freightRoom report fullness. - A jump now delivers each staged freighter's hold to the destination colony. A freighter left EMPTY still auto-fills most-valuable-first (the zero-effort default is unchanged — all existing cargo tests pass), while a hand-loaded one ships exactly what the player chose (e.g. cheap spice over machinery). - The hold rides on the unit, so it serializes for free; cleanEntity sanitizes it on load (real commodities only, positive, clamped to cargoHold) since a save is untrusted. Named `freight`, not `hold`, to stay clear of the combat ho…6325132 -
Tier 5: dedup and derive engine helpers (byte-identical replays)
Claude +1: Claude Opus 4.8Unverified signatureRemove copy-paste and stale magic numbers from the engine, keeping every change deterministic (same-seed replays stay byte-for-byte identical, the determinism + engine-purity guards stay green). - rng.js: centralise the string hash (h=7, h*31+c) as hashStr(). It lived byte-identical in four modules (combat/gather/movement/separation) for a worker's orbit angle, an avoidance dodge, a spread-fire pick and the coincident-pair nudge — one home so a change can't drift between copies. - movement.js: derive MAX_UNIT_RADIUS from the roster instead of a stale literal (10, "Breacher") that under-sized the avoidance query the moment a wider hull (Bulk Freighter, 15) was added; separation.js's SEP_RADIUS now keys off it too. A larger query returns a superset the exact-distance check still filters identically, so replays are unchanged. - movement.js: add keepEscortStation() next to escortSlot() and route the three identical escort branches (combat ship, worker, support drone) thr…8583c67 -
Tier 4: per-tick sim cost — grid int keys, fog squared-dist, galaxy scan cadence
Claude +1: Claude Opus 4.8Unverified signature- Spatial grid (engine/grid.js): replace the per-unit / per-query "cx,cy" STRING cell key (allocated + hashed every tick on the hot broad-phase path) with a packed integer key (cx+PAD)*STRIDE+(cy+PAD). Cells bucket the same units in the same order and queryNeighbors visits the same cells in the same fixed loop, so candidate lists are byte-identical — the determinism test stays green. - Fog reveal (engine/fog.js): compare squared distances (dx*dx+dy*dy vs sight*sight) instead of a Math.hypot per candidate cell, and hoist the row index + dy out of the inner loop. - Galaxy scans (engine/galaxy.js): the conquest/milestone/relief checks change on a minutes timescale but ran every frame — throttle to ~1/sec (tick 1, then tick % 20) on the same deterministic integer schedule the background round-robin uses. Odyssey-only path. All verified byte-identical: determinism + engine-purity + fog + balance + odyssey + wonder tests green (549 total). No behaviour change on the skirmish r…4559046 -
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 2 economy: close the credit printer, cap the annuity, revive Verdani
Claude +1: Claude Opus 4.8Unverified signaturemarket.js: - Raw inputs (ore/crystals/etc.) now cost a wide 1.5x buy spread vs the 1.15x refined-goods spread, so a local credits→ore→refine→metals→credits loop can't round-trip for free — profit needs a real inter-world price gap. - Produced goods gain a slow, deep "glut" term (decays over ~8 min, separate from the ~17s price pressure): dumping factory output on one world saturates its price, so the credit printer is bounded and you're pushed to the intended make-here / sell-there haul loop. - sell()/buy() walk the price across TRADE_LOT chunks, so a bulk trade is priced marginally (each lot after the prior lot's slippage) instead of the whole quantity at the pre-trade price — a future "Sell All" can't dump at full price. The 25-per-click UI trades one lot, so its numbers are unchanged. galaxy.js: - New-world jump fuel scales with frontier distance (data.js planet x) instead of a flat 400 — a real, growing credit sink; return jumps stay free. - Passive colony income…ed7688d -
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 freight: cargo ships in three sizes set the jump's cargo hold
Claude +1: Claude Opus 4.8Unverified signatureReplaces the flat 300-unit auto cargo hold with real freight logistics: a jump's cargo hold is the combined capacity of the CARGO SHIPS you stage for it. No cargo ship, no freight — hauling goods between worlds now means building shipping. - entities.js: three Odyssey-only cargo ships, Spaceport-gated, role "freighter" (unarmed — escort them): Hauler (hold 250, 1 supply), Heavy Hauler (650, 3), Bulk Freighter (1600, 6). Bigger holds cost more ore, supply, and Spaceport jump-capacity, so shipping scales with investment. - galaxy.js: freightCapacity(riders) = summed cargoHold of the staged cargo ships; cargoManifest(from, capacity) + loadCargo fill most-valuable-first up to it; jumpCapital uses the staged ships' capacity (CARGO_CAPACITY removed). - hud.js: Command Center produces the cargo ships (locked until a Spaceport exists, with sprite icons + a "cargo N" stat in the tooltip); the Spaceport panel's cargo hold now reflects the staged ships' capacity. - render.js: the n…97f8989 -
Fix: coming back from a portless world no longer drags the fleet home
Claude +1: Claude Opus 4.8Unverified signatureThe catch-22 fallback evacuated EVERY unit off a portless world, so hopping back to a base to fetch a colony ship dragged the whole fleet you'd transported over back with you — not what you wanted (you meant to leave it there). Now a no-Spaceport fallback is a pure CONTROL SWITCH: it moves no units, so the force you left stays put. The catch-22 is still solved (you hop back for a colony ship and bring the ship TO the stranded force, rather than the force home). To actually ferry units off a world you still build a Spaceport and stage them — the normal, deliberate path. The Spaceport launch (staged, capacity-capped) is unchanged, so reinforcement hops still work. Updated the stranded-fallback test to assert the transported fleet stays on the world left behind (res.riders === 0), not evacuated. 515 pass. Browser-verified: a 5-ship fleet jumped to a new world stays there when you come back home. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/c…7c036e8 -
Fix Odyssey catch-22: a portless world can fall back to a held one
Claude +1: Claude Opus 4.8Unverified signatureBug: jump an army to a new world without a colony ship and you're stranded — no Command Center means no worker to build a Spaceport, and no Spaceport means no way to jump back for the colony ship. Softlock. Fix: a Spaceport on the CURRENT world still lets you jump anywhere, but WITHOUT one you can always FALL BACK to a world where you hold a base (a Command Center or a colony ship). Only opening a NEW frontier needs a Spaceport here. - galaxy.js: canJumpTo(galaxy, destId) — the gate (Spaceport here → anywhere; else → only a world with a player foothold). jumpCapital handles the no-Spaceport fallback by EVACUATING the whole stranded force (there's no pad to meter it). - starmap.js: onWorldClick uses canJumpTo; the header hint explains the fallback when you have no Spaceport. Also fixes a latent id-collision this recovery flow exposes: createGameState resets the global entity-id counter to 1 per world (for deterministic seeding), so once a galaxy holds >1 world, the next thing b…6261999 -
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: free travel between held worlds, no colony-ship jump mandate
Claude +1: Claude Opus 4.8Unverified signatureJumps no longer require a colony ship on the pad. A Spaceport alone can launch, carrying whatever you stage — a colony ship (to settle new ground), an army (to reinforce a colony), or nothing (to hop back and control a world you already hold). Returning to any world you've visited is free; only reaching a NEW world costs the JUMP_COST fuel. - galaxy.js: canJump = a completed Spaceport (drop the vessel gate); jumpCost(galaxy, destId) = 0 for a held world, JUMP_COST for a new one; jumpCapital de-mandates the colony ship and charges jumpCost. - Defeat becomes galaxy-wide (checkGalaxyLoss, run in stepGalaxy): you're beaten only when no Command Center and no colony ship stands on ANY world — so an army-only hop, or a trip to fetch a forgotten colony ship, is never an instant loss while a base holds elsewhere. - victory.js: checkEndlessLoss skips galaxy states (state.inGalaxy), so the per-world check can't fire a false defeat on a world you don't hold; it still governs a stand…51531fb -
Odyssey: the Spaceport jump carries a colony ship, not a deployed base
Claude +1: Claude Opus 4.8Unverified signatureThe interplanetary jump was still teleporting a deployed Command Center to the destination (the origin lost its base). With colony ships as the mobile "seed of a base", that's wrong: a deployed base is permanent. The jump now carries a COLONY SHIP. engine/galaxy.js: - jumpVessel(state) replaces jumpableCC: the jump needs a player colony ship staged within JUMP_LOAD_RADIUS of a completed Spaceport (canJump = !!jumpVessel). - jumpCapital relocates the staged expedition (the colony ship + any army loaded with it), the credit fuel and the cargo hold — and NO deployed base. The origin keeps ALL its buildings and becomes a background colony; deploy the ship at the destination to found your base there. Refused (no fuel spent) if no colony ship is on the pad. - The Capital is reframed: like every deployed base it's permanent and never travels (its double-HP fortification still stands). hud.js/starmap.js: the Spaceport panel and starmap now say "send a Colony Ship" and lock the jump…a6b556b -
Odyssey: colony ships — start with a mobile ship, deploy it into a base
Claude +1: Claude Opus 4.8Unverified signatureReframes Odyssey expansion. Both sides now START with a mobile Colony Ship instead of a placed Command Center; you DEPLOY it in place to found your first base (which can then be upgraded to the Capital). New bases are no longer built from scratch by a worker — you build a Colony Ship at a CC, move it to the site, and deploy it there. Core (engine/colony.js, engine/entities.js, engine/state.js): - colonyship UNIT — odysseyOnly, non-combat (unarmed, never auto-acquires, transparent to the army/threat scans, but still shootable so it's a real escort risk), hp 600, cost 500 ore (a mobility premium over the CC's 400), supply 3. - deployColonyShip(state, id): validates the footprint (canPlaceBuilding), consumes the ship, mints a COMPLETED Command Center + disembarking colonists (3 workers). Instant and pure — no clock/RNG. hasColonyShip is the shared foothold predicate. - seedPlayer seeds a colony ship for endless; the SKIRMISH branch (a placed CC + 3 workers) is byte-identical, s…24f7ec5
-
Odyssey: upgrade a Command Center into the anchored Capital
Claude +1: Claude Opus 4.8Unverified signatureFollow-up to multiple CCs: you can now improve ONE Command Center into your Capital — a fortified anchor — while the "smaller" (non-capital) CCs are what relocate between worlds. engine/galaxy.js: - upgradeToCapital(state, building): pay CAPITAL_UPGRADE_COST (400 ore), mark the CC as the Capital, and scale its HP by CAPITAL_HP_MULT (×2 → 2000). One Capital per owner, never on a constructing CC. Deterministic; the flag + raised HP ride the building object, so they persist across save/load for free. - jumpableCC(state): the first non-capital player CC — the one a jump relocates. - canJump now requires a Spaceport AND a jumpable (non-capital) CC. - jumpCapital relocates the jumpable CC and refuses (no fuel spent) when the only CC is the anchored Capital. hud.js: the CC panel shows "◆ Upgrade to Capital" (or, once upgraded, an anchored-fortress note); the Spaceport panel locks its jump buttons with a clear reason and warns when your only CC is the Capital. Capital/jumper state fo…4cfb896 -
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 -
Fast-follow: cargo-on-jump — make here, sell there
Claude +1: Claude Opus 4.8Unverified signatureCloses the honest gap Phase 4 flagged: jumps carried the CC + units + credits but not goods, so cross-world goods arbitrage was impossible. A jump now hauls a capacity-bounded CARGO HOLD of MANUFACTURED goods to the destination, so a run of production on one world can be sold on another (produced goods price differently by world — engine/market.js), completing the make-here/sell-there loop. - engine/galaxy.js: CARGO_CAPACITY (300), cargoManifest (pure — what a jump would haul, most-valuable-first: machinery > electronics > alloys > metals), and loadCargo, called in jumpCapital; the jump summary now includes the cargo. Raws are too cheap to bother and strategic goods stay committed to the origin (which also keeps the hold empty until you've industrialized, and leaves every existing jump test untouched). - hud.js: the Spaceport jump panel previews the cargo hold ("300/300: 100 machinery, 200 alloys — hauled to sell at the destination"). - test/cargo.test.js: goods haul to th…f13e05b -
Odyssey Phase 4: faction & planet specialization (where you settle matters)
Claude +1: Claude Opus 4.8Unverified signatureFinal phase of the economy import: a world's stats now shape its economy, so choosing where to settle/jump is a real economic decision. Reconciled from a three-way brainstorm — the honest finding is that jumps carry the CC + credits but NOT cargo, so literal "make here, haul there, sell there" is impossible; Phase 4 delivers per-world production/market CHARACTER instead, and defers cargo-on-jump. - engine/industry.js: planetIndustryScale — a world's `industry` rating (1..10) scales factory speed, the RATE twin of Phase 2's researchTimeScale (same clamp [0.5,2], pivot 5). Forge (10) runs 2×, Oort (2) 0.5×. Applied to updateProduction only, so it's a no-op on the skirmish path. - engine/market.js: finished goods (metals/alloys/electronics/machinery) now price by the world's industry — cheap where they're mass-made, dear on a frontier world that can't — continuous with the old flat 1.5× at the industry-5 pivot. This gives a low-industry world a real niche (top prices for manu…016957e -
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 5: the starmap — the galaxy's capstone view
Claude +1: Claude Opus 4.8Unverified signatureA full-screen galaxy map ties the Odyssey together. Every world sits on a ring, coloured by your relationship to it: your active capital, a colony you hold (with its neighbour's stance), or somewhere still unexplored (shown with its AI doctrine). If your capital has a Spaceport and the credits for fuel, clicking a world jumps there straight from the map; otherwise the header prompts you to build one. A visited-worlds count echoes the original game's "see every world" thread. - starmap.js (new): the overlay, self-wired to a topbar "Galaxy" button and the M key (Esc / backdrop closes). Pure view over a new galaxy.js galaxyStatus() snapshot (per-world status + stance + the visited/total counts). - hud.js: the Galaxy button shows only in Odyssey; the skirmish Save/Load buttons hide there (galaxy persistence isn't supported, so a skirmish save would mislead). - index.html/dom.js/style.css: the button, the overlay, and its styles — including the per-component `.hidden` rules thi…842eae1 -
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 3: market + universal credits
Claude +1: Claude Opus 4.8Unverified signatureTrading turns a local surplus into portable, galaxy-wide credits — and credits now fuel the jump, so exploration draws on your economy. Engine: - market.js (new): each world gets a price book. A commodity's equilibrium price is its global base value scaled by LOCAL abundance (a world rich in ore sells it cheap; one short on it pays more), so moving goods between worlds pays. Trades react — selling pushes a price down, buying pushes it up, with a market spread between the two — and the pressure relaxes back toward equilibrium over time. Pure and deterministic; credits live on the galaxy so they carry across jumps. - galaxy.js: every planet is handed a market on creation; a jump now costs JUMP_COST credits (fuel) and is refused when you can't pay; the starting credit stipend funds the first jump. - sim.js: an Odyssey world relaxes its market pressure each tick. UI: - hud.js: the Command Center gains a trade panel — a row per tradeable commodity with its live sell pric…f12f730 -
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