Level-up reroll + skip (design, 2026-10-20)
Level-up reroll + skip
Section titled “Level-up reroll + skip”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.
1. What Chris decided (2026-10-20)
Section titled “1. What Chris decided (2026-10-20)”- 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”.
2. Player-facing behaviour
Section titled “2. Player-facing behaviour”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 2on 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 readsSKIP +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 toPRESS 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 byButtonPrompt(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 SKIPshow instead.
3. Where it is offered
Section titled “3. Where it is offered”| 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.
4. The shop item
Section titled “4. The shop item”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/magnitudeare 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.mdsays it “stays inORDERfor future economy items”. The item brings the tab back, andtests/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 ofmagnitude × levelover every def with effectlevelup_rerolls, rounded.MetaRunSetup.apply_statswrites it toSim.meta_rerolls, the same pattern as Greed →Sim.meta_gold_mult.StatEffectsdoes not know the effect, soMetaState.apply_toskips it, as it does forgold_mult. - Card copy goes through
MetaCopy.desc:{mag}is the flat add, so a patched magnitude re-renders. - Card icon: a new
ShopIconsglyph (two curved arrows) on the effect keylevelup_rerolls.
5. Sim design (determinism-safe)
Section titled “5. Sim design (determinism-safe)”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 likeupgrade_picks(nothing in the tick reads them).Sim.dev_reset_upgradesresets 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_uprefills fromexcludein its original order, with no draws, so a reroll never shows fewer cards than the roll it replaced. With the default emptyexclude, every pool and everyupgrade_rngdraw 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 identicalupgrade_rngstate.reroll_choices(sim, pilot, n, shown): returns[]unlesscan_reroll. Otherwise it incrementspilot.rerolls_usedand returnsroll_upgrade_choices(sim, pilot, n, shown)(or[]-exclude whenREROLL_EXCLUDE_SHOWNis off). It draws only fromupgrade_rng, through the sameroll_upgrade_choices, never fromrng. A test pins thatsim.rngstate is untouched by a reroll.skip_upgrade(sim, pilot) -> float: returns 0 unlesscan_skip. Otherwiserefund = SKIP_XP_REFUND_FRAC × pilot.xp_to_next(0 undersim.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, wherererolls_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.
6. Co-op
Section titled “6. Co-op”- Local co-op: each pilot has its own budget, the same size for every pilot. Each
PlayerStatecounts its ownrerolls_usedagainst the sharedSim.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_statscomment). 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.
7. Replays, networked runs, autopilot
Section titled “7. Replays, networked runs, autopilot”- Replays.
mainrecords#reroll/#skipthroughReplayRecorder.note_pick, in order with the card picks.ReplayDriverandmain._replay_recorded_pickaccept the two action ids. A recorded reroll re-rolls in place, and the next recorded pick plays on the new cards. A recorded skip callsskip_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 newSimCommandskinds (LEVELUP_REROLL,LEVELUP_SKIP), the counters in the lockstep checksum, andMetaRunSetupagreement 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_foris 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.
8. Telemetry
Section titled “8. Telemetry”- New generic event
levelup_actionthroughEventQueue.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 genericeventsrow likelan_run, so it needs no Worker change and no migration. The payload is built by a pureLevelUpTelemetry.action_payload(net/levelup_telemetry.gd). It is unit-tested becauseEventQueue.pushis a no-op headless. run_endgainsrerolls_used,levelup_skips(summed over pilots) andrerolls_owned(Sim.meta_rerolls), fromLevelUpTelemetry.run_end_fields(sim).- Run timeline (
runs.timeline_json): a reroll stamps#rerollas the old row’spickedand appends a new offered row for the same level. A skip stamps#skip. Existing queries that readpickedas a card id must treat#-prefixed values as actions. This is noted indocs/architecture/dev-tools.md§ Telemetry v2. - Privacy: this is gameplay telemetry of the same kind as level-up picks, which
timeline_jsonalready 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.
10. Decisions for Chris’s veto
Section titled “10. Decisions for Chris’s veto”- Shop price 150 / 300 / 600 gold (1,050 total, about 4.8% of the catalogue). The alternative is 120 / 240 / 480.
- Shop tab: Utility (it brings the empty tab back). The alternative is Pilot, next to Greed.
- Skip refund: 25% of the next level’s XP bar, live tunable. Shown as
SKIP +25% XP. - Skip needs a second press (B / Backspace / SKIP tap) within 1.5 s. The alternative is a single press.
- Reroll never repeats the cards it replaced (unless too few others are left). This is a bool tunable, so it can be flipped live.
- 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.
- 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.
- Both actions off while SHIP STATS is open.
- Local co-op: every pilot gets the full budget (unlike stat upgrades, which are P1-only).
- Hidden in networked co-op, Tutorial and App Clip for v1. Networked support goes with the 1.2 relay track.
- Story mode gets both actions (rerolls if owned).
- Telemetry: a generic
levelup_actionevent plus threerun_endfields. No Worker change. - Sound: reroll and skip use the existing
ui_navsound. A dedicated reroll sound is a Sound Map follow-up. - 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.
11. Out of scope
Section titled “11. Out of scope”- 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.