154 lines
7.9 KiB
JavaScript
154 lines
7.9 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) {
|
|
const Geometry = global.TarinaiGeometry;
|
|
const num = Geometry.num;
|
|
const rectLocalPoint = Geometry.rectLocalPoint;
|
|
const rectWorldVector = Geometry.rectWorldVector;
|
|
const rectCorners = Geometry.rectCorners;
|
|
const rectAxes = Geometry.rectAxes;
|
|
const projectPoints = Geometry.projectPoints;
|
|
const aabbOverlap = Geometry.aabbOverlap;
|
|
const rectOverlapInfo = Geometry.rectOverlapInfo;
|
|
const rectsOverlap = Geometry.rectsOverlap;
|
|
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 = global.itemRadiusFor;
|
|
if (item.type === "duplicator") {
|
|
// \u4FDD\u5B58\u6E08\u307F\u30C7\u30FC\u30BF\u306B\u53E4\u3044 r=34 \u304C\u6B8B\u3063\u3066\u3044\u3066\u3082\u3001\u914D\u7F6E\u5224\u5B9A\u306F\u5916\u89B3\u306B\u8FD1\u3044\u534A\u5F84\u3067\u6271\u3046\u3002
|
|
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) || [];
|
|
if (item?.type === "circuit_board") {
|
|
const circuit = global.TarinaiCircuitBoardSystem;
|
|
const halfW = Number(circuit?.boardHalfWidth?.(item) || 32) || 32;
|
|
const halfH = Number(circuit?.boardHalfHeight?.(item) || 32) || 32;
|
|
return [{ oriented: true, cx: num(item.x), cy: num(item.y), halfW, halfH, angle: num(item.angle), type: item.type, 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 (Geometry.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" || item.type === "balloon") ? 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", "one_way_fence", "rotator", "poison_block", "reciprocator", "nest_box", "pipe"]);
|
|
const isPhysicsItem = (global.TarinaiMechanicalSystem.isMechanicalType(item.type) || ["rope", "rod", "spring", "wire", "insulated_wire"].includes(item.type) || 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;
|
|
const freeStacking = global.TarinaiItemToolMetadata?.ITEM_TRAITS?.freeStackingMovable;
|
|
if (freeStacking?.has(item.type) && freeStacking.has(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 = global.isFenceItemType(item.type);
|
|
const otherIsFence = global.isFenceItemType(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({
|
|
// Mechanical-body narrowphase consumes the same SAT primitives as
|
|
// placement/hit testing. Keep these delegates public so collision-enabled
|
|
// rotators, reciprocators, and poison blocks can physically interact.
|
|
rectLocalPoint,
|
|
rectWorldVector,
|
|
rectCorners,
|
|
rectAxes,
|
|
projectPoints,
|
|
aabbOverlap,
|
|
rectOverlapInfo,
|
|
rectsOverlap,
|
|
rectCircleOverlap,
|
|
isSoftPlacementIgnoredType,
|
|
itemFootprintRadius,
|
|
placementRectsFor,
|
|
hitTestItem,
|
|
itemPlacementOverlapBlocked,
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|