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:
- From stage 05: why can't
USE_TOOLandWALKbe true at once? - From stage 04: where does
days_grownlive, and why not inCropData? - 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
ItemData→int - a change notification — and here's a trap worth hitting knowingly: don't declare
signal changed.Resourcealready has one, and redeclaring an inherited member is a parse error. Emit the inherited signal withemit_changed() func add(item: ItemData, amount: int) -> void— merges counts, emitsfunc count_of(item: ItemData) -> int- adding zero or negative amounts is a programmer error — make it loud (the docs for
assertwill 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
Inventorya Resource rather than a Node — and why is the player's inventoryInventory.new()rather than apreloaded.tres? (The second answer is stage 04's shared-.treslesson 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#
changednever fires → didaddactually emit? Resources emit like any object; print insideaddfirst, then inside the handler — which print is missing tells you which side is broken.- Count resets between harvests → you're constructing a fresh
Inventorysomewhere per harvest; who owns it, and how many times does that owner's_readyrun? - Typed dictionary refuses
ItemDatakeys → 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.