diff --git a/src/core/model/animation.ts b/src/core/model/animation.ts index 1deffbb8..3a4d5813 100644 --- a/src/core/model/animation.ts +++ b/src/core/model/animation.ts @@ -548,11 +548,22 @@ export interface AnimationPlayer { /** Seconds a `once-reset` pass takes to blend back to its starting values when its frames run out. */ blendOut?: number; fade?: AnimationFade; - /** - * Who asked for it – the behaviors that played it, which hear its end wherever the object stands. - * Opaque here: the runtime hands in its behavior entities. - */ - startedBy?: object[]; +} + +/** + * Who asked for a player – the behaviors that played it, which hear its end wherever the object + * stands. Kept beside the player, not on it: a player is plain data, read as JSON by the check + * scripts, and what asks is an entity of the run with its parents and children. + */ +const starters = new WeakMap(); + +export function startersOf(player: AnimationPlayer): readonly object[] { + return starters.get(player) ?? []; +} + +function noteStarter(player: AnimationPlayer, startedBy: object | undefined): void { + if (!startedBy || startersOf(player).includes(startedBy)) return; + starters.set(player, [...startersOf(player), startedBy]); } /** @@ -601,7 +612,7 @@ let startCount = 0; * when a `once-reset` pass already ran out, starts over. `blendOutSeconds` is the fade at the end of * a `once-reset` pass. `restart` starts a known one over from its first frame in any case – blended in * from where it stands when `blendSeconds` is set – and keeps its starting values, so a relative row - * does not add up. `startedBy` is noted on the player next to whoever asked before, so each of them + * does not add up. `startedBy` is noted for the player next to whoever asked before, so each of them * hears it finish. */ export function playAnimation( @@ -617,8 +628,7 @@ export function playAnimation( const blend = blendSeconds > 0 ? { seconds: blendSeconds, elapsed: 0 } : undefined; const blendOut = blendOutSeconds > 0 ? blendOutSeconds : undefined; if (known) { - if (startedBy && !known.startedBy?.includes(startedBy)) - known.startedBy = [...(known.startedBy ?? []), startedBy]; + noteStarter(known, startedBy); const leads = players.every((p) => p.order <= known.order); const fade = known.fade; known.playing = true; @@ -635,15 +645,16 @@ export function playAnimation( } else if (fade || !leads) known.blend = blend; return; } - players.push({ + const player: AnimationPlayer = { animationId, time: 0, playing: true, order: ++startCount, ...(blend && { blend }), ...(blendOut && { blendOut }), - ...(startedBy && { startedBy: [startedBy] }), - }); + }; + noteStarter(player, startedBy); + players.push(player); } /** diff --git a/src/runtime/systems/AnimationSystem.ts b/src/runtime/systems/AnimationSystem.ts index fce72803..8afe12c9 100644 --- a/src/runtime/systems/AnimationSystem.ts +++ b/src/runtime/systems/AnimationSystem.ts @@ -9,6 +9,7 @@ import { blendValue, isPositionRow, playbackOf, + startersOf, trackWriteAt, } from '../../core/model/animation'; import type { AnimationAsset, AnimationTrack } from '../../core/model/Asset'; @@ -106,7 +107,7 @@ export class AnimationSystem implements System { continue; } if (!head.finished) continue; - this.finished.push({ entity, animationId: asset.id, startedBy: player.startedBy ?? [] }); + this.finished.push({ entity, animationId: asset.id, startedBy: startersOf(player) }); if (playbackOf(asset) !== 'once-reset') { drop(entity, player); continue; diff --git a/tests/core/animation.test.ts b/tests/core/animation.test.ts index 41fcedf3..94ad2889 100644 --- a/tests/core/animation.test.ts +++ b/tests/core/animation.test.ts @@ -45,6 +45,7 @@ import { playerStatus, type PlayerStatus, playheadAt, + startersOf, stopAnimation, targetDefs, trackDef, @@ -1359,6 +1360,22 @@ describe('the animation nodes', () => { expect(above('plays too')).toEqual({ done: [1, 1], x: 20 }); }); + it('keeps who asked beside the player, which stays plain data', () => { + // What asks is an entity of the run: it reaches the holder again, as a behavior reaches its owner. + const holder: { animations?: AnimationPlayer[]; children: object[] } = { children: [] }; + const asker = { parent: holder }; + holder.children.push(asker); + playAnimation(holder, 'walk', 0, 0, false, asker); + playAnimation(holder, 'walk', 0, 0, false, asker); + const player = holder.animations![0]!; + expect(startersOf(player)).toEqual([asker]); + expect(Object.values(player).every((value) => typeof value !== 'object')).toBe(true); + expect(JSON.parse(JSON.stringify(holder.animations))).toEqual([{ ...player }]); + expect(structuredClone(holder.animations)).toEqual([{ ...player }]); + playAnimation(holder, 'idle'); + expect(startersOf(holder.animations![1]!)).toEqual([]); + }); + it('passes Blend on to the player it starts (#106)', () => { const f = makeSpriteScene(); const box = place(f, f.spriteId, { x: 0, y: 0 });