Skip to content

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

  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

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 clientfleet_service.py and fleetAPI in services/player-client/src/services/api.ts (create / get / add / remove / formation / disband / battle) are shipped on tip; no .move helper on the client (origin/feat 46bce720).
  • 🚧 Fleet manager panel + rosterFleetManagerPanel.tsx is absent on tip (grep empty); tip-pending on LEG-INI-01 / PR #590 and stacked #637 / #639 — none tip-ancestor of 46bce720. 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_log on GET /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