"use strict"; // Layer: physics/placement-preview // Single source of truth for placement preview geometry and the boundary part // of placement blocking. Render and actual placement both delegate here so a // green preview means the same footprint the click will try to place. (function (global) { const num = global.TarinaiCoreHelpers?.finiteOr || ((v, fallback = 0) => { const n = Number(v); return Number.isFinite(n) ? n : fallback; }); function itemRadius(type, fallback = 12) { return global.itemRadiusFor(type, fallback); } function toolTypeFor(worldRef) { return global.toolItemType(worldRef?.tool || ""); } function toolSizeScale(worldRef, type) { return worldRef?.toolSizeScale ? worldRef.toolSizeScale(type) : 1; } function toolSizeName(worldRef, type) { return worldRef?.toolSizeFor ? worldRef.toolSizeFor(type) : (worldRef?.toolSize || "medium"); } function pickMode(worldRef) { const value = String(worldRef?.toolPickMode || "free").toLowerCase(); if (value === "grid") return "grid40"; // compatibility with v39.16.34 return new Set(["grid20", "grid40", "grid60"]).has(value) ? value : "free"; } function gridStep(worldRef) { const mode = pickMode(worldRef); if (mode === "grid20") return 20; if (mode === "grid60") return 60; if (mode === "grid40") return 40; return 0; } function snapPoint(worldRef, x = 0, y = 0) { const step = gridStep(worldRef); if (!step) return { x: num(x), y: num(y), snapped: false, step: 0 }; return { x: Math.round(num(x) / step) * step, y: Math.round(num(y) / step) * step, snapped: true, step, }; } function isRotatable(type) { return global.isRotatableItemType(type); } function defaultAngle(type) { return global.defaultItemAngle(type); } function rectOutside(worldRef, rect, itemType = "") { if (!worldRef || !rect) return true; const pad = global.CONFIG?.worldPadding || 30; const fence = global.isFenceItemType?.(itemType); return num(rect.left) < pad || num(rect.right) > (worldRef.w || 0) - pad || num(rect.top) < 0 || num(rect.bottom) > (worldRef.h || 0) - (fence ? 0 : pad); } function circleOutside(worldRef, x, y, radius) { if (!worldRef) return true; const pad = global.CONFIG?.worldPadding || 30; const r = Math.max(0, num(radius)); return num(x) - r < pad || num(y) - r < 0 || num(x) + r > (worldRef.w || 0) - pad || num(y) + r > (worldRef.h || 0) - pad; } function orientedAabb(rect) { if (!rect) return null; if (!rect.oriented) return rect; const reach = Math.max(num(rect.halfW), num(rect.halfH)); return { left: num(rect.cx) - reach, right: num(rect.cx) + reach, top: num(rect.cy) - reach, bottom: num(rect.cy) + reach }; } function rectsFor(worldRef, item) { return global.TarinaiCollisionFootprints.placementRectsFor(worldRef, item); } function footprintRadius(worldRef, item, rects = null) { return global.TarinaiCollisionFootprints.itemFootprintRadius(worldRef, item, rects); } function applyToolOrientation(worldRef, item, requestedAngle = null) { if (!item || !item.type) return item; const type = String(item.type || ""); if (!isRotatable(type)) return item; const angle = requestedAngle ?? (worldRef?.toolAngleFor ? worldRef.toolAngleFor(type) : defaultAngle(type)); if (type === "reciprocator") { // The tool angle controls the rail axis. The body itself keeps its // canonical placement angle. Apply both after the body exists so the // preview and the placed Item cannot diverge by 90 degrees. const bodyAngle = defaultAngle(type); item.angle = bodyAngle; const pb = global.TarinaiPhysicsBodySystem; const body = pb?.ensureBody?.(item, worldRef || null, { syncFromLegacy: false }); if (body?.pose) body.pose.angle = bodyAngle; pb?.setScalar?.(item, "railAxis", angle, "placement-rail-axis"); pb?.invalidateItem?.(item, "placement-orientation"); return item; } item.angle = angle; if (global.TarinaiMechanicalSystem?.isMechanicalType?.(type)) { const pb = global.TarinaiPhysicsBodySystem; const body = pb?.ensureBody?.(item, worldRef || null, { syncFromLegacy: false }); if (body?.pose) body.pose.angle = angle; pb?.invalidateItem?.(item, "placement-orientation"); } return item; } function ensureMechanicalPreview(item, worldRef) { if (!item || !global.TarinaiMechanicalSystem.isMechanicalType(item.type)) return item; const pb = global.TarinaiPhysicsBodySystem; pb?.ensureBody?.(item, worldRef || null, { syncFromLegacy: false }); if (item.type === "rotator") { pb?.setScalar?.(item, "motorSpeed", Math.PI * 0.65, "preview"); pb?.setScalar?.(item, "motorOn", true, "preview"); pb?.setScalar?.(item, "thickness", 12, "preview"); pb?.setSegments?.(item, [[-78, 0, 78, 0], [0, -52, 0, 52]], "preview"); } else if (item.type === "poison_block") { pb?.setScalar?.(item, "thickness", 11, "preview"); item.world = worldRef || null; } else if (item.type === "reciprocator") { pb?.setScalar?.(item, "railOn", true, "preview"); pb?.setScalar?.(item, "railMotorSpeed", 92, "preview"); pb?.setScalar?.(item, "railTravel", 150, "preview"); pb?.setScalar?.(item, "railPhase", 0, "preview"); pb?.setScalar?.(item, "railAnchorX", item.x, "preview"); pb?.setScalar?.(item, "railAnchorY", item.y, "preview"); pb?.setScalar?.(item, "thickness", 12, "preview"); pb?.setSegments?.(item, [[-78, 0, 78, 0]], "preview"); } return item; } function makePreviewItem(worldRef, type, x, y, opts = {}) { if (!worldRef || !type) return null; const scale = opts.sizeScale ?? toolSizeScale(worldRef, type); const sizeName = opts.toolSize ?? toolSizeName(worldRef, type); const r = (opts.r ?? itemRadius(type, 12)) * scale; const item = { type, x: num(x), y: num(y), r, toolSize: sizeName, foodServingScale: scale, dead: false, isPlacementPreview: true }; if (isServingFoodType(type)) { item.foodServingsMax = foodServingsForSize(sizeName); item.foodServingsRemaining = item.foodServingsMax; item.amount = item.foodServingsRemaining; (typeof updateServingFoodVisualSize === "function" ? updateServingFoodVisualSize(item) : false); } if (isRotatable(type)) applyToolOrientation(worldRef, item, opts.angle); ensureMechanicalPreview(item, worldRef); return item; } function boundsBlocked(worldRef, item, opts = {}) { if (!worldRef || !item || item.dead) return true; const rects = opts.rects || rectsFor(worldRef, item); if (rects.length) { const fence = global.isFenceItemType?.(item.type); return rects.some(rect => rectOutside(worldRef, fence ? rect : orientedAabb(rect), item.type)); } const type = item.type || ""; const radius = opts.radius ?? Math.max(14, (num(item.r, itemRadius(type, 12)) || itemRadius(type, 12)) * (type === "bed" ? 1.55 : 1.25)); return circleOutside(worldRef, item.x, item.y, radius); } function clampPointFor(worldRef, item, x = item?.x || 0, y = item?.y || 0) { if (!worldRef || !item) return { x, y }; const pad = global.CONFIG?.worldPadding || 30; const rects = rectsFor(worldRef, item); if (rects.length) { let left = 0, right = 0, top = 0, bottom = 0; for (const rect of rects) { const aabb = global.isFenceItemType?.(item.type) ? rect : orientedAabb(rect); if (!aabb) continue; left = Math.max(left, num(item.x) - num(aabb.left)); right = Math.max(right, num(aabb.right) - num(item.x)); top = Math.max(top, num(item.y) - num(aabb.top)); bottom = Math.max(bottom, num(aabb.bottom) - num(item.y)); } const fence = global.isFenceItemType?.(item.type); const bottomPad = fence ? 0 : pad; return { x: global.clamp ? global.clamp(x, pad + left, (worldRef.w || 0) - pad - right) : Math.max(pad + left, Math.min((worldRef.w || 0) - pad - right, x)), y: global.clamp ? global.clamp(y, top, (worldRef.h || 0) - bottomPad - bottom) : Math.max(top, Math.min((worldRef.h || 0) - bottomPad - bottom, y)) }; } const radius = Math.max(14, (num(item.r, itemRadius(item.type, 12)) || itemRadius(item.type, 12)) * (item.type === "bed" ? 1.55 : 1.25)); return { x: global.clamp ? global.clamp(x, pad + radius, (worldRef.w || 0) - pad - radius) : Math.max(pad + radius, Math.min((worldRef.w || 0) - pad - radius, x)), y: global.clamp ? global.clamp(y, radius, (worldRef.h || 0) - pad - radius) : Math.max(radius, Math.min((worldRef.h || 0) - pad - radius, y)) }; } function overlayForItem(item) { if (!item || item.dead) return null; if (item.type === "toilet") { const footprint = global.TarinaiToiletSandSystem?.footprint?.(item) || null; if (footprint) return { shape: "roundedRect", ...footprint }; } if (item.type === "rain_shelter") { const footprint = global.TarinaiParasolSystem?.parasolFootprint?.(item) || null; if (footprint) return { shape: "roundedRect", centerX: footprint.centerX, centerY: footprint.centerY, halfWidth: footprint.radiusX, halfHeight: footprint.radiusY, cornerRadius: Math.max(8, Math.min(18, footprint.radiusY * 0.32)) }; } const def = global.toolDefinition?.(item.type) || null; if (def?.collisionShape === "circle") return { shape: "circle", centerX: num(item.x), centerY: num(item.y), radius: Math.max(8, num(item.r, itemRadius(item.type, 12))) }; return null; } function geometryFor(worldRef, item) { if (!worldRef || !item) return null; ensureMechanicalPreview(item, worldRef); if ((item.type === "fence_v" || item.type === "fence_h" || item.type === "bounce_fence" || item.type === "bounce_fence_v" || item.type === "gate_fence" || item.type === "one_way_fence") && worldRef.fenceRect) { return { rect: worldRef.fenceRect(item), rects: [] }; } if ((item.type === "nest_box" || item.type === "pipe") && worldRef.nestBoxBaseRect) return { rect: worldRef.nestBoxBaseRect(item), rects: [] }; const rects = rectsFor(worldRef, item); return { rect: null, rects }; } function previewForItem(worldRef, item, opts = {}) { if (!worldRef || !item) return null; const geometry = geometryFor(worldRef, item) || {}; const rects = geometry.rects || []; const radius = footprintRadius(worldRef, item, rects); const bounds = boundsBlocked(worldRef, item, { rects: rects.length ? rects : (geometry.rect ? [geometry.rect] : null) }); return { type: item.type, x: item.x, y: item.y, r: item.r || radius, rect: geometry.rect || null, rects, overlay: overlayForItem(item), boundsBlocked: bounds, blocked: opts.skipBlocked ? bounds : (bounds || Boolean(worldRef.placementBlocked?.(item) || worldRef.fencePlacementBlocked?.(item))), }; } function previewForWorld(worldRef) { const p = worldRef?.pointer; const type = toolTypeFor(worldRef); if (!p?.inside || !type) return null; const snapped = snapPoint(worldRef, p.x, p.y); const x = num(snapped.x), y = num(snapped.y); const tmp = makePreviewItem(worldRef, type, x, y); if (!tmp) return null; const r = tmp.r; let extra = {}; if (type === "dry_ice" || type === "stove") { const range = Math.max(24, global.CONFIG?.temperatureItemRange ?? 190); extra.influence = { kind: "temperatureEffectRange", shape: "circle", x, y, radius: range, type }; } if (type === "grass") { const overlaps = global.TarinaiPhysicsSoftClear.collectForPlacement(worldRef, tmp) || []; const replacingGrass = overlaps.some(it => it?.type === "grass"); const limitBlocked = !(worldRef.canAddGrass?.(1) ?? true) && !replacingGrass; const softBlocked = Boolean(worldRef.grassBlockedAt?.(x, y, tmp, { ignoreSoftPlacement: true }) || worldRef.grassOnTarinaiAt?.(x, y)); const b = limitBlocked || softBlocked || circleOutside(worldRef, x, y, Math.max(14, r * 1.55)); return { type, x, y, r, blocked: b, item: tmp }; } const geometry = geometryFor(worldRef, tmp) || {}; const rectsForBounds = geometry.rects && geometry.rects.length ? geometry.rects : (geometry.rect ? [geometry.rect] : null); const ownBounds = extra.boundsRadius ? circleOutside(worldRef, x, y, extra.boundsRadius) : boundsBlocked(worldRef, tmp, { rects: rectsForBounds }); const blocked = ownBounds || Boolean(worldRef.placementBlocked?.(tmp)); return { type, x, y, r, rect: geometry.rect || null, rects: geometry.rects || [], overlay: overlayForItem(tmp), influence: extra.influence || null, blocked, item: tmp }; } global.TarinaiPlacementPreviewSystem = Object.freeze({ applyToolOrientation, boundsBlocked, clampPointFor, forItem: previewForItem, forWorld: previewForWorld, gridStep, pickMode, snapPoint, }); })(typeof window !== "undefined" ? window : globalThis);