tarinai/js/world_spatial_budget.js

319 lines
12 KiB
JavaScript
Raw Normal View History

2026-06-21 14:09:35 +09:00
"use strict";
(function (global) {
const World = global.World;
if (!World) throw new Error("World is not available for mixin: world_spatial_budget.js");
2026-06-21 22:29:00 +09:00
function liveCount(list) {
let count = 0;
for (const item of list || []) if (item && !item.dead) count += 1;
return count;
}
2026-06-21 14:09:35 +09:00
Object.defineProperties(World.prototype, Object.getOwnPropertyDescriptors({
performanceLevel() {
if (window.TarinaiPerformance) return window.TarinaiPerformance.level || 0;
const fps = typeof window !== "undefined" ? Number(window.__tarinaiFps || 0) : 60;
if (!fps || !Number.isFinite(fps)) return 0;
if (fps < 22) return 3;
if (fps < 34) return 2;
if (fps < 48) return 1;
return 0;
},
performanceQuality() {
return window.TarinaiPerformance?.quality?.(this) || { level: this.performanceLevel(), antStride: 1, effectLimit: CONFIG.effectLimit ?? 96 };
},
2026-06-24 13:48:22 +09:00
runtimeDiagnostics() {
const behaviorCounts = {};
let forcedBehaviors = 0;
let lockedBehaviors = 0;
for (const t of this.tarinai || []) {
if (!t || t.dead) continue;
const behavior = typeof currentTarinaiBehavior === "function" ? currentTarinaiBehavior(t) : t.behavior;
const id = behavior?.id || t.state || "none";
behaviorCounts[id] = (behaviorCounts[id] || 0) + 1;
if (behavior?.forced) forcedBehaviors += 1;
if ((t.intentLockTimer || 0) > 0.01) lockedBehaviors += 1;
}
return {
spatial: {
version: this.spatialVersion || 0,
dirty: Boolean(this.spatialDirty),
dirtyReason: this.spatialDirtyReason || this.lastSpatialDirtyReason || "",
dirtyMarksThisFrame: this.spatialDirtyMarksThisFrame || 0,
dirtyMarksTotal: this.spatialDirtyMarksTotal || 0,
rebuildsThisFrame: this.spatialRebuildsThisFrame || 0,
rebuildsTotal: this.spatialRebuildsTotal || 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,
},
};
},
2026-06-21 14:09:35 +09:00
grassLoadLevel() {
const grass = this.itemCounts?.grass || 0;
const perf = this.performanceLevel();
if (perf >= 3 || grass >= 180) return 3;
if (perf >= 2 || grass >= 120) return 2;
if (perf >= 1 || grass >= 72) return 1;
return 0;
},
grassUpdateFactor() {
return [1.0, 1.35, 1.85, 2.45][this.grassLoadLevel()] || 1.0;
},
sortedDrawList() {
if (!this.drawList) this.drawList = [];
2026-06-21 22:29:00 +09:00
const antCount = liveCount(this.ants);
const tarinaiCount = liveCount(this.tarinai);
2026-06-21 14:09:35 +09:00
if (this.drawListDirty || this.drawList.length !== tarinaiCount + antCount) {
this.drawList.length = 0;
for (const t of this.tarinai || []) if (t && !t.dead) this.drawList.push(t);
for (const a of this.ants || []) if (a && !a.dead) this.drawList.push(a);
this.drawList.sort((a, b) => (a.y || 0) - (b.y || 0));
this.drawListDirty = false;
}
return this.drawList;
},
2026-06-24 13:48:22 +09:00
ensureSpatial(reason = "read") {
if (!this.spatial) return false;
if (!this.spatialDirty && (this.spatialVersion || 0) > 0) return false;
return this.rebuildSpatial(true, reason);
},
2026-06-21 14:09:35 +09:00
nearbyItems(x, y, radius, precise = false) {
2026-06-24 13:48:22 +09:00
this.ensureSpatial?.("nearby-items");
2026-06-21 14:09:35 +09:00
return this.spatial.nearby(this.spatial.itemCells, x, y, radius, this.spatialItemScratch, precise);
},
nearbyTarinai(x, y, radius, precise = false) {
2026-06-24 13:48:22 +09:00
this.ensureSpatial?.("nearby-tarinai");
2026-06-21 14:09:35 +09:00
return this.spatial.nearby(this.spatial.tarinaiCells, x, y, radius, this.spatialTarinaiScratch, precise);
},
nearbyAnts(x, y, radius, precise = false) {
2026-06-24 13:48:22 +09:00
this.ensureSpatial?.("nearby-ants");
2026-06-21 14:09:35 +09:00
return this.spatial.nearby(this.spatial.antCells, x, y, radius, this.spatialAntScratch, precise);
},
nearbyObstacles(x, y, radius, precise = false) {
2026-06-24 13:48:22 +09:00
this.ensureSpatial?.("nearby-obstacles");
2026-06-21 14:09:35 +09:00
return this.spatial.nearby(this.spatial.obstacleCells, x, y, radius, this.spatialObstacleScratch, precise);
},
nearbyFood(x, y, radius, precise = false) {
2026-06-24 13:48:22 +09:00
this.ensureSpatial?.("nearby-food");
2026-06-21 14:09:35 +09:00
return this.spatial.nearby(this.spatial.foodCells, x, y, radius, this.spatialFoodScratch, precise);
},
nearbyHazards(x, y, radius, precise = false) {
2026-06-24 13:48:22 +09:00
this.ensureSpatial?.("nearby-hazards");
2026-06-21 14:09:35 +09:00
return this.spatial.nearby(this.spatial.hazardCells, x, y, radius, this.spatialHazardScratch, precise);
},
2026-06-24 13:48:22 +09:00
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;
2026-06-21 14:09:35 +09:00
this.spatial.rebuild(this.items, this.tarinai, this.ants || []);
this.spatialDirty = false;
2026-06-24 13:48:22 +09:00
this.spatialDirtyReason = "";
2026-06-21 14:09:35 +09:00
this.spatialVersion = (this.spatialVersion || 0) + 1;
2026-06-24 13:48:22 +09:00
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;
this.items.push(item);
this.markItemBucketsDirty?.(reason);
this.markSpatialDirty?.(reason);
if (opts.terrain !== false) 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;
2026-06-21 14:09:35 +09:00
},
2026-06-24 13:48:22 +09:00
itemsOfType(type) {
this.ensureItemBuckets?.(`items-of-type:${type}`);
const bucket = this.itemTypeBuckets?.get?.(type);
return bucket || [];
},
liveItemsOfType(type, predicate = null) {
const bucket = this.itemsOfType(type);
if (typeof predicate !== "function") return bucket;
const out = [];
for (const item of bucket) if (item && !item.dead && predicate(item)) out.push(item);
return out;
},
updateItemCounts(reason = "manual") {
2026-06-21 14:09:35 +09:00
const counts = this.itemCounts;
2026-06-24 13:48:22 +09:00
if (!this.itemTypeBuckets) this.itemTypeBuckets = new Map();
if (!this.itemIdMap) this.itemIdMap = new Map();
2026-06-21 14:09:35 +09:00
for (const key of Object.keys(counts)) counts[key] = 0;
2026-06-24 13:48:22 +09:00
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;
2026-06-21 14:09:35 +09:00
},
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() {
this.enforceItemBudget();
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");
2026-06-24 13:48:22 +09:00
this.markItemBucketsDirty?.("compact-items");
2026-06-21 14:09:35 +09:00
}
return changed;
},
enforceItemBudget() {
const pruneOldest = (type, limit) => {
if (!Number.isFinite(limit) || limit < 0) return;
2026-06-24 13:48:22 +09:00
const live = this.liveItemsOfType?.(type) || (this.items || []).filter(it => it && it.type === type && !it.dead);
const count = live.length;
2026-06-21 14:09:35 +09:00
if (count <= limit) return;
2026-06-24 13:48:22 +09:00
const victims = live
.slice()
2026-06-21 14:09:35 +09:00
.sort((a, b) => (b.age || 0) - (a.age || 0))
.slice(0, count - limit);
for (const it of victims) it.amount = 0;
};
pruneOldest("zunchi", CONFIG.zunchiLimit ?? 56);
pruneOldest("trace", CONFIG.traceLimit ?? 64);
pruneOldest("splat", CONFIG.splatLimit ?? 48);
pruneOldest("grass", this.grassLimit ? this.grassLimit() : (CONFIG.grassLimit ?? 96));
const fenceLimit = this.fenceLimit ? this.fenceLimit() : (CONFIG.fenceLimit ?? 72);
const liveFences = this.liveFenceCount ? this.liveFenceCount() : 0;
if (liveFences > fenceLimit) {
2026-06-24 13:48:22 +09:00
const victims = (this.items || [])
.filter(it => it && this.isFenceType?.(it.type) && !it.dead)
2026-06-21 14:09:35 +09:00
.sort((a, b) => (b.age || 0) - (a.age || 0))
.slice(0, liveFences - fenceLimit);
for (const it of victims) it.amount = 0;
}
const limit = CONFIG.itemLimit ?? Infinity;
if (!Number.isFinite(limit) || this.items.length <= limit) return;
const priority = { trace: 1, splat: 2, zunchi: 3, water: 4, grass: 5, sweet: 6, firecracker: 7, fence_v: 8, fence_h: 8 };
2026-06-24 13:48:22 +09:00
const removable = (this.items || [])
.filter(it => it && !it.dead && (priority[it.type] || 99) < 99)
2026-06-21 14:09:35 +09:00
.sort((a, b) => (priority[a.type] || 99) - (priority[b.type] || 99) || (b.age || 0) - (a.age || 0));
let extra = this.items.length - limit;
for (const it of removable) {
if (extra <= 0) break;
it.amount = 0;
extra -= 1;
}
},
compactEffects() {
const limit = this.performanceQuality?.().effectLimit ?? 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);