Spaces:
Paused
Paused
Commit History
-
Tier 1, step 4e: clear autoTarget on a plain move order (A13)
Claude +1: Claude Opus 5Unverified signatureOn its own commit because it MOVES THE REPLAY: separation spacing feeds back into positions. updateCombat returns before the acquisition block on a plain move, and that block is the only place in the whole engine that writes autoTarget — so the field was unreachable-for-clearing for the entire duration of a move. engine/separation.js's isCombatMode reads it as a combat-mode register, which directly contradicts its own doc comment two lines above: "Deliberately NOT combat mode for a plain 'move' order… a stale autoTarget/focusId reads as still-combat-mode for one tick too long — harmless." It was not one tick; it was permanent. The consequence is that a unit pulled out of a fight with Move — which combat.js's own header calls the way to disengage, so this is the common case, not an edge — kept the tight combat packing (minDist x1) instead of the padded idle spacing (x1.2) that SEPARATION_PAD_MULT exists to give it. A small, permanent, silent divergence from the stated contract, in the…3589155
-
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 -
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 -
Static-defense ladder: Bastille, Aegis Bastion, Plasma Torpedo Battery
Claude +1: Claude Sonnet 5Unverified signatureShips as one rationalized tier per docs/improvement-roadmap.md Phase 4, not three unrelated additions: all three new structures are tuned against a single shared invariant (every siege unit — Breacher 150, Colossus 185, Leviathan 200 — must still strictly outrange every static-defense structure added here, with one deliberate, documented exception at the top of the ladder), and land together so that invariant is verified once, holistically, rather than three separate times. - Bastille (engine/entities.js): a second, Foundry-gated static-defense tier — hp 600, attack 32, cooldown 1.4, range 115 (deliberately under even the plain turret's 130: a tankier, shorter-ranged brawler, not a sniper). Same combat stat field names the turret uses, so combat.js's updateBuildingCombat needed zero changes to run it. Wired into hudSelection.js's skirmish build list and the Odyssey GROUPS/alwaysShow sets alongside the turret, and into aiEconomy.js's fortify block: the AI's second fortifi…45b37a3 -
Colossus splash: def-driven area damage as the T3 anti-mass verb
Claude +1: Claude Sonnet 5Unverified signatureAdds a generic def.splash = {radius, frac} mechanic and puts it on the Colossus (radius 26, 50% falloff damage), giving T3 a real answer to "spread out vs artillery" that punishes separation.js's own same-owner unit packing — until now every T3 unit was single-target, so clumped deathballs were never punished by anything. engine/combat.js: performAttack's new applySplash helper runs after the primary hit lands. It mirrors acquireTarget's own grid/full-scan idiom (queryNeighbors(state.unitGrid, ...) when a grid exists, else a plain scan over state.units.values()) and, by construction, walks only state.unitGrid/state.units — never state.buildings — so splash can never reach a building or the attacker's own side, however close either stands to the impact point. The primary target itself is excluded from its own splash (it already took the full direct hit). HP is subtracted for every splash victim in one pass before any death is resolved, so the outcome never depends on queryNeighbors' it…8f2921c -
High ground extends weapon acquisition, not just fog sight
Claude +1: Claude Sonnet 5Unverified signatureFog reveal already scales by the source tile's terrain sightMult (engine/fog.js updateFog's srcMult), so a unit or Sentinel Turret on high ground sees 25% farther — but combat acquisition (acquireTarget, stillEngageable) only ever multiplied aggroRange by the sideMod sightMult, never by terrain. A unit holding a mesa or ridge could see an enemy it refused to engage, so README's "sees farther and hits harder" was only half true. Fold the attacker's own terrain sightMult into aggro in both functions, mirroring fog.js: `state.map?.terrain ? sampleTerrain(state.map.terrain, unit.x, unit.y).sightMult : 1`, multiplied in alongside the existing sideMod term. Turrets share this path (updateBuildingCombat calls acquireTarget too), so a turret on high ground gets the same reach. Positional, not a triangle change — same spirit as the existing combatMult damage bonus a few lines above. Tests added in test/combat.test.js, beside the existing high-ground combatMult case: a fresh acquireTarget test…b771956 -
Telegraph counter-triangle hits: bonus flag, hotter tracers, HUD counter text
Claude +1: Claude Sonnet 5Unverified signatureMerges "Counter-triangle telegraphs" and "Surface the counter-triangle on unit buttons and selection rows" into one readability feature, per the roadmap. - engine/combat.js: performAttack stamps bonus:true on the attackHit event whenever the attacker's bonusVs counters the target's type (scoped to the per-type hard counter only, distinct from the pre-existing heavy flag's class-wide siege bonus). Zero sim effect - UI-only field. - effects.js/boot.js: addTracer takes the bonus flag through from the event. - renderEffects.js: a bonus tracer renders hotter (shade()'d toward white) and thicker, with a bigger/hotter impact spark. - hudSelection.js: unitTip() appends "strong vs / falls to" lines derived from UNITS[].bonusVs (+ "structures" for bonusVsBuildings) via a hoisted reverse index (COUNTERED_BY), same idiom as ALL_COSTS and aiMilitary.js's own COUNTER_OF. The aggregated multi-type selection rows get the same "falls to" suffix (shared summaryRowLabel() feeds both the …0056365
-
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 the Helium Bomb distance-based damage falloff and a slow shockwave (#11)
Unverified signatureReplace the flat "erase everything within blast radius" kill with a distance-squared decay of HP loss: entities within BOMB_CORE_RADIUS (1.5x the bomb's own radius) still take a guaranteed-kill hit, but damage tapers off with the square of distance beyond that, reaching zero at BOMB_BLAST_RADIUS. Blast-killed entities still grant no salvage, same as before. Also slows and reworks the detonation VFX into a dramatic, decelerating shockwave ring that crawls out to the true blast radius and cools from a hot core color to ash-grey at the tapering edge, echoing the new damage curve. Updates the HUD panel text and test suite to match. Co-authored-by: Claude <noreply@anthropic.com>33fe16b -
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 -
Formation shapes, leader positioning, and nested formations for grouped units
ClaudeUnverified signatureMulti-unit move/attack-move used to just spread the group across a flat square grid (engine/commands.js formationSpots) with no leader concept and no way to hold that shape once stationary. This adds: - Four selectable formation shapes (engine/formation.js): Grid (the original spread, kept byte-identical for the default case), Line (abreast the direction of travel, leader nudged front or back), Wedge (a spearhead with the leader at the tip, or a protective chevron with the leader shielded at the rear and flanks leading), and Circle (a ring around the leader at its center — "protection of a formation," the group-scale sibling of the existing single-target Escort). - A deterministic "leader": the unit with the most maxHp, ties broken by id — never selection-order-dependent. - Spacing derived from the group's own hull sizes, not a flat constant, for every shape except the untouched default grid path. - Nested formations: a selection already spread into distinct spatial …4c8f450
-
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 -
Sim core: fix stale balance comment, reuse queryNeighbors' scratch buffer
Claude +1: Claude Sonnet 5Unverified signature- combat.js's comment claimed Bastion's range was 24; it's 44 (entities.js bumped it, with its own comment admitting so nearby) - just a stale number. - grid.js's queryNeighbors allocated a fresh result array on every call (~3x per unit per tick across movement/separation/combat), in a file whose own header states a no-per-tick-garbage goal. Now reuses one scratch array. Verified every call site (movement.js, repair.js, separation.js, combat.js) reads the result via an immediate, synchronous, non-nested for-of and discards it before the next call - no caller holds a reference past that or triggers a nested queryNeighbors call while iterating, so sharing the buffer is safe. combat/sim/grid/separation/movement/determinism tests all still pass unmodified. - sim.js's tick() now guards against a missing UNITS[unit.type] def - belt and suspenders; persist.js already strips unknown-type entities on load, so this covers only a hypothetical future code path that bypasses i…1702aab -
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
-
Tier 5: group the AI controller's bookkeeping under state.ai (no behaviour change)
Claude +1: Claude Opus 4.8Unverified signatureMove the 13 flat state.aiThink / state.aiScoutId / state.aiWaveCount / … fields into a single grouped state.ai object, decluttering the top-level state (which is the shared sim world) from the AI *controller's* private scratch state. This is the safe half of the two deferred Tier-5 refactors; the entity-id-counter move was deliberately skipped (moving it per-state would reintroduce cross-planet id collisions — the current single reset-per-construct counter is correct). Kept strictly behaviour- and format-neutral: - The save WIRE shape is unchanged: serPlanet still writes `ai:{aiThink,…}` and rehydrate still reads `P.ai.aiThink`, so every existing save loads untouched — no GALAXY_SAVE_VERSION / SAVE_VERSION bump. Only the live in-memory shape nests. - The think/wave/attack-schedule fields (previously set lazily on first AI tick and read `|| 0` / `?? …`) are now initialised in createGameState, so state.ai has a complete, self-documenting shape. Values are identical to the old …f1048a5 -
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 2 balance: Bastion out-ranges the Skiff; Aegis projects a real anvil aura
Claude +1: Claude Opus 4.8Unverified signatureBastion (entities.js): range 24 → 44 so it now reaches PAST the Skiff it hard- counters (Skiff range 40) — a player-microed Skiff can no longer sit at 40 and orbit forever outside the reach of its counter. Kept under the AI's KITE_MIN_RANGE 50 so it still stands and trades. bonusVs skiff trimmed 14 → 10 to hold the head-on TTK. The rock-paper-scissors auto-battle triangle still closes (balance.test.js). Aegis (entities.js + sim.js + combat.js): the tank finally does its job. It now projects a guardAura — a cryo-armour bubble that multiplies DOWN the damage nearby allies take — so it soaks wherever it stands, with no need to pre-position it as a literal wall and no dependence on enemy targeting. sim.js collects the (rare, Tier-3) aura projectors once per tick into state.anvils; combat.js attackDamage applies the multiplier in O(anvils), usually zero. Speed 40 → 48 so it can keep formation with the line it shields. Deterministic (tick-start position snapshot, like the grid) and transien…30197bc -
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
-
Escort order: right-click a friendly ship to guard and follow it
Claude +1: Claude Opus 4.8Unverified signatureRight-clicking a friendly ship with a group selected now puts the group into a protective escort formation: each unit takes a stable slot on a ring around the target and follows it wherever it's ordered, holding station until given another order. A combat escort still auto-acquires threats that come near the target, then reforms — so it actually protects the ship, not just trails it. - movement.js: escortSlot() — the deterministic per-unit point on the ring around the guarded ship (the ring expands to seat the whole group). Null when the target is gone, so callers drop the order. - commands.js: issueEscort() assigns each unit an {type:"escort", targetId, slot, slots} order (stable slots → deterministic formation). - combat.js / sim.js: handle "escort" for combat, worker, and support units — a persistent follow (never cleared on arrival). Combat escorts fall through the normal auto-acquire first, so they defend then reform. - input.js: a right-click on a friendly UNIT (not bu…b1fda0e
-
Faction asymmetry — pick a side with its own passive identity
Claude +1: Claude Opus 4.8Unverified signatureThree playable factions, each a small bundle of passive trait multipliers — so a faction changes how a side plays without touching the shared roster, the rock-paper-scissors triangle, the tech tree, or the AI's build logic: • Frontier — mobility & vision (speed +8%, sight +6%): strikes first, holds the map. • Miners — industry (haul +15%, build/train −10%): out-scales in the long game. • Syndicate — firepower on a lean economy (damage +10%, haul −8%): win the fight, fast. Distinct axes with a real opportunity cost (Syndicate pays economy for its punch), so the pick is a strategic fork, not power creep. How it plugs in (engine/factions.js): A faction's traits are keyed on the exact side-modifier names the engine already reads, so they fold straight into map.js's sideMod — one seam, and every existing consumer (movement speed, fog/aggro sight, gather yield, build/train time) picks up the faction edge for free, stacking multiplicatively with a world's own per-side mod…ec7c7c8 -
Tier 2: a third upgrade doctrine — Logistics (economy + tempo)
Claude +1: Claude Fable 5Unverified signatureUpgrades were only ever ±damage; add a macro path so "out-produce them" is a real strategic fork against "out-fight them". Logistics is a third mutually-exclusive doctrine (via the same committedDoctrine lock) with two tiers: Logistics Network (T1) — +25% resource yield from every haul Rapid Fabrication (T2) — -20% unit & building production time Committing to it locks Assault AND Bulwark, and vice-versa — so it's a genuine opportunity cost, not a free third buy. Plumbed data-driven: a new shared entities.upgradeMult(upgrades, field) is the one place combat/gather/production read upgrade effects, so a new upgrade is pure data. combat.js's two inline damage-mult loops are refactored onto it (behaviour identical); gather.js reads gatherYieldMult, production.js reads produceTimeMult (construction and unit production), both optional-chained so map-less test stubs still pass. The Refinery UI is already data-driven — it picks up the new doctrine from the UPGRADES table plus a label …3195ab6 -
Tier 2 UX: Hold-position stance + a topbar idle-worker indicator
Claude +1: Claude Fable 5Unverified signatureTwo safe quality-of-life additions while balance items await playtesting. Hold position (H / a panel button): a combat unit stands its ground and fires on anything that enters weapon range but never CHASES an auto-acquired target out of position — the standard defensive stance for holding a line or a choke. combat.js honours a new unit.hold; any move/attack order or Stop clears it. Player-only — the AI never sets it — so determinism and the resolve guarantee are untouched. Idle-worker indicator: a lost worker on a big map is easy to miss, so the topbar now shows a "N idle" badge (hidden at zero) that jumps to the next idle worker on click, mirroring the ` key. Both surfaced in the help overlay and the controls legend. Tests (+2, 279 total): a holding unit fires an in-range enemy but won't chase an out-of-range one; issueHold sets the stance on combat units only and a move/Stop clears it. Browser-verified: the badge shows "3 idle" at start and the help lists Hold; clean boot. Co-Au…fea25dd -
Tier 2: salvage — a comeback softener for the deathball snowball
Claude +1: Claude Fable 5Unverified signatureWhen a COMBAT unit is destroyed, its owner reclaims 25% of its cost as salvage. Because the side taking the heavier losses gets the larger refund, an army being ground down rebuilds faster — relaxing the snowball the review flagged, where one lost engagement cascades into a loss with no way back. Combat units only (not workers or buildings), so it rewards standing and trading rather than feeding the economy. In an outnumbered 6-vs-10 fight the wiped side recoups 150 ore while the flawless winner gets 0 — the cushion goes to whoever is losing, exactly the intended anti-snowball direction (winner-take-all would have amplified it). Symmetric and deterministic. A passive opponent loses no units, so salvage never fires in the resolve/determinism tests — the resolves-to-a-winner guarantee is untouched (275 tests pass). Tests (+2): a destroyed combat unit refunds a quarter of its cost to its owner; a dead worker yields nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude…8e3a38d -
Tier 2: Tactical AI kiting — ranged units stutter-step back from melee
Claude +1: Claude Fable 5Unverified signatureCompletes the AI unit-level micro item. When Tactical, a ranged AI unit (Lancer, Breacher, Dreadnought — range >= 50) that's reloading while an enemy UNIT has closed inside 0.75x its weapon range steps directly away to preserve its range advantage, then stops to fire the instant its weapon is ready — classic stutter-step kiting. Melee brawlers (Skiff, Bastion) stand and trade. Lives in combat.js's in-range/reloading branch (per-tick, gated on state.aiMicro + ai owner + long range). Reacts to enemy UNITS only, never buildings, so a ranged unit shelling an undefended base never backs off — the resolves-to-a- winner guarantee holds (now verified with Tactical forced ON across all nine worlds, kiting included). The retreat point is clamped to the map so a cornered kiter can't walk off the edge. In an 8v8 Lancer-vs-Bastion duel kiting turns a 7-0 win (one lancer lost) into a flawless 8-0. Off by default and for the Standard AI, so nothing changes unless Tactical is chosen. Tests (+2, 273…b2bad03 -
Tier 2: Tactical AI focus-fire + an AI-tactics setup toggle
Claude +1: Claude Fable 5Unverified signatureThe single biggest AI upgrade: an opt-in "Tactical" mode where the opponent micros its army like a skilled player instead of leaving every shot to the dispersed auto-acquire. Focus-fire (engine/ai.js applyFocusFire + combat.js): when Tactical and it can SEE enemy combat, the AI points its whole nearby army at one target — the lowest-HP visible enemy, tie-broken toward the more dangerous one — so kills land faster and incoming damage drops sooner (Lanchester's square law). combat.js reads a new unit.focusId and prefers it while it's a live enemy in aggro, else falls straight through to the normal dispersed acquire, so a stale or too-far focus never freezes a unit and orders/wave-tracking are untouched. In a 10v10 skiff mirror this flips a 0-4 loss under dispersed fire into a decisive 5-0 win. Crucially it's a NO-OP unless real enemy combat is in sight, so razing an undefended base — every resolve test — is unchanged. Difficulty is now more than an APM dial: a new "AI tactics" setup o…e42e5cb -
Asymmetric world modifiers: two starts that differ
Claude +1: Claude Fable 5Unverified signatureWorld modifiers could only tilt both sides equally. A world may now carry an `asym: { player, ai }` block that overrides a modifier for just ONE side, so the two corners play as a real asymmetric matchup. - map.js: sideMod(state, owner, key) resolves the owner's asym override, then the shared modifier, then the default — so symmetric worlds and map-less test stubs behave exactly as before. Movement (speed), fog (sight), combat (aggro sight), production (build time), and gather (a new per-side gatherMult economy edge) all read it with owner context. - Two asymmetric worlds, telegraphed on the map card: - Oort "Contested frontier": player banks 20% more per haul (rich claim); the AI builds 18% faster (war factory). Out-mine vs out-build. - Nimbus "Storm front": player sees almost normally (0.95) where the AI stays half-blind (0.75), but the AI's units are 12% faster. Out-scout vs out-tempo. Safe for the resolves-to-a-winner guarantee by construction: an AI-side edge…d228ca7 -
Combat: spread-targeting + target stickiness (anti-dogpile)
Claude +1: Claude Fable 5Unverified signatureInstead of the whole line re-locking the single nearest enemy every tick, attackers now commit to a target (sticky auto-target) and fan out across the nearest few enemies in the local engagement band. This distributes fire the way a player micros manually — less overkill, units engaging distinct foes — and modestly softens the focus-fire snowball (the winning side bleeds a little more). - combat.js: spreadEnemy() distributes attackers across the SPREAD_TARGETS (3) nearest enemies within range+60, dispersed by a per-unit id hash; falls back to plain nearest while still closing in. updateCombat keeps last tick's autoTarget while it's a live enemy in aggro range, only re-acquiring when it dies or slips away — the stickiness that makes the spread hold. Deterministic (id-hashed, stable sort). - state.js: makeUnit carries an autoTarget field. Honest scope note (measured with a duel harness): this does NOT prevent the loser being wiped in a to-the-death fight — that's intrinsic to…1882245 -
Rework upgrades into two mutually-exclusive doctrines
Claude +1: Claude Fable 5Unverified signatureFixes the "buy both, no decision" upgrade design the review flagged: which upgrade you research is now a real strategic fork with an opportunity cost. - entities.js: the two flat upgrades become two doctrines of two tiers each — Assault (Overcharged Weapons -> Overcharged Core, +damage dealt) and Bulwark (Reinforced Plating -> Reinforced Bulwark, -damage taken). Each carries a doctrine + tier + a Tier-2 `requires` its Tier-1. New committedDoctrine() reports the path a player has locked into. T1 mults eased (1.2->1.15, 0.85->0.88) since a fully-teched doctrine now stacks two tiers. - production.js: researchUpgrade enforces the doctrine lock (committing to one locks the other out) and the tier gate (T2 needs T1, via the tech tree's prereqsMet). Assault costs radioactives, Bulwark crystals, so a world's deposit specialty tilts which comes easier. - combat.js: upgrade multipliers are now applied by iterating researched upgrades (data-driven), so the tiers stack multiplic…d69dcb5 -
Content: terrain — rough ground, high ground, and no-build zones
Claude +1: Claude Fable 5Unverified signatureGives the map spatial identity without any pathfinding rewrite: a static per-cell terrain field (a flat Uint8Array, fog-grid idiom) drives three purely local effects. - map.js: TERRAIN table + generateTerrain (fixed fractional feature specs, mirrored for fairness, scaling self-similarly) + sampleTerrain (O(1), OPEN out of bounds). Consumes ZERO rng draws, so the node layout stays byte-identical and determinism is untouched. Terrain is static for the whole match. Per-world specs on Glacius (rough ice fields flanking a central lane — a soft choke), Forge (rough industrial sprawl), and Pyralis (a high-ground mesa); the original three worlds stay all-open. - movement.js: rough ground slows the step (never to zero — no unit can ever be trapped, so a wave can't deadlock and the resolves-to-a-winner guarantee holds). Honours an optional per-unit allTerrain flag (forward-compat). - fog.js: high ground sees farther (per-source sight scaled by terrain). - combat.js: an attacker …03f71c7 -
Tier 6: performance & scale for large maps
Claude +1: Claude Fable 5Unverified signature- Uniform spatial grid (engine/grid.js), rebuilt once per tick, retires the three per-tick O(n^2) neighbour scans: separation, movement avoidance, and combat target acquisition now query only nearby cells. ~2.6x faster on the separation pass alone at 500 units, and the gap widens with scale. A full-scan fallback keeps every direct unit test byte-for-byte unchanged. - Viewport culling in render.js: fog cells, nodes, buildings and units outside the camera's world rect are skipped, so draw cost tracks what's on screen rather than total map size (16k+ fog cells on a 4x map). - Cache each resource node's silhouette (the seeded-PRNG polygon) instead of recomputing it every frame; only its radius changes as it depletes. - Index map.nodes by id (map.nodesById) — O(1) node lookups in gather, the AI, replacing per-tick linear .find scans that grew with map size. - Loop substep cap so a heavy tick degrades to slow-motion instead of spiralling into an ever-deepening backlog. - Pr…6d7e8fa -
Give workers a weak self-defense attack
Claude +1: Claude Fable 5Unverified signatureWorkers can now be ordered to attack (right-click an enemy) and fight back with a weak short-range strike — 4 damage on a 1.4s cooldown, about 2.9 DPS against a Skiff's 12. Crucially they never auto-acquire: a worker only swings when explicitly told to, so it doesn't abandon the economy to go pick fights, and a group ganging up can drive off a lone raider at a real cost (about five workers to kill one Skiff, taking a loss) rather than replacing military units. Workers route through a dedicated updateWorkerCombat (the ordered-attack path only, no acquire/attack-move machinery) that reuses the same performAttack as real units, so a worker's hit shows a tracer and plays its sound. The right-click order now includes any unit with an attack stat instead of combat-role only, so a mixed selection sends workers in too. Attack orders queue as waypoints like any other command. 167 tests pass (was 162): worker deals/queues/clears its ordered attack, closes when out of reach, stays weakest of al…da8e770
-
Merge Tier 4 into main alongside Tier 3
Claude +1: Claude Fable 5Unverified signaturemain had advanced with Tier 3 (repeated AI attack waves, counter-building, minimap, under-attack alert, combat tracers/death flashes, build-ghost preview, production-queue display + cancel, panel aggregation) while Tier 4 (buildable expansion Command Center, Sentinel Turret + Breacher siege unit, supply cap + Habitat, 9-world roster with per-planet modifiers, AI parity) was built in parallel. This reconciles the two feature sets. Notable resolutions: - Placement validation: the two tiers independently shipped engine/placement.js (isValidPlacement) and engine/colliders.js (canPlaceBuilding + findPlacement). Consolidated on colliders.js — it's a superset the AI's expansion needs — and repointed the build-ghost preview and issueBuild at it; removed placement.js and its now-redundant test. - Combat: kept Tier 4's shared performAttack fire path and folded Tier 3's tracer data (fromX/fromY/unitType) back into its attackHit event, so both the siege heavy-hit sound and the attack …c769ac5 -
Tier 4 AI parity + planet roster with per-world rule modifiers
Claude +1: Claude Fable 5Unverified signatureThe AI now plays all of Tier 4, driven by extended archetype profiles rather than hardcoding: it fortifies with Sentinel Turrets on the approach vector (Economist 2, Balanced 1, Rusher 0), expands to a second Command Center when home ore runs thin (banking for it via an ore reserve that pauses the unit mix instead of starving it), runs one shared production cycle across multiple Barracks (fixing the one-unit-at-a-time ceiling), and folds the Breacher into the patient mixes where the map can pay for it. A spendable-node worker filter keeps a poor-economy world's AI off dead-end commodities. Every new field is read with a use-site default, so a legacy profile still runs. Opens the picker from 3 to 9 curated worlds, each assigned an archetype and up to one data-driven rule modifier threaded symmetrically through the engine: Glacius slows all units, Nimbus/Pyralis shorten/lengthen sight and aggro, Helix adds a crystal cluster, Oort enriches deposits, Forge speeds construction. All modifie…ac7d42f -
Tier 4 combat: Sentinel Turret and the Breacher siege unit
Claude +1: Claude Fable 5Unverified signatureStatic defense and the answer to it, shipped together so neither creates a degenerate strategy on its own. Sentinel Turret: a crystal-costed building (the first repeatable crystal sink) that acquires and fires through a new updateBuildingCombat in the sim's building loop. Combat's fire path is refactored into a shared performAttack used by units and turrets alike, so Overcharged Weapons already multiplies turret damage and all kill/event bookkeeping stays in one place. attackDamage stays strictly additive — a new bonusVsBuildings class bonus is 0 for every unit-vs-unit pairing. Breacher: a radioactive-costed Barracks unit deliberately outside the Skiff/Bastion/Lancer triangle. It outranges the turret and hits every building for a flat class bonus (prefersBuildings targeting), but has the worst anti-unit DPS in the game and dies to a single Skiff at half its cost — a turtle-breaker that is helpless without an escort. acquireTarget is refactored around a nearestEnemy helper; the defau…e6d8701 -
Tier 3: repeated AI waves + counter-building, combat feedback, minimap, build validation
Claude +1: Claude Sonnet 5Unverified signatureAI: - Replace the one-shot attack flag with a home-army/committed-army split, so the AI launches repeated waves for the rest of the match instead of attacking exactly once and then sitting on an ever-growing army. - Every third unit built reacts to whatever combat type the player fields most, building its hard counter (derived from the bonusVs tables, not hardcoded) instead of blindly following the archetype's mix -- the Skiff/Bastion/Lancer triangle now actually matters to the AI's play. Combat feedback: - attackHit/entityKilled sim events now carry enough info for a new effects.js (transient, client-side) to drive tracer lines and death flashes, drained each render frame the same way sound events already were. Situational awareness: - A minimap (fog, deposits, units/buildings, camera viewport) that doubles as a click-to-jump camera shortcut. - An "Under Attack" banner + alarm sound + world/minimap ping when the AI lands a hit on the player, throttled so a siege …cae3dc6 -
Tier 2: rally points, Refinery upgrades, AI archetypes, and sound
Claude +1: Claude Sonnet 5Unverified signatureFour features, in dependency order (smallest/most self-contained first): ## Rally-point UI (engine/commands.js, input.js, render.js) Right-click a single selected production building (Command Center or Barracks) to set where it sends newly produced units — the engine already stored an arbitrary rally point per building and read it fresh at spawn time, there was just no way to change it. Shows a dashed line + marker for the currently selected building only, to avoid cluttering the view with every building's rally line at once. ## Refinery + upgrades (engine/entities.js, production.js, combat.js) A new building (built by a Worker like the Barracks) researches two one-time, player-wide purchases: Reinforced Plating (crystals, -15% damage taken) and Overcharged Weapons (radioactives, +20% damage dealt). Both are live multipliers applied in combat.js's damage calculation rather than baked into unit stats at spawn, so they affect a player's whole army immediately, not just future product…9eded26 -
Tier 1: camera + zoom, fog of war, and a Skiff-countering unit
Claude +1: Claude Sonnet 5Unverified signatureThree independent features, the biggest-leverage items from a brainstormed improvement list, built in dependency order (fog of war's reveal effect only means something once you can actually pan around to see it). ## Camera + zoom (camera.js, new) The whole 1600x1000 map used to be squeezed into one screen — units were tiny dots. The canvas is now a fixed-resolution viewport with a camera (pan via WASD/arrows, zoom via mouse wheel toward the cursor, clamped to map bounds) instead of a 1:1 map-to-canvas mapping. render.js draws through a camera transform; input.js's toWorld() converts screen->world through it too, so selection/commands keep working exactly as before. Fixed a latent bug while wiring this up: attachInput() was never cleaning up its event listeners, so every "Choose another battlefield" replay stacked a fresh full set on the same canvas/window — added an AbortController-based destroy(). ## Fog of war (engine/fog.js, new) A coarse grid tracks which cells are currently in…34ea8bc -
Multiple workers can assist a build; fix skiffs getting stuck fighting
Claude +1: Claude Sonnet 5Unverified signatureTwo independent fixes, both about giving the player more control: 1. Multiple workers can now speed up the same construction. Right- click a worker onto an already-founded (constructing) friendly building to send it in as an extra builder — no new cost, it's not founding a second building. updateBuildingConstruction now scales its rate by how many same-owner workers are actually standing on site (capped at 4, so massing a crowd on one site has a limit), while a lone founding worker still builds at the original, unchanged pace. issueAssistBuild (commands.js) is the new player-facing command; BUILD_REACH is now shared between production.js and sim.js so "counts as on-site" and "close enough to stop walking" agree. 2. Skiffs (and any future combat unit) could not be redirected once an enemy wandered into aggro range: updateCombat called acquireTarget unconditionally whenever there wasn't an explicit 'attack' order, so a plain 'move' order got silently…e735d2f -
Give units proactive local avoidance, not just after-the-fact separation
Claude +1: Claude Sonnet 5Unverified signaturePreviously the only anti-stacking logic was separation.js's reactive pass, run after movement each tick to push already-overlapping units apart. Units still walked straight through each other to get there — correct eventually, but visibly "stepping on" one another en route. stepToward() now senses same-owner units ahead of it (within a radius beyond their combined size) and steers laterally around them while moving, so a group naturally routes around a neighbor instead of overlapping it and getting shoved apart afterward. Opposing-owner units are still ignored, same as separation.js, so combat can still close to weapon range. Caught a real deadlock while testing this: pure radial repulsion (push directly away from the neighbor) has zero sideways component when the neighbor sits exactly on the straight-line path — by symmetry there's no "which way to dodge" signal, so a unit just stalls dead behind it forever. Fixed by steering perpendicular to the direction of travel instead of radia…9175b1f -
First playable slice: 1v1 skirmish vs scripted AI on Ferros Prime
Claude +1: Claude Sonnet 5Unverified signatureGather ore/crystals/radioactives with Workers, build a Barracks, produce Skiffs, fight, win by destroying the enemy Command Center. engine/ holds the sim: a fixed-timestep loop decoupled from render rate, movement, the gather/deposit loop, combat with auto-acquire, building construction + unit production queues, a scripted AI opponent, and the win condition. render.js/input.js are the canvas view and mouse controls (drag-select, right-click context orders, build placement); main.js wires it to a DOM HUD. Converted data.js's top-level tables to ES module exports so the engine can import them directly. Covered by 23 node:test cases including a full simulated skirmish that plays to a winner, and manually playtested end-to-end in a real browser (gather -> build -> produce -> victory/defeat UI) via Playwright. One real bug caught in that process: constructed buildings never got a `radius` copied onto the instance, which went on to poison unit spawn points and building hit-testing with Na…17641e3