Inside the Combat AI: Broadsides, Evasive Rolls, and the Anti-Pileup Problem
There's no scripted "boss AI" here — every ship, friendly or hostile, runs the exact same per-frame decision loop. This is a walkthrough of that loop: how a ship picks a target, decides whether to close head-on or swing broadside, and reacts when it starts taking damage on one side.
One function, every ship
All combat behavior lives in a single function, _updateCombatAI(),
called once per frame for every ship on the map — player-controlled or
AI, friendly or enemy. There's no separate "smart" path for enemies;
the only asymmetry is orders (a player can override a friendly ship's
stance), never capability.
Underneath it, a spatial hash grid (120-unit cells, rebuilt once per frame) turns every "find nearby ships" query from an O(n²) scan into a handful of bucket lookups — that's what makes it affordable to run this logic on every ship, every frame, without the frame rate falling over once a battle has 20+ ships in it.
Picking a target: pursuit class and the anti-pileup cap
Every ship type has a pursuitClass — capital,
fighter, bomber, or utility — set
in its YAML definition, the same idea as FreeSpace 2's
ai_actively_pursues table. It changes who a ship even
considers as a target:
- Capital-class ships (frigates, cruisers, destroyers, dreadnoughts, carriers) prefer other capital/utility targets and only engage fighters opportunistically, within 50 units — they don't chase them.
- Fighters and drones chase whatever's nearest, capital or fighter.
- Bombers behave like capitals — they're strike craft going after real targets, not dogfighters.
- Utility hulls (collectors, cargo haulers, Coms Frigates, recon) never self-initiate a fight on sensor contact — only if explicitly ordered to attack.
There's also a hard cap: MAX_ATTACKERS = 3. Before a ship
locks a fresh target, it checks how many other ships are already
attacking that candidate this frame — if it's already got 3 attackers,
the pick is skipped in favor of a less-contested target. This is checked
and updated live within the same frame's loop (not just once at the top),
so ships processed later in the pass see the picks made earlier in it —
otherwise every ship evaluates the same stale snapshot and they all
still dogpile the same target simultaneously.
STICKY_SWITCH_FACTOR = 0.7).
Head-on or broadside? It depends on the loadout
A ship's hardpoints each have a facing angle. If any equipped hardpoint faces roughly sideways (60°–120° or 240°–300° from the bow), the ship is flagged as having a broadside weapon and fights by presenting a flank instead of pointing its nose at the target. Pure forward-mounted loadouts skip all of that and just drive straight at the enemy, stopping at weapon range.
The engagement distance itself — idealRange — is a
DPS-weighted average across every weapon in the loadout, not the naive
minimum range of any single weapon.
The broadside maneuver
Once a target's locked, the ship computes two candidate broadside positions — one on either side of the target, perpendicular to the bearing between them — and picks whichever is closer, with hysteresis: it only abandons its current side if the other one is more than 30% closer, so it doesn't flip-flop sides every frame while both distances are similar.
That approach bearing is locked for the whole maneuver interval once chosen. Without locking it, the bearing keeps recalculating as the ship moves — since the ship is moving roughly perpendicular to the enemy during a broadside pass, the target position it's steering toward would itself keep orbiting and never settle.
On top of the base position, two more offsets get layered in every time the maneuver re-triggers:
- Juke — a random slide along the broadside axis, so ships don't sit at a perfectly static firing solution.
- Vertical split — ships bias toward a positive or negative altitude offset (±30 units) and occasionally flip it, so a fight isn't flat on one plane.
Skill: turn-rate ratio drives everything
There's no explicit "difficulty" stat. Instead, every maneuver decision
scales off skillRatio — the ship's own turn rate divided by
its current target's turn rate, capped at 5× (skillFactor).
A ship that out-turns its target re-evaluates its maneuver more often,
jukes harder, and presses to a tighter range; a ship that's outclassed
re-evaluates less often and tends to hang back.
0.2 × skillRatio)
became mathematically guaranteed on every single cycle. In practice:
fast ships visibly bounced up and down on the Y axis instead of
occasionally flipping altitude. Capping skillFactor at 5
(rather than using the raw ratio) fixed it across the board.
Evasive rolls: reacting to actual damage
Each ship tracks how many hits it's taken on its port and starboard side
(_hitSides), decaying those counters continuously so old
damage doesn't linger forever. If one side has taken 3 or more hits than
the other, the ship flips which broadside it presents — literally an
evasive roll, presenting the less-damaged flank — and resets both
counters so it doesn't immediately flip back. A 12-second cooldown after
any flip prevents thrashing between sides if both flanks keep taking
roughly even damage.
Retreat and kamikaze: what happens at zero ammo
Ships with only guided weapons (missiles, torpedoes) and no energy weapon in their loadout check their remaining ammo every frame. Once every guided weapon reads zero:
- Regular ships fall back toward their own side's largest capital ship and hold station near it, rather than sitting uselessly in a fight they can no longer contribute to.
- Drones do the opposite — if their current target is a capital-class ship, they switch to kamikaze mode and ram it instead of retreating, dealing a flat 80 damage on impact and destroying themselves in the process.
The short version
None of this is a single "AI system" so much as a stack of independently reasonable rules — pursuit class limits who's a valid target, the attacker cap spreads engagement across a fleet instead of dogpiling, stickiness keeps a lock stable long enough to actually close distance, DPS-weighted range keeps mixed loadouts fighting at a sane distance, and skill ratio scales maneuver aggressiveness without a difficulty slider anywhere. Most of the interesting bugs above came from one formula quietly assuming a narrower range of inputs than the game actually produces once fighters and capital ships share the same code path.