Skip to content

Bullet Heaven — Phase 1 Design (Singleplayer Core)

Bullet Heaven — Phase 1 Design (Singleplayer Core)

Section titled “Bullet Heaven — Phase 1 Design (Singleplayer Core)”

Date: 2026-06-21 Status: Approved (design) — pending spec review Engine: Godot 4.6.3 stable Language: Typed GDScript (with an isolated hot loop swappable to C#/GDExtension if profiling demands)


A neon-abstract “bullet heaven” survival game in the lineage of Vampire Survivors and Nova Drift. Direct, accessible movement (one analog stick) combined with deep, transformative build-craft (Nova Drift DNA). Cute/neon abstract art, rendered almost entirely in code (glowing shapes + particles) — no sprite-artist dependency, lightest possible asset load, gorgeous fast.

iOS, Android, macOS, Windows (Linux/Web essentially free from Godot export).

  1. Accessible to control — one-thumb playable on a phone.
  2. Deep to build — upgrade choices radically change how you play, not just stat bumps.
  3. High performance — thousands of entities on screen at a high, stable frame rate.
  • Phase 1 (this spec): complete, fun, singleplayer timed-survival game.
  • Phase 2: content & depth — many weapons/abilities/enemies, synergies, meta-progression.
  • Phase 3: co-op multiplayer (2–32 players), layered onto the working game.

Phase 1 is built multiplayer-aware (see §7) so Phase 3 is a layer, not a rewrite. No networking is implemented in Phase 1.


Input (per-player InputState) ──► Simulation (fixed deterministic tick) ──► Render (reads sim state)
  • Simulation owns all gameplay state and never touches the screen. Runs on a fixed timestep via _physics_process, with the tick rate set through Engine.physics_ticks_per_second (target 60 Hz). The sim integrates using a constant dt (e.g. 1.0/60.0), NOT the delta argument, so it is bit-reproducible — the precondition for future netcode.
  • Render reads sim state each frame and draws it. Never mutates gameplay. Free to run at the display refresh rate; interpolates between sim ticks for smoothness.
  • Input is abstracted into a per-player InputState struct. A local player and a (future) remote player are just two input sources feeding identical sim slots.

2.2 Entity strategy (Approach C — hybrid)

Section titled “2.2 Entity strategy (Approach C — hybrid)”
  • Node-based entities for the few important objects: player, bosses, pickups that need rich behaviour, UI. Editor-friendly, low count.
  • Data-oriented swarm for the many cheap objects: basic enemies, bullets/projectiles, XP gems. Stored in flat arrays, rendered via MultiMeshInstance2D (one draw call for thousands), collision via a hand-rolled uniform-grid spatial hash. Deterministic and serializable. Built data-oriented from the first enemy — never retrofitted.
  • Swarm: MultiMeshInstance2D with use_colors + use_custom_data. Per-instance transform_2d (position/rotation/scale), color (tint/team), and custom_data feeding a small shader for per-entity effects (hit-flash, glow pulse, spawn-in).
  • Neon look: a WorldEnvironment with glow enabled (4.6 blends glow pre-tonemap, “Screen” default) for the bloom. Shapes drawn as simple meshes/polygons.
  • Juice: GPUParticles2D for bursts (deaths, level-up, hits). Render-only; use_fixed_seed available if we ever want deterministic visuals. Particles never feed the sim.
/sim # pure logic: entity arrays, spatial hash, spawn director, weapons, mods, XP, RNG, run state
/render # MultiMesh swarm renderer, node entities, camera, particles, glow/environment
/input # input sources (touch joystick, keyboard, gamepad) → InputState
/ui # HUD, level-up upgrade picker, results screen, menus
/data # weapon/enemy/mod/evolution definitions (Resources/.tres, data-driven)
/tests # GUT unit tests for /sim (headless)

2.5 Determinism rules (cheap now, essential later)

Section titled “2.5 Determinism rules (cheap now, essential later)”
  • Single seeded RNG owned by the sim; all randomness flows through it.
  • Constant dt integration (above).
  • No engine physics for the swarm (engine physics is non-deterministic cross-machine).
  • Sim state is plain serializable data (arrays + structs) — snapshot/diff friendly.

  1. Move with the analog stick (360°). Weapons auto-fire on cooldown and auto-target (nearest enemy, or per-weapon rule).
  2. Enemies spawn continuously from off-screen, driven by the spawn director which escalates on a timeline (count, type mix, toughness).
  3. Kill enemies → XP gems drop. Walk within pickup radius to collect.
  4. Fill XP bar → level up. Sim pauses; the upgrade picker offers 3–4 choices; pick one; resume.
  5. Difficulty ramps on the timeline; mini-bosses/elites at intervals; a final boss near the end of the timed run (~20 min target).
  6. Run ends on death or surviving the timer → results screen (time, level, kills, build summary).

Endless mode (Phase 2-ish, designed-for now): same loop, end-cap removed, escalation curve continues. Costs ~nothing to keep possible because the spawn director and run state are already parameterised by time.

  • Movement: virtual joystick (touch), WASD/arrows (keyboard), left stick (gamepad). 360° analog.
  • Attacks: fully automatic — auto-fire + auto-target. One-thumb playable.

All upgrades flow through the same level-up picker. Three layers:

  1. Weapons (active). Each auto-fires with its own behaviour; levels up independently (1→max). Player starts with one, acquires more.
  2. Mods (passive). Two deliberately-mixed flavours:
    • Stat mods: fire rate, damage, projectile count, area, move speed, pickup radius, max HP, crit. The reliable backbone.
    • Transformative mods: change how a weapon behaves — pierce, split-on-hit, exploding orbits, chaining beams. These create builds.
  3. Weapon evolutions. At max weapon level + a required mod, a weapon evolves into a dramatically transformed version. Rewards committing to a path; creates the “these combine!” discovery moment.

Data-driven: weapons, mods, enemies, and evolutions are defined as Resources in /data. Adding/tuning content = editing data, not code. Upgrade application logic lives in /sim and is unit-tested.


A complete, shippable singleplayer game — a focused slice, not the full content vision.

Element Phase 1 Notes
Arena 1 large bounded neon field scrolling grid bg, camera follows player, glow/bloom
Player character 1 neon ship/shape, one starting weapon
Weapons 5 Pulse (homing shot), Orbit (orbiting shards), Beam (sweeping laser), Nova (AoE pulse), Turret/Mine
Mods ~10 ~6 stat + ~4 transformative
Evolutions 2–3 prove the evolve hook
Enemy types 5 Swarmer (fast/weak), Tank (slow/tough), Shooter (ranged), Splitter (splits on death), Elite/mini-boss
Boss 1 final boss at end of timed run
Run length ~20 min timed spawn director escalates across it
Meta-progression none deferred to Phase 2 to keep the slice focused

Performance target: stable 60 FPS on desktop with several thousand simultaneous entities (enemies + bullets + gems); mobile verified to hold a smooth frame rate at the densities a 20-minute run reaches.


  • Godot 4.6.3 installed (CLI at /opt/homebrew/bin/godot, editor at /Applications/Godot.app).
  • I edit .gd / .tscn / .tres as text directly; Chris uses the editor for visual verification and any scene wiring easier in the GUI.
  • Live docs: context7 MCP for current Godot 4.6 API (never code from stale memory).
  • Testing: /sim is pure logic → unit-tested headlessly with GUT via godot --headless. TDD the sim: spawn director, collision/damage, XP/leveling, mod & evolution application, RNG determinism. Render/feel verified by playing.
  • Runtime self-verification (added later, vetted): when the render phase begins, add a zero-footprint Godot runtime MCP (e.g. Erodenn/godot-mcp-runtime or satelliteoflove/godot-mcp) so the agent can screenshot, inject input, and read live game state to self-verify visuals/feel. Security-vetted before install; not needed for the headless sim work, so not installed up front.
  • Repo: bullet-heaven is its own git root (init’d) so Godot files aren’t swallowed by the parent ~/Claude ignore-everything gitignore.

7. Multiplayer-aware decisions (Phase 1 cost: ~0)

Section titled “7. Multiplayer-aware decisions (Phase 1 cost: ~0)”

Disciplined choices now so Phase 3 co-op is additive:

  • Fixed deterministic sim tick, decoupled from render (constant dt).
  • Centralized seeded RNG.
  • Input as data (InputState struct) — remote player = another input source.
  • No engine physics for the swarm (own spatial hash) → deterministic + serializable.
  • Sim state is plain serializable data → snapshot/diff for state-sync netcode.

No networking code is written in Phase 1.

7.1 Phase 3 netcode reference (decided, for later)

Section titled “7.1 Phase 3 netcode reference (decided, for later)”

Reuse the authoritative-server + client-prediction model proven in Chris’s chess-moba project (server/src/match-room.ts + src/net/prediction.ts): one authoritative Sim per match owns truth, clients send tick-stamped input commands, the server ticks and broadcasts snapshots, clients predict locally for responsiveness. Do not use the chessdefense-versus-relay lockstep relay — it assumes 2 fixed slots and does not scale to 2–32 players. Transport nuance: chess-moba runs its Sim in TypeScript inside a Cloudflare Durable Object; our sim is GDScript and cannot run in a DO. So the Phase-3 authority will be Godot ENet host-authority or a headless Godot dedicated server, with chess-moba’s DO model as the design discipline reference, not the literal stack.


7.5 Prior art & reuse (from Chris’s existing projects)

Section titled “7.5 Prior art & reuse (from Chris’s existing projects)”
  • moba-bakeoff/slice-godot/core/sim.gd — same extends RefCounted / class_name Sim, no-scene-tree, fixed-tick, seeded-RNG pattern we use here. Our conventions match it deliberately. Mirror its snapshot_string() + per-tick-trace determinism check (tests/determinism_check.gd) — it pinpoints the exact tick of any divergence and doubles as Phase-3 desync detection.
  • chess-moba spawn cadence (maybeSpawnWave()) validates the time-escalating spawn director approach.

  • Multiplayer / networking (Phase 3).
  • Meta-progression / persistent unlocks / currency (Phase 2).
  • Multiple characters, multiple maps (Phase 2).
  • Endless mode UI/flow (designed-for, not built).
  • Final art/audio polish beyond the neon code-drawn baseline.
  • Store/export packaging for each platform (separate go-live effort).

  • A run is genuinely fun and replayable: different upgrade picks produce different play.
  • 60 FPS desktop at several-thousand-entity densities; smooth on a test mobile device.
  • The build-craft pillar is real: at least 2 distinct viable build paths via the 5 weapons, transformative mods, and evolutions.
  • /sim covered by passing headless GUT tests, including an RNG-determinism test.
  • Clean sim/render/input separation verified (no gameplay logic in the render layer).