Skip to content

FunZo VFX Sandbox — Design

Date: 2026-07-03 Status: Approved by Chris, pending implementation plan

Build a sandbox for trying a wide range of Godot graphical techniques on the FunZo boss, so we can (a) judge how impressive the game can genuinely look on iOS/tvOS real hardware, and (b) build Chris’s working knowledge of what Godot 4.6 offers for future games. Winning effects may later be promoted into FunZo’s real in-game renderer as a separate follow-up — this spec covers only the sandbox itself.

  • Not committing to ship any specific effect into the live game — that’s a follow-up decision + plan after the gallery is judged.
  • Not a general “VFX framework” for all bosses — scoped to FunZo only. Reusable pieces (a general dash-trail, a new screen-space shader base) are a bonus, not a requirement.
  • Not touching /sim gameplay logic, only the one dev-only HP/phase-scrub seam described below.

FunZo is sim/funzo.gd (director) + sim/funzo_state.gd (pure attack-state) + render/funzo_renderer.gd (visual). Today it’s a 100% procedural _draw() clown/jester silhouette (dark core, fuchsia ring, rotating crown spikes), additive-blended, with no texture/sprite. Its HP-driven escalation arc is already dramatic and worth exposing directly rather than reskinning a static pose:

  • Body radius grows FUNZO_START_RAD_MULT (0.5×) → FUNZO_GROW_MAX_RAD_MULT (1.5×) as HP drops.
  • Dash chain count increases from 1 lunge toward infinite as HP falls (dashes_for).
  • Zone-drop cadence and dash speed both scale up with HP loss; zones themselves grow unbounded while FunZo is alive.
  • An enrage latch at ≤40% HP adds a throbbing outer aura and faster cadence on everything above.
  • Named fx_events already exist for spawn, enrage, dash-start, zone-summon, jester-minion-pop, and confetti-ring-burst — these are the real showcase beats a sandbox should hang effects on.

Existing render techniques already in the codebase (so the gallery below deliberately avoids re-proposing them): additive CanvasItemMaterial everywhere, several inline canvas_item shaders (screen_punch.gd chromatic aberration, gravity_lens.gd radial warp, shockwave_layer.gd blast rings, death_dissolve.gd noise-erosion dissolve, nebula_bg.gd FBM starfield, archetype_renderer.gd’s vertex-shader motion-smear, player_renderer.gd’s normal-mapped hull lighting), real HDR bloom via WorldEnvironment (already branches on RenderingServer.get_rendering_device() != null), a sparse Light2D system (hero_lights.gd), and manual-injection GPUParticles2D bursts (fx/gpu_burst.gd). No .gdshader resource files exist anywhere (every shader is an inline Shader.code string) — this sandbox will keep that convention rather than introduce the first .gdshader file, for consistency.

Explicitly absent, and each is why a corresponding gallery item below is genuinely new: LightOccluder2D shadow-casting, per-pixel lighting on any entity other than the player, continuous-emission particles, particle attractors, a general-purpose (non-warp-specific) motion trail, and a FunZo-specific screen-space post effect.

There is no reusable external open-source Godot code checked into this repo for graphics (or, on inspection, for networking either — the existing precedents cited in CLAUDE.md, chess-moba and moba-bakeoff/slice-godot, are Chris’s own prior private projects used as design precedent, not vendored dependencies, plus one upstream GitHub issue cited for a bug diagnosis). External research for this sandbox surfaced several MIT/CC-BY-licensed reference libraries worth skimming for technique ideas during implementation (not for direct code import): gdquest-demos/godot-visual-effects, gdquest-demos/godot-4-VFX-assets, haowg/GODOT-VFX-LIBRARY. These are inspiration only — every effect actually built here is authored fresh in this project’s existing inline-shader/procedural-_draw() style, matching repo convention.

Tier 1 — fast local prototyping (tools/funzo_lab/)

Section titled “Tier 1 — fast local prototyping (tools/funzo_lab/)”

Follows the exact tools/ship_preview/ / tools/bg_preview/ convention already established in this project:

  • A bare Node2D root .tscn with only a script attached, booted windowed (never --headless — per the standing rule that headless can’t read back shader/_draw()/particle output): godot --path . res://tools/funzo_lab/preview.tscn [--rendering-method mobile].
  • Instantiates FunZoRenderer directly — no full game/Sim boot.
  • A lightweight phase-timeline driver reusing the real constants from sim/funzo.gd (drift/telegraph/dash/cooldown durations, dash-count-by-hp-frac, zone cadence) so the preview’s timing matches the real fight, without needing a live Sim.
  • A TREATMENT env var (mirroring SHIP_VARIANT) selects which gallery item (1–9 below) is active; a keypress cycles through them live for side-by-side comparison.
  • An HP_FRAC env var (or a keyboard scrub) drives the escalation arc directly, so the “boss falls apart as it dies” progression can be judged without waiting through a real fight.
  • Own minimal WorldEnvironment/glow setup (copied from ship_preview’s pattern) so bloom-dependent treatments render correctly without the full game’s main.gd environment wiring.
  • Capture: same idiom as the existing tools — settle N frames → await RenderingServer.frame_post_drawget_viewport().get_texture().get_image().save_png()get_tree().quit() — writing to tools/funzo_lab/out/.

This tier is for rapid “does the shader/particle code even work, roughly how does it look” iteration — seconds per change, no device involved. Running with --rendering-method mobile (rather than the Mac-default forward_plus) matches the actual iOS/tvOS Metal path for anything HDR/bloom-threshold-sensitive, since both non-gl_compatibility renderers take the same branch in main.gd’s HDR detection; only fill-rate/overdraw cost differs between a Mac GPU and an A-series chip, not visual correctness.

Tier 2 — on-device showcase (real hardware verdict)

Section titled “Tier 2 — on-device showcase (real hardware verdict)”

Reuses the already-shipped, already-proven Remote Playtest Console (net/control_client.gd + the CF Worker relay at bullet-heaven-control.chris-allen-06f.workers.dev) rather than a rebuild-per-effect loop:

  • Two new dev commands, following the exact pattern of the existing dev_spawn_boss/dev_set_player_stat/dev_clear_enemies seams (allow-listed fields, default-inert, determinism-baseline-safe by construction since dev commands are OFF unless armed):
    • spawn_funzo_treatment(n) — spawns a real FunZo via the existing dev_spawn_boss("funzo") path with gallery treatment n’s visual bundle attached to its FunZoRenderer instance.
    • set_funzo_hp_frac(frac) — forces the live FunZo’s hp_frac so the whole escalation arc (body growth, dash chain, zone cadence, enrage) can be scrubbed on demand.
  • The existing web control panel (control/src/panel.html.js) gains a small caps-driven section for these two commands — no bespoke UI, same pattern as every other panel section.
  • One single iPad install covers the entire gallery: once the build is on-device, every treatment and HP state is triggered live over the network relay from a laptop/phone browser while watching the real Metal-rendered result on the iPad. No re-export/re-devicectl install per effect.

Chris’s iPad is now paired and trusted (Chris ipad pro, model iPad13,8, devicectl identifier 4A2B411C-53F7-5BB3-AA08-8B1467E9142B). No new Xcode export preset is needed — the existing iOS preset in the bullet-heaven-tvos repo (preset.1, stock Godot + stock Xcode, builds to build-ios/) already targets iPad’s architecture; this is just a new install destination alongside the already-documented iPhone.

# Treatment Technique Category
1 Living shadow Light2D (fuchsia/void-tinted) + LightOccluder2D cast from FunZo’s body onto the arena/zones Lighting
2 Lit clown-shell Extend player_renderer.gd’s normal-mapped hull-lighting shader to FunZo’s procedural body Lighting/shader
3 Confetti mist aura Continuous-emission GPUParticles2D swirling around the body Particles
4 Reality-tear debris Box/Sphere attractor ParticleProcessMaterial pulling ambient particles into each freshly-summoned DoT zone Particles
5 Dash afterimage Dedicated fading ghost-trail during the dash lunge, generalizing the existing warp-only _SMEAR_SHADER vertex smear into a reusable boss motion-trail Motion/shader
6 Unstable void skin death_dissolve.gd’s FBM noise-erosion technique repurposed as a continuous pulsing skin that intensifies with hurt_intensity/enrage, instead of a one-shot death effect Shader
7 Carnival warp (ultimate) New BackBufferCopy screen-space shader (sibling to screen_punch.gd/gravity_lens.gd) as FunZo’s own signature look, triggered on enrage-latch Screen-space
8 HP-scrub seam The set_funzo_hp_frac dev command above — not a visual, the tooling backbone that makes items 1–7 (and the real fight’s escalation arc) comparable on demand Tooling
9 Stylization wildcard SubViewport + posterize/CRT-scanline post pass Screen-space (experimental)

Items 1, 3, 4, 5, and 7 fill gaps explicitly identified as unbuilt anywhere in the project (no occluder shadows, no continuous particle emission, no attractors, no general-purpose trail, no FunZo-specific screen effect). Item 9 is explicitly a wildcard Chris asked to include for the “wide gallery” — likely won’t ship, worth the ~10 minutes to see.

All nine are render-side only. Item 8 is the sole exception touching Sim at all, and only as a dev-only allow-listed field mutation identical in shape to the already-shipped dev_set_player_stat — inert unless the Remote Playtest Console is armed, so the determinism baseline is untouched by construction.

Headlessly testable (GUT): the treatment registry (does treatment n attach the expected node/material set), the two new dev command dispatches (parsing, allow-listing, default-inert behavior) — same seam-testing pattern already used for the existing dev commands.

NOT headlessly testable: the actual visual output of any treatment. Per the standing project rule, headless Godot can’t read back _draw(), MultiMesh per-instance state, GPUParticles2D, or shader output — judgment happens via Tier 1 screenshots and the Tier 2 on-device session, not automated assertion.

Promotion path (out of scope for this spec)

Section titled “Promotion path (out of scope for this spec)”

Whichever treatments Chris picks get folded into the real FunZoRenderer as a separate follow-up plan, including wiring each into QualityManager’s adaptive tier ladder (bloom_on/halos_on/juice_on-style predicates) — any new glow/particle effect needs an explicit set_low()-style hook or it silently ignores adaptive quality and costs overdraw at every tier, per the gotcha already hit once on the galaxy background’s core glow sprite.