Skip to content

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):

  1. Lock the battle row.
  2. Compute formation bonuses for both fleets (formation, supply-banded).
  3. 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).
  4. For each active defender ship: same as step 3 in reverse.
  5. Damage hits shields first, then hull. Hull ≤ 0 → ship destroyed; record FleetBattleCasualty and remove from fleet.
  6. Surviving ships below 30% hull have a 30% chance to retreat (recorded as casualty with retreated=True).
  7. 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.
  • destroyed vs retreated.
  • damage_taken, damage_dealt, kills (target spec — currently damage_dealt and kills are not populated).
  • battle_phase at 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