Player reported pawns ignoring chop designations. Root cause: ChopProvider/MineProvider iterated World.trees/World.rocks unconditionally — paint set a null sentinel and never touched the entity, so designation was cosmetic only. Pawns auto-chopped nearest unfelled tree. * Added chop_designated: bool to Tree, mine_designated: bool to Rock and BigRock (footprint-aware: paint on any of the 4 footprint cells flags the boulder). Save/load round-trips the flag. * world.gd._on_designation_added 'chop'/'mine' cases now find the entity at the painted tile and flip the flag. _on_designation_cleared inverts. * Boot seed auto-designates SAMPLE_TREES / SAMPLE_ROCKS / SAMPLE_BIG_ROCKS so the cabin demo still produces wood + stone end-to-end without requiring the player to paint first. Also from the same audit (researcher mapped all 11 WorkProviders): * DoctorProvider + EatProvider now pre-check reachability with pathfinder.find_path before issuing a job, mirroring HaulingProvider's pattern. Previously they handed out doomed walks that JobRunner had to cancel, busy-spinning at 20 Hz. Verified end-to-end via MCP runtime: undesignated tree/rock returns null from provider; paint flips the flag and provider returns a chop/mine job; un-paint clears the flag; BigRock footprint paint works on any of the 4 cells. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
92 lines
3.6 KiB
GDScript
92 lines
3.6 KiB
GDScript
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
|