← Design Design

Conflict ecology — the economy fights back (MVP)

Captured 2026-07-19. The "[Combat/AI] Conflict ecology MVP + fleet combat test" card. The codex states what IS (`assets/codex/conflict-ecology.md`); this states WHY, and where the…

Captured 2026-07-19. The “[Combat/AI] Conflict ecology MVP + fleet combat test” card. The codex states what IS (assets/codex/conflict-ecology.md); this states WHY, and where the seams are.

The chain

A closed loop that makes a belt a contested place instead of a still life:

  1. Distress. A gather ship’s brain (GatherBrain) subscribes to its own Health.Damaged and raises EventBus.Distress(FactionDistress) when it takes fire — the economy calling for help. Debounced per ship (DistressDebounce, sim-time) so a sustained skirmish is one call per window, not one per bullet.
  2. Pirate hunts. A pirate bastion (a Station with hunterTemplate) runs a PirateHunterBay that periodically launches hunters up to a cap and points them at gather ships. A hunter is a plain ShipBrain on the Pursue doctrine with a prey preference (ShipBrain.PreyGroup + Targeting.Acquire): a hostile in the gatherers group within range is chosen ahead of a closer non-prey hostile, so a hunter goes for a defenceless prospector over the armed player. No prey left in the system → the base recalls (despawns) its hunters.
  3. Gated response. A watch station (a Station with defenderTemplate) runs a DefenseDispatch that listens for distress from its own or a friendly faction and scrambles a defender (ShipBrain, Pursue — engages the raiders by standings, no per-attacker wiring) — but only if its DefenseLedger can pay. A starved ledger cannot, so the call goes unanswered and EventBus.DefenseStarved fires. Defenders stand down (despawn) once the shooting stops.

Everything is sim-time (accumulated scaled delta), so Engine.TimeScale compresses the whole loop honestly. Both role components are children of a sim-tier Station, so they inherit presence gating for free — a frozen system’s base spawns nothing.

The ledger, and its seam (the owner’s explicit requirement)

The gate is grounded in something real: a station’s defense materials, the worth of the ore its prospectors bring home. GatherBrain banks the value of each unloaded hold (DefenseLedger.ValueOf, units × commodity base worth) into the destination station’s DefenseLedger; DefenseDispatch spends it, DefenderCost per defender. A well-worked belt funds its own guardians; a dead one defends no one. The ledger is a pure, Godot-free class so the credit/debit math is unit-tested without a runtime, and StationDef.DefenseMaterials seeds a starting balance (and is the knob the “starved variant” test zeroes).

SEAM: DefenseLedger is a deliberate stand-in for real per-commodity station stock (docs/design-stations.md, docs/design-economy.md “Trade data access”, the EconomySnapshot Quantity plug point that reads 0 today). It is a bare float on purpose. When station stock lands, a defender debit becomes a withdrawal of a munitions/alloy commodity from that stock and this class retires — nothing above the ledger (the dispatch logic) knows or cares that today it is a float. The class header and StationDef.DefenseMaterials both mark this.

Why these factions / why a militia watch

The prey are independent prospectors (ship.independent_miner_xs, the existing gather hull). The hunters are pirates (ship.raider_fighter_xs). The responders are militia (ship.militia_fighter_xs — the one combat hull that ships for a non-pirate/civilian faction). DefenseDispatch answers a distress call from its own faction OR a friendly one, so a militia watch answering independent prospectors (standings +50) is exactly the “someone who can afford to come” story — and it sidesteps the fact that no independent combat hull exists yet. A future all-independent variant needs only an independent fighter template; the dispatch logic is faction-agnostic.

GatherBrain.NearestStation now also skips stations its faction is hostile to — a prospector will not haul its cargo into a pirate bastion (which would also wrongly bank the ore into the enemy’s ledger). Sensible on its own; load- bearing here.

Testbed + how to run it

--vindicator-conflict-sim (ConflictSim autoload, mirrors EconomySim) swaps the universe for the single compact system under features/universe/data/conflict/ (conflict_karda), force-presents it, and accelerates. scripts/conflict-sim.ps1 launches it windowed; scripts/capture.ps1 -ConflictSim grabs a still (both belt roles and the star sit within ~1900px so one frame holds miners, hunters, and defenders). The system is one belt, a militia watch (seeded ledger), a pirate bastion, and four independent miners — laid out so the fight converges bottom-center for capture.

Alternatives considered / rejected

  • Raise distress from Ship.OnDamaged. Rejected: it couples the central Ship to the conflict feature and every hull would carry the check. The gather ship’s own brain owning the behavior is cleaner and confines the coupling to the Ai/Conflict features that already depend on each other.
  • A dedicated HunterBrain / DefenderBrain. Rejected as over-built: a hunter is ShipBrain + Pursue + a prey preference; a defender is ShipBrain + Pursue + standings. The only genuinely new AI is the optional prey preference, a no-op default added to ShipBrain/Targeting so every existing brain is unchanged.
  • Enable hunts on the shipping Karda bastions (karda.thornpicket, marches.gibbet). Deferred: it changes shipping balance and is out of the MVP’s scope, which is the deterministic testbed. The mechanic is wired generically — any bastion opts in by adding hunterTemplate to its def — so turning it on in the live universe later is a one-line content change.
  • Full station stock now. Out of scope (see the SEAM above). A minimal per-station materials counter fed by unloads is enough to prove the gate and wires cleanly for the real system to replace.
  • Force-present skipped (single system at origin). The player already keeps conflict_karda present, but ConflictSim still force-presents it (matching the documented economy-sim landmine) so the sandbox runs even without a ship resolving into it.