"use strict"; (function (global) { const World = global.World; if (!World) throw new Error("World is not available for mixin: world_spatial_budget.js"); Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({ beginFramePerformanceBudgets() { const profile = global.TarinaiPerf?.performanceProfile?.() || {}; this.workBudget = { ai: Math.max(1, Number(profile.aiBudget || 10)), env: Math.max(1, Number(profile.envBudget || 5)), aiUsed: 0, envUsed: 0, }; this.workStats = { aiRuns: 0, aiSkips: 0, envRuns: 0, envSkips: 0, collisionRuns: 0, collisionSkips: 0, bedConflictRuns: 0, bedConflictSkips: 0, }; this.spatialDirtyReasonCountsThisFrame = {}; this.spatialRebuildReasonCountsThisFrame = {}; this.deferredSpatialReadsThisFrame = 0; this.terrainDirtyStatsThisFrame = { raw: 0, global: 0, chunk: 0, coalesced: 0, reasons: {} }; this.nearbyQueryStatsThisFrame = { obstacleRectHits: 0, obstacleRectMisses: 0, obstacleRectBypass: 0, obstacleRectBuilt: 0 }; this.constraintDirtyStatsThisFrame = { marked: 0, coalesced: 0 }; if (!this._solidObstacleRectQueryCache) this._solidObstacleRectQueryCache = new Map(); }, spendScheduledWork(kind = "ai", urgent = false) { if (!this.workBudget) this.beginFramePerformanceBudgets?.(); const stats = this.workStats || {}; const budget = this.workBudget || {}; const usedKey = `${kind}Used`; const runKey = `${kind}Runs`; const skipKey = `${kind}Skips`; if (urgent || (budget[usedKey] || 0) < (budget[kind] || 0)) { budget[usedKey] = (budget[usedKey] || 0) + 1; stats[runKey] = (stats[runKey] || 0) + 1; return true; } stats[skipKey] = (stats[skipKey] || 0) + 1; return false; }, spatialDirtySummary() { return { staticItems: Boolean(this.spatialStaticItemsDirty), dynamicItems: Boolean(this.spatialDynamicItemsDirty), tarinai: Boolean(this.spatialTarinaiDirty), ants: Boolean(this.spatialAntDirty), }; }, runtimeDiagnostics() { const behaviorCounts = {}; let forcedBehaviors = 0; let lockedBehaviors = 0; let liveTarinai = 0; for (const t of this.tarinai || []) { if (!t || t.dead) continue; liveTarinai += 1; const behavior = currentTarinaiBehavior(t); const id = behavior?.actionId || t.state || "none"; behaviorCounts[id] = (behaviorCounts[id] || 0) + 1; if (isTarinaiBehaviorValueForced(behavior)) forcedBehaviors += 1; if ((t.behaviorLockTimer || 0) > 0.01) lockedBehaviors += 1; } return { spatial: { version: this.spatialVersion || 0, dirty: Boolean(this.spatialDirty), dirtyParts: this.spatialDirtySummary?.() || null, dirtyReason: this.spatialDirtyReason || this.lastSpatialDirtyReason || "", dirtyMarksThisFrame: this.spatialDirtyMarksThisFrame || 0, dirtyMarksTotal: this.spatialDirtyMarksTotal || 0, rebuildsThisFrame: this.spatialRebuildsThisFrame || 0, rebuildsTotal: this.spatialRebuildsTotal || 0, partialRebuildsTotal: this.spatialPartialRebuildsTotal || 0, lastRebuildReason: this.lastSpatialRebuildReason || "", dirtyReasons: this.spatialDirtyReasonCountsThisFrame || {}, rebuildReasons: this.spatialRebuildReasonCountsThisFrame || {}, deferredReads: this.deferredSpatialReadsThisFrame || 0, }, render: { drawList: (this.drawList || []).length, drawListDirty: Boolean(this.drawListDirty), drawSortTimer: Number(this.drawSortTimer || 0), }, items: { total: (this.items || []).length, bucketTypes: this.itemTypeBuckets?.size || 0, bucketsDirty: Boolean(this.itemBucketsDirty), idMapSize: this.itemIdMap?.size || 0, ownerBuckets: this.itemOwnerBuckets?.size || 0, bucketRebuildsTotal: this.itemBucketRebuildsTotal || 0, }, behavior: { forced: forcedBehaviors, locked: lockedBehaviors, counts: behaviorCounts, }, runtime: { liveTarinai, fps: window.__tarinaiFps || 0, dpr: (typeof uiCache !== "undefined" ? uiCache?.canvasDpr : window.uiCache?.canvasDpr) || 0, }, work: { budget: this.workBudget || null, stats: this.workStats || null, }, scheduler: this._itemUpdateSchedulerStats || null, creatures: this._creatureUpdateStats || null, effects: this._effectUpdateStats || null, effectRender: this._effectRenderStats || null, terrain: this.terrainDirtyStatsThisFrame || null, nearby: this.nearbyQueryStatsThisFrame || null, constraintDirty: this.constraintDirtyStatsThisFrame || null, perf: window.TarinaiPerf.snapshot() || null, }; }, grassLimit() { return window.TarinaiGround.grassLimit(this.groundType || "soil", this.fieldType || "garden") ?? (CONFIG.grassLimit ?? 99); }, canAddGrass(extra = 1) { const limit = this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 99); if (!Number.isFinite(limit)) return true; const current = Math.max(0, Math.floor(Number(this.itemCounts?.grass || 0))); return current + Math.max(0, Number(extra) || 0) <= limit; }, enforceGrassLimit(reason = "grass-limit") { const limit = this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 99); if (!Number.isFinite(limit)) return 0; this.updateItemCounts?.(`${reason}:count`); let kept = 0; let removed = 0; for (const it of this.items || []) { if (!it || it.dead || it.type !== "grass") continue; if (kept < limit) { kept += 1; continue; } it.amount = 0; it.growth = 0; it.grassStage = 0; removed += 1; } if (removed > 0) { this.markItemBucketsDirty?.(reason); this.markSpatialDirty?.(reason); this.markTerrainDirty?.(reason); } return removed; }, ensureSpatial(reason = "read") { if (!this.spatial) return false; if (!this.spatialDirty && (this.spatialVersion || 0) > 0) return false; // During the creature pass, Tarinai movement marks only the Tarinai bucket // dirty many times. Obstacle/static item cells are still valid, so spatial // reads can use the frame-start Tarinai grid and collapse those invalidations // into one post-creature rebuild. Do not defer static/dynamic/ant dirtiness: // those buckets are used by collision and contact queries and need fresh data. const inDeferredPhase = (this.deferSpatialRebuildDepth || 0) > 0 && (this.spatialVersion || 0) > 0; const canDeferTarinaiOnly = inDeferredPhase && this.spatialTarinaiDirty && !this.spatialStaticItemsDirty && !this.spatialDynamicItemsDirty && !this.spatialAntDirty; const canDeferMobileOnly = inDeferredPhase && String(this.deferSpatialRebuildMode || "") === "mobile" && !this.spatialStaticItemsDirty && (this.spatialDynamicItemsDirty || this.spatialTarinaiDirty || this.spatialAntDirty); if (canDeferTarinaiOnly || canDeferMobileOnly) { this.deferredSpatialDirtyReason = reason; this.deferredSpatialReadsThisFrame = (this.deferredSpatialReadsThisFrame || 0) + 1; return false; } // Collision/contact queries must not read a previous-grid position after // moving static/dynamic obstacles in the same frame. Partial rebuilds are // already split by static/dynamic/tarinai/ant buckets, so keep those reads // correct here and let dirty flags decide how much work is required. return this.rebuildSpatial(true, reason); }, nearbyItems(x, y, radius, precise = false) { this.ensureSpatial?.("nearby-items"); const out = this.spatialItemScratch || []; out.length = 0; if (this.spatial?.nearbyInto && this.spatial.staticItemCells && this.spatial.dynamicItemCells) { this.spatial.nearbyInto(this.spatial.staticItemCells, x, y, radius, out, precise); this.spatial.nearbyInto(this.spatial.dynamicItemCells, x, y, radius, out, precise); return out; } return this.spatial.nearby(this.spatial.itemCells, x, y, radius, out, precise); }, nearbyTarinai(x, y, radius, precise = false) { this.ensureSpatial?.("nearby-tarinai"); return this.spatial.nearby(this.spatial.tarinaiCells, x, y, radius, this.spatialTarinaiScratch, precise); }, nearbyAnts(x, y, radius, precise = false) { this.ensureSpatial?.("nearby-ants"); return this.spatial.nearby(this.spatial.antCells, x, y, radius, this.spatialAntScratch, precise); }, nearbyFood(x, y, radius, precise = false) { this.ensureSpatial?.("nearby-food"); if (this.spatial?.nearbySplitInto && this.spatial.staticFoodCells && this.spatial.dynamicFoodCells) { return this.spatial.nearbySplitInto(this.spatial.staticFoodCells, this.spatial.dynamicFoodCells, x, y, radius, this.spatialFoodScratch, precise); } return this.spatial.nearby(this.spatial.foodCells, x, y, radius, this.spatialFoodScratch, precise); }, nearbyObstacles(x, y, radius, precise = false) { this.ensureSpatial?.("nearby-obstacles"); if (this.spatial?.nearbySplitInto && this.spatial.staticObstacleCells && this.spatial.dynamicObstacleCells) { return this.spatial.nearbySplitInto(this.spatial.staticObstacleCells, this.spatial.dynamicObstacleCells, x, y, radius, this.spatialObstacleScratch, precise); } return this.spatial.nearby(this.spatial.obstacleCells, x, y, radius, this.spatialObstacleScratch, precise); }, nearbyHazards(x, y, radius, precise = false) { this.ensureSpatial?.("nearby-hazards"); if (this.spatial?.nearbySplitInto && this.spatial.staticHazardCells && this.spatial.dynamicHazardCells) { return this.spatial.nearbySplitInto(this.spatial.staticHazardCells, this.spatial.dynamicHazardCells, x, y, radius, this.spatialHazardScratch, precise); } return this.spatial.nearby(this.spatial.hazardCells, x, y, radius, this.spatialHazardScratch, precise); }, rebuildSpatial(force = false, reason = "manual") { // Direct calls keep their old eager semantics. update() uses markSpatialDirty + ensureSpatial // so repeated rebuild requests in the same phase are collapsed to one rebuild. if (!force && this.spatialDirty === false && reason !== "manual") return false; const profiler = window.TarinaiPerf; const end = profiler.begin("update.spatial") || null; try { const full = reason === "manual" || reason === "reset" || !this.spatialVersion || !this.spatial?.rebuildStaticItems; if (full) { this.spatial.rebuild(this.items, this.tarinai, this.ants || []); } else { const staticDirty = Boolean(this.spatialStaticItemsDirty); const dynamicDirty = Boolean(this.spatialDynamicItemsDirty || staticDirty); const tarinaiDirty = Boolean(this.spatialTarinaiDirty); const antDirty = Boolean(this.spatialAntDirty); if (staticDirty) this.spatial.rebuildStaticItems(this.items, { rebuildCombined: false }); if (dynamicDirty) this.spatial.rebuildDynamicItems(this.items, { rebuildCombined: false }); if (tarinaiDirty) this.spatial.rebuildTarinai(this.tarinai); if (antDirty) this.spatial.rebuildAnts(this.ants || []); this.spatialPartialRebuildsTotal = (this.spatialPartialRebuildsTotal || 0) + 1; } } finally { if (end) end(); } this.spatialDirty = false; this.spatialStaticItemsDirty = false; this.spatialDynamicItemsDirty = false; this.spatialTarinaiDirty = false; this.spatialAntDirty = false; this.spatialDirtyReason = ""; this.spatialVersion = (this.spatialVersion || 0) + 1; this.spatialRebuildsThisFrame = (this.spatialRebuildsThisFrame || 0) + 1; this.spatialRebuildsTotal = (this.spatialRebuildsTotal || 0) + 1; this.lastSpatialRebuildReason = reason; if (!this.spatialRebuildReasonCountsThisFrame) this.spatialRebuildReasonCountsThisFrame = {}; this.spatialRebuildReasonCountsThisFrame[String(reason || "manual")] = (this.spatialRebuildReasonCountsThisFrame[String(reason || "manual")] || 0) + 1; return true; }, markItemBucketsDirty(reason = "manual") { this.itemBucketsDirty = true; }, ensureItemBuckets(reason = "read") { if (!this.itemTypeBuckets || !this.itemIdMap || !this.itemOwnerBuckets || this.itemBucketsDirty) { this.updateItemCounts(reason); return true; } return false; }, normalizeColonyLimit(value = 0) { const n = Math.floor(Number(value) || 0); return Number.isFinite(n) && n > 0 ? n : 0; }, activeTarinaiCount() { let count = 0; for (const t of this.tarinai || []) if (t && !t.dead) count += 1; return count; }, activeObjectCount() { let count = 0; for (const item of this.items || []) if (item && !item.dead) count += 1; return count; }, canAddTarinai(count = 1) { const limit = this.normalizeColonyLimit(this.tarinaiPopulationLimit); return limit <= 0 || this.activeTarinaiCount() + Math.max(0, Math.floor(Number(count) || 0)) <= limit; }, canAddObjects(count = 1) { const limit = this.normalizeColonyLimit(this.objectLimit); return limit <= 0 || this.activeObjectCount() + Math.max(0, Math.floor(Number(count) || 0)) <= limit; }, setTarinaiPopulationLimit(value = 0) { this.tarinaiPopulationLimit = this.normalizeColonyLimit(value); return this.tarinaiPopulationLimit; }, setObjectLimit(value = 0) { this.objectLimit = this.normalizeColonyLimit(value); return this.objectLimit; }, addItem(item, reason = "add-item", opts = {}) { if (!item) return null; if (!this.canAddObjects(1)) return null; if (item.type === "grass" && !this.canAddGrass?.(1)) return null; item.world = this; this.items.push(item); this.markItemBucketsDirty?.(reason); this.markSpatialDirty?.(reason); if (opts.terrain !== false) { if (item.type === "grass" || item.type === "trace" || item.type === "splat") this.markTerrainDirtyAt?.(item.x, item.y, Math.max(item.r || item.radius || 24, 36), reason); else this.markTerrainDirty?.(reason); } if (opts.countNow !== false && item.type) this.itemCounts[item.type] = (this.itemCounts[item.type] || 0) + 1; return item; }, itemById(id) { if (id == null || id === "") return null; this.ensureItemBuckets?.("item-by-id"); const item = this.itemIdMap?.get?.(id) || null; return item && !item.dead ? item : null; }, itemsOfType(type) { this.ensureItemBuckets?.(`items-of-type:${type}`); const bucket = this.itemTypeBuckets?.get?.(type); return bucket || []; }, ownedItemsFor(ownerId) { if (ownerId == null || ownerId === "") return []; this.ensureItemBuckets?.(`owned-items:${ownerId}`); return this.itemOwnerBuckets?.get?.(ownerId) || []; }, updateItemCounts(reason = "manual") { const counts = this.itemCounts; if (!this.itemTypeBuckets) this.itemTypeBuckets = new Map(); if (!this.itemIdMap) this.itemIdMap = new Map(); if (!this.itemOwnerBuckets) this.itemOwnerBuckets = new Map(); if (!this.carriedPlushies) this.carriedPlushies = []; for (const key of Object.keys(counts)) counts[key] = 0; this.itemTypeBuckets.clear(); this.itemIdMap.clear(); this.itemOwnerBuckets.clear(); this.carriedPlushies.length = 0; for (const it of this.items) { if (!it || it.dead) continue; counts[it.type] = (counts[it.type] || 0) + 1; if (it.id != null) this.itemIdMap.set(it.id, it); if (it.isStructure && it.type === "plushie" && it.carriedById) this.carriedPlushies.push(it); if (it.ownerId) { let ownerBucket = this.itemOwnerBuckets.get(it.ownerId); if (!ownerBucket) { ownerBucket = []; this.itemOwnerBuckets.set(it.ownerId, ownerBucket); } ownerBucket.push(it); } let bucket = this.itemTypeBuckets.get(it.type); if (!bucket) { bucket = []; this.itemTypeBuckets.set(it.type, bucket); } bucket.push(it); } this.itemBucketsDirty = false; this.itemBucketRebuildsTotal = (this.itemBucketRebuildsTotal || 0) + 1; }, updateEffectCounts() { const counts = this.effectCounts; for (const key of Object.keys(counts)) counts[key] = 0; for (const ef of this.effects) counts[ef.type] = (counts[ef.type] || 0) + 1; }, compactItems() { const before = this.items.length; let write = 0; for (let read = 0; read < this.items.length; read++) { const it = this.items[read]; if (it.type === "mirror") continue; if (!it.dead) this.items[write++] = it; } this.items.length = write; const changed = before !== this.items.length; if (changed) { this.markSpatialDirty?.("compact-items"); this.markTerrainDirty?.("compact-items"); this.markItemBucketsDirty?.("compact-items"); } return changed; }, compactTarinai() { let write = 0; for (let read = 0; read < this.tarinai.length; read++) { const t = this.tarinai[read]; if (!t.dead) this.tarinai[write++] = t; } if (this.tarinai.length !== write) this.drawListDirty = true; this.tarinai.length = write; }, compactAnts() { if (!this.ants) this.ants = []; let write = 0; for (let read = 0; read < this.ants.length; read++) { const a = this.ants[read]; if (a && !a.dead) this.ants[write++] = a; } if (this.ants.length !== write) this.drawListDirty = true; this.ants.length = write; } })); })(typeof window !== "undefined" ? window : globalThis);