184 lines
10 KiB
JavaScript
184 lines
10 KiB
JavaScript
"use strict";
|
|
|
|
// Layer: world/pathfinding
|
|
// Grid and detour waypoint helpers for obstacle-aware creature routing.
|
|
(function (global) {
|
|
const World = global.World;
|
|
if (!World) return;
|
|
|
|
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 padding = Math.max(16, Number(opts.padding || 0) || rr * 0.78);
|
|
const directClear = !this.pathBlockedByFence?.(ax, ay, tx, ty, padding, { exclude });
|
|
if (directClear) {
|
|
actor._pathWaypoint = null;
|
|
return null;
|
|
}
|
|
|
|
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, Math.max(6, padding * 0.45), { exclude, maxChecks: 18 })
|
|
&& !this.pathBlockedByFence?.(ax, ay, cached.x, cached.y, padding, { exclude })) {
|
|
return { x: cached.x, y: cached.y, dead: false, detour: true, routeTargetId: targetId };
|
|
}
|
|
|
|
const midX = (ax + tx) * 0.5;
|
|
const midY = (ay + ty) * 0.5;
|
|
const totalD = Math.max(1, Math.hypot(tx - ax, ty - ay));
|
|
const rects = this.nearbySolidObstacleRects?.(midX, midY, totalD * 0.5 + padding + 190, { exclude, maxChecks: Math.max(32, Number(opts.maxChecks || 0) || 46), maxRects: 80 }) || [];
|
|
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;
|
|
candidates.push({ x, y, sourceRect, grade });
|
|
};
|
|
const worldPoint = (r, lx, ly) => {
|
|
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(r.angle || 0);
|
|
const sn = Number.isFinite(r.sin) ? r.sin : Math.sin(r.angle || 0);
|
|
const cx = Number(r.cx ?? ((Number(r.left || 0) + Number(r.right || 0)) * 0.5)) || 0;
|
|
const cy = Number(r.cy ?? ((Number(r.top || 0) + Number(r.bottom || 0)) * 0.5)) || 0;
|
|
return { x: cx + lx * c - ly * sn, y: cy + lx * sn + ly * c };
|
|
};
|
|
const addRectCandidates = (r, index = 0) => {
|
|
const pad = Math.max(34, padding + rr * 1.45 + 18 + Math.min(18, index * 3));
|
|
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]]) {
|
|
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);
|
|
}
|
|
};
|
|
|
|
let blockers = 0;
|
|
for (const rect of rects) {
|
|
if (!this.isFenceRoutingRect?.(rect)) continue;
|
|
const inflated = rect?.oriented
|
|
? { ...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;
|
|
if (blockers >= 4) break;
|
|
}
|
|
if (!candidates.length) return null;
|
|
|
|
let best = null;
|
|
let bestScore = Infinity;
|
|
const old = cached && Number.isFinite(cached.x) && Number.isFinite(cached.y) ? cached : null;
|
|
for (const c of candidates) {
|
|
if (this.pointBlockedByObstacle(c.x, c.y, Math.max(5, padding * 0.42), { exclude, maxChecks: 20 })) continue;
|
|
const firstBlocked = this.pathBlockedByFence?.(ax, ay, c.x, c.y, padding, { exclude });
|
|
if (firstBlocked) 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 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;
|
|
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 });
|
|
if (grid) return grid;
|
|
if (!best) return null;
|
|
}
|
|
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 };
|
|
},
|
|
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 padding = Math.max(16, Number(opts.padding || 0) || (Number(actor.radius || 20) * 0.78));
|
|
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, Math.max(6, padding * 0.42), { exclude, maxChecks: 20 })
|
|
&& !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 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 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 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), Math.max(6, padding * 0.45), { exclude, maxChecks: 18 });
|
|
};
|
|
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 dirs = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[1,-1],[-1,1],[-1,-1]];
|
|
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;
|
|
if (!pointOpen(nx, ny)) continue;
|
|
const nb = { ix: nx, iy: ny };
|
|
if (!edgeOpen(cur, nb)) continue;
|
|
const diagonal = dx && dy;
|
|
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 (!found) return null;
|
|
const path = [];
|
|
for (let n = found; n; n = n.parent) path.push(n);
|
|
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 };
|
|
},
|
|
}));
|
|
})(typeof window !== "undefined" ? window : globalThis);
|