Stage 04 — Seeds and days: things grow
You will build: crop definitions as .tres assets, planting on tilled soil, and a debug
day-advance key that grows every planted crop through its stages.
You'll learn: custom Resources and the definition/state split · runtime scene instancing —
and you'll make design decision 3 from the architecture doc.
Warm-up — from memory:
- From stage 03: which node knows the truth about a plot, and what does the tilemap know?
- Write from memory: declare a typed dictionary from
Vector2ito a class namedPlotState. - From the architecture: why must a loaded
.tresnever be mutated at runtime?
Why this exists#
A turnip and a potato differ only in data — textures, days per stage, yield. That's the
Resource pattern: class_name CropData extends Resource with @exports makes each crop an
Inspector-editable asset; adding crop #7 is a .tres file, zero code
(Resources).
The growing is runtime state — per-plot, mutable, savable — and it lives in PlotState,
never in the shared definition.
Design decision 3 — make the call now, record it (and your reason) in the architecture doc:
- (A) each planted Crop node listens for the day signal itself — decentralized, per-crop logic stays local.
- (B) the FarmGrid ticks all plots centrally — one growth loop; save/load and "skip N days" trivial. Recommended; the steps below assume it. Record your choice and reason. (Choosing A is legitimate — you'll wire the signal per-crop and derive the differences yourself; the checkpoint is identical.)
Build it#
A — CropData, the definition#
crops/crop_data.gd — short enough to give you almost whole, with one decision blanked:
class_name CropData
extends Resource
@export var display_name: String = ""
@export var stage_textures: Array[Texture2D] = []
@export var days_per_stage: Array[int] = []
func stage_count() -> int:
return stage_textures.______ # why is this the right source of truth, not days_per_stage?
Create crops/turnip.tres in the FileSystem dock (right-click → New Resource → CropData).
Fill it: 3 placeholder textures (colored 32×32 squares are fine), days [1, 1] — two
day-ticks to maturity.
B — The Crop scene#
crops/crop.tscn: Node2D root named Crop + Sprite2D. Its script holds the runtime
state and renders the definition — signatures given, bodies yours:
class_name Crop
extends Node2D
var data: CropData
var stage: int = 0
## Called by FarmGrid right after instancing.
func setup(crop_data: CropData) -> void:
# store, then show stage 0
pass
## Advance one visual stage; clamp at the last texture.
func show_stage(new_stage: int) -> void:
pass
func is_mature() -> bool:
pass
C — Planting#
Extend PlotState (stage 03) with var crop: Crop = null and var days_grown: int = 0.
In FarmGrid, a plant(cell: Vector2i, crop_data: CropData) -> bool that: refuses if the
plot isn't tilled or already has a crop → instances crop.tscn (preload + instantiate —
you did this pattern in the rhythm prototype) → positions it at the cell's world center
(ground.map_to_local — note what space it returns) → parents it under a Crops Node2D →
records it in the plot. Player-side: act on a tilled, empty plot plants; the crop to plant
is an @export var seed: CropData on the player for now — turnip today, a hotbar later.
Act now does two different things (till grass / plant soil). Keep that decision in one
place — a small _use_plot(cell) on the player or grid that checks state and dispatches.
Sprawl here is what stage 05's state machine will clean up.
D — Days pass#
Input Map: debug_next_day (N). In FarmGrid:
## One day passes for every planted plot.
func advance_day() -> void:
# for each plot with a crop: days_grown += 1
# translate days_grown → stage using data.days_per_stage, then crop.show_stage(...)
pass
The days→stage translation is the one genuinely fiddly loop in this stage. Write it on paper
first for turnip's [1, 1]: day 0 → stage 0, day 1 → stage 1, day 2 → stage 2 (mature).
Off-by-ones here are a rite of passage — the paper table is your test.
Predict before you run: press N once after planting. Which stage shows? Check against your paper table, not your hopes.
E — Commit#
git add . && git commit -m "stage 04: CropData, planting, day-tick growth"
Checkpoint — definition of done#
- Act on grass tills; act on tilled soil plants a turnip; act on a planted plot does nothing (for now)
- Two N-presses take the turnip through all three textures; further presses change nothing
- A second
.trescrop (makepotato.tres, days[2, 1]) grows on its own schedule — zero code changes - Zero warnings; committed; decision 3 recorded in the architecture doc
- You can explain: two turnips are planted; both reference
turnip.tres. Why doesn't one turnip's growth affect the other — and what field, if you'd put it in CropData instead of PlotState, would have broken that?
Stretch (no instructions)#
Watering: a tilled plot must be watered (second act with a different tool key) or the day tick skips its growth. Data, visuals, and rules — all yours.
If you get stuck#
- Crop sprite appears at the wrong spot →
map_to_localreturns local coordinates of the Ground node; whose child is the Crop? Draw the two coordinate spaces before touching code. - All crops share one look after planting two kinds → you mutated the shared definition somewhere; recall question 3 was the warning.
- Growth skips stages → print
days_grownand the computed stage side by side each N; compare to the paper table row by row.