Drone System — Phase 2 (Classes) Implementation Plan
Drone System — Phase 2 (Classes) Implementation Plan
Section titled “Drone System — Phase 2 (Classes) Implementation Plan”For agentic workers: REQUIRED SUB-SKILL: superpowers:subagent-driven-development or executing-plans. Each task = one
bh-dev-chunkcycle (TDD →--import→ boot-check → full GUT suite →scripts/check-test-count.sh→ determinism). Steps use- [ ].
Goal: Turn the Phase-1 drone framework into the build-craft layer — four new drone classes (Bomber / Interceptor / Disruptor / Logistics) alongside the ported Sentinel, each with a distinct role behaviour that hard-counters a different enemy threat.
Architecture: A per-klass behaviour dispatch in the drone tick (_drone_behavior(d, dt)), with
shared targeting helpers (nearest heavy / small-fast / ranged) classifying enemies by type_id. Classes
differ in movement, attack, life, and targeting — all driven by their resolved cfg (built render-side
from MetaState; Phase 4 shop fills the numbers). No player-facing way to FIELD non-Sentinel classes yet —
that’s Phase 3 (Bay UI) + Phase 4 (shop unlocks); Phase 2 is the behaviours, exercised by deploy_drones
with class specs + unit tests + the dev console.
Tech Stack: Godot 4.6 typed GDScript; deterministic /sim; GUT 9.6.
Branch/worktree: feat/drone-system at ~/Claude/bullet-heaven-drones.
Global Constraints
Section titled “Global Constraints”- Determinism stays byte-identical — survival
1405185210/3122397125, crystals91572468/1173256610. Drones are opt-in (baseline never deploys), and the one new ENEMY-side mechanic (Disruptor slow) defaults to a no-op (slow_timer 0 → ×1.0), so the no-drone baseline is unchanged. Re-run determinism each task; a move = a leak → STOP, don’t re-pin. /simpurity: all class logic is pure; the loadout (class + numbers) is INPUT. Randomness viaSim.rng.- Numbers are tunable starting points (flag for playtest, like every prior content cycle).
- One chunk = one commit on
feat/drone-system. Never touchCLAUDE.md/git add -A.
Class behaviours (the design)
Section titled “Class behaviours (the design)”All classes deploy/expire/recharge via the Phase-1 pool. They differ in the per-tick behaviour:
| Class | Move | Attack | Targets | Life |
|---|---|---|---|---|
| Sentinel (done) | weave near swarm | AoE pulse + taunt (pulls aggro) | nearest enemy (weave anchor) | base |
| Bomber | slow drift toward target | periodic big AoE with armor-pen (ignores enemy armor) + bonus vs heavy | nearest heavy | short |
| Interceptor | fast chase | frequent small pulses (low cd), small radius | nearest small/fast | base |
| Disruptor | drift toward target | slows enemies in a field (new mechanic), little damage | nearest ranged | long |
| Logistics | follow the player | periodic repair of player (+nearby drones), no damage | the player | long |
Enemy kind classification (sim-side, by type_id):
- heavy: TANK, BRUTE, ELITE, ACCUMULATOR
- small/fast: SWARMER, SPIDER, RUSHER, ZAPPER, GHOST
- ranged: SHOOTER, SCATTERER, BOMBER, LANCER, ORBITER, SKIRMISHER
Task P2.1: Class consts + behaviour dispatch + targeting helpers
Section titled “Task P2.1: Class consts + behaviour dispatch + targeting helpers”Files: sim/sim.gd, tests/test_drone_classes.gd (new).
Interfaces — Produces:
- consts
DRONE_BOMBER/DRONE_INTERCEPTOR/DRONE_DISRUPTOR/DRONE_LOGISTICS(+ existingDRONE_SENTINEL). _drone_behavior(d: DroneState, dt: float)— the per-tick switch ond.klass; default → Sentinel path.- Targeting:
_drone_kinds_for(klass) -> Array[int](the TYPE_* a class prefers),_nearest_enemy_of_kinds(pos, kinds) -> int(pool index or -1; kinds empty = any), and_is_kind(type_id, kind_set) -> bool. - Refactor: move the Sentinel’s weave+pulse out of
_update_drones’s inline loop into_drone_behavior(so_update_dronesjust does life-decrement +_drone_behavior(d, dt)per drone). Sentinel behaviour byte-identical. - Step 1: Test — a deployed Sentinel still weaves + pulses (the Phase-1
test_dronesstill pass);_nearest_enemy_of_kindsfinds a heavy among mixed enemies; an unknown klass falls back to Sentinel. - Step 2: FAIL (consts/dispatch missing) → implement. Sentinel path unchanged (re-run determinism + the Phase-1 drone tests; all green, byte-identical).
- Step 3: Commit.
Task P2.2: Bomber
Section titled “Task P2.2: Bomber”Files: sim/sim.gd (_drone_bomber(d, dt)), tests/test_drone_classes.gd.
Mechanics: drift toward the nearest heavy at BOMBER_SPEED (slow); every BOMBER_CD lob a big
AoE at the drone’s pos: BOMBER_DMG (armor-pen — route a flag through _damage_enemy or apply raw to
data ignoring armor) with BOMBER_RADIUS, +BOMBER_HEAVY_BONUS× vs heavy types. Short life.
- Test: deploy a Bomber + a heavy enemy in range → the heavy takes (bomb dmg incl. armor-pen) within
~
BOMBER_CD; the Bomber drifts toward the heavy. Determinism byte-identical (opt-in). Commit.
Task P2.3: Interceptor
Section titled “Task P2.3: Interceptor”Files: sim/sim.gd (_drone_interceptor(d, dt)), tests.
Mechanics: chase the nearest small/fast at INTERCEPTOR_SPEED (fast); frequent small pulses
(INTERCEPTOR_CD short, INTERCEPTOR_RADIUS small, INTERCEPTOR_DMG low) — catches what outruns the
Sentinel. Base life.
- Test: deploy an Interceptor + a spider → it closes distance fast + damages it on the short cd. Determinism byte-identical. Commit.
Task P2.4: Disruptor (+ the enemy-slow mechanic — the novel one)
Section titled “Task P2.4: Disruptor (+ the enemy-slow mechanic — the novel one)”Files: sim/enemy_pool.gd (new slow_timer: PackedFloat32Array column, swapped in add/remove_at),
sim/sim.gd (_drone_disruptor, _enemy_speed_scale(i), decrement slow in _apply_status_and_decay),
tests.
Mechanics: drift toward the nearest ranged cluster; each tick set slow_timer = DISRUPTOR_SLOW_S
on enemies within DISRUPTOR_RADIUS (refreshed while in the field). _enemy_speed_scale(i) returns
DISRUPTOR_SLOW_MULT when slow_timer[i] > 0 else 1.0; thread it into the enemy movement branches
(_move_enemies WALK, _step_dash, _step_rush). Little/no direct damage. slow_timer decrements in
_apply_status_and_decay.
- Determinism:
slow_timerdefaults 0 →_enemy_speed_scalereturns 1.0 → enemy movement unchanged in the baseline (no disruptor) → byte-identical. This is the load-bearing check for this chunk — verify the baseline hash is unmoved after threading the scale into movement. - Test: deploy a Disruptor + a ranged enemy in range → the enemy’s
slow_timer > 0and it moves slower than an un-slowed peer; the field expires (slow_timer decays) when the Disruptor leaves. Determinism byte-identical. Commit.
Task P2.5: Logistics
Section titled “Task P2.5: Logistics”Files: sim/sim.gd (_drone_logistics), tests.
Mechanics: follow the player (steer toward player.pos); every LOGI_CD repair the player
LOGI_REPAIR HP (capped at max_hp) and any drone within LOGI_RADIUS (restore some life). No damage.
- Test: deploy a Logistics with the player damaged → player.hp rises on the cd; a nearby drone’s life is topped up. Determinism byte-identical (baseline player never has a logistics drone). Commit.
Task P2.6: Phase-2 review + merge prep
Section titled “Task P2.6: Phase-2 review + merge prep”- Whole-branch review (determinism parity +
/simpurity + the slow-column lockstep swap-remove). Then fetch + merge origin/main into the branch; FLAG Phase 2 ready for Chris to merge + deploy + playtest. Phase 3 (Drone Bay UI — the pause-menu configure/launch + quick-deploy + targeting policies) gets its own plan: that’s what makes the classes player-fieldable.
Sequencing notes
Section titled “Sequencing notes”- P2.1 is the dispatch refactor (Sentinel parity preserved). P2.2/P2.3/P2.5 are self-contained class
behaviours. P2.4 (Disruptor) is the only one touching ENEMY state (the slow column) — its determinism
check (baseline unmoved) is the chunk that most warrants care. Until Phase 3’s Bay UI lands, the classes
are exercised by
deploy_drones([{klass:…}])+ the dev console, not player-fieldable in a normal run.