diff --git a/scenes/ai/hauling_provider.gd b/scenes/ai/hauling_provider.gd index 94d669b..6eab1c5 100644 --- a/scenes/ai/hauling_provider.gd +++ b/scenes/ai/hauling_provider.gd @@ -65,6 +65,12 @@ func find_best_for(pawn) -> Job: # Skip items another pawn has already claimed (target_node = item node). if Job.is_target_taken_by_other(item, pawn): continue + # Reachability gate — don't hand out a haul this pawn can't path to. + # Without this, JobRunner cancels the job on the walk toil and Decision + # reassigns it next tick, busy-spinning at 20 Hz with no progress. + # Pre-check is O(pathfinder.find_path) ~10 μs; cheap at MVP scale. + if pawn.tile != item.tile and World.pathfinder.find_path(pawn.tile, item.tile).is_empty(): + continue # Find the best destination for this item type + priority. var dest = _find_best_destination_for(item) @@ -111,6 +117,9 @@ func find_best_for(pawn) -> Job: # Skip corpses another pawn has already claimed. if Job.is_target_taken_by_other(corpse, pawn): continue + # Reachability gate — same rationale as the item loop above. + if pawn.tile != corpse.tile and World.pathfinder.find_path(pawn.tile, corpse.tile).is_empty(): + continue var dest = _find_best_destination_for(corpse) if dest == null: diff --git a/scenes/ai/job_runner.gd b/scenes/ai/job_runner.gd index de80ce3..6bae384 100644 --- a/scenes/ai/job_runner.gd +++ b/scenes/ai/job_runner.gd @@ -179,11 +179,16 @@ func _tick_walk(t) -> void: return var path: Array[Vector2i] = pathfinder.find_path(pawn.tile, dest) if path.is_empty(): + # Unreachable — abort the entire job so Decision can pick something + # else next tick. Previously this marked the walk toil done and the + # subsequent toils (pickup, deposit, etc.) ran at the wrong tile, + # making haul/pickup silently fail with "no item at ". Audit.log( "job_runner", - "%s unreachable: %s → %s" % [pawn.pawn_name, pawn.tile, dest] + "%s unreachable: %s → %s (canceling job '%s')" % + [pawn.pawn_name, pawn.tile, dest, job.label] ) - t.done = true + cancel_job() return pawn.walk_along_path(path) t.data["started"] = true