BuildingData Resource¶
What you'll learn
- How
BuildingDatadiffers fromUpgradeDatadespite both beingResourcesubclasses — buildings are repeatable (own many) where upgrades are one-time (own once or never). - The
base_cost+cost_multiplier+base_outputshape and the per-purchase scaling formulacost = base_cost × cost_multiplier^count. - Why a method on the Resource (
get_cost(count: int) -> float) is preferred over a precomputed Array of costs — methods read live data, Arrays go stale on balance edits. - The five-tier blood-knight ladder (Initiate → Aspirant → Adept → Sunwalker → Champion) and how the same
BuildingDatashape carries every tier with only field values changing. - The
pow(base, n)math and why1.15is the genre-canonical scaling factor that makes the first ten purchases approachable and the hundredth purchase aspirational.
How it applies
- Designer-tunable economy. Every cost curve, output rate, and tier balance is one number in one
.tresfile. A balance pass to slow the early game (raisecost_multiplierfrom1.15to1.18) is one edit per building; the math compounds across all owned counts automatically. - Save-format minimalism. The save records
_buildings: Dictionary[String, int]— five entries, each a building id mapped to an owned count. Total state for the entire economy is a few hundred bytes regardless of session length. Per-purchase events do not bloat the save; only the cumulative count does. - Runtime introspection. A QA tester sees
_buildings["initiate"] = 47in the Remote tab. The current cost of the next Initiate isBuildingData.get_cost(47)— they call it from the REPL and get the answer in microseconds. The economy state is queryable from outside the source code. - Balance-versus-engineering separation. Engineering owns
BuildingData.gd(the schema). Designers own the.tresfiles (the values). The two roles touch different files; merge conflicts are rare; sign-off cycles are independent. - Modding curve customization. A mod can ship a new
.treswithcost_multiplier = 1.30(extreme exponential growth) for an "Iron Mode" challenge, or1.05(very gentle) for an accessibility variant. Same building id, different.tres, swapped at startup. The math is the same; the curve is data.
A note on the blood-knight order¶
The example table later in this chapter introduces the five building tiers — Initiate, Aspirant, Adept, Sunwalker, Champion — by name for the first time in the textbook. These are the acolytes the player has been training since M2.1's Train button. Until now the project has used placeholder flavor; M5 is where the fiction concretizes. The acolytes tithe Light to the chapel; trained acolytes train more advanced acolytes; the advanced tiers (Sunwalkers, Champions) draw on the prestige currency (Honor) you will earn in M6. The tier names are not arbitrary; they carry the order's internal hierarchy and recur in M6's Crusade ceremony.
Concepts¶
Buildings are repeatable, upgrades are one-time¶
The architectural distinction between BuildingData and UpgradeData is cardinality. An UpgradeData is owned at most once — _purchased_upgrades.has(id) is a yes/no question. A BuildingData is owned 0 to N times — the question is "how many?"
This changes every downstream system:
- The data structure:
_buildings: Dictionary[String, int](id → count) instead ofDictionary[String, UpgradeData](id → reference). - The UI: a row that shows "Initiate × 47" with a Buy button that always works (until the player can't afford the next), instead of a row that disappears on first purchase.
- The cost:
base_cost × cost_multiplier^count, increasing with each purchase, instead of a fixed cost. - The output integration: each building contributes
base_output × count × multipliersto passive income, summed across all owned buildings.
BuildingData is therefore a sibling of UpgradeData, not a subtype. Both extend Resource; the data shapes are different because the cardinality is different.
Example
A blood-knight idle economy might have three Blessings (one-time multipliers on click) and five tiers of acolytes (Initiate, Aspirant, Adept, Sunwalker, Champion — each ownable hundreds of times). The Blessings are UpgradeData; the acolytes are BuildingData. The same Inspector-edit workflow drives both, but the data structures, UI, and game logic diverge after the file format.
base_cost, cost_multiplier, base_output¶
The three fields that define a building's economic curve:
base_cost— the cost of unit zero. The first Initiate costsbase_cost; the second costsbase_cost × cost_multiplier; the eleventh costsbase_cost × cost_multiplier^10.cost_multiplier— the per-unit growth factor.1.15is the Cookie Clicker default and the genre's lingua franca;1.10is gentle (purchases stay frequent for longer);1.30is harsh (the curve becomes punishing fast).base_output— the per-second contribution per owned unit, before any multipliers. An Initiate atbase_output = 0.1produces 0.1 Light per second; ten Initiates produce 1.0; a hundred produce 10.
The triple (base_cost, cost_multiplier, base_output) is the entire description of a tier's economy. Two buildings with the same triple are economically identical (only flavor differs). Tuning a tier is a balance-pass on three numbers.
Example
The five-tier blood-knight ladder, each tier roughly 10× the prior in base_cost and proportionally more output. The numeric pattern across the genre is approximately:
| Tier | base_cost | base_output |
|---|---|---|
| Initiate | 15 | 0.1 |
| Aspirant | 100 | 1.0 |
| Adept | 1100 | 8.0 |
| Sunwalker | 12000 | 47.0 |
| Champion | 130000 | 260.0 |
Numbers are rough; balance passes adjust them. The ratio base_output / base_cost increases per tier (later tiers are more efficient per Light spent), driving the player to higher tiers as the game progresses.
get_cost(count) as a method, not an array¶
The cost of the next building is base_cost × cost_multiplier^count. A naive design pre-computes an Array of costs (costs[0] = 15, costs[1] = 17.25, ...) up to some cap. This goes stale the moment a balance edit touches base_cost or cost_multiplier.
The method form computes on demand:
The method reads base_cost and cost_multiplier from the Resource fields at call time. A balance edit changes the fields; the next get_cost call uses the new values. Both work in the editor (live values from the .tres) and at runtime (the same fields). No precomputed cache means no cache invalidation.
Performance: pow is microseconds. A UI refresh that calls get_cost per row, ten rows, twenty times per second is tens of microseconds per second. The Array-cache optimization is not worth the staleness risk.
Example
A balance pass changes Initiate's cost_multiplier from 1.15 to 1.18. A pre-computed Array still holds the old values; the player buys an Initiate at the old cost; the discrepancy surfaces when reading the data file but not the cached costs. With the method form, the change is live: the next get_cost(count) call reads 1.18 and produces the correct cost. The data is the source of truth; methods read the data; nothing caches. Simplicity earns its keep here.
The formula has a name, and a paper¶
The cost curve Cₙ = base_cost × cost_multiplier^n and the multiplier 1.15 are not arbitrary genre folklore. Demaine, Ito, Langerman, Lynch, Rudoy, and Xiao (2018) analyzed Cookie Clicker's economy and proved several results worth citing:
- The item tuple
(x, y, α)— every building is described by three numbers:xis the per-second output gain,yis the initial cost, andα∈ [1, ∞) is the multiplicative cost increase per purchase. Cookie Clicker ships withα = 1.15. This curriculum'sBuildingDatacorresponds:base_output = x,base_cost = y,cost_multiplier = α. - The cost formula is
Cₙ = C₁ · α^(n-1), which is whatget_cost(count)computes. Then-1exponent (vs the chapter'sn) is a 1-based-vs-0-based indexing choice; both produce the same sequence. - Optimal play decomposes into two phases. Demaine et al. prove that every optimal strategy is a Buying Phase (purchase items as fast as you can afford them) followed by a Waiting Phase (no further purchases, accumulate to the goal). Claim 1.1 in their paper: "If the next step of the optimal strategy involves buying an item at some point in the future, you should buy the item as soon as you can afford it." For the single-item case, the optimal time to reach
Mcookies is(y/x) · ln(M/y). - Greedy is asymptotically optimal. For the multi-item case, "buy whatever has the best ratio of rate-gain-to-cost right now" is provably within
1 + O(1/log M)of the true optimum at largeM. Players who develop this heuristic during play are not just intuiting — they are converging on the strategy the math endorses.
These results matter for the game's design (they justify the curve shape and the player intuition that "tier-up beats max-out") and they matter for the chapter's pedagogy (the player's "10 of these costs 10× one of them" intuition is deliberately wrong, and that wrongness is the engagement engine — the cost curve is a math problem the player slowly internalizes).
The paper is at https://erikdemaine.org/papers/CookieClicker_GC/paper.pdf. Worth a read once the M5 system is in place; the optimal-play proof formalizes what idle-game players have always known intuitively.
pow(base, n) and the 1.15 convention¶
Idle/incremental games converged on 1.15 as the default cost_multiplier after Cookie Clicker established it (the reasoning is in the Demaine et al. analysis above). The math:
1.15^10 ≈ 4.05— the tenth purchase costs ~4× the first.1.15^30 ≈ 66.2— the thirtieth costs 66×.1.15^100 ≈ 1174— the hundredth costs 1174×.
The curve is gentle in the early game (a player can buy 5–10 of a tier in their first session) and steep in the late game (the hundredth costs more than the previous ninety combined, encouraging the player to ascend to the next tier rather than max out the current one). The asymmetry is the design lever.
1.10 flattens the curve — players max out tiers more easily, the late game becomes "buy more of what works" rather than "switch tiers." 1.30 steepens it — early purchases feel sparse, the player is forced through tiers fast. Genre balance points cluster around 1.15; deviations are deliberate design choices that should be playtested.
Example
A "casual" idle game variant uses 1.07 for all buildings. The first ten cost 2×, the hundredth costs 868×. Gentler ramp, slower late-game wall, faster mid-game progression. A "hardcore" variant uses 1.25. The first ten cost 7×, the hundredth costs 4.6 billion ×. Brutal late-game, players forced to next tier within minutes. The same engine, the same script, the same BuildingData shape — just one number per file changed.
unlock_threshold reused from M4.4¶
BuildingData shares the unlock-threshold concept with UpgradeData: a per-building unlock_threshold field gates visibility in the Building list. Initiate is visible from the start (threshold = 0); Aspirant appears at 100 Light; Champion only at 100,000. The same locked / available / owned tri-state from M4.4 applies, with one twist: a building is never owned in the M4.3 sense — it can always be re-purchased. The state is locked / available, with a count badge instead of removal.
The reuse is intentional. Designer experience is "unlock thresholds work the same way for both." Engineering experience is "the threshold-check code is one line in two places." Save format is "buildings have counts, upgrades have ownership flags, both gate by threshold."
Example
A late-game player with Light = 2,000,000 sees Initiate × 87, Aspirant × 64, Adept × 31, Sunwalker × 12, Champion × 4. Every tier is past its threshold. The list is full; every row is enabled or disabled based on the next-purchase cost. The threshold mechanism scales identically from "first session, only Initiate visible" to "long session, every tier visible."
Walkthrough¶
You will perform these in your own Godot editor.
- In the FileSystem dock, right-click
scripts/→ New → Script. Name itbuilding_data.gd. Click Create. - Replace the body. The full code-fragment ceiling for this chapter, in two pieces:
Nine
class_name BuildingData extends Resource @export var id: String = "" @export var display_name: String = "" @export var description: String = "" @export var base_cost: float = 15.0 @export var cost_multiplier: float = 1.15 @export var base_output: float = 0.1 @export var unlock_resource: String = "light" @export var unlock_threshold: float = 0.0@exportdeclarations. Compare toUpgradeDatafrom M4.1: sameid/display_name/description/unlock_resource/unlock_thresholdshape, different economic fields (base_cost/cost_multiplier/base_outputinstead ofcost/effect_type/multiplier). - Add the cost calculation method:
The only behavior on this Resource. Pure function: no side effects, no signal emits, no state mutation. Save (
Ctrl+S). - Author the first
.tres. In FileSystem dock, navigate toresources/. Ifbuildings/does not exist, right-click → New Folder → namebuildings. Click into it. - Right-click → New → Resource… → BuildingData. Save as
initiate.tres. In Inspector: Id:initiateDisplay Name:InitiateDescription:A novice acolyte who tithes Light.Base Cost:15.0Cost Multiplier:1.15Base Output:0.1Unlock Resource:lightUnlock Threshold:0.0(visible from start)- Repeat for the second tier:
aspirant.tres. Inspector: Id:aspirantDisplay Name:AspirantDescription:A devoted aspirant tithing more Light per moment.Base Cost:100.0Cost Multiplier:1.15Base Output:1.0Unlock Threshold:50.0- Repeat for the third tier:
adept.tres. Inspector: Id:adeptDisplay Name:AdeptDescription:A practiced adept whose Light flows freely.Base Cost:1100.0Cost Multiplier:1.15Base Output:8.0Unlock Threshold:500.0- (Optional) Author
sunwalker.tres(base_cost = 12000.0,base_output = 47.0,unlock_threshold = 6000.0) andchampion.tres(base_cost = 130000.0,base_output = 260.0,unlock_threshold = 65000.0) for late-game progression. M5.2 will iterate whatever.tresfiles are dropped into the BuildingList; three is enough to test the system, five completes the ladder. These two tiers are optional for M5–M7 — the textbook's walkthroughs only require Initiate, Aspirant, and Adept. The two extra tiers become meaningful in late-game play: their per-tick output is 47.0 and 260.0 Light respectively, dwarfing the lower tiers, and they unlock at thresholds (6,000 and 65,000 Light) that a player typically reaches in their second or third Crusade run. - Verify the cost calculation. The current count for any building is zero (no buildings owned yet).
Initiate.get_cost(0) = 15.0 × 1.15^0 = 15.0;Initiate.get_cost(1) = 15.0 × 1.15^1 = 17.25;Initiate.get_cost(10) = 15.0 × 1.15^10 ≈ 60.7;Initiate.get_cost(100) ≈ 17,617.6. M5.2 will display these values in the row UI; for now they exist only as method calls. - Press
F5. No visual change —BuildingDatais data,BuildingList(the emptyVBoxContainerfrom M4.2) does not yet read the array. M5.2 will populate it.
Optional sanity check. Open
initiate.tresin a text editor outside Godot. Verify the file is roughly 10 lines: one[gd_resource]header, one[ext_resource]block referencingbuilding_data.gd, one[resource]block with each@exportfield's value. Theunlock_threshold = 0.0line should be present (saved even when equal to default, in 4.6) — the resource format is explicit, not implicit.
Self-check quiz¶
Q1 — A balance pass changes Initiate's base_cost from 15.0 to 20.0. The player has owned 30 Initiates. What is the cost of their next (31st) Initiate, and how was it computed?
A. 20.0 — costs reset to the new base on balance edits.
B. 20.0 × 1.15^30 ≈ 1324 — the new base, scaled by the existing count.
C. 15.0 × 1.15^30 ≈ 993 — the old base persists, balance edits do not affect already-owned-track counts.
D. 15.0 × 1.15^31 ≈ 1141 — the next purchase scales from the old base.
Reveal answer
B — get_cost(count) reads live fields. The method return base_cost * pow(cost_multiplier, count) reads base_cost from the Resource at call time. The balance edit changed the field; the next get_cost(30) call uses the new value. Result: 20.0 × 1.15^30 ≈ 1324. The same change applies retroactively to the cost of the next purchase but does not refund or charge anything for the previously-purchased units. A is wrong: counts persist; only the price scaling changes. C describes a precomputed-cache design that the method form explicitly avoids. D is wrong: the count for "next purchase" is the current count (30), so the exponent is 30, not 31. The lesson: methods read live data, balance edits propagate to future purchases automatically, no cache invalidation work needed.
Q2 — You compute cost_multiplier^100 for a building with cost_multiplier = 1.15. The result is approximately 1174. If you instead used cost_multiplier = 1.07, what is 1.07^100, and what does this say about the curve's ergonomics?
A. About 868 — slightly cheaper at unit 100, the curve is materially gentler.
B. About 1174 — same answer, the multiplier is irrelevant to large exponents.
C. About 1500 — counterintuitively higher because of floating-point error.
D. About 0.7 — 1.07 is below 1 and the curve flattens.
Reveal answer
A — 1.07^100 ≈ 868, gentler curve. A multiplier slightly above 1.0 produces exponential growth; the rate of growth is tiny per step (1.07 adds 7% per unit) but compounds across many units. 1.07^100 = 868 versus 1.15^100 = 1174 — the lower multiplier saves about 25% on the hundredth unit's cost. The whole curve is shifted downward; players reach higher counts before hitting the wall, the late game stretches further. B is wrong: small differences in the multiplier produce large differences at high exponents (the very nature of exponential growth). C imagines floating-point misbehavior at this scale — pow(1.07, 100) is exact to many digits. D is wrong: 1.07 > 1, the curve grows, just less aggressively. The lesson: cost_multiplier is a very sensitive design knob; a 0.05 change at unit 100 changes cost by 35%.
Q3 — BuildingData and UpgradeData both extend Resource. Why are they not designed as one combined type with a 'is_repeatable: bool' flag?
A. The flag would work; the textbook just chose two types for clarity.
B. A single type with a flag forces every consuming method (purchase_*, get_cost_*, _apply_effect_*) to branch on is_repeatable; two types keep each consumer focused on one shape.
C. Godot's Inspector cannot show conditional fields based on a boolean flag.
D. The save format breaks if a single type holds both a "count" and a "purchased" boolean.
Reveal answer
B — type discipline pays in code clarity. A combined type would make every UI loop, purchase guard, and effect dispatcher branch: "is this a repeatable building or a one-time upgrade?" Every consumer carries the dispatch, the dispatch is duplicated, the dispatch is the bug source. Two types means each consumer code path operates on exactly one shape: purchase_upgrade(UpgradeData) knows it's one-time; purchase_building(BuildingData) knows it's repeatable. The type system carries the distinction. A misses the structural reason. C is wrong: _validate_property and @export patterns can hide fields conditionally, but it's cosmetic — the underlying values still exist. D is wrong: a combined type's save format would just have nullable fields; the issue is consumer-side, not save-side.
Integration question¶
Q4 — open
BuildingData reuses UpgradeData's schema (id, display_name, unlock_threshold) but introduces base_cost, cost_multiplier, base_output, and get_cost(count). The UpgradeData had cost (a single fixed value), effect_type, and multiplier. Compare the two designs: which fields are unique to which Resource type, and which exist on both? What does this say about composing two Resource types from a shared "common identity / unlock" base?
Reveal expected answer
Common to both: id, display_name, description, unlock_resource, unlock_threshold — the identity and visibility fields. Unique to UpgradeData: cost, effect_type, multiplier — the one-time-purchase and direct-multiplier-effect fields. Unique to BuildingData: base_cost, cost_multiplier, base_output, get_cost() — the repeatable-purchase and exponential-scaling fields. The architectural pattern: two Resources share a common "what is this thing called and when does it appear" surface, with type-specific economics layered on top. A natural extension is to factor the common fields into a parent Resource (PurchasableData with id, display_name, unlock_*) and have both UpgradeData and BuildingData extend it. Godot supports this — extends PurchasableData would inherit the common fields, then each subclass adds its own. The textbook does not do this refactor because: (1) the duplication is small (five fields), (2) Resource inheritance creates editor-side complications (Inspector dropdowns expecting concrete types, .tres files locked to a class hierarchy), (3) the principle "prefer flat schemas to deep hierarchies" applies to Resources as it does to scenes. The deeper architectural property: shared schemas across Resource types are an inheritance opportunity, but the inheritance cost (editor complexity, save-format coupling) often exceeds the duplication cost. Two flat schemas with overlapping field names are usually the lighter design.
Glossary¶
Glossary
BuildingData- A
Resourcesubclass for repeatable purchases — generators the player can own many of, each adding to passive income. Distinct fromUpgradeData, which is one-time. Fields:id,display_name,base_cost,cost_multiplier(typically1.15),base_output,unlock_threshold. Methods:get_cost(count) -> float. base_cost/cost_multiplier- The two fields that define a building's exponential cost curve.
base_costis the cost of unit zero;cost_multiplieris the per-unit growth factor. Genre canon is1.15; common range is1.07(gentle) to1.30(brutal). Cost of unit N isbase_cost × cost_multiplier^N. pow(base, n)- GDScript's exponentiation built-in. Returns
base^nasfloat. Used for cost scaling (base_cost × pow(cost_multiplier, count)) and any other exponential math. Distinct from integer-only**syntax —powalways produces a float.