155 lines
6 KiB
JavaScript
155 lines
6 KiB
JavaScript
"use strict";
|
|
|
|
// Layer: simulation/system-effects
|
|
// Owns frame updates for transient visual/effect entities.
|
|
(function (global) {
|
|
function effectPriority(effect) {
|
|
const explicit = Number(effect?._effectImportance);
|
|
if (Number.isFinite(explicit)) return Math.max(0, Math.min(3, explicit));
|
|
const type = String(effect?.type || "");
|
|
if (type === "explosion" || type === "shoot_impact" || type === "bubble" || type === "heart" || type === "electric_shock" || type === "ascend_soul") 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;
|
|
else if (effect) worldRef.releaseEffect?.(effect);
|
|
}
|
|
effects.length = write;
|
|
const compacted = before - write;
|
|
const cap = Math.max(1, Number(global.TarinaiPerf?.performanceProfile?.().effectCap || 220));
|
|
let needDrop = Math.max(0, effects.length - cap);
|
|
if (!needDrop) return { compacted, dropped: 0 };
|
|
|
|
const dropMask = new Uint8Array(effects.length);
|
|
for (let priority = 1; priority <= 3 && needDrop > 0; priority += 1) {
|
|
for (let i = 0; i < effects.length && needDrop > 0; i += 1) {
|
|
if (dropMask[i] || effectPriority(effects[i]) !== priority) continue;
|
|
if (effects[i]?._forceVisual === true || effects[i]?.type === "bubble" || effects[i]?.type === "ascend_soul") continue;
|
|
dropMask[i] = 1;
|
|
needDrop -= 1;
|
|
}
|
|
}
|
|
write = 0;
|
|
let dropped = 0;
|
|
for (let i = 0; i < effects.length; i += 1) {
|
|
const effect = effects[i];
|
|
if (dropMask[i]) {
|
|
dropped += 1;
|
|
worldRef.releaseEffect?.(effect);
|
|
} else effects[write++] = effect;
|
|
}
|
|
effects.length = write;
|
|
worldRef._effectPressureDroppedTotal = (worldRef._effectPressureDroppedTotal || 0) + dropped;
|
|
return { compacted, dropped };
|
|
}
|
|
|
|
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 clearDisabledEffects(worldRef, effects, dt) {
|
|
if (!effects?.length) return 0;
|
|
let write = 0;
|
|
let updated = 0;
|
|
for (const effect of effects) {
|
|
if (!effect) continue;
|
|
const forced = effect._forceVisual === true || effect.type === "bubble" || effect.type === "ascend_soul";
|
|
if (!forced) {
|
|
worldRef.releaseEffect?.(effect);
|
|
continue;
|
|
}
|
|
if (!effect.dead) effect.update(Math.min(0.24, Math.max(0, dt)));
|
|
if (!effect.dead) {
|
|
effects[write++] = effect;
|
|
updated += 1;
|
|
} else worldRef.releaseEffect?.(effect);
|
|
}
|
|
effects.length = write;
|
|
worldRef._effectUpdateCursor = 0;
|
|
worldRef._renderEffectCursor = 0;
|
|
return updated;
|
|
}
|
|
|
|
function updateEffects(worldRef, dt) {
|
|
const effects = worldRef.effects || [];
|
|
if (global.TarinaiPerf?.visualLevel?.("effects", "high") === "off") {
|
|
clearDisabledEffects(worldRef, effects, dt);
|
|
worldRef._effectUpdateStats = null;
|
|
return;
|
|
}
|
|
|
|
const beforePressure = effects.length;
|
|
const pressure = compactEffectPressure(worldRef, effects);
|
|
const total = effects.length;
|
|
const frameId = (worldRef._effectUpdateFrameId || 0) + 1;
|
|
worldRef._effectUpdateFrameId = frameId;
|
|
|
|
let lightweight = 0;
|
|
let active = 0;
|
|
for (const effect of effects) {
|
|
if (!effect || effect.dead) continue;
|
|
if (effect.type === "ring") {
|
|
effect.life -= dt;
|
|
effect._effectUpdatedFrame = frameId;
|
|
lightweight += 1;
|
|
} else active += 1;
|
|
}
|
|
|
|
const budget = effectUpdateBudgetFor(active);
|
|
let updated = 0;
|
|
let runScale = 1;
|
|
if (budget >= active) {
|
|
for (const effect of effects) {
|
|
if (!effect || effect.dead || effect.type === "ring") continue;
|
|
if (updateEffect(effect, dt, frameId, 1)) updated += 1;
|
|
}
|
|
} else if (active > 0) {
|
|
// Walk the source array directly instead of allocating a temporary
|
|
// budgeted-effects array every animation frame.
|
|
runScale = Math.min(3.0, Math.max(1, active / Math.max(1, budget)));
|
|
let cursor = Math.max(0, Math.floor(worldRef._effectUpdateCursor || 0)) % Math.max(1, effects.length);
|
|
let scanned = 0;
|
|
while (updated < budget && scanned < effects.length) {
|
|
const effect = effects[cursor];
|
|
cursor = (cursor + 1) % Math.max(1, effects.length);
|
|
scanned += 1;
|
|
if (!effect || effect.dead || effect.type === "ring") continue;
|
|
updateEffect(effect, dt, frameId, runScale);
|
|
updated += 1;
|
|
}
|
|
worldRef._effectUpdateCursor = cursor;
|
|
}
|
|
|
|
if (global.TarinaiPerf?.diagnosticsEnabled?.() === true) {
|
|
const spawn = worldRef._effectSpawnStats || {};
|
|
worldRef._effectUpdateStats = {
|
|
total, beforePressure, updated: updated + lightweight, lightweight,
|
|
skipped: Math.max(0, active - updated), budget,
|
|
runScale: Number(runScale.toFixed(2)), compacted: pressure.compacted, dropped: pressure.dropped,
|
|
droppedTotal: worldRef._effectPressureDroppedTotal || 0, batched: budget < active,
|
|
spawnRequested: spawn.requested || 0, offscreenCulled: spawn.offscreenCulled || 0,
|
|
poolEligible: spawn.poolEligible || 0, pooled: spawn.pooled || 0,
|
|
};
|
|
} else worldRef._effectUpdateStats = null;
|
|
}
|
|
|
|
global.TarinaiEffectsSimulationSystem = Object.freeze({
|
|
phases: Object.freeze({ updateEffects }),
|
|
});
|
|
})(typeof window !== "undefined" ? window : globalThis);
|