Skip to content

Shop "Glassy Ambient" Restyle Implementation Plan

Shop “Glassy Ambient” Restyle Implementation Plan

Section titled “Shop “Glassy Ambient” Restyle Implementation Plan”

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Give the shop panel (ui/meta_shop_panel.gd) a translucent “glassy neon” look — reusing the start menu’s existing drifting glow-blob backdrop and making card fills genuinely translucent — without touching any other menu in the game.

Architecture: Extract StartMenu’s private _Backdrop inner class into a standalone, reusable ui/neon_backdrop.gd (class_name NeonBackdrop). Point both StartMenu and MetaShopPanel at it. Then lower the alpha in MetaShopPanel._card_box()’s bg_color so cards read as translucent glass over that backdrop instead of solid boxes.

Tech Stack: Godot 4.6.3 / GDScript, GUT 9.6.0 test framework.

  • Scope is the shop panel ONLY (ui/meta_shop_panel.gd). Do not modify the start menu’s visual look, its “no glassmorphism” comment (ui/start_menu.gd:10-11), the pause menu, level-up picker, results screen, or any other overlay (codex, weapon detail, drone bay, ship config).
  • No real backdrop-blur shader (SCREEN_TEXTURE). The glass look comes from genuine alpha translucency over the existing soft glow-blob backdrop — nothing else.
  • ui/neon_backdrop.gd’s extracted logic must be byte-identical in behavior to the current StartMenu._Backdrop — this is a pure extraction, not a redesign.
  • This is a render/UI-only change touching no /sim files. The determinism baseline (snapshot_string().hash() = 2730172591, state_checksum() = 4075578713, per tests/test_determinism_checksum.gd) must remain byte-identical — re-verify, don’t just assume.
  • After adding the new ui/neon_backdrop.gd class_name file, run godot --headless --path . --import before trusting any boot/test run (stale global-class-cache gotcha — a missing import step can make GUT silently skip a new class’s tests).
  • End-to-end verification for the whole plan: full GUT suite green, scripts/check-test-count.sh passing (protects against a parse error silently dropping a test file), and a boot smoke check (grep "SCRIPT ERROR" on stderr must be empty).

Task 1: Extract NeonBackdrop into its own reusable file

Section titled “Task 1: Extract NeonBackdrop into its own reusable file”

Files:

  • Create: ui/neon_backdrop.gd
  • Modify: ui/start_menu.gd:47-53 (call site in _ready()), ui/start_menu.gd:391-437 (remove the _Backdrop inner class)
  • Test: tests/test_neon_backdrop.gd

Interfaces:

  • Produces: class_name NeonBackdrop extends Control — a Control node that, once added to the tree, self-configures full-rect anchors, absorbs mouse input (mouse_filter = Control.MOUSE_FILTER_STOP), and builds its own children (1 tinted-near-black ColorRect base + 5 drifting additive Sprite2D glow blobs). No public methods or constructor params — NeonBackdrop.new() then add_child() is the entire API, matching how StartMenu already used the private version.

  • Step 1: Write the failing test

Create tests/test_neon_backdrop.gd:

extends GutTest
# NeonBackdrop is the drifting glow-blob backdrop extracted from StartMenu's private
# _Backdrop (2026-07-02) so the shop panel can reuse the same atmosphere. This test locks
# down its structure: a tinted-near-black base ColorRect + 5 additive glow blobs.
func test_creates_base_and_five_blobs() -> void:
var b := NeonBackdrop.new()
add_child_autofree(b)
await get_tree().process_frame
assert_eq(b.get_child_count(), 6, "1 base ColorRect + 5 glow blobs")
assert_true(b.get_child(0) is ColorRect, "first child is the tinted-near-black base")
var blob_count := 0
for c in b.get_children():
if c is Sprite2D:
blob_count += 1
assert_eq(blob_count, 5, "five drifting glow blobs")
func test_absorbs_clicks_behind_it() -> void:
var b := NeonBackdrop.new()
add_child_autofree(b)
await get_tree().process_frame
assert_eq(b.mouse_filter, Control.MOUSE_FILTER_STOP,
"must absorb clicks so whatever's behind (start menu / shop) doesn't get accidental input")
func test_base_is_never_pure_black() -> void:
var b := NeonBackdrop.new()
add_child_autofree(b)
await get_tree().process_frame
var base: ColorRect = b.get_child(0)
assert_gt(base.color.b, base.color.r, "tinted toward blue, never pure #000 (design rule)")
  • Step 2: Run test to verify it fails

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_neon_backdrop.gd -gexit

Note: -gtest= does NOT isolate a single file in this project (it still runs the whole suite) and referencing an undefined class_name is a GDScript PARSE error, not a normal assertion failure — so this does not show as a clean “FAILED” line. Expected: the run output’s Scripts count is one lower than a full baseline run (test_neon_backdrop.gd fails to parse and is silently dropped), or the console shows a parse/compile error mentioning NeonBackdrop. Either observation confirms the test doesn’t exist/pass yet.

  • Step 3: Create ui/neon_backdrop.gd
class_name NeonBackdrop
extends Control
# Living neon backdrop: drifting additive glow blobs over a tinted near-black base.
# Extracted from StartMenu's private _Backdrop (2026-07-02) so MetaShopPanel can reuse
# the same atmosphere instead of a flat dim ColorRect. Logic is byte-identical to the
# original — this file is a pure extraction, not a redesign.
var _blobs: Array[Sprite2D] = []
var _vel: Array[Vector2] = []
var _base_scale: Array[float] = []
var _t: float = 0.0
func _ready() -> void:
set_anchors_preset(Control.PRESET_FULL_RECT)
mouse_filter = Control.MOUSE_FILTER_STOP # absorb clicks behind the menu
var base := ColorRect.new()
base.set_anchors_preset(Control.PRESET_FULL_RECT)
base.color = Color(0.014, 0.02, 0.045) # tinted near-black (toward blue; never pure #000)
base.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(base)
var hues := [
Color(0.2, 0.8, 1.0), Color(1.0, 0.35, 0.72),
Color(0.55, 0.42, 1.0), Color(0.28, 1.0, 0.82), Color(0.2, 0.66, 1.0),
]
for i in range(hues.size()):
var s := Sprite2D.new()
s.texture = GlowTexture.shared()
s.centered = true
var sc := 5.0 + randf() * 4.0
s.scale = Vector2(sc, sc)
s.position = Vector2(randf() * 1400.0, randf() * 820.0)
var c: Color = hues[i]
s.modulate = Color(c.r, c.g, c.b, 0.13)
var mat := CanvasItemMaterial.new()
mat.blend_mode = CanvasItemMaterial.BLEND_MODE_ADD
s.material = mat
add_child(s)
_blobs.append(s)
_vel.append(Vector2(randf_range(-14.0, 14.0), randf_range(-10.0, 10.0)))
_base_scale.append(sc)
func _process(dt: float) -> void:
_t += dt
for i in range(_blobs.size()):
var s := _blobs[i]
s.position += _vel[i] * dt
if s.position.x < -300.0: s.position.x = 1700.0
elif s.position.x > 1700.0: s.position.x = -300.0
if s.position.y < -300.0: s.position.y = 1100.0
elif s.position.y > 1100.0: s.position.y = -300.0
var b := _base_scale[i] * (1.0 + 0.1 * sin(_t * 0.5 + float(i) * 1.7))
s.scale = Vector2(b, b)
  • Step 4: Remove _Backdrop from ui/start_menu.gd and re-point the call site

In ui/start_menu.gd, change (around line 51-52):

# Living neon backdrop (atmosphere) — replaces the flat dim panel.
add_child(_Backdrop.new())

to:

# Living neon backdrop (atmosphere) — replaces the flat dim panel.
add_child(NeonBackdrop.new())

Then delete the entire _Backdrop inner class from ui/start_menu.gd — it starts at the comment # ── Living neon backdrop: drifting additive glow blobs over a tinted near-black base ── (line 391) and runs to the end of the file (line 437). After deletion, the file should end with the _AccentDot class (the accent-dot-at-card-edge helper, unrelated, stays untouched).

  • Step 5: Refresh the class cache

Run: godot --headless --path . --import Expected: exits cleanly (no “Cannot open file” / import errors). This is required any time a new class_name file is added, or GUT can silently skip its tests in a later step.

  • Step 6: Boot smoke check

Run: godot --headless --path . --quit-after 120 2>&1 | grep "SCRIPT ERROR" Expected: empty output (no match). Do NOT wrap this command with timeout (not on macOS PATH in this environment — it silently no-ops).

  • Step 7: Run the new test + the existing start menu suite

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_neon_backdrop.gd -gexit Expected: PASS, 3/3 tests.

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_start_menu.gd -gexit Expected: PASS, all tests green (start menu’s own behavior is unaffected by the extraction).

  • Step 8: Commit
Terminal window
git add ui/neon_backdrop.gd ui/start_menu.gd tests/test_neon_backdrop.gd
git commit -m "$(cat <<'EOF'
refactor(ui): extract StartMenu's glow backdrop into a shared NeonBackdrop
Pure extraction, no behavior change on the start menu -- makes the
drifting glow-blob atmosphere reusable by the shop panel (next task).
EOF
)"

Task 2: Reuse NeonBackdrop behind the shop panel

Section titled “Task 2: Reuse NeonBackdrop behind the shop panel”

Files:

  • Modify: ui/meta_shop_panel.gd:52 (field declaration), ui/meta_shop_panel.gd:71-75 (construction in _ready())
  • Test: tests/test_meta_shop_panel.gd (append)

Interfaces:

  • Consumes: NeonBackdrop (from Task 1) — class_name NeonBackdrop extends Control, NeonBackdrop.new() then add_child().

  • Produces: MetaShopPanel._backdrop: NeonBackdrop — replaces the old MetaShopPanel._dim: ColorRect field. No other code in the file references _dim (verified: it only appears in the field declaration + 4 lines inside _ready()), so this is a clean rename-and-replace.

  • Step 1: Write the failing test

Append to tests/test_meta_shop_panel.gd:

func test_shop_background_is_the_shared_neon_backdrop() -> void:
# 2026-07-02, Chris: "we already have good glowing large orbs in the background [on the
# start menu] which look good... we should use them in the shop too" -- the shop no
# longer uses a flat dim ColorRect, it reuses the same NeonBackdrop as the start menu.
var p := MetaShopPanel.new()
add_child_autofree(p)
await get_tree().process_frame
assert_true(p._backdrop is NeonBackdrop,
"shop reuses the shared drifting glow backdrop, not a flat ColorRect")
assert_eq(p.get_child(0), p._backdrop,
"backdrop is the first child so it renders behind every card/header")
  • Step 2: Run test to verify it fails

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_meta_shop_panel.gd -gexit

Note: p._backdrop doesn’t exist yet (_dim is still the field name), and GDScript treats an undeclared member access as a parse error — so test_meta_shop_panel.gd itself will fail to parse and be dropped from the run, not show a single “FAILED” assertion. Expected: the Scripts count is one lower than the current baseline, or a parse/compile error mentioning _backdrop appears in the console output.

  • Step 3: Replace _dim with _backdrop in ui/meta_shop_panel.gd

Change the field declaration (line 52):

var _dim: ColorRect

to:

var _backdrop: NeonBackdrop

Change the construction block in _ready() (lines 71-75):

_dim = ColorRect.new()
_dim.set_anchors_preset(Control.PRESET_FULL_RECT)
_dim.color = Color(0.0, 0.0, 0.02, 0.72)
_dim.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(_dim)

to:

_backdrop = NeonBackdrop.new()
add_child(_backdrop)

(NeonBackdrop’s own _ready() sets full-rect anchors, the tinted-near-black base, and mouse_filter = MOUSE_FILTER_STOP itself — no need to set them again from the caller.)

  • Step 4: Run the shop panel test suite

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_meta_shop_panel.gd -gexit Expected: PASS, all tests green (including the new one).

  • Step 5: Boot smoke check

Run: godot --headless --path . --quit-after 120 2>&1 | grep "SCRIPT ERROR" Expected: empty output.

  • Step 6: Commit
Terminal window
git add ui/meta_shop_panel.gd tests/test_meta_shop_panel.gd
git commit -m "$(cat <<'EOF'
feat(shop): reuse the start menu's glow backdrop instead of a flat dim
Chris: "we already have good glowing large orbs in the background...
we should use them in the shop too." Replaces MetaShopPanel's flat
near-black ColorRect with the shared NeonBackdrop.
EOF
)"

Files:

  • Modify: ui/meta_shop_panel.gd (add a GLASS_ALPHA const near the existing GOLD/STEEL consts; modify _card_box())
  • Test: tests/test_meta_shop_panel.gd (append)

Interfaces:

  • Produces: MetaShopPanel.GLASS_ALPHA: float (a new const, 0.58) and an updated _card_box(accent: Color, fill: float) -> StyleBoxFlat whose returned StyleBoxFlat.bg_color.a is now GLASS_ALPHA instead of 0.97. Signature and every call site are unchanged — this task only changes what’s inside the function body.

  • Step 1: Write the failing test

Append to tests/test_meta_shop_panel.gd:

func test_card_fill_is_translucent_glass_not_opaque() -> void:
# 2026-07-02: shop cards were ~97% opaque (solid boxes). The "glassy ambient" restyle
# makes the fill translucent so the NeonBackdrop's glow blobs read through the card,
# which is what actually makes it look like glass instead of a flat tinted box.
var p := MetaShopPanel.new()
add_child_autofree(p)
await get_tree().process_frame
var sb: StyleBoxFlat = p._card_box(Color.CYAN, 0.2)
assert_almost_eq(sb.bg_color.a, MetaShopPanel.GLASS_ALPHA, 0.001,
"card fill alpha should be the new glass constant")
assert_lt(sb.bg_color.a, 0.8,
"translucent enough for the backdrop to actually show through")
func test_card_box_still_keeps_its_neon_border_and_glow_shadow() -> void:
# The translucency change must NOT touch the border/shadow -- those already read as
# "neon glow" and are untouched by this restyle (spec: only the fill becomes glass).
var p := MetaShopPanel.new()
add_child_autofree(p)
await get_tree().process_frame
var sb: StyleBoxFlat = p._card_box(Color.CYAN, 0.2)
assert_eq(sb.border_color, Color.CYAN)
assert_eq(sb.border_width_left, 2)
assert_eq(sb.corner_radius_top_left, 12)
assert_almost_eq(sb.shadow_color.a, 0.3, 0.001)
assert_eq(sb.shadow_size, 5)
  • Step 2: Run test to verify it fails

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_meta_shop_panel.gd -gexit

Note: MetaShopPanel.GLASS_ALPHA doesn’t exist yet, which is again a parse error (not a normal assertion failure) — test_meta_shop_panel.gd will fail to parse and drop out of the run. Expected: the Scripts count is one lower than the current baseline, or a parse/compile error mentioning GLASS_ALPHA appears in the console output.

  • Step 3: Add the const and update _card_box()

Add near the existing GOLD/STEEL consts (around line 49-50 in ui/meta_shop_panel.gd):

const GOLD := Color(1.0, 0.84, 0.3)
const STEEL := Color(0.55, 0.8, 1.0)
const GLASS_ALPHA := 0.58 # 2026-07-02 "glassy ambient" restyle: translucent card fill so the
# NeonBackdrop's glow blobs read through (was 0.97, near-opaque)

Change _card_box() (currently at the end of the file):

func _card_box(accent: Color, fill: float) -> StyleBoxFlat:
var sb: StyleBoxFlat = StyleBoxFlat.new()
sb.bg_color = Color(0.04, 0.05, 0.09, 0.97).lerp(
Color(accent.r, accent.g, accent.b, 0.97), fill)
sb.set_border_width_all(2)
sb.border_color = accent
sb.set_corner_radius_all(12)
sb.shadow_color = Color(accent.r, accent.g, accent.b, 0.3)
sb.shadow_size = 5
return sb

to:

func _card_box(accent: Color, fill: float) -> StyleBoxFlat:
var sb: StyleBoxFlat = StyleBoxFlat.new()
sb.bg_color = Color(0.04, 0.05, 0.09, GLASS_ALPHA).lerp(
Color(accent.r, accent.g, accent.b, GLASS_ALPHA), fill)
sb.set_border_width_all(2)
sb.border_color = accent
sb.set_corner_radius_all(12)
sb.shadow_color = Color(accent.r, accent.g, accent.b, 0.3)
sb.shadow_size = 5
return sb
  • Step 4: Run the shop panel test suite

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_meta_shop_panel.gd -gexit Expected: PASS, all tests green (including both new ones).

  • Step 5: Boot smoke check

Run: godot --headless --path . --quit-after 120 2>&1 | grep "SCRIPT ERROR" Expected: empty output.

  • Step 6: Commit
Terminal window
git add ui/meta_shop_panel.gd tests/test_meta_shop_panel.gd
git commit -m "$(cat <<'EOF'
feat(shop): translucent glass card fill (was ~97% opaque)
Cards now let the NeonBackdrop's glow blobs read through the fill --
the border and accent-tinted glow shadow are untouched, they already
read as neon; the fill just needed to be translucent to look like glass.
EOF
)"

Task 4: Full verification pass + visual confirmation

Section titled “Task 4: Full verification pass + visual confirmation”

Files:

  • Create (temporary, deleted at the end of this task): marketing/capture/_shot.gd, marketing/capture/_shot.tscn
  • No permanent file changes — this task is verification only.

Interfaces:

  • Consumes: everything from Tasks 1-3 (NeonBackdrop, MetaShopPanel._backdrop, MetaShopPanel.GLASS_ALPHA).

  • Produces: nothing permanent — a screenshot for human/visual review plus a green full-suite run.

  • Step 1: Full test suite

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gdir=res://tests -ginclude_subdirs -gexit Expected: ---- All tests passed! ----, Scripts count is 3 higher than the pre-plan baseline (188 + test_neon_backdrop.gd = 189; confirm the exact pre-plan count via git log if unsure).

  • Step 2: Test-count guard

Run: bash scripts/check-test-count.sh Expected: test-count guard OK: <N>/<N> test scripts ran. with no mismatch.

  • Step 3: Determinism baseline check

Run: godot --headless --path . -s res://addons/gut/gut_cmdln.gd -gtest=res://tests/test_determinism_checksum.gd -gexit Expected: PASS. This change touches no /sim files, so the baseline (snapshot_string().hash() = 2730172591, state_checksum() = 4075578713) must be byte-identical to before this plan started — a failure here means something leaked outside ui/, which would be a bug in an earlier task, not expected drift.

  • Step 4: Boot smoke check

Run: godot --headless --path . --quit-after 120 2>&1 | grep "SCRIPT ERROR" Expected: empty output.

  • Step 5: Visual verification via a temporary capture harness

Create marketing/capture/_shot.gd:

extends Node
# TEMPORARY verification harness for the shop glassy-neon restyle -- delete after use.
# Renders MetaShopPanel alone into an off-screen SubViewport and saves a PNG.
func _ready() -> void:
var vp := SubViewport.new()
vp.size = Vector2i(1280, 720)
vp.render_target_update_mode = SubViewport.UPDATE_ALWAYS
add_child(vp)
var content := ContentLoader.load_from_path("res://data/bible.json")
var meta := MetaState.new()
var panel := MetaShopPanel.new()
vp.add_child(panel)
await get_tree().process_frame
panel.open_shop(meta, content.meta_upgrades(), func() -> void: pass)
# The card entrance tween is staggered (~i*0.04 + 0.18s per card) -- for a 4-card root
# view that finishes around ~0.5-0.7s. Wait generously so the shot isn't mid-fade.
for i in 90:
await get_tree().process_frame
await RenderingServer.frame_post_draw
await RenderingServer.frame_post_draw
var img := vp.get_texture().get_image()
var dir := ProjectSettings.globalize_path("res://marketing/raw")
DirAccess.make_dir_recursive_absolute(dir)
img.save_png(dir.path_join("shop_root_glassy.png"))
print("[shot] saved shop_root_glassy.png")
get_tree().quit()

Create marketing/capture/_shot.tscn (a bare scene whose root node script is _shot.gd) — easiest done in-editor (Scene > New Scene > Other Node > Node, attach _shot.gd, save as marketing/capture/_shot.tscn), or headlessly:

Terminal window
cat > /tmp/make_shot_scene.gd << 'EOF'
extends SceneTree
func _init():
var n := Node.new()
n.set_script(load("res://marketing/capture/_shot.gd"))
var packed := PackedScene.new()
packed.pack(n)
ResourceSaver.save(packed, "res://marketing/capture/_shot.tscn")
quit()
EOF
godot --headless --path . -s /tmp/make_shot_scene.gd

Run it and inspect the output:

Terminal window
godot --headless --path . marketing/capture/_shot.tscn

Expected: [shot] saved shop_root_glassy.png printed, and marketing/raw/shop_root_glassy.png exists. Open it (as a file:// URL) and visually confirm: cards show the drifting glow blobs translucently through their fill, the border

  • glow shadow are still clearly neon, and text/icons stay legible over the busier backdrop. If it doesn’t look right, this is the point to go back and adjust GLASS_ALPHA in Task 3 (re-run Task 3’s tests after any change) before continuing.
  • Step 6: Delete the temporary harness
Terminal window
rm -f marketing/capture/_shot.gd marketing/capture/_shot.gd.uid marketing/capture/_shot.tscn
rm -f marketing/raw/shop_root_glassy.png # verification artifact, not a committed asset

Confirm nothing was accidentally staged:

Terminal window
git status --short

Expected: no _shot.* or shop_root_glassy.png entries (they were never committed, this is just confirming the temp files are gone from the working tree too).

  • Step 7: Final confirmation commit (if Step 5 required a GLASS_ALPHA tweak)

If Step 5’s visual check required changing GLASS_ALPHA, re-run Task 3 Steps 4-6 (test, boot check, commit the new value) before finishing this task. If no change was needed, there is nothing to commit here — Tasks 1-3’s commits already cover the whole plan.