Fleet Tactics¶
Status: 🚧 Partial — fleet formation and coordinated combat shipped at moderate scale; large-scale (100v100+) tier remains design-only. (re-verified 2026-08-21 vs Sectorwars2102
46bce720.)
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. Friendliness is enforced: resupply_fleet calls check_friendly_port (fleet_service.py:619-630) and rejects resupply at a hostile station — ruled 2026-08-07 (DECISIONS.md fleet-resupply-friendly-station).
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 |
+10% first-shot bonus, −5% defense |
✅ Shipped — FleetRole enum and FleetMember.role field.
✅ Shipped 2026-08-04. The Scout magnitude (+10% first-shot / −5% defense, symmetric with Defender's +10% absorption) resolves the NO-CANON gap per ADR-0093 item 6 (folded from ADR-0093, re-verified 2026-08-07). Defender (+10% damage absorption when targeted) and Scout (+10% first-shot outgoing damage on round 1 only, +5% incoming damage taken when targeted — fleet_service.py:SCOUT_FIRST_SHOT_MULT=1.10 / SCOUT_DEFENSE_PENALTY_MULT=1.05, scout_outgoing_mult/role_incoming_damage_mult) are both implemented in combat. Support (+5% repair regen) is now wired too: an in-battle repair mechanic (_apply_round_repair) runs at the end of every combat round — every active ship regens BASE_REPAIR_REGEN_PCT of its max_hull, boosted by SUPPORT_REPAIR_REGEN_MULT (the canon +5%) fleet-wide if any active Support-role ship is present in the fleet (no positional/distance system exists in fleet battles, so "nearby" is interpreted fleet-wide, matching every other role bonus in this module). 📐 NO-CANON: BASE_REPAIR_REGEN_PCT (2%/round) is a conservative invented baseline — canon only specified the Support MODIFIER, not a base rate for it to modify — flagged for a DECISIONS.md ruling.
A fleet can have at most one Flagship. The commander is automatically the flagship's pilot. If the flagship is destroyed (or otherwise removed) and ships remain, leadership ✅ automatically transitions to the next-most-senior surviving member via FleetService._promote_flagship_successor (fleet_service.py:371-427, WO-FLEET-CASUALTY-SUCCESSION) — earliest FleetMember.joined_at, ties broken by lowest member UUID; fleet.commander_id transfers in the same transaction when the fallen pilot held command. (Seniority definition ratified in DECISIONS.md fleet-flagship-seniority-definition.)
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¶
Status: Consolidated ✅/🚧/📐 markers live in Player-facing affordances below. This section is retained as a cross-link only — do not read it as an independent live/pending claim.
See Player-facing affordances for tip-honest fleet UI status vs the open land stack (PR #590, #637, #639, #697).
Player-facing affordances¶
- ✅ GS fleet service + API client —
fleet_service.pyandfleetAPIinservices/player-client/src/services/api.ts(create / get / add / remove / formation / disband / battle) are shipped on tip; no.movehelper on the client (origin/feat46bce720). - 🚧 Fleet manager panel + roster —
FleetManagerPanel.tsxis absent on tip (grep empty); tip-pending on LEG-INI-01 / PR #590 and stacked #637 / #639 — none tip-ancestor of46bce720. Do not claim tip mounts the panel or posts fleet unit-move. - 🚧 Formation picker + attack/defense preview — tip-pending PR #697 @
b9224946(not tip-ancestor). No fleet panel → no formation<select>/ multiplier preview on tip. - 🚧 Morale and supply gauges — same #697 land stack; no fleet panel to render gauges or raw morale/supply numbers on tip.
- 🚧 Active battle viewer + round log — same #697 UI surface; full round log also needs player
battle_logonGET /fleets/battles/{id}(LEG-400, still open) — do not claim tip exposes the array.
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.