Skip to content

Crystals full-screen level-up panel

Status: DESIGN APPROVED 2026-06-26 — not yet implemented. Crystals-mode only. Author: Opus.

Replace the compact level-up card row, IN CRYSTALS MODE ONLY, with a full-screen decision screen: upgrade choices on the left, the player’s full build/inventory on the right, a complete change-preview for the focused choice, and a best→worst ranking — so the player makes an informed pick fast and Chris can see at a glance whether the crystals build system is working. After a pick, a 0.5s “get ready” freeze (with brief i-frames) lets the player re-orient before live play resumes.

  • Crystals ruleset only (sim.ruleset == RULESET_CRYSTALS). Reactions/story/survival keep the existing LevelUpPanel unchanged.
  • Out (YAGNI): no change to WHICH upgrades are offered (still the Task-4 roll); no mouse — “hover” is controller/remote FOCUS; no animation polish beyond the freeze; no persistence.

Sim.upgrade_effects(id: String) -> Dictionary (NEW, pure /sim, read-only)

Section titled “Sim.upgrade_effects(id: String) -> Dictionary (NEW, pure /sim, read-only)”

The testable core. Given an offered upgrade id, returns its full change-set WITHOUT mutating any state:

{
"kind": "crystal" | "weapon" | "stat" | "mod",
"headline": String, # e.g. "+4 Fire, +2 Light"
"changes": [ {"target": String, "detail": String, "evolve": bool}, ... ],
"dead": bool, # true if it does nothing useful in crystals (e.g. a reaction-buff mod)
"score": float, # impact score for ranking
}

Per kind:

  • crystal grant (crystals:fire=4,light=2): simulate wallet' = wallet + bundle; for every OWNED weapon, diff “threshold rules met under wallet’ vs wallet” → each NEWLY-crossed rule is a change (target = weapon name, detail = the mod kind or “EVOLVE”, evolve = rule is an evolve). Also a “progress” change per affected element (“Fire 7→11, next: Tesla evolve @18”). Pure: builds a temp count map, never touches Sim.crystals or the weapons.
  • weapon grant (weapon:X): change list = “Add Weapon X” + the thresholds X would IMMEDIATELY inherit from the current wallet (grant_weapon back-applies them — Task 5).
  • stat mod / transformative mod: reuse the existing upgrade_preview(id) now→after; for a transformative mod whose effect is inert in crystals mode (reaction buffs — Catalyst/Lingering), set dead = true and say so.

Helper: a pure WeaponThresholds/Sim function rules_met(weapon_id, count_map) -> Array[int] (indices of rules satisfied by a given count map) so both the live _eval_thresholds and the preview diff share one definition (DRY).

Scoring / ranking (diagnostic — “for now”)

Section titled “Scoring / ranking (diagnostic — “for now”)”

score in upgrade_effects: evolutions weighted highest (e.g. +100 each), then +20 per newly-crossed non-evolve threshold, weapon grant = +15 (free slot) + inherited-threshold value, stat mod = its magnitude × a per-stat weight, dead picks = 0. The panel sorts choices by score and labels them BEST → GOOD → OK → WEAK (or 1..N). Transparent (the change list justifies the rank) so the system’s reasoning is auditable. This is a temporary evaluation aid, easily removed later.

ui/crystals_levelup_panel.gd (NEW, render/UI CanvasLayer)

Section titled “ui/crystals_levelup_panel.gd (NEW, render/UI CanvasLayer)”
  • Full-screen, dims the game behind. Two columns:
    • Left: the N offered choices as a vertical, focus-navigable card list; each card shows name + one-line headline + a rank badge. The focused card is the “hovered” one.
    • Right: the live build readout — each owned weapon with level (count of wm:-equivalent threshold upgrades applied) + evolved flag, the full player stat block, the crystal wallet (per-element counts), and the list of upgrades taken this run.
  • On focus change → render the focused choice’s changes (the full preview) in a detail area.
  • signal chosen(id). Controller nav mirrors LevelUpPanel (stick/d-pad + ui_accept/JOY_BUTTON_A, debounced); the Apple TV Siri Remote works as a joypad.
  • _open_levelup(): if crystals mode, build choices as {id, effects = sim.upgrade_effects(id)} and show the crystals panel; else the existing panel. Keep _paused_for_levelup semantics.
  • _on_upgrade_chosen(id): sim.apply_upgrade(id); hide panel; if more levels pending, re-open; else start the resume grace: _resume_grace = 0.5, show a “READY…” beat, and grant the player a short invuln window (via the existing i-frame mechanism) so re-orientation is safe.
  • _process: while _resume_grace > 0, decrement by real delta and DO NOT tick the sim (frozen beat); when it hits 0, resume normal ticking with live input_router.poll(). (No input is replayed — the player re-grips during the freeze, per the chosen resume model.)

upgrade_effects is pure + read-only → no state change. The panel is render/UI. The resume grace + the crystals i-frame window are set only by main in crystals mode, which the determinism test never enters → reactions baseline byte-identical; crystals determinism trace unaffected.

  • upgrade_effects (pure) is unit-tested: crystal-grant cascade lists the correct newly-crossed thresholds (incl. an evolve at a high count); weapon-grant lists inherited thresholds; a reaction-buff mod is flagged dead; ranking order is correct for a constructed wallet (best = the evolve-crossing crystal, worst = the dead mod).
  • rules_met pure helper unit-tested (met/unmet boundaries).
  • The panel + freeze/i-frame resume are render/UI → boot-check + playtest (no GUT seam).
  • Exact score weights (above are starting values — tune so the BEST label matches intuition).
  • Invuln window length (start 0.5s, matching the freeze).
  • Right-column density on the tvOS safe-area (overscan) — keep within the safe rect.