tarinai/js/simulation_effects_system.js
2026-07-10 16:40:06 +09:00

100 lines
3.8 KiB
JavaScript

"use strict";
// Layer: simulation/system-effects
// Owns frame updates for transient visual/effect entities.
(function (global) {
function effectPriority(effect) {
const type = String(effect?.type || "");
if (type === "explosion" || type === "shoot_impact" || type === "bubble" || type === "heart" || type === "electric_shock") return 3;
if (type === "ring" || type === "fall") return 2;
return 1;
}
function compactEffectPressure(worldRef, effects) {
if (!Array.isArray(effects) || !effects.length) return { compacted: 0, dropped: 0 };
const before = effects.length;
let write = 0;
for (let i = 0; i < effects.length; i += 1) {
const effect = effects[i];
if (effect && !effect.dead) effects[write++] = effect;
}
effects.length = write;
const compacted = before - write;
const cap = Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().effectCap || 220));
if (effects.length <= cap) return { compacted, dropped: 0 };
const drop = effects
.map((effect, index) => ({
effect,
index,
score: effectPriority(effect) * 10 + Math.max(0, Math.min(1, Number(effect.life || 0) / Math.max(0.001, Number(effect.maxLife || effect.life || 1)))),
}))
.sort((a, b) => a.score - b.score)
.slice(0, effects.length - cap);
const dropSet = new Set(drop.map(entry => entry.index));
write = 0;
for (let i = 0; i < effects.length; i += 1) {
if (!dropSet.has(i)) effects[write++] = effects[i];
}
effects.length = write;
worldRef._effectPressureDroppedTotal = (worldRef._effectPressureDroppedTotal || 0) + dropSet.size;
return { compacted, dropped: dropSet.size };
}
function effectUpdateBudgetFor(total) {
const budget = Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().effectUpdateBudget || 84));
return Math.min(total, budget);
}
function updateEffect(effect, dt, frameId = 0, runScale = 1) {
if (!effect || effect.dead) return false;
const runDt = Math.min(0.24, Math.max(0, dt * Math.max(1, Number(runScale) || 1)));
effect._effectUpdatedFrame = frameId;
effect.update(runDt);
return true;
}
function updateEffects(worldRef, dt) {
const effects = worldRef.effects || [];
const beforePressure = effects.length;
const pressure = compactEffectPressure(worldRef, effects);
const total = effects.length;
const frameId = (worldRef._effectUpdateFrameId || 0) + 1;
worldRef._effectUpdateFrameId = frameId;
const budget = effectUpdateBudgetFor(total);
if (budget >= total) {
for (const ef of effects) updateEffect(ef, dt, frameId, 1);
worldRef._effectUpdateStats = { total, beforePressure, updated: total, skipped: 0, budget: total, compacted: pressure.compacted, dropped: pressure.dropped, batched: false };
return;
}
const runScale = Math.min(3.0, Math.max(1, total / Math.max(1, budget)));
let cursor = Math.max(0, Math.floor(worldRef._effectUpdateCursor || 0)) % total;
let updated = 0;
let scanned = 0;
while (updated < budget && scanned < total) {
const ef = effects[cursor];
cursor = (cursor + 1) % total;
scanned += 1;
if (!ef || ef.dead) continue;
updateEffect(ef, dt, frameId, runScale);
updated += 1;
}
worldRef._effectUpdateCursor = cursor;
worldRef._effectUpdateStats = {
total,
beforePressure,
updated,
skipped: Math.max(0, total - updated),
budget,
runScale: Number(runScale.toFixed(2)),
compacted: pressure.compacted,
dropped: pressure.dropped,
droppedTotal: worldRef._effectPressureDroppedTotal || 0,
batched: true,
};
}
global.TarinaiEffectsSimulationSystem = Object.freeze({
phases: Object.freeze({ updateEffects }),
});
})(typeof window !== "undefined" ? window : globalThis);