Skip to content

Level-up reroll + skip (design, 2026-10-20)

GitHub issue #77. Target: 1.2. The 1.1 feature freeze was 2026-10-17, and docs/RELEASE-1.1.md §3 already lists “level-up reroll/skip” as a 1.2 candidate. This ships as a feature players can see in a reviewed store version. No remote flag turns it on for store players (Apple 2.3.1; see the CLAUDE.md Builds & releases rule).

Implementation plan: docs/superpowers/plans/2026-10-20-levelup-reroll-skip.md.

  • Rerolls come from the meta shop. One shop item with three tiers that give 1 / 2 / 3 rerolls per run. This follows the balance philosophy: power grows across runs through the shop, never within a run.
  • Skip is always there and free. A skip refunds a little XP so it is not a pure loss. The refund is a live tunable.

Everything below that is not in this section was decided by the spec author. Those calls are collected in §10, “Decisions for Chris’s veto”.

The level-up panel (ui/level_up_panel.gd, the 1.1 big glass cards) gets two actions next to CHOOSE and SHIP STATS:

Action Pad (TV, Mac, iOS with a pad) Keyboard (Mac) Touch (iPhone/iPad)
Reroll X / Square: one press R REROLL button, bottom-left
Skip B / Circle: press twice within LevelUpPanel.SKIP_CONFIRM_S (1.5 s) Backspace, twice SKIP button, tap twice
  • Reroll replaces the three cards with a new roll. It is shown only when the save owns at least one Reroll tier. The label carries what is left: REROLL (2) in the hint row, REROLL 2 on the touch button. With 0 left the prompt stays (reading (0)), and a press flashes the hint and does nothing.
  • Skip gives up this level’s card and refunds UpgradeSystem.SKIP_XP_REFUND_FRAC (0.25) of the next level’s XP bar. The label reads SKIP +25% XP. It is computed from the live tunable at display time, so a remote patch never leaves stale copy. The first press arms it, and the label changes to PRESS AGAIN TO SKIP. A second press inside the window confirms. If the window runs out, the skip disarms and the label goes back. Two presses are required because B is right next to A, and a B that went straight through on its own would throw away a level.
  • Neither action is accepted during the card entry animation (_accepting, the same guard that card taps already use), so mashing a button as the panel opens cannot spend a reroll or skip a level.
  • While SHIP STATS is open, both actions are off. The prompts and buttons hide, and B keeps its current job of closing the sheet. This keeps the hint row short while the sheet adds its WEAPON and scroll prompts, and it stops a reroll from replacing the card being previewed.
  • Siri Remote / tvOS focus. The panel does not use Godot’s focus ring. It handles pad events itself in _input, and on tvOS a pad is always present at a level-up (CLAUDE.md UI rule). Reroll and skip are face buttons, not focusable widgets. D-pad left/right still cycles only the cards, and nothing new joins the focus order.
  • Touch. REROLL and SKIP are 44 pt glass buttons at the bottom-left inside the safe area. They sit on the same baseline as the existing STATS button at the bottom-right, so the card strip’s bottom clearance is unchanged. While armed, the SKIP button reads TAP AGAIN. On touch the hint row is hidden by ButtonPrompt (style "none"), so these buttons are the only way to reroll or skip there. When a pad is connected to an iPhone or iPad, the hint row shows again, but it leaves out the X / B parts: the buttons already carry those actions, and a five-part row centred at the bottom would run under them on the 985-unit iPad.
  • Mac with a mouse. There are no clickable buttons, which matches STATS (touch-only today). The keycap prompts R REROLL / BKSP SKIP show instead.
Mode Reroll Skip Why
Survival (Normal, Hard) if owned yes the main case
Story (“Ascent”) if owned yes the shop applies there (MetaRunSetup.apply)
Tutorial no no lessons force specific offers (TutorialLessons.forced_offer, crystal-only lesson 8)
App Clip no no curated showpiece offers (ClipDirector.roll_offers), no shop
Networked co-op (LAN / relay) no no v1: the actions are not SimCommands yet (§7)
Legacy panel (VisualLook.glass_ui = false, Dev fallback) no no LevelUpBase.set_actions is a no-op there

Fresh-save runs (learning runs, human-replay recordings, the App Clip) get meta_rerolls = 0 through MetaRunSetup.apply on a MetaState.new(), so the reroll prompt never appears in them. Skip still works in them.

One new meta_upgrades entry in data/bible.json, added by hand-editing the file (python json round-trip, tab indent) as docs/architecture/content-and-elemental.md requires:

{"id": "levelup-reroll", "archetypes": ["utility"], "name": "Reroll", "effect": "levelup_rerolls",
"magnitude": 1, "max_level": 3, "base_cost": 150, "cost_growth": 2.0,
"desc": "+{mag} level-up reroll per run, per level", "category": "Utility"}
  • Prices: 150 / 300 / 600 gold (1,050 total). The catalogue costs 21,693 today, so this is about 4.8% of it. In the 2026-09-27 career curve (docs/research/2026-09-27-shop-economy-first-curve.md), early runs pay 140-400 gold. Tier 1 therefore costs about one good early run. It is priced above every Pilot level-1 item (40-70), so core survival stats still come first, and level with the cheaper Arsenal unlocks (100-300). Tier 3 (600) lands in mid-career, below the 800-gold Obsidian cruiser. base_cost / cost_growth / magnitude are already patchable through the bible overlay (meta_upgrades.*), and the refund ledger covers any later price patch (docs/architecture/remote-config.md § Shop prices).
  • Category: Utility. That tab has been empty since Greed moved to Pilot (2026-10-04), and docs/architecture/meta-and-story.md says it “stays in ORDER for future economy items”. The item brings the tab back, and tests/test_shop_categories.gd’s “Utility is hidden while empty” test is rewritten to “Utility shows with the Reroll item” (fix, don’t grandfather).
  • Mechanism. MetaState.rerolls_per_run(defs) returns the sum of magnitude × level over every def with effect levelup_rerolls, rounded. MetaRunSetup.apply_stats writes it to Sim.meta_rerolls, the same pattern as Greed → Sim.meta_gold_mult. StatEffects does not know the effect, so MetaState.apply_to skips it, as it does for gold_mult.
  • Card copy goes through MetaCopy.desc: {mag} is the flat add, so a patched magnitude re-renders.
  • Card icon: a new ShopIcons glyph (two curved arrows) on the effect key levelup_rerolls.

All reroll and skip logic lives in sim/upgrade_system.gd (pure RefCounted). The per-pilot counters live on PlayerState, and the run’s budget lives on Sim.

  • Sim.meta_rerolls: int = 0: an input set at run start. The tick never reads it.
  • PlayerState.rerolls_used: int, PlayerState.levelup_skips: int: counters, kept out of the checksum like upgrade_picks (nothing in the tick reads them). Sim.dev_reset_upgrades resets them.
  • UpgradeSystem.REROLL_ID := "#reroll", SKIP_ID := "#skip": action ids. The # prefix can never collide with a card id (wm:, weapon:, evolve:, crystals:, aim:, bible ids). They go into the replay pick stream and the run timeline.
  • roll_upgrade_choices(sim, pilot, n, exclude: Array[String] = []): the new optional parameter drops each excluded id from every candidate pool (mods, Stasis Web, weapon grants, per-weapon mods, evolutions) before shuffling. If too few fresh cards are left, _top_up refills from exclude in its original order, with no draws, so a reroll never shows fewer cards than the roll it replaced. With the default empty exclude, every pool and every upgrade_rng draw is unchanged. Every existing caller (main, SimCommands, replay driver, soak, autopilot bench, determinism probe, story trace, tests) passes nothing, so the three pinned baselines cannot move. A new test pins that the default and an explicit empty array leave identical upgrade_rng state.
  • reroll_choices(sim, pilot, n, shown): returns [] unless can_reroll. Otherwise it increments pilot.rerolls_used and returns roll_upgrade_choices(sim, pilot, n, shown) (or []-exclude when REROLL_EXCLUDE_SHOWN is off). It draws only from upgrade_rng, through the same roll_upgrade_choices, never from rng. A test pins that sim.rng state is untouched by a reroll.
  • skip_upgrade(sim, pilot) -> float: returns 0 unless can_skip. Otherwise refund = SKIP_XP_REFUND_FRAC × pilot.xp_to_next (0 under sim.dev_no_xp); pilot.xp += refund, levelup_skips += 1, pending_levelups -= 1. It returns the refund. A refund that crosses the bar levels the pilot on the next tick’s _process_levelups, the same path any XP takes. No level is granted inline.
  • can_reroll / can_skip: pending_levelups > 0, no tutorial, no App Clip, and (for reroll) rerolls_left > 0, where rerolls_left = max(sim.meta_rerolls - pilot.rerolls_used, 0).
  • action_state(sim, pilot) -> Dictionary: the read-only display helper the panel gets (rerolls_left, rerolls_max, can_reroll, can_skip, skip_pct).

Sanctioned mutations. The one-way data-flow rule in CLAUDE.md lists sim.tick and the level-up pick as the only render-side mutations. reroll_choices and skip_upgrade join apply_upgrade as the level-up resolvers. The CLAUDE.md line and docs/architecture/sim-architecture.md are updated to say so.

  • Local co-op: each pilot has its own budget, the same size for every pilot. Each PlayerState counts its own rerolls_used against the shared Sim.meta_rerolls. Both pilots fly the same device save. A late-joining P2 (Sim.add_pilot) starts at 0 used, so it gets the full budget with no extra wiring. This differs on purpose from stat upgrades, which today reach P1 only (MetaRunSetup.apply_stats comment). A choice-screen convenience that one pilot has and the other lacks would read as a bug.
  • The panel already gates input to the leveling pilot’s pad (_gate_device). X and B follow the same gate. On touch, P2’s buttons are disabled, matching how P2’s cards ignore the mouse.
  • Networked co-op is in §7.
  • Replays. main records #reroll / #skip through ReplayRecorder.note_pick, in order with the card picks. ReplayDriver and main._replay_recorded_pick accept the two action ids. A recorded reroll re-rolls in place, and the next recorded pick plays on the new cards. A recorded skip calls skip_upgrade. A recorded action that cannot apply is reported as a mismatch and falls back, the same as a stale pick, and never hidden. Human replays fly a fresh save, so in practice only skips appear in them. Reroll handling is there in case that changes.
  • Networked runs: hidden in v1. main._levelup_actions() returns {} when _net_run(). Wiring them needs two new SimCommands kinds (LEVELUP_REROLL, LEVELUP_SKIP), the counters in the lockstep checksum, and MetaRunSetup agreement across peers (today a networked run flies the base ship on every peer because saves differ). That belongs with the 1.2 relay track (docs/architecture/coop-and-pilots.md § Internet co-op), not this change.
  • Autopilot / soak / economy sim never reroll or skip. AutoPlayer.pick_upgrade_for is unchanged. Consequence for the economy sim: the item adds 1,050 gold of catalogue cost with zero in-sim benefit. Its curve is a slight underestimate of real players, which is the usual “autopilot is pessimistic” caveat. Plan Task 7 measures the shift.
  • New generic event levelup_action through EventQueue.push, one per reroll or skip: {action: "reroll"|"skip", run_time, level, pilot, shown: [the 3 card ids it replaced or skipped], rerolls_left, xp_refund}. It is a generic events row like lan_run, so it needs no Worker change and no migration. The payload is built by a pure LevelUpTelemetry.action_payload (net/levelup_telemetry.gd). It is unit-tested because EventQueue.push is a no-op headless.
  • run_end gains rerolls_used, levelup_skips (summed over pilots) and rerolls_owned (Sim.meta_rerolls), from LevelUpTelemetry.run_end_fields(sim).
  • Run timeline (runs.timeline_json): a reroll stamps #reroll as the old row’s picked and appends a new offered row for the same level. A skip stamps #skip. Existing queries that read picked as a card id must treat #-prefixed values as actions. This is noted in docs/architecture/dev-tools.md § Telemetry v2.
  • Privacy: this is gameplay telemetry of the same kind as level-up picks, which timeline_json already sends. It is not a new kind of data, so the privacy page and the ASC label do not change. Plan Task 7 includes reading the privacy page’s gameplay wording to confirm that.

9. Remote-config tunables (all registered, all with a docs.d label)

Section titled “9. Remote-config tunables (all registered, all with a docs.d label)”
Key Default Bounds Scope Notes
UpgradeSystem.SKIP_XP_REFUND_FRAC 0.25 0.0-1.0 sim, live (read at each skip) the refund; label reads it live
UpgradeSystem.REROLL_EXCLUDE_SHOWN true bool sim, live (read at each reroll) off = a reroll may repeat shown cards
LevelUpPanel.SKIP_CONFIRM_S 1.5 0.3-5.0 local (render-only) the two-press window
meta_upgrades.levelup-reroll.base_cost / cost_growth / magnitude 150 / 2.0 / 1 overlay default next run (bible) already patchable, no new code

The liveness probe cannot prove the two sim keys, because the autopilot never rerolls or skips. They are pinned by wiring tests through the real Tunables.apply path instead, the same precedent as LOGI_OVERCHARGE_HEAL in remote-config.md Batch 5. max_level stays structural, because it is the tier count.

  1. Shop price 150 / 300 / 600 gold (1,050 total, about 4.8% of the catalogue). The alternative is 120 / 240 / 480.
  2. Shop tab: Utility (it brings the empty tab back). The alternative is Pilot, next to Greed.
  3. Skip refund: 25% of the next level’s XP bar, live tunable. Shown as SKIP +25% XP.
  4. Skip needs a second press (B / Backspace / SKIP tap) within 1.5 s. The alternative is a single press.
  5. Reroll never repeats the cards it replaced (unless too few others are left). This is a bool tunable, so it can be flipped live.
  6. Controls: X / Square = reroll, B / Circle = skip (B still closes SHIP STATS while it is open), R and Backspace on the keyboard, and bottom-left REROLL / SKIP buttons on touch. There are no mouse buttons on Mac, matching STATS.
  7. Reroll prompt hidden until a tier is owned. It is not advertised as a locked teaser on the panel, so players discover it in the shop.
  8. Both actions off while SHIP STATS is open.
  9. Local co-op: every pilot gets the full budget (unlike stat upgrades, which are P1-only).
  10. Hidden in networked co-op, Tutorial and App Clip for v1. Networked support goes with the 1.2 relay track.
  11. Story mode gets both actions (rerolls if owned).
  12. Telemetry: a generic levelup_action event plus three run_end fields. No Worker change.
  13. Sound: reroll and skip use the existing ui_nav sound. A dedicated reroll sound is a Sound Map follow-up.
  14. What’s New (console control/src/whats_new.js, unbuilt entry): “Level-up: Reroll (new Utility shop item, 1-3 rerolls per run) and Skip (+25% XP). X / B on a pad, R / Backspace on a Mac keyboard, REROLL / SKIP buttons on iPhone and iPad. Skip asks for a second press.” The store What’s New for 1.2 is written at that release, from this console entry.
  • An autopilot policy for skipping or rerolling (for example, skip when every card scores ≤ 0).
  • Banish/lock mechanics (Vampire Survivors’ Banish, Seal), and a reroll that keeps one card.
  • In-run sources of rerolls (pickups, level milestones), which Chris’s balance philosophy rules out.