@guinetik/gcanvas 1.0.0 → 1.0.2

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 (102) hide show
  1. package/demos/coordinates.html +698 -0
  2. package/demos/cube3d.html +23 -0
  3. package/demos/demos.css +17 -3
  4. package/demos/dino.html +42 -0
  5. package/demos/fluid-simple.html +22 -0
  6. package/demos/fluid.html +37 -0
  7. package/demos/gameobjects.html +626 -0
  8. package/demos/index.html +19 -7
  9. package/demos/js/blob.js +18 -5
  10. package/demos/js/coordinates.js +840 -0
  11. package/demos/js/cube3d.js +789 -0
  12. package/demos/js/dino.js +1420 -0
  13. package/demos/js/fluid-simple.js +253 -0
  14. package/demos/js/fluid.js +527 -0
  15. package/demos/js/gameobjects.js +176 -0
  16. package/demos/js/plane3d.js +256 -0
  17. package/demos/js/platformer.js +1579 -0
  18. package/demos/js/sphere3d.js +229 -0
  19. package/demos/js/sprite.js +473 -0
  20. package/demos/js/tde/accretiondisk.js +65 -12
  21. package/demos/js/tde/blackholescene.js +2 -2
  22. package/demos/js/tde/config.js +2 -2
  23. package/demos/js/tde/index.js +152 -27
  24. package/demos/js/tde/lensedstarfield.js +32 -25
  25. package/demos/js/tde/tdestar.js +78 -98
  26. package/demos/js/tde/tidalstream.js +24 -8
  27. package/demos/plane3d.html +24 -0
  28. package/demos/platformer.html +43 -0
  29. package/demos/sphere3d.html +24 -0
  30. package/demos/sprite.html +18 -0
  31. package/docs/README.md +230 -222
  32. package/docs/api/FluidSystem.md +173 -0
  33. package/docs/concepts/architecture-overview.md +204 -204
  34. package/docs/concepts/coordinate-system.md +384 -0
  35. package/docs/concepts/rendering-pipeline.md +279 -279
  36. package/docs/concepts/shapes-vs-gameobjects.md +187 -0
  37. package/docs/concepts/two-layer-architecture.md +229 -229
  38. package/docs/fluid-dynamics.md +99 -0
  39. package/docs/getting-started/first-game.md +354 -354
  40. package/docs/getting-started/installation.md +175 -157
  41. package/docs/modules/collision/README.md +2 -2
  42. package/docs/modules/fluent/README.md +6 -6
  43. package/docs/modules/game/README.md +303 -303
  44. package/docs/modules/isometric-camera.md +2 -2
  45. package/docs/modules/isometric.md +1 -1
  46. package/docs/modules/painter/README.md +328 -328
  47. package/docs/modules/particle/README.md +3 -3
  48. package/docs/modules/shapes/README.md +221 -221
  49. package/docs/modules/shapes/base/euclidian.md +123 -123
  50. package/docs/modules/shapes/base/shape.md +262 -262
  51. package/docs/modules/shapes/base/transformable.md +243 -243
  52. package/docs/modules/state/README.md +2 -2
  53. package/docs/modules/util/README.md +1 -1
  54. package/docs/modules/util/camera3d.md +3 -3
  55. package/docs/modules/util/scene3d.md +1 -1
  56. package/package.json +3 -1
  57. package/readme.md +19 -5
  58. package/src/collision/collision.js +75 -0
  59. package/src/game/game.js +11 -5
  60. package/src/game/index.js +2 -1
  61. package/src/game/objects/index.js +3 -0
  62. package/src/game/objects/platformer-scene.js +411 -0
  63. package/src/game/objects/scene.js +14 -0
  64. package/src/game/objects/sprite.js +529 -0
  65. package/src/game/pipeline.js +20 -16
  66. package/src/game/systems/FluidSystem.js +835 -0
  67. package/src/game/systems/index.js +11 -0
  68. package/src/game/ui/button.js +39 -18
  69. package/src/game/ui/cursor.js +14 -0
  70. package/src/game/ui/fps.js +12 -4
  71. package/src/game/ui/index.js +2 -0
  72. package/src/game/ui/stepper.js +549 -0
  73. package/src/game/ui/theme.js +123 -0
  74. package/src/game/ui/togglebutton.js +9 -3
  75. package/src/game/ui/tooltip.js +11 -4
  76. package/src/io/input.js +75 -45
  77. package/src/io/mouse.js +44 -19
  78. package/src/io/touch.js +35 -12
  79. package/src/math/fluid.js +507 -0
  80. package/src/math/index.js +2 -0
  81. package/src/mixins/anchor.js +17 -7
  82. package/src/motion/tweenetik.js +16 -0
  83. package/src/shapes/cube3d.js +599 -0
  84. package/src/shapes/index.js +3 -0
  85. package/src/shapes/plane3d.js +687 -0
  86. package/src/shapes/sphere3d.js +75 -6
  87. package/src/util/camera2d.js +315 -0
  88. package/src/util/camera3d.js +218 -12
  89. package/src/util/index.js +1 -0
  90. package/src/webgl/shaders/plane-shaders.js +332 -0
  91. package/src/webgl/shaders/sphere-shaders.js +4 -2
  92. package/types/fluent.d.ts +361 -0
  93. package/types/game.d.ts +303 -0
  94. package/types/index.d.ts +144 -5
  95. package/types/math.d.ts +361 -0
  96. package/types/motion.d.ts +271 -0
  97. package/types/particle.d.ts +373 -0
  98. package/types/shapes.d.ts +107 -9
  99. package/types/util.d.ts +353 -0
  100. package/types/webgl.d.ts +109 -0
  101. package/disk_example.png +0 -0
  102. package/tde.png +0 -0
@@ -0,0 +1,529 @@
1
+ import { GameObject } from './go.js';
2
+ import { Painter } from '../../painter/painter.js';
3
+
4
+ /**
5
+ * Sprite - A MovieClip-style GameObject with timeline animation
6
+ *
7
+ * A Sprite manages a collection of Shapes as frames in a timeline.
8
+ * Each frame displays a different Shape, creating frame-by-frame animation
9
+ * similar to Adobe Flash MovieClip.
10
+ *
11
+ * Supports named animations for complex sprite sheets:
12
+ *
13
+ * @example
14
+ * const sprite = new Sprite(game, {
15
+ * frameRate: 12,
16
+ * loop: true
17
+ * });
18
+ *
19
+ * // Add shapes as frames
20
+ * sprite.addFrame(new Circle({ radius: 20, color: 'red' }));
21
+ * sprite.addFrame(new Circle({ radius: 25, color: 'orange' }));
22
+ * sprite.addFrame(new Circle({ radius: 20, color: 'red' }));
23
+ *
24
+ * // Control playback
25
+ * sprite.play();
26
+ * sprite.gotoAndStop(0);
27
+ * sprite.gotoAndPlay(1);
28
+ *
29
+ * @example
30
+ * // Named animations
31
+ * sprite.addAnimation('idle', [idleShape]);
32
+ * sprite.addAnimation('walk', [walkFrame1, walkFrame2, walkFrame3]);
33
+ * sprite.addAnimation('jump', [jumpShape]);
34
+ *
35
+ * sprite.playAnimation('walk'); // Plays walk animation
36
+ * sprite.playAnimation('idle'); // Switch to idle
37
+ */
38
+ export class Sprite extends GameObject {
39
+ /**
40
+ * Creates a new Sprite
41
+ * @param {Game} game - The game instance
42
+ * @param {Object} options - Configuration options
43
+ * @param {number} [options.frameRate=12] - Frames per second for playback
44
+ * @param {boolean} [options.loop=true] - Whether to loop the animation
45
+ * @param {boolean} [options.autoPlay=false] - Whether to start playing automatically
46
+ * @param {Array<Shape>} [options.frames=[]] - Initial frames to add
47
+ */
48
+ constructor(game, options = {}) {
49
+ super(game, options);
50
+
51
+ // Timeline properties
52
+ this._frames = [];
53
+ this._currentFrame = 0;
54
+ this._frameAccumulator = 0;
55
+ this._isPlaying = options.autoPlay || false;
56
+ this._loop = options.loop !== undefined ? options.loop : true;
57
+ this._frameRate = options.frameRate || 12; // frames per second
58
+ this._frameDuration = 1 / this._frameRate; // seconds per frame
59
+
60
+ // Named animations support
61
+ this._animations = new Map(); // name -> { frames: Shape[], loop: boolean, frameRate: number }
62
+ this._currentAnimation = null; // current animation name
63
+
64
+ // Add initial frames if provided
65
+ if (options.frames && Array.isArray(options.frames)) {
66
+ options.frames.forEach(frame => this.addFrame(frame));
67
+ }
68
+ }
69
+
70
+ // ==================== Named Animations ====================
71
+
72
+ /**
73
+ * Adds a named animation
74
+ * @param {string} name - Animation name (e.g., 'walk', 'idle', 'jump')
75
+ * @param {Array<Shape>} frames - Array of shapes for this animation
76
+ * @param {Object} [options] - Animation options
77
+ * @param {boolean} [options.loop=true] - Whether this animation loops
78
+ * @param {number} [options.frameRate] - Frame rate for this animation (uses sprite's default if not set)
79
+ * @returns {Sprite} This sprite for chaining
80
+ */
81
+ addAnimation(name, frames, options = {}) {
82
+ if (!name || typeof name !== 'string') {
83
+ throw new Error('Sprite.addAnimation: name is required');
84
+ }
85
+ if (!frames || !Array.isArray(frames) || frames.length === 0) {
86
+ throw new Error('Sprite.addAnimation: frames array is required');
87
+ }
88
+
89
+ // Set parent reference for all frames
90
+ frames.forEach(frame => {
91
+ frame.parent = this;
92
+ });
93
+
94
+ this._animations.set(name, {
95
+ frames,
96
+ loop: options.loop !== undefined ? options.loop : true,
97
+ frameRate: options.frameRate || null, // null means use sprite's default
98
+ });
99
+
100
+ return this;
101
+ }
102
+
103
+ /**
104
+ * Removes a named animation
105
+ * @param {string} name - Animation name to remove
106
+ * @returns {boolean} True if animation was found and removed
107
+ */
108
+ removeAnimation(name) {
109
+ const anim = this._animations.get(name);
110
+ if (anim) {
111
+ anim.frames.forEach(frame => {
112
+ frame.parent = null;
113
+ });
114
+ this._animations.delete(name);
115
+
116
+ // If this was the current animation, clear it
117
+ if (this._currentAnimation === name) {
118
+ this._currentAnimation = null;
119
+ this._frames = [];
120
+ }
121
+ return true;
122
+ }
123
+ return false;
124
+ }
125
+
126
+ /**
127
+ * Plays a named animation
128
+ * @param {string} name - Animation name to play
129
+ * @param {boolean} [restart=false] - If true, restarts animation even if already playing
130
+ * @returns {Sprite} This sprite for chaining
131
+ */
132
+ playAnimation(name, restart = false) {
133
+ const anim = this._animations.get(name);
134
+ if (!anim) {
135
+ console.warn(`Sprite.playAnimation: animation '${name}' not found`);
136
+ return this;
137
+ }
138
+
139
+ // If already playing this animation and not forcing restart, do nothing
140
+ if (this._currentAnimation === name && this._isPlaying && !restart) {
141
+ return this;
142
+ }
143
+
144
+ // Switch to this animation's frames
145
+ this._currentAnimation = name;
146
+ this._frames = anim.frames;
147
+ this._loop = anim.loop;
148
+
149
+ // Use animation-specific frame rate if set
150
+ if (anim.frameRate !== null) {
151
+ this._frameRate = anim.frameRate;
152
+ this._frameDuration = 1 / this._frameRate;
153
+ }
154
+
155
+ // Reset and play
156
+ this._currentFrame = 0;
157
+ this._frameAccumulator = 0;
158
+ this._isPlaying = true;
159
+
160
+ return this;
161
+ }
162
+
163
+ /**
164
+ * Stops and switches to a named animation at frame 0
165
+ * @param {string} name - Animation name
166
+ * @returns {Sprite} This sprite for chaining
167
+ */
168
+ stopAnimation(name) {
169
+ const anim = this._animations.get(name);
170
+ if (!anim) {
171
+ console.warn(`Sprite.stopAnimation: animation '${name}' not found`);
172
+ return this;
173
+ }
174
+
175
+ this._currentAnimation = name;
176
+ this._frames = anim.frames;
177
+ this._loop = anim.loop;
178
+ this._currentFrame = 0;
179
+ this._frameAccumulator = 0;
180
+ this._isPlaying = false;
181
+
182
+ return this;
183
+ }
184
+
185
+ /**
186
+ * Gets the current animation name
187
+ * @returns {string|null}
188
+ */
189
+ get currentAnimationName() {
190
+ return this._currentAnimation;
191
+ }
192
+
193
+ /**
194
+ * Gets all animation names
195
+ * @returns {string[]}
196
+ */
197
+ get animationNames() {
198
+ return Array.from(this._animations.keys());
199
+ }
200
+
201
+ /**
202
+ * Checks if an animation exists
203
+ * @param {string} name - Animation name
204
+ * @returns {boolean}
205
+ */
206
+ hasAnimation(name) {
207
+ return this._animations.has(name);
208
+ }
209
+
210
+ /**
211
+ * Adds a Shape as a new frame to the timeline
212
+ * @param {Shape} shape - The Shape to add as a frame
213
+ * @returns {number} The frame index
214
+ */
215
+ addFrame(shape) {
216
+ if (!shape) {
217
+ throw new Error('Sprite.addFrame: shape is required');
218
+ }
219
+
220
+ // Set parent reference for transform hierarchy
221
+ shape.parent = this;
222
+
223
+ // Add to frames collection
224
+ this._frames.push(shape);
225
+
226
+ // Mark bounds as dirty since we added a new shape
227
+ this.markBoundsDirty();
228
+
229
+ return this._frames.length - 1;
230
+ }
231
+
232
+ /**
233
+ * Removes a frame at the specified index
234
+ * @param {number} index - The frame index to remove
235
+ * @returns {Shape|null} The removed shape, or null if index is invalid
236
+ */
237
+ removeFrame(index) {
238
+ if (index < 0 || index >= this._frames.length) {
239
+ return null;
240
+ }
241
+
242
+ const removed = this._frames.splice(index, 1)[0];
243
+ if (removed) {
244
+ removed.parent = null;
245
+ this.markBoundsDirty();
246
+
247
+ // Adjust current frame if necessary
248
+ if (this._currentFrame >= this._frames.length && this._frames.length > 0) {
249
+ this._currentFrame = this._frames.length - 1;
250
+ }
251
+ }
252
+
253
+ return removed;
254
+ }
255
+
256
+ /**
257
+ * Removes all frames from the timeline
258
+ */
259
+ clearFrames() {
260
+ this._frames.forEach(frame => {
261
+ frame.parent = null;
262
+ });
263
+ this._frames = [];
264
+ this._currentFrame = 0;
265
+ this.markBoundsDirty();
266
+ }
267
+
268
+ /**
269
+ * Gets the total number of frames
270
+ * @returns {number}
271
+ */
272
+ get totalFrames() {
273
+ return this._frames.length;
274
+ }
275
+
276
+ /**
277
+ * Gets the current frame index
278
+ * @returns {number}
279
+ */
280
+ get currentFrame() {
281
+ return this._currentFrame;
282
+ }
283
+
284
+ /**
285
+ * Gets the current frame Shape
286
+ * @returns {Shape|null}
287
+ */
288
+ get currentShape() {
289
+ return this._frames[this._currentFrame] || null;
290
+ }
291
+
292
+ /**
293
+ * Gets all frames
294
+ * @returns {Array<Shape>}
295
+ */
296
+ get frames() {
297
+ return this._frames;
298
+ }
299
+
300
+ /**
301
+ * Gets whether the sprite is currently playing
302
+ * @returns {boolean}
303
+ */
304
+ get isPlaying() {
305
+ return this._isPlaying;
306
+ }
307
+
308
+ /**
309
+ * Gets whether the sprite will loop
310
+ * @returns {boolean}
311
+ */
312
+ get loop() {
313
+ return this._loop;
314
+ }
315
+
316
+ /**
317
+ * Sets whether the sprite will loop
318
+ * @param {boolean} value
319
+ */
320
+ set loop(value) {
321
+ this._loop = value;
322
+ }
323
+
324
+ /**
325
+ * Gets the frame rate (frames per second)
326
+ * @returns {number}
327
+ */
328
+ get frameRate() {
329
+ return this._frameRate;
330
+ }
331
+
332
+ /**
333
+ * Sets the frame rate (frames per second)
334
+ * @param {number} value
335
+ */
336
+ set frameRate(value) {
337
+ if (value <= 0) {
338
+ throw new Error('Sprite.frameRate must be greater than 0');
339
+ }
340
+ this._frameRate = value;
341
+ this._frameDuration = 1 / value;
342
+ }
343
+
344
+ /**
345
+ * Starts playing the timeline
346
+ * @returns {Sprite} This sprite for chaining
347
+ */
348
+ play() {
349
+ this._isPlaying = true;
350
+ return this;
351
+ }
352
+
353
+ /**
354
+ * Pauses the timeline at the current frame
355
+ * @returns {Sprite} This sprite for chaining
356
+ */
357
+ pause() {
358
+ this._isPlaying = false;
359
+ return this;
360
+ }
361
+
362
+ /**
363
+ * Stops the timeline and resets to frame 0
364
+ * @returns {Sprite} This sprite for chaining
365
+ */
366
+ stop() {
367
+ this._isPlaying = false;
368
+ this._currentFrame = 0;
369
+ this._frameAccumulator = 0;
370
+ return this;
371
+ }
372
+
373
+ /**
374
+ * Rewinds to the first frame without stopping playback
375
+ * @returns {Sprite} This sprite for chaining
376
+ */
377
+ rewind() {
378
+ this._currentFrame = 0;
379
+ this._frameAccumulator = 0;
380
+ return this;
381
+ }
382
+
383
+ /**
384
+ * Jumps to a specific frame without affecting playback state
385
+ * @param {number} frame - The frame index to jump to
386
+ * @returns {Sprite} This sprite for chaining
387
+ */
388
+ goto(frame) {
389
+ if (this._frames.length === 0) {
390
+ return this;
391
+ }
392
+
393
+ // Clamp frame to valid range
394
+ this._currentFrame = Math.max(0, Math.min(frame, this._frames.length - 1));
395
+ this._frameAccumulator = 0;
396
+ return this;
397
+ }
398
+
399
+ /**
400
+ * Jumps to a specific frame and stops playback
401
+ * @param {number} frame - The frame index to jump to
402
+ * @returns {Sprite} This sprite for chaining
403
+ */
404
+ gotoAndStop(frame) {
405
+ this.goto(frame);
406
+ this.pause();
407
+ return this;
408
+ }
409
+
410
+ /**
411
+ * Jumps to a specific frame and starts playback
412
+ * @param {number} frame - The frame index to jump to
413
+ * @returns {Sprite} This sprite for chaining
414
+ */
415
+ gotoAndPlay(frame) {
416
+ this.goto(frame);
417
+ this.play();
418
+ return this;
419
+ }
420
+
421
+ /**
422
+ * Updates the sprite timeline
423
+ * @param {number} dt - Delta time in seconds
424
+ */
425
+ update(dt) {
426
+ super.update(dt);
427
+
428
+ if (!this._isPlaying || this._frames.length === 0) {
429
+ return;
430
+ }
431
+
432
+ // Accumulate time
433
+ this._frameAccumulator += dt;
434
+
435
+ // Advance frames if enough time has passed
436
+ while (this._frameAccumulator >= this._frameDuration) {
437
+ this._frameAccumulator -= this._frameDuration;
438
+ this._advanceFrame();
439
+ }
440
+
441
+ // Update current frame if it has an update method
442
+ const currentShape = this.currentShape;
443
+ if (currentShape && typeof currentShape.update === 'function') {
444
+ currentShape.update(dt);
445
+ }
446
+ }
447
+
448
+ /**
449
+ * Advances to the next frame
450
+ * @private
451
+ */
452
+ _advanceFrame() {
453
+ this._currentFrame++;
454
+
455
+ // Handle end of timeline
456
+ if (this._currentFrame >= this._frames.length) {
457
+ if (this._loop) {
458
+ this._currentFrame = 0;
459
+ } else {
460
+ this._currentFrame = this._frames.length - 1;
461
+ this._isPlaying = false;
462
+ }
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Renders the current frame
468
+ */
469
+ draw() {
470
+ super.draw();
471
+
472
+ const currentShape = this.currentShape;
473
+ if (!currentShape) {
474
+ return;
475
+ }
476
+
477
+ // Only render if the shape is visible
478
+ if (currentShape.visible === false) {
479
+ return;
480
+ }
481
+
482
+ // Render the current frame's shape
483
+ Painter.save();
484
+
485
+ // Apply shape's transform if needed (shapes handle their own transforms in render())
486
+ currentShape.render();
487
+
488
+ Painter.restore();
489
+ }
490
+
491
+ /**
492
+ * Calculates bounds based on all frames
493
+ * @returns {Object} Bounding box {x, y, width, height}
494
+ */
495
+ calculateBounds() {
496
+ if (this._frames.length === 0) {
497
+ return { x: this.x, y: this.y, width: 0, height: 0 };
498
+ }
499
+
500
+ // Calculate union of all frame bounds
501
+ let minX = Infinity;
502
+ let minY = Infinity;
503
+ let maxX = -Infinity;
504
+ let maxY = -Infinity;
505
+
506
+ this._frames.forEach(frame => {
507
+ const bounds = frame.getBounds();
508
+ minX = Math.min(minX, bounds.x);
509
+ minY = Math.min(minY, bounds.y);
510
+ maxX = Math.max(maxX, bounds.x + bounds.width);
511
+ maxY = Math.max(maxY, bounds.y + bounds.height);
512
+ });
513
+
514
+ return {
515
+ x: minX + this.x,
516
+ y: minY + this.y,
517
+ width: maxX - minX,
518
+ height: maxY - minY
519
+ };
520
+ }
521
+
522
+ /**
523
+ * Creates a string representation of the Sprite
524
+ * @returns {string}
525
+ */
526
+ toString() {
527
+ return `[Sprite frames=${this.totalFrames} current=${this.currentFrame} playing=${this.isPlaying}]`;
528
+ }
529
+ }
@@ -52,7 +52,6 @@ export class Pipeline extends Loggable {
52
52
  * @private
53
53
  */
54
54
  _hoverObject(obj, e) {
55
- //console.log("hoverObject", obj.constructor.name, obj.interactive, obj._hitTest != null);
56
55
  // Only applies to interactive objects with a shape and a _hitTest method.
57
56
  if (!obj.interactive || !obj._hitTest) return;
58
57
  const hit = obj._hitTest(e.x, e.y);
@@ -60,7 +59,6 @@ export class Pipeline extends Loggable {
60
59
  // Pointer entered this object
61
60
  obj._hovered = true;
62
61
  obj.events.emit("mouseover", e);
63
- //this.logger.log("Mouseover", obj, e.x, e.y);
64
62
  } else if (!hit && obj._hovered) {
65
63
  // Pointer left this object
66
64
  obj._hovered = false;
@@ -70,19 +68,25 @@ export class Pipeline extends Loggable {
70
68
 
71
69
  /**
72
70
  * Recursively checks all children of a Scene for hover state.
71
+ * Also checks if the Scene itself is interactive.
73
72
  * @param {Scene} scene - The scene whose children will be hover-tested.
74
73
  * @param {object} e - Event data containing pointer coordinates (e.x, e.y).
75
74
  * @private
76
75
  */
77
76
  _hoverScene(scene, e) {
78
- for (let i = scene.children.length - 1; i >= 0; i--) {
79
- const child = scene.children[i];
80
- if (child instanceof Scene) {
81
- this._hoverScene(child, e); // recurse into nested scenes
82
- } else {
83
- this._hoverObject(child, e); // hover actual objects
77
+ // First check children
78
+ if (scene.children && scene.children.length > 0) {
79
+ for (let i = scene.children.length - 1; i >= 0; i--) {
80
+ const child = scene.children[i];
81
+ if (child instanceof Scene) {
82
+ this._hoverScene(child, e); // recurse into nested scenes
83
+ } else {
84
+ this._hoverObject(child, e); // hover actual objects
85
+ }
84
86
  }
85
87
  }
88
+ // Also check the Scene itself for hover state
89
+ this._hoverObject(scene, e);
86
90
  }
87
91
 
88
92
  /**
@@ -97,9 +101,6 @@ export class Pipeline extends Loggable {
97
101
  // Check from topmost to bottommost object to find the first that was hit.
98
102
  for (let i = this.gameObjects.length - 1; i >= 0; i--) {
99
103
  const obj = this.gameObjects[i];
100
- if (type === "inputdown") {
101
- //this.logger.log("inputdown", obj);
102
- }
103
104
  if (obj instanceof Scene) {
104
105
  // If it's a Scene, see if any of its children were hit.
105
106
  if (this._dispatchToScene(obj, type, e)) {
@@ -130,7 +131,6 @@ export class Pipeline extends Loggable {
130
131
  // Check from topmost to bottommost for hover changes.
131
132
  for (let i = this.gameObjects.length - 1; i >= 0; i--) {
132
133
  const obj = this.gameObjects[i];
133
- //this.logger.log("Hover test for", obj, e.x, e.y);
134
134
  if (obj instanceof Scene) {
135
135
  this._hoverScene(obj, e);
136
136
  } else {
@@ -141,30 +141,34 @@ export class Pipeline extends Loggable {
141
141
 
142
142
  /**
143
143
  * Recursively dispatch an event to a Scene and possibly its nested child Scenes.
144
+ * Also checks if the Scene itself is interactive and was hit.
144
145
  * @param {Scene} scene - The scene to dispatch the event to.
145
146
  * @param {string} type - The type of pointer event ("inputdown", "inputup", etc).
146
147
  * @param {object} e - Event data with pointer coordinates.
147
- * @returns {boolean} True if the event was handled by a child, false otherwise.
148
+ * @returns {boolean} True if the event was handled by a child or the scene itself, false otherwise.
148
149
  * @private
149
150
  */
150
151
  _dispatchToScene(scene, type, e) {
151
- //if(type === "inputdown") this.logger.log("inputdown", scene);
152
+ // First check children (they render on top, so should get priority)
152
153
  for (let i = scene.children.length - 1; i >= 0; i--) {
153
154
  const child = scene.children[i];
154
155
  if (child instanceof Scene) {
155
156
  // Recurse deeper if child is also a Scene
156
157
  const hit = this._dispatchToScene(child, type, e);
157
158
  if (hit) {
158
- //if(type === "inputdown") this.logger.log("HIT", child, type);
159
159
  return true;
160
160
  }
161
161
  } else if (child.interactive && child._hitTest?.(e.x, e.y)) {
162
162
  // Found a child that was hit
163
- //if(type === "inputdown") this.logger.log("Dispatching to child", child, type);
164
163
  child.events.emit(type, e);
165
164
  return true;
166
165
  }
167
166
  }
167
+ // If no children handled the event, check if the Scene itself is interactive and hit
168
+ if (scene.interactive && scene._hitTest?.(e.x, e.y)) {
169
+ scene.events.emit(type, e);
170
+ return true;
171
+ }
168
172
  return false;
169
173
  }
170
174