diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c0e4fa1..192e8783 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed +- Fixed issue where the first action in a sequence would not execute after calling `clearActions()` mid-execution. All action types now properly reset their initialization state when stopped, resolving issue #3468 - Performance: Font/Text now use smaller texture sizes, improving performance on Safari especially when rendering text ### Updates diff --git a/src/engine/actions/action/action-sequence.ts b/src/engine/actions/action/action-sequence.ts index ac14c8e0..14c28ddc 100644 --- a/src/engine/actions/action/action-sequence.ts +++ b/src/engine/actions/action/action-sequence.ts @@ -31,6 +31,11 @@ export class ActionSequence implements Action { public stop(): void { this._stopped = true; + // Stop current action but don't clear queue, as reset() may reuse it + const currentAction = this._actionQueue.getCurrentAction(); + if (currentAction) { + currentAction.stop(); + } } public reset(): void { diff --git a/src/engine/actions/action/blink.ts b/src/engine/actions/action/blink.ts index 266312e6..fba3aa2d 100644 --- a/src/engine/actions/action/blink.ts +++ b/src/engine/actions/action/blink.ts @@ -56,6 +56,7 @@ export class Blink implements Action { this._graphics.isVisible = true; } this._stopped = true; + this._started = false; } public reset() { diff --git a/src/engine/actions/action/curve-by.ts b/src/engine/actions/action/curve-by.ts index 675bc3ff..f17a8f4a 100644 --- a/src/engine/actions/action/curve-by.ts +++ b/src/engine/actions/action/curve-by.ts @@ -92,5 +92,6 @@ export class CurveBy implements Action { } stop(): void { this._stopped = true; + this._started = false; } } diff --git a/src/engine/actions/action/curve-to.ts b/src/engine/actions/action/curve-to.ts index 26f91618..38226c8b 100644 --- a/src/engine/actions/action/curve-to.ts +++ b/src/engine/actions/action/curve-to.ts @@ -90,6 +90,7 @@ export class CurveTo implements Action { } stop(): void { this._stopped = true; + this._started = false; this._currentMs = 0; } } diff --git a/src/engine/actions/action/delay.ts b/src/engine/actions/action/delay.ts index 03df9934..f823a141 100644 --- a/src/engine/actions/action/delay.ts +++ b/src/engine/actions/action/delay.ts @@ -25,6 +25,7 @@ export class Delay implements Action { public stop(): void { this._stopped = true; + this._started = false; } reset(): void { diff --git a/src/engine/actions/action/ease-by.ts b/src/engine/actions/action/ease-by.ts index 32971e12..294dddb0 100644 --- a/src/engine/actions/action/ease-by.ts +++ b/src/engine/actions/action/ease-by.ts @@ -83,5 +83,6 @@ export class EaseBy implements Action { public stop(): void { this._motion.vel = vec(0, 0); this._stopped = true; + this._initialized = false; } } diff --git a/src/engine/actions/action/ease-to.ts b/src/engine/actions/action/ease-to.ts index ecf95d74..e7581f2c 100644 --- a/src/engine/actions/action/ease-to.ts +++ b/src/engine/actions/action/ease-to.ts @@ -81,5 +81,6 @@ export class EaseTo implements Action { public stop(): void { this._motion.vel = vec(0, 0); this._stopped = true; + this._initialized = false; } } diff --git a/src/engine/actions/action/fade.ts b/src/engine/actions/action/fade.ts index 7d8b8204..c6b4aea8 100644 --- a/src/engine/actions/action/fade.ts +++ b/src/engine/actions/action/fade.ts @@ -57,6 +57,7 @@ export class Fade implements Action { public stop(): void { this._stopped = true; + this._started = false; } public reset(): void { diff --git a/src/engine/actions/action/flash.ts b/src/engine/actions/action/flash.ts index 6033a81a..bb6b259d 100644 --- a/src/engine/actions/action/flash.ts +++ b/src/engine/actions/action/flash.ts @@ -77,6 +77,7 @@ export class Flash implements Action { this._graphics.isVisible = true; } this._stopped = true; + this._started = false; } public reset() { diff --git a/src/engine/actions/action/follow.ts b/src/engine/actions/action/follow.ts index 470e8799..f305018c 100644 --- a/src/engine/actions/action/follow.ts +++ b/src/engine/actions/action/follow.ts @@ -71,6 +71,7 @@ export class Follow implements Action { public stop(): void { this._motion.vel = vec(0, 0); this._stopped = true; + this._started = false; } public isComplete(): boolean { diff --git a/src/engine/actions/action/meet.ts b/src/engine/actions/action/meet.ts index 1da678a4..45fc99b8 100644 --- a/src/engine/actions/action/meet.ts +++ b/src/engine/actions/action/meet.ts @@ -81,6 +81,7 @@ export class Meet implements Action { public stop(): void { this._motion.vel = vec(0, 0); this._stopped = true; + this._started = false; } public reset(): void { diff --git a/src/engine/actions/action/move-by.ts b/src/engine/actions/action/move-by.ts index 73ac0b55..8eb67815 100644 --- a/src/engine/actions/action/move-by.ts +++ b/src/engine/actions/action/move-by.ts @@ -94,6 +94,7 @@ export class MoveByWithOptions implements Action { public stop(): void { this._motion.vel = vec(0, 0); this._stopped = true; + this._started = false; this._currentMs = 0; } @@ -158,6 +159,7 @@ export class MoveBy implements Action { public stop(): void { this._motion.vel = vec(0, 0); this._stopped = true; + this._started = false; } public reset(): void { diff --git a/src/engine/actions/action/move-to.ts b/src/engine/actions/action/move-to.ts index b56ded98..32e72614 100644 --- a/src/engine/actions/action/move-to.ts +++ b/src/engine/actions/action/move-to.ts @@ -91,6 +91,7 @@ export class MoveToWithOptions implements Action { public stop(): void { this._motion.vel = vec(0, 0); this._stopped = true; + this._started = false; this._currentMs = 0; } @@ -150,6 +151,7 @@ export class MoveTo implements Action { public stop(): void { this._motion.vel = vec(0, 0); this._stopped = true; + this._started = false; } public reset(): void { diff --git a/src/engine/actions/action/repeat.ts b/src/engine/actions/action/repeat.ts index 52872d59..dc9596aa 100644 --- a/src/engine/actions/action/repeat.ts +++ b/src/engine/actions/action/repeat.ts @@ -39,9 +39,14 @@ export class Repeat implements Action { public stop(): void { this._stopped = true; + this._actionQueue.clearActions(); } public reset(): void { this._repeat = this._originalRepeat; + this._stopped = false; + this._actionQueue.clearActions(); + this._repeatBuilder(this._repeatContext); + this._repeat--; // current execution is the first repeat } } diff --git a/src/engine/actions/action/rotate-by.ts b/src/engine/actions/action/rotate-by.ts index 1fa1e496..489cfcba 100644 --- a/src/engine/actions/action/rotate-by.ts +++ b/src/engine/actions/action/rotate-by.ts @@ -80,6 +80,7 @@ export class RotateByWithOptions implements Action { public stop(): void { this._motion.angularVelocity = 0; this._stopped = true; + this._started = false; this._currentMs = 0; } @@ -191,6 +192,7 @@ export class RotateBy implements Action { public stop(): void { this._motion.angularVelocity = 0; this._stopped = true; + this._started = false; } public reset(): void { diff --git a/src/engine/actions/action/rotate-to.ts b/src/engine/actions/action/rotate-to.ts index bab6468e..6298dd0f 100644 --- a/src/engine/actions/action/rotate-to.ts +++ b/src/engine/actions/action/rotate-to.ts @@ -80,6 +80,7 @@ export class RotateToWithOptions implements Action { public stop(): void { this._motion.angularVelocity = 0; this._stopped = true; + this._started = false; this._currentMs = 0; } @@ -187,6 +188,7 @@ export class RotateTo implements Action { public stop(): void { this._motion.angularVelocity = 0; this._stopped = true; + this._started = false; } public reset(): void { diff --git a/src/engine/actions/action/scale-by.ts b/src/engine/actions/action/scale-by.ts index 7c28f77d..b1f86958 100644 --- a/src/engine/actions/action/scale-by.ts +++ b/src/engine/actions/action/scale-by.ts @@ -74,6 +74,7 @@ export class ScaleByWithOptions implements Action { public stop(): void { this._motion.scaleFactor = Vector.Zero; this._stopped = true; + this._started = false; this._currentMs = 0; } @@ -141,6 +142,7 @@ export class ScaleBy implements Action { this._motion.scaleFactor.x = 0; this._motion.scaleFactor.y = 0; this._stopped = true; + this._started = false; } public reset(): void { diff --git a/src/engine/actions/action/scale-to.ts b/src/engine/actions/action/scale-to.ts index 133cff6c..04ff80bc 100644 --- a/src/engine/actions/action/scale-to.ts +++ b/src/engine/actions/action/scale-to.ts @@ -73,6 +73,7 @@ export class ScaleToWithOptions implements Action { public stop(): void { this._motion.scaleFactor = Vector.Zero; this._stopped = true; + this._started = false; this._currentMs = 0; } @@ -150,6 +151,7 @@ export class ScaleTo implements Action { this._motion.scaleFactor.x = 0; this._motion.scaleFactor.y = 0; this._stopped = true; + this._started = false; } public reset(): void { diff --git a/src/spec/vitest/action-spec.ts b/src/spec/vitest/action-spec.ts index b3762fe8..d7815418 100644 --- a/src/spec/vitest/action-spec.ts +++ b/src/spec/vitest/action-spec.ts @@ -591,6 +591,118 @@ describe('Action', () => { }); }); + describe('clearActions', () => { + it('should allow first action to execute after clearActions (issue #3468)', () => { + // Replicate exact scenario from bug report + actor.pos = ex.vec(0, 0); + + // Start a sequence with blink, scale, scale (like in the bug report) + actor.actions + .blink(100, 100, 1) + .scaleTo({ scale: ex.vec(1.4, 1.4), duration: 250 }) + .scaleTo({ scale: ex.vec(1, 1), duration: 250 }); + + // Let actions run partially + scene.update(engine, 50); + + // Clear actions mid-execution (simulating user calling clearActions) + actor.actions.clearActions(); + + // Now restart with a new sequence - FIRST action (blink) should execute + actor.actions + .blink(100, 100, 1) + .scaleTo({ scale: ex.vec(1.4, 1.4), duration: 250 }) + .scaleTo({ scale: ex.vec(1, 1), duration: 250 }); + + // The blink action (first in sequence) should work + expect(actor.graphics.visible).toBe(true); + scene.update(engine, 100); // Should trigger blink + expect(actor.graphics.visible).toBe(false); + + // Subsequent actions should also work + scene.update(engine, 100); + scene.update(engine, 250); + expect(actor.scale).toBeVector(ex.vec(1.4, 1.4)); + }); + + it('should properly reset easeBy after clearActions', () => { + actor.pos = ex.vec(0, 0); + + // Start easeBy and interrupt it + actor.actions.easeBy(100, 0, 1000, ex.EasingFunctions.Linear); + scene.update(engine, 500); + expect(actor.pos.x).toBeCloseTo(50, 1); + + actor.actions.clearActions(); + + // Start new easeBy - should initialize correctly + actor.pos = ex.vec(0, 0); + actor.actions.easeBy(100, 0, 1000, ex.EasingFunctions.Linear); + + scene.update(engine, 500); + expect(actor.pos.x).toBeCloseTo(50, 1); + + scene.update(engine, 500); + expect(actor.pos.x).toBeCloseTo(100, 1); + }); + + it('should execute first action after clearActions with recursive actions (original bug #3468)', () => { + // This test replicates the EXACT scenario from the bug report: + // 1. Actor has recursive actions running (rotateBy → moveTo → callMethod that recursively calls again) + // 2. clearActions() is called mid-execution + // 3. New sequence is started: blink → scaleTo → scaleTo → callMethod + // 4. The FIRST action (blink) should execute (was being skipped in the bug) + + actor.pos = ex.vec(0, 0); + actor.rotation = 0; + actor.scale = ex.vec(1, 1); + let recursiveCalls = 0; + + // Setup recursive actions pattern from bug report + const setupRecursiveActions = () => { + if (recursiveCalls < 2) { + recursiveCalls++; + actor.actions + .rotateBy(Math.PI / 4, Math.PI / 2) + .moveTo(ex.vec(50, 50), 100) + .callMethod(() => { + setupRecursiveActions(); // Recursive call (like in bug report) + }); + } + }; + + // Start recursive actions (simulating normal game behavior) + setupRecursiveActions(); + + // Let actions run partially (mid-execution of recursive pattern) + scene.update(engine, 100); + expect(recursiveCalls).toBe(1); // First iteration started + + // NOW: Mid-execution, call clearActions (like killWithEffect() in bug report) + actor.actions.clearActions(); + + // Start the death effect sequence from bug report: + // blink → scaleTo → scaleTo → callMethod (to kill actor) + let blinkExecuted = false; + actor.actions.blink(100, 100, 1).callMethod(() => { + blinkExecuted = true; // Track if blink completed + }); + + // THE BUG: The first action (blink) was being skipped after clearActions() + // THE FIX: It should now execute properly + + expect(actor.graphics.visible).toBe(true); + scene.update(engine, 100); // Blink on (100ms) + expect(actor.graphics.visible).toBe(false); // ✓ BLINK EXECUTED! Bug is fixed! + + scene.update(engine, 100); // Blink off (100ms) + expect(actor.graphics.visible).toBe(true); + + scene.update(engine, 1); // CallMethod executes + expect(blinkExecuted).toBe(true); // Confirms the action sequence worked + }); + }); + describe('easeTo', () => { it('can be reset', () => { const easeTo = new ex.EaseTo(actor, 100, 0, 100, ex.EasingFunctions.EaseInOutCubic);