rimlike/scenes/ai/chop_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

56 lines
1.9 KiB
GDScript

class_name ChopProvider extends WorkProvider
## WorkProvider for the "chop" work category.
##
## Scans World.trees for the nearest choppable Tree (Manhattan distance from
## the requesting pawn) and returns a two-toil Job:
## walk_to(tree.tile) → interact(tree.get_path(), "on_chop_tick")
##
## The INTERACT toil calls Tree.on_chop_tick() once per sim tick; the Tree
## internally tracks chop_progress and calls fell() when CHOP_TICKS is
## reached. The toil finishes automatically when is_choppable() returns
## false (felled) or when the node is freed.
##
## Phase 4 simplification: trees do not block pathfinding, so walking directly
## to tree.tile is valid. No adjacency-offset needed.
##
## Duck-typing note: Tree is referenced without class_name (class may not be
## registered yet when this provider loads). We rely only on:
## tree.tile: Vector2i
## tree.is_choppable() -> bool
## tree.get_path() -> NodePath
func _init() -> void:
category = &"chop"
priority = 5 # Higher than rest (priority 0); scanned before it by Decision.
## Returns a Job targeting the nearest choppable Tree, or null if none exists.
## `pawn` is duck-typed: must expose .tile (Vector2i).
func find_best_for(pawn) -> Job:
var best = null
var best_dist: int = 999999
for tree in World.trees:
if not tree.is_choppable():
continue
# Gate on player designation — pawns don't auto-chop undesignated trees.
# Boot seed in world.gd auto-designates SAMPLE_TREES so the demo still runs.
if not tree.chop_designated:
continue
if Job.is_target_taken_by_other(tree, pawn):
continue
var d: int = abs(tree.tile.x - pawn.tile.x) + abs(tree.tile.y - pawn.tile.y)
if d < best_dist:
best_dist = d
best = tree
if best == null:
return null
var j := Job.new()
j.label = "Chop tree at %s" % best.tile
j.target_node = best
j.toils.append(Toil.walk_to(best.tile))
j.toils.append(Toil.interact(best.get_path(), &"on_chop_tick"))
return j