doc 8 of 9

Stage 06 — The harvest counts: an inventory that's data

You will build: items and an inventory as pure data resources — harvesting a mature crop clears the plot, credits the inventory, and anything can observe the change. You'll learn: Resource signals · data-first inventory design. This stage is mostly requirements rather than code — deliberately: you've now met every ingredient it needs. If you want the worked version of the ideas underneath it, or you get stuck, Systems → Inventory is the full treatment — part 01 covers the definition/state split this stage leans on hardest. Warm-up — from memory:

  1. From stage 05: why can't USE_TOOL and WALK be true at once?
  2. From stage 04: where does days_grown live, and why not in CropData?
  3. From the rhythm project, dredge it up: how does a signal get connected in code, Godot-4 style — write the line.

Why this exists#

The architecture's rule: truth lives in data objects; nodes render and mutate the truth. An inventory that's a Resource with a changed signal doesn't know or care whether a debug print, a hotbar, or a shop screen is listening — all of those are interchangeable observers. Build the data right and the future UI module becomes trivial; build it into a UI node and the future save module becomes surgery. Resources can declare and emit signals like any Object (Resources).

Build it — requirements, not recipes#

You have every tool this stage needs. What follows is what, not how.

A — ItemData#

items/item_data.gd: a Resource definition with a display_name (icon slot can wait). One asset: items/turnip_item.tres. Wire produce to crop: add @export var yield_item: ItemData and @export var yield_count: int to CropData; fill them on turnip.tres and potato.tres.

B — Inventory#

items/inventory.gd: a Resource holding item counts. Requirements:

  • typed storage mapping ItemDataint
  • a change notification — and here's a trap worth hitting knowingly: don't declare signal changed. Resource already has one, and redeclaring an inherited member is a parse error. Emit the inherited signal with emit_changed()
  • func add(item: ItemData, amount: int) -> void — merges counts, emits
  • func count_of(item: ItemData) -> int
  • adding zero or negative amounts is a programmer error — make it loud (the docs for assert will tell you where it belongs and when it runs)

The player owns one: var inventory: Inventory = Inventory.new() — constructed, not .tres-loaded. Checkpoint question below asks why; think about it before reading on.

C — Harvesting#

Extend the act dispatch (stage 04's _use_plot): a plot whose crop is_mature() → free the Crop node, reset the plot to tilled and empty, inventory.add(...) from the crop's definition. The FSM swing from stage 05 should already gate this for free — no new guard code, which is the point of having built it first.

D — Observe it#

Connect inventory.changed (in the player's _ready) to a handler that prints every item and count. This print handler is this slice's UI — and its replacement by a hotbar later must not require touching Inventory at all. If your design would require it, the signal is carrying too little or the handler knows too much — fix the design, not the handler.

Predict before you run: plant two turnips, grow both, harvest both. Exactly how many lines does your handler print across the whole session, and what does the last one say?

E — Commit#

git add . && git commit -m "stage 06: ItemData, Inventory resource, harvesting"

Checkpoint — definition of done#

  • Harvesting a mature crop: crop disappears, plot is immediately replantable, Output shows the updated count
  • Harvesting an immature crop does nothing
  • Two different crops accumulate separate counts in one inventory
  • Zero warnings; committed
  • You can explain both: why is Inventory a Resource rather than a Node — and why is the player's inventory Inventory.new() rather than a preloaded .tres? (The second answer is stage 04's shared-.tres lesson wearing a new hat.)

Stretch (no instructions)#

Seeds are items too: planting consumes a seed from the inventory; harvesting yields produce and seeds. Watch the design pressure this puts on act's dispatch — and enjoy that the state machine doesn't care.

If you get stuck#

  • changed never fires → did add actually emit? Resources emit like any object; print inside add first, then inside the handler — which print is missing tells you which side is broken.
  • Count resets between harvests → you're constructing a fresh Inventory somewhere per harvest; who owns it, and how many times does that owner's _ready run?
  • Typed dictionary refuses ItemData keys → check your Godot minor version supports typed dictionaries with Resource keys (4.4+); if stuck on the syntax, the GDScript reference's typed-collections section is the source of truth.