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.
Global Constraints
Section titled “Global Constraints”- 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 currentStartMenu._Backdrop— this is a pure extraction, not a redesign.- This is a render/UI-only change touching no
/simfiles. The determinism baseline (snapshot_string().hash() = 2730172591,state_checksum() = 4075578713, pertests/test_determinism_checksum.gd) must remain byte-identical — re-verify, don’t just assume. - After adding the new
ui/neon_backdrop.gdclass_namefile, rungodot --headless --path . --importbefore 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.shpassing (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_Backdropinner class) - Test:
tests/test_neon_backdrop.gd
Interfaces:
-
Produces:
class_name NeonBackdrop extends Control— aControlnode 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-blackColorRectbase + 5 drifting additiveSprite2Dglow blobs). No public methods or constructor params —NeonBackdrop.new()thenadd_child()is the entire API, matching howStartMenualready 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 NeonBackdropextends 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
_Backdropfromui/start_menu.gdand 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
git add ui/neon_backdrop.gd ui/start_menu.gd tests/test_neon_backdrop.gdgit 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 thedrifting 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()thenadd_child(). -
Produces:
MetaShopPanel._backdrop: NeonBackdrop— replaces the oldMetaShopPanel._dim: ColorRectfield. 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
_dimwith_backdropinui/meta_shop_panel.gd
Change the field declaration (line 52):
var _dim: ColorRectto:
var _backdrop: NeonBackdropChange 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
git add ui/meta_shop_panel.gd tests/test_meta_shop_panel.gdgit 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 flatnear-black ColorRect with the shared NeonBackdrop.EOF)"Task 3: Make shop cards translucent glass
Section titled “Task 3: Make shop cards translucent glass”Files:
- Modify:
ui/meta_shop_panel.gd(add aGLASS_ALPHAconst near the existingGOLD/STEELconsts; 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) -> StyleBoxFlatwhose returnedStyleBoxFlat.bg_color.ais nowGLASS_ALPHAinstead of0.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 sbto:
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
git add ui/meta_shop_panel.gd tests/test_meta_shop_panel.gdgit 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 alreadyread 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:
cat > /tmp/make_shot_scene.gd << 'EOF'extends SceneTreefunc _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()EOFgodot --headless --path . -s /tmp/make_shot_scene.gdRun it and inspect the output:
godot --headless --path . marketing/capture/_shot.tscnExpected: [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_ALPHAin Task 3 (re-run Task 3’s tests after any change) before continuing.
- Step 6: Delete the temporary harness
rm -f marketing/capture/_shot.gd marketing/capture/_shot.gd.uid marketing/capture/_shot.tscnrm -f marketing/raw/shop_root_glassy.png # verification artifact, not a committed assetConfirm nothing was accidentally staged:
git status --shortExpected: 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_ALPHAtweak)
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.