@arcane-engine/runtime 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -2
- package/src/agent/protocol.ts +35 -1
- package/src/agent/types.ts +98 -13
- package/src/particles/emitter.test.ts +323 -0
- package/src/particles/emitter.ts +409 -0
- package/src/particles/index.ts +25 -0
- package/src/particles/types.ts +236 -0
- package/src/pathfinding/astar.ts +27 -0
- package/src/pathfinding/types.ts +39 -0
- package/src/physics/aabb.ts +55 -8
- package/src/rendering/animation.ts +73 -0
- package/src/rendering/audio.ts +29 -9
- package/src/rendering/camera.ts +28 -4
- package/src/rendering/input.ts +45 -9
- package/src/rendering/lighting.ts +29 -3
- package/src/rendering/loop.ts +16 -3
- package/src/rendering/sprites.ts +24 -1
- package/src/rendering/text.ts +52 -6
- package/src/rendering/texture.ts +22 -4
- package/src/rendering/tilemap.ts +36 -4
- package/src/rendering/types.ts +37 -19
- package/src/rendering/validate.ts +48 -3
- package/src/state/error.ts +21 -2
- package/src/state/observe.ts +40 -9
- package/src/state/prng.ts +88 -10
- package/src/state/query.ts +115 -15
- package/src/state/store.ts +42 -11
- package/src/state/transaction.ts +116 -12
- package/src/state/types.ts +31 -5
- package/src/systems/system.ts +77 -5
- package/src/systems/types.ts +52 -6
- package/src/testing/harness.ts +103 -5
- package/src/testing/mock-renderer.test.ts +16 -20
- package/src/tweening/chain.test.ts +191 -0
- package/src/tweening/chain.ts +103 -0
- package/src/tweening/easing.test.ts +134 -0
- package/src/tweening/easing.ts +288 -0
- package/src/tweening/helpers.test.ts +185 -0
- package/src/tweening/helpers.ts +166 -0
- package/src/tweening/index.ts +76 -0
- package/src/tweening/tween.test.ts +322 -0
- package/src/tweening/tween.ts +296 -0
- package/src/tweening/types.ts +134 -0
- package/src/ui/colors.ts +129 -0
- package/src/ui/index.ts +1 -0
- package/src/ui/primitives.ts +44 -5
- package/src/ui/types.ts +41 -2
package/src/systems/types.ts
CHANGED
|
@@ -1,35 +1,81 @@
|
|
|
1
1
|
import type { ArcaneError } from "../state/error.ts";
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A predicate that checks whether a rule's preconditions are met.
|
|
5
|
+
* Must be a pure function.
|
|
6
|
+
*
|
|
7
|
+
* @param state - Current game state (read-only by convention).
|
|
8
|
+
* @param args - Additional arguments passed to {@link applyRule}.
|
|
9
|
+
* @returns True if the condition is satisfied.
|
|
10
|
+
*/
|
|
4
11
|
export type Condition<S> = (state: S, args: Record<string, unknown>) => boolean;
|
|
5
12
|
|
|
6
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* A pure transform that produces new state when a rule fires.
|
|
15
|
+
* Multiple actions on a rule are chained: each receives the output of the previous.
|
|
16
|
+
*
|
|
17
|
+
* @param state - Current game state.
|
|
18
|
+
* @param args - Additional arguments passed to {@link applyRule}.
|
|
19
|
+
* @returns New state (must not mutate the input).
|
|
20
|
+
*/
|
|
7
21
|
export type Action<S> = (state: S, args: Record<string, unknown>) => S;
|
|
8
22
|
|
|
9
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* A named rule consisting of conditions and actions.
|
|
25
|
+
*
|
|
26
|
+
* When applied via {@link applyRule}, all conditions must pass for the
|
|
27
|
+
* actions to execute. Actions are chained in order.
|
|
28
|
+
*
|
|
29
|
+
* @typeParam S - The game state type.
|
|
30
|
+
*/
|
|
10
31
|
export type Rule<S> = Readonly<{
|
|
32
|
+
/** Unique name within the system. Used for lookup by {@link applyRule} and {@link extend}. */
|
|
11
33
|
name: string;
|
|
34
|
+
/** Conditions that must all return true for the rule to fire. Empty = always fires. */
|
|
12
35
|
conditions: readonly Condition<S>[];
|
|
36
|
+
/** State transforms to apply in order when the rule fires. */
|
|
13
37
|
actions: readonly Action<S>[];
|
|
38
|
+
/** If set, this rule replaces the rule with the given name when used in {@link extend}. */
|
|
14
39
|
replaces?: string;
|
|
15
40
|
}>;
|
|
16
41
|
|
|
17
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* A named system: an ordered collection of rules that together define game mechanics.
|
|
44
|
+
* Created via {@link system} and extended via {@link extend}.
|
|
45
|
+
*
|
|
46
|
+
* @typeParam S - The game state type.
|
|
47
|
+
*/
|
|
18
48
|
export type SystemDef<S> = Readonly<{
|
|
49
|
+
/** System name (e.g., "combat", "inventory"). */
|
|
19
50
|
name: string;
|
|
51
|
+
/** Ordered list of rules in this system. */
|
|
20
52
|
rules: readonly Rule<S>[];
|
|
21
53
|
}>;
|
|
22
54
|
|
|
23
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Result of applying a single rule via {@link applyRule}.
|
|
57
|
+
*
|
|
58
|
+
* @typeParam S - The game state type.
|
|
59
|
+
*/
|
|
24
60
|
export type RuleResult<S> = Readonly<{
|
|
61
|
+
/** True if all conditions passed and actions executed successfully. */
|
|
25
62
|
ok: boolean;
|
|
63
|
+
/** The resulting state (unchanged if ok is false). */
|
|
26
64
|
state: S;
|
|
65
|
+
/** Name of the rule that was applied. */
|
|
27
66
|
ruleName: string;
|
|
67
|
+
/** Error details if ok is false (rule not found or conditions failed). */
|
|
28
68
|
error?: ArcaneError;
|
|
29
69
|
}>;
|
|
30
70
|
|
|
31
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Options for extending a system via {@link extend}.
|
|
73
|
+
*
|
|
74
|
+
* @typeParam S - The game state type.
|
|
75
|
+
*/
|
|
32
76
|
export type ExtendOptions<S> = {
|
|
77
|
+
/** New rules to add. Rules with `replaces` set will replace existing rules by name. */
|
|
33
78
|
rules?: readonly Rule<S>[];
|
|
79
|
+
/** Names of existing rules to remove from the system. */
|
|
34
80
|
remove?: readonly string[];
|
|
35
81
|
};
|
package/src/testing/harness.ts
CHANGED
|
@@ -1,20 +1,97 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Universal test harness for Arcane — works in both Node.js and V8 (deno_core).
|
|
3
|
+
*
|
|
4
|
+
* In **Node mode**: delegates to `node:test` and `node:assert`.
|
|
5
|
+
* In **V8 mode**: standalone implementations with result reporting via
|
|
6
|
+
* `globalThis.__reportTest(suite, test, passed, error?)`.
|
|
7
|
+
*
|
|
8
|
+
* Test files import `{ describe, it, assert }` from this module and work
|
|
9
|
+
* identically in both environments.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { describe, it, assert } from "../testing/harness.ts";
|
|
14
|
+
*
|
|
15
|
+
* describe("math", () => {
|
|
16
|
+
* it("adds numbers", () => {
|
|
17
|
+
* assert.equal(1 + 1, 2);
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* it("supports deep equality", () => {
|
|
21
|
+
* assert.deepEqual({ a: 1 }, { a: 1 });
|
|
22
|
+
* });
|
|
23
|
+
* });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
6
26
|
|
|
27
|
+
/** A synchronous or async test function. */
|
|
7
28
|
type TestFn = () => void | Promise<void>;
|
|
29
|
+
|
|
30
|
+
/** Function signature for `describe()` — defines a test suite. */
|
|
8
31
|
type DescribeFn = (name: string, fn: () => void) => void;
|
|
32
|
+
|
|
33
|
+
/** Function signature for `it()` — defines a single test case. */
|
|
9
34
|
type ItFn = (name: string, fn: TestFn) => void;
|
|
10
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Assertion interface providing common test assertions.
|
|
38
|
+
*
|
|
39
|
+
* All assertion methods throw on failure with a descriptive error message.
|
|
40
|
+
*/
|
|
11
41
|
interface Assert {
|
|
42
|
+
/**
|
|
43
|
+
* Assert strict equality (`===`).
|
|
44
|
+
* @param actual - The value to test.
|
|
45
|
+
* @param expected - The expected value.
|
|
46
|
+
* @param message - Optional custom failure message.
|
|
47
|
+
*/
|
|
12
48
|
equal(actual: unknown, expected: unknown, message?: string): void;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Assert deep structural equality (recursive comparison of objects/arrays).
|
|
52
|
+
* @param actual - The value to test.
|
|
53
|
+
* @param expected - The expected structure.
|
|
54
|
+
* @param message - Optional custom failure message.
|
|
55
|
+
*/
|
|
13
56
|
deepEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Assert strict inequality (`!==`).
|
|
60
|
+
* @param actual - The value to test.
|
|
61
|
+
* @param expected - The value that `actual` must not equal.
|
|
62
|
+
* @param message - Optional custom failure message.
|
|
63
|
+
*/
|
|
14
64
|
notEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Assert that two values are NOT deeply equal.
|
|
68
|
+
* @param actual - The value to test.
|
|
69
|
+
* @param expected - The value that `actual` must not deeply equal.
|
|
70
|
+
* @param message - Optional custom failure message.
|
|
71
|
+
*/
|
|
15
72
|
notDeepEqual(actual: unknown, expected: unknown, message?: string): void;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Assert that a value is truthy.
|
|
76
|
+
* @param value - The value to test.
|
|
77
|
+
* @param message - Optional custom failure message.
|
|
78
|
+
*/
|
|
16
79
|
ok(value: unknown, message?: string): void;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Assert that a string matches a regular expression.
|
|
83
|
+
* @param actual - The string to test.
|
|
84
|
+
* @param expected - The regex pattern to match against.
|
|
85
|
+
* @param message - Optional custom failure message.
|
|
86
|
+
*/
|
|
17
87
|
match(actual: string, expected: RegExp, message?: string): void;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Assert that a function throws an error.
|
|
91
|
+
* @param fn - The function expected to throw.
|
|
92
|
+
* @param expected - Optional regex to match against the error message.
|
|
93
|
+
* @param message - Optional custom failure message.
|
|
94
|
+
*/
|
|
18
95
|
throws(fn: () => unknown, expected?: RegExp, message?: string): void;
|
|
19
96
|
}
|
|
20
97
|
|
|
@@ -244,8 +321,29 @@ declare const __reportTest: (
|
|
|
244
321
|
// Node.js — delegate to built-in modules
|
|
245
322
|
// ---------------------------------------------------------------------------
|
|
246
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Define a test suite. Can be nested with other describe() calls.
|
|
326
|
+
* In V8 mode, nested suites have their test names prefixed with the parent suite name.
|
|
327
|
+
*
|
|
328
|
+
* @param name - Suite name displayed in test output.
|
|
329
|
+
* @param fn - Function containing `it()` test cases and/or nested `describe()` calls.
|
|
330
|
+
*/
|
|
247
331
|
let describe: DescribeFn;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Define a single test case within a describe() suite.
|
|
335
|
+
* Supports both synchronous and async test functions.
|
|
336
|
+
*
|
|
337
|
+
* @param name - Test name displayed in test output.
|
|
338
|
+
* @param fn - Test function. Throw (or reject) to indicate failure; return to pass.
|
|
339
|
+
*/
|
|
248
340
|
let it: ItFn;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Assertion helpers for test cases. Methods throw on failure with descriptive messages.
|
|
344
|
+
*
|
|
345
|
+
* Available assertions: `equal`, `deepEqual`, `notEqual`, `notDeepEqual`, `ok`, `match`, `throws`.
|
|
346
|
+
*/
|
|
249
347
|
let assert: Assert;
|
|
250
348
|
|
|
251
349
|
if (isNode) {
|
|
@@ -2,16 +2,12 @@
|
|
|
2
2
|
* Tests demonstrating mock renderer usage
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// @ts-nocheck - Uses Node-specific test APIs (beforeEach/afterEach) not in harness
|
|
6
5
|
import { describe, it, assert } from "./harness.ts";
|
|
7
6
|
import { mockRenderer, installMockRenderer, restoreRenderer } from "./mock-renderer.ts";
|
|
8
7
|
|
|
9
8
|
describe("Mock Renderer", () => {
|
|
10
|
-
beforeEach(() => {
|
|
11
|
-
mockRenderer.reset();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
9
|
it("should validate drawRect parameters", () => {
|
|
10
|
+
mockRenderer.reset();
|
|
15
11
|
mockRenderer.drawRect(10.0, 20.0, 100.0, 50.0, { color: { r: 1.0, g: 0.5, b: 0.0 } });
|
|
16
12
|
|
|
17
13
|
mockRenderer.assertNoErrors();
|
|
@@ -19,24 +15,27 @@ describe("Mock Renderer", () => {
|
|
|
19
15
|
});
|
|
20
16
|
|
|
21
17
|
it("should catch wrong parameter types", () => {
|
|
18
|
+
mockRenderer.reset();
|
|
22
19
|
// Wrong: passing object instead of separate params
|
|
23
20
|
mockRenderer.drawRect({ x: 10 } as any, undefined, undefined, undefined);
|
|
24
21
|
|
|
25
|
-
assert(mockRenderer.hasErrors(), "Should have errors");
|
|
22
|
+
assert.ok(mockRenderer.hasErrors(), "Should have errors");
|
|
26
23
|
const errors = mockRenderer.getErrors();
|
|
27
|
-
assert(errors.some(e => e.includes("must be number")), "Should complain about type");
|
|
24
|
+
assert.ok(errors.some(e => e.includes("must be number")), "Should complain about type");
|
|
28
25
|
});
|
|
29
26
|
|
|
30
27
|
it("should catch invalid color values", () => {
|
|
28
|
+
mockRenderer.reset();
|
|
31
29
|
// Color values must be 0.0-1.0
|
|
32
30
|
mockRenderer.drawRect(0, 0, 100, 100, { color: { r: 255, g: 255, b: 255 } });
|
|
33
31
|
|
|
34
|
-
assert(mockRenderer.hasErrors(), "Should have errors");
|
|
32
|
+
assert.ok(mockRenderer.hasErrors(), "Should have errors");
|
|
35
33
|
const errors = mockRenderer.getErrors();
|
|
36
|
-
assert(errors.some(e => e.includes("color.r must be 0.0-1.0")), "Should catch invalid color");
|
|
34
|
+
assert.ok(errors.some(e => e.includes("color.r must be 0.0-1.0")), "Should catch invalid color");
|
|
37
35
|
});
|
|
38
36
|
|
|
39
37
|
it("should validate drawText parameters", () => {
|
|
38
|
+
mockRenderer.reset();
|
|
40
39
|
mockRenderer.drawText("Hello", { x: 10.0, y: 20.0, size: 16.0, color: { r: 1.0, g: 1.0, b: 1.0 } });
|
|
41
40
|
|
|
42
41
|
mockRenderer.assertNoErrors();
|
|
@@ -44,26 +43,21 @@ describe("Mock Renderer", () => {
|
|
|
44
43
|
});
|
|
45
44
|
|
|
46
45
|
it("should catch NaN values", () => {
|
|
46
|
+
mockRenderer.reset();
|
|
47
47
|
mockRenderer.drawRect(NaN, 0, 100, 100);
|
|
48
48
|
|
|
49
|
-
assert(mockRenderer.hasErrors(), "Should catch NaN");
|
|
49
|
+
assert.ok(mockRenderer.hasErrors(), "Should catch NaN");
|
|
50
50
|
const errors = mockRenderer.getErrors();
|
|
51
|
-
assert(errors.some(e => e.includes("NaN")), "Should complain about NaN");
|
|
51
|
+
assert.ok(errors.some(e => e.includes("NaN")), "Should complain about NaN");
|
|
52
52
|
});
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// Example: Testing a game's rendering code
|
|
56
56
|
describe("Game Visual Tests", () => {
|
|
57
|
-
|
|
57
|
+
it("should render HUD correctly", () => {
|
|
58
58
|
mockRenderer.reset();
|
|
59
59
|
installMockRenderer();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
afterEach(() => {
|
|
63
|
-
restoreRenderer();
|
|
64
|
-
});
|
|
65
60
|
|
|
66
|
-
it("should render HUD correctly", () => {
|
|
67
61
|
// Import game code that uses rendering
|
|
68
62
|
function renderHUD(health: number, gold: number) {
|
|
69
63
|
(globalThis as any).drawText(`HP: ${health}`, {
|
|
@@ -87,7 +81,9 @@ describe("Game Visual Tests", () => {
|
|
|
87
81
|
mockRenderer.assertCalled("drawText", 2);
|
|
88
82
|
|
|
89
83
|
const calls = mockRenderer.getCalls("drawText");
|
|
90
|
-
assert(calls[0].params[0] === "HP: 100", "First text correct");
|
|
91
|
-
assert(calls[1].params[0] === "Gold: 50", "Second text correct");
|
|
84
|
+
assert.ok(calls[0].params[0] === "HP: 100", "First text correct");
|
|
85
|
+
assert.ok(calls[1].params[0] === "Gold: 50", "Second text correct");
|
|
86
|
+
|
|
87
|
+
restoreRenderer();
|
|
92
88
|
});
|
|
93
89
|
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for tween chaining
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, assert } from "../testing/harness.ts";
|
|
6
|
+
import { sequence, parallel, stagger } from "./chain.ts";
|
|
7
|
+
import { updateTweens, stopAllTweens, getActiveTweenCount, reverseTween } from "./tween.ts";
|
|
8
|
+
|
|
9
|
+
describe("Tween Chaining", () => {
|
|
10
|
+
it("sequence should run tweens one after another", () => {
|
|
11
|
+
stopAllTweens();
|
|
12
|
+
|
|
13
|
+
const target1 = { x: 0 };
|
|
14
|
+
const target2 = { y: 0 };
|
|
15
|
+
const target3 = { z: 0 };
|
|
16
|
+
|
|
17
|
+
sequence([
|
|
18
|
+
{ target: target1, props: { x: 100 }, duration: 1.0 },
|
|
19
|
+
{ target: target2, props: { y: 100 }, duration: 1.0 },
|
|
20
|
+
{ target: target3, props: { z: 100 }, duration: 1.0 },
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// First tween should start immediately
|
|
24
|
+
updateTweens(1.0);
|
|
25
|
+
assert.equal(target1.x, 100);
|
|
26
|
+
assert.equal(target2.y, 0); // Not started yet
|
|
27
|
+
assert.equal(target3.z, 0);
|
|
28
|
+
|
|
29
|
+
// Second tween starts after first completes
|
|
30
|
+
updateTweens(1.0);
|
|
31
|
+
assert.equal(target1.x, 100);
|
|
32
|
+
assert.equal(target2.y, 100);
|
|
33
|
+
assert.equal(target3.z, 0); // Not started yet
|
|
34
|
+
|
|
35
|
+
// Third tween starts after second completes
|
|
36
|
+
updateTweens(1.0);
|
|
37
|
+
assert.equal(target1.x, 100);
|
|
38
|
+
assert.equal(target2.y, 100);
|
|
39
|
+
assert.equal(target3.z, 100);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("parallel should run tweens simultaneously", () => {
|
|
43
|
+
stopAllTweens();
|
|
44
|
+
|
|
45
|
+
const target1 = { x: 0 };
|
|
46
|
+
const target2 = { y: 0 };
|
|
47
|
+
const target3 = { z: 0 };
|
|
48
|
+
|
|
49
|
+
parallel([
|
|
50
|
+
{ target: target1, props: { x: 100 }, duration: 1.0 },
|
|
51
|
+
{ target: target2, props: { y: 100 }, duration: 1.0 },
|
|
52
|
+
{ target: target3, props: { z: 100 }, duration: 1.0 },
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
// All tweens should run simultaneously
|
|
56
|
+
updateTweens(0.5);
|
|
57
|
+
assert.equal(target1.x, 50);
|
|
58
|
+
assert.equal(target2.y, 50);
|
|
59
|
+
assert.equal(target3.z, 50);
|
|
60
|
+
|
|
61
|
+
updateTweens(0.5);
|
|
62
|
+
assert.equal(target1.x, 100);
|
|
63
|
+
assert.equal(target2.y, 100);
|
|
64
|
+
assert.equal(target3.z, 100);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("stagger should delay each tween by stagger amount", () => {
|
|
68
|
+
stopAllTweens();
|
|
69
|
+
|
|
70
|
+
const target1 = { x: 0 };
|
|
71
|
+
const target2 = { y: 0 };
|
|
72
|
+
const target3 = { z: 0 };
|
|
73
|
+
|
|
74
|
+
stagger(
|
|
75
|
+
[
|
|
76
|
+
{ target: target1, props: { x: 100 }, duration: 1.0 },
|
|
77
|
+
{ target: target2, props: { y: 100 }, duration: 1.0 },
|
|
78
|
+
{ target: target3, props: { z: 100 }, duration: 1.0 },
|
|
79
|
+
],
|
|
80
|
+
0.5
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// First tween starts immediately (delay 0)
|
|
84
|
+
updateTweens(0.5);
|
|
85
|
+
assert.equal(target1.x, 50);
|
|
86
|
+
assert.equal(target2.y, 0); // Still in delay
|
|
87
|
+
assert.equal(target3.z, 0); // Still in delay
|
|
88
|
+
|
|
89
|
+
// Second tween starts (delay 0.5), first continues
|
|
90
|
+
updateTweens(0.5);
|
|
91
|
+
assert.equal(target1.x, 100); // Complete
|
|
92
|
+
assert.equal(target2.y, 50); // Halfway
|
|
93
|
+
assert.equal(target3.z, 0); // Still in delay
|
|
94
|
+
|
|
95
|
+
// Third tween starts (delay 1.0), second continues
|
|
96
|
+
updateTweens(0.5);
|
|
97
|
+
assert.equal(target1.x, 100);
|
|
98
|
+
assert.equal(target2.y, 100); // Complete
|
|
99
|
+
assert.equal(target3.z, 50); // Halfway
|
|
100
|
+
|
|
101
|
+
// All complete
|
|
102
|
+
updateTweens(0.5);
|
|
103
|
+
assert.equal(target1.x, 100);
|
|
104
|
+
assert.equal(target2.y, 100);
|
|
105
|
+
assert.equal(target3.z, 100);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("sequence should chain callbacks correctly", () => {
|
|
109
|
+
stopAllTweens();
|
|
110
|
+
|
|
111
|
+
let completionOrder: number[] = [];
|
|
112
|
+
|
|
113
|
+
sequence([
|
|
114
|
+
{
|
|
115
|
+
target: { x: 0 },
|
|
116
|
+
props: { x: 100 },
|
|
117
|
+
duration: 1.0,
|
|
118
|
+
options: { onComplete: () => completionOrder.push(1) },
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
target: { y: 0 },
|
|
122
|
+
props: { y: 100 },
|
|
123
|
+
duration: 1.0,
|
|
124
|
+
options: { onComplete: () => completionOrder.push(2) },
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
target: { z: 0 },
|
|
128
|
+
props: { z: 100 },
|
|
129
|
+
duration: 1.0,
|
|
130
|
+
options: { onComplete: () => completionOrder.push(3) },
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
updateTweens(1.0);
|
|
135
|
+
assert.equal(completionOrder.length, 1);
|
|
136
|
+
assert.equal(completionOrder[0], 1);
|
|
137
|
+
|
|
138
|
+
updateTweens(1.0);
|
|
139
|
+
assert.equal(completionOrder.length, 2);
|
|
140
|
+
assert.equal(completionOrder[1], 2);
|
|
141
|
+
|
|
142
|
+
updateTweens(1.0);
|
|
143
|
+
assert.equal(completionOrder.length, 3);
|
|
144
|
+
assert.equal(completionOrder[2], 3);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("reverseTween should swap start and target values", () => {
|
|
148
|
+
stopAllTweens();
|
|
149
|
+
|
|
150
|
+
const target = { x: 0 };
|
|
151
|
+
const tweens = parallel([
|
|
152
|
+
{ target, props: { x: 100 }, duration: 1.0 },
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
// Animate halfway
|
|
156
|
+
updateTweens(0.5);
|
|
157
|
+
assert.equal(target.x, 50);
|
|
158
|
+
assert.equal(getActiveTweenCount(), 1); // Tween still active
|
|
159
|
+
|
|
160
|
+
// Reverse the tween
|
|
161
|
+
reverseTween(tweens[0]);
|
|
162
|
+
assert.equal(getActiveTweenCount(), 1); // Tween still active
|
|
163
|
+
|
|
164
|
+
// Now it should animate back towards 0
|
|
165
|
+
updateTweens(0.5);
|
|
166
|
+
assert.equal(getActiveTweenCount(), 1); // Should still be active
|
|
167
|
+
assert.equal(target.x, 25); // Halfway from 50 to 0
|
|
168
|
+
|
|
169
|
+
updateTweens(0.5);
|
|
170
|
+
assert.equal(getActiveTweenCount(), 0); // Now complete
|
|
171
|
+
assert.equal(target.x, 0);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("sequence should handle empty array", () => {
|
|
175
|
+
stopAllTweens();
|
|
176
|
+
const result = sequence([]);
|
|
177
|
+
assert.equal(result.length, 0);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("parallel should handle empty array", () => {
|
|
181
|
+
stopAllTweens();
|
|
182
|
+
const result = parallel([]);
|
|
183
|
+
assert.equal(result.length, 0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("stagger should handle empty array", () => {
|
|
187
|
+
stopAllTweens();
|
|
188
|
+
const result = stagger([], 0.5);
|
|
189
|
+
assert.equal(result.length, 0);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tween chaining utilities for composing multiple tweens.
|
|
3
|
+
*
|
|
4
|
+
* - {@link sequence} — run tweens one after another.
|
|
5
|
+
* - {@link parallel} — run tweens simultaneously.
|
|
6
|
+
* - {@link stagger} — run tweens with a staggered delay between each.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { tween, stopTween } from "./tween.ts";
|
|
10
|
+
import type { Tween, TweenOptions, TweenProps } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for a single tween within a {@link sequence}, {@link parallel}, or {@link stagger} chain.
|
|
14
|
+
*/
|
|
15
|
+
export interface TweenConfig {
|
|
16
|
+
/** The object whose properties will be interpolated. */
|
|
17
|
+
target: any;
|
|
18
|
+
/** Map of property names to target (end) values. */
|
|
19
|
+
props: TweenProps;
|
|
20
|
+
/** Animation duration in seconds. */
|
|
21
|
+
duration: number;
|
|
22
|
+
/** Optional tween options (easing, callbacks, etc.). */
|
|
23
|
+
options?: TweenOptions;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Run tweens one after another in sequence. Each tween starts when the
|
|
28
|
+
* previous one completes. Wraps `onComplete` callbacks to chain automatically.
|
|
29
|
+
*
|
|
30
|
+
* Note: Only the first tween is created immediately. Subsequent tweens are
|
|
31
|
+
* created lazily as each predecessor completes.
|
|
32
|
+
*
|
|
33
|
+
* @param tweens - Array of tween configurations to run in order.
|
|
34
|
+
* @returns Array of created tweens (initially contains only the first; more are added as the sequence progresses).
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* sequence([
|
|
39
|
+
* { target: sprite, props: { x: 100 }, duration: 0.5 },
|
|
40
|
+
* { target: sprite, props: { y: 200 }, duration: 0.3 },
|
|
41
|
+
* ]);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function sequence(tweens: TweenConfig[]): Tween[] {
|
|
45
|
+
if (tweens.length === 0) return [];
|
|
46
|
+
|
|
47
|
+
const createdTweens: Tween[] = [];
|
|
48
|
+
let currentIndex = 0;
|
|
49
|
+
|
|
50
|
+
function startNext() {
|
|
51
|
+
if (currentIndex >= tweens.length) return;
|
|
52
|
+
|
|
53
|
+
const config = tweens[currentIndex];
|
|
54
|
+
const options = config.options ?? {};
|
|
55
|
+
|
|
56
|
+
// Wrap onComplete to start next tween
|
|
57
|
+
const originalOnComplete = options.onComplete;
|
|
58
|
+
options.onComplete = () => {
|
|
59
|
+
if (originalOnComplete) {
|
|
60
|
+
originalOnComplete();
|
|
61
|
+
}
|
|
62
|
+
currentIndex++;
|
|
63
|
+
startNext();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const t = tween(config.target, config.props, config.duration, options);
|
|
67
|
+
createdTweens.push(t);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Start the first tween
|
|
71
|
+
startNext();
|
|
72
|
+
|
|
73
|
+
return createdTweens;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run all tweens simultaneously. All tweens start immediately.
|
|
78
|
+
*
|
|
79
|
+
* @param tweens - Array of tween configurations to run in parallel.
|
|
80
|
+
* @returns Array of all created tween instances.
|
|
81
|
+
*/
|
|
82
|
+
export function parallel(tweens: TweenConfig[]): Tween[] {
|
|
83
|
+
return tweens.map((config) =>
|
|
84
|
+
tween(config.target, config.props, config.duration, config.options)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Run tweens with a staggered delay between each start.
|
|
90
|
+
* The i-th tween gets an additional delay of `i * staggerDelay` seconds
|
|
91
|
+
* (added to any delay already specified in its options).
|
|
92
|
+
*
|
|
93
|
+
* @param tweens - Array of tween configurations to stagger.
|
|
94
|
+
* @param staggerDelay - Delay in seconds between each successive tween start. Must be >= 0.
|
|
95
|
+
* @returns Array of all created tween instances.
|
|
96
|
+
*/
|
|
97
|
+
export function stagger(tweens: TweenConfig[], staggerDelay: number): Tween[] {
|
|
98
|
+
return tweens.map((config, index) => {
|
|
99
|
+
const options = { ...(config.options ?? {}) };
|
|
100
|
+
options.delay = (options.delay ?? 0) + index * staggerDelay;
|
|
101
|
+
return tween(config.target, config.props, config.duration, options);
|
|
102
|
+
});
|
|
103
|
+
}
|