Skip to content

Elemental Dimensions v2 — design

Status: brainstormed with Chris 2026-07-05, from a content note written by Toby (“Various Dimensions” — 8 dimensions: Generic, Fire, Ice, Void, Blood, Electricity, Light, Poison, each with a themed enemy roster, boss, environmental hazard, and weapon set). This spec covers the foundation needed to support all 8 long-term, plus the first content wave — the 4 dimensions buildable with zero new bosses/enemies/weapons. The other 4 are explicitly deferred (see Decision 8).

Replace the current 3-dimension Elemental Dimensions feature (pyre/null_dim/drift — fire/void/aether via Graviton/FunZo/Eye, shipped BUILD 141-142, spec docs/superpowers/specs/2026-07-02-elemental-dimensions-design.md) with Toby’s mapping, and generalize the underlying system so each future dimension is a data-only addition: a new AreaDefs entry, not new code.

Context (what already exists — verified via code, not memory)

Section titled “Context (what already exists — verified via code, not memory)”
  • AreaDefs (sim/area_defs.gd) holds home/aurora/ember_reach (the plain wormhole cycle) plus pyre/null_dim/drift (the 3 live Dimensions, DIMENSION_IDS array). Each Dimension entry has element, boss, and a flat enemy_types: Array[int] (pool type ids).
  • Every enemy type’s element is otherwise hardcoded in bible.json (_enemy_base_el[type_id] at load). The live Dimensions feature deliberately only rosters enemies that already carry the dimension’s native element (old spec’s Decision 6: “no new bespoke enemy types” — true here too, but it also never needed to reflavor an enemy’s element, which Toby’s design requires).
  • 3 bosses are parameterized by element today: FunzoDirector/GravitonDirector/EyeDirector each take element_idx, dispatched from Sim._spawn_phase_boss() (_boss_gate_count % 3). Warden and Boss2 are NOT in that rotation — they were demoted to mid-wave “elite” spawns (Sim._spawn_due_elites, gated not AreaDefs.is_dimension(current_area)). However, Warden already has a separate, real arena-clearing “sole boss” spawn path: Sim.spawn_story_boss (today story-mode-only, element hardcoded to "void") spawns the exact same pooled TYPE_BOSS/BEHAVIOR_BOSS entity with the full attack-pattern state machine that _spawn_due_elites also uses — same underlying mechanic, different call site and element.
  • TYPE_BOSS’s death branch (Sim._sweep_dead, ~line 1654) already awards gold and reschedules the next boss, but deliberately does NOT call _award_dimension_crystals — because today Warden only ever dies as the mid-wave elite, never as a Dimension’s assigned boss.
  • update_tank_fire (sim/enemy_attacks.gd) already makes the tank/“Pyromancer” archetype periodically fire a fanned volley of homing tank_missile/“Fireball” enemies — i.e. it is already a summon-style mechanic; “Pyromancer” is just its fire-flavored skin.
  • scatterer (blood-element, 5-pellet fan) has no name field in bible.json today.
  • The orbiter enemy (orbit_shards/orbit_radius/orbit_spin/orbit_damage) and Graviton’s “orbiting satellite blobs” enrichment both already implement satellite-orbit math, independent of each other.
  • Sentinel is an existing, unrelated name in this codebase — a drone class (ui/drone_bay_panel.gd, ui/shop_categories.gd, ui/shop_icons.gd).
  • ContentDB.weapon_available_in(weapon_id, dimension_id) exists (old spec’s Decision 4) but is unused by any of the 7 shipped weapons today — a hook with no real restriction behind it yet.

Toby’s 8-dimension note (source material, verbatim intent)

Section titled “Toby’s 8-dimension note (source material, verbatim intent)”
Dimension Enemies Buffs Boss Hazard Weapon(s)
Generic (temporary) Swarmer, Shooter, Charger none Warden none all of them
Fire Swarmer, Charger, Shooter, Lancer speed, fire rate, proj speed Warden orbiting-fireball body Nova
Ice Swarmer, Orbitor, Weaver, Summoner health, damage Nautilus* floating ice chunk Orbit
Void Charger, Orbitor, Wyrm*, Ghost, Weaver size, damage Graviton asteroids + poor visibility Scythe*
Blood Swarmer, Brute, Accumulator health, size, speed The Queen* player-slow zones Scatter
Electricity Lancer, Shooter, Scatterer, Summoner fire rate, proj speed The Last Computer* warned lightning strikes Pulse, Turret
Light Swarmer, Lancer, Orbitor, Accumulator fire rate, damage Sentinel (=Eye, reskinned) warned lasers Beam
Poison Weaver, Wyrm*, Swarmer, Brute HP-drain-on-hit (new status) FunZo green FunZones Chalice*

* = not in the game yet (Toby’s own notation), except Sentinel, which is new as a boss despite lacking a star — see Decision 3.

Decision 1 — this spec ships Generic, Fire, Void, Light; defers Ice, Blood, Electricity, Poison

Section titled “Decision 1 — this spec ships Generic, Fire, Void, Light; defers Ice, Blood, Electricity, Poison”

Chosen because these 4 need zero new bosses, enemies, or weapons — only the foundation below plus reflavored existing content. The other 4 each need at least one hard blocker: Ice/Blood/Electricity need a brand-new boss (Nautilus/Queen/Last Computer — each a 5-part undertaking per this repo’s own documented lesson: death branch, nuke exemption, renderer+color LUT, HP hookup, movement skip); Poison needs a new player-status mechanic (HP-drain-on-hit, not a simple stat multiplier). Wyrm (Void, Poison) and Scythe/Chalice (Void, Poison, Electricity) are independently deferred and don’t block those dimensions shipping without them — Void ships with Charger/Orbitor/Ghost/Weaver (no Wyrm yet) and a temporary weapon set (no Scythe yet).

Decision 2 — retire pyre/null_dim/drift, replace with Toby’s mapping

Section titled “Decision 2 — retire pyre/null_dim/drift, replace with Toby’s mapping”

The current live mapping (Pyre/fire←Graviton, Null/void←FunZo, Drift/aether←Eye) is superseded, not kept alongside the new one. AreaDefs.DIMENSION_IDS becomes [fire, void_dim, light] (same size as today — 3 — since Ice/Blood/Electricity/Poison aren’t added until their own specs land; the “always offer all 3” rule needs no random-subset logic yet). The Drift/aether dimension has no equivalent in Toby’s 8 — Eye moves to Light instead (see Decision 3). Aurora and Ember Reach (the plain non-Dimension wormhole cycle) are untouched.

New id Element Boss Enemies (native element in parens)
fire fire Warden (new dispatch, see Decision 4) Swarmer(fire, native), Charger(void→fire), Shooter(lightning→fire), Lancer(light→fire)
void_dim void Graviton (native — zero rework) Charger(void, native), Orbitor(cold→void), Ghost(void, native), Weaver(decay→void)
light light Eye, renamed “Sentinel” (light←aether) Swarmer(fire→light), Lancer(light, native), Orbitor(cold→light), Accumulator(fire→light)
Generic (not a portal dest.) none Warden (new dispatch) Swarmer, Shooter, Charger — all native, no reflavor

Decision 3 — “Sentinel” is Eye, renamed and re-elemented; flag the naming collision, don’t avoid it

Section titled “Decision 3 — “Sentinel” is Eye, renamed and re-elemented; flag the naming collision, don’t avoid it”

Eye’s predictive-laser/blink/multi-beam moveset fits “a watching Sentinel” thematically better than “The Eye” fits Light specifically, so I’m keeping Toby’s name. Sentinel is already a drone-class display name elsewhere in the UI (ui/drone_bay_panel.gd etc.) — no code collision (different systems, different ids), but it’s a real “will a player conflate these” risk (e.g. a future “defeat the Sentinel” banner appearing near a drone-shop screen). Recorded here so it isn’t rediscovered later as a surprise; a copy pass before wide release should sanity-check any UI surface where both could appear together.

Decision 4 — wire Warden into the Dimension-boss system as a third parameterized call site

Section titled “Decision 4 — wire Warden into the Dimension-boss system as a third parameterized call site”

Sim.spawn_story_boss already does 90% of this (pooled TYPE_BOSS, full attack pattern, element parameter) — it’s just hardcoded to "void" and story-mode-only. Adding a survival/Dimension dispatch means: (a) parameterize the element argument instead of the hardcoded literal (same one-line change Decision 1 of the old spec made for FunZo/Graviton/Eye), (b) add "warden" as a valid AreaDefs.boss_for(id) value and a matching dispatch arm alongside the existing FunZo/Graviton/Eye rotation, gated to fire only when current_area is Generic or Fire, (c) add a spawn-origin flag (_boss_is_dimension_boss: bool, set at spawn, cleared on death) so TYPE_BOSS’s death branch only calls _award_dimension_crystals when Warden died AS a Dimension’s assigned boss — not when it died as the ordinary _spawn_due_elites mid-wave elite in Home/Aurora/Nebula, which must keep behaving exactly as it does today. Both call sites remain; only their death-reward divergence is new.

Decision 5 — enemy element override + the Tank/TankMissile pairing

Section titled “Decision 5 — enemy element override + the Tank/TankMissile pairing”

Each AreaDefs roster entry changes shape from Array[int] (pool type ids) to Array[{type: int, element: String}], element optional (omitted = use the enemy’s bible-native element — covers Void’s Graviton-native case and Generic/Fire’s Swarmer with zero special casing). Sim._spawn_one’s element lookup becomes: check the current dimension’s per-type override first, fall back to _enemy_base_el[type_id] otherwise. Because TYPE_TANK (“Pyromancer”/“Summoner”) always fires TYPE_TANK_MISSILE (“Fireball”) as a linked pair (update_tank_fire), overriding Tank’s element for a dimension implicitly overrides the missile’s too — a roster entry for Tank carries a paired_with: EnemyPool.TYPE_TANK_MISSILE note so a future dimension doesn’t have to redundantly list both. No dimension in this phase uses the Tank/Summoner pairing (it’s Ice/Electricity content) — the mechanism is built now because it’s the same code path as every other override, so the later Ice/Electricity specs don’t need to reopen sim.gd’s spawn logic.

Decision 6 — per-dimension enemy stat-buff framework

Section titled “Decision 6 — per-dimension enemy stat-buff framework”

New optional AreaDefs field per dimension: enemy_buffs: {speed_mult, fire_rate_mult, proj_speed_mult, hp_mult, damage_mult, size_mult}, every field defaulting to 1.0 (no-op) when absent. Applied ONCE, at spawn time, in Sim._spawn_one — multiplying the relevant EnemyPool columns immediately after the existing per-enemy _vary_stats jitter, and scaling the same per-type fire-timer/proj_speed fields update_tank_fire/enemy_attacks.gd’s ranged loops already read. Not a live per-tick multiplier, so it can’t interact with anything time-based or threaten determinism beyond the existing boss-death gate. This phase’s actual dimensions use: Fire → {speed_mult, fire_rate_mult, proj_speed_mult}; Void → {size_mult, damage_mult}; Light → {fire_rate_mult, damage_mult}; Generic → none (matches Toby’s “Enemy buffs: none”). Exact magnitudes are placeholder-but-sane starting values, flagged as tunable consts (same convention as FIRE_RATE_CAP/ELITE_HP_MULT elsewhere) — real numbers come from playtesting, not this spec.

Decision 7 — hazards (one new reusable primitive) and weapons (a real gate behind the existing hook)

Section titled “Decision 7 — hazards (one new reusable primitive) and weapons (a real gate behind the existing hook)”

Hazards, dispatched from the existing per-dimension timer pattern (old spec’s Decision 4): one new reusable primitive, a drifting hazard body (position, slow drift velocity, contact-damage radius, optional list of orbiting child bodies) covers both Fire’s “large fireball with smaller ones orbiting it” (reusing the orbiter enemy’s orbit-shard math and Graviton’s “orbiting satellite blobs” enrichment, detached from an enemy/boss) and Void’s “asteroids” (the same primitive, empty child list). Void’s “poor visibility” and Light’s “warned lasers” need no new primitive at all: visibility reuses the render-only vision-flicker effect the old spec built for the now-retired Drift/aether hazard (carried over rather than deleted and rebuilt); lasers reuse the existing Lancer enemy’s telegraphed beam mechanic (enemy_attacks.gd + lancer_telegraph_renderer.gd), fired environmentally instead of by an enemy — the same reuse pattern the old spec used for its Fire/bomber hazard. Generic has no hazard (matches Toby).

Weapons: new optional AreaDefs field per dimension, weapons: Array[{id: String, temporary: bool}] — absent/empty means unrestricted (Generic’s case, matching home today). ContentDB.weapon_available_in (currently unused by any real weapon) becomes a real gate: if a dimension defines a list, roll_upgrade_choices’s weapon-grant filter only offers listed weapons. Proposed sets for this phase (creative picks, not load-bearing architecture — Chris may retune at spec review):

Dimension Weapons Temporary?
Generic (all, unrestricted)
Fire Nova (native) + Turret, Scatter Turret/Scatter
void_dim Orbit, Blade both — Void’s real weapon (Scythe) doesn’t exist yet
Light Beam (native) + Pulse, Blade Pulse/Blade

temporary: true is a data-only marker (no runtime behavior difference) so a later pass can grep for it to know what to retire once Scythe/Chalice and other elemental weapons ship, per Toby’s own stated intent.

Decision 8 — explicitly deferred (documented so it isn’t silently forgotten)

Section titled “Decision 8 — explicitly deferred (documented so it isn’t silently forgotten)”
  • Ice, Blood, Electricity, Poison as playable dimensions — each needs its own spec once its blocking new content exists (a new boss for the first three; the HP-drain-on-hit status effect for Poison).
  • 3 new bosses — Nautilus (Ice), The Queen (Blood), The Last Computer (Electricity). Not designed at all here beyond Toby’s one-line flavor notes (“water-themed”, “summons/dashes only”, “colourful bullet-hell”).
  • Wyrm — the shared-health, individually-damageable multi-segment worm. No existing EntityPool/EnemyPool mechanic resembles this; needs its own design pass before any dimension that lists it (Void, Poison) gets it. Both dimensions ship without it in this phase.
  • Scythe and Chalice — new weapons (%-health damage; poison AOE respectively). Void and Poison both ship with temporary weapon substitutes until these exist.
  • Poison’s HP-drain-on-hit mechanic — “10% of damage taken becomes temporary max-HP loss, restored on leaving the dimension” is a new player-status effect, not a stat multiplier; out of scope for the enemy_buffs framework in Decision 6.
  • Random-subset portal selectionDIMENSION_IDS stays “always offer all 3” in this phase; once Ice/Blood/Electricity/Poison bring the pool past 3, a later spec needs to decide how a boss-kill picks which 3 of N to offer.

New tests/test_dimensions_v2.gd (extending, not replacing, tests/test_dimensions.gdtests/test_areas.gd/tests/test_wormhole.gd are flagged by CLAUDE.md as having bitten a prior N-way generalization before; grep both for hardcoded assumptions before considering this pass done):

  • AreaDefs.DIMENSION_IDS == [fire, void_dim, light]; pyre/null_dim/drift no longer resolve as dimensions.
  • Enemy element override: a roster entry with an override spawns at the overridden element; an entry with none falls back to native (covers Void’s Graviton-native case with no special casing).
  • Tank/TankMissile pairing: overriding Tank’s element for a dimension re-elements the missiles it fires too.
  • Stat-buff framework: each of the 6 multiplier fields applies once at spawn; every field defaults to a no-op (1.0) when a dimension omits it.
  • Warden-as-dimension-boss: spawning via the new Generic/Fire dispatch sets the spawn-origin flag and its death branch awards dimension crystals; spawning via the pre-existing _spawn_due_elites mid-wave path does not (regression guard against the two paths bleeding into each other).
  • The 3 hazards (Fire drift-and-orbit body, Void asteroids + visibility, Light warned laser) each fire at most once per interval and never while a boss is alive.
  • weapon_available_in correctly restricts to each dimension’s list; Generic stays unrestricted.
  • Existing determinism tests (tests/test_determinism_checksum.gd, tests/test_determinism_crystals.gd) still pass — every new spawn/rng path here is boss-death-gated exactly like the current live feature, so the baseline should hold by construction; re-verify anyway per this repo’s standing rule.

Once green (full suite + both determinism tests + boot check), deploy via the established bh-deploy skill to Apple TV + iPhone. Update CLAUDE.md’s “Current status” section and the roadmap memory noting this replaced Pyre/Null/Drift, plus every Decision above so Chris (or whoever designs Ice/Blood/Electricity/Poison next) doesn’t have to re-derive the reasoning.