140 lines
5.2 KiB
JavaScript
140 lines
5.2 KiB
JavaScript
|
|
"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);
|