class_name EatProvider extends WorkProvider ## WorkProvider for the Eating work category. Slots into the 5-layer pawn AI ## (Decision → WorkProvider → Job + JobRunner) as layer 2. ## ## When a pawn is hungry and not already carrying an item, scans World.items for ## the nearest edible item and builds a 3-toil eat job: walk → pickup → eat. ## ## Food priority ladder (higher = preferred): ## 3 TYPE_MEAL — cooked meal, most nutrition ## 2 TYPE_BREAD — baked, mid-tier ## 1 TYPE_VEGETABLE — raw veg, acceptable ## 0 TYPE_GRAIN — raw grain, last resort (−5 mood "Ate raw food" in Phase 8) ## ## Priority 7 means hungry pawns prefer eating over hauling (3) and construction ## (6, for general builds), but does not override a player forced-job (Layer 2). ## When hunger drops below is_starving() the Phase 9 status interrupt will ## preempt current jobs — that hook is not yet wired; EatProvider handles ## voluntary eating only. ## ## Pawn is intentionally duck-typed (no class_name reference) to avoid the ## class_name registration-order trap documented in Phase 2. ## ## See docs/architecture.md "Pawn AI / job system" and ## docs/design.md "Health & status effects". func _init() -> void: category = &"eat" # 7 > construction (6) > hauling (3) > rest (0). # Hunger trumps routine work but player-forced jobs (Layer 2) still win. priority = 7 # ── WorkProvider override ───────────────────────────────────────────────────── ## Returns an eat Job for `pawn`, or null if the pawn is not hungry or there is ## no reachable food in the world. ## ## Selection logic: ## Higher food-priority type always beats a closer lower-priority item. ## Within the same priority tier, nearest item (Manhattan distance) wins. ## Being-carried items are skipped — another pawn has claimed them. func find_best_for(pawn) -> Job: if not pawn.is_hungry(): return null # Don't interrupt an ongoing carry — the pawn should drop or deposit first. if pawn.carried_item != null: return null var best = null var best_dist: int = 999999 var best_priority: int = -1 for it in World.items: if it.being_carried: continue var fp: int = _food_priority(it.item_type) if fp < 0: continue # Reachability — same pattern as HaulingProvider. Skip food we can't # path to instead of returning a doomed walk job. if pawn.tile != it.tile and World.pathfinder.find_path(pawn.tile, it.tile).is_empty(): continue var d: int = abs(it.tile.x - pawn.tile.x) + abs(it.tile.y - pawn.tile.y) # Higher food-priority tier beats distance; within same tier nearest wins. if fp > best_priority or (fp == best_priority and d < best_dist): best_priority = fp best_dist = d best = it if best == null: return null var j := Job.new() j.label = "Eat %s" % best.item_type j.toils.append(Toil.walk_to(best.tile)) j.toils.append(Toil.pickup()) j.toils.append(Toil.eat()) return j # ── private helpers ─────────────────────────────────────────────────────────── ## Returns the food desirability for `item_type`, or -1 if not edible. ## Pawns prefer cooked food; raw grain is a reluctant last resort. func _food_priority(item_type: StringName) -> int: match item_type: Item.TYPE_MEAL: return 3 Item.TYPE_BREAD: return 2 Item.TYPE_VEGETABLE: return 1 Item.TYPE_GRAIN: return 0 _: return -1 # not edible