"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);