Skip to content

Overnight Review — 2026-07-08

Generated by an unattended /loop run (10-min cadence, ~23:35 BST → ~02:35 BST) that fans out Haiku-model agents across the codebase looking for bugs and improvement ideas from multiple angles, one lens per cycle-slot, rotating. Sonnet reviews every finding before acting:

  • Applied automatically: only genuinely trivial, non-gameplay fixes (typos, stale comments/docs, confirmed-dead code with zero references) — each as its own small [overnight-loop]-tagged commit so they’re easy to spot and revert.
  • Logged only, not applied: real bugs, gameplay/balance ideas, perf ideas, anything touching /sim or the determinism baseline, anything needing the full bh-dev-chunk process. These are candidates for Chris to triage in the morning, not verified fixes.

This is a one-off scratch doc for this run, not a standing project doc like NEXT_TASKS.md.

16 cycles, ~64 Haiku scouts, every finding reviewed and re-verified by Sonnet before logging. 2 trivial fixes committed. The rest below is triage-ready — most claims were checked against the actual code, not just relayed.

2026-07-09 triage pass note: the “crit” upgrade finding below (item 6, and cycle 17’s writeup) is a false positive — re-verified via sim/content_db.gd:69’s upgrades(), which filters the offerable list through StatEffects.is_known(effect) before anything reaches upgrade_system.roll_upgrade_choices(). Since crit_chance isn’t a known effect, the card is never offered to a player at all — dead content sitting in bible.json, not a live bug. Also during this pass: the Centurion bestiary bug (item 1) and the 13-renderer queue_redraw() cluster (item 3) were both fixed and committed; boss_rotation.gd’s dead maybe_spawn_survival_boss() (cycle 13) was removed along with its dedicated test file; the bh-dev-chunk/bh-area-background skill drift (cycle 8) was fixed. See git log 2026-07-09 for the actual commits. Remaining items (co-op P1-hardcoding catalog, Boss2 enrage visual, cruiser/frigate balance, skill ideas) are carried forward in NEXT_TASKS.md.

If you only read five things:

  1. Centurion is permanently stuck at “???” in the bestiary (content/bestiary_db.gd’s TYPE_KEYS array is missing index 23) — verified, one-line fix, cycle 13.
  2. Co-op is far more broken than documented — not just the 2 known gaps (XP, drones), but ~34 call sites across /sim, render/, main.gd, and ui/ hardcode P1 instead of _nearest_pilot(). See the ⭐ cross-cutting section below — it’s a ready-made fix checklist, and the good news is the API already supports per-pilot calls, so it’s a threading job, not a redesign.
  3. 13 of 14 per-effect renderers waste a queue_redraw() call every frame even with nothing to draw — cheap, mechanical fix, wormhole_renderer.gd is the reference pattern to copy. See the other ⭐ cross-cutting section.
  4. Two of your 8 project skills have real drift: bh-dev-chunk cites a stale determinism baseline (wrong numbers — ironic, given what that skill exists to protect), and bh-area-background points at the archived bullet-heaven-tvos repo. Both worth a 2-minute fix.
  5. Boss2 has no visual tell when it enrages — every sibling boss (FunZo/Graviton/Eye) gets a pulsing aura, Boss2 doesn’t, despite genuinely being more dangerous once enraged (cycle 14).
  6. ⚠️ The “crit” upgrade is a complete dud — data/bible.json:572-579 (id "crit", display name “Focus”) grants effect "crit_chance", but that string doesn’t exist in sim/stat_effects.gd or sim/sim_mods.gd (zero matches, grep-verified, independently re-confirmed by 3 separate scouts across cycles 17-18). sim/upgrades.gd:26-31’s fallback for an unknown effect just logs a console push_error and returns — no crash, no player-visible sign anything’s wrong. A player who picks this upgrade during a run gets literally zero benefit and no way to know. Interesting related detail: crit mechanics DO exist and work for drone attacks (sim/drone_director.gd), just with no corresponding handler for this player-facing upgrade card — so this may be a half-finished feature (drone crit shipped, player crit upgrade never got wired up) rather than pure leftover cruft. The most severe single finding of the night — worth checking first thing.

Other notable, verified findings:

  • The Obsidian cruiser is close to a strict upgrade over all 6 frigate hulls (quantified: +70% HP, +67% slots, +10 armor, for a 19% speed penalty easily offset by one level-up card) — a real input for the frigate/cruiser playtest pass already on your list (cycle 13).
  • boss_rotation.gd’s maybe_spawn_survival_boss() has an entire dedicated test file (20+ call sites) but zero production callers — either orphaned from an earlier design, or something never got wired in. Genuinely unclear which (cycle 13).
  • NEXT_TASKS.md cites “204/204 tests” — actual count is now 209. This loop couldn’t touch that file; a manual bump is all that’s needed (cycle 10).
  • 4 solid new skill ideas surfaced independently across multiple cycles: bh-add-renderer, bh-add-sound-event, bh-determinism-repin, bh-write-test — see Skill ideas at the bottom.

Quality control, for calibration: roughly a third of “confirmed bug” claims from Haiku scouts turned out to be false positives on verification — hypothetical scenarios with no reachable code path, backwards understanding of Godot’s input-consumption semantics, or claims about code the scout hadn’t actually read closely enough. Every one of those is marked (checked, refuted) inline rather than silently dropped, so nothing gets re-investigated by mistake. Take anything NOT marked “verified” or “confirmed” with a bit more salt — those are logged as-is because they were plausible and worth your attention, not because they were independently re-derived.

  • 23:35 cycle 1 — main.gd:123 comment still said the Elemental Dimensions were named “Pyre/Null/Drift”; they were renamed Fire/Void/Light in v2 (2026-07-05). Updated the comment to note the rename. Commit 5e6e20b.
  • 00:12 cycle 5 — tools/glow_bake/live_check.gd set main.camera.zoom twice (lines 33 and 39) with nothing in between that could change it — removed the redundant second assignment. Dev preview-tool script only. Commit 8b17074.

⭐ Cross-cutting: 13 renderers redraw unconditionally every frame

Section titled “⭐ Cross-cutting: 13 renderers redraw unconditionally every frame”

Found piecemeal across cycles 1 and 3, then swept repo-wide for the full picture (grep across render/). 13 of 14 per-effect renderer files call queue_redraw() unconditionally in _process(), with no check for whether there’s anything to draw: funzo_renderer.gd, weapon_pickup_renderer.gd, orbiter_renderer.gd, ghost_telegraph_renderer.gd, boss2_renderer.gd, bomb_renderer.gd, lancer_telegraph_renderer.gd, story_map_renderer.gd, eye_renderer.gd, boss_renderer.gd, graviton_renderer.gd, dash_telegraph_renderer.gd, zone_renderer.gd. Most of these already guard inside their own _draw() (e.g. bomb_renderer.gd:28’s if _sim == null or _sim.bombs.is_empty(): return), so this isn’t a crash risk — it’s wasted queue_redraw()/canvas-dispatch overhead every frame for renderers with nothing active most of the time (outside their specific enemy/effect being on-screen). wormhole_renderer.gd:39 is the one file that already does this right — if not _wormholes.is_empty(): queue_redraw() — and is the reference pattern to copy into the other 13.

Given ADAPTIVE_QUALITY is off (main.gd) and Chris’s stated priority is cutting raw render cost at the source (not relying on the ladder), this is the single most actionable perf finding of the night — same shape as the FunZo/nebula perf work already documented in docs/architecture/rendering-and-performance.md. Not auto-applied: 13 files, behavioral change (even though low-risk), belongs in a real bh-dev-chunk pass with a quick visual sanity check per renderer, not an unattended 10-minute-window edit.

⭐ Cross-cutting: co-op P1-only hardcoding is far more widespread than CLAUDE.md’s “two gaps” framing

Section titled “⭐ Cross-cutting: co-op P1-only hardcoding is far more widespread than CLAUDE.md’s “two gaps” framing”

Found piecemeal across cycles 1, 4, 5, 7, 8, 9, then swept comprehensively this cycle (grep for sim.player.pos/sim.player.hp across all of /sim, cross-referenced against _nearest_pilot() usage in the same files). CLAUDE.md currently documents co-op as having “two real, load-bearing gaps” (kill-XP/upgrade routing, and the drone/decoy system) — that undersells the actual scope. At least 26 confirmed call sites across 12 files hardcode sim.player instead of routing through sim._nearest_pilot(), meaning P2 in co-op is unaffected by most of the game’s own attacks and hazards, not just blocked from progressing:

  • The 2 already-documented architectural gaps (CLAUDE.md’s own language): kill-XP/upgrade application (sim/upgrade_system.gd:71,200,223 — P2 can’t level up or progress weapons) and the drone/decoy system (sim/drone_director.gd — P2’s drone never fires). Not new, just now precisely located.
  • Newly-catalogued: nearly every enemy/boss attack or hazard that can actually HIT the player also hardcodes P1, meaning P2 is functionally invulnerable to most of the game’s danger, not just stuck at level 1: sim/boss2.gd:95,164 (blast + charge beam), sim/boss_warden.gd:125,155 (swing + missile — Warden is currently LIVE via Elemental Dimensions, not gated off), sim/eye.gd:86,92,215,223,237 (afterimage + beam hits), sim/graviton.gd:171 (satellite DoT), sim/funzo.gd:168,288 (zone DoT), sim/elemental_system.gd:238 (web slow), sim/enemy_attacks.gd:245,259,343,406-407 (orbiter shards, lancer beam, brute contact), sim/sim.gd’s Void hazard, sim/story_director.gd:63,65,76,88,192 (NPC/tutorial — likely intentionally single-pilot given story mode’s scope, lower priority).
  • sim/ship_bonuses.gd:54,56 applies ship stats (max_hp/speed) only to sim.player — P2 never gets the selected ship’s bonuses at all, a third category beyond “can’t progress” or “can’t be hit.”
  • The root cause, confirmed clean: sim.is_invulnerable() and sim._hurt_player() DO already accept an explicit pilot parameter (defaulting to P1) — the API supports per-pilot calls, but no enemy/boss/hazard call site in the game actually passes it. This means fixing this isn’t a deep architecture change — it’s systematically threading an existing parameter through ~26 call sites, most of which just need _nearest_pilot(...) swapped in for sim.player.
  • Also found in the RENDER layer this time (cycle 11), not just /sim: render/dash_telegraph_renderer.gd:33 and render/eye_renderer.gd:124 both compute their telegraph direction from sim.player.pos instead of _nearest_pilot() — so even where the underlying attack might someday be fixed to hit P2, its visual warning telegraph would still only ever point at P1. Same checklist, same fix shape, just render-side call sites, so a real fix pass should sweep both /sim and render/.
  • Positive confirmation, same sweep: zero violations found for a stricter, more serious check — no /sim file reads Time./OS./Engine./Input./get_tree() directly (grepped explicitly). The core determinism rule (“/sim never touches engine APIs”) holds cleanly across the whole directory, unlike the co-op-pilot convention which was clearly not enforced as consistently.

Not auto-applied or fixed — this is squarely /sim gameplay logic requiring the real bh-dev-chunk process (TDD, determinism re-verification) given co-op’s own tick()/inputs plumbing is capped at 2 pilots today per CLAUDE.md. But this list is now a ready-made checklist for whoever picks up P2 progression — the earlier framing of “two gaps” was accurate about what’s documented, not what’s actually broken.

Final tally (as of cycle 12, comprehensively swept in all 4 layers — this is now believed complete):

  • /sim: ~26 call sites across 12 files (boss2.gd, boss_warden.gd, eye.gd, graviton.gd, funzo.gd, elemental_system.gd, enemy_attacks.gd, sim.gd, story_director.gd, drone_director.gd, upgrade_system.gd, ship_bonuses.gd) — enemy/boss/hazard attacks, XP/upgrades, drones, ship stats.
  • render/: 2 sites (dash_telegraph_renderer.gd:33, eye_renderer.gd:124) — confirmed exhaustive via a dedicated repo-wide grep sweep in cycle 12, nothing else found.
  • main.gd (audio + screen feedback): 1 site (main.gd:1282) — P1-only hurt sound AND screen-punch camera feedback share the same single sim.player.hp check, so P2 gets no hit sound or camera kick when hurt.
  • ui/: 5 elements confirmed P1-only display (HP bar, weapon-aim stats, level readouts on 2 panels, results screen, drone dock) — all correctly identified by the scout as symptoms of the sim-layer gap above, not independent bugs; there is currently no P2-facing UI at all (no HP bar, no join indicator, no stats row).
  • Root cause reminder: is_invulnerable()/_hurt_player() already accept a pilot param — this is a threading job across ~34 call sites total, not a redesign.

23:35 cycle 1 — lenses: sim/determinism, rendering/perf, UI/UX, code quality

Section titled “23:35 cycle 1 — lenses: sim/determinism, rendering/perf, UI/UX, code quality”

sim/determinism

  • [idea] FunZo zone player-damage (sim/funzo.gd:288) uses sim.player.pos directly, not _nearest_pilot() — in co-op, P2 won’t take FunZo zone damage. Same root cause as the already-documented P2-progression/drone gaps (CLAUDE.md Architecture section), just one more instance — low priority until co-op ships for real.
  • (checked, refuted) the scout also claimed “FunZo zones never damage the player” at all — that’s false, funzo.gd:285-289 explicitly calls sim._hurt_player(...). Logging the correction so it isn’t re-investigated next cycle.
  • P2 XP/upgrade routing and P1-only drones — re-surfaced by the scout but these are already documented gaps in CLAUDE.md (M-A gaps 1/2), not new.
  • spawn_table.gd’s _lerp_weights() boundary — flagged again, but this is likely the same boundary case already found+fixed during the Centurion enemy work (see roadmap memory) — probably not still broken, worth a quick second look rather than treating as fresh.
  • drone_rng has no determinism-baseline coverage yet since nothing draws from it — note for whenever a real drone feature does.

rendering/perf

  • [idea] main.gd’s _enemy_colors()/_enemy_proj_colors()/_proj_colors() (~line 1414-1470) allocate fresh PackedColorArrays every frame instead of reusing a pre-sized member array — minor GC-pressure cleanup, not a blocker.
  • [bug, verified] render/zone_renderer.gd:19-20 — _process() calls queue_redraw() unconditionally every frame, even when sim.zones/webs/funzones are all empty (most of the game, outside active zone effects). Cheap fix: guard with an early return when all three are empty. Not auto-applied (behavioral change, however small) — a clean pickup for a real bh-dev-chunk cycle.
  • [idea] damage_numbers.gd:56-71’s pool scans all 72 slots linearly every frame regardless of active count — minor.
  • [idea] fx/fx_manager.gd’s consume() (line ~343-471) dispatches ~14 event kinds via string match — an enum would be marginally faster in the hot loop.
  • [idea] player2_renderer isn’t bound to QualityManager the way player_renderer is (main.gd:1180-1190) — P2 keeps full visual detail even at low quality tiers. Moot while co-op is gated off, but worth fixing alongside it.
  • NebulaBg has no adaptive-quality hook yet — already flagged as known deferred work in docs/architecture/rendering-and-performance.md, not new.

UI/UX

  • [bug, unconfirmed] ui/ship_config_panel.gd:240 — a ship-carousel touch drag that STARTS off the back button but ENDS (release) over its rect might still trigger the drag’s tap-confirm path. Genuinely uncertain from static reading; the code already has a deliberate, reviewed fix for the reverse case (press starting on the back button). Needs a real on-device touch check, not a blind fix.
  • (checked, refuted) ui/start_menu.gd:455-461 “stale signal binding” claim — false positive. The pressed signal disconnect/reconnect for a re-roled carousel card is synchronous and unconditional right after _place_card(); there’s no window for a stale binding to fire.
  • [idea] No visual “locked” indicator on unpurchased shop carousel side-cards during drag (ui/meta_shop_panel.gd:79-93).
  • [idea] No focus/selection highlight in the Codex Overlay’s browse list (ui/codex_overlay.gd:149-150) — matters on tvOS joypad nav with no mouse hover to fall back on.
  • [idea] DroneBayPanel/PauseMenu lists wrap silently at the ends with no boundary indicator — minor tvOS nav polish.

code quality

  • [duplication] _nearest_enemy_index is byte-identical across sim/weapon_scatter.gd:29, sim/weapon_beam.gd:19, sim/weapon_melee.gd:24 (verified via grep) — candidate to extract into a shared helper. Not applied — touches /sim, needs a real bh-dev-chunk pass with tests, not an unattended edit.

23:42 cycle 2 — lenses: content/balance, audio, tooling/skill gaps, UI/UX round 2

Section titled “23:42 cycle 2 — lenses: content/balance, audio, tooling/skill gaps, UI/UX round 2”

No safe trivial fixes this cycle — every concrete “bug” claim turned out to be a false positive on verification (see below). Zero commits made.

content/balance/enemy & weapon design

  • [idea] Starter Blaster (8.9 DPS) noticeably outclasses the first acquirable weapon, Pulse (5.0 DPS) — breaks the “new weapon = progression” feel. Matches the already-open “balance-tune the ladder numbers” item in CLAUDE.md, not new territory but a concrete number to start from.
  • [idea] Turret’s base damage (1.0) is half of Scatter’s (1.4) despite being a placed structure, not a spread weapon — worth confirming this is intentional (“weakest by design”) rather than an oversight.
  • [idea, needs a real look] Centurion’s wave-15 debut (sim/spawn_table.gd:25) — worth checking whether typical Survival runs actually reach wave 15 in practice; if not, the enemy is functionally rare-to-absent despite being fully wired. (checked, partially refuted — see below)
  • (checked, refuted) “Centurion never spawns in Survival except via a one-time swarm burst” — false. sim/spawn_table.gd shows it IS in the normal weighted spawn blend (weight 0.05) from wave 15 onward, not gated to swarm-burst-only.
  • (checked, refuted) “ELITE_CHARGER_HP_MULT comment conflates the math order” — false. sim/sim.gd:132-138’s comment accurately narrates two sequential same-day tuning edits (Chris: “-20%”, then later “÷3 more”); the resulting formula (0.8 / 3.0) matches the code exactly. No change needed.
  • SOFT_ENEMY_CAP isn’t in a single obvious tuning-const block — minor findability nit, low priority.

audio

  • [idea, real but not urgent] audio/audio_manager.gd:105 preloads eye_laser.wav (confirmed: the file exists, 52KB) but it’s never actually played — Eye’s laser fire currently uses the shared beam_hum.wav via the generic beam fx kind, which audio/SOUND_MAP.md already documents as an intentional, deferred differentiation. So this isn’t dead code to delete — it’s a half-finished feature: a bespoke sound asset sitting ready, waiting for sim/eye.gd’s laser-fire code to emit its own fx_event kind instead of falling through to the shared one. Worth finishing, not cleaning up.
  • (checked, refuted) “ui/pause_menu.gd:146 calls the wrong audio method (ui_nav() instead of ui_select())” — false. There is no ui_select() method on AudioManager at all (only ui_nav()/ui_buy()), and audio/SOUND_MAP.md explicitly documents that _ui_nav/_ui_select intentionally both play ui_tap — already audited, marked DONE in the Sound Phase 4 pass, not a bug.

UI/UX round 2

  • [idea] HUD wave/boss banner (ui/hud.gd:105-106, y=146) may visually collide with the boss name label (starts y=88) on a 720p TV — worth an on-device look.
  • [idea] ui/debug_settings_panel.gd:308-309 — cycling a dev-settings row (Ship Tilt, Background) doesn’t re-grab focus after _refresh_row_label(), so the joypad focus ring can drop briefly. Dev-tool only, low priority.
  • [idea] ui/dialogue_box.gd:177 portrait-initial letter + faint background alpha (0.18) — worth an on-device contrast check for 10-foot viewing.
  • ui/crystals_levelup_panel.gd null-check idea was too vague to act on — skipped.
  • No accessibility/contrast issues found across hud.gd/pause_menu.gd/crystals_levelup_panel.gd/debug_settings_panel.gd/dialogue_box.gd — high-contrast neon palette, generous font sizes throughout.

23:52 cycle 3 — lenses: sim/determinism round 2, rendering round 2, code quality round 2, tooling/skill gaps round 2

Section titled “23:52 cycle 3 — lenses: sim/determinism round 2, rendering round 2, code quality round 2, tooling/skill gaps round 2”

No safe trivial fixes this cycle — zero commits made. The renderer queue_redraw() pattern (found this cycle via bomb_renderer.gd) is written up as its own cross-cutting section above rather than duplicated here.

sim/determinism round 2

  • (checked, refuted) “weaken_mult() (sim/elemental_system.gd:144) could silently multiply damage down if weaken_amt ever starts negative as a sentinel” — false, and purely hypothetical. Grepped every assignment site: weaken_amt is only ever set in sim/drone_director.gd:190 as weaken_lvl * DISRUPTOR_WEAKEN_PER_LEVEL (always ≥0) or reset to 0.0. No negative-sentinel pattern exists anywhere in this codebase.
  • [idea, low priority] sim/upgrade_system.gd:187’s active_element_count() reads sim._weapon_by_id["blaster"].element_idx with no null-check — probably always safe now that Blaster is the starter weapon, but a defensive .get() would be cheap insurance.
  • [idea, cosmetic] weapon_mod_label() (sim/upgrade_system.gd:293) can display a nonsensical “+9900%” if a rate mod’s magnitude is ever exactly 0 — display-only edge case, not a real bug path today.
  • [idea, likely already-intentional] Crystals mode doesn’t offer AIM_MODS as pickable level-up cards (sim/upgrade_system.gd:89-91) — this is probably correct by design (crystals mode progresses the aim weapon automatically via WeaponThresholds.TABLE/_eval_thresholds, not player-chosen cards, per the existing starter+manual-weapon architecture) rather than a gap. Worth a sentence of clarifying comment, not a behavior change.

rendering round 2

  • (see the cross-cutting renderer section above for the BombRenderer finding that triggered the full sweep)
  • [idea, low value] render/archetype_renderer.gd:92’s CORE_HEAT := 0.30 const was tuned down from 0.62 for HDR cost — scout flagged it as “untested,” but this is normal for a tunable const with no dedicated visual-regression test; not actionable without an on-device look.
  • [idea, already known] Ship3DRenderer’s SubViewport isn’t quality-gated yet — the code’s own comment already says perf hasn’t been measured and it’s not a shipping default; not new.
  • [idea, low severity] render/hero_lights.gd:80 falls back to NEUTRAL (magenta) silently if an fx_event has a missing/invalid element key — safe fallback, would only matter if an upstream event is already malformed.

code quality round 2

  • [duplication] _HologramBrackets inner class is duplicated between ui/start_menu.gd:615 and ui/meta_shop_panel.gd:1141 (verified via grep) with different tuning (arm 0.12 vs 0.18, an extra draw_rect call in the shop version) — candidate to extract into a shared render helper, but the parameter differences may be intentional per-card-size tuning, so any consolidation needs to preserve both visual results, not just merge blindly.
  • (checked, not actually stale) the scout flagged ui/ship_config_panel.gd:72-74’s comment referencing a specific old commit (git show 6328f43^:ui/start_menu.gd) as “no longer navigable” — that’s not accurate, the git reference is still valid and correctly documents where the LOCKED-tag pattern was ported from before the source function was deleted. Historical provenance comments like this are fine to keep as-is.
  • No dead code found this pass.

tooling/skill gaps round 2

  • bh-write-test (second strong skill candidate) — scaffolds new GUT test files with this project’s conventions: extends GutTest, the _sim()/SimContentFixture.db() fixture pattern, the assert_lte/assert_gte naming gotcha (NOT assert_le/assert_ge — a typo here silently drops the whole file from the run), and the .gutconfig.json dirs hardcode that makes -gtest/-gdir silently no-op (already known but the ROOT CAUSE isn’t documented). Concrete and well-scoped — worth building alongside bh-determinism-repin.
  • [doc-gap, minor] a one-line note in CLAUDE.md § Running & testing explaining why -gtest doesn’t narrow (the .gutconfig.json override) would save a newcomer’s debugging step — the symptom is already documented, just not the cause.
  • [doc-gap, minor] the determinism-baseline prose in CLAUDE.md is deliberately treated as a stale-prone example (“always read the pinned assertion, not prose”) but a newcomer can’t tell that’s intentional framing vs. an oversight — a bolder callout would help. Overlaps with the bh-determinism-repin skill idea above; would be resolved as a side-effect of building it.

00:02 cycle 4 — lenses: content/balance round 2, audio round 2, sim round 3, tooling/skill gaps round 3

Section titled “00:02 cycle 4 — lenses: content/balance round 2, audio round 2, sim round 3, tooling/skill gaps round 3”

No safe trivial fixes this cycle — zero commits made.

content/balance round 2

  • [idea] Decoy unlocks (~200-250g, one-and-done) cluster around the same price as 2 levels of geometrically-scaling stat upgrades, front-loading the shop — worth reviewing the unlock/stat price relationship as a set, not each in isolation.
  • [idea] The Obsidian cruiser unlock (800g, single unlock) is a big outlier vs. every other major unlock (<500g) — 4+ runs of grinding for a single gate. Worth confirming that’s the intended “aspirational” pacing rather than an oversight.
  • (checked, mostly refuted) “13 named reactions generate NO damage, only a name/visual event” — false as stated. Traced the full path (sim/elemental.gd:32 → sim/elemental_system.gd:on_reaction()): EVERY reaction, burst-tagged or not, always spawns a DPS terrain zone (spawn_zone) and chain-primes nearby enemies (seed_primed) in addition to its fx event — that’s real, ongoing damage. The "effect": "burst" field only adds an extra instant nova on top for reactions tagged that way; other effect tags (shatter/spread/pull/cc/special) skip the instant nova but keep the shared zone+priming machinery. Not a bug — worth a design confirmation with Chris on whether that split (instant-nova vs. zone-only) matches intent per reaction, not a code fix.
  • No bugs found in sim/meta_state.gd’s cost formula or shop gating logic.

audio round 2

  • (checked, corrected) a scout claimed SOUND_MAP.md already documents eye_laser.wav as “dead/unused,” recommending its preload be removed. Checked the actual “Dead/unused .wav assets” row (line 159) — it lists 5 different files (attack.wav, enemy_hit.wav, special_aegis.wav, wave_clear.wav, special_rally.wav); eye_laser.wav isn’t among them. Reinforces the cycle-2 conclusion: this is a half-finished feature asset, not dead code — leaving it as-is.
  • SOUND_MAP.md’s SILENT/GENERIC rows all cross-checked accurate against current code; no doc drift found.

sim round 3

  • No real bugs found in sim/enemy_behaviors.gd/sim/enemy_attacks.gd after a thorough pass (division-by-zero guards, angle math, state machine transitions, timer dict pruning all checked out).
  • Confirms the known co-op P1-only pattern extends further than previously logged: orbiter shard DoT, lancer beam hits, and bomber contact damage all hard-target sim.player (P1) via is_invulnerable()/_hurt_player() with no pilot argument. Same root cause as the FunZo-zone instance already logged in cycle 1 — not new, just broader confirmation of how much co-op work is P1-only right now.

00:12 cycle 5 — lenses: rendering round 3, UI/UX round 3, content/balance round 3, code quality round 3

Section titled “00:12 cycle 5 — lenses: rendering round 3, UI/UX round 3, content/balance round 3, code quality round 3”

One trivial fix applied this cycle (see Applied fixes above, commit 8b17074).

rendering round 3

  • (re-confirmed, already known) NebulaBg still isn’t wired into QualityManager’s adaptive ladder — this is the third time tonight this surfaced (cycles 1, 2, and now 5). Not re-logging in detail again; see cycle 1’s entry. Worth flagging to Chris purely because of how often it keeps resurfacing — a decent sign it should just get fixed alongside ADAPTIVE_QUALITY re-enabling at v0.1.
  • [idea, very low priority] render/swarm_renderer.gd:73-78 and render/archetype_renderer.gd:497-503 write per-instance transform/colour updates even for culled (off-screen) entities before the final visible_instance_count gate limits the draw — CPU→GPU upload bandwidth is spent on entities that never render. Minor; worth a profile before touching.
  • [idea, cosmetic] manual Color(col.r, col.g, col.b, alpha) construction in a couple of hot per-instance loops could use .with_alpha() — code hygiene only, not a bug.
  • No shader bugs found — uniforms update at the correct frequency, blend modes correct.

UI/UX round 3

  • (checked, refuted) ui/level_up_panel.gd:83-85’s early-return when invisible/no-cards was flagged as “leaking unconsumed input” — false. NOT calling set_input_as_handled() here is correct: the panel isn’t active, so it shouldn’t consume input meant for something else. The scout had this backwards.
  • (checked, refuted) ui/boss_edge_indicator.gd’s label-position math was flagged as breaking when the boss is “near screen center” — false, structurally unreachable: visible = false returns early at line 77 whenever the boss is on-screen (near center), before the label math at line 87 ever runs.
  • [idea, low confidence] ui/results_panel.gd’s recap-text labels have a fixed custom_minimum_size with no scroll guard — could overflow on a very long recap. Plausible but unverified in depth.
  • The rest of this scout’s findings (drone bay focus edge cases, touch hold-timing, “icon state race”) were too hedged/speculative (“could,” “theoretically,” “brittle”) to act on or verify cheaply — skipped rather than logged as if confirmed.

content/balance round 3

  • (checked, refuted) “[bug] Lancer/Zapper might not fire — fire_interval wiring unconfirmed” — the scout flagged this without actually checking; grepped sim/enemy_attacks.gd myself: both have real, wired firing code (Zapper via the shared fire_interval block at line 145/152, Lancer via its own dedicated beam-attack block at lines 289-372). Not a bug.
  • [idea] Charger’s XP-per-HP ratio (~0.77 xp/hp) is ~4.6x Skirmisher’s (~0.17 xp/hp) — worth a balance look, though Charger’s higher contact damage partially justifies the premium.
  • [idea] Elite Charger’s ELITE_CHARGER_CONTACT_MULT = 0.98 multiplies base contact down slightly rather than up — scout confirms this matches the documented intent (the double-dash mechanic is the elite’s real threat, not raw contact damage), just flagging as a “does the resulting feel match intent” playtest question, not a bug.
  • [idea, needs clarification] Centurion is listed on Home’s enemy roster but its spawn table entry doesn’t seem to reach the early (<120s) window Home mostly plays in — may mean Home runs practically never see it despite the roster claim. Related to the wave-15 timing question already logged in cycle 2; worth resolving both together.
  • No discontinuities found in boss HP/damage scaling formulas.

code quality round 3

  • [duplication, low priority] _setup_canvas_glow() is byte-identical across tools/ship_preview/preview.gd:97-109 and tools/glow_bake/preview.gd:40-52 (verified via diff) — dev-tool harnesses only, low value to consolidate.
  • [stale-comment, NOT auto-fixed] sim/sim.gd:1328 and :1348 still label _spawn_fire_hazard()/_spawn_void_hazard() as “Pyre:”/“Null:” in their doc-comments (same stale-naming pattern as the main.gd:123 fix from cycle 1) — verified accurate, but deliberately not applied: this file is under /sim, and tonight’s ground rules say never touch /sim during this unattended loop, full stop, even for a comment-only change. Trivial pickup for the morning: just Pyre:→Fire: and Null:→Void: in those two comment lines, zero logic change.
  • No dead code found in tools/ or the content pipeline this pass.

00:22 cycle 6 — lenses: audio round 3, UI/UX round 4, content/balance round 4, code quality round 4

Section titled “00:22 cycle 6 — lenses: audio round 3, UI/UX round 4, content/balance round 4, code quality round 4”

No fixes applied this cycle — one attempted comment fix was made, verified wrong mid-cycle, and reverted before committing (see code quality round 4 below). Zero commits made.

audio round 3

  • [idea, low priority] "BOSS"/"WARDEN"/"BOSS DOWN" named-reaction sounds and the game_over()/hurt()/victory() one-shot helpers are correctly wired (verified) but have zero test coverage in tests/test_audio_manager.gd, unlike FREEZE/DRONE/BLAST/GRAVITON which are tested. Matches SOUND_MAP.md’s own existing “PARTIAL” test-coverage note — not a new gap, just a specific list of which cases are missing.
  • Clean bill of health otherwise: no per-frame audio spam, no typo’d sound keys, /sim and render/ correctly have zero direct audio calls, all UI panels null-guard the audio property.

UI/UX round 4

  • [idea, verified] ui/weapon_panel.gd:233 recomputes _hint.modulate.a every frame via a fade formula, but _hint.visible = false is set once at setup (line 152, “Chris: hide the label for now”) and never re-enabled anywhere in the file — the fade math runs forever on a permanently-invisible label. Harmless (no observable effect) but genuinely wasted per-frame work. Not auto-applied — same category as the renderer queue_redraw() cluster, a UI-behavior file, better as a real dev-chunk pickup than an unattended edit.
  • [idea, low confidence] ui/touch_controls.gd’s _decoy_btn_idx could theoretically stay unreset if a touch-down event’s matching touch-up is lost (device edge case) — scout itself flagged this as rare/theoretical, logging for completeness only.
  • This cycle’s scout was noticeably more disciplined than earlier UI/UX passes — explicitly reported “no bug found” for 2 files it checked rather than padding the list, which is exactly the calibration asked for.

content/balance round 4

  • Clean bill of health: mod stacking (multiplicative, intentionally uncapped for damage — confirmed EVE-style by design), evolution gating (evolved flag prevents double-evolution, mods.has_any() gate is clean), and the existing caps (turret count ≤4, orbit shards ≤6, fire-rate ≤FIRE_RATE_CAP) were all traced through the actual code and confirmed correct. Nothing new to log.

code quality round 4

  • [stale-comment, real but NOT safely auto-fixable] tests/test_boss_gate.gd:100-102’s comment says “Today’s 3 real Dimensions (Pyre/Null/Drift)… can only ever return ‘graviton’/‘funzo’/‘eye’”. I attempted a quick fix (names only) but caught my own error mid-edit: sim/area_defs.gd’s DIMENSION_IDS is genuinely [FIRE, VOID_DIM, LIGHT] (3 entries, correct count — Home is explicitly NOT a portal destination, so my first instinct to “correct” it to 4 was itself wrong), but the boss-name list in the same comment doesn’t match either: current boss_for() mapping is fire→warden, void_dim→graviton, light→eye — not “graviton/funzo/eye” as the comment claims (no “warden”, and “funzo” isn’t reachable via this path at all, only via the separate generic/non-Dimension rotation tested elsewhere in the same file). Reverted my edit rather than risk introducing a fresh inaccuracy. This needs someone who actually understands whether funzo/warden reachability changed here to fix correctly — flagging with the full detail above so whoever picks it up doesn’t have to re-derive it.
  • [stale-test-data, NOT auto-fixed] tests/test_wormhole_renderer.gd:12-13,25 hardcodes old dimension name strings ("pyre", "null_dim", "Pyre", "Null") as actual test fixture data, not just comments — changing these would need the test re-run to confirm it still passes with the new values, which this loop’s ground rules explicitly don’t allow. Logged only.
  • tests/test_dimensions.gd’s “old Pyre” comments (lines 83, 92, 129, 223) are intentional regression-test documentation explaining what old behavior is being guarded against, not stale — correctly left alone.

00:32 cycle 7 — lenses: sim round 4, rendering round 4, audio round 4, tooling/skill synthesis

Section titled “00:32 cycle 7 — lenses: sim round 4, rendering round 4, audio round 4, tooling/skill synthesis”

No fixes applied this cycle — zero commits made.

sim round 4

  • [idea, verified] Two more confirmed instances of the known co-op P1-only pattern: sim/sim.gd:1354’s Void Dimension hazard pulls player.pos directly instead of via _nearest_pilot() (same function whose stale “Null:” comment was found in cycle 5), and sim/story_director.gd:76’s NPC talk-radius check does the same (sim.player.pos.distance_to(npos)), so P2 can’t trigger NPC dialogue/story drops even standing right next to one. Both grep-verified accurate. Not new territory conceptually (same root cause as the FunZo/orbiter/lancer/bomber instances already logged), but useful to have the full site list in one place for whoever eventually does the P2-progression work.
  • No determinism breaks found in story_director.gd/drone_director.gd — RNG draws are clean, no-drone baseline stays an exact no-op.

rendering round 4

  • Elaborates on the cycle-1 finding (main.gd’s per-frame PackedColorArray allocation) rather than adding new territory: also applies to _enemy_scales(), wormhole_list.duplicate() (main.gd:1260), and a small boss-info array rebuilt every frame despite at most 1 boss being alive (main.gd:1237-1254). Same fix shape as already logged (pool/cache instead of reallocating) — not re-logging as a separate item, just noting the fuller scope for whoever picks up the cycle-1 finding.
  • Clean bill of health on SpatialHash.query_circle() (necessarily allocates its one result array, can’t be pooled without an API change) and FxManager’s pools (already correctly pre-allocated).

audio round 4

  • Bus layout, volumes, and polyphony capping all checked out correctly against project.godot/bus_layout.tres (no wiring bugs) — but this pass did surface 3 legitimate, correctly-caveated mixing ideas rather than another clean pass:
    • [idea] Named reactions (BOSS, NUKE, FREEZE, ENRAGED, etc.) intentionally bypass PER_FRAME_CAP (audio_manager.gd:157) so high-priority events always play — but that means there’s no upper bound on how many can stack in one frame; at high enemy counts with heavy elemental activity, uncapped named reactions plus capped weapon fire could still saturate the 16-voice pool.
    • [idea] There’s no ducking of the Music bus when SFX gets loud (buses are correctly isolated per bus_layout.tres, just no dynamic mixing between them) — in a dense swarm fight, music never yields room to the weapon/reaction barrage.
    • [idea] set_weapon_load() attenuation only applies to the 6 WEAPON_FIRE_KINDS — enemy/boss projectile sounds (enemy_ranged_fire, boss_missile_launch, etc.) always play at 0dB, uncapped by load. At very high enemy counts this could contribute to pool saturation before named reactions even land.
    • All three are explicitly framed as “may need tuning under real load,” not bugs — genuinely untested without on-device telemetry at extreme spawn counts.

tooling/skill synthesis

  • This cycle asked a scout to review the 4 skill ideas proposed so far rather than hunt for more, given returns were diminishing. Its recommendation to merge bh-determinism-repin + bh-write-test into one bh-determinism-baseline skill doesn’t fully hold up on reflection — they’re genuinely different concerns (test-authoring conventions for newcomers vs. a narrow re-pin ritual for an intentional baseline move), just both “testing-adjacent.” Its prioritization call is worth keeping though: build determinism-baseline tooling first — it gates every sim-touching PR, while the renderer/sound skills are additive. Also a good structural observation: bh-dev-chunk’s 7-gate checklist currently bundles determinism-checking in with steps that don’t need it (UI/render-only changes) — extracting it as an independently-callable gate (used only when a change actually touches /sim) would keep bh-dev-chunk leaner without losing the safety net.

00:42 cycle 8 — lenses: content/balance (“The Chasm”), code quality (TODO sweep), UI/UX (accessibility), skill-audit

Section titled “00:42 cycle 8 — lenses: content/balance (“The Chasm”), code quality (TODO sweep), UI/UX (accessibility), skill-audit”

No fixes applied this cycle — zero commits made. Standout finding: two existing skill files have real drift (see Skill ideas below).

content/balance — “The Chasm”

  • Status clarified: “The Chasm” isn’t a separate unshipped area — it’s the codename for Cycle 21 (BUILD 51), a completed content-import pass that shipped Ghost, Accumulator, tank homing missiles, and 3 new bosses (FunZo/Graviton/Eye) extending the boss rotation from 2→5. All merged, all deployed to Apple TV. Graviton and Eye are code-complete but deliberately gated off in survival via BossRotation.V01_FUNZO_ONLY=true (Chris’s call, 2026-07-02) — not a bug, an intentional ship gate.
  • [idea, verified] Two more confirmed co-op P1-only sites, more nuanced than earlier findings: sim/graviton.gd correctly uses _nearest_pilot() in its main pull/push mechanics (lines 90, 104, 125, 143) EXCEPT its satellite-blob DoT check at line 171, which hardcodes sim.player.pos — P2 walks through satellite blobs unharmed. Similarly sim/eye.gd correctly uses _nearest_pilot() for targeting (lines 67, 262) but its afterimage-line and beam/pupil hit-checks (lines 86, 92, 215, 223, 237) hardcode sim.player.pos — P2 takes no damage from the Eye’s signature dash-afterimage attack. Since Graviton/Eye are gated off in survival right now, low urgency, but useful detail for whoever eventually does the P2-progression work — these are per-line gaps within otherwise-correct files, not whole-file omissions.
  • [idea, low priority] Graviton’s pull-then-arena-clamp ordering (sim/graviton.gd:145-150) clamps the player back INTO the arena after the pull is applied, which could silently truncate the pull’s effect near an edge — flagged as “probably intentional” by the scout (hard arena boundary), worth a playtest confirmation rather than a code concern.

code quality — TODO/FIXME sweep

  • Very clean: only ONE TODO marker exists anywhere in sim/, render/, ui/, fx/, main.gd, audio/, meta/, net/, ai/ combined (sim/sim.gd:636, the N-pilot integrate() gap) — and it’s already documented in CLAUDE.md’s Architecture section, not a hidden untracked debt. Good sign of project discipline; nothing new to log.

UI/UX — accessibility & legibility

  • Clean pass overall: no red/green-only signal found anywhere (threat levels, locked ships, shop affordability all pair color with shape/text/motion, not color alone — verified across codex_card.gd:48, ship_config_panel.gd:196, hud.gd:256-262). Neon palette (cyan/magenta/orange/violet) doesn’t clash for deutan/protan colorblindness.
  • [idea, verified] ui/weapon_panel.gd:177 sets the aim-weapon stat label to font_size 10, smaller than the FONT_SM := 14 baseline used elsewhere — worth an on-device TV legibility check at 10 feet.
  • [gap] No player-facing accessibility/settings screen exists at all — only the dev-only DebugSettingsPanel (gated behind BuildConfig.dev_tools()). Not urgent for v0.1 given the palette already avoids color-only signaling, but worth a post-launch backlog note if a text-size or colorblind-mode option is ever requested.

skill-audit (new angle this cycle — checked existing skills instead of proposing new ones)

  • [skill-drift, high value] .claude/skills/bh-dev-chunk/SKILL.md:53 hardcodes a STALE determinism baseline — snapshot_string().hash() = 4152236597, state_checksum() = 1267954985. Verified directly: the actual current pinned assertion (tests/test_determinism_checksum.gd:47-48) is 4217109746/2666143677. This is exactly the failure mode the bh-determinism-repin skill idea (proposed earlier tonight) exists to prevent — and ironically it’s the determinism-focused skill itself that’s stale. High-value fix for Chris to make directly (skill files are off-limits for this loop to touch, gitignored/local-only, not part of the git-reviewable trail).
  • [skill-drift] .claude/skills/bh-area-background/SKILL.md:112-113 references syncing to ~/Claude/bullet-heaven-tvos/ — that repo was archived 2026-07-04 when tvOS folded into platform/tvos/ with symlinked gameplay (no sync step needed anymore, per CLAUDE.md’s top-of-file note). Dead instructions pointing at retired infra.
  • [skill-ok] bh-add-content, bh-sdd-concurrent, bh-deploy, bh-appstore-release all checked current — correctly reference the sim-module-split, the tvOS fold, and the -Dev/production preset split. No drift found in these four.
  • Confirms .claude/skills/ IS readable from disk tonight (gitignored but present, as the global CLAUDE.md convention describes) — a fresh Haiku agent CAN see real skill content, not just names, when asked to look.

00:52 cycle 9 — lenses: sim round 5 (pattern hunt), rendering round 5, audio round 5, UI/UX round 6

Section titled “00:52 cycle 9 — lenses: sim round 5 (pattern hunt), rendering round 5, audio round 5, UI/UX round 6”

No fixes applied this cycle — zero commits made.

sim round 5 — targeted hunt for more “hardcodes player instead of _nearest_pilot()” instances

  • [idea, verified, genuinely new] sim/elemental_system.gd:238’s web_slow_mult() checks only sim.player.pos against active webs — P2 never gets web-slowed in co-op. Unlike most other co-op findings tonight, this isn’t inside an already-documented “P1-only” subsystem (drones/story/XP) — it’s a core elemental status-effect calculation, so it’s a genuinely new site, not just enumerating a known gap.
  • [idea, verified, more relevant than earlier finds] sim/boss_warden.gd:125 (swing hit-check) and :155 (missile hit-check) hardcode sim.player.pos, while the rest of the same file correctly uses _nearest_pilot() (lines 47, 71, 143). Worth flagging above the Graviton/Eye instances found earlier tonight — Warden is the currently LIVE, reachable boss (Fire/Home dimensions, not gated off), unlike Graviton/Eye which are behind V01_FUNZO_ONLY.
  • The rest of this scout’s findings (drone_director.gd ~10 lines, story_director.gd 5 lines) are real but not new information — they enumerate specific lines within systems CLAUDE.md already documents as P1-only by design (the drone/decoy gap, story mode’s single-pilot scope). Not re-logging in detail.

rendering round 5

  • [idea, verified but likely intentional] Root project.godot has no rendering/ section at all (confirmed: zero matches), while platform/tvos/project.godot:27 explicitly sets viewport/hdr_2d=true. Per [[bullet-heaven-tvos-metal-renderer]] memory, this is probably correct as-is: root/web builds use gl_compatibility (no HDR by design), tvOS uses the Mobile/Metal renderer (HDR available). Genuinely uncertain whether the local macOS export path (via the same root project.godot) also needs this — worth a quick on-device bloom comparison rather than assuming either way.
  • Two more re-confirmations of already-logged items (ADAPTIVE_QUALITY off, NebulaBg quality-ladder gap) — not re-logging in detail, this is now the 4th+ time both have surfaced.
  • Clean pass on scene files: no broken resource references, all sprite paths resolve, main.tscn is intentionally empty (nodes built in code).

audio round 5

  • SOUND_MAP.md’s cited line numbers have drifted a few lines from actual fire sites (expected doc staleness as code grew) but every wiring/mapping itself checked out correct. eye_laser.wav correctly does NOT appear in the doc’s “dead asset” list — consistent with tonight’s established conclusion that it’s a half-finished feature, not dead code. No new findings.

UI/UX round 6

  • [idea, verified, minor] ui/hud.gd:195 — when a boss dies, _boss_group.visible correctly goes false but _boss_label.text is never cleared, leaving the previous boss’s name in the stale label. Currently harmless (label stays hidden in the normal path) but a robustness gap if a debug panel or a visibility-toggle bug ever re-shows it. Cheap fix (_boss_label.text = "" alongside the visibility flip), not auto-applied since it’s ui/hud.gd render-behavior code.

01:02 cycle 10 — lenses: content/balance (story mode), code quality round 6, sim round 6 (comprehensive P1-only sweep), tooling/skill round 6

Section titled “01:02 cycle 10 — lenses: content/balance (story mode), code quality round 6, sim round 6 (comprehensive P1-only sweep), tooling/skill round 6”

No fixes applied this cycle — zero commits made. Sim round 6’s sweep is written up in the new cross-cutting section above rather than duplicated here.

content/balance — story mode

  • [idea, verified structurally, low priority] Vex’s story arc (data/story.json) has a narrative pacing gap: defeating her in encounter lw_e_vex fires dialogue vex_beaten (line 1411), but her reconciliation dialogue vex_ally only fires later when the player physically walks to a different room (lw_safe1, NPC entry at line 1631-1638) — a room transition sits between “she accepts defeat” and “she becomes an ally,” with nothing narratively bridging the two. Confirmed via direct JSON read. Not a functional bug — nothing fails to trigger, all encounters/rewards/dialogue references resolve correctly — just a writing/pacing observation, and moot for v0.1 anyway since Story mode is fully gated off (StartMenu.V01_CRYSTALS_ONLY=true). Low priority, logged for whenever Story mode ships.
  • No uncollectable rewards, impossible encounters, or broken dialogue triggers found elsewhere in story content.

code quality round 6

  • Re-confirms the 5 already-documented dead audio assets (attack.wav, enemy_hit.wav, special_aegis.wav, wave_clear.wav, special_rally.wav) — not new, matches SOUND_MAP.md:159 exactly.
  • No schema gaps found — every .get("x", default) call in /sim reads a field that genuinely exists in bible.json or an internal TABLE structure.
  • (checked, refuted) scout flagged SOUND_MAP.md:160’s “untested: BOSS/WARDEN/BOSS DOWN” note as stale because those sounds “are implemented and working” — this conflates two different claims. The doc’s “untested” note is about unit-test coverage (there genuinely isn’t one, confirmed independently by the audio scout back in cycle 6), not about whether the runtime wiring works (it does). The doc is accurate; the scout misread it.

tooling/skill round 6

  • bh-diagnose-render-timing and bh-live-debug-deploy both checked clean — no stale numbers, no dead references, scope still matches current architecture.
  • [real finding, NOT auto-fixable] NEXT_TASKS.md’s check-test-count.sh DONE entry (and the roadmap memory echoing it) cites “204/204 test scripts” as the verified baseline — actual current count is 209 (ls tests/test_*.gd | wc -l), having grown with v0.1-prep feature work (ship-tilt, weapon-aim, blaster, weapon-thresholds tests) added since that entry was written. The entry is still correctly marked DONE (the guard script itself works), just the cited number has drifted. This loop’s ground rules explicitly forbid editing NEXT_TASKS.md — flagging clearly here so Chris (or a future session) can bump 204/204 → 209/209 by hand in both NEXT_TASKS.md and the roadmap memory.

01:12 cycle 11 — lenses: sim round 7 (net/ai), rendering round 6, content/balance round 7 (drones/decoys), code quality round 7

Section titled “01:12 cycle 11 — lenses: sim round 7 (net/ai), rendering round 6, content/balance round 7 (drones/decoys), code quality round 7”

No fixes applied this cycle — zero commits made. Rendering round 6’s 2 new co-op findings are folded into the cross-cutting section above rather than duplicated here.

sim round 7 — net/ai directories

  • Clean: net/ (telemetry, gameplay_telemetry, control_client, telemetry_device) and ai/ (auto_player, attract-mode only) are both small, purpose-scoped, and correctly gated — no dead code or dangling stubs. The unmerged networked-MP work correctly lives only in its separate worktree, not leaking into main.
  • [security note, checked, low-risk confirmed] net/control_client.gd:16 hardcodes CONTROL_KEY in source (matches ~/.secrets:BULLET_HEAVEN_CONTROL_KEY). Verified: the GitHub repo (chrisallenuk/bullet-heaven) is private, and the code’s own inline comment already reasons through this exact trade-off (dev/playtest-only tool, gated behind BuildConfig.dev_tools(), never in shipping/web builds, same key already exposed in the console URL anyway). Confirmed as designed, not a fresh concern — logging only because hardcoded secrets are worth a second set of eyes, not because anything looks wrong.

rendering round 6

  • [idea, low priority] render/story_map_renderer.gd:27’s _pulse accumulates indefinitely across a run rather than wrapping modulo TAU — theoretically could hit float-precision wobble in its sine-based shimmer after many hours of continuous runtime. Not remotely reachable in a normal play session; logging for completeness only.
  • [idea] render/lancer_telegraph_renderer.gd uses a fixed yellow/gold telegraph color rather than tinting by the enemy’s current element (like dash_telegraph_renderer.gd already does) — minor visual-consistency polish idea.

content/balance round 7 — drones/decoys

  • [idea, partially verified] Decoy shop-unlock costs confirmed accurate against data/bible.json (Damage 200g, Tank 250g, Healer 300g, Ultimate 500g). The scout’s cost-per-DPS comparison (claiming Tank and Healer are strictly worse value than the cheaper Damage decoy) is plausible and internally consistent, but I couldn’t independently verify the underlying per-decoy damage/interval constants in drone_director.gd within this cycle’s time budget — logging as a balance idea worth Chris’s own look with the actual numbers in front of him, not a confirmed bug.
  • [idea] Decoy recharge (~25s) vs. active duration (~8s) creates a long idle gap between uses — flagged as a cadence/feel question for playtesting, not a bug.

code quality round 7

  • [idea, low confidence] sim/mod_state.gd:16-17’s has_any() uses direct float equality (!= 1.0) rather than an epsilon comparison — technically fragile in the abstract, but the scout’s own proposed failure case doesn’t hold up: mod magnitudes are always meaningfully different from 1.0 (15-50% boosts), far beyond any realistic floating-point rounding error, so the claimed false-negative isn’t actually reachable with real content values. Logging as a very low-priority theoretical note, not a live bug.
  • No other correctness issues found in content_db.gd/mod_state.gd — lookups, defaults, and type guards all checked sound.

01:22 cycle 12 — lenses: audio round 6, UI/UX round 7, tooling round 7, rendering round 7 (all pushed toward the co-op pattern this cycle)

Section titled “01:22 cycle 12 — lenses: audio round 6, UI/UX round 7, tooling round 7, rendering round 7 (all pushed toward the co-op pattern this cycle)”

No fixes applied this cycle — zero commits made. All 4 findings are folded into the cross-cutting co-op section above (now believed comprehensive across sim/render/audio/UI) rather than duplicated here.

tooling round 7 — bh-add-content and bh-sdd-concurrent both audited in full detail this pass (not just sampled): both fully current, no stale paths/numbers/references found. Two more clean skills confirmed, following the drift found earlier in bh-dev-chunk/bh-area-background.

rendering round 7 — dedicated repo-wide grep sweep (grep -rn "sim.player.pos\|sim.player.hp" render/) confirms only the 2 already-known sites exist in the entire render/ directory — the render-layer portion of the co-op pattern is now exhaustively catalogued, not just sampled.

01:32 cycle 13 — lenses: sim round 8 (core data structures), content/balance round 8 (ship classes), code quality round 8 (boss_rotation.gd), UI/UX round 8 (codex/bestiary)

Section titled “01:32 cycle 13 — lenses: sim round 8 (core data structures), content/balance round 8 (ship classes), code quality round 8 (boss_rotation.gd), UI/UX round 8 (codex/bestiary)”

No fixes applied this cycle — zero commits made. Two standout findings this cycle, both flagged prominently rather than folded into a bullet list.

⭐ [bug, high-confidence, verified] Centurion can never be marked “seen” in the bestiary/codex — sim/enemy_pool.gd:27 defines TYPE_CENTURION := 23, but content/bestiary_db.gd’s TYPE_KEYS array only has 23 entries (indices 0-22, ending at TYPE_CUSTOM) — there’s no index 23. key_for_type(23) returns "" (out of bounds), which first_encounters() treats as “no codex entry,” so Centurion — the enemy that shipped in BUILD 165 with its own dedicated 7-task review cycle — will show as permanently locked (“???”) in the codex no matter how many times the player fights it. One-line fix (add "centurion", # 23 TYPE_CENTURION to TYPE_KEYS), but not auto-applied: it changes real UI-visible behavior and could interact with existing bestiary tests that assert the array’s length/contents — belongs in a proper bh-dev-chunk pass with the test suite run, not an unattended edit.

  • [idea, same file] TYPE_RUSHER/TYPE_ZAPPER (indices 9-10) are in TYPE_KEYS and have bestiary entries but the scout couldn’t find them in the live SpawnTable — worth a decision on whether they’re shelved content (remove) or context-specific spawns (keep), a smaller version of the same “codex entry vs. actual spawn reachability” question as Centurion’s wave-15 discussion from cycle 2/8.

⭐ [idea, well-quantified] Obsidian cruiser is close to a strict upgrade over all 6 frigate hulls, not a genuine playstyle choice — elaborates on CLAUDE.md’s own already-flagged “needs playtesting” note with real numbers: +70% HP, +67% weapon slots, +10 armor (worth ~60% damage reduction, capped at 75% globally), for only a 19% speed penalty that’s easily offset by a single move-speed level-up card, and a 50% larger hitbox that the armor bonus largely compensates for anyway. All 6 frigate hulls share identical base stats — their only differentiation is a cosmetic “headline bonus” (Manta=damage, Cobalt=fire-rate, etc.), so right now frigates read as “pick your flavor of early-game placeholder” rather than a real alternative to the cruiser. Not a bug — a balance-design input for whenever Chris does the frigate/cruiser playtest pass already on the books.

sim round 8 — spatial_hash.gd / enemy_pool.gd

  • Clean, thorough pass: swap-remove patterns, backwards-iteration in _sweep_dead(), hash rebuild-before-query ordering, and capacity/bounds checks all verified correct. No bugs found in the core entity/collision data structures.

code quality round 8 — boss_rotation.gd

  • No logic bugs in the rotation/index math itself (mod-5 rotation, boss-type checks, HP scaling all correct).
  • [dead-code, verified] boss_rotation.gd’s maybe_spawn_survival_boss() is called from zero production code — grepped the whole repo: its only callers are tests/test_boss_rotation.gd (20+ call sites) and tests/test_v01_funzo_only.gd. The actual live boss-spawning path is Sim._spawn_phase_boss() (sim.gd:1271-1284). Either this is orphaned code from an earlier design whose test file was never cleaned up, or something intended to wire it in never happened — genuinely unclear which without more context, so not characterizing it as either “safe to delete” or “a real gap,” just flagging that a substantial, well-tested function has no production caller at all.

01:42 cycle 14 — lenses: rendering round 8 (boss renderers), content/balance round 9 (elites), audio round 7 (test cross-check), tooling round 8 (bh-deploy/bh-appstore-release re-audit)

Section titled “01:42 cycle 14 — lenses: rendering round 8 (boss renderers), content/balance round 9 (elites), audio round 7 (test cross-check), tooling round 8 (bh-deploy/bh-appstore-release re-audit)”

No fixes applied this cycle — zero commits made.

⭐ [bug, verified] Boss2 has a real enrage mechanic with zero visual telegraph — sim/boss2.gd:114-117 genuinely enrages Boss2 at a low-HP threshold (BOSS2_ENRAGE_FRAC), speeding up its attack cadence (BOSS2_ENRAGE_TIME_MULT), and render_info() (sim/boss2.gd:239) correctly exposes "enraged": sim.boss2.enraged to the renderer. But render/boss2_renderer.gd never reads that key at all (zero matches, grep-verified) — unlike FunZoRenderer, GravitonRenderer, and EyeRenderer, which all render a distinct pulsing/crackling aura on enrage. Boss2 looks visually identical calm vs. enraged even though it’s mechanically more dangerous, breaking the established visual-telegraph language every other boss follows. Real, missing-feature-shaped gap (not a trivial fix — needs new render code matching the established pattern), logged for a real dev pass.

content/balance round 9 — elites

  • [status, confirmed accurate] CLAUDE.md’s “~12 promotable enemy types get only the generic elite buff” checks out exactly right: 4 types have bespoke elite mechanics (Charger’s chained dash, Spider’s landing web+ray-fan, Shooter’s predictive aim+2x damage, Swarmer’s brood-burst-instead-of-XP), the other 12 (Tank/Splitter/Brute/Rusher/Zapper/Scatterer/Bomber/Orbiter/Lancer/Ghost/Accumulator/Centurion) get only the generic HP/contact/radius/speed multipliers. No type skipped or double-applied. Useful precise checklist for whoever eventually gives more elites bespoke powers.

audio round 7 — test cross-check

  • Thorough pass (46 + 13 assertions individually traced against current code): zero test drift found in test_audio_manager.gd/test_ui_sound_wiring.gd. Sixth consecutive audio-lens pass without a new confirmed issue — this system is genuinely solid.

tooling round 8 — skill re-audit

  • bh-deploy and bh-appstore-release both re-checked thoroughly (not just re-trusting the earlier light pass) — zero staleness found in either: BUILD/flag/path references, ASC app ID, bundle ID, export preset names, and the tvOS-fork Godot binary path all still current. That’s now 6 of 8 project skills confirmed clean tonight, 2 with real drift found earlier (bh-dev-chunk, bh-area-background).

01:52 cycle 15 — lenses: audio round 8, sim round 9 (turret/evolutions), UI/UX round 9 (sibling-panel comparison), tooling round 9 (exhaustive re-verify)

Section titled “01:52 cycle 15 — lenses: audio round 8, sim round 9 (turret/evolutions), UI/UX round 9 (sibling-panel comparison), tooling round 9 (exhaustive re-verify)”

No fixes applied this cycle — zero commits made.

audio round 8

  • (checked, refuted) “_pool_cursor never resets between runs, could cause stale/doubled audio” — false. audio_manager.gd:139-140 shows a standard round-robin pool: play() always grabs whichever player the cursor currently points to and immediately calls play on it, which replaces/interrupts whatever that player was previously doing regardless of cursor value. This is identical to how the cursor behaves wrapping mid-run (happens constantly) — there’s nothing special about a run boundary. Not a bug; a round-robin cursor persisting across runs is the correct, intended behavior, not state leakage.
  • Music transition logic and per-frame SFX cap reset both checked clean.

sim round 9 — turret/evolutions

  • Clean: turret deployment/lifetime/cooldown/cap logic and the evolution evolved-flag double-apply guard (can_evolve()) both checked out correct.

UI/UX round 9 — sibling-panel comparison

  • (checked, likely refuted — high confidence but not click-tested) “MetaShopPanel’s RESET SHOP confirm dialog blocks mouse clicks on desktop because _handle_reset_dialog_input() doesn’t explicitly route InputEventMouseButton” — this doesn’t hold up against the code’s own logic. The exact same non-handling pattern already exists, documented and intentional, on the carousel’s own _input() just above it (meta_shop_panel.gd:689-691: “Deliberately does NOT route InputEventMouseButton here — left-click press/release must keep flowing to Button.pressed for click-to-activate on desktop”). Godot dispatches Button.pressed via its own _gui_input/click-capture pathway, independent of this custom _input() override, unless set_input_as_handled() is explicitly called on that same event — which _handle_reset_dialog_input() never does for mouse clicks (only for confirm/back/nav matches). So by the same reasoning the carousel already relies on, clicks should still reach the dialog’s buttons normally. Logged as likely-a-false-positive rather than confirmed-fixed-or-broken, since I can’t literally click-test without running the game — a 30-second Mac playtest would settle it either way.

tooling round 9 — exhaustive re-verify

  • bh-diagnose-render-timing and bh-live-debug-deploy re-checked with actual file/line grep verification for every referenced path/function (not just skimmed) — both fully current. That’s all 8 project skills now individually audited tonight: 6 clean, 2 with real drift found earlier (bh-dev-chunk, bh-area-background).

02:02 cycle 16 — lenses: sim round 10 (starter weapons), content/balance round 10 (crystals thresholds), code quality round 9 (spawn_director), rendering round 9 (player ship tilt)

Section titled “02:02 cycle 16 — lenses: sim round 10 (starter weapons), content/balance round 10 (crystals thresholds), code quality round 9 (spawn_director), rendering round 9 (player ship tilt)”

No fixes applied this cycle — zero commits made. This is the last discovery cycle of the run; remaining time goes to a synthesis pass.

  • [clean] sim/weapon_blaster.gd/sim/weapon_aim.gd (starter weapons) — nearest-enemy targeting, cooldown timing, pierce/burst/knockback parameter passing, fire-rate scaling all checked correct.
  • [clean] sim/spawn_director.gd — every time-bucket’s spawn-weight table sums to exactly 100% with no gaps/overlaps/dead branches (scout’s own mid-analysis “critical bug” flag was a false alarm it self-corrected).
  • [idea, cosmetic only] sim/weapon_thresholds.gd:23-28 — Orbit’s crystals-mode threshold array is [cold@5, cold@12, lightning@9, void@18], not strictly increasing by trigger value like every other weapon’s table (all 7 others + the aim weapon are cleanly ascending). Scout confirms this is functionally harmless — threshold application doesn’t care about array order — purely a source-readability nit. Trivial reorder candidate for whenever that file is next touched for real work.
  • [idea, dev-tool only, unverified] render/player_renderer.gd:340’s Shader Warp tilt mode may leave a stale tilt shader uniform when switching to another tilt mode and back — could cause a one-frame visual snap. This only affects the dev-only Debug Settings tilt-mode comparison toggle, not normal play; not deeply verified given end-of-run time constraints, logged as a plausible low-priority lead rather than a confirmed bug.

02:12 cycle 17 — lenses: code quality round 10, content/balance round 11 (both final systematic sweeps)

Section titled “02:12 cycle 17 — lenses: code quality round 10, content/balance round 11 (both final systematic sweeps)”

No fixes applied this cycle — zero commits made. This is the last cycle of the run.

⚠️ [bug, verified, high severity] The “crit” upgrade grants zero effect — data/bible.json:575 defines an upgrade (id: "crit") with "effect": "crit_chance", but that effect string exists in neither sim/stat_effects.gd nor sim/sim_mods.gd (grep-verified, zero matches in both). sim/upgrades.gd:26-31’s apply() checks StatEffects.is_known() then SimMods.is_known(); if neither recognizes the effect, it logs a console-only push_error and returns — no crash, no in-game indication. A player who takes this upgrade card during a run gets no stat change at all, with nothing telling them it didn’t work. Written up in the executive summary as the most severe finding of the night. Not auto-fixed — implementing crit_chance (or removing the dead upgrade entry) is real feature/content work requiring a bh-dev-chunk pass, not something to guess at unattended.

[bug, independently re-confirmed] A second scout, working a different systematic method (checking every enemy type’s bestiary/index mapping rather than investigating the Centurion specifically), independently rediscovered the exact same TYPE_KEYS-missing-index-23 bug already logged in cycle 13 — good cross-validation of that finding. Importantly, this systematic re-check found no other enemy type with a similar mismatch, so the Centurion gap appears to be an isolated one-off, not a wider pattern.

02:22 cycle 18 — final cycle: systematic hunt for more unimplemented-effect-string bugs (crit_chance’s bug class)

Section titled “02:22 cycle 18 — final cycle: systematic hunt for more unimplemented-effect-string bugs (crit_chance’s bug class)”

No fixes applied — zero commits made.

  • (checked, refuted) A scout claimed the "drone" effect (used across 47 meta-upgrade entries: drone-slot/recharge/sentinel/bomber/interceptor/disruptor/logistics unlocks) is unimplemented the same way crit_chance is — false, and NOT the same bug class. Checked data/bible.json:1220-1232: every one of these entries has "type": "unlock" + a "target" field (e.g. "target": "class:bomber"), the identical pattern already confirmed working for the decoy unlocks earlier tonight (cycle 11). These dispatch through a separate unlock-handling path (matching on type/target), not through StatEffects/SimMods’ magnitude-effect tables — "effect": "drone" here is just a category tag, not a dispatch key. The scout’s methodology (checking only the two magnitude-effect tables) doesn’t apply to type: "unlock" entries. crit_chance genuinely IS broken because it has no type: "unlock" field and truly does rely on the magnitude-effect dispatch it’s missing from — that finding stands, this one doesn’t.
  • All 17 other magnitude-effect strings (damage_mult, fire_rate_mult, move_speed, pickup_radius, max_hp, armor, decoy_power, decoy_life, thrusters, still_heal, stack_bonus, reaction_damage_mult, aura_duration_mult, projectiles_pierce, split_on_hit, lifesteal_per_kill) cross-checked present and correct in StatEffects/SimMods — crit_chance remains the one genuine gap found tonight in this category.
  • bh-add-renderer (strong candidate) — a checklist for the OTHER repeatable multi-file wiring task besides bh-add-content (enemies/weapons): adding a new renderer/visual effect. Five steps get routinely missed or deferred: create the renderer script, wire the instance + per-frame update into main.gd, wire it into QualityManager (or add an explicit no-op predicate), update docs/architecture/rendering-and-performance.md’s cost model, and — validated directly by tonight’s own findings — guard _process()’s queue_redraw() against an empty-state early-out (the exact pattern 13 existing renderers are missing, see the cross-cutting section above). Concrete, well-scoped, and this session is itself evidence of the gap.

  • bh-add-sound-event (strong candidate) — a checklist for wiring a new sound-triggering event: emit the fx_events kind from /sim, map it in AudioManager.SOUND_FOR_FX/SOUND_FOR_NAMED_REACTION, and update audio/SOUND_MAP.md’s status table. bh-add-content doesn’t mention audio at all today, so a weapon/enemy with a new sound cue means cross-referencing two unrelated docs from memory.

  • [doc-gap] the “spawn eligibility is a 3-tier gate” lesson (docs/architecture/enemies-bosses-survival.md’s Centurion section) is written as enemy-specific, but the same “wiring must be complete across N independent systems or it silently fails” shape also applies to renderer LUT-sizing (main.gd’s _TYPE_SHAPE partition count) — worth generalizing into docs/godot-gotchas.md as a named anti-pattern rather than leaving it as one feature’s footnote.

  • [doc-gap, minor] NEXT_TASKS.md has no stated intake template for how a fresh agent should file a new finding — a short header note would help findings actually stick instead of evaporating into chat/memory.

  • bh-determinism-repin (strong candidate) — CLAUDE.md itself documents the determinism baseline has been re-pinned “many times” across 3 separate locations (prose in CLAUDE.md, the actual assertions in tests/test_determinism_checksum.gd + test_determinism_crystals.gd, and memory bullet-heaven-roadmap), and explicitly warns “this line has gone stale before” — a skill wrapping the re-pin checklist (update all 3 locations, run the 600-tick trace to get the new signature, commit with a standard message) would directly prevent a documented recurring failure mode. Worth building.

  • bh-write-test (second strong candidate) — scaffolds new GUT test files with this project’s non-obvious conventions: extends GutTest, the _sim()/SimContentFixture.db() fixture pattern, the assert_lte/assert_gte naming trap (typo’d as assert_le/assert_ge silently drops the whole file from the suite), and the .gutconfig.json dirs-hardcode root cause behind the already-known -gtest-doesn’t-narrow gotcha. Concrete, well-scoped, surfaced independently by a second scout in a later cycle.

  • “P2 progression completion” skill — re-scoped by me: this is a one-off feature-completion task (bridge _award_xp/upgrade_system/drone_director to per-pilot), not a repeatable workflow pattern. Belongs in a spec/plan, not a skill.

  • [doc-gap, minor] NEXT_TASKS.md has no stated convention for when to prune DONE entries — a one-line note at the top would help.

  • [doc-gap, already covered] scout suggested documenting the export_presets.cfg-must-be-copied-into-fresh-worktrees gotcha — this is already in this project’s own CLAUDE.md under “Running & testing,” not actually missing.

  • [doc-gap, low value] a .claude/ hooks/conventions index was suggested — marginal value given .claude/ is mostly machine-local/gitignored already; skipping.

18 sweep cycles ran (23:35 → 02:32 BST) across all 7 lenses, several rotated through 8-10 rounds each. 2 trivial fixes committed (5e6e20b, 8b17074), both comment/dead-line cleanups outside /sim. 146 findings logged for review (bugs, balance ideas, perf ideas, false-positive corrections all included in that count) plus 10 skill ideas. Read the Executive summary at the top of this doc first — it has the 6 highest-priority items distilled out, including the most severe finding of the night (the “crit” upgrade being a complete no-op). Doc is ready for Chris to review.