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 -
Make Bulwark's structure shielding official
Claude +1: Claude Sonnet 5Unverified signatureattackDamage (engine/combat.js) already applies damageTakenMult to every target it computes damage for -- units AND buildings alike, with no kind/role filter -- so a Bulwark player's turrets, Command Center, and Habitats already took reduced damage, not just their combat units. The UPGRADES.reinforcedPlating/reinforcedBulwark desc strings (updated in the prior doctrine-timing commit, since it was already touching these same entries) now say "all units and structures" instead of "all combat units", and README.md's doctrine paragraph now says so too: Bulwark is the turtle doctrine for the base as much as the army. This is documentation plus coverage of live behavior, not a new mechanic -- confirmed by reading attackDamage/performAttack/ updateBuildingCombat directly (performAttack is shared verbatim by updateCombat, updateWorkerCombat, and updateBuildingCombat, and attackDamage's damageTakenMult multiply has no target.kind check). Added two new pinning tests to test/combat.test.js: a tu…4692ce2 -
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
-
Test turret stale-target, transitive clustering, bomb blast asymmetry, scout re-pick
Claude +1: Claude Sonnet 5Unverified signatureFour defense-in-depth gaps: a turret's stale target (killed by someone else the same tick) had only the mobile-unit case covered; clusterUnits was only ever proven on all-close or all-far fixtures, never a genuine A-B-C transitive chain; a constructing building's fuse-immunity vs. blast-vulnerability asymmetry in bomb.js was unverified in either direction; and the scout's early-reveal re-pick branch was never isolated from the weaker "closes distance over time" test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F4Z8xu1oLPhhJsPnb3bgg3bb95d9b -
Test the broad-phase grid path and exact combat multipliers
Claude +1: Claude Sonnet 5Unverified signatureacquireTarget, senseLateralAvoidance, and applySeparation were only ever exercised via their O(n) fallback (no state.unitGrid populated) - not the spatial-grid path real gameplay always takes. Each new test deliberately straddles a grid cell boundary to catch a broken or silently-skipped neighbor-cell query. Also adds exact-value assertions for terrain's combatMult and the Aegis's guardAura damage reduction (including its self-exclusion), previously only checked as static data or qualitatively. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F4Z8xu1oLPhhJsPnb3bgg34a12ed2
-
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
-
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 -
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 -
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
-
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 -
Add the Lancer: close the rock-paper-scissors triangle
Claude +1: Claude Sonnet 5Unverified signatureSkiff/Bastion was a single hard counter, not a real RPS cycle. The Lancer is a third combat unit (long-ranged, armor-piercing, squishier than a Bastion) whose bonus damage targets Bastion specifically, while Skiff's existing speed/cost edge is repurposed with a new bonus against Lancer — so each unit counters exactly the matchup that would otherwise be its worst, and nothing beats all three at once. - entities.js: new lancer unit def, bonusVs wired for all three legs of the cycle, added to Barracks' production list - render.js: distinct 4-pointed star silhouette so all three combat types plus Worker read apart at a glance - main.js: Produce Lancer button - aiArchetypes.js/ai.js: bastionRatio replaced with a generic unitMix cycle per archetype (all three now include Lancer), and the AI's army filter/attack-wave logic generalized off UNITS[type].role instead of hardcoded Skiff/Bastion checks - new combat.test.js/entities.test.js coverage proving each leg of the cycle and tha…89c12eb -
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 -
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