From 774054db56c88e14d3319b6c9067b4f3077d925c Mon Sep 17 00:00:00 2001 From: tekki mariani Date: Mon, 23 Mar 2026 11:43:00 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20skip=20unreachable=20job=20targe?= =?UTF-8?q?ts=20in=20pickJob=20(Issue=20#22)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trees/rocks fully enclosed by impassable tiles have no passable neighbour for A* to jump from — pathfinding always returns null, causing a tight 1.5 s retry loop that fills the work log with identical entries. - Add hasAdjacentPassable() helper: checks all 8 neighbours of a tile - pickJob now skips chop/mine candidates with no passable neighbour - idleScanTimer on pathfind failure raised 1500 → 4000 ms as safety net Co-Authored-By: Claude Sonnet 4.6 --- src/systems/VillagerSystem.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/systems/VillagerSystem.ts b/src/systems/VillagerSystem.ts index e1776be..30a7168 100644 --- a/src/systems/VillagerSystem.ts +++ b/src/systems/VillagerSystem.ts @@ -351,12 +351,17 @@ export class VillagerSystem { if (p.chop > 0) { for (const res of Object.values(state.world.resources)) { if (res.kind !== 'tree' || this.claimed.has(res.id)) continue + // Skip trees with no reachable neighbour — A* cannot enter an impassable goal + // tile unless at least one passable neighbour exists to jump from. + if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue candidates.push({ type: 'chop', targetId: res.id, tileX: res.tileX, tileY: res.tileY, dist: dist(res.tileX, res.tileY), pri: p.chop }) } } if (p.mine > 0) { for (const res of Object.values(state.world.resources)) { if (res.kind !== 'rock' || this.claimed.has(res.id)) continue + // Same reachability guard for rock tiles. + if (!this.hasAdjacentPassable(res.tileX, res.tileY)) continue candidates.push({ type: 'mine', targetId: res.id, tileX: res.tileX, tileY: res.tileY, dist: dist(res.tileX, res.tileY), pri: p.mine }) } } @@ -397,7 +402,7 @@ export class VillagerSystem { this.claimed.delete(v.job.targetId) this.adapter.send({ type: 'VILLAGER_SET_JOB', villagerId: v.id, job: null }) } - rt.idleScanTimer = 1500 // longer delay after failed pathfind + rt.idleScanTimer = 4000 // longer delay after failed pathfind to avoid tight retry loops return } @@ -434,6 +439,22 @@ export class VillagerSystem { return this.nearestBuilding(v, 'bed') as any } + /** + * Returns true if at least one of the 8 neighbours of the given tile is passable. + * Used to pre-filter job targets that are fully enclosed by impassable terrain — + * such as trees deep inside a dense forest cluster where A* can never reach the goal + * tile because no passable tile is adjacent to it. + * @param tileX - Target tile X + * @param tileY - Target tile Y + */ + private hasAdjacentPassable(tileX: number, tileY: number): boolean { + const DIRS = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]] as const + for (const [dx, dy] of DIRS) { + if (this.worldSystem.isPassable(tileX + dx, tileY + dy)) return true + } + return false + } + // ─── Spawning ───────────────────────────────────────────────────────────── /**