"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() { this.workBudget = { ai: Math.max(1, Math.ceil(((this.tarinai || []).length || 1) / 2)), env: Math.max(1, Math.ceil(((this.tarinai || []).length || 1) / 3)), aiUsed: 0, envUsed: 0, }; this.workStats = { aiRuns: 0, aiSkips: 0, envRuns: 0, envSkips: 0, collisionRuns: 0, collisionSkips: 0, bedConflictRuns: 0, bedConflictSkips: 0, }; }, 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 = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(t) : t.behavior; const id = behavior?.actionId || t.state || "none"; behaviorCounts[id] = (behaviorCounts[id] || 0) + 1; if (typeof isTarinaiBehaviorValueForced === "function" ? isTarinaiBehaviorValueForced(behavior) : Boolean(behavior?.source === "forced" || behavior?.forcedId || behavior?.forcedRequest)) 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 || "", }, 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, 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, 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; // Rebuild at most once per frame. Queries after movement may use the previous // frame's grid until the next update-start; this is acceptable for this game // and avoids repeated full-grid rebuilds under dense scenes. if ((this.spatialRebuildsThisFrame || 0) > 0 && (this.spatialVersion || 0) > 0) return false; 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; return true; }, markItemBucketsDirty(reason = "manual") { this.itemBucketsDirty = true; this.itemBucketsDirtyReason = reason; this.lastItemBucketsDirtyReason = reason; this.itemBucketDirtyMarksTotal = (this.itemBucketDirtyMarksTotal || 0) + 1; }, ensureItemBuckets(reason = "read") { if (!this.itemTypeBuckets || !this.itemIdMap || this.itemBucketsDirty) { this.updateItemCounts(reason); return true; } return false; }, addItem(item, reason = "add-item", opts = {}) { if (!item) 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 || []; }, updateItemCounts(reason = "manual") { const counts = this.itemCounts; if (!this.itemTypeBuckets) this.itemTypeBuckets = new Map(); if (!this.itemIdMap) this.itemIdMap = new Map(); for (const key of Object.keys(counts)) counts[key] = 0; this.itemTypeBuckets.clear(); this.itemIdMap.clear(); 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); let bucket = this.itemTypeBuckets.get(it.type); if (!bucket) { bucket = []; this.itemTypeBuckets.set(it.type, bucket); } bucket.push(it); } this.itemBucketsDirty = false; this.itemBucketsDirtyReason = ""; this.itemBucketRebuildsTotal = (this.itemBucketRebuildsTotal || 0) + 1; this.lastItemBucketRebuildReason = reason; }, liveItemsOfType(type, predicate = null) { const list = this.itemsOfType ? this.itemsOfType(type) : (this.items || []).filter(it => it && !it.dead && it.type === type); return predicate ? list.filter(predicate) : list; }, 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; }, compactEffects() { const limit = CONFIG.effectLimit ?? 96; if (Number.isFinite(limit) && this.effects.length > limit) { this.effects.sort((a, b) => (a.life / Math.max(a.maxLife || 1, 0.001)) - (b.life / Math.max(b.maxLife || 1, 0.001))); const remove = this.effects.length - limit; for (let i = 0; i < remove; i++) this.effects[i].life = 0; } let write = 0; for (let read = 0; read < this.effects.length; read++) { const ef = this.effects[read]; if (!ef.dead) this.effects[write++] = ef; } this.effects.length = write; }, 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);