Skip to content

Nebula (Ember Reach) galaxy area background + `bh-area-background` skill

Nebula (Ember Reach) galaxy area background + bh-area-background skill

Section titled “Nebula (Ember Reach) galaxy area background + bh-area-background skill”

Date: 2026-07-02

Add a third explorable area — display name “Nebula”, internal id ember_reach (chosen to avoid colliding with the existing internal VARIANT_NEBULA name, which is an unrelated soft-glow-cloud look already used in the Home random pool) — with a warm gold/amber/orange galaxy backdrop: a spiral-armed core plus a sparser scattered star field filling the rest of the arena. Package the process as a reusable local skill so future area backgrounds are faster to add correctly.

Areas are currently gated off for the v0.1 launch (main.V01_LOCK_AREAS = true) — this work adds the area and its background but does not need to unlock exploration; it ships dark until the flag flips, same as Aurora did.

Current system (confirmed by reading the code)

Section titled “Current system (confirmed by reading the code)”
  • sim/area_defs.gd (AreaDefs, pure /sim data, no engine APIs): a static dict id → {name, difficulty_mult, reward_mult, background}. background is a plain string — sim never touches rendering, it just names a look.
  • main.gd reads AreaDefs.get_def(_warp_dest)["background"] and calls ArenaBackground.set_variant_by_name(name, seed).
  • render/arena_background.gd (ArenaBackground): set_variant_by_name special-cases known area names to a dedicated variant constant; anything unrecognized falls into the random “Home pool” (VARIANT_GRID / VARIANT_STARFIELD / VARIANT_NEBULA, picked by a render-side seeded RNG). Aurora already works this way (VARIANT_AURORA = 3, animated ribbon bands, area-selected only — explicitly excluded from the random pool via VARIANT_COUNT).
  • The scatter/placement data (star positions, nebula-cloud positions) is generated once per run in set_variant() from a render-side seeded RandomNumberGenerator, not sim.rng — background choice and appearance never affect the determinism baseline.
  • AreaDefs.other(id) is currently a hard binary toggle (Aurora if id == Home else Home) for the two-way wormhole. With a third area this must become a real list/cycle.

Part 1 — the Nebula area + galaxy background

Section titled “Part 1 — the Nebula area + galaxy background”
  • Add const EMBER_REACH := "ember_reach".
  • Add a _DEFS entry: {"name": "Nebula", "difficulty_mult": 1.3, "reward_mult": 1.25, "background": "ember_reach"} — deliberately between Home (1.0/1.0) and Aurora (1.6/1.5).
  • Replace other(id) with a cycle across all non-Home areas (or a full ordered list) so adding a 4th area later doesn’t require touching the wormhole logic again. Exact shape decided during implementation planning — keep it a pure data function, no behavior change for Home↔Aurora.
  • Add const VARIANT_GALAXY := 4 (area-selected only, excluded from VARIANT_COUNT’s random pool — same treatment as VARIANT_AURORA).
  • set_variant_by_name: add an "ember_reach" branch → set_variant(VARIANT_GALAXY, seed_val).
  • set_variant(): on VARIANT_GALAXY, generate two star sets from the seeded RNG:
    1. Spiral core — 2–3 arms via a logarithmic-spiral parametric curve (r = a * exp(b * theta)), walking theta outward per arm with per-star angle/radius jitter so it reads as a dense star cloud shaped like a spiral, not a printed curve. Reuses the same per-star record shape as the existing starfield (p, r, a, phase, tw) so the existing twinkle animation in _process/_draw works unchanged.
    2. Scattered field — a sparser uniform scatter across the rest of the arena, generated the same way as VARIANT_STARFIELD today (lower count than the core). Store both in _stars (or a second array if keeping them visually distinct — e.g. brighter/larger near the core — is easier that way); decide during implementation based on what reads best.
  • _draw(): add a VARIANT_GALAXY branch drawing the star sets with a warm palette — core near white-gold fading through amber to deep orange in the outer arms (exact hex values tuned visually during implementation, e.g. core Color(1.0, 0.95, 0.75), arms ranging toward Color(0.85, 0.35, 0.05)). Keep the same additive blend + twinkle approach as VARIANT_STARFIELD.
  • Update the VARIANT_COUNT/wrap comment at the top of the file to note the galaxy variant, same as the existing Aurora note.
  • No /sim changes beyond area_defs.gd; no determinism impact (render-side RNG only, same as every other variant).

Custom _draw() painting and per-instance colors can’t be read back headless (established gotcha: headless MultiMesh/canvas readback returns dummy values). Verify visually:

  • A temporary windowed check (same pattern as tools/ship_preview, or a throwaway _shot.gd-style scene) rendering ArenaBackground alone at VARIANT_GALAXY to confirm the spiral reads as a galaxy and the palette looks right before wiring it into main.gd.
  • Then boot-check + full suite + determinism baseline per bh-dev-chunk, expecting the baseline to be unchanged (render-only).

New local skill at .claude/skills/bh-area-background/SKILL.md (gitignored, project-only — matches the existing bh-dev-chunk / bh-deploy / bh-appstore-release convention). Same terse, checklist-driven format as bh-dev-chunk.

Checklist it documents, generalized from this exact task:

  1. Naming gotcha up front: pick an internal variant id that does NOT collide with any existing VARIANT_* name or area id — the visible in-game name can differ from the internal id (as with ember_reach → “Nebula”). Grep render/arena_background.gd and sim/area_defs.gd for existing names before choosing one.
  2. sim/area_defs.gd: add the id const + _DEFS entry (name, difficulty_mult, reward_mult between existing areas unless told otherwise, background string). Update other()/any area-cycling logic if the area count changed. Stays pure /sim data — no engine APIs.
  3. render/arena_background.gd: add a new VARIANT_* const, EXCLUDE it from the random VARIANT_COUNT pool if it’s area-selected-only (the norm — Aurora and Galaxy both are), add the set_variant_by_name branch, add scatter/placement generation in set_variant() seeded from the render-side RNG (never sim.rng), add the _draw() branch for the new look, reuse the existing per-star record shape + twinkle/pulse animation pattern where the new look is star-based.
  4. Verify visually — headless can’t read back custom _draw() painting; use a windowed throwaway scene/harness to confirm the look before wiring into main.gd.
  5. Wire into main.gd where the area is entered (arena_bg.set_variant_by_name(...)) if not already generic (it already is — no main.gd change needed unless the entry point logic itself changes).
  6. Full bh-dev-chunk gate: boot-check, full suite + count guard, determinism baseline (expect no change — render-only), commit. Sync to tvOS per bh-dev-chunk §7 if shipping to device.
  7. Not covered by this skill: enemy rosters, difficulty tuning depth, or unlocking V01_LOCK_AREAS — this skill is scoped to “area identity + its backdrop” only.
  • Unlocking areas for players (V01_LOCK_AREAS stays true; this ships dark like Aurora did).
  • New enemies/mechanics specific to Nebula — v1 areas share the same enemy roster per area_defs.gd’s existing comment (“same enemy roster, no bespoke enemies yet”).
  • Web demo / site changelog updates (areas are locked off, nothing visible changes publicly).