Combat Mechanics

Purpose: AI-readable reference for Structs combat. Consolidates formulas, requirements, outcomes, and edge cases.


Combat Actions

Action Message Description
Attack struct-attack Direct struct-to-struct combat
Defend struct-defense-set Set struct to defense mode (blocking)
Raid planet-raid-complete Planet assault; steals unrefined ore

Ambit Targeting

Combat in Structs revolves around four ambits. Each struct operates in one ambit, and each weapon can only target specific ambits. This creates a strategic mesh where fleet composition and positioning determine what you can hit and what can hit you.

Ambit Reach Bit Value
none 1
Water 2
Land 4
Air 8
Space 16
local 32

The four combat ambits are Water/Land/Air/Space; none (1) is a placeholder and local (32) is the Command Ship’s “current ambit” flag (its weapon reaches whatever ambit it currently occupies).

Two ambit numberings — do not conflate. The bit values above are the reach bitmask (bit = 1 << enum) used for StructType.possibleAmbit and weapon-reach fields. Transaction messages (struct-build-initiate, struct-move) and a struct’s stored operatingAmbit use a different enum: none=0, water=1, land=2, air=3, space=4, local=5. When building or moving, pass the enum (the CLI takes the name space|air|land|water), not the bitmask number. See building.md — Ambit Encoding and api/integration-notes.md — Ambit.

Weapon Target Matrix

Which ambits each struct’s primary weapon can hit:

Struct Lives In Targets (Primary) Targets (Secondary)
Command Ship Any (movable) Local only (flag 32 = current ambit)
Battleship Space Land, Water (armour-piercing) Space
Starfighter Space Space Space
Frigate Space Space, Air
Pursuit Fighter Air Air
Stealth Bomber Air Land, Water
High Altitude Interceptor Air Space, Air
Mobile Artillery Land Land, Water
Tank Land Land
SAM Launcher Land Space, Air
Cruiser Water Land, Water Air
Destroyer Water Air, Water
Submersible Water Space, Water

Threatened-By Matrix

Which structs can attack into each ambit:

Target Ambit Threatened By
Space Battleship (secondary), Starfighter, Frigate, High Altitude Interceptor, SAM Launcher, Submersible
Air Frigate, Pursuit Fighter, High Altitude Interceptor, SAM Launcher, Cruiser (secondary), Destroyer
Land Battleship, Stealth Bomber, Mobile Artillery, Tank, Cruiser
Water Battleship, Stealth Bomber, Mobile Artillery, Cruiser, Destroyer, Submersible

The Command Ship can attack into any ambit but must first move there (see below). The Battleship’s armour-piercing primary covers Land and Water; its guided secondary reaches Space.

Targets per attack

struct-attack accepts a comma-separated list of target IDs, but how many of those targets a weapon engages is governed by that weapon’s primaryWeaponTargets / secondaryWeaponTargets value.

Command Ship Ambit Mobility

The Command Ship is the only struct that can change ambits (movable=true). All other structs are fixed in their operating ambit, and the chain rejects struct-move on a non-movable struct — only the Command Ship can be moved.


Health Points

Each struct has a Max HP that determines how much damage it can absorb before destruction.

Struct Max HP
Command Ship 6
Other fleet structs (IDs 2-13) 3
Baseline planetary structs (Ore Extractor, Ore Refinery, Orbital Shield Generator, Jamming Satellite, Ore Bunker, PDC) 6
Field Generator 8
Continental Power Plant 10
World Engine 10

Planetary structs are hardened so a raider cannot casually demolish a planet’s infrastructure; the power generators are the toughest (and carry armour, damage reduction 1), making a power kill a deliberate objective. Armour-piercing weapons (Battleship primary) bypass that reduction.


Damage Formulas

Multi-Shot Damage

damage = sum(successful_shots) - damageReduction
if damage >= health then health = 0
else health = health - damage
Variable Description
weaponShots Number of shots per attack (primaryWeaponShots / secondaryWeaponShots)
weaponShotSuccessRate Per-shot success (Numerator/Denominator)
weaponGuaranteedShots Minimum number of shots that hit before the success rate roll applies (primaryWeaponGuaranteedShots / secondaryWeaponGuaranteedShots)
weaponDamage Damage per successful shot
damageReduction Defense reduction (target’s attackReduction, e.g. Tank/generator armour = 1); negated when the weapon is armour-piercing
armourPiercing If the weapon is armour-piercing (primaryWeaponArmourPiercing / secondaryWeaponArmourPiercing), the target’s damageReduction is treated as 0
health Target current health

Algorithm: For shot index i in 0..weaponShots, the shot hits if i < weaponGuaranteedShots OR IsSuccessful(weaponShotSuccessRate). The first weaponGuaranteedShots shots are auto-hits; only the remaining shots roll against the success rate. Sum the damage from successful shots, then apply damageReduction (unless the weapon is armour-piercing, in which case reduction is skipped). Minimum damage after reduction is 1. Cap at target health.

Armour-piercing: A weapon flagged armour-piercing negates the target’s damage reduction during volley resolution. The Battleship’s primary is armour-piercing — it deals full damage to Tanks and to power generators (which otherwise reduce incoming damage by 1). Each shot’s piercing is reported on EventAttackShotDetail.armourPiercing.

Why guaranteed shots exist: A weapon with shots=3 and successRate=1/3 has the same expected value as a single guaranteed hit, but its variance is much higher — most attacks would deal zero damage. Setting guaranteedShots=1 floors the damage at one hit per volley while preserving the upside of the other rolls. Guaranteed shots apply only to the Starfighter Attack Run (secondary weapon, secondaryWeaponGuaranteedShots = 1); other weapons leave the field at 0, which means “no guarantee, all shots roll”.

Querying note: primaryWeaponGuaranteedShots / secondaryWeaponGuaranteedShots are omitempty in the chain’s JSON output. Because the value is 0 for every struct type except the Starfighter secondary, the field is omitted entirely from most struct_type query responses. A payload with no guaranteed-shots key means the value is 0, not that the field was removed from the chain. (Verified present in current structsd source.)

Attack results: Attack events include health results (remaining health after attack) in addition to damage amounts.

Evasion

if weaponControl == guided then successRate = guidedDefensiveSuccessRate
else successRate = unguidedDefensiveSuccessRate
canEvade = IsSuccessful(successRate) if successRate.Numerator != 0

Weapon Control vs Defense Type

The interaction between a weapon’s control type (guided/unguided) and the target’s defense type is the core of combat tactics. This matrix determines whether shots can be evaded:

Target Defense vs Guided vs Unguided
Signal Jamming (Battleship, Pursuit Fighter, Cruiser) 66% miss Full hit
Defensive Maneuver (High Alt Interceptor) Full hit 66% miss
Armour (Tank, Field Generator, Continental Power Plant, World Engine) Full hit, -1 damage Full hit, -1 damage
Stealth Mode (Stealth Bomber, Submersible) Same-ambit only Same-ambit only
Indirect Combat Module (Mobile Artillery) Full hit Full hit
None Full hit Full hit

Tactical takeaways: Use unguided weapons against Signal Jamming targets (Battleship, Pursuit Fighter, Cruiser). Use guided weapons against Defensive Maneuver targets (High Alt Interceptor). Armour reduces damage by 1 regardless of weapon control — except against armour-piercing weapons (Battleship primary), which ignore it entirely. The Battleship is the dedicated answer to Tanks and to armoured power generators.

Stealth

Stealthed structs (Stealth Bomber, Submersible) are not invisible – they can still be targeted by structs in the same ambit. Stealth blocks cross-ambit targeting only. A stealthed Submersible (water) can be attacked by other water structs, but air/land/space structs cannot target it.

Recoil Damage

Attacker takes damage after firing: health = health - weaponRecoilDamage. Recoil only applies if the attacker survives the entire shot sequence (including counter-attacks).

Post-Destruction Damage

If health == 0 and postDestructionDamage > 0, damage applies to surrounding structs.

Assigning Defenders (struct-defense-set)

Any struct can be assigned to defend another, regardless of ambit. Assignment checks only that the defender is co-located with the protected struct (same planet or fleet) and is built and online — an offline or still-building struct is rejected. Ambit does not gate assignment; it gates what the defender can do once an attack lands:

So assigning a water Submersible to defend a land struct is valid and useful: it will counter air/space-reachable attackers even though it can never block for the land struct. Spread defenders across ambits to widen counter coverage, and keep same-ambit defenders where you need actual damage interception. There is no per-ambit cap on how many structs can defend (see Edge Cases).

Blocking

if !evaded and defender exists and defender.operatingAmbit == target.operatingAmbit then
  if weapon.blockable and defender.ReadinessCheck() then
    canBlock = IsSuccessful(defender.blockingSuccessRate)

Requirements (all must be true):

  1. The shot was NOT evaded – block does not fire on evaded shots
  2. Weapon must be blockable (GetWeaponBlockable returns true)
  3. Defender must pass ReadinessCheck – struct online AND owner online
  4. Defender must be in the same ambit as the target being defended (not the attacker)

A struct cannot block for a friendly in a different ambit. Blocking is strictly same-ambit defense. Unlike counter-attacks, block is attempted on every shot (not limited to once per attack).

Counter-Attack

Each struct can counter-attack at most once per struct-attack invocation. Counter-spent state is tracked per struct per attack command (not per target, not per shot). For a 3-shot Attack Run: the defender counters on the first shot only, but can attempt to block all 3 shots. The target counters once after all shots resolve.

Counter-attacks are ambit-independent from the defended target. A space-based defender can counter-attack a space-based attacker even while defending a land-based struct. Defenders do not take counter-attack damage — only the original attacker and target can be damaged by counters.

Range rule: A struct on a fleet that is away from the home planet cannot defend planetary structs at that planet. Only on-station fleet structs and planet-based structs can defend their home planet.

Scenario Damage
Same ambit as attacker the defender type’s counterAttackSameAmbit value
Different ambit from attacker the defender type’s counterAttack value

Counter damage comes from two per-type fields, not a flat number: counterAttackSameAmbit (when the counter-attacker shares the attacker’s ambit) and counterAttack (when it does not). Most hulls are 1 / 1; the Command Ship is 2 / 2; the Destroyer is 1 / 2 (cross / same). See struct-types.md — Defensive Properties for the per-type values.

Counters are a backstop, not a damage plan. The values are small (typically 1), and an attacker striking from an ambit your structs can’t reach takes no counter at all. Real damage comes from active struct-attack volleys — build offense around attacking from a safe ambit, not around baiting counters.

Requirements (all must be true):

  1. Weapon must be counterable (GetWeaponCounterable returns true)
  2. Neither counter-attacker nor attacker is destroyed
  3. Defender’s weapons must be able to target the attacker’s ambit (via CanCounterTargetAmbit)
  4. Location reachability to the attacker:

Two types of counter-attack:

Both the defender and the target can counter-attack – an attacker may take damage from two sources per target.

Planetary Defense Cannon

damage = planetaryShieldBase + sum(defenseCannon.damage for each cannon on planet)

Other Planetary Defense Structs

Two planetary defenses are wired to structs and affect combat: the Planetary Defense Cannon and the low-orbit ballistic interceptor network (provided by the Jamming Satellite struct).


Attack Resolution Sequence

When struct-attack is executed, the following steps occur in order per target:

  1. Validation – Verify weapon ambits can reach target ambit. Stealthed targets are only targetable from the same ambit. Verify target struct exists.
  2. Stealth break – If the attacker has stealth active, it is instantly deactivated (attacking reveals position).
  3. Evasion check (per-target, not per-shot) – Evaluate weapon control (guided/unguided) vs target defense type. If evaded, ALL shots against this target miss but counters still fire.
  4. Per-shot loop (inside ResolveDefenders) – For each projectile:
  5. Target counter-attack (once per struct-attack invocation) – fires after all shots resolve. Destroyed targets cannot counter.
  6. Early termination – If the attacker is destroyed mid-sequence, remaining targets do not process.

After all targets are resolved:

  1. Recoil damage – Applied to attacker if it survived all shots
  2. Planetary Defense Cannon auto-fire – If any target was a planetary struct, PDCs fire against the attacker

Per-Projectile Events

Each projectile gets its own EventAttackShotDetail row. For a 3-shot Attack Run, the attack event contains 3 separate shot detail entries with per-projectile hit/miss breakdowns. targetPlayerId is on EventAttackShotDetail (not EventAttackDetail).

In the live struct_attack event detail, the attacker context is flat at the top and the per-shot rows are a nested array eventAttackShotDetail[]. See api/integration-notes.md — struct_attack event detail schema for the exact field layout integrators receive.

Key implications:


Requirements

Requirement Attack Raid
Operating struct online, and its owner online
Raider player online (sufficient power)
Sufficient charge
Target struct built (a struct cannot be attacked until it finishes building)
Raider fleet away (at the target)
Defender’s shields vulnerable (defender’s fleet off-station, or their Command Ship offline / destroyed / non-existent)
Raid clock started (blockStartRaid != 0)
Proof-of-work

An attack action depends only on the attacking struct (and its owner) being online — the attacker’s Command Ship does not need to be online, and the same holds for defensive changes (struct-defense-set) and stealth changes. The target must be a built struct; struct-attack against a struct that is still building is rejected (unbuilt), and a destroyed struct is rejected (destroyed). The target’s online status is irrelevant.

Note: planet-raid-complete does not consume charge (it is a proof-of-work message, not a charge message). Direct struct-attack does consume the player’s charge.


Raid Phases and SHIELDS_VULNERABLE

A raid is only winnable while the defending planet’s shields are vulnerable. Shields are vulnerable whenever the defender’s fleet is off-station (the Command Ship only defends the home planet while the fleet is on station), or the defender’s Command Ship is offline, destroyed, or non-existent. While the defender’s fleet is on station with a built, online Command Ship, the planet’s shields are up and planet-raid-complete is rejected (shields_active) no matter how much work the raider does. The single most effective raid defense is therefore keeping your fleet on station with the Command Ship online — and note that sending your own fleet away to raid someone else leaves your planet’s shields vulnerable until it returns.

The blockStartRaid attribute is the vulnerability clock, and raid PoW age is measured from it:

So a raider must catch the defender vulnerable and let the clock age before the puzzle becomes solvable. If the defender restores their shields mid-raid (Command Ship back online with the fleet on station), the clock resets and the raider must wait for the planet to become vulnerable again.

Raid statuses

Status Meaning
initiated Raider fleet has arrived at the planet
shieldsVulnerable Defender’s shields are down (fleet off-station, or Command Ship offline/destroyed) — the raid is now winnable and the clock is running
ongoing Defender restored shields mid-raid (Command Ship back online with the fleet on station) — completion blocked
raidSuccessful Raider won and seized all of the defender’s stored ore
attackerDefeated Raider’s own Command Ship was destroyed while away (trigger_raid_defeat_by_destruction) — the raiding fleet is defeated and sent home
attackerRetreated Raider withdrew before completion
demilitarized Planet has no defenders to resolve against (fleet.PeaceDeal())

Status values are the RaidStatus_* enum emitted on EventRaid. The most a defender loses to a raid is all of their stored ore (raidSuccessful).

Raid status also includes seizedOre – the amount of ore stolen during the raid, tracked on the planet_raid record. See schemas/entities/planet.md for the planet_raid table schema.

What a raid does

A successful planet-raid-complete seizes all of the defender’s storedOre, sends the raider’s fleet home, and emits raidSuccessful. Ore is the only thing a raid takes — a raid does not destroy the defending player or their structs.

Destroying the defender’s Command Ship (or catching their fleet off-station) makes the planet’s shields vulnerable (shieldsVulnerable), which is the condition that lets a raid complete. If the defender restores their shields before completion — Command Ship back online with the fleet on station — the shields return and planet-raid-complete is rejected with shields_active. So the planet is either vulnerable at completion — and the raider takes all the ore — or shielded, and the raid is rejected.

trigger_raid_defeat_by_destruction is a property of the Command Ship. When a Command Ship is destroyed while away from home (its planet’s owner differs from its own owner), its fleet is defeated: the raid ends with attackerDefeated and the fleet is sent home. This defeats an attacking fleet whose Command Ship dies during a raid.

Raid attack doctrine:

  1. Strip same-ambit blockers so your attacks reach the Command Ship (cross-ambit defenders counter but cannot block — see Blocking).
  2. Destroy the defender’s Command Ship to open the shieldsVulnerable window.
  3. Complete the raid while it is down to seize all stored ore.
  4. Expect an online defender to rebuild the Command Ship — a destroyed Command Ship can be rebuilt (see Struct Destruction), which shuts the window. Raids are most reliable against an offline defender who cannot restore it.

Edge Cases


Struct Destruction

When a struct reaches 0 HP, it is destroyed and removed from the game. The destroyed instance is gone forever and cannot be repaired. However, you can build a new struct of the same type as a replacement — full build PoW required.

Consequence Detail
Destroyed struct Instance gone forever; build a replacement (full PoW)
Lost defenders Each destroyed defender must be individually rebuilt
Rebuild cost Full PoW + power draw, same as original build

FAQ: Can I rebuild a destroyed Command Ship?

YES. A destroyed Command Ship cannot be repaired, but you can build a brand new Command Ship (type 1) to replace it. The new Command Ship gets a new struct ID and requires full build PoW (~17 min at D=3). You choose the starting ambit at build time.

Until the replacement is online, the fleet cannot move, raid, or build in space. This downtime is the real cost of losing a Command Ship. Always assign defenders to protect it via struct-defense-set before engaging in offensive operations.


See Also