The cockpit's spells fire: fx timers survive the state message
Two lightning blasts at an adjacent wizard drew nothing in first person. The fx pipeline staged the bolt correctly — then died unfired: the server sends `events` and `state` back to back, the state message re-derives `me`, and the fx effect tracked it, so Svelte ran the cleanup — which cleared every pending timer and stripped in-flight sprites — before the bolt's zero-delay start could fire. Every live cast effect in the pane died this way since the pane was born. The effect now tracks only the batch; everything else reads under untrack, and timers are cleared only when the component goes down. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
773f0a7d21
commit
825a73b154
@@ -224,25 +224,35 @@
|
|||||||
let actorPos = $state<Record<string, { x: number; y: number }>>({});
|
let actorPos = $state<Record<string, { x: number; y: number }>>({});
|
||||||
|
|
||||||
// --- The fx: each batch plays once, on its own beat. -----------------
|
// --- The fx: each batch plays once, on its own beat. -----------------
|
||||||
|
// The effect must track ONLY the batch: the server sends `events` then
|
||||||
|
// `state` back to back, so anything else in the dependency set (me, the
|
||||||
|
// view) re-runs this before a zero-delay timer can fire — and an eager
|
||||||
|
// cleanup would cancel every spell before it left the wand. Timers die
|
||||||
|
// only with the component.
|
||||||
let playedBatch = 0;
|
let playedBatch = 0;
|
||||||
|
const fxTimers = new Set<ReturnType<typeof setTimeout>>();
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const b = batch;
|
const b = batch;
|
||||||
if (!b || b.n === playedBatch || !me) return;
|
if (!b) return;
|
||||||
|
untrack(() => {
|
||||||
|
if (b.n === playedBatch || !me) return;
|
||||||
playedBatch = b.n;
|
playedBatch = b.n;
|
||||||
const timers: ReturnType<typeof setTimeout>[] = [];
|
|
||||||
const started: number[] = [];
|
|
||||||
for (const { fx, delay } of fpFxForEvents(b.events, b.view, povId)) {
|
for (const { fx, delay } of fpFxForEvents(b.events, b.view, povId)) {
|
||||||
timers.push(setTimeout(() => {
|
const starter = setTimeout(() => {
|
||||||
started.push(fx.id);
|
fxTimers.delete(starter);
|
||||||
fpFx = [...fpFx, { ...fx, t0: performance.now() }];
|
fpFx = [...fpFx, { ...fx, t0: performance.now() }];
|
||||||
timers.push(setTimeout(() => (fpFx = fpFx.filter((f) => f.id !== fx.id)), fx.dur + 80));
|
const ender = setTimeout(() => {
|
||||||
}, delay));
|
fxTimers.delete(ender);
|
||||||
|
fpFx = fpFx.filter((f) => f.id !== fx.id);
|
||||||
|
}, fx.dur + 80);
|
||||||
|
fxTimers.add(ender);
|
||||||
|
}, delay);
|
||||||
|
fxTimers.add(starter);
|
||||||
}
|
}
|
||||||
return () => {
|
|
||||||
timers.forEach(clearTimeout);
|
|
||||||
started.forEach((id) => (fpFx = fpFx.filter((f) => f.id !== id)));
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
$effect(() => () => fxTimers.forEach(clearTimeout));
|
||||||
|
|
||||||
// --- Other bodies glide between views. -------------------------------
|
// --- Other bodies glide between views. -------------------------------
|
||||||
let prevView: GameView | null = null;
|
let prevView: GameView | null = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user