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:
- From stage 04: definition vs runtime state — which one was
days_grown, and where did it live? - Write from memory: a typed signal declaration announcing a harvest of some item count.
- 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_walkingmeant 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? Didtimeoutactually 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
stateonly on change (that's what_set_state's early return is for) and read the log like a story.