This commit is contained in:
33333-33333 2026-07-20 14:36:59 +09:00
commit f6d7412744
94 changed files with 3836 additions and 1226 deletions

View file

@ -201,12 +201,10 @@
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);
if (!this.spatial?.nearbyInto) return out;
this.spatial.nearbyInto(this.spatial.staticItemCells, x, y, radius, out, precise);
this.spatial.nearbyInto(this.spatial.dynamicItemCells, x, y, radius, out, precise);
return out;
},
nearbyGrass(x, y, radius, precise = false) {
@ -253,26 +251,20 @@
},
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);
if (!this.spatial?.nearbySplitInto) return this.spatialFoodScratch;
return this.spatial.nearbySplitInto(this.spatial.staticFoodCells, this.spatial.dynamicFoodCells, 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);
if (!this.spatial?.nearbySplitInto) return this.spatialObstacleScratch;
return this.spatial.nearbySplitInto(this.spatial.staticObstacleCells, this.spatial.dynamicObstacleCells, 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);
if (!this.spatial?.nearbySplitInto) return this.spatialHazardScratch;
return this.spatial.nearbySplitInto(this.spatial.staticHazardCells, this.spatial.dynamicHazardCells, x, y, radius, this.spatialHazardScratch, precise);
},
rebuildSpatial(force = false, reason = "manual") {
// Direct calls keep their old eager semantics. update() uses markSpatialDirty + ensureSpatial
@ -449,9 +441,135 @@
return item;
},
residueCellKey(x, y) {
const size = 160;
return `${Math.floor((Number(x) || 0) / size)}:${Math.floor((Number(y) || 0) / size)}`;
},
addResidue(type, x, y, opts = {}) {
const residueType = String(type || "");
if (residueType !== "trace" && residueType !== "splat") return null;
if (!(this.residueById instanceof Map)) this.residueById = new Map();
if (!(this.residueTypeBuckets instanceof Map)) this.residueTypeBuckets = new Map();
if (!(this.residueCells instanceof Map)) this.residueCells = new Map();
if (!Array.isArray(this.residues)) this.residues = [];
const id = String(opts.id || `res${++this.residueSerial}`);
const residue = {
id, type: residueType,
x: Number(x) || 0, y: Number(y) || 0,
r: Math.max(1, Number(opts.r ?? (residueType === "splat" ? 26 : 16)) || 1),
amount: Math.max(0, Number(opts.amount ?? (residueType === "splat" ? 260 : 220)) || 0),
seed: Number.isFinite(Number(opts.seed)) ? Number(opts.seed) : Math.random() * 1000,
dead: false,
_memorialReservedBy: String(opts._memorialReservedBy || ""),
};
this.residues.push(residue);
this.residueById.set(id, residue);
let typeBucket = this.residueTypeBuckets.get(residueType);
if (!typeBucket) { typeBucket = new Set(); this.residueTypeBuckets.set(residueType, typeBucket); }
typeBucket.add(residue);
const cellKey = this.residueCellKey(residue.x, residue.y);
residue._cellKey = cellKey;
let cell = this.residueCells.get(cellKey);
if (!cell) { cell = new Set(); this.residueCells.set(cellKey, cell); }
cell.add(residue);
this.markTerrainDirtyAt?.(residue.x, residue.y, Math.max(residue.r, 36), opts.reason || "residue-add");
return residue;
},
rebuildResidueIndex() {
if (!(this.residueById instanceof Map)) this.residueById = new Map();
if (!(this.residueTypeBuckets instanceof Map)) this.residueTypeBuckets = new Map();
if (!(this.residueCells instanceof Map)) this.residueCells = new Map();
this.residueById.clear(); this.residueTypeBuckets.clear(); this.residueCells.clear();
for (const residue of this.residues || []) {
if (!residue || residue.dead || residue.amount <= 0) continue;
this.residueById.set(String(residue.id), residue);
let typeBucket = this.residueTypeBuckets.get(residue.type);
if (!typeBucket) { typeBucket = new Set(); this.residueTypeBuckets.set(residue.type, typeBucket); }
typeBucket.add(residue);
const key = this.residueCellKey(residue.x, residue.y); residue._cellKey = key;
let cell = this.residueCells.get(key);
if (!cell) { cell = new Set(); this.residueCells.set(key, cell); }
cell.add(residue);
}
return this.residueById.size;
},
residueByRuntimeId(id) {
if (id == null || id === "") return null;
const residue = this.residueById?.get?.(String(id)) || null;
return residue && !residue.dead && residue.amount > 0 ? residue : null;
},
residuesOfType(type) {
return this.residueTypeBuckets?.get?.(String(type || "")) || [];
},
nearbyResidues(x, y, radius, type = "") {
const out = this._nearbyResidueScratch || (this._nearbyResidueScratch = []);
out.length = 0;
if (!(this.residueCells instanceof Map) || !this.residueCells.size) return out;
const size = 160;
const r = Math.max(0, Number(radius) || 0);
const minX = Math.floor(((Number(x) || 0) - r) / size), maxX = Math.floor(((Number(x) || 0) + r) / size);
const minY = Math.floor(((Number(y) || 0) - r) / size), maxY = Math.floor(((Number(y) || 0) + r) / size);
const wanted = String(type || "");
const r2 = r * r;
for (let cy = minY; cy <= maxY; cy++) for (let cx = minX; cx <= maxX; cx++) {
const cell = this.residueCells.get(`${cx}:${cy}`);
if (!cell) continue;
for (const residue of cell) {
if (!residue || residue.dead || residue.amount <= 0 || (wanted && residue.type !== wanted)) continue;
const dx = residue.x - x, dy = residue.y - y;
if (dx * dx + dy * dy <= r2) out.push(residue);
}
}
return out;
},
removeResidue(residue, reason = "residue-remove") {
if (!residue || residue.dead) return false;
residue.dead = true;
residue.amount = 0;
this.residueById?.delete?.(residue.id);
const typeBucket = this.residueTypeBuckets?.get?.(residue.type);
typeBucket?.delete?.(residue);
if (typeBucket && typeBucket.size === 0) this.residueTypeBuckets.delete(residue.type);
const cell = this.residueCells?.get?.(residue._cellKey || this.residueCellKey(residue.x, residue.y));
cell?.delete?.(residue);
if (cell && cell.size === 0) this.residueCells.delete(residue._cellKey || this.residueCellKey(residue.x, residue.y));
this.markTerrainDirtyAt?.(residue.x, residue.y, Math.max(residue.r || 24, 36), reason);
return true;
},
updateResidues(dt) {
if (!Array.isArray(this.residues) || !this.residues.length) return 0;
this.residueDecayAccumulator = Math.max(0, Number(this.residueDecayAccumulator || 0)) + Math.max(0, Number(dt) || 0);
if (this.residueDecayAccumulator < 0.75) return 0;
const step = this.residueDecayAccumulator;
this.residueDecayAccumulator = 0;
let removed = 0;
for (const residue of this.residues) {
if (!residue || residue.dead) continue;
const before = Number(residue.amount || 0) || 0;
residue.amount = Math.max(0, before - step * (residue.type === "splat" ? 1.05 : 1.35));
if (residue.type === "splat" && Math.floor(before / 18) !== Math.floor(residue.amount / 18)) {
this.markTerrainDirtyAt?.(residue.x, residue.y, Math.max(residue.r || 24, 36), "splat-fade");
}
if (residue.amount <= 0.001 && this.removeResidue(residue, "residue-decay")) removed += 1;
}
if (removed > 0 || this.residues.length > 512) this.residues = this.residues.filter(r => r && !r.dead && r.amount > 0);
return removed;
},
noteItemInactive(item, reason = "item-inactive", notifyAchievements = true) {
if (!item || item._worldCountedActive !== true) return false;
item._worldCountedActive = false;
if (item._achievementPlayerPlaced && item._achievementPlayerPlacedCounted !== false) {
this.achievementPlayerPlacedActiveCount = Math.max(0, (Number(this.achievementPlayerPlacedActiveCount || 0) || 0) - 1);
item._achievementPlayerPlacedCounted = false;
}
if (item.type) this.itemCounts[item.type] = Math.max(0, (Number(this.itemCounts[item.type] || 0) || 0) - 1);
if (Number.isFinite(this._activeObjectCountCache)) this._activeObjectCountCache = Math.max(0, this._activeObjectCountCache - 1);
if (notifyAchievements && !this._suspendAchievementEvents) global.TarinaiAchievements?.evaluateEvent?.(this, "items", { item, delta: -1, reason });
@ -489,10 +607,17 @@
this.itemOwnerBuckets.clear();
this.carriedPlushies.length = 0;
let activeObjectCount = 0;
let playerPlacedActiveCount = 0;
for (const it of this.items) {
if (!it || it.dead) continue;
activeObjectCount += 1;
it._worldCountedActive = true;
if (it._achievementPlayerPlaced) {
playerPlacedActiveCount += 1;
it._achievementPlayerPlacedCounted = true;
} else {
it._achievementPlayerPlacedCounted = false;
}
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);
@ -512,6 +637,7 @@
bucket.push(it);
}
this._activeObjectCountCache = activeObjectCount;
this.achievementPlayerPlacedActiveCount = playerPlacedActiveCount;
this.itemBucketsDirty = false;
this.itemBucketRebuildsTotal = (this.itemBucketRebuildsTotal || 0) + 1;
},