Combat Resolver¶
Purpose¶
The combat resolver is the deterministic-but-stochastic pipeline that runs whenever an attack is initiated. It validates the attack, drains drones, exchanges damage between ships, applies destruction effects, and fires post-combat hooks (ranking, ARIA consciousness, medals, reputation, bounty). It is a single transaction so a fight either fully resolves or fully rolls back — there is no half-applied combat state.
Inputs¶
The pipeline reads:
- Attacker Player row (locked) — current_ship, turns, current_sector_id, team_id, personal_reputation, is_docked, is_landed, aria_* fields.
- Defender Player row (locked) for ship-vs-ship; or a Sector / Planet / Station for non-PvP variants.
- The defender's Ship and ShipSpecification — attack_turn_cost, hull, shields, armor, type.
- SHIP_COMBAT_MODIFIERS, WEAPON_TYPES, SHIP_DEFAULT_WEAPONS, FAST_ESCAPE_SHIP_TYPES constants.
- Drone counts (Player.attack_drones, Player.defense_drones) and any DroneDeployment rows in the sector for sector combat.
- Sector flags (combat-allowed, faction zone, region governance settings).
The system fires when an attack endpoint is invoked: POST /api/v1/combat/attack/player, .../drones, .../planet, .../port — or via fleet orders.
Process¶
Phases¶
┌─────────────────────────────────────────────┐
│ 1. Initiation │
│ - lock attacker + defender rows │
│ - regenerate attacker turn pool │
│ - validate same sector, no docking, │
│ attacker has ship, defender exists │
│ - look up turn_cost (defender ship spec) │
│ - assert combat allowed in sector │
└──────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 2. Drone screen │
│ - exchange drones round-by-round │
│ - winner keeps survivors │
│ - if attacker drones wiped + ship-v-ship │
│ continues, attacker takes hull damage │
│ bonus from defender drones │
└──────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 3. Ship-vs-ship exchange │
│ - select weapons (default by type) │
│ - apply ship-matchup multiplier │
│ - resolve N rounds (cap ~10) until one │
│ side reaches hull <= 0 or escapes │
└──────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 4. Cleanup │
│ - apply ship destruction (escape pod) │
│ - cargo theft / loss │
│ - update sector last_combat │
│ - persist CombatLog │
│ - charge attacker turns │
└──────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 5. Post-combat hooks │
│ - rank points (winner) │
│ - ARIA consciousness +1 (winner) │
│ - medal check (winner) │
│ - personal reputation deltas │
│ - bounty collection │
│ - realtime broadcast │
└─────────────────────────────────────────────┘
Damage stack — order of operations¶
For each weapon attack against a target (per round):
1. base_damage = attacker.weapon.base_damage
* attacker.attack_drones_modifier (+5% per 10 drones)
* SHIP_COMBAT_MODIFIERS.get((attacker.type, defender.type), 1.0)
* sector.modifier (nebula, radiation, etc.)
* (1 + rank.combat_bonus / 100)
2. shield_hit = min(base_damage, defender.shields)
× (1 - defender.shield_resistance)
× weapon.shield_effectiveness
3. residual = base_damage - min(base_damage, defender.shields)
hull_hit = residual
× (1 - defender.armor_rating)
× weapon.hull_effectiveness
× (1 - defender.defense_drones_modifier) (-5% per 10 drones)
4. critical = (RNG < 0.05) ? hull_hit * 0.5 : 0
5. defender.shields = max(0, defender.shields - shield_hit)
defender.hull = max(0, defender.hull - hull_hit - critical)
The order is fixed: shields absorb first, then armor + drone defense apply to the residual, then a critical-hit bonus is added directly to hull. Drones act as both first-line absorbers (in phase 2) and a passive damage-reduction modifier (in phase 3).
Weapon profile¶
| Weapon | Base | vs Shields | vs Hull |
|---|---|---|---|
| laser | 1.0 | 0.8 | 1.0 |
| plasma | 1.2 | 1.2 | 0.9 |
| missile | 1.5 | 0.6 | 1.5 |
| emp | 0.5 | 2.0 | 0.3 |
Default weapon by ship: scout = emp, defender / warp jumper = plasma, carrier = missile, others = laser.
Escape mechanic (defender side)¶
After each round, the defender may attempt to flee. Success probability:
escape_chance = 0.15
+ (FAST_ESCAPE_SHIP_TYPES ? 0.20 : 0.0)
+ (1 - defender.hull / defender.max_hull) * 0.30
+ sector_edge_proximity * 0.10
- pursuer_class_factor * 0.10
A successful escape ends combat with result = DRAW and no destruction.
Fleet-coordinated extension¶
When the attacker (or defender) is acting under fleet orders:
1. Pre-resolution: aggregate stats from all fleet member ships (formation modifies offence/defence). See ../FEATURES/gameplay/combat.md for formations.
2. The fleet commander's ship is the "primary" for damage stack purposes; member ships contribute pooled hull / shields / drones.
3. On loss, casualties are recorded as FleetBattleCasualty rows per member ship; on win, rank/reputation hooks fan out to all participants by participation share.
4. Status machine: forming → ready → deployed → in_combat → disbanded.
Outputs / state changes¶
Per combat:
- Player.turns — attacker decremented by turn_cost.
- Player.attack_drones, Player.defense_drones — both sides decremented per phase 2.
- Ship.hull, Ship.shields — both sides updated after each round.
- Ship.is_destroyed — set if hull reaches 0; the player is auto-ejected to escape pod.
- Player.current_ship_id — flipped to escape pod on destruction.
- Player.credits — bounty collected (winner) on a kill where defender had bounties.
- Player.personal_reputation — adjusted per the trigger table (see bounty-and-reputation.md).
- Player.aria_total_interactions — winner +1; consciousness level promoted at 50/150/400/1000 thresholds.
- Player.rank_points — winner gains points; promotion side-effect rolls into Player.military_rank.
- Sector.last_combat — set to now.
- CombatLog — one row per fight, with full combat_details JSON.
- FleetBattle / FleetBattleCasualty — fleet variant only.
Events emitted:
- combat_started — broadcast to both participants and the sector.
- combat_round — round-by-round delta (for spectators in the sector).
- combat_resolved — final result; piggybacks rank/medal/bounty payloads.
- ship_destroyed — global broadcast; nudges sector listeners.
- bounty_collected — to collector and (anonymized) bounty board.
Downstream systems notified: ranking, medals, bounty, personal reputation, ARIA consciousness, realtime bus.
Invariants¶
- The fight is one DB transaction. Either every mutation lands or none do.
- Both player rows are locked (
SELECT … FOR UPDATE) before any state read. attacker.turnsis only decremented after combat resolves successfully (preflight failures don't burn turns).Ship.hull ≥ 0andShip.shields ≥ 0always — no negative values.attacker.id != defender.id(no self-attack).- Defenders never spend turns to defend (
defend turn_cost = 0). - Hooks (rank, medal, reputation, bounty) are best-effort: each runs in its own try/except so a failure in one does not roll back the fight, but the failure is logged.
- A fight's
CombatLogis written before any post-combat hook fires — the audit trail exists even if hooks fail.
Failure modes¶
| Mode | Target handling |
|---|---|
| Defender already destroyed mid-flight (race) | Lock acquisition serializes; second attacker sees ship destroyed and gets a 409. |
| Attacker not in same sector by the time lock is held | Pre-flight rechecks current_sector_id after the lock; mismatch → 400 Target not in your sector. |
| Sector flagged combat-disallowed (Federation, sanctuary, region policy) | Pre-flight rejects with 400 Combat not allowed. |
| Hooks raise (e.g. medal service down) | Each hook in its own try/except; combat result still committed; error logged. |
| Insurance payout missing | Skip payout; combat log still written; player surfaced an alert. |
| Excessive round count (stalemate) | Hard cap at 10 rounds; if still alive on both sides, result = DRAW. |
| Escape pod targeting | Defender ship type = ESCAPE_POD → attacker eats −500 reputation hit on kill (kill_escape_pod). |
| Concurrent bounty collection | Both players locked; BountyService.collect_bounty uses SELECT … FOR UPDATE; only the winning attacker collects. |
| Fleet member disconnect mid-fight | Fleet aggregate stats are computed once at phase 1; later disconnects do not change resolution. Disconnected members still get casualty records. |
Source map¶
| Concern | Path (target) |
|---|---|
| Resolver entry points | services/gameserver/src/services/combat_service.py (attack_player, attack_sector_drones, attack_planet, attack_port) |
| Ship-vs-ship resolver | services/gameserver/src/services/combat_service.py:_resolve_ship_combat |
| Drone resolver | services/gameserver/src/services/combat_service.py:_resolve_drone_combat |
| Planet resolver | services/gameserver/src/services/combat_service.py:_resolve_planet_combat |
| Port resolver | services/gameserver/src/services/combat_service.py:_resolve_port_combat |
| Fleet orchestration | services/gameserver/src/services/fleet_service.py |
| Ranking hook | services/gameserver/src/services/ranking_service.py:award_rank_points |
| ARIA consciousness write | inline in combat_service.py (target: aria_consciousness_service.py) |
| Medal hook | services/gameserver/src/services/medal_service.py:check_combat_medals |
| Reputation hook | services/gameserver/src/services/personal_reputation_service.py:adjust_reputation |
| Bounty hook | services/gameserver/src/services/bounty_service.py:collect_bounty |
| Realtime broadcast | services/gameserver/src/services/websocket_service.py:send_combat_update |
| Combat log model | services/gameserver/src/models/combat_log.py |
| Combat REST routes | services/gameserver/src/api/routes/combat.py, player_combat.py |
Related¶
- DATA_MODELS:
../DATA_MODELS/combat.md,../DATA_MODELS/player.md. - FEATURES:
../FEATURES/gameplay/combat.md. - SYSTEMS: turn-regeneration.md, bounty-and-reputation.md, realtime-bus.md.
- REST API: combat & player_combat routes auto-published at
<api-host>/docs.