Ace Loadout-Aware Weapon Range — Design
Ace Loadout-Aware Weapon Range — Design
Section titled “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.
Problem
Section titled “Problem”Three concrete, evidence-backed gaps:
WeaponAim(the manual, always-on right-stick weapon) has a real effective range of ~1012px (proj_speed(920) × proj_lifetime(1.1), confirmed insim/weapon_aim.gd), butAutoPlayer.AIM_RADIUS = 400.0gives up on it far earlier — Ace regularly stops aiming at a real, hittable target well within the weapon’s actual reach.- 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 frompilot.pos. Confirmed ranges vary widely and are NOT close to each other:WeaponScatter’s implicit range (proj_speed × proj_lifetime) is ~207px,WeaponRailgun’s explicitLANCE_RANGEis 900px,data/bible.jsonlistsmomentum_core=300,chronowell=460 as explicitmax_rangefields. - Ace’s current positioning thresholds are single fixed constants (
THREAT_RADIUS=300for both flee AND the engage-force cutoff,ENGAGE_DASH_MIN/MAX_RANGE=150/400for 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.
Non-goals
Section titled “Non-goals”- 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/simchange 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’sfleeterm, driven by the existingTHREAT_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 (currentlyTHREAT_RADIUSconflates both). - The manual weapon’s range fix is independent of the loadout-median work —
WeaponAimis 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.
Architecture
Section titled “Architecture”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) -> floatThree 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’smax_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 onTHREAT_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.
4. Per-pilot, not global
Section titled “4. Per-pilot, not global”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.
Testing
Section titled “Testing”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
PilotArsenalloadout (a small number of weapons with known/mocked ranges), asserting the median is computed correctly andWeaponAimis 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 intests/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_sinkdamage-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,/simuntouched).
Approval note
Section titled “Approval note”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.