doc 5 of 9

Stage 03 — The soil grid: tiles you can till

You will build: a tile-painted field and the act of tilling — pressing a key turns the grass cell in front of the farmer into soil, in both pixels and data. You'll learn: TileMapLayer + TileSet · keeping game truth out of visuals · typed dictionaries — and you'll make design decision 2 from the architecture doc. Warm-up — from memory:

  1. Layers vs masks — which one is "what I am" and which is "what I look for"?
  2. Write from memory: the full signature of a typed function taking a Vector2i cell and returning whether tilling succeeded.
  3. From stage 01: what does Input.get_vector clamp its result to, and why did that matter?

Why this exists#

A TileMapLayer paints a grid efficiently — one node, thousands of cells (Using TileMaps; note it's TileMapLayer, one node per layer — the single-TileMap-with-layers workflow died in Godot 4.3, and most online tutorials predate that). But tiles are pixels, not state: "is this plot tilled? watered? what's planted?" is savable game truth, and the architecture splits truth from presentation on purpose.

Design decision 2 — make the call now, record it (and your reason) in the architecture doc:

  • (A) Dictionary[Vector2i, PlotState] beside the tilemap — tilemap purely visual; trivially savable; one lookup per interaction. Recommended, and the build steps below assume it.
  • (B) TileSet custom data layers — state rides the tiles; fewer structures; save/load and growth iteration get awkward. If you choose B, the build steps are yours to derive from the TileSet docs — a deliberate spec-level exercise. Record your choice and your reason either way.

Build it#

A — A TileSet with two tiles#

Add Ground (TileMapLayer) under World (above the bodies in the tree so it draws behind). Inspector → Tile Set → New TileSet, tile size 32×32. In the TileSet panel add an atlas with your placeholder art — two 32×32 tiles are enough: grass, tilled. (Placeholder squares were the plan; real art swaps in later without touching logic.) Paint a field.

B — The farmer faces somewhere#

Tilling needs "the cell in front of me." In player.gd, keep a facing direction — a few pieces are blanked below; those decisions are yours to make:

var facing: Vector2 = Vector2.DOWN

func _physics_process(_delta: float) -> void:
	var direction: Vector2 = Input.get_vector("move_left", "move_right", "move_up", "move_down")
	if direction != ______:            # only update facing while actually moving — why?
		facing = direction.______()    # snap to the dominant axis; find it in the Vector2 class ref
	velocity = direction * speed
	move_and_slide()

One of those blanks has three plausible answers in the class reference — pick by testing diagonals and watching which feels right for a 4-directional farmer.

C — The FarmGrid: truth lives here#

New child of World: plain Node named FarmGrid, script farm_grid.gd. The signatures and intent comments are given; the bodies are yours to write:

class_name FarmGrid
extends Node

## One plot's runtime state. Grows in stage 04.
class PlotState:
	var tilled: bool = false

@export var ground: TileMapLayer

var _plots: Dictionary[Vector2i, PlotState] = {}

## Turn a world position into the cell it lands in.
func cell_at(world_pos: Vector2) -> Vector2i:
	# ground.local_to_map + ground.to_local — mind which space each expects
	pass

## Till the cell if it's grass. Returns whether anything changed.
func till(cell: Vector2i) -> bool:
	# 1. already tilled? refuse.  2. record truth in _plots  3. repaint via ground.set_cell
	pass

set_cell's signature (coords, source_id, atlas_coords) trips everyone once — read it in the TileMapLayer class reference before writing, not after failing.

D — The act action#

Input Map: add act (E and/or Space). In player.gd, on act just-pressed, ask the grid to till cell_at(global_position + facing * 32.0). Getting the FarmGrid reference is a design decision you've already met in the architecture — the parent injects it (@export), the child never reaches out.

Predict before you run: stand at the field's left edge facing left and press act. What cell gets computed — and should till refuse it? Does your code?

E — Commit#

git add . && git commit -m "stage 03: tileset, facing, FarmGrid tilling"

Checkpoint — definition of done#

  • Grass cell in front of the farmer becomes tilled soil on act — visually AND in _plots (prove the second half with one temporary print)
  • Tilling the same cell twice does nothing the second time
  • Facing works in all four directions (till above, below, left, right)
  • Zero warnings; committed; decision 2 recorded in the architecture doc, with a reason
  • You can explain: if you deleted the Ground node entirely, what tilling information survives — and why is that the design's whole point?

Stretch (no instructions)#

Hoe range: tilling works from one tile away but not two — and something visual (a modulate flash, anything) tells the player which cell act would hit right now.

If you get stuck#

  • Tilled tile paints in the wrong place → you're mixing world/local space; print both the input position and the mapped cell, walk one tile, watch how each changes.
  • Nothing paints → is ground actually assigned in the Inspector? (@export nodes are null until wired — the error message will have told you exactly this.)
  • set_cell errors → count its arguments against the 4.x class reference; a Godot-3 or early-4 snippet has a different shape.