@arcane-engine/runtime 0.1.0 → 0.2.1

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.
Files changed (47) hide show
  1. package/package.json +4 -2
  2. package/src/agent/protocol.ts +35 -1
  3. package/src/agent/types.ts +98 -13
  4. package/src/particles/emitter.test.ts +323 -0
  5. package/src/particles/emitter.ts +409 -0
  6. package/src/particles/index.ts +25 -0
  7. package/src/particles/types.ts +236 -0
  8. package/src/pathfinding/astar.ts +27 -0
  9. package/src/pathfinding/types.ts +39 -0
  10. package/src/physics/aabb.ts +55 -8
  11. package/src/rendering/animation.ts +73 -0
  12. package/src/rendering/audio.ts +29 -9
  13. package/src/rendering/camera.ts +28 -4
  14. package/src/rendering/input.ts +45 -9
  15. package/src/rendering/lighting.ts +29 -3
  16. package/src/rendering/loop.ts +16 -3
  17. package/src/rendering/sprites.ts +24 -1
  18. package/src/rendering/text.ts +52 -6
  19. package/src/rendering/texture.ts +22 -4
  20. package/src/rendering/tilemap.ts +36 -4
  21. package/src/rendering/types.ts +37 -19
  22. package/src/rendering/validate.ts +48 -3
  23. package/src/state/error.ts +21 -2
  24. package/src/state/observe.ts +40 -9
  25. package/src/state/prng.ts +88 -10
  26. package/src/state/query.ts +115 -15
  27. package/src/state/store.ts +42 -11
  28. package/src/state/transaction.ts +116 -12
  29. package/src/state/types.ts +31 -5
  30. package/src/systems/system.ts +77 -5
  31. package/src/systems/types.ts +52 -6
  32. package/src/testing/harness.ts +103 -5
  33. package/src/testing/mock-renderer.test.ts +16 -20
  34. package/src/tweening/chain.test.ts +191 -0
  35. package/src/tweening/chain.ts +103 -0
  36. package/src/tweening/easing.test.ts +134 -0
  37. package/src/tweening/easing.ts +288 -0
  38. package/src/tweening/helpers.test.ts +185 -0
  39. package/src/tweening/helpers.ts +166 -0
  40. package/src/tweening/index.ts +76 -0
  41. package/src/tweening/tween.test.ts +322 -0
  42. package/src/tweening/tween.ts +296 -0
  43. package/src/tweening/types.ts +134 -0
  44. package/src/ui/colors.ts +129 -0
  45. package/src/ui/index.ts +1 -0
  46. package/src/ui/primitives.ts +44 -5
  47. package/src/ui/types.ts +41 -2
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Tweening helper functions for common game "juice" effects.
3
+ *
4
+ * Camera shake and screen flash are implemented as global singletons.
5
+ * Only one shake and one flash can be active at a time; starting a new one
6
+ * replaces the previous.
7
+ *
8
+ * Usage: call the effect function, then read the offset/flash state each frame
9
+ * when rendering.
10
+ */
11
+
12
+ import { tween } from "./tween.ts";
13
+ import { easeOutQuad } from "./easing.ts";
14
+
15
+ /** Internal camera shake state (global singleton). */
16
+ let shakeState = {
17
+ offsetX: 0,
18
+ offsetY: 0,
19
+ active: false,
20
+ };
21
+
22
+ /**
23
+ * Start a camera shake effect that decays over time using easeOutQuad.
24
+ *
25
+ * Each frame, read the offset via {@link getCameraShakeOffset} and add it
26
+ * to your camera position. The offset oscillates randomly and decays to zero.
27
+ *
28
+ * @param intensity - Maximum shake offset in pixels. Higher = more violent. Must be > 0.
29
+ * @param duration - Duration of the shake in seconds. Must be > 0.
30
+ * @param frequency - Unused currently; reserved for future use. Default: 20.
31
+ */
32
+ export function shakeCamera(
33
+ intensity: number,
34
+ duration: number,
35
+ frequency: number = 20,
36
+ ): void {
37
+ shakeState.active = true;
38
+
39
+ // Store original intensity for interpolation
40
+ const startIntensity = intensity;
41
+
42
+ // Create a decay tween
43
+ const decay = { value: 1.0 };
44
+ tween(decay, { value: 0 }, duration, {
45
+ easing: easeOutQuad,
46
+ onUpdate: (progress) => {
47
+ // Decay intensity over time
48
+ const currentIntensity = startIntensity * (1 - progress);
49
+
50
+ // Generate random offsets with current intensity
51
+ // Use a simple pseudo-random pattern based on time
52
+ const angle = Math.random() * Math.PI * 2;
53
+ shakeState.offsetX = Math.cos(angle) * currentIntensity;
54
+ shakeState.offsetY = Math.sin(angle) * currentIntensity;
55
+ },
56
+ onComplete: () => {
57
+ shakeState.active = false;
58
+ shakeState.offsetX = 0;
59
+ shakeState.offsetY = 0;
60
+ },
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Get the current camera shake offset for this frame.
66
+ * Returns {0, 0} when no shake is active.
67
+ *
68
+ * @returns Object with `x` and `y` pixel offsets to add to camera position.
69
+ */
70
+ export function getCameraShakeOffset(): { x: number; y: number } {
71
+ return {
72
+ x: shakeState.offsetX,
73
+ y: shakeState.offsetY,
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Check whether a camera shake effect is currently active.
79
+ * @returns True if shake is in progress, false otherwise.
80
+ */
81
+ export function isCameraShaking(): boolean {
82
+ return shakeState.active;
83
+ }
84
+
85
+ /**
86
+ * Stop the camera shake immediately, resetting the offset to zero.
87
+ */
88
+ export function stopCameraShake(): void {
89
+ shakeState.active = false;
90
+ shakeState.offsetX = 0;
91
+ shakeState.offsetY = 0;
92
+ }
93
+
94
+ /** Internal screen flash state (global singleton). */
95
+ let flashState = {
96
+ opacity: 0,
97
+ r: 1,
98
+ g: 1,
99
+ b: 1,
100
+ active: false,
101
+ };
102
+
103
+ /**
104
+ * Flash the screen with a colored overlay that fades out using easeOutQuad.
105
+ *
106
+ * Each frame, read the flash state via {@link getScreenFlash} and render
107
+ * a full-screen rectangle with the returned color and opacity.
108
+ *
109
+ * @param r - Red component, 0.0 (none) to 1.0 (full).
110
+ * @param g - Green component, 0.0 (none) to 1.0 (full).
111
+ * @param b - Blue component, 0.0 (none) to 1.0 (full).
112
+ * @param duration - Fade-out duration in seconds. Must be > 0.
113
+ * @param startOpacity - Initial opacity of the flash overlay. Default: 0.8. Range: 0.0..1.0.
114
+ */
115
+ export function flashScreen(
116
+ r: number,
117
+ g: number,
118
+ b: number,
119
+ duration: number,
120
+ startOpacity: number = 0.8,
121
+ ): void {
122
+ flashState.active = true;
123
+ flashState.r = r;
124
+ flashState.g = g;
125
+ flashState.b = b;
126
+ flashState.opacity = startOpacity;
127
+
128
+ // Fade out the flash
129
+ tween(flashState, { opacity: 0 }, duration, {
130
+ easing: easeOutQuad,
131
+ onComplete: () => {
132
+ flashState.active = false;
133
+ },
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Get the current screen flash color and opacity for this frame.
139
+ *
140
+ * @returns Flash state with `r`, `g`, `b` (0..1) and `opacity` (0..1), or `null` if no flash is active.
141
+ */
142
+ export function getScreenFlash(): { r: number; g: number; b: number; opacity: number } | null {
143
+ if (!flashState.active) return null;
144
+ return {
145
+ r: flashState.r,
146
+ g: flashState.g,
147
+ b: flashState.b,
148
+ opacity: flashState.opacity,
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Check whether a screen flash effect is currently active.
154
+ * @returns True if flash is in progress, false otherwise.
155
+ */
156
+ export function isScreenFlashing(): boolean {
157
+ return flashState.active;
158
+ }
159
+
160
+ /**
161
+ * Stop the screen flash immediately, resetting opacity to zero.
162
+ */
163
+ export function stopScreenFlash(): void {
164
+ flashState.active = false;
165
+ flashState.opacity = 0;
166
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Tweening system
3
+ *
4
+ * Provides smooth animation of numeric properties over time.
5
+ */
6
+
7
+ export type {
8
+ EasingFunction,
9
+ TweenCallback,
10
+ TweenUpdateCallback,
11
+ TweenOptions,
12
+ TweenProps,
13
+ Tween,
14
+ } from "./types.ts";
15
+ export { TweenState } from "./types.ts";
16
+
17
+ export {
18
+ tween,
19
+ updateTweens,
20
+ stopTween,
21
+ pauseTween,
22
+ resumeTween,
23
+ reverseTween,
24
+ stopAllTweens,
25
+ getActiveTweenCount,
26
+ linear,
27
+ } from "./tween.ts";
28
+
29
+ export {
30
+ linear as easingLinear,
31
+ easeInQuad,
32
+ easeOutQuad,
33
+ easeInOutQuad,
34
+ easeInCubic,
35
+ easeOutCubic,
36
+ easeInOutCubic,
37
+ easeInQuart,
38
+ easeOutQuart,
39
+ easeInOutQuart,
40
+ easeInQuint,
41
+ easeOutQuint,
42
+ easeInOutQuint,
43
+ easeInSine,
44
+ easeOutSine,
45
+ easeInOutSine,
46
+ easeInExpo,
47
+ easeOutExpo,
48
+ easeInOutExpo,
49
+ easeInCirc,
50
+ easeOutCirc,
51
+ easeInOutCirc,
52
+ easeInBack,
53
+ easeOutBack,
54
+ easeInOutBack,
55
+ easeInElastic,
56
+ easeOutElastic,
57
+ easeInOutElastic,
58
+ easeInBounce,
59
+ easeOutBounce,
60
+ easeInOutBounce,
61
+ Easing,
62
+ } from "./easing.ts";
63
+
64
+ export type { TweenConfig } from "./chain.ts";
65
+ export { sequence, parallel, stagger } from "./chain.ts";
66
+
67
+ export {
68
+ shakeCamera,
69
+ getCameraShakeOffset,
70
+ isCameraShaking,
71
+ stopCameraShake,
72
+ flashScreen,
73
+ getScreenFlash,
74
+ isScreenFlashing,
75
+ stopScreenFlash,
76
+ } from "./helpers.ts";
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Tests for core tweening system
3
+ */
4
+
5
+ import { describe, it, assert } from "../testing/harness.ts";
6
+ import {
7
+ tween,
8
+ updateTweens,
9
+ stopTween,
10
+ pauseTween,
11
+ resumeTween,
12
+ stopAllTweens,
13
+ getActiveTweenCount,
14
+ } from "./tween.ts";
15
+ import { TweenState } from "./types.ts";
16
+
17
+ describe("Core Tweening", () => {
18
+ it("should tween a single property", () => {
19
+ stopAllTweens();
20
+ const target = { x: 0 };
21
+ tween(target, { x: 100 }, 1.0);
22
+
23
+ // At 0s
24
+ assert.equal(target.x, 0);
25
+
26
+ // At 0.5s (halfway)
27
+ updateTweens(0.5);
28
+ assert.equal(target.x, 50);
29
+
30
+ // At 1.0s (complete)
31
+ updateTweens(0.5);
32
+ assert.equal(target.x, 100);
33
+ });
34
+
35
+ it("should tween multiple properties", () => {
36
+ stopAllTweens();
37
+ const target = { x: 0, y: 0, scale: 1 };
38
+ tween(target, { x: 100, y: 50, scale: 2 }, 1.0);
39
+
40
+ updateTweens(0.5);
41
+ assert.equal(target.x, 50);
42
+ assert.equal(target.y, 25);
43
+ assert.equal(target.scale, 1.5);
44
+
45
+ updateTweens(0.5);
46
+ assert.equal(target.x, 100);
47
+ assert.equal(target.y, 50);
48
+ assert.equal(target.scale, 2);
49
+ });
50
+
51
+ it("should handle delay", () => {
52
+ const target = { x: 0 };
53
+ const t = tween(target, { x: 100 }, 1.0, { delay: 0.5 });
54
+
55
+ // Initially pending
56
+ assert.equal(t.state, TweenState.PENDING);
57
+
58
+ // During delay, target unchanged
59
+ updateTweens(0.3);
60
+ assert.equal(target.x, 0);
61
+ assert.equal(t.state, TweenState.PENDING);
62
+
63
+ // After delay, tween starts
64
+ updateTweens(0.2);
65
+ assert.equal(t.state, TweenState.ACTIVE);
66
+
67
+ // Now tweening
68
+ updateTweens(0.5);
69
+ assert.equal(target.x, 50);
70
+ });
71
+
72
+ it("should call onStart callback after delay", () => {
73
+ let started = false;
74
+ const target = { x: 0 };
75
+
76
+ tween(target, { x: 100 }, 1.0, {
77
+ delay: 0.5,
78
+ onStart: () => {
79
+ started = true;
80
+ },
81
+ });
82
+
83
+ assert.equal(started, false);
84
+
85
+ // Before delay ends
86
+ updateTweens(0.3);
87
+ assert.equal(started, false);
88
+
89
+ // After delay ends
90
+ updateTweens(0.2);
91
+ assert.equal(started, true);
92
+ });
93
+
94
+ it("should call onUpdate callback", () => {
95
+ let updateCount = 0;
96
+ let lastProgress = 0;
97
+ const target = { x: 0 };
98
+
99
+ tween(target, { x: 100 }, 1.0, {
100
+ onUpdate: (progress) => {
101
+ updateCount++;
102
+ lastProgress = progress;
103
+ },
104
+ });
105
+
106
+ updateTweens(0.5);
107
+ assert.equal(updateCount, 1);
108
+ assert.ok(Math.abs(lastProgress - 0.5) < 0.01, `Expected progress ~0.5, got ${lastProgress}`);
109
+
110
+ updateTweens(0.5);
111
+ assert.equal(updateCount, 2);
112
+ assert.ok(Math.abs(lastProgress - 1.0) < 0.01, `Expected progress ~1.0, got ${lastProgress}`);
113
+ });
114
+
115
+ it("should call onComplete callback", () => {
116
+ let completed = false;
117
+ const target = { x: 0 };
118
+
119
+ tween(target, { x: 100 }, 1.0, {
120
+ onComplete: () => {
121
+ completed = true;
122
+ },
123
+ });
124
+
125
+ updateTweens(0.5);
126
+ assert.equal(completed, false);
127
+
128
+ updateTweens(0.5);
129
+ assert.equal(completed, true);
130
+ });
131
+
132
+ it("should remove completed tweens from active list", () => {
133
+ const target = { x: 0 };
134
+ tween(target, { x: 100 }, 1.0);
135
+
136
+ assert.equal(getActiveTweenCount(), 1);
137
+
138
+ updateTweens(1.0);
139
+ assert.equal(getActiveTweenCount(), 0);
140
+ });
141
+
142
+ it("should handle repeat", () => {
143
+ let repeatCount = 0;
144
+ const target = { x: 0 };
145
+
146
+ tween(target, { x: 100 }, 1.0, {
147
+ repeat: 2,
148
+ onRepeat: () => {
149
+ repeatCount++;
150
+ },
151
+ });
152
+
153
+ // First iteration
154
+ updateTweens(1.0);
155
+ assert.equal(target.x, 100);
156
+ assert.equal(repeatCount, 1);
157
+
158
+ // Second iteration
159
+ updateTweens(1.0);
160
+ assert.equal(target.x, 100);
161
+ assert.equal(repeatCount, 2);
162
+
163
+ // Third iteration (completes)
164
+ updateTweens(1.0);
165
+ assert.equal(target.x, 100);
166
+ assert.equal(repeatCount, 2);
167
+ assert.equal(getActiveTweenCount(), 0);
168
+ });
169
+
170
+ it("should handle infinite repeat", () => {
171
+ const target = { x: 0 };
172
+ tween(target, { x: 100 }, 1.0, { repeat: -1 });
173
+
174
+ for (let i = 0; i < 10; i++) {
175
+ updateTweens(1.0);
176
+ assert.equal(target.x, 100);
177
+ }
178
+
179
+ // Should still be active
180
+ assert.equal(getActiveTweenCount(), 1);
181
+ });
182
+
183
+ it("should handle yoyo mode", () => {
184
+ stopAllTweens();
185
+ const target = { x: 0 };
186
+ tween(target, { x: 100 }, 1.0, { repeat: 1, yoyo: true });
187
+
188
+ // Forward
189
+ updateTweens(1.0);
190
+ assert.equal(target.x, 100);
191
+
192
+ // Backward
193
+ updateTweens(1.0);
194
+ assert.equal(target.x, 0);
195
+
196
+ assert.equal(getActiveTweenCount(), 0);
197
+ });
198
+
199
+ it("should stop a tween", () => {
200
+ stopAllTweens();
201
+ const target = { x: 0 };
202
+ const t = tween(target, { x: 100 }, 1.0);
203
+
204
+ updateTweens(0.5);
205
+ assert.equal(target.x, 50);
206
+
207
+ stopTween(t);
208
+ assert.equal(t.state, TweenState.STOPPED);
209
+ assert.equal(getActiveTweenCount(), 0);
210
+
211
+ // Further updates do nothing
212
+ updateTweens(0.5);
213
+ assert.equal(target.x, 50);
214
+ });
215
+
216
+ it("should pause and resume a tween", () => {
217
+ const target = { x: 0 };
218
+ const t = tween(target, { x: 100 }, 1.0);
219
+
220
+ updateTweens(0.3);
221
+ assert.equal(target.x, 30);
222
+
223
+ pauseTween(t);
224
+ assert.equal(t.state, TweenState.PAUSED);
225
+
226
+ // No update while paused
227
+ updateTweens(0.5);
228
+ assert.equal(target.x, 30);
229
+
230
+ resumeTween(t);
231
+ assert.equal(t.state, TweenState.ACTIVE);
232
+
233
+ // Continues from where it left off
234
+ updateTweens(0.5);
235
+ assert.equal(target.x, 80);
236
+ });
237
+
238
+ it("should handle multiple simultaneous tweens", () => {
239
+ stopAllTweens();
240
+ const target1 = { x: 0 };
241
+ const target2 = { y: 0 };
242
+ const target3 = { z: 0 };
243
+
244
+ tween(target1, { x: 100 }, 1.0);
245
+ tween(target2, { y: 200 }, 2.0);
246
+ tween(target3, { z: 50 }, 0.5);
247
+
248
+ assert.equal(getActiveTweenCount(), 3);
249
+
250
+ updateTweens(0.5);
251
+ assert.equal(target1.x, 50);
252
+ assert.equal(target2.y, 50);
253
+ assert.equal(target3.z, 50);
254
+ assert.equal(getActiveTweenCount(), 2); // target3 completed
255
+
256
+ updateTweens(0.5);
257
+ assert.equal(target1.x, 100);
258
+ assert.equal(target2.y, 100);
259
+ assert.equal(getActiveTweenCount(), 1); // target1 completed
260
+
261
+ updateTweens(1.0);
262
+ assert.equal(target2.y, 200);
263
+ assert.equal(getActiveTweenCount(), 0); // all completed
264
+ });
265
+
266
+ it("should stop all tweens", () => {
267
+ stopAllTweens();
268
+ tween({ x: 0 }, { x: 100 }, 1.0);
269
+ tween({ y: 0 }, { y: 100 }, 1.0);
270
+ tween({ z: 0 }, { z: 100 }, 1.0);
271
+
272
+ assert.equal(getActiveTweenCount(), 3);
273
+
274
+ stopAllTweens();
275
+ assert.equal(getActiveTweenCount(), 0);
276
+ });
277
+
278
+ it("should handle custom easing function", () => {
279
+ const target = { x: 0 };
280
+ // Square easing: t^2
281
+ tween(target, { x: 100 }, 1.0, {
282
+ easing: (t) => t * t,
283
+ });
284
+
285
+ updateTweens(0.5);
286
+ // With square easing: 0.5^2 = 0.25
287
+ assert.equal(target.x, 25);
288
+
289
+ updateTweens(0.5);
290
+ assert.equal(target.x, 100);
291
+ });
292
+
293
+ it("should handle negative target values", () => {
294
+ const target = { x: 100 };
295
+ tween(target, { x: -50 }, 1.0);
296
+
297
+ updateTweens(0.5);
298
+ assert.equal(target.x, 25); // Halfway between 100 and -50
299
+
300
+ updateTweens(0.5);
301
+ assert.equal(target.x, -50);
302
+ });
303
+
304
+ it("should initialize missing properties to zero", () => {
305
+ const target: any = {};
306
+ tween(target, { x: 100 }, 1.0);
307
+
308
+ updateTweens(0.5);
309
+ assert.equal(target.x, 50);
310
+ });
311
+
312
+ it("should handle zero duration", () => {
313
+ stopAllTweens();
314
+ const target = { x: 0 };
315
+ tween(target, { x: 100 }, 0);
316
+
317
+ // Should complete immediately
318
+ updateTweens(0);
319
+ assert.equal(target.x, 100);
320
+ assert.equal(getActiveTweenCount(), 0);
321
+ });
322
+ });