Skip to content

Ace Loadout-Aware Weapon Range — Design

Date: 2026-07-17 Status: Approved (Chris, 2026-07-17, live in chat) Origin: Chris asked whether Ace could be tuned to “make most use of his weapons by trying to kill off any enemies as fast as possible taking into account weapon range etc.” Investigated before designing (not guessed): Ace’s manual-weapon aim cutoff (AIM_RADIUS = 400) and its engage/dash positioning (fixed 300px flee threshold, 150-400px proactive-dash window) are both constants invented independently of the actual equipped loadout, not derived from it.

Three concrete, evidence-backed gaps:

  1. WeaponAim (the manual, always-on right-stick weapon) has a real effective range of ~1012px (proj_speed(920) × proj_lifetime(1.1), confirmed in sim/weapon_aim.gd), but AutoPlayer.AIM_RADIUS = 400.0 gives up on it far earlier — Ace regularly stops aiming at a real, hittable target well within the weapon’s actual reach.
  2. Auto-firing weapons (Blaster, Scatter, Railgun, etc.) pick their own targets entirely inside /sim (nearest/farthest-enemy search) — the AI has zero influence over WHICH enemy they hit, only over WHERE THE PILOT STANDS, since every weapon’s range is measured from pilot.pos. Confirmed ranges vary widely and are NOT close to each other: WeaponScatter’s implicit range (proj_speed × proj_lifetime) is ~207px, WeaponRailgun’s explicit LANCE_RANGE is 900px, data/bible.json lists momentum_core=300, chronowell=460 as explicit max_range fields.
  3. Ace’s current positioning thresholds are single fixed constants (THREAT_RADIUS=300 for both flee AND the engage-force cutoff, ENGAGE_DASH_MIN/MAX_RANGE=150/400 for proactive dash) — coincidentally in the right ballpark for SOME loadouts, but not derived from whatever’s actually equipped. A ship carrying a short-range weapon (Scatter, ~207px) could sit at Ace’s current typical engagement distance and have that weapon slot never contribute at all.
  • No change to which enemy an auto-weapon targets. That selection logic lives entirely inside /sim (sim/weapon_*.gd’s own nearest/farthest-enemy search) — changing it would be a /sim change requiring a determinism re-pin, which is out of scope for what Chris asked (AI positioning/aim, not weapon target-selection logic itself).
  • No per-weapon-type placement optimization. Actually computing “which position brings the most weapons into range simultaneously” each tick is a real optimization problem — YAGNI here; a single loadout-derived reference distance (median range, see Architecture) captures most of the value far more simply and cheaply.
  • Flee behavior is untouched. Survival positioning (compute_move_for’s flee term, driven by the existing THREAT_RADIUS) stays exactly as tuned earlier tonight — this spec only changes how eagerly Ace closes distance to actually USE its weapons, not how it evades danger. The two concepts get properly separated (currently THREAT_RADIUS conflates both).
  • The manual weapon’s range fix is independent of the loadout-median work — WeaponAim is always equipped and doesn’t benefit from “getting closer” the way auto-weapons do (it’s player/AI-aimed, not auto-targeted), so its range gets computed and used on its own.

1. ai/weapon_range.gd (new) — class_name WeaponRange extends RefCounted

Section titled “1. ai/weapon_range.gd (new) — class_name WeaponRange extends RefCounted”

A pure dispatch function reading each weapon INSTANCE’s own fields (not a hardcoded per-id table — these are mutable instance vars, confirmed via sim/weapon_aim.gd:20-22, sim/weapon_blaster.gd:11-26, sim/weapon_railgun.gd:15-22, so ship bonuses or future rebalances that touch proj_speed/proj_lifetime/range consts stay correct automatically):

static func effective_range(weapon: Object) -> float

Three dispatch categories, confirmed to already exist in the codebase (exact per-class field names for every one of the ~15 weapon archetypes get verified during plan-writing, not guessed here — this spec fixes the three CATEGORIES, not every individual weapon’s exact field name):

  • Speed × lifetime (Blaster, Aim, Scatter-style projectile weapons): proj_speed * proj_lifetime.
  • Explicit range field (Railgun’s lance_range, Momentum Core’s/Chronowell’s max_range).
  • Melee reach (melee_range-style fields for close-combat weapons).

2. Ace’s engage/dash positioning becomes loadout-derived

Section titled “2. Ace’s engage/dash positioning becomes loadout-derived”

SkilledAutoPlayer gains a helper computing the MEDIAN WeaponRange.effective_range() across pilot.arsenal.active_weapon_ids (via pilot.arsenal.weapon_by_id) — excluding WeaponAim (handled separately, see below). Median, not shortest-range or longest-range (Chris’s call, confirmed in chat): balances bringing more of the loadout into use against not forcing risky close-range positioning just because one short-range weapon happens to be equipped.

This median REPLACES two currently-fixed values, properly separated from the (unchanged) flee threshold:

  • The engage force’s “close enough already, stop pulling” cutoff in compute_move_for() (currently piggybacks on THREAT_RADIUS, which is really about flee — gets its own new, loadout-derived value instead).
  • wants_dash()’s proactive engage-dash window (currently the fixed [150, 400] — becomes a window centered on the median range instead).

3. The manual weapon’s aim cutoff becomes computed, not hardcoded

Section titled “3. The manual weapon’s aim cutoff becomes computed, not hardcoded”

AutoPlayer.AIM_RADIUS (currently a flat 400.0) is replaced with a value computed via WeaponRange.effective_range() against the pilot’s own WeaponAim instance — this alone fixes gap #1 above, independent of the loadout-median work, since it doesn’t depend on what other weapons are equipped.

Both the median-range computation and the aim-range fix take pilot: PlayerState (matching every other SkilledAutoPlayer method’s existing signature convention) — this generalizes correctly to co-op’s second pilot with zero special-casing, since each pilot owns its own arsenal.

  • WeaponRange.effective_range(): pure, testable per dispatch category with synthetic weapon instances (or the real weapon classes constructed directly, matching how existing weapon tests in this codebase already do it).
  • The median-range helper: testable with a synthetic PilotArsenal loadout (a small number of weapons with known/mocked ranges), asserting the median is computed correctly and WeaponAim is excluded.
  • The updated engage-cutoff and proactive-dash-window logic: unit tests with synthetic loadouts of known median range, asserting the new behavior at representative distances (matching the style of the existing test_engage_force_*/test_proactive_dash_* tests already in tests/test_skilled_auto_player.gd).
  • Real effect: a same-seed soak A/B against the current shipped behavior (this session’s now- established validation method for every AI tuning change), checking BOTH survival time and a new “weapon uptime” signal if cheap to add (e.g. how many of the equipped weapons’ stats_sink damage-by-context buckets are non-zero over the run) — not just movement in isolation.
  • Full suite + count guard + boot smoke; no determinism re-pin expected (ai/-only, /sim untouched).

Approved live in chat after one targeted clarifying question (median vs. shortest vs. longest weapon range as the positioning target) and a design presented in sections — same pattern as every other AI-tooling spec tonight. Proceeding to writing-plans.