doc 7 of 9

Stage 05 — The farmer thinks: a state machine

You will build: the player's state machine — IDLE, WALK, USE_TOOL — with tool use that takes real time and locks movement while it happens. You'll learn: enum state machines — and design decision 1 from the architecture doc comes due here at the latest. The general treatment of this pattern, including the rungs above it, is Systems → State Machines — part 02 is the one that matches this stage. Read it now or straight after. Warm-up — from memory:

  1. From stage 04: definition vs runtime state — which one was days_grown, and where did it live?
  2. Write from memory: a typed signal declaration announcing a harvest of some item count.
  3. From stage 02: what slides the farmer along a wall instead of stopping dead?

Why this exists#

Right now "is the farmer busy?" has no answer — act spam re-triggers instantly, and nothing stops movement mid-swing. The flag road (is_using_tool, can_move, is_planting…) breeds contradictions: flags multiply and combinations nobody meant become reachable. One enum makes invalid states unrepresentable — the smallest real state machine, and design decision 1 from the architecture doc. (If your recorded choice was A — plain ifs: build this stage your way, hit the same checkpoint, and write one honest paragraph in the architecture doc about which parts fought you. Comparing the pain is the lesson; converting later is cheap.)

Build it#

A — The enum and the switch#

In player.gd — the enter/exit structure is given; the transitions are yours to write:

enum State { IDLE, WALK, USE_TOOL }

var state: State = State.IDLE

func _set_state(new_state: State) -> void:
	if new_state == state:
		return
	# exit side effects of the old state, if any
	state = new_state
	match state:
		State.IDLE:
			pass   # what stops when you stop?
		State.WALK:
			pass
		State.USE_TOOL:
			pass   # start the swing timer; freeze intent

Transitions in _physics_process: IDLE↔WALK from whether direction is zero — but only when not mid-swing. Guard reads: if state != State.USE_TOOL:. That single guard is the whole payoff — say why out loud.

B — The swing takes time#

Add a Timer child named ToolTimer: one_shot on, wait_time 0.25. Wire its timeout signal (editor-connect or code-connect — you've now done both across two projects; pick one and be consistent) to return the farmer to IDLE. Pressing act:

  • in IDLE/WALK → _set_state(State.USE_TOOL), then do the plot action from stage 04
  • in USE_TOOL → ignored entirely

Movement while USE_TOOL: velocity = Vector2.ZERO — the farmer is committed to the swing.

C — Make state visible (debug juice)#

No animations yet, but state should be observable: modulate the sprite per state in the match (WALK = normal, USE_TOOL = a tint). Placeholder feedback now, AnimatedSprite2D in a later module — same match, different lines.

Predict before you run: hold a movement key, then hammer act. Describe the exact sequence of states for one swing — and what the farmer does the frame the timer fires while the key is still held. Then run and watch the tints.

D — Commit#

git add . && git commit -m "stage 05: player state machine, timed tool use"

Checkpoint — definition of done#

  • Farmer freezes for 0.25s on every act; movement resumes exactly when the tint drops
  • Act during a swing does nothing — no queued double-tills
  • Walking into a wall while swinging doesn't wedge the state (still returns to IDLE)
  • Zero warnings; committed; decision 1's outcome and reason recorded in the architecture doc
  • You can explain: in the old flag world, what combination like is_using_tool && is_walking meant nothing — and what grammar rule of the enum forbids it?

Stretch (no instructions)#

A hold-to-charge swing: releasing act after a full second tills a 3-cell row. The state machine earns its keep — where does CHARGING live?

If you get stuck#

  • Farmer freezes forever → is the Timer one_shot? Did timeout actually connect (check the signal panel's connection icon)? Print inside the handler — is it firing?
  • Swing does nothing to the plot → order matters: are you changing state before or after dispatching the plot action, and which did you intend?
  • States flicker IDLE/WALK every frame → your transition condition and your guard are fighting; print state only on change (that's what _set_state's early return is for) and read the log like a story.