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 -= 1per 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):
- Lock the battle row.
- Compute formation bonuses for both fleets (formation × morale).
- 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'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.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.