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