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.mdon 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. SeeCLAUDE.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.gdremote_requestedsignal →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_Ato confirm in menus) — a controller’s Menu button did nothing because nothing listened. Nowmain._inputopens a centered pause overlay onJOY_BUTTON_START/JOY_BUTTON_BACKorKEY_ESCAPE;_paused_for_menugates_physics_process; the overlay shows the pairing code LARGE + centered and offers Arm Remote / Resume / Back to Start Menu (_return_to_menureuses the_new_runchild-sweep, setssim=null, re-shows the menu). Dash=RB / decoy=LB, so Start/Back are free. TV-overscan lesson: the original top-right greenREMOTE ▸ codeHUD 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 + D19c509410-…, separate from the telemetry worker). Panel served withCache-Control: no-storeso a stale cached copy can’t mask an update.GET /serves the panel fail-closed behindCONTROL_KEY(in~/.secretsasBULLET_HEAVEN_CONTROL_KEY);POST /cmdenqueues,GET /polldrains-by-cursor+prunes,POST/GET /status+/caps. All POSTs guard malformed/missing bodies → 400 (never 500).queue.jsis 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 fromGET /caps, so it can’t drift from the game’s enums; server data rendered viatextContentonly (stored-XSS guard). /simdev seams (plain data, default OFF → baseline byte-identical):Sim.dev_suppress_spawns(early-return in_spawn_enemies),PlayerState.dev_invuln(makesis_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 toDEV_PLAYER_FIELDS— no arbitraryset()). The determinism test never arms any of these.- Custom enemies = decoupling body+weapon from
type_id.EnemyPool.TYPE_CUSTOM=22+ new lockstep columnsshape_id/attack_id(resized in_init, default -1 inadd, swapped inremove_at).ArchetypeRenderer.shape_for(type_id, override)usesshape_idfor custom; firing routes viaSim.ATTACK_*ids — bolt/fan/bomb in a self-contained_update_custom_attackspass (deliberately NOT sharing helpers with_update_rangedto 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_commanddispatchesspawn_preset/spawn_boss/spawn_custom(placement via render-siderandf, NEVERsim.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-guardssim).- Known limit:
_dev_spawn_boss("warden")draws onesim.rngvalue (_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) getdisplay:nonefrom a STYLESHEET rule, so revealing them withel.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/nodesmoke. - 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 copyproject.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 usedtnotedt;TYPE_CUSTOMhas 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.shre-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 whatevermain.gdMEASURE_FULL_QUALITYcurrently is — while it’strue(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-flipMEASURE_FULL_QUALITY = falsefor the export, run the script, then restore it totrue(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-sitechangelog 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 deploydoes NOT honor.assetsignoreand 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 (onlyindex.html/404.html/favicon.svg/bible/), or physically move internal files out first. A file removed from a later deploy keeps serving a stale200from 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).