rimlike/scenes/ai/eat_provider.gd
megaproxy a4163ba222 Chop/mine designation gate + reachability gates on Doctor & Eat
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>
2026-05-15 14:53:50 +01:00

92 lines
3.6 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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