Skip to content

Archive Reactions Mode + Rename Crystals Ruleset to Survival — design

Archive Reactions Mode + Rename Crystals Ruleset to Survival — design

Section titled “Archive Reactions Mode + Rename Crystals Ruleset to Survival — design”

Date: 2026-07-09 Status: approved (design), pre-implementation Determinism baseline at design time: snapshot_string().hash()=4217109746, state_checksum()=3312602286 → read the literal pinned assertion in tests/test_determinism_checksum.gd / test_determinism_crystals.gd, not this note. (Note: test_determinism_checksum.gd itself is archived by this change — see below — so post-archival, test_determinism_crystals.gd alone is the surviving source of truth, and it gets renamed too.)

This was originally scoped as a prerequisite for a separate, larger P2 (co-op) upgrade-pipeline design — that work is paused until this lands, so it targets the final (renamed, simplified) code instead of code that’s about to move.

The game currently has two run rulesets on Sim: RULESET_REACTIONS (the original mode — reactions/elemental combos, ui/level_up_panel.gd for level-up cards) and RULESET_CRYSTALS (reads elemental crystals from kills to grant weapon progression via sim/weapon_thresholds.gd, ui/crystals_levelup_panel.gd for level-up cards). ui/start_menu.gd’s V01_CRYSTALS_ONLY = true already restricts the live menu to crystals mode only — reactions mode is not reachable by a player today, though its code still fully exists, runs, and is tested.

Chris: crystals mode is effectively the whole game now, and the “crystals” qualifier no longer makes sense to keep as the mode’s identity (the elemental crystal currency itself keeps its name — only the ruleset’s identity is being renamed). He’s not sure reactions mode will ever be revived, but wants it preserved somewhere reachable rather than deleted outright.

Naming correction from initial framing: there is no RULESET_SURVIVAL in the codebase — the old ruleset is internally RULESET_REACTIONS. The display label “SURVIVAL” is already shown for crystals mode in the live (v0.1-gated) menu today — the underlying identifiers just haven’t caught up to that yet.

In scope:

  • Archive RULESET_REACTIONS-exclusive code (files that move whole + code extracted from shared files) into _archive/reactions-mode/, with a short README.
  • Flip Sim.new()’s default ruleset from reactions to the renamed survival ruleset.
  • Rename RULESET_CRYSTALS and its identifiers (constant, method, UI class, file names, main.gd’s _mode string, the menu card id, telemetry label, and every test file that’s really about the ruleset’s identity rather than the crystal currency) to reclaim “survival.”
  • Tag the pre-archival commit for safety.

Out of scope:

  • Collapsing the ruleset field/enum itself. Chris’s call: keep the field, even though only one value (the renamed survival ruleset) is reachable going forward — a hook for a hypothetical future third mode. Concretely: both RULESET_REACTIONS and RULESET_SURVIVAL (renamed from RULESET_CRYSTALS) stay defined as named constants — RULESET_REACTIONS becomes unreachable in practice (nothing ever sets ruleset to it once the default flips, and archival deletes the code that used to run under it), but the name itself stays as a documented placeholder for “the retired mode,” not deleted from the enum. This is deliberately the smaller of the two options considered — it preserves the field/enum shape Chris asked to keep without also deciding whether to prune now-effectively-dead branches like if sim.ruleset == sim.RULESET_SURVIVAL: checks that no longer have a meaningful else — those stay as literal (now-always-true) checks rather than being unwound, since archival already removed everything they used to guard against.
  • The elemental crystal currency (sim/crystal_state.gd’s CrystalState, crystals.counts, WeaponThresholds, tests/test_crystal_state.gd, tests/test_crystal_endless_tuning.gd) — stays named “crystal.” This is a real in-game collectible, not the ruleset’s name, and must not be touched.
  • The P2 co-op upgrade-pipeline work this was originally blocking — resumes as its own design once this lands.
  • .claude/worktrees/networked-multiplayer-mb-mc’s own copy of this code (the unmerged M-B/M-C branch) — out of scope until that branch is separately reconciled against main.

Two shapes of archived code, since some lives in dedicated files and some is intermixed in shared files that also contain code that stays:

Whole files, moved via git mv into _archive/reactions-mode/:

  • ui/level_up_panel.gd (the LevelUpPanel class — reactions-mode’s level-up card UI)
  • tests/test_level_up_panel.gd
  • tests/test_determinism_checksum.gd (pins the reactions-mode baseline specifically — no longer meaningful once that mode’s behavior is gone)

Code extracted from shared files as a reference copy (.gd.txt, so it’s inert, not live GDScript), then deleted from the live file:

  • sim/elemental_system.gd: the reaction path gated behind ruleset != RULESET_CRYSTALS in apply_element()Elemental.apply, on_reaction, reaction_burst, priming/pop_primed, terrain zones — plus the reaction-only tuning consts (REACTION_BURST_RADIUS, GENERIC_REACTION_RADIUS, GENERIC_REACTION_MAGNITUDE, REACTION_DAMAGE_SCALE, REACTION_COOLDOWN).
  • sim/upgrade_system.gd: the AIM_MODS-as-level-up-cards path (offered only when ruleset != RULESET_CRYSTALS — confirmed via code read, corrects an earlier assumption that this was crystals-exclusive) and the CRYSTALS_DEAD_EFFECTS gating logic that exists specifically to hide reaction-only mods from crystals-mode rolls (becomes dead once those mods’ own gating context is gone).
  • main.gd: the _mode == "survival" (reactions) branch and its handling.
  • ui/start_menu.gd: the already-menu-hidden ["survival", "SURVIVAL", "Endless waves, score attack", …] card (removing this changes no live behavior — it’s unreachable behind V01_CRYSTALS_ONLY today).

_archive/reactions-mode/README.md explains what’s there, which live file each piece came from, and that it’s inert reference material (not wired into any build).

Default ruleset flip: Sim.new() currently defaults ruleset to RULESET_REACTIONS (sim/sim.gd:329). Once reactions’ behavior is deleted, every Sim.new(...) call that doesn’t explicitly call enable_crystals()/enable_survival() would otherwise silently exercise a now-incomplete path — so the default flips to the renamed survival ruleset as part of this same step. This is exactly the kind of change that could silently break tests that relied on the old default without touching ruleset code at all — the full suite run after this step specifically watches for that, not just the tests already known to be reactions-specific.

Safety net: git tag pre-reactions-archive-2026-07-09 on the commit immediately before archival begins, in addition to the _archive/ copy itself.

4. Rename mechanics — ordered, to avoid the _mode string collision

Section titled “4. Rename mechanics — ordered, to avoid the _mode string collision”

The research surfaced a real landmine: main.gd‘s _mode is a bare string, not tied to the ruleset enum, and the OLD reactions-mode menu card also used _mode = "survival" (just menu-hidden, not removed). Renaming crystals’ identifiers to “survival” is only collision-safe once that old pathway is truly deleted — so archival (step 3) must complete before any renaming below begins, not run concurrently with it.

  1. RULESET_CRYSTALS (const on Sim) → RULESET_SURVIVAL. Sim.enable_crystals()enable_survival().
  2. ui/crystals_levelup_panel.gd’s class CrystalsLevelUpPanelLevelUpPanel, file renamed to ui/level_up_panel.gd — reusing the exact name/path the archived old panel vacates in step 3.
  3. main.gd: the remaining _mode == "crystals" branch relabels to _mode == "survival" (the old reactions branch using that same string is already gone by this point).
  4. ui/start_menu.gd: the surviving crystals menu card’s id becomes "survival" to match.
  5. net/gameplay_telemetry.gd’s "ruleset": "crystals" if … else "reactions" telemetry label → "survival" if … else "reactions" (the "reactions" label stays, matching the kept-but-unreachable enum value).
  6. Test file renames — ruleset-identity tests get renamed to match (reusing names vacated by step 3’s archival where applicable); currency tests keep “crystal” in their name:
    • tests/test_crystals_ruleset.gdtests/test_survival_ruleset.gd
    • tests/test_crystals_levelup_panel.gdtests/test_level_up_panel.gd (vacated by archival)
    • tests/test_crystals_manual_progression.gdtests/test_survival_manual_progression.gd
    • tests/test_determinism_crystals.gdtests/test_determinism_survival.gd (becomes THE determinism baseline test, now that the reactions-mode one is archived)
    • tests/test_crystal_state.gd, tests/test_crystal_endless_tuning.gdunchanged, these test the currency
  • CLAUDE.md “Current status” — bullets describing “crystals (PRIMARY) mode” / the survival-vs-crystals distinction get reworded to reflect there’s now one mode.
  • .claude/skills/bh-add-content/SKILL.md — explicitly documents the survival/crystals wiring distinction being eliminated; must update in lockstep (it’s the canonical guidance future work reads).
  • .claude/skills/bh-determinism-repin/SKILL.md — references both determinism test files by their old names/roles.
  • docs/architecture/weapons-and-buildcraft.md, docs/architecture/meta-and-story.md — both currently describe the dual-ruleset split; reworded once there’s one.
  • docs/ROADMAP.md — checked during implementation (not read during this design pass).
  • Memory bullet-heaven-crystals-is-primary-mode — superseded by this change once it lands; updated or retired.

Left alone deliberately: docs/superpowers/specs|plans/*.md (historical record of past work, not live documentation — renaming these would falsify history) and every other “survival” usage that means the run/wave game mode vs. story mode (docs/architecture/enemies-bosses-survival.md, ai/auto_player.gd, ui/hud.gd, etc.) — those are a different, orthogonal use of the word and are out of scope here.

Done in the isolated worktree (.claude/worktrees/reactions-archive-rename, branch reactions-archive-rename), bh-dev-chunk discipline throughout:

  1. After the archival step (section 3): full suite (-gdir=res://tests -ginclude_subdirs -gexit) + bash scripts/check-test-count.sh — specifically watching for any test that implicitly relied on the old default ruleset, not just the tests already known to be reactions-exclusive. Commit.
  2. After the rename step (section 4): full suite again, test_determinism_survival.gd (renamed from crystals) re-verified against the pinned baseline, check-test-count.sh, a headless boot smoke test (--quit-after 60, grep for SCRIPT ERROR). Commit.
  3. Land back on main via a guarded --ff-only update once main is confirmed quiet (no uncommitted changes, no recent unrelated commits) — same landing pattern used for the 8-Dimensions merge and Ship Classes.