Skip to content

Overnight improvement loop (2026-10-07)

Overnight improvement loop, 2026-10-07 → 08

Section titled “Overnight improvement loop, 2026-10-07 → 08”

Trigger: Chris, 2026-10-07 23:13: “run the improvement loop overnight for the next 4 hours, every 10 mins fan out to find things to improve, short term medium term and long term, all areas, sound definitely need some more work so put that in the plan but cover everything, lets start on internet play tomorrow too”.

Mechanism: bh-overnight-audit-loop skill. A cron fires every 10 minutes from 23:15 to 03:15. Each firing runs .claude/workflows/improvement-loop-cycle.js, or waits for the previous cycle if it is still running. A cycle:

  • runs 7 finders: 2 sound lenses (Sonnet, rotating through coverage, mix, music, feel, tech and roadmap), 1 internet-play readiness lens (Sonnet, rotating through determinism, transport, latency and product), and 4 rotating whole-game lenses (Haiku);
  • tags each finding with a kind (bug / improvement / opportunity) and a horizon (short / medium / long);
  • has 2 Sonnet skeptics try to refute each finding (both must fail), capped at 14 per cycle with the rest carried to the next cycle;
  • has Sonnet agents fix the surviving low-risk findings one at a time.

Guardrails: low risk only; every fix passes the GUT suite and test-count guard before commit and push. The loop never touches audio assets or the bus layout, whole-mix rebalances, balance numbers, platform/tvos/, project.godot, BUILD, the determinism baselines, or any branch but main. Medium and high risk findings are reported only.

Output: the cycle log below, then a Plan section at wrap-up: short, medium and long term, with a dedicated sound plan and an internet-play kickoff for 2026-10-08.

Legend: FIXED = committed and pushed. Reported = confirmed but left for a human.

Lenses: sound-coverage, sound-mix, net-determinism, weapon-logic, enemy-ai, bosses, game-feel. Verified 14, 11 survived both skeptics, 3 refuted, 11 carried to the next cycle.

  • FIXED (1635e17a) · improvement/short · sound-mix: High-frequency SFX have no pitch or volume variation (audio/audio_manager.gd:349). Add a small per-key jitter table in _route (for example +/-6 to 10% pitch and +/-1.5 dB). Give pickup its own pitch ladder that rises with consecutive pickups. Use the global randf, which is render-side and safe for determinism. Test with the existing audio tests, and add one asserting that pitch_sc
  • FIXED (1be21bc1) · bug/short · weapon-logic: Scar Tissue lifesteal card reads ‘+-92% lifesteal’ (sim/upgrade_system.gd:399). In weapon_mod_label, add a lifesteal case that uses the additive value directly: ‘return “+%d%% lifesteal on discharge” % int(round(mag * 100.0))’. This is display-only and touches no sim state, so the determinism baselines are unaffected. Add a test asserting the label for wm:scar_tissue:lifesteal
  • FIXED (da2fb27d) · bug/short · bosses: Boss2 charge telegraph ends 480px short of where the dash actually hits (render/boss2_renderer.gd:90). Derive the length once from the constants (BOSS2_CHARGE_SPEED * BOSS2_ACTIVE_TIME) and use it in the renderer’s cend, the fx ‘length’ and the telegraph corridor arc. Render/fx-only change; no balance or sim-state change, so determinism baselines are untouched. Add a test asserting telegraph length >
  • reported [medium] · improvement/short · sound-coverage: Weapon evolution and the weapon-threshold rungs have no audio or fx event (sim/sim.gd:1106). Emit an audio-capable fx_event such as {kind:‘weapon_evolve’} at the evolve branch, and map it in the SOUND_FOR_FX or priority path. Reuse an existing sting such as special_aegis or special_rally as a stopgap. The new event must not touch rng or the pinned determinism baselines, since fx_events are
  • reported [medium] · improvement/short · sound-coverage: Shop purchase reuses the level-up sting; gem pickups reuse the menu click (audio/audio_manager.gd:499). First, small mappings using existing assets: give shop buy a distinct existing cue (for example special_rally or victory pitched down) and give the pickup kind a quieter or pitch-varied cue. Then commission dedicated cues for buy, deny and dimension confirm.
  • reported [medium] · improvement/short · sound-mix: Music is louder than almost every SFX, with no bus trim or ducking (audio/bus_layout.tres:19). Set the Music bus to about -6 dB, or use a LUFS-matched trim. Add a short sidechain-style duck: on priority cues (boss/wave_start/level_up/victory) drop the Music bus by 4 to 6 dB for roughly 0.4 s with a tween in AudioManager.play(priority=true). Expose the Music offset and duck depth as remote-con
  • reported [medium] · improvement/short · sound-mix: Stings and level-up are 15+ LU louder than gameplay SFX (audio/audio_manager.gd:481). Trim level_up, victory, wave_clear and wave_start by 4 to 6 dB at play time, using a per-key gain table in AudioManager instead of re-exporting the wavs. Give ui_buy() a dedicated quieter sound or a -8 dB offset. Confirm by ear before and after. Chris owns the final levels.
  • skipped [medium] · improvement/short · net-determinism: net_checksum omits weapon cooldowns, projectile and gem payload and upgrade stats, so divergence is detected late and cannot be localized (sim/sim.gd:3414). Add a debug-only ‘full’ checksum or per-section hash vector (pilots, enemies, projectiles, weapons, upgrades, boss, rng) sent alongside the row at a lower cadence. Clients report which section differs first. Keep net_checksum separate so the pinned determinism baselines stay untouched. Add a test th
  • reported [medium] · bug/short · weapon-logic: Dead re-offers: Wildfire decal and Turret count stay on the card after they saturate (sim/upgrade_system.gd:107). Extend the shard-style filter in roll_upgrade_choices: skip ‘decal’ when the wildfire instance’s leaves_decal is already true, and skip ‘count’ when turret.max_turrets >= MAX_TURRETS_CAP. Keep draw order stable where possible, since removing pool entries changes the shuffle. Medium risk because it c
  • reported [medium] · bug/short · enemy-ai: Skirmisher ignores Dimension fire-rate, projectile-speed and Halo damage buffs (sim/enemy_behaviors.gd:150). In step_skirmish, read fr = sim.enemies.fire_rate_mult[i] and ps = sim.enemies.proj_speed_mult[i]; use fire_interval / fr for the fire check and SKIRMISH_PROJ_SPEED * ps for the speed. Multiply SHOOTER_PROJ_DAMAGE by sim.halo_chain_director.dmg_mult_for(sim, i). Mirror the Shooter pattern in enemy_a
  • reported [medium] · bug/short · enemy-ai: Coil silently discards manual-weapon and Orbit-shard knockback (sim/sim.gd:2692). Decide the intent. Either (a) add TYPE_COIL to ElementalSystem.kinetic_immune with a comment, so knockback is never stored (the explicit version of current behaviour), or (b) make step_coil add the pending knockback to coil_center so the shove displaces the ring and decays back. Option (a) is the sm

Refuted: Chronowell heal card shows +3 even when the next pick gives +9; Warden boss telegraph and lunge-end events, ghost_warn, and phase_flicker emit fx events with no sound; Voice stealing and the per-frame cap can silently drop gameplay cues in large fights.

Lenses: sound-music, sound-feel, net-transport, onboarding-retention, ui-layout, input-modals, perf. Verified 14, 12 survived both skeptics, 2 refuted, 4 carried to the next cycle.

  • FIXED (e1d9a5a7) · bug/short · ui-layout: Story objective label rect overlaps the boss HP bar by 8 units (ui/hud.gd:162). Move the story label up or the boss bar down so the rects do not intersect. The simplest is BOSS_BAR_Y = 100 (still clear of the banner at 146 and the P2 block at 42-58), or move the story label to y=62 (still clear of the HP block at 22-38 and the P2 block at 42-58 only if P2 is off). Check both re
  • FIXED (ac431575) · bug/short · input-modals: Start/Back closes the pause menu under Debug Settings and the run keeps ticking behind it (ui/debug_settings_panel.gd:1208). Two options that follow existing patterns. (a) In DebugSettingsPanel._input, consume JOY_BUTTON_START and JOY_BUTTON_BACK the way the ESC/B branches do, so Start cannot reach main while the panel is up. Codex BROWSE needs the same treatment. (b) Narrower and more robust: in main._toggle_pause, check
  • reported [medium] · improvement/short · sound-music: Music changes are hard cuts that restart from 0, including boss end returning to the loop (audio/audio_manager.gd:472). Add a second AudioStreamPlayer on the Music bus and tween volume_db for about 0.8-1.5 s on swap and about 0.6 s on stop_music(). Remember each track’s playback position so a returning loop uses play(from_position). Keep the ‘same key is a no-op’ contract. Fade values and any boss-entry stinger timin
  • reported [medium] · bug/short · sound-music: Both looping MP3s have 50-80 ms of leading silence, so every loop restart leaves a small gap (audio/audio_manager.gd:497). Set loop_offset in the .import files to the leading-silence length (about 0.05 s and 0.08 s), or re-encode to a gapless format such as OGG with trimmed priming. Audition the seam after the change. Only the offset is a small local change. Re-encoding means new assets.
  • reported [medium] · improvement/medium · sound-feel: No player-facing music/SFX volume or mute setting in release builds (ui/pause_menu.gd:202). Add Music and SFX volume controls (or at least on/off toggles) to the pause menu, modelled on the Haptics toggle: a meta.audio_music and meta.audio_sfx value loaded at boot, applied with AudioServer.set_bus_volume_db, and persisted by a main.sync*_pref hook like _sync_haptics_pref. Check tvOS focu
  • reported [medium] · improvement/medium · sound-feel: Low-hull warning exists only as haptics; no audio equivalent, and the player-hurt sound is very quiet (input/haptics_director.gd:37). First, raise king_hit by trimming its volume in AudioManager.hurt() (or remaster it), which is a mix-level decision for Chris. Second, add an audio low-hull state driven from main.gd’s existing hp fraction (the screen_fx.set_hp_fraction call at main.gd ~2577). A cheap version with no new assets is a
  • reported [medium] · improvement/short · onboarding-retention: Unfinished tutorial re-forces WAKE UP on every cold launch, with no lesson resume and no Survival latch (main.gd:496). Either (a) latch tutorial_done when the first non-tutorial run is banked (one line in _bank_run, a product call), or (b) persist the lesson index in MetaState (a new field, with from_dict defaulting to 0) and offer ‘Resume tutorial / Skip to Survival’ on the menu instead of forcing boot into the tut
  • reported [medium] · improvement/medium · onboarding-retention: Locked weapons never appear in level-up offers, so the unlock goal is invisible during a run (sim/upgrade_system.gd:95). Add a non-selectable teaser card for the next locked weapon in weapon_ids, using a dedicated card kind that the panel renders as locked and that is never applied. This changes the offer pool and the upgrade_rng draw count, so it needs a determinism re-pin (bh-determinism-repin) and a balance check,
  • reported [medium] · bug/short · ui-layout: Touch dialogue panel covers the top-centre HUD stack (boss bar, story objective, wave banner) in story mode (ui/dialogue_box.gd:211). On touch, when the HUD boss/story/banner stack is visible, either (a) push the panel below the banner (offset_top about 200 on touch, check it still clears the touch move stick and aim ring at the bottom edge), or (b) hide or dim the story label and banner while dialogue is showing (the dialogue alr
  • reported [high] · improvement/medium · sound-music: survival_loop and menu_theme are the same file, so gameplay has no music of its own and only two distinct tracks play (audio/audio_manager.gd:460). Plan 2-3 distinct gameplay tracks or stems, such as a calmer survival bed and a higher-intensity variant, plus a per-dimension or area key map next to the existing key-to-stream map. Needs new composed or generated assets and a decision on the audio direction.
  • reported [high] · opportunity/long · sound-music: No adaptive layering: music does not respond to intensity beyond a binary boss swap (main.gd:1877). Introduce a small MusicDirector in the render/UI layer only (never /sim). It would read sim state each frame and drive N synchronized stem players on the Music bus, or an AudioStreamSynchronized with per-layer volume ramps (intensity, low-HP filter, boss layer). Quantize switches to bars. This depen
  • reported [high] · opportunity/medium · net-transport: No internet transport exists: only ENet-by-IP, and the RelayTransport and room DO are still unbuilt (net/net_transport.gd:6). Build the room-code Durable Object (Hibernation WebSocket API) plus RelayTransport. Add a join-by-code UI and replace broadcast discovery with relay-assisted listing. For tomorrow only, a stopgap is to run an ENet host on a public VPS (the Hetzner box) or a Tailscale network, with clients using manu

Refuted: Projectile broad-phase queries every hit with the largest-enemy radius (80px), not the live maximum; iOS/tvOS/Android audio session and interruption behaviour is left at engine defaults and unverified.

Lenses: sound-tech, sound-roadmap, net-latency, data-consistency, save-meta, coop, node-lifecycle. Verified 14, 11 survived both skeptics, 3 refuted, 10 carried to the next cycle.

  • FIXED (2f0b0e38) · improvement/short · net-latency: No RTT or packet-loss readout exists for tomorrow’s internet playtest (net/enet_transport.gd:66). Add func rtt_ms() -> int: return -1 to NetTransport. Override it in ENetTransport using get_peer(HOST_PEER) or the first peer, and return round-trip time and packet loss. Print them in the banner and in _net_report. Keep it to the dev banner and CLI print only, with no new telemetry fields, so the
  • FIXED (93c9bf0e) · bug/short · data-consistency: Bomber shop copy hard-codes ‘+60% vs heavies’ while BOMBER_HEAVY_BONUS is a live tunable; heavy list also omits Accumulator (ui/shop_categories.gd:77). Build the Bomber section text at display time from DroneDirector.BOMBER_HEAVY_BONUS, e.g. ‘(+%d%% vs heavies)’ with (bonus-1)*100, following the MetaCopy token pattern. Add ‘accumulators’ to the target list. Copy-only: no balance number changes, and the pinned determinism baselines are not affected.
  • FIXED (2788fca8) · improvement/short · data-consistency: Obsidian cruiser card hand-types hull numbers that live in code, not in the bible or tunables (data/bible.json:2267). Generate the hull desc from ShipBonuses.TABLE at display time (slots, base_hp delta vs DEFAULT frigate, speed, and the existing label), as MetaCopy does for other numbers. Or add a unit test that parses the bible desc and asserts it against ShipBonuses.TABLE. The test is the cheapest fix and needs n
  • NOT LANDED (moved to branch overnight/meta-unreadable-save-guard for Chris’s call) · bug/short · save-meta: Unreadable meta.json is overwritten by the next purchase or bank save (meta/meta_store.gd:227). Guard save_state(): when last_load_compatible is false, do not overwrite meta.json. Keep the file, and report the save as not saved so the player sees a notice. Better still, on the first unreadable load copy meta.json to meta.json.unreadable- once, so a later write cannot destroy it. Also w
  • FIXED (b7c26920) · bug/short · coop: Results screen shows only P1’s level in co-op (ui/results_panel.gd:270). Display-only change: in co-op, show the max level across pilots (or ‘L3 / L9’ per pilot) in both the stats line and the LEVEL tile; leave DPS/KILLS (already party-wide via dmg_dealt_total and kills). Keep the 1P path byte-identical. Decide the death_cause wording separately (first vs last killer) as
  • conflict [low] · bug/short · coop: Powerup pickups are P1-only and can revive a downed P1 (sim/sim.gd:3212). Mirror _collect_gems: loop over pilots with hp > 0, apply the effect to the collecting pilot (heal, slow/freeze are global timers, nuke is party-wide). Add a guard for hp > 0 on P1 as the gem and weapon paths already do for P2. Add a test that drives a downed P1 over a heal drop and asserts hp stays
  • conflict [low] · improvement/short · node-lifecycle: Run-sweep keep-list is duplicated in two places and can drift (main.gd:1140). Move the keep-list into a single static predicate (or a const Array of persistent node refs) and have both _new_run and _return_to_menu call one _sweep_run_children() helper. Keep the existing order-of-operations (hide/reset calls stay in each caller). Verify with the existing run-restart tests; n
  • reported [medium] · bug/short · sound-tech: funzo_confetti (and boss_swing_impact) are effectively inaudible assets (audio/audio_manager.gd:40). Peak/loudness-normalise the quiet files offline and re-import, or add a per-key trim table (key -> volume_db) in AudioManager.play() so levels can be tuned by ear without re-rendering assets. Chris should choose target levels, e.g. around -22 LUFS to match the other boss cues. Add a GUT test that as
  • reported [medium] · improvement/short · sound-tech: Enemy-death variants differ by up to 12 LU / 19 dB within the same pool (audio/audio_manager.gd:84). Add a per-key gain trim (a const Dictionary key -> volume_db, applied in _start or _death_sound_for) that levels all 8 variants to a common LUFS. Alternatively normalise the assets offline and re-import. Measure with ebur128 again afterwards. This changes the mix, so the target level is Chris’s call
  • reported [medium] · bug/short · net-latency: No ready barrier at start; a slow client load triggers a catch-up burst or a silent 5 s timeout drop (net/lockstep_session.gd:36). Add a client READY message sent after the run is built (and a client keepalive during load). The host holds row issuance, via the existing advance=false hold path, until every client is ready or a generous join timeout (about 30 s) passes. Needs a PROTOCOL_VERSION bump and a lockstep test. Alternati
  • reported [medium] · bug/short · save-meta: Quitting to the menu mid-run forfeits all run gold (main.gd:799). Call _bank_run(‘quit_menu’) at the top of _return_to_menu(), before the teardown and before _send_run_end. _bank_run is already idempotent via _run_banked and already sends run_end with the reason, so death-then-menu stays a no-op. Also consider banking on NOTIFICATION_APPLICATION_PAUSED for iOS/tvO

Concurrency incident: a parallel session (webs merge train) swept the meta-save fix agent’s uncommitted edits into its own commit 5b41ab48, then reverted them in 57800cb2; the fix agent reported that hash as its own. The two conflict rows above are fallout. From cycle 4 the fix agents work in a dedicated worktree (.claude/worktrees/overnight-loop-fix) and push HEAD:main. The meta fix also refuses every save while the on-disk file is unreadable, which is a design call (a downgraded build could never save again), so it is parked on a branch instead of landing.

Refuted: Web: SFX are not registered as samples at boot, so each first play can decode on the main thread; Level-up entry failsafe lambda can fire on a freed panel; Tutorial deaths bank gold and write the real save, unlike bay saves.

Lenses: sound-coverage, sound-mix, net-product, accessibility, content-depth, test-masking, tooling-dx. Verified 14, 11 survived both skeptics, 3 refuted, 5 carried to the next cycle.

  • conflict [low] · improvement/short · tooling-dx: Sound Map and Story Board JS/Python test suites are not run by the merge gate (scripts/gate.sh:48). Add a gate step: node –test tools/sound_map/tests/.test.js tools/story_board/tests/.test.js, plus python3 -m unittest for the two server.test.py files (run from the tests’ parent directories). Investigate the 120 s runtime first so the gate does not get slower unexpectedly. Test-only change, no e
  • conflict [low] · improvement/short · tooling-dx: gate.sh hardcodes a single-machine absolute path and copies a file it never uses (scripts/gate.sh:24). Delete line 24. If a worktree truly needs export_presets.cfg for some step, derive the main checkout from git rev-parse –git-common-dir instead of a literal path.
  • conflict [low] · improvement/short · tooling-dx: Godot gotchas doc cites a retired test file as the example to grep (docs/architecture/godot-gotchas.md:38). Rewrite the example to name a live test that exercises AreaDefs.other() (for example tests/test_areas.gd and whichever test now covers the N-way cycle), or mark the example as historical with the 2026-07-02 date. Documentation only.
  • reported [medium] · improvement/medium · sound-coverage: Railgun, Chalice, Thunder Wake, Wildfire and Stasis Web fire fx events but have no sound (audio/audio_manager.gd:16). Reuse existing assets to avoid new files. For example: railgun_tell -> special_freeze or pulse_zap pitched low; railgun_lance -> eye_laser at attenuated volume; mark_pop -> turret_impact; chalice_cast -> drone_pulse; thunder_wake_strike -> scatter_explosion; wildfire_pop -> nova_whoosh. Add each to
  • reported [medium] · improvement/short · sound-coverage: Wormhole and dimension warps are silent, and the portal sound uses the player-dash cue only for dashes (main.gd:752). In _begin_warp() play an existing cue as a priority sound, for example audio.play(‘graviton_pull’, …) or ‘special_freeze’, and a short ‘wave_start’-style or ‘drone_summon’ hit in _apply_area_change(). Alternatively add one audio.portal() helper alongside dash()/hurt(). This is a local main.gd chan
  • reported [medium] · opportunity/short · sound-coverage: Three finished sound assets (wave_clear, special_aegis, special_rally) are never loaded or played (audio/audio_manager.gd:169). Wire the existing files: add OVERCHARGE -> special_rally and BOMB -> special_stomp (or special_aegis) to SOUND_FOR_NAMED_REACTION, and add the new keys to the _ready() list. If wave_clear is meant for survival wave completion or a story beat, hook it into the matching sim event. Otherwise delete the
  • reported [medium] · improvement/short · sound-coverage: Powerup pickups for Slow and Heal are silent or reuse the quiet gem tick (sim/sim.gd:3225). Emit named reactions for these two (for example ‘HEAL’ and ‘SLOW’) from _apply_powerup. Map them in SOUND_FOR_NAMED_REACTION to the unused special_aegis and special_rally assets, or to special_frost. This reuses the pattern FREEZE and NUKE already follow. Adding fx events is render-side only, but co
  • reported [medium] · improvement/short · sound-mix: Unnamed ‘reaction’ fx events (bomber blast, tank missile telegraph, elite slam, Warden bursts) play the loud special_barrage at full volume, ungated and unthrottled (audio/audio_manager.gd:258). Split unnamed enemy reactions from named or boss reactions in _route. Give name==‘’ events with an enemy source the ENEMY_FX_KINDS treatment (visible_rect gate plus ENEMY_FX_ATTEN_DB), add jitter, and add a short cooldown for ‘special_barrage’ in SFX_COOLDOWN_TICKS. For the crystal burst, either emi
  • reported [medium] · improvement/short · accessibility: Fire and Blood are near-identical reds, and several other live-element pairs collapse under colour-vision deficiency (data/bible.json). Make the hex shifts in bible.json, for example move Blood toward magenta or a deeper crimson and Aether toward a warmer or cooler white, then re-check with the same CVD simulation. Alternatively add a per-element shape or pattern cue on enemy auras and bullets. Colour values are content and change v
  • reported [high] · bug/short · net-product: Online pause and session-end UX: host pause freezes the guest with no explanation, a guest pause does nothing, and a dropped session is a dev-only text banner (main.gd:4193). Design call needed: define online pause semantics (a guest ‘request pause’ command, or a guest pause menu that overlays without gating input plus a visible ‘Host paused / waiting for X’ overlay driven by a PING/HOLD-state flag). Add a proper ‘Connection lost / host left’ modal with Return to menu an
  • reported [high] · improvement/medium · net-product: No reconnect grace: any 5 s silence permanently kills the guest’s pilot or ends the whole session, and there is no way to rejoin (net/lockstep_session.gd:36). Short term: raise and tune the timeout for online sessions (a tunable), and add a ‘reconnecting…’ state on both sides before declaring a drop. Medium: reserve the pilot slot and let a client rejoin with a session token. The host can replay rows from a retained log, or send a keyframe (snapshot_str

The three conflict rows were caused by the loop’s own setup: the godot --import that seeded the new fix worktree rewrote audio/bus_layout.tres, so every fix agent correctly stopped at its clean-tree check. Fixed, and those three plus cycle 3’s two collided low-risk fixes are re-queued into cycle 5.

Refuted: Master HardLimiter has +2 dB pre-gain while the music peaks above 0 dBFS, so the limiter is engaged by music alone; Interceptor, Blight and Stasis Web are finished but default-off, so players never meet them; Light dimension’s weapon pool collapses to one mechanic.

Lenses: sound-music, sound-feel, net-determinism, telemetry, weapon-logic, enemy-ai, bosses. Verified 14, 14 survived both skeptics, 0 refuted, 12 carried to the next cycle.

  • FIXED (7dbc4e22) · bug/short · coop: Powerup pickups are P1-only and can revive a downed P1 (sim/sim.gd:3212). Mirror _collect_gems: loop over pilots with hp > 0, apply the effect to the collecting pilot (heal, slow/freeze are global timers, nuke is party-wide). Add a guard for hp > 0. Add a test that drives a downed P1 over a heal drop and asserts hp stays 0. The change is a no-op while POWERUPS_ENABLED is
  • FIXED (1c39429e) · improvement/short · node-lifecycle: Run-sweep keep-list is duplicated in two places and can drift (main.gd:1140). Move the keep-list into a single helper _sweep_run_children() that both _new_run and _return_to_menu call. Keep the existing order-of-operations. Verify with the existing run-restart tests.
  • FIXED (d1a9b99b) · improvement/short · tooling-dx: Sound Map and Story Board JS/Python test suites are not run by the merge gate (scripts/gate.sh:48). Add a gate step for node –test tools/sound_map/tests/.test.js tools/story_board/tests/.test.js plus python3 -m unittest for the two server.test.py files. Investigate the 120 s runtime first; if a suite hangs (e.g. an open server handle), fix that or report instead of slowing the gate.
  • FIXED (a7417ec5) · improvement/short · tooling-dx: gate.sh hardcodes a single-machine absolute path and copies a file it never uses (scripts/gate.sh:24). Confirm nothing in the gate (or scripts it calls) needs export_presets.cfg, then delete the line or derive the main checkout from git rev-parse –git-common-dir.
  • FIXED (46b5071c) · improvement/short · tooling-dx: Godot gotchas doc cites a retired test file as the example to grep (docs/architecture/godot-gotchas.md:38). Rewrite the example to name a live test that exercises AreaDefs.other(), or mark it historical with the 2026-07-02 date. Documentation only.
  • FIXED (34275f64) · bug/short · telemetry: Dash-encounter player_dashed flag fires on enemy-imposed cooldown bumps, not only the pilot’s dash (net/dash_encounter_telemetry.gd:58). Detect the pilot’s own dash by its rising edge on pilot.dash_timer, which is assigned DASH_TIME only in sim/sim.gd:2612 (grep confirms no other writer of pilot.dash_timer). Keep the previous tick’s dash_timer in the encounter dict and set player_dashed when it jumps up. Render-side telemetry only, s
  • FIXED (f955d1f4) · improvement/short · telemetry: Queue cap drops are counted but never reported, so offline data loss is silent (net/event_queue_core.gd:166). Add a dropped count to the next launch event (or the prev_exit payload, which already rides the same launch flow), then reset it to zero once that event is queued. This adds a field to an existing event type and creates no new kind of data, so the privacy page does not change. Cover it with a test
  • FIXED (13264b10) · bug/short · weapon-logic: Prism Lance text says the lance splits on impact, but the code splits at launch (sim/weapon_railgun.gd:200). Short term: change the card and the code comment to describe what happens (‘fires 2 thinner lances, 0.35 rad apart, from the ship’). That is a copy-only, low-risk fix that makes the text true. If the impact-split is the intended design, implementing it is a new mechanic (high risk): it needs a post-
  • FIXED (71266221) · bug/short · bosses: Boss2 charge is unclamped, so the dash can leave the arena and the stray leash then teleports the boss (sim/boss2.gd:160). Apply clamp_arena to the boss position in active_update’s CHARGE branch, the same way update_lunge does. Keep the charge’s hit check on the unclamped path so only the boss’s final position changes.
  • reported [medium] · improvement/short · sound-music: Story mode (and any non-survival/tutorial mode) plays no music at all (main.gd:829). Short term, treat “story” like survival in _on_mode_chosen and in the boss-music block (use a shared _mode_has_music(), or reuse is_story()), so it gets the loop plus boss_theme. Longer term, give story its own region-keyed tracks. The StoryDirector already emits region events (main.gd:1914 region
  • reported [medium] · improvement/short · sound-feel: Crit upgrade has no audio or visual feedback at all (sim/player_crit.gd:9). Have PlayerCrit.roll (or a thin wrapper) mark the hit as critical in a render-only channel, e.g. a ‘crit’ flag on the dmgnum event or a separate audio-only ‘crit’ fx event. This must not touch sim state or checksums, and player_rng draw order must stay the same. In AudioManager, play a short bright
  • reported [medium] · opportunity/medium · sound-feel: Non-lethal enemy hits are silent: enemy_hit.wav ships but is never loaded (audio/audio_manager.gd:167). Load enemy_hit and play it from a render-side hit event. Gate it to heavy enemies and bosses, or to dmgnum ‘big’ hits. Give it its own cooldown entry (about 4 to 6 ticks), -9 dB or lower, with pitch jitter, and keep it off the reserved boss voices. Prototype it behind a DevFlag and A/B it live throu
  • reported [medium] · bug/short · net-determinism: Dash and decoy edge presses ride only 3 unreliable packets and are never acked or retransmitted (net/lockstep_session.gd:38). Send edge presses as a reliable message (a small MSG_INPUT_EDGE on the reliable channel), or keep an edge in the redundancy window until the host’s rows show it applied. The host’s rows carry the quantized input, so the client can confirm from the row log. Add a lockstep test that drops 3 consecutiv
  • reported [medium] · opportunity/medium · telemetry: Runs ended by quit-to-menu or dev restart send no run summary, so abandoned-run build data is never captured (main.gd:808). In _send_run_end, when the run was not banked, include the GameplayTelemetry.run_body summary (or a compact subset: build_picks, weapons_list, kills_by_type, dmg_dealt, damage_taken, peak_enemies, timeline) inside the run_end data, with a banked:false flag. Keep it on the run_end event instead of in

Lenses: sound-tech, sound-roadmap, net-transport, game-feel, onboarding-retention, ui-layout, input-modals. Verified 14, 12 survived both skeptics, 2 refuted, 7 carried to the next cycle.

  • FIXED (95c4cb1d) · bug/short · sound-tech: Orbit hum never plays in LAN co-op runs (main.gd:1740). Extract the orbit-hitting scan (main.gd:1733-1740) into a helper that takes the sim and a frozen flag, and call it from _post_tick or the end of _net_physics as well. Use the same frozen rule (hold/menu/levelup/gameover => false) so the hum stops under overlays. It is render-side only and does not t
  • FIXED (01931e07) · improvement/short · net-transport: The loopback test harness models LAN, not WAN: latency is counted in polls, reliable packets never drop or retransmit, and the top latency used is 2 to 3 polls (net/loopback_transport.gd:1). Add a WAN profile to LoopbackTransport that works in ms of simulated time. It should model a Gilbert-Elliott burst loss on reliable packets, so a lost reliable packet delays itself and everything behind it by about 1.5 RTT. It should also support RTT of 100-250 ms, and asymmetric and spiky jitter. A
  • FIXED (4d237737) · bug/short · game-feel: Screen shake, hurt vignette, low-HP border pulse and death dissolve decay per render frame, not per second (main.gd:2579). Pass the real frame delta, clamped the same way as warden_fx (minf(delta, 0.1)), to screen_fx.advance and death_dissolve.advance in _process. Make _pulse time-based (accumulate delta inside advance() and read it in take_shake_offset) so the jitter frequency is also independent of frame rate. Identic
  • FIXED (b234bb2a) · improvement/short · onboarding-retention: Locked hull shows ‘Unlock in the SHOP’ with no price or progress (ui/ship_config_panel.gd:263). Append the price to the lock line using the existing MetaState cost for ‘unlock-hull-’ (for example ‘Unlock in the SHOP: 800 gold, you have N’). The value is already available from meta.cost(def) and banked_gold, so no new state is needed. Use the same wording pattern as the shop’s SAVE UP text
  • FIXED (548cc06e) · bug/short · ui-layout: Drone dock ignores the safe-area inset that the weapon dock above it uses (ui/drone_dock.gd:17). Mirror weapon_panel.gd:53-56: compute var safe := Platform.safe_insets_units(get_viewport()) in DroneDock._ready() and set _root.position = DOCK_ORIGIN + Vector2(safe.x, safe.y). Better still, derive the drone origin from WeaponPanel’s origin plus the 80 px row gap so the two docks cannot drift apar
  • FIXED (98a4c989) · bug/short · ui-layout: Level-up STATS button is sized and placed in canvas units, ignoring points and the safe area (ui/level_up_panel.gd:204). Build the rect from points, following the touch_controls.gd pattern: upt = Platform.units_per_pt(get_viewport()); ins = Platform.safe_insets_units(get_viewport()); size the button at max(44 pt, its label width) * upt tall; anchor it bottom-right and offset it by ins.z and ins.w plus a small margin.
  • FIXED (822509e8) · bug/short · input-modals: C key cannot close the codex: the toggle-close branch is unreachable (main.gd). Let C toggle-close when the codex is open: handle KEY_C in main._input before the Stage 2 guard, or have codex_overlay._input consume KEY_C in BROWSE mode. Then add a headless test that opens the codex with C, presses C again and asserts it closed. Keep the tutorial hotkey block in mind (tutorial_ho
  • FIXED (8fad782b) · improvement/short · input-modals: Drone-button hold is silently dropped when a menu opens mid-press (ui/touch_controls.gd). In release_all(), keep the existing finger-release behaviour but decide what to do with a pending DRONE press explicitly: either cancel it with a short haptic/visual so the player knows it did not fire, or record a ‘held across menu’ flag like _held_joy_idx so the next release after reopen is ignore
  • reported [medium] · bug/short · net-transport: Client input stamping uses a stale row tick, so input_delay is dead above about 50 ms RTT and client inputs apply at arrival time (net/lockstep_session.gd:374). Stamp against an estimate of the host tick: est_host_tick = _max_row_tick + 1 + ceil(rtt_ms / 2 / 16.7), then add a small safety margin. Alternatively, drop the ‘future target’ model and have the host apply each input at arrival, with a one-tick smoothing queue. Raise INPUT_REDUNDANCY with measured
  • reported [medium] · improvement/short · onboarding-retention: Shop tip never recommends the Ships category, so the 800-gold hull is never advised (ui/shop_advice.gd:52). Add an optional hull row as the last slot in recommend(), chosen only after survival, weapon and drone rows are maxed or unaffordable, and surface it with a SAVE UP label when unaffordable. Because tools/economy_sim/shop_tip_policy.gd calls ShopAdvice.recommend (line 24), decide the placement with a
  • reported [medium] · opportunity/short · onboarding-retention: Results screen gives no next-goal line after a run (ui/results_panel.gd:270). Pass the first ShopAdvice row (name, cost, banked gold) into show_results from main.gd, which already passes banked_gold and shop_affordable. Render one line such as ‘NEXT: Bulwark 3/5, 120 more gold’. Hide the line when the list is empty or everything is maxed. No sim state changes.
  • reported [high] · opportunity/medium · sound-tech: No spatial audio: all SFX are mono assets on non-positional players, so nothing pans (audio/audio_manager.gd:197). Prototype with a pan parameter on play(): derive pan from the event pos and the camera rect, clamp to about +/-0.6, and only apply it to ENEMY_FX_KINDS, boss/priority kinds and death. Options: AudioStreamPlayer2D with a camera-following listener, or N pre-made buses each with an AudioEffectPanner at

Refuted: Damage-taken feedback disappears on the adaptive low tiers, and the player ship has no hurt cue of its own; orbit_ambience loop dips ~12 dB for ~0.4 s at its seam every 10 s.

Lenses: sound-coverage, sound-mix, net-latency, perf, data-consistency, save-meta, coop. Verified 14, 12 survived both skeptics, 2 refuted, 2 carried to the next cycle.

  • FIXED (8b10a3e6) · improvement/short · perf: Wave refill rescans the whole enemy pool once per spawned enemy, all in one tick (sim/sim.gd:2293). Read total_biomass() and the per-type counts once before the loop. Then add each spawned enemy’s biomass and type to running totals as the loop goes, or have _spawn_one return the biomass it wrote. Totals are maintained by the same spawn path, so values are unchanged and the draw order is untouched.
  • FIXED (c9517057) · bug/short · data-consistency: Bestiary stats blocks are dead and drift silently; Sentinel block is wrong on 5 fields (data/bestiary.json:67). Delete the stats blocks from the warden and sentinel entries (or keep them only for keys absent from the bible), then rewrite test_boss_stats_from_block to assert the warden codex hp equals content.enemy(‘warden’).hp. Optionally add a loader validation that errors when a stats block disagrees with t
  • reported [low] · bug/short · save-meta: Save is written in place, so an interrupted write leaves an empty or truncated meta.json (meta/meta_store.gd:254). Write to a sibling temp file (e.g. meta.json.tmp), close it, then DirAccess.rename_absolute over meta.json (or copy the previous good file to a .bak before writing). Keep the existing PATH-then-cache fallback. Have load_from try the .bak when the primary is empty or unparseable.
  • reported [low] · bug/short · save-meta: A newer cache-fallback save is shadowed by a stale user:// save on the next boot (meta/meta_store.gd:105). Add a monotonic save counter or saved_at field to to_dict (additive, schema stays 1) and have load_from read every existing copy and keep the highest. Alternatively mirror every successful write to both locations so they cannot diverge. Either fix is additive and does not change any balance value.
  • reported [medium] · bug/short · sound-coverage: Dimension boss kill stacks four identical full-volume special_barrage plays in one tick (sim/sim.gd:2550). Give the crystal ring events a name (e.g. ‘CRYSTAL’) or a flag. Either map it to nothing, or play one soft reward sound once. Alternatively add a SFX_COOLDOWN_TICKS entry for special_barrage so coincident reactions collapse to one voice. A cooldown on special_barrage also helps the other unnamed rea
  • reported [medium] · improvement/medium · sound-coverage: Tutorial and story progression beats are silent, and the weapon pickup plays the generic barrage (main.gd:1883). In main.gd, next to dialogue_box.consume, scan sim.story.events and play a short confirm cue on lesson ‘step’/‘done’, a softer one on ‘region’/‘secret’. Reuse existing assets (for example the unused wave_clear.wav, special_aegis or special_rally, which are shipped but never loaded) via a small helpe
  • reported [medium] · bug/short · sound-mix: Hostile beams (Lancer, Eye, Boss2 charge) share the player’s Beam sound path: attenuated by the player’s build load, ungated off-screen (audio/audio_manager.gd:127). Emit a distinct fx kind (or a hostile: true flag) for enemy beams in enemy_attacks.gd, eye.gd and boss2.gd. Route it to beam_hum (or a quieter variant) outside WEAPON_FIRE_KINDS, give it its own cooldown key, and gate non-boss ones with the ENEMY_FX_KINDS on-screen check and ENEMY_FX_ATTEN_DB. Kee
  • reported [medium] · improvement/short · sound-mix: Weapon-load attenuation counts silent and passive weapons, so the audible weapons get quieter for nothing (audio/audio_manager.gd:415). Count only weapons that currently map to a WEAPON_FIRE_KINDS sound (a small set of wids, e.g. blaster/pulse/nova/blade/beam/turret/scatter) in main.gd, or expose an audible flag per wid in PilotArsenal. Alternatively derive the load from the number of weapon-fire plays actually made in the last se
  • reported [medium] · improvement/short · data-consistency: Codex Warden description states a fixed 40% enrage threshold that is a live tunable (data/bestiary.json:65). Stop hand-typing the number in bestiary.json. Have BestiaryLoader substitute a token such as {enrage_pct} from the live BossWarden.BOSS_ENRAGE_FRAC (or the HardTuning-aware value) when building the desc, the same way ui/meta_copy.gd fills {pct} tokens. Touches a tunable’s displayed value, so Chris s
  • reported [medium] · bug/short · save-meta: Mid-run shop stat and weapon unlock purchases silently do nothing until the next run (ui/meta_shop_panel.gd:1223). Short term (copy-only, low risk): when the shop opens with a live run, show a note such as ‘Takes effect from your next run’ on stat and weapon cards and in the post-buy toast. Longer term: re-derive stat effects mid-run from a stored baseline. Do NOT just re-call MetaState.apply_to on the live play
  • reported [medium] · improvement/short · coop: Low-HP screen vignette, border pulse and hit flash read P1 only; a hurt or near-death P2 gives the shared screen no feedback (main.gd:2571). Feed screen_fx the lowest HP fraction among live pilots (min over sim.pilots where hp>0, falling back to 0 if none) instead of sim.player alone. Optionally call flash_damage(hard_hit) for any pilot’s discrete hit, keeping the punch P1-only. Both changes are local to main.gd’s per-frame block, follow
  • reported [high] · opportunity/medium · net-latency: Client’s own ship responds about one full RTT plus the jitter buffer late (about 11 ticks, 48px at 150ms); the spec’s render-only prediction is not built (net/lockstep_session.gd:372). Build the spec’s render-only own-ship prediction. Keep a scratch PlayerState copy for the local pilot. Each physics step, advance it with the local input for (RTT/2 + cushion) ticks beyond the last applied row. Draw the ship there, with a short blend back to the authoritative pos when they differ. N

Refuted: Enemy hit-flash tracker rebuilds a Dictionary and allocates a Vector2 per enemy every render frame; Reliable-ordered rows plus a fixed 2-row jitter buffer will freeze the client on every lost packet at 150ms RTT.

Lenses: sound-music, sound-feel, net-product, node-lifecycle, accessibility, content-depth, test-masking. Verified 14, 12 survived both skeptics, 2 refuted, 3 carried to the next cycle.

  • FIXED (ba45039d) · bug/short · sound-music: Redeploy / Climb again after a run leaves the next run with no music (main.gd:1941). Have the results.redeploy_requested / restart_requested handlers call a small wrapper, for example _redeploy(), that calls audio.play_music(“survival_loop”) when _mode is survival or tutorial and then calls _new_run(). Alternatively, centralise this in _new_run with a mode-to-track lookup so every e
  • FIXED (ca60e65e) · improvement/short · node-lifecycle: Feedback send continuation resumes on a freed panel (debug-build error) (ui/feedback_panel.gd:267). Replace the await with a timer whose timeout is connected to a bound method (close is re-entrant-safe because it checks is_inside_tree), or create the timer and guard with is_instance_valid(self) on resume, so the continuation never runs on a freed instance. Keep close() as the only place that emits
  • reported [medium] · bug/short · sound-feel: Drone expiry and destruction play the enemy-kill pop (sim/drone_director.gd:430). Add a discriminator to the drone event, for example “drone”: true or a distinct kind such as “drone_end”, keeping “death” visuals working. In _route, skip the death sound for that event, or give it a quiet dedicated cue later. Check FxManager, gpu_burst and death_dissolve first, since they also cons
  • reported [medium] · improvement/short · sound-feel: Boss cues and the player’s warp are 10-20 LU quieter than ordinary weapon SFX (audio/audio_manager.gd:60). Do a loudness-normalisation pass: pick a target for priority/boss cues, for example around -18 LUFS, and apply per-key trim in a small table (e.g. SFX_TRIM_DB, applied in play()) before touching assets. Tune by ear with Chris, then re-baseline sound_map.json. Re-normalising the assets themselves wou
  • reported [medium] · improvement/short · sound-feel: Enemy kill pops are not camera-gated and share one cooldown with on-screen kills (audio/audio_manager.gd:324). Apply the same visible_rect test to death events, keeping a small margin and perhaps a quiet floor for off-screen kills instead of a hard drop. Optionally sort on-screen events ahead of off-screen ones before the cap. Mix levels are involved, so confirm with Chris.
  • reported [medium] · bug/short · net-product: Host has a single ENet slot and no pre-handshake timeout, so one idle or wrong-build connection locks the lobby (net/lockstep_session.gd:196). Track connected-but-unregistered peers in _host_service with a short handshake deadline (about 3 s since connect). After sending REJECT, flush and disconnect that peer. Let the lobby show ‘a guest tried to join with BUILD X’ or ‘rejected: reason’ on the host side (main.gd _lan_pump only drives host
  • reported [medium] · improvement/short · net-product: Net sessions have no telemetry, and a remote desync snapshot is written only to a file on the player’s device (main.gd:4315). Push one ‘net_session’ event through EventQueue at session end. Fields: role, build, end_reason, ticks, stalls, checksums_compared, desync_tick, and RTT stats once they exist. Upload a truncated desync snapshot (a few KB) as an event or via the existing feedback path. Add a net flag to run_start and
  • reported [medium] · improvement/short · accessibility: No player-facing motion or flash reduction; the only off switch is dev-only (fx/screen_feedback.gd:6). Add a player-facing ‘Reduce motion’ setting (Settings or pause menu, persisted in the meta save or a user:// config) with a multiplier that scales SHAKE_KICK, VIGNETTE_PEAK and the screen_punch strength, and a full-off option for the punch. Apply it inside ScreenFeedback.flash_damage and ScreenPunch
  • reported [medium] · improvement/short · accessibility: HUD and weapon-panel text is below the team’s own 23pt TV floor, with no text-size setting (ui/weapon_panel.gd:95). Audit the font.2xs and font.xs call sites that carry information the player needs during play. Raise the important ones to font.sm or font.base, which is a token-only change in ui/tokens/defaults.json and the callers. Also add a ‘Text size’ player setting that multiplies the font tokens, which is th
  • reported [medium] · bug/short · test-masking: Elite Swarmer brood drops total biomass rounded to a multiple of 2, not the elite’s real worth (sim/sim.gd:3114). Distribute the exact total instead of rounding the count: n = max(total_biomass / unit_bm, 1) with the remainder (total_biomass % unit_bm) added to the first child(ren) so the sum equals total_biomass exactly (or cap children at ceil and give the last child the remainder). This changes only the biom
  • reported [high] · opportunity/long · sound-music: Single boss track for five different bosses and no per-dimension or per-story music (main.gd:1867). Short step: refactor play_music into a data-driven table (context key to track key) so new tracks are a data change, and add a second AudioStreamPlayer to give a crossfade of about 1 s. Medium step: a distinct track for Story, and a boss variant per boss family. Long step: adaptive stems driven by s
  • reported [high] · improvement/short · net-product: Privacy page and ASC privacy label say nothing about networked play, host IP exposure or the room-code registry (/Users/chris/Claude/bullet-heaven-site/privacy/index.html:90). Before opening internet play beyond Chris and Toby, add a Multiplayer section to the privacy page. It should say that peers see each other’s IP, that a relay (if built) forwards gameplay data without storing it, and that the room registry keeps only a hashed network key for 60 s. Update compliance.m

Refuted: Touch play needs two thumbs; there is no single-stick or one-handed mode; Room codes resolve only to private LAN IPs, and join error text assumes Wi-Fi, so the invite flow cannot carry over to the internet.

Lenses: sound-tech, sound-roadmap, net-determinism, tooling-dx, telemetry, weapon-logic, enemy-ai. Verified 14, 11 survived both skeptics, 3 refuted, 5 carried to the next cycle.

  • FIXED (f1c2bab8) · improvement/short · tooling-dx: Gate guard tolerates any number of pending/risky tests, so skipped coverage is invisible (scripts/check-test-count.sh:61). Store an expected Risky/Pending count (or a list of allowed pending test names) next to expected in check-test-count.sh and fail when the actual count exceeds it. Print the pending test names from the GUT log in the same way failing_names() does. Where a test skips for an environment reason, prefer
  • FIXED (9ef4af0d) · opportunity/short · telemetry: Run and sample rows cannot be split by Dimension or area (net/gameplay_telemetry.gd). Put visited_dimensions and the death area into the RunTimeline.to_dict() output (net/run_timeline.gd:55). That lands in runs.timeline_json, which needs no schema migration. Adding a key to run_body alone is a silent no-op, because worker.js runRow() (lines 104-125) maps only named columns and drops
  • FIXED (d7965b76) · opportunity/short · telemetry: Per-source damage taken is computed per run but never shipped (sim/sim.gd). Add damage_by_source and max_hit_by_source to RunTimeline.to_dict() (net/run_timeline.gd:55), rounded to ints. Keep the top N sources, for example 12, so timeline_json stays well under the Worker’s 60000-character cap (see the next finding). Both dictionaries are render-side and not in the checksum,
  • FIXED (5e3ead3a) · bug/short · weapon-logic: Dock damage readout ignores per-weapon power multipliers (sim/upgrade_system.gd:762). Add a helper that returns base_damage * damage_mult * dm (and orbit’s *3 DPS term) for each weapon, then use it in every branch. Check the scythe pct branch separately, since it already reflects ‘power’ through pct. Add a test that applies ‘power’ and asserts the dock number changes.
  • FIXED (55f87c42) · bug/short · weapon-logic: Supernova evolution card says ‘fires far faster’ but the code makes it fire slower and deal x7 damage (sim/weapon_evolutions.gd:18). Rewrite the nova desc to match the code, for example ‘huge radius · fires far less often · ×7 damage · leaves a burning crater’. This is copy only, so no balance number changes. Check the tests that pin evolved_desc text (test_upgrade_copy).
  • FIXED (70ae50e8) · bug/short · weapon-logic: Stopwatch evolution promises a ‘full freeze’ the target does not get (sim/weapon_chronowell.gd:156). Short term: change the desc to what the code does, for example ‘bonus 1s single-target slow on an elite or boss; freezes enemy shots in its ring’. Real fix (medium, touches shared enemy movement and balance): thread the zone’s slow_mult into enemy movement and attack-rate scaling for that one target
  • reported [medium] · bug/short · sound-tech: LAN client catch-up frames drop the first tick’s SFX (and damage numbers) (main.gd:1873). For the net path, invoke the per-tick audio and damage-number consumption from a per-tick hook (session.on_tick_us / _note_sim_tick already exists) or loop it once per applied row, instead of once per frame in _post_tick. Alternatively, drain a per-frame accumulated list with the _tick_counter advan
  • reported [medium] · improvement/short · net-determinism: Cross-device determinism has never been measured, and the probe cannot see the code that will diverge first (net/determinism_probe.gd:19). Before the playtest: build one BUILD, run det_probe on every device pair that will play, and compare digests. Extend the probe to about 15000 ticks, or add a second ‘boss’ phase using debug spawn of Warden and Boss2, with non-invulnerable pilots, then run it again. For the handshake: put the device’
  • reported [medium] · bug/short · weapon-logic: Scar Tissue threshold rung does nothing: round(3 x 0.85) = 3 (sim/weapon_scar_tissue.gd:116). Make the mod match its label: discharge_threshold = maxi(discharge_threshold - 1, 1), and use the same expression in mod_now_after. Alternatively keep the multiplier and switch to floor, then update the label. Either way the discharge threshold changes from 3 to 2 at this rung, which is a balance ch
  • reported [medium] · bug/short · enemy-ai: Accumulator spawn-time stat multipliers are overwritten every tick (sim/enemy_behaviors.gd:394). Capture the spawn-time base values per enemy instead of reading the global bible cache. Either add per-enemy base_speed/base_contact/base_radius columns to EnemyPool (set in _spawn_one, swap-removed in remove_at), or store the spawn-time values and multiply by growth scale. Then keep the growth scal
  • reported [high] · opportunity/long · sound-roadmap: No per-element sound palette: element-bearing fx events all play one element-blind sound (audio/audio_manager.gd:13). Add a palette layer: ELEMENT_SOUND_VARIANT[kind][element_idx] resolves to a sound key, falling back to today’s kind-only key. Per element, ship a short tonal ‘tint’ layer, or a pitch/filter profile, mixed over the base cast. Layers are cheaper than N x weapons full assets and can be generated with

Refuted: ENet lockstep and transport tests depend on wall-clock deadlines that the gate’s own concurrency can starve; Timeline truncates silently in two places, and the Worker cut produces invalid JSON; Skirmisher waves fire and strafe in lockstep.

  • 9 cycles ran (23:16 to 03:04), not the 24 the cadence allowed: a cycle took 6 to 40 minutes, so firings that arrived mid-cycle waited instead of stacking. About 29M subagent tokens in total (roughly 3.2M per cycle).
  • 36 fixes landed on main, each gated by the full GUT suite (listed under “Already landed overnight” below). 1 more fix (unreadable-save guard) is parked on branch overnight/meta-unreadable-save-guard (ea1a0f4d, GUT green) pending a design call.
  • ~60 verified findings reported for a human (medium/high risk, balance, mix levels, new assets, design), plus ~60 unverified leads that hit the per-cycle verification cap (kept in the plan as “(lead)”).
  • Recurring, never fixed by the loop (needs a deliberate pass): audio mix and loudness (raised in 6 of 9 cycles), silent events and unused sound assets (5 cycles), save-file safety (3 cycles), internet-play transport and session robustness (every cycle).
  • Process incidents: (1) cycle 3, a concurrent session swept a fix agent’s uncommitted edits into its own commit, and the agent reported that hash as its fix; (2) cycle 4, the godot --import that seeded the new fix worktree dirtied audio/bus_layout.tres and blocked every fix. Both fixed mid-run: fix agents now work in a dedicated worktree, reset Godot’s rewritten files, and report only a hash they made themselves.

Source: 165 findings over 9 cycles (36 fixed, 63 reported, 59 unverified). “Reported” items survived two skeptics. Items marked (lead) were found but never verified; treat them as leads, not facts.

  1. Mix levels: approve a per-key gain trim table in AudioManager (stings -4 to -6 dB, boss cues and warp raised 10-20 LU, Music bus about -6 dB, death variants levelled to one LUFS). Recommend: yes, trims in code, not re-exported wavs, tuned live by ear. Same call covers camera-gating off-screen kill pops and a special_barrage cooldown.
  2. Gameplay music: commission a separate survival track (today survival_loop is the menu theme). Recommend: yes, one gameplay track plus a story track before any adaptive work or per-boss tracks.
  3. Release volume settings: add Music/SFX sliders (or toggles) to the pause menu. Recommend: yes, it is the most-asked-for missing option in the genre.
  4. Quit-to-menu banks run gold (_bank_run('quit_menu')). Recommend: yes; today quitting forfeits everything.
  5. Save safety, one decision for three findings: (a) meta.json is written in place, so an interrupted write leaves it empty or truncated; (b) a newer cache-fallback save is shadowed by a stale user:// save on the next boot; (c) the parked branch overnight/meta-unreadable-save-guard refuses every save while meta.json is unreadable. Recommend: temp-file-then-rename plus a .bak that load_from falls back to, a monotonic saved_at so the newest copy wins, keep the guard’s one-time .unreadable-<schema> backup and drop its blanket refusal (a downgraded build could never save again).
  6. Tutorial on cold launch: latch tutorial_done on first banked non-tutorial run vs persist lesson index with Resume/Skip. Recommend: the latch now, resume later.
  7. Online pause semantics: any player can pause everyone (spec call 5) with a visible “X paused” overlay. Recommend: yes.
  8. Relay tech: Cloudflare DO WebSocket relay (spec call 2, TCP, accepts head-of-line stalls) vs a UDP relay on the Hetzner VPS. Recommend: DO relay as planned, but measure WAN stalls with the new loopback WAN profile and a real ENet-over-internet test first.
  9. Locked-weapon teaser card in level-up offers: needs a determinism re-pin. Recommend: defer until after internet play’s first milestone.
  10. Fire vs Blood colours under colour-vision deficiency: recommend shifting Blood toward magenta in bible.json.
  11. Coil knockback: make Coil explicitly kinetic-immune (current behaviour, documented) vs let knockback shove the ring. Recommend: immune, it is the smaller change.
  12. Scar Tissue threshold rung does nothing (round(3 x 0.85) = 3). Recommend: maxi(threshold - 1, 1) so the label is true; it is a balance change (3 to 2), check determinism.
  13. Mid-run shop purchases of stats and weapon unlocks do nothing until the next run. Recommend: a “Takes effect from your next run” note now; mid-run re-derive later (never re-call apply_to on the live player, effects are cumulative).
  14. Privacy check on two shipped telemetry fields: 9ef4af0d (visited_dimensions + end_area) and d7965b76 (damage_by_source + max_hit_by_source), both in the run timeline. The fix agents judged these the same kind of data, so the privacy page and ASC label were not touched. Confirm the privacy page still covers them (repo rule: a new KIND of data updates both).

Phase 1: mix fixes with existing assets (days)

  • Per-key gain trim table in AudioManager, the tool for everything below. Stings and level-up are 15+ LU louder than gameplay SFX. ~2 h.
  • Trim stings/level_up/victory/wave_start by 4-6 dB; quieter ui_buy. Same finding. Chris’s ear.
  • Raise boss cues and the warp to the gameplay level via the same table. Boss cues and the player’s warp are 10-20 LU quieter than ordinary weapon SFX. 1 h + listen.
  • Music bus trim (about -6 dB) plus a 0.4 s duck of 4-6 dB on priority cues, both as tunables. Music is louder than almost every SFX, with no bus trim or ducking. ~2 h.
  • Level the 8 enemy-death variants to one LUFS via the trim table. Enemy-death variants differ by up to 12 LU / 19 dB within the same pool. ~1 h.
  • Raise funzo_confetti and boss_swing_impact. funzo_confetti (and boss_swing_impact) are effectively inaudible assets. 30 min.
  • Raise king_hit (player hurt). Low-hull warning exists only as haptics; the player-hurt sound is very quiet. 30 min.
  • Gate unnamed enemy reactions like ENEMY_FX_KINDS (visible rect, attenuation, cooldown on special_barrage). Unnamed ‘reaction’ fx events … play the loud special_barrage at full volume, ungated and unthrottled. ~2 h.
  • Name the crystal ring events so a Dimension boss kill plays one soft cue. Dimension boss kill stacks four identical full-volume special_barrage plays in one tick. 1 h, rides on the cooldown above.
  • Camera-gate kill pops with a quiet floor for off-screen kills, on-screen sorted first. Enemy kill pops are not camera-gated and share one cooldown with on-screen kills. ~1 h.
  • Drone expiry/destruction gets a drone flag and skips the kill pop (check FxManager, gpu_burst, death_dissolve). Drone expiry and destruction play the enemy-kill pop. ~1 h.
  • Hostile beams (Lancer, Eye, Boss2 charge) get their own fx kind/flag, cooldown and on-screen gate; boss ones stay audible. Hostile beams share the player’s Beam sound path. ~2 h.
  • Weapon-load attenuation counts only audible weapons. Weapon-load attenuation counts silent and passive weapons. ~1 h.
  • Set loop_offset in both MP3 .import files (about 0.05/0.08 s); do not re-encode (the 2026-09-29 audio audit says don’t convert music). Both looping MP3s have 50-80 ms of leading silence. 30 min + listen.
  • Crossfade music swaps on a second Music player, resume the loop from its saved position after the boss; make play_music a context-to-track table. Music changes are hard cuts that restart from 0. ~3 h.
  • Music/SFX volume controls in the pause menu, persisted like the Haptics toggle. No player-facing music/SFX volume or mute setting in release builds. ~half day incl. tvOS focus check.

Phase 2: coverage with existing assets (days)

  • Map silent weapon events to existing cues (railgun, Chalice, Thunder Wake, Wildfire, Stasis Web). Railgun, Chalice, Thunder Wake, Wildfire and Stasis Web fire fx events but have no sound. ~3 h.
  • Wire the three unused assets: special_rally/special_aegis to OVERCHARGE/BOMB/HEAL, wave_clear to a survival wave or story beat, or delete it. Three finished sound assets … are never loaded or played. ~1 h.
  • Tutorial step/done and story region/secret cues from sim.story.events; a distinct weapon-pickup cue. Tutorial and story progression beats are silent, and the weapon pickup plays the generic barrage. ~2 h.
  • Named HEAL/SLOW reactions for powerup pickups. Powerup pickups for Slow and Heal are silent or reuse the quiet gem tick. ~1 h.
  • weapon_evolve fx event + stopgap sting. Weapon evolution and the weapon-threshold rungs have no audio or fx event. ~2 h.
  • Portal cue in _begin_warp() and on area change. Wormhole and dimension warps are silent. ~1 h.
  • Story mode gets the loop plus boss theme via a shared _mode_has_music(). Story mode (and any non-survival/tutorial mode) plays no music at all. ~1 h.
  • Load enemy_hit.wav for heavy/boss/big hits only, behind a DevFlag for A/B. Non-lethal enemy hits are silent. ~2 h.
  • Render-only crit flag on the dmgnum event + a short bright cue (player_rng order unchanged). Crit upgrade has no audio or visual feedback at all. ~2 h.
  • Audio low-hull heartbeat driven from main.gd’s hp fraction (lowest live pilot in co-op). Low-hull warning exists only as haptics. ~2 h.
  • Distinct existing cue for shop buy; quieter pitch-varied pickup. Shop purchase reuses the level-up sting; gem pickups reuse the menu click. ~1 h.
  • LAN client: consume SFX and damage numbers per applied tick, not per frame. LAN client catch-up frames drop the first tick’s SFX (and damage numbers). ~2 h + test.

Phase 3: new assets (weeks)

  • Dedicated gameplay track (survival bed + higher-intensity variant), separate from the menu. survival_loop and menu_theme are the same file. Composition + 1 day wiring.
  • Story/region tracks keyed off StoryDirector region events, and a boss variant per boss family. Single boss track for five different bosses and no per-dimension or per-story music.
  • New cues: shop buy/deny, dimension confirm, evolution flourish, portal, per-boss arrival stingers (lead: Boss arrival announcements are shared across most bosses).
  • A sound-asset source of truth (generator prompts, LUFS targets, variant sets) next to audio/sound_map.json, plus a loudness check in the gate (leads: no sound-design pipeline source of truth or loudness QA; no loudness target).

Phase 4: long term

  • MusicDirector (render-side, never /sim) driving synchronized stems by intensity, low-HP filter and boss layer, bar-quantized. No adaptive layering: music does not respond to intensity beyond a binary boss swap. Needs Phase 3 stems.
  • State-driven bus DSP (low-HP filter, pause/freeze/dimension) (lead: buses carry only a static limiter).
  • Stereo panning by on-screen position for enemy/boss/death cues (AudioStreamPlayer2D or panner buses), clamped to about +/-0.6. No spatial audio: all SFX are mono assets on non-positional players. ~2-3 days prototype.
  • Per-element sound palette: ELEMENT_SOUND_VARIANT[kind][element] tint layers over the base cast, falling back to today’s key. No per-element sound palette. Per-Dimension themes (lead).
  • Layered weapon audio for stacks/evolutions (lead: No procedural/dynamic SFX layer).

Current state. Main has host-scheduled input lockstep (net/lockstep_session.gd, protocol v2 with command rows and HOLD rows), NetTransport with LoopbackTransport and ENetTransport (rows on a reliable channel, inputs unreliable-sequenced with 3x redundancy), NetCoopRun (Survival only, 2 pilots, no shop/hull/dimension, defaults config), a dev-only LAN lobby with beacon, manual IP and easy-join room codes via the control Worker /mp/* registry (lan-easy-join is merged), and a cross-platform DeterminismProbe. Nothing internet-capable exists: ENet by IP only, no relay, no NAT traversal, and NetTransport still names the Cloudflare room relay as Phase 4. No net-session telemetry exists. Unmerged: lan-jitter (7 commits, 3 review rounds, based on main at 23:35 last night, 47 commits behind now) adds the JITTER_ROWS client cushion, stall/cushion/pause-wait counters and lan_run telemetry. networked-multiplayer-mb-mc is the July prototype, 2,015 commits behind; never merge it, but its Sim.export_full_state/import_full_state and per-class export/import (none of which exist on main) are the basis for reconnect and late join. Fixed overnight: RTT and packet-loss readout in the LAN banner and [lan] report (2f0b0e38), and an opt-in WAN profile for LoopbackTransport with ms-based delay, burst loss and head-of-line blocking on reliable packets (01931e07).

Blocker before any public internet build: the privacy page and ASC privacy label say nothing about networked play, peers seeing the host IP, or the room-code registry. Add a Multiplayer section and update marketing/appstore/compliance.md before anyone beyond Chris and Toby plays online.

First-day tasks, smallest first

  1. Review and merge lan-jitter (merge origin/main in first, gate, bh-merge-reviewed-branch); it is the prerequisite for any WAN pacing.
  2. Host lobby hardening: a ~3 s handshake deadline for connected-but-unregistered peers, disconnect after REJECT, show the rejection reason on the host. Host has a single ENet slot and no pre-handshake timeout, so one idle or wrong-build connection locks the lobby.
  3. Net-session telemetry: one net_session event at session end (role, build, end_reason, ticks, stalls, RTT, desync_tick) plus a truncated desync snapshot upload, so playtests produce data. Net sessions have no telemetry; a remote desync snapshot is only a file on the device. Same privacy check as decision 14.
  4. Add a WAN-profile lockstep test (100-250 ms RTT, burst loss) that reports stalls, so every fix below has a number to move.
  5. Measure cross-device determinism: one BUILD, det_probe on every device pair that will play, extend it past ~15000 ticks with a boss phase. Cross-device determinism has never been measured, and the probe cannot see the code that will diverge first. Must precede any cross-device WAN test.
  6. ✅ DONE (branch net-input-stamp, 2026-10-08) Fix client input stamping: target est_host_tick = _max_row_tick + 1 + ceil(rtt/2/DT) + margin, using the new RTT readout. Shipped as a FULL ceil(rtt/DT) (plus capped row age), not rtt/2: the newest row is one-way old on arrival and the input flies one-way back; the measured lateness (4 at 100 ms, 13.8 at 250 ms with delay 2) matches RTT/DT − 2. Late inputs 100% → 0 at 100/250 ms. Client input stamping uses a stale row tick, so input_delay is dead above about 50 ms RTT. The jitter buffer makes this worse, so it follows task 1.
  7. ✅ DONE (branch net-edge-reliable-impl, merged 2026-10-08) Reliable edge inputs: Shipped as protocol v3 cumulative press counters on every MSG_INPUT, not a reliable MSG_INPUT_EDGE: a lost press lands on the next delivered packet (one frame), entry bits still pick the tick. New 150ms_long_bursts WAN profile: edges 23/24 -> 24/24, lost=0 extra=0. send dash/decoy presses as a reliable MSG_INPUT_EDGE, or keep them in the redundancy window until a row confirms them. Dash and decoy edge presses ride only 3 unreliable packets and are never acked.
  8. ✅ DONE (branch worktree-agent-a2aec299239fdd44f, 2026-10-08) Shipped as protocol v4 MSG_READY + mark_ready() on every peer (main: end of _start_net_run); the host holds on the HOLD path until it and every client are ready, a still-loading client is dropped after READY_TIMEOUT_MS (30 s, local tunable) as drop_reason ready_timeout and the host plays on alone; not-ready peers are exempt from the 5 s timeout on both sides; lan_run gains ready_wait_ms / drop_reason. An 8 s blocked client load (loopback): 0 tick rows, no drop, then 1 tick/frame with 0 stalls; over real localhost ENet a 12 s block survives because the session raises ENet’s peer timeout (default cut 6-10 s) to the join timeout until the barrier opens. A ready client sends only pings until the first tick row. Ready barrier: client READY after run build, host holds rows (existing HOLD path) up to about 30 s. PROTOCOL_VERSION bump. No ready barrier at start.
  9. Reconnect grace, step one: timeout becomes a tunable with a visible “Reconnecting…” state on both sides before a drop. No reconnect grace: any 5 s silence permanently kills the guest’s pilot.
  10. “Host paused / Connection lost” overlays replacing the dev text banner. Online pause and session-end UX. Needs decision 7.
  11. Relay spike: smallest real WAN test first (ENet host on the Hetzner VPS or a port-forwarded home host, iPhone on cellular) to measure RTT and stalls; then the Cloudflare DO room relay + RelayTransport (WebSocketPeer) per spec section 6, reusing the room-code UI.
  12. Render-only own-ship prediction for the client (scratch PlayerState advanced RTT/2 + cushion ahead, blended back, never fed to the sim). Client’s own ship responds about 11 ticks (48 px) late at 150 ms. After task 6.

Open design questions

  • Relay tech: DO WebSocket relay (TCP, head-of-line stalls on loss, about $0.003/match) vs a UDP relay on the VPS vs WebRTC. (lead: a WebSocket relay makes the unreliable input channel reliable-ordered, sharing head-of-line blocking with rows.) Decide from task 11’s measurements.
  • Pause semantics: any player pauses everyone (proposed) vs host-only; what a guest’s pause button does today (nothing).
  • Rejoin: slot reservation with a session token, then either row-log replay or a full-state keyframe (port the July export/import behind its completeness test). Also iOS backgrounding.
  • Join handshake: strict BUILD + config-hash equality and dev-only lobby; what a release build’s join flow and version-mismatch UX look like; whether a probe digest or math-canary hash joins the handshake.
  • Desync handling: net_checksum omits weapon cooldowns, projectile/gem payload and upgrade stats (needs a debug per-section hash); a desync is terminal with no resync path (lead); per-platform libm cos/sin in input decode (lead).

Bugs

  • Save/meta: quit-to-menu forfeits run gold. Quitting to the menu mid-run forfeits all run gold. (Decision 4)
  • Save/meta: non-atomic write, cache-fallback shadowing, unreadable-save guard. (Decision 5)
  • Save/meta: mid-run shop purchases do nothing until the next run. (Decision 13)
  • Enemy AI: Skirmisher ignores Dimension fire-rate, projectile-speed and Halo buffs; mirror the Shooter pattern.
  • Enemy AI: Coil discards manual-weapon and Orbit-shard knockback. (Decision 11)
  • Enemy AI: Accumulator spawn-time stat multipliers are overwritten every tick; store per-enemy base values.
  • Enemies: Elite Swarmer brood biomass rounds to a multiple of 2; distribute the exact total (no rng draw).
  • Weapons: dead re-offers for saturated Wildfire decal and Turret count (changes the shuffle, check determinism).
  • Weapons: Scar Tissue threshold rung is a no-op. (Decision 12)
  • UI: touch dialogue panel covers the boss bar, story objective and wave banner in story mode.
  • Co-op: low-HP vignette, border pulse and hit flash read P1 only; feed the lowest live pilot’s HP.
  • Copy: codex Warden text hard-codes the 40% enrage threshold; substitute the live tunable.
  • Leads to verify: Warden homing missiles keep hunting after the Warden dies; boss hits resolve only against the nearest pilot; Leech drain ignores dash i-frames; evolved melee arc mods shrink the cone; Overdrive speed boost only updates on fire ticks; story-mode walkers pin against walls; Coil snaps between pilots’ rings.

Improvements

  • Onboarding: tutorial re-forced on every cold launch. (Decision 6)
  • Onboarding: results screen “NEXT: X, N more gold” line from the first ShopAdvice row.
  • Onboarding: shop tip never recommends Ships; add a last-slot hull row (check tools/economy_sim/shop_tip_policy.gd).
  • Accessibility: Fire vs Blood colour pair. (Decision 10)
  • Accessibility: player-facing “Reduce motion” setting scaling shake, vignette and punch inside ScreenFeedback/ScreenPunch; today the only off switch is dev-only.
  • Accessibility: HUD and weapon-panel text below the 23pt TV floor; raise informational font.2xs/font.xs uses, then a text-size setting.
  • Leads: Boss2 RINGS gap direction never telegraphed; Boss2 Sentinel summons uncapped; enemy “Interceptor” shares a name with the drone class; gem magnet visuals are dead code.
  • Onboarding: locked-weapon teaser card in level-up offers (determinism re-pin). (Decision 9)
  • Telemetry: abandoned runs (quit, dev restart) send no run summary; add the summary with banked:false.
  • Game feel (leads): hit-stop on boss/elite kills and discrete player hits.
  • Enemy identity (leads): Centurion fires the same attack as Shooter.
  • Content (lead): 12 of 14 element statuses are data-only; only burn and shock have a mechanism.
  • Content (lead): four of seven Dimensions share the Warden fight.
  • Content (lead): the elemental reaction layer is dead in live survival; 13 bible reactions and 3 transformative mods are unreachable.
  • Enemy identity (lead): Spider and Charger (the dash family) share one state machine written three times.
  • Co-op (lead): pilots 3 and 4 are inert; tick() and integrate() stop at two pilots (the 4-pilot plan).

All 36 verified on origin/main (git branch -r --contains).

  • 1635e17a audio: pitch/volume jitter on frequent SFX, rising pickup pitch ladder
  • 1be21bc1 Scar Tissue card reads +8% instead of +-92%
  • da2fb27d Boss2 charge telegraph covers the full dash
  • e1d9a5a7 boss bar no longer overlaps the story objective
  • ac431575 Start/Back under Debug Settings or Bestiary no longer closes the pause menu
  • 2f0b0e38 RTT and packet-loss readout in the LAN banner and [lan] report
  • 93c9bf0e Bomber card reads BOMBER_HEAVY_BONUS live and lists Accumulator
  • 2788fca8 Obsidian shop card numbers pinned to ShipBonuses.TABLE by a test
  • b7c26920 co-op results screen shows the highest pilot level
  • 7dbc4e22 powerup pickups are per-pilot and skip downed pilots
  • 1c39429e single _sweep_run_children() keep-list
  • d1a9b99b gate runs the Sound Map and Story Board tool tests
  • a7417ec5 gate drops the machine-specific export_presets.cfg copy
  • 46b5071c gotchas doc marks the retired test example historical
  • 34275f64 dash-encounter player_dashed uses the dash_timer rising edge
  • f955d1f4 queue cap drops reported on the launch event
  • 13264b10 Prism Lance card matches the code
  • 71266221 Boss2 charge dash clamped to the arena
  • 95c4cb1d Orbit/Event Horizon hum plays in LAN co-op
  • 01931e07 opt-in WAN profile for LoopbackTransport
  • 4d237737 screen shake, hurt vignette, border pulse and death dissolve use real frame time
  • b234bb2a locked hull shows its price and banked gold
  • 548cc06e drone dock honours the safe-area inset
  • 98a4c989 level-up STATS button sized in points inside the safe area
  • 822509e8 C key closes an open codex
  • 8fad782b pending DRONE press cancelled explicitly when a menu opens
  • 8b10a3e6 _spawn_wave keeps running biomass/brute/beacon totals instead of rescanning the pool
  • c9517057 bestiary drops dead warden/sentinel stats blocks; test asserts live bible HP
  • ba45039d Redeploy / Climb again restart the survival music loop
  • ca60e65e feedback send-delay close uses a bound timer, not await on a freeable panel
  • f1c2bab8 gate guard caps pending/risky tests so skipped coverage is visible
  • 9ef4af0d visited_dimensions + end_area in the run timeline (privacy check: decision 14)
  • d7965b76 damage_by_source and max_hit_by_source in the run timeline (privacy check: decision 14)
  • 5e3ead3a dock damage readout includes per-weapon power multipliers
  • 55f87c42 Supernova evolution card matches the code (rarer, x7 damage, burning crater)
  • 70ae50e8 Stopwatch evolution card no longer promises a full freeze
  • Not landed: the unreadable-save guard (the edits were reverted in 57800cb2 and live on branch overnight/meta-unreadable-save-guard). (Decision 5)

Rule change, 2026-10-08 (Chris): a genuinely low-risk audio mix/feel change ships (flagged “listen on next Dev build”) rather than being held as his call. 1635e17a (SFX pitch/volume jitter + rising gem-pickup pitch) stays; listen to it on the next Dev build. Balance numbers stay gated.