Permanent Multipliers¶
What you'll learn
- How a derived
honor_multiplieris computed from_resources["honor"]on every_recalculate_incomecall, 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_multipliermirrors the tick-side multiplier — different formula, same architectural pattern. Honor buffs both arms of the economy independently. - Why
honor_multiplierlives onGameState(notLight, 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_multiplieris not saved to disk. It is derived from_resources["honor"](which is saved). On load, the first_recalculate_incomecall 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 × honorvs0.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:
- One read site for the tick handler.
_on_tickreads_income_rates._recalculate_incomeproduces_income_ratesfrom 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. - 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 onGameState, 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.
- Open
scripts/game_state.gd. Add the multiplier helpers below the existing methods:The helpers are private (underscore-prefixed) becausefunc _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")_recalculate_incomeandget_click_valueare the only legitimate callers. - Update
_recalculate_income— find the linetotals["light"] *= tick_multiplierand add the Honor multiplier after it: The two-line split is the readability discipline from this chapter. - Add
get_click_valuetoGameState: - Open the click handler — likely on
Buttonsomewhere with a_on_pressedthat callsGameState.add_to_resource("light", click_value). Replace the literalclick_valuereference with the new getter: Save scripts.
Architectural note. This step changes the click handler to write through
GameState.add_to_resource("light", ...)instead ofLight.value += .... The M2.1 click handler usedLight.value; M3.3 introduced the parallelGameState._resources["light"]and acknowledged the dual write-path. M6.2 is the moment that dual path resolves in practice: the click handler now writes throughGameStateexclusively, bypassing theLight.valuesetter (M2.2) and theLight.value_changedsignal entirely. The CounterLabel's update path now depends entirely onGameState.resource_changed. If your CounterLabel script still listens toLight.value_changed(M2.3's original wiring), it will stop receiving updates after this edit — confirm the M3.3-era refactor connected the Label toGameState.resource_changedas 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. PressF5. 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. SetGameState._resources["honor"] = 5.0. The next_recalculate_incomecall (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 be1.0 × (1.0 + 0.05 × 5) = 1.25Light 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_lightabove 1M and Honor accumulating, the pledge wipes Light and buildings but Honor is preserved. After the pledge, the next click should still earn1.25Light (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.1Light/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.15Light/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.0 — Honor only multiplies click income, not tick income.
B. 62.0 — Honor adds 0.1 × 12 = 1.2 Light/s flat to the rate.
C. 110.0 — 50 × (1.0 + 0.1 × 12) = 50 × 2.2. Honor scales the rate multiplicatively.
D. 60.0 — Honor 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
GameStatecomputed from current Honor:1.0 + 0.1 × honor. Recomputed on every_recalculate_incomecall. Not saved — reconstructed from_resources["honor"](which is saved). Multiplied into income alongsidetick_multiplierin_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-sidehonor_multiplierwith a smaller per-Honor coefficient (clicks are active, ticks are passive — passive gets the larger reward by design). Applied inget_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 bypurchase_upgrade—tick_multiplieris cached state,honor_multiplieris derived state.