Skip to content

Bullet Heaven — dev tools, public site & web demo

Bullet Heaven — dev tools, public site & web demo

Section titled “Bullet Heaven — dev tools, public site & web demo”

Extracted from CLAUDE.md on 2026-07-04 to keep the always-loaded file lean. This is the current architecture reference for these systems, not a changelog — the “M2 cycle N, DONE” headings document present code. Keep it current when you change the code. See CLAUDE.md § “Subsystem architecture — read on demand”.

Remote Playtest Console (M2 cycle 23, DONE) — dev tool, off-by-default

Section titled “Remote Playtest Console (M2 cycle 23, DONE) — dev tool, off-by-default”

A web panel that controls a LIVE survival playtest on ANY platform (esp. the Apple TV) via a CF Worker relay the game POLLS — spawn any enemy/boss, build custom enemies (body × movement × weapon × stat sliders × element), edit live player stats, isolate (pause spawns + clear arena), survive (invuln + time-scale), and read back alive-counts/fps/hp. Spec docs/superpowers/specs/2026-06-25-remote-playtest-console-design.md, plan docs/superpowers/plans/2026-06-25-remote-playtest-console.md. 6 TDD’d tasks, each reviewed; determinism baseline 1432233777/2300319179 byte-identical (everything defaults OFF). Fully verified end-to-end on the Apple TV @ BUILD 58 (pause-menu arm + full panel + spawn/builder/player-editor all confirmed live).

  • Transport = telemetry.gd run backwards. tvOS/web can’t accept an inbound socket but CAN poll. net/control_client.gd (ControlClient extends Node, render-side, OFF until armed) GETs /poll?code=&after= draining commands → main.apply_dev_command(cmd), and POSTs /status + /caps. Armed by F6 (desktop) or the start-menu Remote Control card (ui/start_menu.gd remote_requested signal → control_client.enable(self)); a 6-char pairing code shows on the HUD and scopes commands to that instance.
  • In-run PAUSE menu (ui/pause_menu.gd, BUILD 58) — the real arm + code-readout path on a TV. Before this, the game had ZERO pause/back-to-menu binding (only F2/F4/F6 keyboard + JOY_BUTTON_A to confirm in menus) — a controller’s Menu button did nothing because nothing listened. Now main._input opens a centered pause overlay on JOY_BUTTON_START/JOY_BUTTON_BACK or KEY_ESCAPE; _paused_for_menu gates _physics_process; the overlay shows the pairing code LARGE + centered and offers Arm Remote / Resume / Back to Start Menu (_return_to_menu reuses the _new_run child-sweep, sets sim=null, re-shows the menu). Dash=RB / decoy=LB, so Start/Back are free. TV-overscan lesson: the original top-right green REMOTE ▸ code HUD label was CLIPPED by the TV bezel — never put critical 10-foot UI at a screen edge; the centered pause menu is the readable home for the code.
  • Relay worker control/ (bullet-heaven-control.chris-allen-06f.workers.dev, CF Worker + D1 9c509410-…, separate from the telemetry worker). Panel served with Cache-Control: no-store so a stale cached copy can’t mask an update. GET / serves the panel fail-closed behind CONTROL_KEY (in ~/.secrets as BULLET_HEAVEN_CONTROL_KEY); POST /cmd enqueues, GET /poll drains-by-cursor+prunes, POST/GET /status + /caps. All POSTs guard malformed/missing bodies → 400 (never 500). queue.js is pure + node-tested (cd control && node --test). Deploy: cd control && CLOUDFLARE_API_TOKEN="$CF_LUMARA_DEPLOY_TOKEN" CLOUDFLARE_ACCOUNT_ID="$CF_ACCOUNT_ID" npx wrangler deploy. The panel page (src/panel.html.js) is caps-driven — every dropdown/slider from GET /caps, so it can’t drift from the game’s enums; server data rendered via textContent only (stored-XSS guard).
  • /sim dev seams (plain data, default OFF → baseline byte-identical): Sim.dev_suppress_spawns (early-return in _spawn_enemies), PlayerState.dev_invuln (makes is_invulnerable() true → covers EVERY damage site, the one guard), Sim.dev_clear_enemies() (drains enemies+enemy_proj, clears bombs/boss_missiles/boss_rockets/zones/webs/funzones/powerups), Sim.dev_spawn_custom(spec,pos), Sim.dev_set_player_stat(field,value) (ALLOW-LISTED to DEV_PLAYER_FIELDS — no arbitrary set()). The determinism test never arms any of these.
  • Custom enemies = decoupling body+weapon from type_id. EnemyPool.TYPE_CUSTOM=22 + new lockstep columns shape_id/attack_id (resized in _init, default -1 in add, swapped in remove_at). ArchetypeRenderer.shape_for(type_id, override) uses shape_id for custom; firing routes via Sim.ATTACK_* ids — bolt/fan/bomb in a self-contained _update_custom_attacks pass (deliberately NOT sharing helpers with _update_ranged to avoid churning the hot deterministic tick), beam/shards by broadening the lancer/orbiter guards (tid==TYPE_CUSTOM and attack_id==…). Determinism-safe: TYPE_CUSTOM never spawns in the baseline.
  • main.apply_dev_command dispatches spawn_preset/spawn_boss/spawn_custom (placement via render-side randf, NEVER sim.rng), clear/pause_spawns/invuln/timescale (render-side: _ts_accum-driven tick loop in _physics_process, default 1.0 = one tick/frame), player_stat. build_caps()/build_status() publish the manifest + readout (the latter null-guards sim).
  • Known limit: _dev_spawn_boss("warden") draws one sim.rng value (_spawn_boss() has no positioned overload) — perturbs the run’s spawn stream; acceptable (using the console already abandons reproducibility), documented in code.
  • Panel-reveal gotcha (shipped, fixed live): the panel sections (#spawn/#builder/#player/#survivability) get display:none from a STYLESHEET rule, so revealing them with el.style.display='' is a NO-OP (it only clears an inline style → falls back to the rule). Must set 'block'. It shipped because the build check only asserts the panel HTML string contains the sections — it never RUNS the panel. Lesson: verify any generated web panel in a real browser (or headless), not just a string/node smoke.
  • tvOS deploy is DONE (BUILD 58). When the tvOS repo was behind on MULTIPLE features (this console + the parallel Element Crystals work), I rsynced the WHOLE gameplay surface (ai audio content data fonts fx input meta net render sim tests ui + main.gd), not just the changed files — guarantees the tvOS build mirrors main. NEVER copy project.godot/icon.png/export_presets.cfg/.godot//builds/ (platform-only). main now also carries the Element Crystals ruleset (BUILD 57, the other agent), so BUILD 58 ships both.
  • Deferred polish (.superpowers/sdd/remote-playtest-console-progress.md): custom attacks use dt not edt; TYPE_CUSTOM has no distinct base colour (NEUTRAL fallback); a couple of weak tests to harden; the web demo R2 redeploy (the console is opt-in, never polls unless armed, so the public demo is unaffected).

Live site — ~/Claude/bullet-heaven-site/ (separate repo)

Section titled “Live site — ~/Claude/bullet-heaven-site/ (separate repo)”

Public game site — canonical https://dark-cosmos.lumara.digital (rebranded to Dark Cosmos 2026-07-01; https://bullet-heaven.lumara.digital still serves the same site, both are custom domains on the one CF Pages project bullet-heaven) — neon landing + the Balance Bible at /bible/ + a playable Godot web demo embedded from R2 (the demo iframe still points at bullet-heaven-play.lumara.digital — not user-visible, rename deferred). ⚠️ scripts/deploy-site.sh stages a clean-dir ALLOWLIST PUBLIC=(index.html 404.html favicon.svg bible privacy) — a NEW public page/dir must be added to PUBLIC or it silently won’t deploy (adding /privacy/ needed this). It also verifies /privacy/ is 200 now. Hosting is split: landing+bible on CF Pages, the Godot web build on CF R2 at bullet-heaven-play.lumara.digital (the 36MB wasm exceeds CF Pages’ 25MB/file limit). The web build is exported from THIS repo (export_presets.cfg, Web preset, no-threads variant → builds/web/) and uploaded to R2. Full deploy + re-export instructions live in ~/Claude/bullet-heaven-site/CLAUDE.md. The /bible/ on the site is a COPY of tools/design-bible/ (this repo is the source of truth).

  • One-command demo deploy: scripts/deploy-demo.sh re-exports the Web build and uploads it to the R2 bucket, then verifies the live Content-Length matches (fail-loud). Run after game changes. It does NOT touch the landing/Bible (a separate CF Pages deploy in the site repo). Measurement-flag gotcha (2026-06-26): the export ships whatever main.gd MEASURE_FULL_QUALITY currently is — while it’s true (the ATV-measurement open loop) a redeploy would put the debug fps overlay + L0-pinned quality on the PUBLIC demo. So when refreshing the public demo with measurement mode on, temp-flip MEASURE_FULL_QUALITY = false for the export, run the script, then restore it to true (the export reads the flag early, so the uploaded artifact is the clean adaptive build and the TV loop stays open). The web demo + bullet-heaven-site changelog are CURRENT at build 78 (the 2026-06-29 coherence release — see below; demo boots to the Tutorial/Story/Survival/Crystals menu). Pages-deploy gotcha (bit us 2026-06-23): wrangler pages deploy does NOT honor .assetsignore and walks the dir OFF DISK, so it will upload internal files (CLAUDE.md, .claude/) and make them public — deploy the site from a CLEAN dir (only index.html/404.html/favicon.svg/bible/), or physically move internal files out first. A file removed from a later deploy keeps serving a stale 200 from CF Pages’ own asset cache (7-day s-maxage, immune to zone purge) — the instant fix is a CF Dynamic Redirect for that path (one already guards /CLAUDE.md → landing in the lumara.digital dynamic-redirect ruleset).