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, morale, 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 scales the formation modifier — at 50 morale, both attack and defense bonuses are halved.

attack_modifier  = formation_attack  * (morale / 100)
defense_modifier = formation_defense * (morale / 100)

Morale drops: - After every battle: −20 (commander's order discipline degraded). - During combat if a flagship is destroyed: −30 (one-shot penalty). - If supply level falls below 25: −1 per tick.

Morale below 20 triggers an automatic battle end (the fleet breaks). ✅ Shipped (drop on battle end), 🚧 Partial (in-battle morale events, supply-driven decay).

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 × morale).
  • Below 25: −15% to attack and defense, and morale -= 1 per tick.
  • At 0: fleet cannot initiate combat.

Supply replenishes when the fleet is docked at a friendly station, at a rate proportional to station class.

📐 Design-only — supply system is modeled (Fleet.supply_level exists) but not yet read in combat math or written by station refuel. Target spec is the table above.

Role assignments

Each FleetMember has a role:

Role Code Combat function
Flagship flagship Commander's ship; destruction triggers fleet morale −30
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 — only Flagship has implemented combat behavior; Defender / Support / Scout role bonuses are 📐 Design-only.

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 is a flat addition to the formation attack modifier. It rewards investment in fleet size without making 50-ship blobs trivially dominant.

📐 Design-only — coordination bonus formula. Current code does not apply it.

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 × morale).
  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's morale is below 20.
  • 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 cross-team credit transfer in the system. ✅ Shipped.

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