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:
- Distress. A gather ship’s brain (
GatherBrain) subscribes to its ownHealth.Damagedand raisesEventBus.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. - Pirate hunts. A pirate bastion (a
StationwithhunterTemplate) runs aPirateHunterBaythat periodically launches hunters up to a cap and points them at gather ships. A hunter is a plainShipBrainon 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. - Gated response. A watch station (a
StationwithdefenderTemplate) runs aDefenseDispatchthat 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 itsDefenseLedgercan pay. A starved ledger cannot, so the call goes unanswered andEventBus.DefenseStarvedfires. 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 centralShipto 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 isShipBrain+ Pursue + a prey preference; a defender isShipBrain+ Pursue + standings. The only genuinely new AI is the optional prey preference, a no-op default added toShipBrain/Targetingso 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 addinghunterTemplateto 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_kardapresent, butConflictSimstill force-presents it (matching the documented economy-sim landmine) so the sandbox runs even without a ship resolving into it.