tarinai/js/simulation_maintenance_system.js
2026-07-20 14:36:59 +09:00

154 lines
7.2 KiB
JavaScript

"use strict";
// Layer: simulation/system-maintenance
// Owns low-frequency cleanup and cache maintenance. Heavy sweeps are deliberately
// staggered so they do not all land on the same animation frame.
(function (global) {
function initializeSchedule(worldRef) {
if (worldRef._maintenanceScheduleReady) return;
const now = Number(worldRef.time || 0) || 0;
worldRef._maintenanceScheduleReady = true;
worldRef._nextStructureDependencyCheckAt = now + 0.85;
worldRef._nextItemCompactAt = now + 1.65;
worldRef._nextAntCompactAt = now + 3.35;
worldRef._nextEffectCountRefreshAt = now + 2.15;
worldRef._nextTransientRuntimePruneAt = now + 4.75;
if (!Number.isFinite(worldRef.nextGrassLimitCheckAt) || worldRef.nextGrassLimitCheckAt <= now) worldRef.nextGrassLimitCheckAt = now + 1.25;
if (!Number.isFinite(worldRef.nextFireCompactAt) || worldRef.nextFireCompactAt <= now) worldRef.nextFireCompactAt = now + 0.45;
if (!Number.isFinite(worldRef.nextGrassBedExpiryCheckAt) || worldRef.nextGrassBedExpiryCheckAt <= now) worldRef.nextGrassBedExpiryCheckAt = now + 2.85;
if (!Number.isFinite(worldRef.outOfBoundsCheckAt)) worldRef.outOfBoundsCheckAt = now - 1.55;
}
function runMaintenance(worldRef, dt, itemCountBefore) {
initializeSchedule(worldRef);
const now = Number(worldRef.time || 0) || 0;
let itemsCompacted = false;
worldRef.updateResidues?.(dt);
global.TarinaiMemorialBellSystem?.updateWorldNaturalDecay?.(worldRef);
// Link/plushie reconciliation is a safety net. Deletions already reconcile
// dependencies immediately, so a periodic pass is sufficient during normal play.
if (now >= (worldRef._nextStructureDependencyCheckAt || 0)) {
worldRef._nextStructureDependencyCheckAt = now + 0.85;
const relevantCount = (worldRef.itemCounts?.plushie || 0)
+ (worldRef.itemCounts?.rope || 0) + (worldRef.itemCounts?.rod || 0)
+ (worldRef.itemCounts?.spring || 0) + (worldRef.itemCounts?.wire || 0)
+ (worldRef.itemCounts?.insulated_wire || 0);
if (relevantCount > 0 || worldRef.itemBucketsDirty) global.TarinaiStructureLifecycle?.maintainDependencies?.(worldRef);
}
// Keep the existing creature cleanup cadence separate; item/ant sweeps are
// offset so large arrays are not compacted on the same frame.
worldRef.compactTimer += dt;
if (worldRef.compactTimer >= 2.5) {
worldRef.compactTarinai();
worldRef.compactTimer = 0;
}
if (now >= (worldRef._nextItemCompactAt || 0)) {
worldRef._nextItemCompactAt = now + 3.7;
itemsCompacted = worldRef.compactItems();
}
if (now >= (worldRef._nextAntCompactAt || 0)) {
worldRef._nextAntCompactAt = now + 4.3;
worldRef.compactAnts?.();
}
// Runtime-only collision/social dedupe caches must expire independently of
// deaths. Stable live population does not imply stable entity identities, and
// the old death-triggered cleanup let these collections grow for many seasons.
if (now >= (worldRef._nextTransientRuntimePruneAt || 0)) {
worldRef._nextTransientRuntimePruneAt = now + 5.0;
if (worldRef.ballCollisionMemo instanceof Map) {
for (const [key, at] of worldRef.ballCollisionMemo) {
if (now - Number(at || 0) > 2.0) worldRef.ballCollisionMemo.delete(key);
}
}
if (worldRef.tarinaiCollisionMemo instanceof Map) {
for (const [key, at] of worldRef.tarinaiCollisionMemo) {
if (now - Number(at || 0) > 2.0) worldRef.tarinaiCollisionMemo.delete(key);
}
}
if (worldRef.relationNotices && typeof worldRef.relationNotices === "object") {
for (const [key, at] of Object.entries(worldRef.relationNotices)) {
if (now - Number(at || 0) > 120) delete worldRef.relationNotices[key];
}
}
if (worldRef.fightPairCooldowns && typeof worldRef.fightPairCooldowns === "object") {
for (const [key, until] of Object.entries(worldRef.fightPairCooldowns)) {
if (Number(until || 0) <= now) delete worldRef.fightPairCooldowns[key];
}
}
if (worldRef.resolvedFightIds && typeof worldRef.resolvedFightIds === "object") {
for (const [key, at] of Object.entries(worldRef.resolvedFightIds)) {
if (!Number.isFinite(Number(at)) || now - Number(at) > 8.0) delete worldRef.resolvedFightIds[key];
}
}
}
// Family graph normalization/component pruning is intentionally deferred to
// the family view. It is unrelated to simulation correctness and its full-
// graph sweep becomes increasingly expensive as generations accumulate.
const itemCountChanged = worldRef.items.length !== itemCountBefore || worldRef.items.length !== (worldRef._lastMaintenanceItemCount ?? worldRef.items.length);
const effectCountChanged = worldRef.effects.length !== (worldRef._lastMaintenanceEffectCount ?? worldRef.effects.length);
if (itemsCompacted || itemCountChanged || worldRef.itemBucketsDirty) {
worldRef.updateItemCounts();
worldRef._lastMaintenanceItemCount = worldRef.items.length;
}
if (effectCountChanged || now >= (worldRef._nextEffectCountRefreshAt || 0)) {
worldRef._nextEffectCountRefreshAt = now + 4.6;
worldRef.updateEffectCounts();
worldRef._lastMaintenanceEffectCount = worldRef.effects.length;
}
if (now >= (worldRef.nextGrassLimitCheckAt || 0)) {
worldRef.nextGrassLimitCheckAt = now + 3.0;
const removedGrass = worldRef.enforceGrassLimit?.("periodic-grass-limit") || 0;
if (removedGrass > 0) {
worldRef.compactItems();
worldRef.updateItemCounts();
}
}
if (now >= (worldRef.nextFireCompactAt || 0)) {
worldRef.nextFireCompactAt = now + 0.8;
const compactedFires = global.TarinaiItemDynamicToolSystem?.compactWorldFires?.(worldRef) || 0;
if (compactedFires > 0) worldRef.updateItemCounts();
}
if (now >= (worldRef.nextGrassBedExpiryCheckAt || 0)) {
worldRef.nextGrassBedExpiryCheckAt = now + 5.0;
const lifespan = Math.max(1, CONFIG.dayLength || 120);
let removedBeds = 0;
const grassBeds = typeof worldRef.itemsOfType === "function" ? worldRef.itemsOfType("grass_bed") : (worldRef.items || []);
for (const it of grassBeds || []) {
if (!it || it.dead || it.type !== "grass_bed") continue;
if (!Number.isFinite(Number(it.createdAt))) it.createdAt = now;
if (now - Number(it.createdAt || 0) < lifespan) continue;
const removed = global.TarinaiStructureLifecycle?.deleteItem?.(worldRef, it, {
reason: "grass-bed-expired",
userReason: "\u304b\u3093\u305f\u3093\u30d9\u30c3\u30c9\u304c\u53e4\u304f\u306a\u3063\u3066\u6d88\u3048\u305f",
wake: false,
panicOwner: false,
});
if (removed) removedBeds += 1;
}
if (removedBeds > 0) {
worldRef.compactItems();
worldRef.updateItemCounts();
worldRef.markSpatialDirty?.("grass-bed-expired");
}
}
if ((worldRef.outOfBoundsCheckAt || -999) + 5.0 <= now) {
worldRef.outOfBoundsCheckAt = now;
worldRef.sanitizeOutOfBounds();
}
worldRef.drawSortTimer = 0;
}
global.TarinaiMaintenanceSimulationSystem = Object.freeze({
phases: Object.freeze({ runMaintenance }),
});
})(typeof window !== "undefined" ? window : globalThis);