Skip to content

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-chunk cycle (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.

  • Determinism stays byte-identical — survival 1405185210/3122397125, crystals 91572468/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.
  • /sim purity: all class logic is pure; the loadout (class + numbers) is INPUT. Randomness via Sim.rng.
  • Numbers are tunable starting points (flag for playtest, like every prior content cycle).
  • One chunk = one commit on feat/drone-system. Never touch CLAUDE.md / git add -A.

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 (+ existing DRONE_SENTINEL).
  • _drone_behavior(d: DroneState, dt: float) — the per-tick switch on d.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_drones just 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_drones still pass); _nearest_enemy_of_kinds finds 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.

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.

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_timer defaults 0 → _enemy_speed_scale returns 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 > 0 and it moves slower than an un-slowed peer; the field expires (slow_timer decays) when the Disruptor leaves. Determinism byte-identical. Commit.

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.
  • Whole-branch review (determinism parity + /sim purity + 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.
  • 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.