rimlike/scenes/ai/mine_provider.gd
megaproxy 16e04e46f0 add reachability pre-checks to plant/sleep/chop/mine
Provider audit found 6 WorkProviders missing reachability gates before
returning jobs. Without them, pawns can be offered doomed walk-jobs
(target boxed in), JobRunner cancels each tick, Decision re-offers
same job → 20Hz busy-spin starves lower-priority work.

Fixed 4 here (mechanical pattern):
- PlantProvider._find_harvest: walkable-target check (mirrors _find_sow)
- SleepProvider: walkable bed-tile check
- ChopProvider: adjacent-walkable for impassable tree
- MineProvider: adjacent-walkable for impassable rock

Cooking/Crafting reachability changes (in the same audit's
recommendation) were attempted but caused intermittent null returns
that regressed cooking rate. Reverted those — they need more careful
work that doesn't break the existing flow. Filed separately.

Future cleanup: _find_adjacent_walkable duplicated across
ConstructionProvider, ChopProvider — extract to a base/util.

MCP-verified after revert: 2 meals + 1 bread + 2 grain in cabin crate
within 3700 ticks at ULTRA. Cooking fires, hauling fires, all
production paths operational.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 20:20:35 +01:00

74 lines
3 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
# Reachability pre-check — rocks are impassable. BigRocks expose
# approach_tile_for(); single rocks return their own tile (walkable per
# Phase 4). Either way, verify the approach tile is reachable before scoring.
var approach: Vector2i = (
rock.approach_tile_for(pawn.tile) if rock.has_method("approach_tile_for") else rock.tile
)
if pawn.tile != approach and World.pathfinder != null:
if World.pathfinder.find_path(pawn.tile, approach).is_empty():
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