"use strict"; // Layer: world/pathfinding // 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 = {}) { 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 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; 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) || 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, 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); } }; 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++); if (blockers >= 4) break; } if (!candidates.length) { const route = { mode: "none" }; return setActorRouteCache(this, actor, ctx, opts, route, 0.35); } 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; 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), 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 + 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, skipRouteReuse: true }); if (grid) return grid; if (!best) { const route = { mode: "none" }; return setActorRouteCache(this, actor, ctx, opts, route, 0.35); } } const route = { mode: "detour", x: best.x, y: best.y }; return setActorRouteCache(this, actor, ctx, opts, route); }, findGridPathWaypoint(actor, target, opts = {}) { 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, actorRadius * 0.45); const margin = Math.max(190, step * 3.2); 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 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) { 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 nbId = idOf(nx, ny), diagonal = dx && dy; if (diagonal) { 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(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 (foundId < 0) return null; const path = []; for (let id = foundId; id >= 0; id = parent[id]) path.push(id); path.reverse(); 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);