253 lines
12 KiB
JavaScript
253 lines
12 KiB
JavaScript
"use strict";
|
|
|
|
// Layer: physics/footprints
|
|
// Shared hit-testing and placement-overlap geometry for oriented rect, circle,
|
|
// and item footprint checks. This keeps placement, grabbing, and physics using
|
|
// the same current-frame shape instead of separate radius approximations.
|
|
(function (global) {
|
|
function num(v, fallback = 0) {
|
|
const n = Number(v);
|
|
return Number.isFinite(n) ? n : fallback;
|
|
}
|
|
|
|
function rectAabb(rect) {
|
|
if (!rect) return null;
|
|
if (!rect.oriented) return rect;
|
|
return {
|
|
left: num(rect.left), right: num(rect.right), top: num(rect.top), bottom: num(rect.bottom),
|
|
cx: num(rect.cx), cy: num(rect.cy), halfW: num(rect.halfW), halfH: num(rect.halfH),
|
|
angle: num(rect.angle), cos: Number.isFinite(rect.cos) ? rect.cos : Math.cos(num(rect.angle)),
|
|
sin: Number.isFinite(rect.sin) ? rect.sin : Math.sin(num(rect.angle)), oriented: true,
|
|
type: rect.type, item: rect.item,
|
|
};
|
|
}
|
|
|
|
function rectLocalPoint(rect, x, y) {
|
|
const dx = num(x) - num(rect?.cx);
|
|
const dy = num(y) - num(rect?.cy);
|
|
const c = Number.isFinite(rect?.cos) ? rect.cos : Math.cos(num(rect?.angle));
|
|
const s = Number.isFinite(rect?.sin) ? rect.sin : Math.sin(num(rect?.angle));
|
|
return { x: dx * c + dy * s, y: -dx * s + dy * c };
|
|
}
|
|
|
|
function rectWorldVector(rect, x, y) {
|
|
const c = Number.isFinite(rect?.cos) ? rect.cos : Math.cos(num(rect?.angle));
|
|
const s = Number.isFinite(rect?.sin) ? rect.sin : Math.sin(num(rect?.angle));
|
|
return { x: num(x) * c - num(y) * s, y: num(x) * s + num(y) * c };
|
|
}
|
|
|
|
function pointInRect(x, y, rect, padding = 0) {
|
|
if (!rect) return false;
|
|
const pad = Math.max(0, num(padding));
|
|
if (rect.oriented) {
|
|
const local = rectLocalPoint(rect, x, y);
|
|
return local.x >= -num(rect.halfW) - pad && local.x <= num(rect.halfW) + pad
|
|
&& local.y >= -num(rect.halfH) - pad && local.y <= num(rect.halfH) + pad;
|
|
}
|
|
return num(x) >= num(rect.left) - pad && num(x) <= num(rect.right) + pad
|
|
&& num(y) >= num(rect.top) - pad && num(y) <= num(rect.bottom) + pad;
|
|
}
|
|
|
|
function rectCorners(rect, padding = 0) {
|
|
const r = rectAabb(rect);
|
|
const pad = Math.max(0, num(padding));
|
|
if (!r) return [];
|
|
if (!r.oriented) {
|
|
return [
|
|
{ x: num(r.left) - pad, y: num(r.top) - pad },
|
|
{ x: num(r.right) + pad, y: num(r.top) - pad },
|
|
{ x: num(r.right) + pad, y: num(r.bottom) + pad },
|
|
{ x: num(r.left) - pad, y: num(r.bottom) + pad },
|
|
];
|
|
}
|
|
const c = Number.isFinite(r.cos) ? r.cos : Math.cos(num(r.angle));
|
|
const s = Number.isFinite(r.sin) ? r.sin : Math.sin(num(r.angle));
|
|
const hw = num(r.halfW) + pad;
|
|
const hh = num(r.halfH) + pad;
|
|
const out = [];
|
|
for (const [lx, ly] of [[-hw, -hh], [hw, -hh], [hw, hh], [-hw, hh]]) {
|
|
out.push({ x: num(r.cx) + lx * c - ly * s, y: num(r.cy) + lx * s + ly * c });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function rectAxes(rect) {
|
|
if (!rect?.oriented) return [{ x: 1, y: 0 }, { x: 0, y: 1 }];
|
|
const c = Number.isFinite(rect.cos) ? rect.cos : Math.cos(num(rect.angle));
|
|
const s = Number.isFinite(rect.sin) ? rect.sin : Math.sin(num(rect.angle));
|
|
return [{ x: c, y: s }, { x: -s, y: c }];
|
|
}
|
|
|
|
function projectPoints(points, axis) {
|
|
let min = Infinity;
|
|
let max = -Infinity;
|
|
for (const p of points || []) {
|
|
const v = num(p.x) * axis.x + num(p.y) * axis.y;
|
|
if (v < min) min = v;
|
|
if (v > max) max = v;
|
|
}
|
|
return { min, max };
|
|
}
|
|
|
|
function aabbOverlap(a, b, padding = 0) {
|
|
const pad = Math.max(0, num(padding));
|
|
return Boolean(a && b && num(a.left) <= num(b.right) + pad && num(a.right) >= num(b.left) - pad
|
|
&& num(a.top) <= num(b.bottom) + pad && num(a.bottom) >= num(b.top) - pad);
|
|
}
|
|
|
|
function rectOverlapInfo(a, b, padding = 0) {
|
|
if (!aabbOverlap(a, b, padding)) return null;
|
|
const ptsA = rectCorners(a, padding);
|
|
const ptsB = rectCorners(b, padding);
|
|
if (!ptsA.length || !ptsB.length) return null;
|
|
let bestOverlap = Infinity;
|
|
let bestAxis = null;
|
|
for (const axis of [...rectAxes(a), ...rectAxes(b)]) {
|
|
const len = Math.hypot(axis.x, axis.y) || 1;
|
|
const n = { x: axis.x / len, y: axis.y / len };
|
|
const pa = projectPoints(ptsA, n);
|
|
const pb = projectPoints(ptsB, n);
|
|
const overlap = Math.min(pa.max, pb.max) - Math.max(pa.min, pb.min);
|
|
if (overlap <= 0) return null;
|
|
if (overlap < bestOverlap) { bestOverlap = overlap; bestAxis = n; }
|
|
}
|
|
if (!bestAxis) return null;
|
|
const acx = num(a.cx, (num(a.left) + num(a.right)) * 0.5);
|
|
const acy = num(a.cy, (num(a.top) + num(a.bottom)) * 0.5);
|
|
const bcx = num(b.cx, (num(b.left) + num(b.right)) * 0.5);
|
|
const bcy = num(b.cy, (num(b.top) + num(b.bottom)) * 0.5);
|
|
if ((bcx - acx) * bestAxis.x + (bcy - acy) * bestAxis.y < 0) bestAxis = { x: -bestAxis.x, y: -bestAxis.y };
|
|
return {
|
|
overlap: bestOverlap,
|
|
nx: bestAxis.x,
|
|
ny: bestAxis.y,
|
|
x: (Math.max(num(a.left), num(b.left)) + Math.min(num(a.right), num(b.right))) * 0.5,
|
|
y: (Math.max(num(a.top), num(b.top)) + Math.min(num(a.bottom), num(b.bottom))) * 0.5,
|
|
};
|
|
}
|
|
|
|
function rectsOverlap(a, b, margin = 0) {
|
|
return Boolean(rectOverlapInfo(a, b, margin));
|
|
}
|
|
|
|
function rectCircleOverlap(rect, cx, cy, radius, margin = 0) {
|
|
if (!rect) return false;
|
|
const r = Math.max(0, num(radius));
|
|
const m = Math.max(0, num(margin));
|
|
if (rect.oriented) {
|
|
const local = rectLocalPoint(rect, cx, cy);
|
|
const qx = Math.max(-num(rect.halfW) - m, Math.min(num(rect.halfW) + m, local.x));
|
|
const qy = Math.max(-num(rect.halfH) - m, Math.min(num(rect.halfH) + m, local.y));
|
|
return Math.hypot(local.x - qx, local.y - qy) < r + m;
|
|
}
|
|
const px = Math.max(num(rect.left) - m, Math.min(num(rect.right) + m, num(cx)));
|
|
const py = Math.max(num(rect.top) - m, Math.min(num(rect.bottom) + m, num(cy)));
|
|
return Math.hypot(num(cx) - px, num(cy) - py) < r + m;
|
|
}
|
|
|
|
function itemFootprintRadius(worldRef, item, rects = null) {
|
|
if (!item) return 14;
|
|
const ownRects = Array.isArray(rects) ? rects : (worldRef?.solidObstacleRects?.(item) || []);
|
|
if (ownRects.length) {
|
|
let reach = 14;
|
|
const ix = num(item.x);
|
|
const iy = num(item.y);
|
|
for (const r of ownRects) {
|
|
for (const p of rectCorners(r, 0)) reach = Math.max(reach, Math.hypot(p.x - ix, p.y - iy));
|
|
}
|
|
return Math.max(14, reach);
|
|
}
|
|
const radiusFor = typeof global.itemRadiusFor === "function" ? global.itemRadiusFor : ((type, fallback = 12) => fallback);
|
|
if (item.type === "duplicator") {
|
|
// 保存済みデータに古い r=34 が残っていても、配置判定は外観に近い半径で扱う。
|
|
return Math.max(14, Math.min(24, num(item.r, radiusFor(item.type, 24)) || radiusFor(item.type, 24)));
|
|
}
|
|
return Math.max(14, num(item.r, radiusFor(item.type, 12)) || radiusFor(item.type, 12));
|
|
}
|
|
|
|
function isSoftPlacementIgnoredType(type = "") {
|
|
const key = String(type || "");
|
|
return key === "zunchi" || key === "grass_bed" || key === "grass" || key === "water";
|
|
}
|
|
|
|
function placementRectsFor(worldRef, item) {
|
|
const solid = worldRef?.solidObstacleRects?.(item) || [];
|
|
if (solid.length) return solid;
|
|
if (global.TarinaiMechanicalSystem.isMechanicalType(item?.type)) return global.TarinaiMechanicalSystem.placementRects(item) || [];
|
|
return solid;
|
|
}
|
|
|
|
function hitTestItem(worldRef, item, x, y, opts = {}) {
|
|
if (!item || item.dead) return { hit: false, distance: Infinity };
|
|
const padding = Math.max(0, num(opts.padding, 0));
|
|
let rects = opts.rects || worldRef?.solidObstacleRects?.(item) || [];
|
|
if ((!rects || !rects.length) && item?.type === "poison_block") rects = global.TarinaiMechanicalSystem.poisonHazardRects(item) || [];
|
|
let bestDistance = Math.hypot(num(x) - num(item.x), num(y) - num(item.y));
|
|
let hit = false;
|
|
if (rects.length) {
|
|
for (const rect of rects) {
|
|
if (pointInRect(x, y, rect, padding)) hit = true;
|
|
bestDistance = Math.min(bestDistance, Math.hypot(num(x) - num(rect.cx, item.x), num(y) - num(rect.cy, item.y)));
|
|
}
|
|
}
|
|
const mechHit = global.TarinaiMechanicalSystem.isMechanicalType(item.type);
|
|
if (mechHit) {
|
|
const hub = Math.max(18, Math.min(40, (global.TarinaiPhysicsBodySystem.scalar(item, "thickness", 12) ?? 12) * 2.3));
|
|
if (Math.hypot(num(x) - num(item.x), num(y) - num(item.y)) <= hub + padding) hit = true;
|
|
} else if (!rects.length) {
|
|
const baseRadius = item.type === "duplicator" ? Math.min(24, num(item.r, 24) || 24) : num(item.r, 12);
|
|
const defaultMul = item.type === "ball" ? 4.0 : (item.type === "duplicator" ? 1.45 : 2.2);
|
|
const mul = num(opts.radiusMultiplier, defaultMul);
|
|
const r = Math.max(item.type === "duplicator" ? 18 : 24, baseRadius * mul);
|
|
hit = bestDistance <= r + padding;
|
|
}
|
|
return { hit, distance: bestDistance };
|
|
}
|
|
|
|
function itemPlacementOverlapBlocked(worldRef, item, opts = {}) {
|
|
if (!worldRef || !item || item.dead) return true;
|
|
const important = opts.important || new Set(["zunchi", "grass", "grass_bed", "water", "fence_v", "fence_h", "glass_wall", "bounce_fence", "bounce_fence_v", "gate_fence", "rotator", "poison_block", "reciprocator", "nest_box"]);
|
|
const isPhysicsItem = (global.TarinaiMechanicalSystem.isMechanicalType(item.type) || item.type === "rope" || item.type === "rod" || isFenceItemType(item.type));
|
|
if (!important.has(item.type) && !isPhysicsItem) return false;
|
|
const itemRects = placementRectsFor(worldRef, item);
|
|
const itemRadius = itemFootprintRadius(worldRef, item, itemRects);
|
|
const search = Math.max(132, itemRadius * 3.2);
|
|
for (const other of worldRef.nearbyItems?.(item.x, item.y, search) || []) {
|
|
if (!other || other === item || other.dead) continue;
|
|
if (isSoftPlacementIgnoredType(other.type)) continue;
|
|
if (!important.has(other.type)) continue;
|
|
const otherRects = placementRectsFor(worldRef, other);
|
|
const otherRadius = itemFootprintRadius(worldRef, other, otherRects);
|
|
const hasMechanism = global.TarinaiMechanicalSystem.isMechanicalType(item.type) || global.TarinaiMechanicalSystem.isMechanicalType(other.type);
|
|
const itemIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(item.type) : /fence/.test(String(item.type || ""));
|
|
const otherIsFence = typeof global.isFenceItemType === "function" ? global.isFenceItemType(other.type) : /fence/.test(String(other.type || ""));
|
|
const margin = (itemIsFence || otherIsFence) ? (item.type === "gate_fence" || other.type === "gate_fence" ? 1.5 : 2.5) : (hasMechanism ? 1.25 : Math.max(4, Math.min(itemRadius, otherRadius) * 0.18));
|
|
if (itemRects.length && otherRects.length) {
|
|
if (itemRects.some(a => otherRects.some(b => rectsOverlap(a, b, margin)))) return true;
|
|
continue;
|
|
}
|
|
if (itemRects.length) {
|
|
if (itemRects.some(r => rectCircleOverlap(r, other.x, other.y, otherRadius, margin))) return true;
|
|
continue;
|
|
}
|
|
if (otherRects.length) {
|
|
if (otherRects.some(r => rectCircleOverlap(r, item.x, item.y, itemRadius, margin))) return true;
|
|
continue;
|
|
}
|
|
if (Math.hypot(num(item.x) - num(other.x), num(item.y) - num(other.y)) < itemRadius + otherRadius + margin) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
global.TarinaiCollisionFootprints = Object.freeze({
|
|
rectLocalPoint,
|
|
rectWorldVector,
|
|
pointInRect,
|
|
rectsOverlap,
|
|
rectCircleOverlap,
|
|
itemFootprintRadius,
|
|
placementRectsFor,
|
|
hitTestItem,
|
|
itemPlacementOverlapBlocked,
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|