doc 6 of 9

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:

  1. From stage 03: which node knows the truth about a plot, and what does the tilemap know?
  2. Write from memory: declare a typed dictionary from Vector2i to a class named PlotState.
  3. From the architecture: why must a loaded .tres never 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 .tres crop (make potato.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_local returns 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_grown and the computed stage side by side each N; compare to the paper table row by row.