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

65 lines
2.5 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 MineProvider extends WorkProvider
## WorkProvider for the "mine" work category.
##
## Scans World.rocks for the nearest mineable Rock (Manhattan distance from
## the requesting pawn) and returns a two-toil Job:
## walk_to(rock.tile) → interact(rock.get_path(), "on_mine_tick")
##
## The INTERACT toil calls Rock.on_mine_tick() once per sim tick; the Rock
## internally tracks mine_progress and handles removal when MINE_TICKS is
## reached. The toil finishes automatically when is_mineable() returns
## false (exhausted) or when the node is freed.
##
## Phase 4 simplification: rocks are assumed walkable during mining approach
## (they may block movement in a later phase once obstruction is added to the
## pathfinder).
##
## Duck-typing note: Rock is referenced without class_name (class may not be
## registered yet when this provider loads). World.rocks may also hold BigRock
## instances; both expose the same interface so we just iterate the array:
## rock.tile: Vector2i — for distance scoring
## rock.is_mineable() -> bool
## rock.approach_tile_for(pawn_tile) -> Vector2i — walk destination
## rock.get_path() -> NodePath
func _init() -> void:
category = &"mine"
priority = 4 # Slightly lower than chop (5); both higher than rest (0).
## Returns a Job targeting the nearest mineable Rock, 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 rock in World.rocks:
if not rock.is_mineable():
continue
# Gate on player designation — pawns don't auto-mine undesignated rocks.
# Boot seed in world.gd auto-designates SAMPLE_ROCKS so the demo still runs.
if not rock.mine_designated:
continue
if Job.is_target_taken_by_other(rock, pawn):
continue
var d: int = abs(rock.tile.x - pawn.tile.x) + abs(rock.tile.y - pawn.tile.y)
if d < best_dist:
best_dist = d
best = rock
if best == null:
return null
var j := Job.new()
j.label = "Mine rock at %s" % best.tile
j.target_node = best
# Ask the entity where the pawn should stand. Single rocks return their own
# tile (walkable). BigRocks return a perimeter tile so the pawn doesn't try
# to pathfind into the blocked 2×2 footprint.
var walk_tile: Vector2i = (
best.approach_tile_for(pawn.tile) if best.has_method("approach_tile_for") else best.tile
)
j.toils.append(Toil.walk_to(walk_tile))
j.toils.append(Toil.interact(best.get_path(), &"on_mine_tick"))
return j