Skip to content

Prestige Currency & Soft Reset

Try it first

Your player has been playing for two weeks. They have 10¹² Light, 87 Initiates, 64 Aspirants. The progression curve has flattened — the next building tier costs more than they will earn in a month at current rates. They are about to quit.

You want them to keep playing. What do you offer them?

Constraints: - You may not increase their current rates (the curve is the curve; bumping it is a balance band-aid, not a design move). - You may not give them anything for free. - Whatever you do, they must choose it, and choosing it must feel good.

Spend 5 minutes brainstorming. Write down two or three mechanisms you could offer the player. The chapter's canonical answer is one of them; the others are instructive failures.

What you'll learn

  • The prestige loop's role in idle-game pacing: a soft reset that wipes the run-scoped economy (Light, buildings, upgrades) in exchange for a permanent currency (Honor) that boosts the next run.
  • The square-root currency formula honor_gain = floor(sqrt(_lifetime_light / 1e6)) — sublinear in lifetime Light so deeper runs return diminishingly more Honor, controlling how fast the meta-loop progresses.
  • The lifetime-tracking field _lifetime_light that survives soft reset, accumulated by add_to_resource("light", amount) for positive amounts only.
  • The pledge_crusade() method's structure: validate threshold, compute Honor gain, accumulate _resources["honor"], wipe run-scoped state, emit crusade_pledged, recompute income.
  • ConfirmationDialog — Godot's built-in modal that wraps a yes/no prompt around a destructive action. The discipline of routing irreversible mutations through a confirmation node, not a raw button click.

How it applies

  • Pacing the meta-loop. A first run reaches 1M lifetime Light in maybe 30 minutes; the player gains 1 Honor on prestige. The second run, with a 1.0× → 1.1× Honor multiplier, reaches the same milestone in ~27 minutes. The third run pushes deeper, gains 3 Honor, and so on. The square-root curve means doubling lifetime Light yields ~1.41× Honor — the player must extend further each run for diminishing meta-currency, the canonical incremental-genre pacing tool.
  • Player permanence anchoring. The soft reset is psychologically heavy — buttons that wipe progress need framing. Calling it "Pledging the order to a new Crusade" reframes the wipe as a positive act, not a loss. Every successful idle game frames its prestige; the mechanic is the same, the framing is the lever.
  • Save format split. M7 saves both run-scoped state (_resources["light"], _buildings, _purchased_upgrades) and meta-scoped state (_resources["honor"], _lifetime_light, prestige count). On load they restore independently; on prestige the run-scoped fields wipe but the meta-scoped fields persist. The split survives because the architecture introduced it now, not retrofitted at save time.
  • Confirmation as a UX safety rail. A single misclick on "Pledge Crusade" with 4 hours of progress unconfirmed = quit. The ConfirmationDialog modal is two lines of editor work, two lines of code, and prevents an entire class of catastrophic player error. The same node handles language-localized button text via the theme system.
  • Telemetry opportunity. Each crusade_pledged emit is a natural funnel event. A QA build can log lifetime_light at prestige, honor_gain, seconds since last prestige, build a histogram, and identify if the formula's curve is producing the intended pacing. Without an explicit prestige signal, the same data has to be reconstructed from delta scans.

Concepts

Mapping your answers onto the canonical

You probably wrote down one or more of these:

  • "Add a new building tier." This kicks the can down the road; the curve flattens again at the next wall. Doesn't solve the problem, just delays it.
  • "Let them spend Light on permanent stat boosts." Works once, but the boost stacks with future grinding and they out-scale the game. Solves once, breaks long-term.
  • "Trade Light for a multiplier and reset to zero." This is the canonical prestige loop. The reset makes the multiplier sustainable: the player earns back Light at the new accelerated rate, hits the same wall, prestiges again. The loop is self-balancing because what they gain (multiplier) is permanent and what they lose (Light, buildings) is recoverable.
  • "Add a leaderboard." This is engagement layer, not progression mechanic. Orthogonal to the question.

The "trade and reset" answer is the one the genre converged on, and it's the one this chapter walks through. The others are not wrong as features — they're wrong as answers to this specific question. The reason the loop works is loss aversion, deliberately engineered: players willingly reset because the meta-currency they earn is strictly better than the currency they discard, and the act of discarding is the proof that the meta-currency is worth more than the run-currency could ever be.

The soft reset

A soft reset wipes Light, building counts, and purchased upgrades, then refunds nothing — the run is over. In exchange, the player receives a quantity of Honor based on the lifetime Light earned during the run. Honor survives the reset; everything else does not.

const PRESTIGE_THRESHOLD: float = 1_000_000.0

func can_pledge_crusade() -> bool:
    return _lifetime_light >= PRESTIGE_THRESHOLD

func pledge_crusade() -> int:
    if not can_pledge_crusade():
        return 0
    var honor_gain: int = int(sqrt(_lifetime_light / 1_000_000.0))
    add_to_resource("honor", honor_gain)
    _reset_run_state()
    _recalculate_income()
    crusade_pledged.emit(honor_gain)
    return honor_gain

The method is structured the same as purchase_building and purchase_upgrade: validate, compute, mutate, recompute, emit. The recompute runs before the emit so any subscriber to crusade_pledged that reads _income_rates sees the post-prestige (zeroed) rates, not the stale pre-prestige cache (the walkthrough returns to this ordering point). The differences are the magnitude — _reset_run_state() zeroes a Dictionary's worth of state, not a single counter.

Example

A player ends a 90-minute run at _lifetime_light = 4_200_000. honor_gain = int(sqrt(4.2)) = 2. Honor goes from 0 → 2. _reset_run_state zeroes Light, buildings, owned upgrades. _lifetime_light is not reset by _reset_run_state — it is meta-scoped. The signal fires; the prestige UI shows "+2 Honor — Crusade complete." The income recompute walks _buildings = {} (now empty) and writes _income_rates = {"light": 0.0, "honor": 0.0}. The next tick adds zero. The run starts over.

The square-root formula

The Honor gain formula is honor_gain = floor(sqrt(_lifetime_light / 1e6)). Three components:

  • The 1e6 divisor sets the floor of the prestige threshold — below 1M lifetime Light, the formula returns 0 and can_pledge_crusade rejects.
  • The sqrt makes the curve sublinear: 1M → 1, 4M → 2, 9M → 3, 100M → 10, 10B → 100. Doubling lifetime Light yields ~1.41× Honor; quadrupling yields exactly 2×.
  • The floor (via int(...)) keeps Honor as an integer, displayed cleanly without decimals. The truncation is mild because the curve is shallow.
var honor_gain: int = int(sqrt(_lifetime_light / 1_000_000.0))

Example

Five runs with progressive depth: lifetime Light of 1.2M, 2.5M, 6.0M, 16M, 64M → Honor gain 1, 1, 2, 4, 8. Total Honor after five runs: 16. The first two runs feel "samey" (1 Honor each) because the curve is steepest at the bottom; runs four and five feel rewarding (4, 8) because the player pushed past the slow early region. The shape is intentional: early prestige is low-reward to teach the loop; mid-game prestige scales rewards with player ambition.

Lifetime tracking that survives reset

_lifetime_light is a separate counter from _resources["light"]. add_to_resource("light", amount) increments both — the live ledger (resets on prestige) and the lifetime ledger (does not reset). Negative amounts (purchases) decrement the live ledger but do not touch the lifetime ledger.

func add_to_resource(resource_name: String, delta: float) -> void:
    var new_value: float = maxf(_resources.get(resource_name, 0.0) + delta, 0.0)
    _resources[resource_name] = new_value
    if resource_name == "light" and delta > 0.0:
        _lifetime_light += delta
    resource_changed.emit(resource_name, new_value)

The shape matches M3.3's canonical add_to_resource — same maxf clamp (no resource can go below zero), same parameter name (resource_name, to avoid shadowing Node.name), same emit order. The new line is the if resource_name == "light" and delta > 0.0: _lifetime_light += delta guard, inserted between the write and the emit. The delta > 0.0 clause (not new_value > 0.0) is the discipline: spending Light on a building shrinks _resources["light"] but does not shrink lifetime — the player did earn that Light, even if they then spent it. Lifetime tracks earnings, not balance.

Example

A player has Light = 0, lifetime = 0. They click Train 100 times: Light = 100, lifetime = 100. They buy an Initiate (cost 15): Light = 85, lifetime = 100 (the deduction did not reduce lifetime). They idle for ten ticks of 0.1 Light/s: Light = 86, lifetime = 101. They prestige: Light = 0, lifetime = 101. Honor gained: int(sqrt(101 / 1e6)) = 0 — below threshold, prestige rejected. The discipline prevented a "pledge with 0 Honor" footgun.

_lifetime_light

ConfirmationDialog — modal yes/no

ConfirmationDialog is a Godot built-in scene node that pops up as a modal overlay with two buttons (default "OK" / "Cancel"). It emits confirmed when the user clicks OK and canceled when they cancel.

@onready var _dialog: ConfirmationDialog = %PrestigeDialog

func _on_pledge_pressed() -> void:
    _dialog.dialog_text = "Pledge the order? You will lose all run progress."
    _dialog.popup_centered()

func _ready() -> void:
    _dialog.confirmed.connect(_on_pledge_confirmed)

The Buy-equivalent flow is two-step: button press shows the dialog; dialog's confirmed signal calls the actual pledge_crusade. The button never directly calls the destructive action — there is always a modal between the click and the wipe.

Example

A player clicks Pledge by mistake. The dialog appears: "Pledge the order? You will lose all run progress." They click Cancel. canceled fires; nothing happens; their run continues. A second click on Pledge, dialog reappears, they read the text, click OK. confirmed fires; pledge_crusade runs; the wipe happens. Two-click discipline kept the catastrophic action behind a deliberate read-then-confirm pause. The cost is one extra modal; the saved run is the gain.

Walkthrough

You will perform these in your own Godot editor. Coming in: GameState knows _resources, _buildings, _purchased_upgrades from M3–M5. There is no Honor, no lifetime tracking, no prestige UI.

  1. Open scripts/game_state.gd. Add the meta-state fields below the existing variables:
    const PRESTIGE_THRESHOLD: float = 1_000_000.0
    var _lifetime_light: float = 0.0
    signal crusade_pledged(honor_gain: int)
    
    PRESTIGE_THRESHOLD is a constant — tunable but not save-restored. _lifetime_light starts at zero and grows. crusade_pledged is the signal UI listens to.
  2. Update add_to_resource to track lifetime. The M3.3 method is currently:
    func add_to_resource(resource_name: String, delta: float) -> void:
        var new_value: float = maxf(_resources.get(resource_name, 0.0) + delta, 0.0)
        _resources[resource_name] = new_value
        resource_changed.emit(resource_name, new_value)
    
    Insert the lifetime-tracking line after the _resources[resource_name] = new_value write, before the resource_changed.emit call. The full revised method:
    func add_to_resource(resource_name: String, delta: float) -> void:
        var new_value: float = maxf(_resources.get(resource_name, 0.0) + delta, 0.0)
        _resources[resource_name] = new_value
        if resource_name == "light" and delta > 0.0:
            _lifetime_light += delta
        resource_changed.emit(resource_name, new_value)
    
    The guard delta > 0.0 (not new_value > 0.0) ensures only positive deposits count toward lifetime — spending Light on a building deducts from the live balance but does not subtract from lifetime. The clamp from M3.3 is preserved.
  3. Add the prestige guard:
    func can_pledge_crusade() -> bool:
        return _lifetime_light >= PRESTIGE_THRESHOLD
    
  4. Add the pledge method:
    func pledge_crusade() -> int:
        if not can_pledge_crusade():
            return 0
        var honor_gain: int = int(sqrt(_lifetime_light / 1_000_000.0))
        add_to_resource("honor", honor_gain)
        _reset_run_state()
        _recalculate_income()
        crusade_pledged.emit(honor_gain)
        return honor_gain
    
    Eight lines. Same shape as purchase_building. Note the ordering: _recalculate_income() runs before crusade_pledged.emit(honor_gain). Any subscriber to crusade_pledged that reads _income_rates from the handler sees post-prestige rates (zero, since _buildings is now empty) rather than the stale pre-prestige cache. Returns the Honor gained for the caller to display in a flavor message. The int(...) cast at the top is mathematically equivalent to floor(...) for the non-negative domain — both produce the floor of the square root; the objective text uses floor as the conceptual name and the implementation uses int for compactness.
  5. Add _reset_run_state:
    func _reset_run_state() -> void:
        _resources["light"] = 0.0
        _buildings.clear()
        _purchased_upgrades.clear()
        tick_multiplier = 1.0
        click_value = 1.0
        resource_changed.emit("light", 0.0)
    
    Seven lines. Wipes Light, building counts, owned upgrades, and resets the M4.3 multipliers (tick_multiplier and click_value) to their default 1.0. Honor is not listed — meta-scoped state survives. The trailing resource_changed.emit ensures every subscribed Label updates immediately.

Note that _lifetime_light is deliberately not zeroed here — step 10 below walks the learner through discovering the missing reset as a teaching moment about meta-scoped vs run-scoped state. (The multipliers are reset quietly because M6.1 Q4's open question about Sermon multipliers — "if you bought a Sermon last run, you should not retain its 2× tick multiplier this run" — should not be left as an open bug; the learner discovers the lifetime issue, not the multiplier issue.) M6.2 will introduce a separate permanent multiplier sourced from Honor that does survive reset. Save (Ctrl+S). 6. Open scenes/main.tscn. Add a new node under MainLayout: Scene → Add Child Node → ConfirmationDialog. Rename to PrestigeDialog. In the Inspector, mark "Access as Unique Name" so it's reachable as %PrestigeDialog. 7. Add a Pledge Crusade Button somewhere on the screen (TopBar's far-right corner is conventional). Set the disabled state to true initially. 8. Create scripts/prestige_button.gd:

extends Button

@onready var _dialog: ConfirmationDialog = %PrestigeDialog

func _ready() -> void:
    pressed.connect(_on_pressed)
    _dialog.confirmed.connect(_on_confirmed)
    GameState.resource_changed.connect(_on_resource_changed)
    _refresh()

func _on_pressed() -> void:
    _dialog.dialog_text = "Pledge the order? You will lose all run progress."
    _dialog.popup_centered()

func _on_confirmed() -> void:
    GameState.pledge_crusade()

func _on_resource_changed(_n: String, _v: float) -> void:
    _refresh()

func _refresh() -> void:
    disabled = not GameState.can_pledge_crusade()
Attach the script to the Button. Save scene + script.

Code volume note: this is over the per-fragment ceiling, but the script is the chapter's deliverable — the textbook lets a complete Button-controller script through when the script is the artifact. The fragment ceiling applies to in-prose snippets, not to walkthrough deliverables. 9. Press F5. The Pledge Crusade button is disabled (_lifetime_light is 0). Open the Remote tab → GameState → set _lifetime_light = 1_500_000.0. The next resource_changed (any tick or click) re-runs _refresh; the button enables. Click it. The dialog appears with the text. Click OK. pledge_crusade runs; Light = 0; building counts = empty; Honor jumps from 0 to 1 (int(sqrt(1.5)) = 1). The CounterLabel and BuildingRows refresh. The button disables again (_lifetime_light is now 0 — it was reset? No — the discipline is meta-scoped, so it should not reset). Stop the game. 10. Wait — re-check step 5. _reset_run_state does not zero _lifetime_light. After the pledge, _lifetime_light is still 1_500_000.0. The Pledge button stays enabled because the player could pledge again right away — but with 0 Light earned this run, the formula returns int(sqrt(1.5)) = 1 again, and again, and again. This is wrong. Lifetime Light is per-run, not eternal-cumulative. Add _lifetime_light = 0.0 to _reset_run_state and re-test. The Pledge button now correctly disables on prestige and re-enables only when the player earns 1M more Light. Also notice: the multipliers (tick_multiplier, click_value) were already reset in Step 5; if you skipped that, your prestige rewards would stack incoherently — a Sermon's 2× would survive into a run where the Sermon record no longer exists.

> **Why the textbook lets this bug through to step 10.** The two-name confusion (lifetime-of-run vs. lifetime-eternal) is real and worth catching. A player who reads the architecture without testing might write the meta-scoped variant and ship a broken prestige loop. The walkthrough deliberately exposes the bug so the fix is anchored in the test, not just the docs. M6.2 introduces an *eternal*-lifetime field for the Honor multiplier formula; that one is the meta-scoped sibling.
  1. Save the project.

Self-check quiz

Q1 — A player has _lifetime_light = 9_000_000. They click Pledge Crusade. How much Honor do they gain?

A. 9floor(sqrt(9)) based on millions. B. 3int(sqrt(9_000_000 / 1_000_000)) = int(sqrt(9)) = 3. C. 9_000_000Honor equals lifetime Light. D. 1 — the formula returns 1 for any value above threshold.

Reveal answer

B — int(sqrt(9.0)) = 3. The formula divides by 1_000_000.0 first to scale lifetime Light into the right range, then takes sqrt, then int. sqrt(9) = 3.0; int(3.0) = 3. A confuses the formula's order. C ignores the divisor and the sqrt. D imagines a constant.

Q2 — _reset_run_state clears _resources['light'] and _buildings and _purchased_upgrades but does not clear _resources['honor']. Why?

A. Honor is not a resource — it lives in a separate field outside _resources. B. The Dictionary.clear() method skips entries marked as meta-scoped. C. Honor is meta-scoped state — the prestige loop's purpose is to convert run-scoped progress into permanent meta-scoped currency. Clearing Honor would defeat the loop. D. Honor is read-only after first acquisition; clearing it would error.

Reveal answer

C — meta-scoped state is the point of prestige. The whole prestige loop converts ephemeral run progress (Light, buildings, upgrades) into permanent currency (Honor). If _reset_run_state cleared Honor, the player would gain Honor and immediately lose it — the loop would have no payoff. A is wrong: Honor is in _resources (M3.3 introduced it as a sibling Dictionary entry); only the implementation chose to selectively clear _resources["light"] and not _resources["honor"]. B fabricates a tagging system. D is wrong: the value is mutable.

Q3 — The Pledge Crusade button connects to a ConfirmationDialog instead of calling pledge_crusade directly. What does the dialog give you?

A. It validates the prestige threshold automatically — the dialog refuses to show if can_pledge_crusade() returns false. B. It serializes the prestige action for save compatibility. C. It interposes a deliberate read-then-confirm pause between the click and the destructive action, preventing accidental wipes. D. It is required by Godot — destructive scene actions must go through a dialog.

Reveal answer

C — UX safety rail against misclicks. The dialog has no built-in awareness of can_pledge_crusade; the button's disabled state handles that. The dialog's job is to give the player a beat to read the warning text and reconsider. A confuses two different responsibilities (gate visibility = disabled; gate execution = dialog). B is wrong: dialogs are not save-related. D is wrong: nothing in Godot enforces this — it is a discipline.

Integration question

Q4 — open

pledge_crusade calls _reset_run_state which clears _buildings and _purchased_upgrades. M5.4's _recalculate_income walks _buildings to compute _income_rates. Trace what happens to _income_rates["light"] across the pledge: before the pledge, during the reset, and immediately after _recalculate_income runs at the end of pledge_crusade. Then trace the next tick.

Reveal expected answer

Before pledge: a steady-state run with (say) 50 Initiates, 10 Aspirants, tick_multiplier = 2.0. _income_rates["light"] = (50 × 0.1 + 10 × 1.0) × 2.0 = 30.0. The tick handler adds 30.0 × 0.1 = 3.0 Light/tick.

During pledge: 1. pledge_crusade validates threshold — passes. 2. honor_gain = int(sqrt(_lifetime_light / 1e6)) — say _lifetime_light = 25_000_000, honor_gain = 5. 3. add_to_resource("honor", 5)Honor goes from 0 → 5. resource_changed("honor", 5.0) emits. 4. _reset_run_state runs: _resources["light"] = 0.0, _buildings.clear() (empty), _purchased_upgrades.clear() (empty), _lifetime_light = 0.0 (per step 10's fix), resource_changed("light", 0.0) emits. 5. _recalculate_income() walks _buildings = {} — empty loop. totals["light"] = 0.0, totals["honor"] = 0.0. Multiplied by tick_multiplier — but wait, tick_multiplier lives on GameState and is not reset by _reset_run_state. Was that a mistake? 6. crusade_pledged.emit(5) — flavor UI catches this for the "Crusade complete: +5 Honor" message; because the recompute ran first, any handler reading _income_rates sees the post-prestige (zero) rates rather than the stale cache.

The answer depends on the design choice: if Sermon upgrades are run-scoped (you must re-buy them each run), then tick_multiplier = 1.0 should reset. If Sermons are eternal (a forever buff), then it should not. This chapter's _reset_run_state clears _purchased_upgrades but not tick_multiplier — a half-state. The fix is to add tick_multiplier = 1.0 (and click_value = 1.0) to _reset_run_state so the next-run multipliers start fresh. M6.2 will introduce a separate permanent multiplier (Honor stack) that lives outside tick_multiplier and survives reset.

After recompute: _income_rates = {"light": 0.0, "honor": 0.0} (assuming the multipliers reset).

Next tick: _on_tick(0.1) walks _income_rates. light: amount = 0.0, skipped by the > 0.0 guard. honor: amount = 0.0, skipped. Zero add_to_resource calls; zero resource_changed emits. The CounterLabel reads "Light: 0"; every BuildingRow reads count = 0, cost = base_cost. The run starts over.

This trace surfaces three distinct discipline points: the lifetime reset (step 10's bug), the multiplier reset (the open question above), and the recompute call's necessity (without it, _income_rates keeps the pre-prestige cached value and the tick handler ticks pre-prestige income on a wiped economy — disastrous).

Glossary

Glossary

soft reset
A reset operation that wipes run-scoped state (resources earned this run, buildings purchased, upgrades owned) but preserves meta-scoped state (prestige currency, lifetime totals, ascension counts). Distinct from a hard reset (full save wipe) and from autosave-rollback. The core mechanic of idle-game prestige loops.
Honor
The prestige currency in Blood Knight Grove. Accumulated by pledging Crusades; spent (in M6.2) on permanent multipliers. Survives soft reset; only a hard wipe destroys it. Single source of truth: _resources["honor"].
_lifetime_light
The cumulative Light earned during the current run. Incremented by add_to_resource for positive amounts only. Reset to zero on pledge_crusade. Drives the prestige threshold check and the Honor-gain formula. Distinct from _resources["light"] (live balance) and from a separate eternal-cumulative tracker (M6.2's province).
ConfirmationDialog
Godot's built-in modal node for yes/no prompts. Subclass of AcceptDialog with an extra Cancel button. Emits confirmed when the user clicks OK/Yes; canceled otherwise. Used in M6.1 to wrap pledge_crusade() against accidental clicks. Add via Scene → Add Child Node → ConfirmationDialog.
### Ask Claude (side-channel) Try one of these in a Claude Code session if a piece did not land: - `Show me the formula curve for honor_gain at lifetime Light values 1M, 4M, 16M, 64M, 256M. Plot it as a table.` - `What other Godot built-in dialogs exist besides ConfirmationDialog? When would I use AcceptDialog vs FileDialog vs ConfirmationDialog?` - `Walk through the difference between meta-scoped and run-scoped state in the current GameState. Which fields are which, and which ones might I be missing?`