Fleet Tactics¶
A fleet is a group of ships fighting as one. Where solo combat resolves in combat.md, fleets add formations, supply, role assignments, and multi-ship coordination bonuses that turn coordinated teams into a force multiplier — or, if mismanaged, an expensive single point of failure.
A fleet is owned by a Team. Ships are added by their owners; the fleet aggregates each ship's combat stats and applies the fleet's formation modifier on top.
Lifecycle states¶
FORMING --(add ships)--> READY --(initiate_battle)--> IN_BATTLE
^ | |
| | (battle ends)
| v |
+--------------------- DISBANDED <--(disband)----------+
^
(no ships left)
FleetStatus values: forming, ready, in_battle, retreating, disbanded. Ships can only be added while the fleet is forming or ready. Movement is blocked during in_battle. ✅ Shipped (state transitions enforced by FleetService).
Formation modifiers¶
Formation is a single string field on the fleet, applied to all members when computing damage and defense. The formation can be changed any time the fleet isn't in battle.
| Formation | Attack | Defense | Used when |
|---|---|---|---|
standard |
×1.00 | ×1.00 | Default — no specialization |
aggressive (Wedge) |
×1.15 | ×0.85 | Pushing into an enemy fleet, going for kills |
defensive |
×0.85 | ×1.15 | Protecting a high-value target or cargo run |
flanking (Offensive) |
×1.10 | ×0.90 | Attacking from flank — moderate aggression |
turtle (Scatter / max defense) |
×0.60 | ×1.40 | Surviving until reinforcements arrive |
✅ Shipped — modifier table in FleetService._calculate_formation_bonus.
The five names in the description above (Standard / Wedge / Defensive / Offensive / Scatter) correspond to the five code values (standard / aggressive / defensive / flanking / turtle) — UI surface picks the friendly name; the code stores the enum.
Morale¶
Each fleet has a morale score 0–100, initialized at 100 on creation. Morale is a cosmetic/display stat — it surfaces in the fleet UI and narrative flavor but is not a combat factor: it does not scale the formation modifier, does not gate battle termination, and contributes no damage multiplier. Combat math is driven entirely by formation, supply, and the coordination bonus.
✅ Shipped — Fleet.morale is a display column read by the fleet UI; combat resolution does not consult it.
Supply¶
Fleet.supply_level is 0–100. Tracks the fleet's logistics tail — fuel, ammunition, food.
- Above 50: no penalty.
- 25–50: −5% to attack and defense (compounded with formation).
- Below 25: −15% to attack and defense.
- At 0: fleet cannot initiate combat.
Supply replenishes when the fleet is docked at a friendly station, at a rate proportional to station class.
✅ Live (combat math) — Fleet.supply_level is read in _calculate_formation_bonus: above 50 no penalty, 25–50 −5%, below 25 −15% to both attack and defense (compounded on top of the formation modifier), and initiate_battle is blocked at 0 supply. Proven on dev 2026-06-18 (bands + compounding exact).
✅ Shipped — station resupply raises supply, unblocking the 0-supply combat lock. FleetService.resupply_fleet (route POST /fleets/{id}/resupply) is the only path that raises Fleet.supply_level: it row-locks the fleet and paying player, requires the player be a fleet member docked at a station in the fleet's sector, charges credits per supply point, caps at 100, and rejects when full / insufficient credits / in battle. The per-supply-point cost and the per-class restore ceiling are a NO-CANON kernel awaiting a DECISIONS.md ruling (canon says "friendly station"; the kernel accepts any docked station for now).
Role assignments¶
Each FleetMember has a role:
| Role | Code | Combat function |
|---|---|---|
| Flagship | flagship |
Commander's ship; at most one per fleet, pilots fleet leadership |
| Attacker | attacker |
Default; full firepower contribution |
| Defender | defender |
+10% damage absorption when targeted |
| Support | support |
+5% repair regen to nearby members |
| Scout | scout |
First-shot bonus, lower defense |
✅ Shipped — FleetRole enum and FleetMember.role field.
🚧 Partial — Defender (+10% damage absorption when targeted) is ✅ implemented in combat. Support (+5% repair regen) and Scout (first-shot bonus, lower defense) remain 📐 Design-only — Support needs an in-battle repair mechanic and Scout has no canon magnitude for the first-shot bonus / defense reduction (NO-CANON; awaits a Max ruling before implementation).
A fleet can have at most one Flagship. The commander is automatically the flagship's pilot. If the flagship is destroyed and the commander survives in another ship, leadership transitions to the next-most-senior member (target spec — succession is currently manual).
Multi-ship coordination bonuses¶
When a fleet has 3 or more ships, it gains coordination bonuses that scale with size:
coord_bonus = min(0.20, (ships - 2) * 0.025)
| Fleet size | Coordination bonus |
|---|---|
| 1–2 | 0% |
| 3 | +2.5% |
| 5 | +7.5% |
| 8 | +15% |
| 10+ | +20% (capped) |
Coordination bonus rewards investment in fleet size without making 50-ship blobs trivially dominant.
✅ Live — fleet_service computes coordination_bonus = min(0.20, max(0,(ships-2)*0.025)), persists it on every roster change (create / add / remove / KIA), and the fleet damage resolver applies it per ADR-0061 S-I3 as the outer multiplier × (1 + coordination_bonus). Proven on dev 2026-06-18 (formula exact across the size table; baseline identical at ≤2 ships).
Battle resolution (fleet vs fleet)¶
Two fleets in the same sector can engage. The resolver runs round-by-round simulation until a termination condition is met.
Round shape¶
For each round (called by simulate_battle_round):
- Lock the battle row.
- Compute formation bonuses for both fleets (formation, supply-banded).
- For each active attacker ship: 70% hit chance → roll damage =
attack_rating × 10 × attack_bonus × U(0.8, 1.2). Pick a random defender ship. Apply damage (defense bonus reduces incoming). - For each active defender ship: same as step 3 in reverse.
- Damage hits shields first, then hull. Hull ≤ 0 → ship destroyed; record
FleetBattleCasualtyand remove from fleet. - Surviving ships below 30% hull have a 30% chance to retreat (recorded as casualty with
retreated=True). - Append round summary to
FleetBattle.battle_log.
Phase progression¶
Battle phase transitions purely on round count:
- Rounds 1–5: engagement
- Rounds 6–15: main_battle
- Rounds 16+: pursuit
- Battle end: aftermath
✅ Shipped.
Termination¶
The battle ends when any of the following are true:
- One side has zero active ships.
- Either fleet has lost more than 70% of its initial ships (destroyed + retreated).
- 30 rounds elapsed.
Winner is determined by remaining strength (hull + shields). A 1.5× advantage is required for a decisive victory; otherwise the result is draw.
Loot¶
The winner's team treasury gains 10% of the loser's team treasury. This is the only involuntary / combat-driven cross-team credit transfer in the system. ✅ Shipped.
Kill rewards (reputation + bounty)¶
When a fleet kill destroys an enemy ship, the per-kill reputation and bounty are split across the distinct participating members of the killing fleet, mirroring the solo destroy-ship hooks but resolved per member. Each collecting member resolves their own even share of the system bounty pot through their own per-(hunter, target) claim ledger — a member who already holds a paid claim against that target contributes zero — so the fleet total is entitlement-bounded (it may be less than a solo single-kill, which is canon-correct) and collector-rotation alt-farming is impossible. Reputation splits in parallel: each collecting member earns the defeat_bounty_target standing when the target carried a bounty, and the attack_innocent penalty is split across participants when the target carried none.
✅ Shipped — FleetService._distribute_fleet_kill_rewards fires once per genuinely destroyed (non-escape-pod) ship inside the kill resolver, awarding each distinct member their own share via BountyService.collect_bounty_share (locks each member's row, pays only against their own unclaimed entitlement) and FleetService._split_reputation. Even-split is the canon fallback (no per-attacker damage ledger).
Per-ship contribution¶
When fleet stats are aggregated:
- total_firepower = Σ ship.combat["attack_rating"]
- total_shields = Σ ship.combat["shields"]
- total_hull = Σ ship.combat["hull"]
- average_speed = mean(ship.current_speed)
These are denormalized onto the fleet row for fast UI reads. Recomputed on every add/remove. ✅ Shipped (_recalculate_fleet_stats).
Casualty bookkeeping¶
Each ship lost (destroyed or retreated) writes a FleetBattleCasualty row capturing:
ship_name,ship_type(preserved if the ship row is later deleted).was_attacker— which side lost it.destroyedvsretreated.damage_taken,damage_dealt,kills(target spec — currentlydamage_dealtandkillsare not populated).battle_phaseat time of casualty.
After the battle, the FleetBattle.battle_log JSON contains the per-round summary plus the aftermath entry.
UI surface¶
Fleet management UI lives in the player client: - Fleet creation and member roster. - Formation picker with attack/defense preview. - Morale and supply gauges. - Active battle viewer with round-by-round log.
🚧 Partial — service-layer is solid; client UI for fleet management is functional but minimal.
Source map¶
| Concern | Path (target) |
|---|---|
| Fleet service | services/gameserver/src/services/fleet_service.py |
| Fleet models | services/gameserver/src/models/fleet.py (Fleet, FleetMember, FleetBattle, FleetBattleCasualty) |
| Role / status / phase enums | same file (FleetRole, FleetStatus, BattlePhase) |
| Ship combat JSONB | services/gameserver/src/models/ship.py (Ship.combat) |
| Casualty processing | FleetService._record_ship_casualty |
| Realtime broadcast | services/gameserver/src/services/websocket_service.py |
Related¶
combat.md— solo ship combat resolver.SYSTEMS/fleet-coordination.md— fleet state machine, formation math, casualty contract.SYSTEMS/combat-resolver.md— single-ship resolution that fleet-vs-fleet is composed from.factions-and-teams.md— Team that owns the fleet.