Skip to content

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 ShipSpecificationattack_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

  1. The fight is one DB transaction. Either every mutation lands or none do.
  2. Both player rows are locked (SELECT … FOR UPDATE) before any state read.
  3. attacker.turns is only decremented after combat resolves successfully (preflight failures don't burn turns).
  4. Ship.hull ≥ 0 and Ship.shields ≥ 0 always — no negative values.
  5. attacker.id != defender.id (no self-attack).
  6. Defenders never spend turns to defend (defend turn_cost = 0).
  7. 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.
  8. A fight's CombatLog is 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