One Decision, Not a Committee
Last post was about the Utility AI system — how an NPC scores a handful of options and picks the highest one. This time it's about something that looks like the same mechanism but isn't: what happens when the schedule already knows the answer.
A schedule entry says: 8am, forge, Work. Simple enough on paper. But the Utility AI system doesn't read English — it reads scores. So somewhere between "the schedule says Work" and "the NPC is standing at the anvil swinging a hammer," something has to translate intent into action. That's the part I got wrong on the first pass.
The tempting wrong answer
The instinct is to treat Work, Sleep, and Break exactly like Combat and Flee — as siblings, each scoring itself and competing for control every frame. It's consistent with how the rest of the tree works, and it means no special case for schedule-driven behavior.
Except it falls apart the moment you think about what "competing" actually means here. So there were really two ways to wire it up:
A) Give Work, Sleep, and Break each their own top-level state, scored and competing directly against Combat and Flee.
B) One Schedule state, sitting at idle priority, that internally resolves to whichever activity is currently active.
Combat and Flee earn their spot at the top because they're genuinely uncertain — the AI doesn't know in advance whether there's danger nearby, so it has to check every frame. Work, Sleep, and Break aren't uncertain at all. The schedule already picked. Giving each one its own top-level slot means every single one has to independently ask "is it actually my turn?" and answer no most of the time. That's not competition — it's the same lookup done three times over instead of once.
Worse: every new activity means wiring a new state into the part of the tree that's supposed to stay small and stable — the Combat / Flee / Idle comparison. That list should almost never grow.
One Schedule node, one job: figure out where the NPC should be, walk there if it isn't, and hand off to whichever ActivityTag is active. Everything below that — which UtilityBlock, which animation, which exact spot to stand — lives inside the handoff, not in the top-level comparison.
The nice side effect is that the same tag can mean two completely different things depending on who's running it. Work for the blacksmith is a hammer-swing loop at the forge. Work for the bartender is wiping the same spot on the counter and glancing at the door every so often. Same tag, same schedule system, two different blocks — the variation lives in the mapping, not in a pile of increasingly specific tags like WorkAtForge and WorkAtBar.
Scoring doesn't disappear, it just moves down a level. If the bartender's Work block has three valid stools behind the counter, that's where the Utility AI earns its keep again — picking the nearest free one. It's proper competition, just scoped to a decision that's actually still open.
Combat and Flee still sit above Schedule as full siblings, which matters more than it sounds — it's what lets an NPC mid-swing at the forge actually notice a gunfight starting outside and drop the hammer. If Schedule owned the whole tree, interrupting it would mean tearing into activity logic instead of simply outscoring it. Keeping that boundary is what makes an NPC feel like it's paying attention, not just running a script with extra steps.