Skip to content

Permanent Multipliers

What you'll learn

  • How a derived honor_multiplier is computed from _resources["honor"] on every _recalculate_income call, threaded into income aggregation as a final multiplier.
  • The "permanent" label as architectural shorthand: a multiplier whose source data (Honor) survives soft reset is permanent, regardless of whether the multiplier value itself is cached or recomputed.
  • The stacking order: base_output × count × tick_multiplier × honor_multiplier. Each multiplier is a multiplicand; ordering does not change the product, but ordering shapes which combinations are easy to express and which are not.
  • How a click-side click_honor_multiplier mirrors the tick-side multiplier — different formula, same architectural pattern. Honor buffs both arms of the economy independently.
  • Why honor_multiplier lives on GameState (not Light, not its own autoload) — single recompute call site, single ordering decision, single read site for the tick handler.

How it applies

  • Multiplicative stacking is genre canon. Cookies-style runs hit 10^200 income late-game because every layer (buildings, upgrades, prestige, ascension) multiplies. Additive stacking would saturate around 100×. The architectural choice — multiplier composition in recompute — is what lets the game produce visibly large numbers years into a run without rewriting the math.
  • First-prestige psychology. A first prestige usually returns 1 Honor. With honor_multiplier = 1.0 + 0.1 × honor, that translates to a 10% income boost on the next run. Small in absolute terms, but every system the player touches feels 10% better — clicks earn 10% more, buildings produce 10% more, costs pay off 10% sooner. The "everything got better" feeling, not "this one stat got better," is what makes prestige loops compelling.
  • Late-game scaling. Honor 50 → 6× multiplier. Honor 200 → 21× multiplier. The linear-on-Honor formula stays gentle late-game; switching to honor_multiplier = pow(1.05, honor) turns Honor 200 into ~17,000×, blowing up the curve. The formula choice — linear vs. exponential — is the late-game pacing dial. M6.2 picks linear because it pairs with M6.1's sqrt curve to keep total scaling manageable; an exponential Honor multiplier with a sqrt currency curve produces an explosive composite.
  • Save round-trip. honor_multiplier is not saved to disk. It is derived from _resources["honor"] (which is saved). On load, the first _recalculate_income call reads Honor and reconstructs the multiplier. M7 stores source-of-truth fields and lets recompute reconstruct everything else — three lines saved, no version-bump needed when the formula tweaks.
  • Tunability without touching saves. A balance designer wants to test 0.15 × honor vs 0.10 × honor. They edit one line in _compute_honor_multiplier, ship the build. Existing saves load with the same Honor value; the new formula applies on the next recompute. No save migration, no version field, no rollout coordination.

Concepts

honor_multiplier as a derived property

honor_multiplier is not stored. It is computed on every _recalculate_income call from _resources["honor"]:

func _compute_honor_multiplier() -> float:
    var honor: float = get_resource("honor")
    return 1.0 + 0.1 * honor

Two lines. The +1.0 keeps the multiplier ≥ 1 even at zero Honor (so the first run plays at full speed). The 0.1 × honor is the per-unit Honor contribution.

honor_multiplier

The choice to keep it derived (not cached as a field) is M3.2's recompute-on-change discipline applied at one more level: Honor mutates on prestige; income recomputes on prestige (as part of pledge_crusade); the multiplier reads the latest Honor. No staleness window.

Example

A player ends a run with 10 Honor. _compute_honor_multiplier returns 1.0 + 0.1 × 10 = 2.0. The next run's tick income is 2× compared to the first run's. They prestige again, gaining 5 more Honor. Honor = 15. The next _recalculate_income after the pledge re-reads Honor, returns 2.5. Income jumps from 2× to 2.5× over the previous run's tier — without any field assignment in pledge_crusade. Recompute does the work.

Stacking order in _recalculate_income

M5.4's _recalculate_income walked _buildings, summed count × base_output, multiplied by tick_multiplier. M6.2 inserts honor_multiplier as a final multiplier:

var honor_multiplier: float = _compute_honor_multiplier()
totals["light"] *= tick_multiplier
totals["light"] *= honor_multiplier
_income_rates = totals

The two multiplications could be combined as totals["light"] *= tick_multiplier * honor_multiplier. Splitting them is a readability choice — each line names one multiplier source, so a reader scrolling through the recompute can see "tick_multiplier from upgrades, honor_multiplier from prestige" without parsing a compound expression.

Example

A run with 50 Initiates, 10 Aspirants, tick_multiplier = 2.0 (one Sermon owned), Honor = 5 (honor_multiplier = 1.5). Recompute: 50 × 0.1 + 10 × 1.0 = 15.0. Then × 2.0 = 30.0. Then × 1.5 = 45.0. _income_rates["light"] = 45.0. The tick handler adds 4.5 Light/tick, ten times per second — the run is 1.5× faster than the equivalent Honor=0 state, exactly as the formula predicts.

Click-side multiplier

The tick income gets honor_multiplier. Clicks should too — otherwise the prestige loop punishes click-focused playstyles. M6.2 adds click_honor_multiplier as a sibling derivation:

func _compute_click_honor_multiplier() -> float:
    var honor: float = get_resource("honor")
    return 1.0 + 0.05 * honor

func get_click_value() -> float:
    return click_value * _compute_click_honor_multiplier()

The click multiplier is half the rate of the tick multiplier (0.05 × honor vs 0.1 × honor) because clicks are active and ticks are passive — the prestige reward favors the passive arm by design.

click-side multiplier

The button handler that previously called add_to_resource("light", click_value) now calls add_to_resource("light", get_click_value()). One line change, one new method call site.

Example

A run with click_value = 1.0, Honor = 8 (click_honor_multiplier = 1.4). Each click adds 1.0 × 1.4 = 1.4 Light. The button label could display "Train (+1.4)" — the live click value feeds the UI through a derived getter, just as the tick value feeds the income rate.

Why both multipliers live on GameState

tick_multiplier, honor_multiplier, click_value, click_honor_multiplier — all four sit on GameState. The temptation is to scatter them: Light owns the click multiplier, Tick owns the tick multiplier, a Prestige autoload owns the Honor-derived ones.

Resist. Two reasons:

  1. One read site for the tick handler. _on_tick reads _income_rates. _recalculate_income produces _income_rates from one place. If multipliers were spread across autoloads, the recompute would have to import all of them, and the dependency graph would tangle. Single autoload = single recompute = single source of truth.
  2. One ordering decision. (base × count × tick_multiplier × honor_multiplier) versus ((base × count × honor_multiplier) × tick_multiplier) produce the same number, but if multiplier sources lived on different autoloads, the order would emerge implicitly from method-call sequence in recompute — fragile to refactor. With all multipliers on GameState, the order is one explicit line in one method.

Example

A future feature adds a faith multiplier from a new "Devotion" mechanic. The right place is GameState.faith_multiplier, with a _compute_faith_multiplier() helper, multiplied into totals["light"] in _recalculate_income. One file, one method, one new line. If Faith were its own autoload owning its own multiplier, the recompute would have to know about a sibling autoload, breaking GameState's isolation.

Walkthrough

You will perform these in your own Godot editor. Coming in: M6.1 added Honor accumulation but no multiplier consumption. The next-run boost is purely psychological at this point — you have Honor in the bank but it does nothing.

  1. Open scripts/game_state.gd. Add the multiplier helpers below the existing methods:
    func _compute_honor_multiplier() -> float:
        return 1.0 + 0.1 * get_resource("honor")
    
    func _compute_click_honor_multiplier() -> float:
        return 1.0 + 0.05 * get_resource("honor")
    
    The helpers are private (underscore-prefixed) because _recalculate_income and get_click_value are the only legitimate callers.
  2. Update _recalculate_income — find the line totals["light"] *= tick_multiplier and add the Honor multiplier after it:
    totals["light"] *= tick_multiplier
    totals["light"] *= _compute_honor_multiplier()
    
    The two-line split is the readability discipline from this chapter.
  3. Add get_click_value to GameState:
    func get_click_value() -> float:
        return click_value * _compute_click_honor_multiplier()
    
  4. Open the click handler — likely on Button somewhere with a _on_pressed that calls GameState.add_to_resource("light", click_value). Replace the literal click_value reference with the new getter:
    GameState.add_to_resource("light", GameState.get_click_value())
    
    Save scripts.

Architectural note. This step changes the click handler to write through GameState.add_to_resource("light", ...) instead of Light.value += .... The M2.1 click handler used Light.value; M3.3 introduced the parallel GameState._resources["light"] and acknowledged the dual write-path. M6.2 is the moment that dual path resolves in practice: the click handler now writes through GameState exclusively, bypassing the Light.value setter (M2.2) and the Light.value_changed signal entirely. The CounterLabel's update path now depends entirely on GameState.resource_changed. If your CounterLabel script still listens to Light.value_changed (M2.3's original wiring), it will stop receiving updates after this edit — confirm the M3.3-era refactor connected the Label to GameState.resource_changed as well, or this step silently breaks the label. The right resolution is documented in M3.3's "dual source-of-truth" caveat — pick one signal source per UI consumer. 5. Press F5. With Honor = 0, the multipliers are 1.0 — gameplay should feel identical to M5.4. Click Train; verify Light += 1.0. Buy an Initiate; verify income rate matches the pre-M6.2 calculation. 6. Open the Remote tab. Set GameState._resources["honor"] = 5.0. The next _recalculate_income call (any purchase or pledge) will read the updated Honor. Trigger a recompute by buying any cheap upgrade or building — _income_rates["light"] should jump by 50% (1.5× multiplier). 7. Click Train. The button's add-to-Light should be 1.0 × (1.0 + 0.05 × 5) = 1.25 Light per click. Confirm by clicking once and watching the counter increment by 1 (the integer cast in the label rounds — but the underlying float is 1.25).

Verification through fractional clicks. Click four times; Light increments to 5 (4 × 1.25 = 5). Click eight times total; Light = 10 (8 × 1.25 = 10). Patterns of "every 4th click adds 2 instead of 1" are the rounding tell. The float math is correct; the integer display is just the display. 8. Pledge the order. With _lifetime_light above 1M and Honor accumulating, the pledge wipes Light and buildings but Honor is preserved. After the pledge, the next click should still earn 1.25 Light (the Honor multiplier survived the wipe). The next building purchase recomputes income against the new _buildings = {} and the unchanged Honor_income_rates["light"] = 0.0 (no buildings yet) regardless of multiplier; a 1.5× multiplier on zero is still zero.

The "feels nothing changed" complaint. A first-time prestige player buys their first new Initiate after the pledge and sees the same 0.1 Light/s as their first-ever Initiate. They expect more — they have Honor! The math is correct: 0.1 × 1 × 1.0 (no Sermons yet) × 1.5 (Honor) = 0.15 Light/s. The display rounds to "0.1 light/sec" if the UI doesn't show fractional rates. The fix is in M8.1's number formatting — make the rate display "0.15 light/sec" with one decimal. For now, the multiplier is real but invisible until counts grow large enough to overcome rounding. 9. Save the project.

Self-check quiz

Q1 — A player has Honor = 12. The tick income, before any Honor multiplier, is 50 Light/s. What does _recalculate_income write to _income_rates['light']?

A. 50.0Honor only multiplies click income, not tick income. B. 62.0Honor adds 0.1 × 12 = 1.2 Light/s flat to the rate. C. 110.050 × (1.0 + 0.1 × 12) = 50 × 2.2. Honor scales the rate multiplicatively. D. 60.0Honor adds 0.1 × 12 × 50 = 60 Light/s in absolute terms, but the multiplier replaces rather than adding.

Reveal answer

C — 110.0. honor_multiplier = 1.0 + 0.1 × 12 = 2.2. The recompute multiplies the building-summed rate by tick_multiplier (1.0 here, no Sermons) and then by honor_multiplier: 50 × 1.0 × 2.2 = 110. A is wrong: Honor multiplies tick income via _compute_honor_multiplier. B confuses additive scaling with multiplicative — the formula is 1.0 + 0.1 × honor which is a multiplier, not an additive offset. D garbles two different scaling models. The takeaway: Honor scales the entire tick rate, and the 1.0 + floor means even Honor-zero players get 1.0× (no slowdown).

Q2 — honor_multiplier is not saved to the save file. Why is that correct?

A. It would slow down the save format with too many fields. B. It is derived from _resources["honor"], which is saved. On load, the first _recalculate_income reconstructs it from Honor. C. Multipliers are run-scoped — they do not need to survive across launches. D. Godot does not allow saving floating-point fields.

Reveal answer

B — derived properties are reconstructed from saved source data. Saving honor_multiplier would mean the save format duplicates information already represented by _resources["honor"]. On load, if Honor is restored to 12, the next recompute writes 2.2 back into honor_multiplier. Saving the cache directly would also create a class of bugs where the cache and source disagree (e.g., the formula changed in a patch but old saves still have stale cached values). A is wrong: one float makes no measurable size difference. C is wrong: prestige multipliers are meta-scoped, surviving across runs and launches. D fabricates a constraint.

Q3 — The chapter's design uses honor_multiplier = 1.0 + 0.1 × honor (linear) rather than honor_multiplier = pow(1.05, honor) (exponential). What is the design tradeoff?

A. Linear is computationally cheaper to evaluate. B. Linear scales gently — Honor 50 → 6×; exponential balloons — Honor 50 → 11.5×, Honor 200 → ~17,000×. The chapter pairs linear Honor with M6.1's sqrt currency to keep composite scaling manageable. C. Exponential cannot be saved to disk. D. Linear is required when multiple multipliers stack.

Reveal answer

B — pacing tradeoff between formula curves. The composite curve (Honor-currency formula × Honor-multiplier formula) determines late-game progression. M6.1's sqrt currency formula is sublinear in lifetime Light, so each prestige returns less Honor than the prior. Pairing it with a linear multiplier keeps total scaling polynomial. Pairing with an exponential multiplier produces compound super-polynomial growth — playable for some genres (incremental idle), unplayable for others (this textbook's pacing target). A is true but trivial. C and D are fabricated.

Integration question

Q4 — open

A new feature request: "Sermon upgrades should be permanent, surviving prestige." Currently _purchased_upgrades is run-scoped (cleared in _reset_run_state); tick_multiplier is also run-scoped. How would you architect "permanent Sermons" without breaking the existing run-scoped Sermon path?

Reveal expected answer

The cleanest architecture: introduce a separate _permanent_upgrades: Dictionary[String, UpgradeData] keyed by upgrade.id, with a separate permanent_tick_multiplier: float derived from it. _reset_run_state clears _purchased_upgrades and tick_multiplier (existing behavior); does not touch _permanent_upgrades or permanent_tick_multiplier. _recalculate_income multiplies by both: totals["light"] *= tick_multiplier × permanent_tick_multiplier × honor_multiplier.

The new UpgradeData fields: a is_permanent: bool flag distinguishes the two upgrade types. The existing purchase_upgrade branches on the flag, routing to _purchased_upgrades (run-scoped) or _permanent_upgrades (meta-scoped). The save format stores both dictionaries; load restores both; recompute reconstructs both multipliers.

Why not modify _purchased_upgrades to track per-entry permanence? Because the reset path (_reset_run_state) becomes an iteration that filters: "remove non-permanent entries." Three lines of imperative filtering. The two-dictionary split makes the reset a one-line clear() on the run-scoped dictionary; permanence is encoded in where the entry lives, not in a flag the reset has to inspect.

Why not just put permanent upgrades in a separate UI/list/data file with no dictionary at all? Because the patterns we developed in M4 (purchase, refresh, unlock thresholds) already work for run-scoped upgrades. Reusing them for permanent upgrades — same purchase_upgrade shape, different storage — leverages existing code.

The architectural principle: permanence is a property of where state lives, not a property of the state's contents. Run-scoped dictionaries get cleared on reset; meta-scoped dictionaries don't. The is_permanent flag is metadata that routes purchases; the dictionaries themselves are dumb storage.

This is the same pattern as M6.1's _resources["light"] (run-scoped, cleared) vs _resources["honor"] (meta-scoped, preserved). One Dictionary, two semantic categories of keys, the reset path knows which category to clear by listing them by name. The "list by name" works for two; for ten or twenty, you'd want category metadata in the value or two separate dictionaries — same architectural choice at the upgrade layer.

Glossary

Glossary

honor_multiplier
The derived multiplier on GameState computed from current Honor: 1.0 + 0.1 × honor. Recomputed on every _recalculate_income call. Not saved — reconstructed from _resources["honor"] (which is saved). Multiplied into income alongside tick_multiplier in _recalculate_income's final pass.
click-side multiplier
The Honor-derived multiplier that scales each click's Light gain: 1.0 + 0.05 × honor. Mirrors the tick-side honor_multiplier with a smaller per-Honor coefficient (clicks are active, ticks are passive — passive gets the larger reward by design). Applied in get_click_value() because clicks aren't aggregated; each click reads the current value at click time.
derived multiplier
A multiplier whose value is computed from primary state on every read (or every recompute), not stored as its own field. Always current with the source data; saved by saving the source, not the cache. Distinct from tick_multiplier, which is mutated directly by purchase_upgradetick_multiplier is cached state, honor_multiplier is derived state.
### Ask Claude (side-channel) Try one of these in a Claude Code session if a piece did not land: - `Show me what _recalculate_income looks like end-to-end after M6.2 — every multiplier line, every comment.` - `If I want Honor to scale exponentially instead of linearly, what changes? Show me the exact diff in _compute_honor_multiplier.` - `What's the difference between _resources["honor"] and honor_multiplier? Trace where each lives, when each updates, and which one is saved.`