Skip to content

Reset Shop (respec) — design

Date: 2026-07-05 Status: approved (design), pre-implementation Determinism: unaffected. MetaState is meta-progression, applied to a fresh run’s player render-side at run start (apply_to) — the same category as buy()/from_dict(), never called from inside Sim.tick(). No baseline re-pin needed.

Add a player-facing “Reset Shop” button to the meta shop: a full respec that refunds every gold coin ever spent (stat levels AND one-time unlocks — weapons, the Obsidian hull, decoy flavours, drone classes), clears all of it back to zero, and reverts the currently-selected ship/decoy/drone-loadout to their starter defaults (since whatever was selected may no longer be owned after the wipe). Requires a confirmation dialog first — this is a real, irreversible loss of purchased content, not a cosmetic toggle.

Confirmed with Chris:

  • Respec, not a full save wipe — seen_enemies (bestiary codex) and tutorial_done are untouched; they aren’t shop purchases.
  • Refunds EVERYTHING (stats and unlocks), not stats-only.
  • Real player-facing button in the shop UI, not gated behind dev-tools.
  • Requires a confirm/cancel step before it happens.
  • Per-item respec (refunding a single upgrade line only) — this is a full reset, not selective.
  • Any “are you sure, really?” second confirmation — one confirm dialog is enough.
  • Undo — once confirmed, it’s saved immediately, matching how every other shop action already saves immediately (MetaStore.save_state after buy()).

New method, mirroring the existing apply_to(player, defs) / purchased_unlocks(defs) pattern of taking the meta-upgrade defs as a parameter (pure logic, no ContentDB reference held on MetaState itself):

# Full respec: refunds every gold coin ever spent (stats AND unlocks) and clears all of it back
# to zero. Reverts ship/decoy/drone-loadout selection to starter defaults, since whatever was
# selected may no longer be owned after the wipe. Does NOT touch seen_enemies/tutorial_done --
# those aren't shop purchases.
func reset_shop(defs: Array) -> void:
var refund := 0
for def in defs:
if not (def is Dictionary):
continue
var lvl := level_of(def["id"])
var growth: float = float(def.get("cost_growth", 1.5))
var base: float = float(def["base_cost"])
for i in range(lvl):
refund += int(round(base * pow(growth, i)))
banked_gold += refund
levels = {}
selected_ship = ShipBonuses.DEFAULT_SHIP
selected_decoy = "basic"
drone_loadout = ["sentinel"]

The refund loop reuses cost()’s exact formula (base_cost * cost_growth^level) evaluated at each already-owned level, so the refund is always exactly what was spent — not a flat “give back some gold” heuristic. This makes it naturally correct if per-upgrade costs are rebalanced later (no separate “spent” ledger to keep in sync).

Render side (wherever the shop calls buy() today) calls MetaStore.save_state(_meta) immediately after reset_shop(), matching the existing buy-then-save pattern — the reset is durable right away, not deferred to the next natural save point.

  • A “RESET SHOP” button on the shop’s root view (near the banked-gold header), in a distinct warning color (not the shop’s normal cyan/gold palette) so it reads as a different KIND of action from browsing categories.

  • Pressing it opens a new confirmation overlay (no such pattern exists anywhere in this codebase yet, so this introduces the first one): a dimmed full-screen backdrop behind a centered neon-styled panel (matching the existing Panel + StyleBoxFlat convention used throughout the shop), reading:

    RESET SHOP? Refunds all gold spent and clears every stat level and unlock — including owned ships, weapons, and drones. This cannot be undone.

    with CONFIRM / CANCEL buttons.

  • CONFIRM_meta.reset_shop(defs)MetaStore.save_state(_meta) → close the dialog → _rebuild_current() (the panel’s existing full-refresh path, already used after every purchase) so the gold display and every category’s levels/costs reflect the reset immediately.

  • CANCEL → close the dialog, no state change at all.

  • Confirm/Cancel are ordinary focusable Buttons — Godot’s built-in Control focus system already handles d-pad/arrow-key movement between them and controller-A/Enter to activate, the same as every other button in this panel. No bespoke nav state machine needed for a 2-button dialog.

  • The confirm overlay is implemented inline in meta_shop_panel.gd (not a new reusable ConfirmDialog class) — there is exactly one caller today, and building a generic reusable component for a single use site is premature; if a second confirm-dialog need shows up later, extract one then.

tests/test_meta_state.gd:

  • reset_shop refunds exactly the sum of what was spent (buy a few levels of a couple of upgrades at known costs, call reset_shop, assert banked_gold matches the pre-purchase balance).
  • levels is empty afterward.
  • selected_ship/selected_decoy/drone_loadout revert to their defaults, even when a purchased-and-selected ship/decoy/drone class had been active beforehand.
  • seen_enemies and tutorial_done are untouched by reset_shop.

tests/test_meta_shop_panel.gd:

  • The RESET SHOP button exists on the root view.
  • Pressing it opens the confirm dialog without changing any state yet.
  • CANCEL closes the dialog with _meta completely unchanged.
  • CONFIRM resets _meta, persists it, closes the dialog, and the panel’s displayed gold/levels reflect the reset (i.e. _rebuild_current() ran).