"use strict"; // Layer: simulation/helpers // Shared, UI-free helpers used by the concrete simulation systems. (function (global) { const policy = () => global.TarinaiItemUpdatePolicy || {}; function forScheduledItems(worldRef, visitor) { if (policy().forScheduledItems) return policy().forScheduledItems(worldRef, visitor); if (!worldRef || typeof visitor !== "function") return 0; let count = 0; for (const it of worldRef.items || []) { if (!it || it.dead || it.type === "grass") continue; visitor(it); count += 1; } return count; } function itemUpdateInterval(item) { const interval = policy().itemUpdateInterval?.(item); return Number.isFinite(interval) ? interval : Infinity; } function updateItemsScheduled(worldRef, dt) { if (global.TarinaiItemRuntime?.updateScheduled) return global.TarinaiItemRuntime.updateScheduled(worldRef, dt); if (global.TarinaiItemUpdateScheduler?.run) return global.TarinaiItemUpdateScheduler.run(worldRef, dt); let ran = 0; forScheduledItems(worldRef, (it) => { const interval = itemUpdateInterval(it); if (!Number.isFinite(interval)) return; if (interval <= 0) { if (global.TarinaiItemRuntime?.updateOne) global.TarinaiItemRuntime.updateOne(it, dt, worldRef); else it.update?.(dt, worldRef); ran += 1; return; } it._scheduledUpdateDt = Math.min(10, (it._scheduledUpdateDt || 0) + dt); if (it._scheduledUpdateDt < interval) return; const scheduledDt = it._scheduledUpdateDt; it._scheduledUpdateDt = 0; if (global.TarinaiItemRuntime?.updateOne) global.TarinaiItemRuntime.updateOne(it, scheduledDt, worldRef); else it.update?.(scheduledDt, worldRef); ran += 1; }); return ran; } function markSpatialDirtyIfMoved(worldRef, list, reason = "moved", threshold = 0.35) { if (!worldRef || !Array.isArray(list) || !list.length) return false; const thresholdSq = threshold * threshold; let moved = false; for (const entity of list) { if (!entity || entity.dead) continue; const x = Number.isFinite(entity.x) ? entity.x : 0; const y = Number.isFinite(entity.y) ? entity.y : 0; const oldX = entity._spatialTrackX; const oldY = entity._spatialTrackY; if (!Number.isFinite(oldX) || !Number.isFinite(oldY)) { entity._spatialTrackX = x; entity._spatialTrackY = y; moved = true; continue; } const dx = x - oldX; const dy = y - oldY; if (dx * dx + dy * dy <= thresholdSq) continue; entity._spatialTrackX = x; entity._spatialTrackY = y; moved = true; } if (moved) worldRef.markSpatialDirty?.(reason); return moved; } function markScheduledItemsMoved(worldRef, reason = "items-moved", threshold = 0.35) { if (!worldRef) return false; const thresholdSq = threshold * threshold; let moved = false; const tracked = global.TarinaiItemRuntime?.trackedDynamicItems?.(worldRef) || global.TarinaiItemUpdateScheduler?.trackedDynamicItems?.(worldRef); const source = tracked && tracked.length ? tracked : null; const visit = (entity) => { const x = Number.isFinite(entity.x) ? entity.x : 0; const y = Number.isFinite(entity.y) ? entity.y : 0; const oldX = entity._spatialTrackX; const oldY = entity._spatialTrackY; if (!Number.isFinite(oldX) || !Number.isFinite(oldY)) { entity._spatialTrackX = x; entity._spatialTrackY = y; moved = true; return; } const dx = x - oldX; const dy = y - oldY; if (dx * dx + dy * dy <= thresholdSq) return; entity._spatialTrackX = x; entity._spatialTrackY = y; moved = true; }; if (source) { for (const entity of source) if (entity && !entity.dead) visit(entity); } else { forScheduledItems(worldRef, visit); } if (moved) worldRef.markSpatialDirty?.(reason); return moved; } function updateVisibleWorldRect(worldRef, margin = 120) { const scale = worldRef.viewScale ? worldRef.viewScale() : 1; const vw = (worldRef.viewportW || worldRef.w || 1000) / Math.max(0.01, scale); const vh = (worldRef.viewportH || worldRef.h || 720) / Math.max(0.01, scale); const cameraX = worldRef.cameraX || 0; const cameraY = worldRef.cameraY || 0; return { left: cameraX - margin, top: cameraY - margin, right: cameraX + vw + margin, bottom: cameraY + vh + margin }; } function pointInRect(x, y, rect) { return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; } const helpers = Object.freeze({ forScheduledItems, itemUpdateInterval, updateItemsScheduled, markSpatialDirtyIfMoved, markScheduledItemsMoved, updateVisibleWorldRect, pointInRect, }); global.TarinaiSimulationRuntime = Object.freeze({ helpers, constants: Object.freeze({ get REALTIME_ITEM_TYPES() { return policy().REALTIME_ITEM_TYPES || new Set(); }, get PIN_ITEM_TYPES() { return policy().PIN_ITEM_TYPES || new Set(); }, get SCHEDULED_ITEM_TYPES() { return policy().SCHEDULED_ITEM_TYPES || Object.freeze([]); }, }), }); })(typeof window !== "undefined" ? window : globalThis);