Skip to content

BuildingData Resource

What you'll learn

  • How BuildingData differs from UpgradeData despite both being Resource subclasses — buildings are repeatable (own many) where upgrades are one-time (own once or never).
  • The base_cost + cost_multiplier + base_output shape and the per-purchase scaling formula cost = 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 BuildingData shape carries every tier with only field values changing.
  • The pow(base, n) math and why 1.15 is 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 .tres file. A balance pass to slow the early game (raise cost_multiplier from 1.15 to 1.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"] = 47 in the Remote tab. The current cost of the next Initiate is BuildingData.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 .tres files (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 .tres with cost_multiplier = 1.30 (extreme exponential growth) for an "Iron Mode" challenge, or 1.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 of Dictionary[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 × multipliers to 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 costs base_cost; the second costs base_cost × cost_multiplier; the eleventh costs base_cost × cost_multiplier^10.
  • cost_multiplier — the per-unit growth factor. 1.15 is the Cookie Clicker default and the genre's lingua franca; 1.10 is gentle (purchases stay frequent for longer); 1.30 is harsh (the curve becomes punishing fast).
  • base_output — the per-second contribution per owned unit, before any multipliers. An Initiate at base_output = 0.1 produces 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:

func get_cost(count: int) -> float:
    return base_cost * pow(cost_multiplier, count)

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: x is the per-second output gain, y is the initial cost, and α ∈ [1, ∞) is the multiplicative cost increase per purchase. Cookie Clicker ships with α = 1.15. This curriculum's BuildingData corresponds: base_output = x, base_cost = y, cost_multiplier = α.
  • The cost formula is Cₙ = C₁ · α^(n-1), which is what get_cost(count) computes. The n-1 exponent (vs the chapter's n) 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 M cookies 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 large M. 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.

  1. In the FileSystem dock, right-click scripts/New → Script. Name it building_data.gd. Click Create.
  2. Replace the body. The full code-fragment ceiling for this chapter, in two pieces:
    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
    
    Nine @export declarations. Compare to UpgradeData from M4.1: same id/display_name/description/unlock_resource/unlock_threshold shape, different economic fields (base_cost/cost_multiplier/base_output instead of cost/effect_type/multiplier).
  3. Add the cost calculation method:
    func get_cost(count: int) -> float:
        return base_cost * pow(cost_multiplier, count)
    
    The only behavior on this Resource. Pure function: no side effects, no signal emits, no state mutation. Save (Ctrl+S).
  4. Author the first .tres. In FileSystem dock, navigate to resources/. If buildings/ does not exist, right-click → New Folder → name buildings. Click into it.
  5. Right-click → New → Resource… → BuildingData. Save as initiate.tres. In Inspector:
  6. Id: initiate
  7. Display Name: Initiate
  8. Description: A novice acolyte who tithes Light.
  9. Base Cost: 15.0
  10. Cost Multiplier: 1.15
  11. Base Output: 0.1
  12. Unlock Resource: light
  13. Unlock Threshold: 0.0 (visible from start)
  14. Repeat for the second tier: aspirant.tres. Inspector:
  15. Id: aspirant
  16. Display Name: Aspirant
  17. Description: A devoted aspirant tithing more Light per moment.
  18. Base Cost: 100.0
  19. Cost Multiplier: 1.15
  20. Base Output: 1.0
  21. Unlock Threshold: 50.0
  22. Repeat for the third tier: adept.tres. Inspector:
  23. Id: adept
  24. Display Name: Adept
  25. Description: A practiced adept whose Light flows freely.
  26. Base Cost: 1100.0
  27. Cost Multiplier: 1.15
  28. Base Output: 8.0
  29. Unlock Threshold: 500.0
  30. (Optional) Author sunwalker.tres (base_cost = 12000.0, base_output = 47.0, unlock_threshold = 6000.0) and champion.tres (base_cost = 130000.0, base_output = 260.0, unlock_threshold = 65000.0) for late-game progression. M5.2 will iterate whatever .tres files 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.
  31. 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.
  32. Press F5. No visual change — BuildingData is data, BuildingList (the empty VBoxContainer from M4.2) does not yet read the array. M5.2 will populate it.

Optional sanity check. Open initiate.tres in a text editor outside Godot. Verify the file is roughly 10 lines: one [gd_resource] header, one [ext_resource] block referencing building_data.gd, one [resource] block with each @export field's value. The unlock_threshold = 0.0 line 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.71.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 Resource subclass for repeatable purchases — generators the player can own many of, each adding to passive income. Distinct from UpgradeData, which is one-time. Fields: id, display_name, base_cost, cost_multiplier (typically 1.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_cost is the cost of unit zero; cost_multiplier is the per-unit growth factor. Genre canon is 1.15; common range is 1.07 (gentle) to 1.30 (brutal). Cost of unit N is base_cost × cost_multiplier^N.
pow(base, n)
GDScript's exponentiation built-in. Returns base^n as float. Used for cost scaling (base_cost × pow(cost_multiplier, count)) and any other exponential math. Distinct from integer-only ** syntax — pow always produces a float.
### Ask Claude (side-channel) Try one of these in a Claude Code session if a piece did not land: - `Show me the cost ladder for Initiate (base 15, multiplier 1.15) at counts 0, 10, 50, 100. Compare against multiplier 1.07 and 1.25 — same counts, different ergonomics.` - `Walk me through what happens if I write get_cost as a const dict precomputed up to count=200. What balance-edit bug pattern does that introduce?` - `Compare BuildingData/UpgradeData with Unity ScriptableObject hierarchies — when do you inherit, when do you keep flat?`