y
This commit is contained in:
parent
61718e2981
commit
f657b6a4a4
94 changed files with 3970 additions and 1241 deletions
|
|
@ -1,37 +1,113 @@
|
|||
"use strict";
|
||||
|
||||
// Layer: world/pathfinding
|
||||
// Grid and detour waypoint helpers for obstacle-aware creature routing.
|
||||
// Throttled actor-local obstacle-aware routing for creature movement.
|
||||
(function (global) {
|
||||
const World = global.World;
|
||||
if (!World) return;
|
||||
|
||||
const ROUTE_TARGET_CELL = 56;
|
||||
|
||||
const routeHash = (value) => {
|
||||
const s = String(value ?? "");
|
||||
let h = 2166136261 >>> 0;
|
||||
for (let i = 0; i < s.length; i++) h = Math.imul(h ^ s.charCodeAt(i), 16777619) >>> 0;
|
||||
return h >>> 0;
|
||||
};
|
||||
const routeBias = (seed, x, y, amplitude = 14) => {
|
||||
let h = seed ^ Math.imul((Math.round(x) | 0) + 0x9e37, 0x85ebca6b) ^ Math.imul((Math.round(y) | 0) + 0x7f4a, 0xc2b2ae35);
|
||||
h ^= h >>> 16;
|
||||
h = Math.imul(h, 0x7feb352d);
|
||||
h ^= h >>> 15;
|
||||
return (((h >>> 0) / 4294967295) * 2 - 1) * amplitude;
|
||||
};
|
||||
const cellKey = (x, y, size) => `${Math.floor(x / size)},${Math.floor(y / size)}`;
|
||||
const routingContext = (world, actor, target, opts = {}) => {
|
||||
if (!actor || !target) return null;
|
||||
const ax = Number(actor.x), ay = Number(actor.y), tx = Number(target.x), ty = Number(target.y);
|
||||
if (![ax, ay, tx, ty].every(Number.isFinite)) return null;
|
||||
const targetItem = opts.targetItem || target.hostItem || (target.isStructure || target.type ? target : null);
|
||||
const exclude = opts.exclude || targetItem || null;
|
||||
const radius = Math.max(8, Number(actor.radius || 20) || 20);
|
||||
const requestedPadding = Number(opts.padding || 0) || radius * 0.78;
|
||||
const padding = Math.max(20, requestedPadding, radius * 0.95);
|
||||
const targetId = targetItem?.id || target.id || `pos:${Math.round(tx)},${Math.round(ty)}`;
|
||||
const spatialVersion = world.routingObstacleVersion || 0;
|
||||
return {
|
||||
ax, ay, tx, ty, targetItem, exclude, radius, padding,
|
||||
pointClearance: Math.max(10, padding * 0.72),
|
||||
targetId,
|
||||
targetBucket: cellKey(tx, ty, ROUTE_TARGET_CELL),
|
||||
spatialVersion,
|
||||
seed: routeHash(actor.id ?? actor.uid ?? actor.name ?? ""),
|
||||
};
|
||||
};
|
||||
const routeCheckInterval = (ctx, opts = {}) => {
|
||||
const state = String(opts.state || "");
|
||||
const base = state === "fight" || state === "seek_enemy" || state === "ant_attack"
|
||||
? 0.18
|
||||
: state === "follow_parent"
|
||||
? 0.24
|
||||
: Math.hypot(ctx.tx - ctx.ax, ctx.ty - ctx.ay) > 900 ? 0.58 : 0.42;
|
||||
return base * (0.9 + ((ctx.seed & 255) / 255) * 0.2);
|
||||
};
|
||||
const cacheIdentityValid = (cache, ctx) => Boolean(cache
|
||||
&& cache.targetId === ctx.targetId
|
||||
&& cache.targetBucket === ctx.targetBucket
|
||||
&& cache.spatialVersion === ctx.spatialVersion);
|
||||
const cacheResult = (cache, targetId) => {
|
||||
if (!cache || cache.mode === "direct" || cache.mode === "none") return null;
|
||||
if (!Number.isFinite(cache.x) || !Number.isFinite(cache.y)) return null;
|
||||
return {
|
||||
x: cache.x, y: cache.y, dead: false, detour: true, routeTargetId: targetId,
|
||||
...(cache.mode === "grid" ? { gridPath: true } : null),
|
||||
};
|
||||
};
|
||||
const cacheGeometryValid = (world, cache, ctx) => {
|
||||
if (cache.mode === "direct") return !world.pathBlockedByFence?.(ctx.ax, ctx.ay, ctx.tx, ctx.ty, ctx.padding, { exclude: ctx.exclude });
|
||||
if (cache.mode === "none") return false;
|
||||
return Number.isFinite(cache.x) && Number.isFinite(cache.y)
|
||||
&& !world.pointBlockedByObstacle?.(cache.x, cache.y, ctx.pointClearance, { exclude: ctx.exclude, maxChecks: 20, directionalOneWay: true })
|
||||
&& !world.pathBlockedByFence?.(ctx.ax, ctx.ay, cache.x, cache.y, ctx.padding, { exclude: ctx.exclude });
|
||||
};
|
||||
const setActorRouteCache = (world, actor, ctx, opts, route, ttl = 1.1) => {
|
||||
const now = world.time || 0;
|
||||
actor._routeCache = {
|
||||
mode: route.mode,
|
||||
x: route.x,
|
||||
y: route.y,
|
||||
targetId: ctx.targetId,
|
||||
targetBucket: ctx.targetBucket,
|
||||
spatialVersion: ctx.spatialVersion,
|
||||
nextCheckAt: now + routeCheckInterval(ctx, opts),
|
||||
expiresAt: now + ttl,
|
||||
};
|
||||
return cacheResult(actor._routeCache, ctx.targetId);
|
||||
};
|
||||
const reuseActorRoute = (world, actor, ctx, opts) => {
|
||||
const cache = actor._routeCache;
|
||||
if (!cacheIdentityValid(cache, ctx)) return undefined;
|
||||
const now = world.time || 0;
|
||||
if (now < (cache.nextCheckAt || 0)) return cacheResult(cache, ctx.targetId);
|
||||
if (now < (cache.expiresAt || 0) && cacheGeometryValid(world, cache, ctx)) {
|
||||
cache.nextCheckAt = now + routeCheckInterval(ctx, opts);
|
||||
return cacheResult(cache, ctx.targetId);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
|
||||
findTarinaiPathWaypoint(actor, target, opts = {}) {
|
||||
if (!actor || !target || !Number.isFinite(Number(actor.x)) || !Number.isFinite(Number(actor.y)) || !Number.isFinite(Number(target.x)) || !Number.isFinite(Number(target.y))) return null;
|
||||
const tx = Number(target.x);
|
||||
const ty = Number(target.y);
|
||||
const ax = Number(actor.x);
|
||||
const ay = Number(actor.y);
|
||||
const targetItem = opts?.targetItem || target?.hostItem || (target?.isStructure || target?.type ? target : null);
|
||||
const exclude = opts?.exclude || targetItem || null;
|
||||
const rr = Math.max(8, Number(actor.radius || 20) || 20);
|
||||
const requestedPadding = Number(opts.padding || 0) || rr * 0.78;
|
||||
const padding = Math.max(20, requestedPadding, rr * 0.95);
|
||||
const pointClearance = Math.max(10, padding * 0.72);
|
||||
const directClear = !this.pathBlockedByFence?.(ax, ay, tx, ty, padding, { exclude });
|
||||
if (directClear) {
|
||||
actor._pathWaypoint = null;
|
||||
return null;
|
||||
}
|
||||
const ctx = routingContext(this, actor, target, opts);
|
||||
if (!ctx) return null;
|
||||
const { ax, ay, tx, ty, targetItem, exclude, radius: rr, padding, pointClearance, targetId, spatialVersion, seed } = ctx;
|
||||
|
||||
const targetId = targetItem?.id || target?.id || `pos:${Math.round(tx)},${Math.round(ty)}`;
|
||||
const cached = actor._pathWaypoint || null;
|
||||
if (cached && cached.targetId === targetId && (this.time || 0) < (cached.until || 0)
|
||||
&& Number.isFinite(cached.x) && Number.isFinite(cached.y)
|
||||
&& !this.pointBlockedByObstacle(cached.x, cached.y, pointClearance, { exclude, maxChecks: 18, directionalOneWay: true })
|
||||
&& !this.pathBlockedByFence?.(ax, ay, cached.x, cached.y, padding, { exclude })) {
|
||||
return { x: cached.x, y: cached.y, dead: false, detour: true, routeTargetId: targetId };
|
||||
const ownRoute = reuseActorRoute(this, actor, ctx, opts);
|
||||
if (ownRoute !== undefined) return ownRoute;
|
||||
|
||||
if (!this.pathBlockedByFence?.(ax, ay, tx, ty, padding, { exclude })) {
|
||||
const route = { mode: "direct" };
|
||||
return setActorRouteCache(this, actor, ctx, opts, route, 0.72);
|
||||
}
|
||||
|
||||
const midX = (ax + tx) * 0.5;
|
||||
|
|
@ -41,8 +117,7 @@
|
|||
const worldPad = (typeof CONFIG !== "undefined" ? CONFIG.worldPadding : 28) + rr * 0.45;
|
||||
const candidates = [];
|
||||
const add = (x, y, sourceRect, grade = 0) => {
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
||||
if (x < worldPad || y < worldPad || x > this.w - worldPad || y > this.h - worldPad) return;
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y) || x < worldPad || y < worldPad || x > this.w - worldPad || y > this.h - worldPad) return;
|
||||
candidates.push({ x, y, sourceRect, grade });
|
||||
};
|
||||
const worldPoint = (r, lx, ly) => {
|
||||
|
|
@ -57,18 +132,15 @@
|
|||
if (r.oriented) {
|
||||
const hw = Math.max(1, Number(r.halfW || 0) || 1) + pad;
|
||||
const hh = Math.max(1, Number(r.halfH || 0) || 1) + pad;
|
||||
for (const [lx, ly, grade] of [[-hw, -hh, 0], [hw, -hh, 0], [hw, hh, 0], [-hw, hh, 0], [0, -hh, 1], [hw, 0, 1], [0, hh, 1], [-hw, 0, 1]]) {
|
||||
for (const [lx, ly, grade] of [[-hw,-hh,0],[hw,-hh,0],[hw,hh,0],[-hw,hh,0],[0,-hh,1],[hw,0,1],[0,hh,1],[-hw,0,1]]) {
|
||||
const p = worldPoint(r, lx, ly);
|
||||
add(p.x, p.y, r, grade);
|
||||
}
|
||||
} else {
|
||||
const left = Number(r.left || 0) - pad;
|
||||
const right = Number(r.right || 0) + pad;
|
||||
const top = Number(r.top || 0) - pad;
|
||||
const bottom = Number(r.bottom || 0) + pad;
|
||||
const cx = (left + right) * 0.5;
|
||||
const cy = (top + bottom) * 0.5;
|
||||
for (const [x, y, grade] of [[left, top, 0], [right, top, 0], [right, bottom, 0], [left, bottom, 0], [cx, top, 1], [right, cy, 1], [cx, bottom, 1], [left, cy, 1]]) add(x, y, r, grade);
|
||||
const left = Number(r.left || 0) - pad, right = Number(r.right || 0) + pad;
|
||||
const top = Number(r.top || 0) - pad, bottom = Number(r.bottom || 0) + pad;
|
||||
const cx = (left + right) * 0.5, cy = (top + bottom) * 0.5;
|
||||
for (const [x, y, grade] of [[left,top,0],[right,top,0],[right,bottom,0],[left,bottom,0],[cx,top,1],[right,cy,1],[cx,bottom,1],[left,cy,1]]) add(x, y, r, grade);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -79,117 +151,142 @@
|
|||
? { ...rect, halfW: (rect.halfW || 0) + padding, halfH: (rect.halfH || 0) + padding }
|
||||
: { left: (rect.left || 0) - padding, right: (rect.right || 0) + padding, top: (rect.top || 0) - padding, bottom: (rect.bottom || 0) + padding };
|
||||
if (!this.segmentIntersectsRect?.(ax, ay, tx, ty, inflated)) continue;
|
||||
addRectCandidates(rect, blockers);
|
||||
blockers += 1;
|
||||
addRectCandidates(rect, blockers++);
|
||||
if (blockers >= 4) break;
|
||||
}
|
||||
if (!candidates.length) return null;
|
||||
if (!candidates.length) {
|
||||
const route = { mode: "none" };
|
||||
return setActorRouteCache(this, actor, ctx, opts, route, 0.35);
|
||||
}
|
||||
|
||||
let best = null;
|
||||
let bestScore = Infinity;
|
||||
const old = cached && Number.isFinite(cached.x) && Number.isFinite(cached.y) ? cached : null;
|
||||
let best = null, bestScore = Infinity;
|
||||
const old = actor._routeCache && Number.isFinite(actor._routeCache.x) && Number.isFinite(actor._routeCache.y) ? actor._routeCache : null;
|
||||
for (const c of candidates) {
|
||||
if (this.pointBlockedByObstacle(c.x, c.y, pointClearance, { exclude, maxChecks: 20, directionalOneWay: true })) continue;
|
||||
const firstBlocked = this.pathBlockedByFence?.(ax, ay, c.x, c.y, padding, { exclude });
|
||||
if (firstBlocked) continue;
|
||||
if (this.pathBlockedByFence?.(ax, ay, c.x, c.y, padding, { exclude })) continue;
|
||||
const secondBlocked = this.pathBlockedByFence?.(c.x, c.y, tx, ty, padding, { exclude });
|
||||
const d1 = Math.hypot(c.x - ax, c.y - ay);
|
||||
const d2 = Math.hypot(tx - c.x, ty - c.y);
|
||||
const d1 = Math.hypot(c.x - ax, c.y - ay), d2 = Math.hypot(tx - c.x, ty - c.y);
|
||||
const progress = totalD - d2;
|
||||
const cacheBias = old ? Math.min(90, Math.hypot(c.x - old.x, c.y - old.y) * 0.35) : 0;
|
||||
const score = d1 + d2 + (secondBlocked ? 780 - progress * 0.45 : 0) + (c.grade || 0) * 26 + cacheBias;
|
||||
const score = d1 + d2 + (secondBlocked ? 780 - progress * 0.45 : 0) + (c.grade || 0) * 26 + cacheBias + routeBias(seed, c.x, c.y);
|
||||
if (score < bestScore) { best = { ...c, secondBlocked }; bestScore = score; }
|
||||
}
|
||||
if (!best || (best.secondBlocked && opts.preferFullPath)) {
|
||||
const grid = this.findGridPathWaypoint?.(actor, target, { ...opts, targetItem, exclude, padding, targetId });
|
||||
const grid = this.findGridPathWaypoint?.(actor, target, { ...opts, targetItem, exclude, padding, targetId, skipRouteReuse: true });
|
||||
if (grid) return grid;
|
||||
if (!best) return null;
|
||||
if (!best) {
|
||||
const route = { mode: "none" };
|
||||
return setActorRouteCache(this, actor, ctx, opts, route, 0.35);
|
||||
}
|
||||
}
|
||||
actor._pathWaypoint = { x: best.x, y: best.y, targetId, until: (this.time || 0) + 0.95 };
|
||||
return { x: best.x, y: best.y, dead: false, detour: true, routeTargetId: targetId };
|
||||
const route = { mode: "detour", x: best.x, y: best.y };
|
||||
return setActorRouteCache(this, actor, ctx, opts, route);
|
||||
},
|
||||
|
||||
findGridPathWaypoint(actor, target, opts = {}) {
|
||||
if (!actor || !target || !Number.isFinite(Number(actor.x)) || !Number.isFinite(Number(actor.y)) || !Number.isFinite(Number(target.x)) || !Number.isFinite(Number(target.y))) return null;
|
||||
const ax = Number(actor.x), ay = Number(actor.y), tx = Number(target.x), ty = Number(target.y);
|
||||
const targetId = opts.targetId || opts.targetItem?.id || target?.id || `pos:${Math.round(tx)},${Math.round(ty)}`;
|
||||
const cached = actor._gridPathWaypoint || null;
|
||||
const actorRadius = Math.max(8, Number(actor.radius || 20) || 20);
|
||||
const requestedPadding = Number(opts.padding || 0) || actorRadius * 0.78;
|
||||
const padding = Math.max(20, requestedPadding, actorRadius * 0.95);
|
||||
const pointClearance = Math.max(10, padding * 0.72);
|
||||
const exclude = opts.exclude || opts.targetItem || target || null;
|
||||
if (cached && cached.targetId === targetId && (this.time || 0) < (cached.until || 0)
|
||||
&& Number.isFinite(cached.x) && Number.isFinite(cached.y)
|
||||
&& !this.pointBlockedByObstacle?.(cached.x, cached.y, pointClearance, { exclude, maxChecks: 20, directionalOneWay: true })
|
||||
&& !this.pathBlockedByFence?.(ax, ay, cached.x, cached.y, padding, { exclude })) {
|
||||
return { x: cached.x, y: cached.y, dead: false, detour: true, routeTargetId: targetId, gridPath: true };
|
||||
const ctx = routingContext(this, actor, target, opts);
|
||||
if (!ctx) return null;
|
||||
const { ax, ay, tx, ty, targetId, padding, pointClearance, exclude, radius: actorRadius, seed } = ctx;
|
||||
if (!opts.skipRouteReuse) {
|
||||
const ownRoute = reuseActorRoute(this, actor, ctx, opts);
|
||||
if (ownRoute !== undefined) return ownRoute;
|
||||
}
|
||||
|
||||
const dTotal = Math.max(1, Math.hypot(tx - ax, ty - ay));
|
||||
const step = Math.max(42, Math.min(72, Number(opts.gridStep || 0) || dTotal / 7));
|
||||
const padWorld = (CONFIG.worldPadding || 28) + Math.max(8, Number(actor.radius || 20) * 0.45);
|
||||
const padWorld = (CONFIG.worldPadding || 28) + Math.max(8, actorRadius * 0.45);
|
||||
const margin = Math.max(190, step * 3.2);
|
||||
const minX = Math.max(padWorld, Math.min(ax, tx) - margin);
|
||||
const maxX = Math.min(this.w - padWorld, Math.max(ax, tx) + margin);
|
||||
const minY = Math.max(padWorld, Math.min(ay, ty) - margin);
|
||||
const maxY = Math.min(this.h - padWorld, Math.max(ay, ty) + margin);
|
||||
const minX = Math.max(padWorld, Math.min(ax, tx) - margin), maxX = Math.min(this.w - padWorld, Math.max(ax, tx) + margin);
|
||||
const minY = Math.max(padWorld, Math.min(ay, ty) - margin), maxY = Math.min(this.h - padWorld, Math.max(ay, ty) + margin);
|
||||
const cols = Math.max(2, Math.min(18, Math.ceil((maxX - minX) / step) + 1));
|
||||
const rows = Math.max(2, Math.min(18, Math.ceil((maxY - minY) / step) + 1));
|
||||
const px = (ix) => cols <= 1 ? minX : minX + (maxX - minX) * ix / (cols - 1);
|
||||
const py = (iy) => rows <= 1 ? minY : minY + (maxY - minY) * iy / (rows - 1);
|
||||
const nearest = (x, y) => ({ ix: clamp(Math.round((x - minX) / Math.max(1, maxX - minX) * (cols - 1)), 0, cols - 1), iy: clamp(Math.round((y - minY) / Math.max(1, maxY - minY) * (rows - 1)), 0, rows - 1) });
|
||||
const start = nearest(ax, ay);
|
||||
const goal = nearest(tx, ty);
|
||||
const key = (ix, iy) => `${ix},${iy}`;
|
||||
const inBounds = (ix, iy) => ix >= 0 && iy >= 0 && ix < cols && iy < rows;
|
||||
const pointOpen = (ix, iy) => {
|
||||
if (!inBounds(ix, iy)) return false;
|
||||
if (ix === start.ix && iy === start.iy) return true;
|
||||
if (ix === goal.ix && iy === goal.iy) return true;
|
||||
return !this.pointBlockedByObstacle?.(px(ix), py(iy), pointClearance, { exclude, maxChecks: 18, directionalOneWay: true });
|
||||
};
|
||||
const edgeOpen = (a, b) => !this.pathBlockedByFence?.(px(a.ix), py(a.iy), px(b.ix), py(b.iy), padding, { exclude });
|
||||
const h = (ix, iy) => Math.hypot(px(ix) - tx, py(iy) - ty);
|
||||
const open = [{ ...start, g: 0, f: h(start.ix, start.iy), parent: null }];
|
||||
const bestByKey = new Map([[key(start.ix, start.iy), open[0]]]);
|
||||
let found = null;
|
||||
let guard = 0;
|
||||
const count = cols * rows;
|
||||
const xs = new Float64Array(cols), ys = new Float64Array(rows);
|
||||
for (let i = 0; i < cols; i++) xs[i] = cols <= 1 ? minX : minX + (maxX - minX) * i / (cols - 1);
|
||||
for (let i = 0; i < rows; i++) ys[i] = rows <= 1 ? minY : minY + (maxY - minY) * i / (rows - 1);
|
||||
const nearest = (x, y) => ({
|
||||
ix: clamp(Math.round((x - minX) / Math.max(1, maxX - minX) * (cols - 1)), 0, cols - 1),
|
||||
iy: clamp(Math.round((y - minY) / Math.max(1, maxY - minY) * (rows - 1)), 0, rows - 1),
|
||||
});
|
||||
const start = nearest(ax, ay), goal = nearest(tx, ty);
|
||||
const startId = start.iy * cols + start.ix, goalId = goal.iy * cols + goal.ix;
|
||||
const pointCache = new Int8Array(count); pointCache.fill(-1);
|
||||
const edgeCache = new Int8Array(count * 8); edgeCache.fill(-1);
|
||||
const bestG = new Float64Array(count); bestG.fill(Infinity);
|
||||
const scoreF = new Float64Array(count); scoreF.fill(Infinity);
|
||||
const parent = new Int16Array(count); parent.fill(-1);
|
||||
const inOpen = new Uint8Array(count);
|
||||
const open = [];
|
||||
const dirs = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
|
||||
const idOf = (ix, iy) => iy * cols + ix;
|
||||
const pointOpen = (ix, iy) => {
|
||||
if (ix < 0 || iy < 0 || ix >= cols || iy >= rows) return false;
|
||||
const id = idOf(ix, iy);
|
||||
if (id === startId || id === goalId) return true;
|
||||
if (pointCache[id] >= 0) return pointCache[id] === 1;
|
||||
const ok = !this.pointBlockedByObstacle?.(xs[ix], ys[iy], pointClearance, { exclude, maxChecks: 18, directionalOneWay: true });
|
||||
pointCache[id] = ok ? 1 : 0;
|
||||
return ok;
|
||||
};
|
||||
const edgeOpen = (aId, bId) => {
|
||||
const aix = aId % cols, aiy = (aId / cols) | 0, bix = bId % cols, biy = (bId / cols) | 0;
|
||||
const dx = bix - aix, dy = biy - aiy;
|
||||
const dir = dx === 1 ? (dy === 0 ? 0 : dy === 1 ? 4 : dy === -1 ? 5 : -1)
|
||||
: dx === -1 ? (dy === 0 ? 1 : dy === 1 ? 6 : dy === -1 ? 7 : -1)
|
||||
: dx === 0 ? (dy === 1 ? 2 : dy === -1 ? 3 : -1) : -1;
|
||||
if (dir < 0) return !this.pathBlockedByFence?.(xs[aix], ys[aiy], xs[bix], ys[biy], padding, { exclude });
|
||||
const ci = aId * 8 + dir;
|
||||
if (edgeCache[ci] >= 0) return edgeCache[ci] === 1;
|
||||
const ok = !this.pathBlockedByFence?.(xs[aix], ys[aiy], xs[bix], ys[biy], padding, { exclude });
|
||||
edgeCache[ci] = ok ? 1 : 0;
|
||||
return ok;
|
||||
};
|
||||
const heuristic = (id) => {
|
||||
const ix = id % cols, iy = (id / cols) | 0;
|
||||
return Math.hypot(xs[ix] - tx, ys[iy] - ty) + routeBias(seed, xs[ix], ys[iy], 8);
|
||||
};
|
||||
const pushOpen = (id) => {
|
||||
if (!inOpen[id]) { inOpen[id] = 1; open.push(id); }
|
||||
};
|
||||
bestG[startId] = 0;
|
||||
scoreF[startId] = heuristic(startId);
|
||||
pushOpen(startId);
|
||||
let foundId = -1, guard = 0;
|
||||
while (open.length && guard++ < 420) {
|
||||
open.sort((a, b) => a.f - b.f);
|
||||
const cur = open.shift();
|
||||
if (cur.ix === goal.ix && cur.iy === goal.iy) { found = cur; break; }
|
||||
for (const [dx, dy] of dirs) {
|
||||
const nx = cur.ix + dx, ny = cur.iy + dy;
|
||||
let bestAt = 0;
|
||||
for (let i = 1; i < open.length; i++) if (scoreF[open[i]] < scoreF[open[bestAt]]) bestAt = i;
|
||||
const curId = open[bestAt];
|
||||
open[bestAt] = open[open.length - 1];
|
||||
open.pop();
|
||||
inOpen[curId] = 0;
|
||||
if (curId === goalId) { foundId = curId; break; }
|
||||
const cix = curId % cols, ciy = (curId / cols) | 0;
|
||||
for (let dir = 0; dir < dirs.length; dir++) {
|
||||
const [dx, dy] = dirs[dir], nx = cix + dx, ny = ciy + dy;
|
||||
if (!pointOpen(nx, ny)) continue;
|
||||
const nb = { ix: nx, iy: ny };
|
||||
const diagonal = dx && dy;
|
||||
const nbId = idOf(nx, ny), diagonal = dx && dy;
|
||||
if (diagonal) {
|
||||
const sideA = { ix: cur.ix + dx, iy: cur.iy };
|
||||
const sideB = { ix: cur.ix, iy: cur.iy + dy };
|
||||
if (!pointOpen(sideA.ix, sideA.iy) || !pointOpen(sideB.ix, sideB.iy)) continue;
|
||||
if (!edgeOpen(cur, sideA) || !edgeOpen(cur, sideB) || !edgeOpen(sideA, nb) || !edgeOpen(sideB, nb)) continue;
|
||||
const sideAId = idOf(cix + dx, ciy), sideBId = idOf(cix, ciy + dy);
|
||||
if (!pointOpen(cix + dx, ciy) || !pointOpen(cix, ciy + dy)) continue;
|
||||
if (!edgeOpen(curId, sideAId) || !edgeOpen(curId, sideBId) || !edgeOpen(sideAId, nbId) || !edgeOpen(sideBId, nbId)) continue;
|
||||
}
|
||||
if (!edgeOpen(cur, nb)) continue;
|
||||
const ng = cur.g + (diagonal ? 1.42 : 1) * step;
|
||||
const k = key(nx, ny);
|
||||
const oldNode = bestByKey.get(k);
|
||||
if (oldNode && oldNode.g <= ng) continue;
|
||||
const node = { ix: nx, iy: ny, g: ng, f: ng + h(nx, ny), parent: cur };
|
||||
bestByKey.set(k, node);
|
||||
open.push(node);
|
||||
if (!edgeOpen(curId, nbId)) continue;
|
||||
const ng = bestG[curId] + (diagonal ? 1.42 : 1) * step;
|
||||
if (bestG[nbId] <= ng) continue;
|
||||
bestG[nbId] = ng;
|
||||
parent[nbId] = curId;
|
||||
scoreF[nbId] = ng + heuristic(nbId);
|
||||
pushOpen(nbId);
|
||||
}
|
||||
}
|
||||
if (!found) return null;
|
||||
if (foundId < 0) return null;
|
||||
const path = [];
|
||||
for (let n = found; n; n = n.parent) path.push(n);
|
||||
for (let id = foundId; id >= 0; id = parent[id]) path.push(id);
|
||||
path.reverse();
|
||||
const chosen = path[Math.min(path.length - 1, Math.max(1, path.length > 3 ? 2 : 1))];
|
||||
if (!chosen) return null;
|
||||
const wx = px(chosen.ix), wy = py(chosen.iy);
|
||||
actor._gridPathWaypoint = { x: wx, y: wy, targetId, until: (this.time || 0) + 1.15 };
|
||||
actor._pathWaypoint = { x: wx, y: wy, targetId, until: (this.time || 0) + 1.15 };
|
||||
return { x: wx, y: wy, dead: false, detour: true, routeTargetId: targetId, gridPath: true };
|
||||
const chosenId = path[Math.min(path.length - 1, Math.max(1, path.length > 3 ? 2 : 1))];
|
||||
if (!Number.isInteger(chosenId)) return null;
|
||||
const route = { mode: "grid", x: xs[chosenId % cols], y: ys[(chosenId / cols) | 0] };
|
||||
return setActorRouteCache(this, actor, ctx, opts, route, 1.2);
|
||||
},
|
||||
}));
|
||||
})(typeof window !== "undefined" ? window : globalThis);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue