@blorkfield/overlay-core 0.7.0 → 0.8.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/dist/index.d.cts CHANGED
@@ -196,6 +196,31 @@ interface DynamicObject {
196
196
  angle: number;
197
197
  tags: string[];
198
198
  }
199
+ /**
200
+ * Extended object state for querying and manipulation.
201
+ * Includes velocity data not present in DynamicObject.
202
+ */
203
+ interface ObjectState {
204
+ id: string;
205
+ x: number;
206
+ y: number;
207
+ velocity: {
208
+ x: number;
209
+ y: number;
210
+ };
211
+ angle: number;
212
+ tags: string[];
213
+ }
214
+ /**
215
+ * Lifecycle events that can be subscribed to.
216
+ */
217
+ type LifecycleEvent = 'objectSpawned' | 'objectRemoved' | 'objectCollision';
218
+ /**
219
+ * Callback type for lifecycle events.
220
+ * - objectSpawned/objectRemoved: receives the affected object
221
+ * - objectCollision: receives both colliding objects
222
+ */
223
+ type LifecycleCallback<T extends LifecycleEvent> = T extends 'objectCollision' ? (a: ObjectState, b: ObjectState) => void : (object: ObjectState) => void;
199
224
  interface UpdateCallbackData {
200
225
  /** All dynamic objects (objects with 'falling' tag) */
201
226
  objects: DynamicObject[];
@@ -509,7 +534,6 @@ declare class OverlayScene {
509
534
  private objects;
510
535
  private boundaries;
511
536
  private updateCallbacks;
512
- private mouseX;
513
537
  private config;
514
538
  private animationFrameId;
515
539
  private mouse;
@@ -525,6 +549,8 @@ declare class OverlayScene {
525
549
  private floorSegmentPressure;
526
550
  private collapsedSegments;
527
551
  private backgroundManager;
552
+ private lifecycleCallbacks;
553
+ private followTargets;
528
554
  static createContainer(parent: HTMLElement, options?: ContainerOptions): {
529
555
  canvas: HTMLCanvasElement;
530
556
  bounds: Bounds;
@@ -544,6 +570,11 @@ declare class OverlayScene {
544
570
  * Draws transparency/frosted glass layer after physics objects.
545
571
  */
546
572
  private handleAfterRender;
573
+ /**
574
+ * Handler for Matter.js collision events.
575
+ * Emits objectCollision lifecycle events.
576
+ */
577
+ private handleCollisionStart;
547
578
  /** Get a display name for an obstacle (letter char or short ID) */
548
579
  private getObstacleDisplayName;
549
580
  /** Update pressure tracking - check which dynamic objects rest on static obstacles */
@@ -638,7 +669,94 @@ declare class OverlayScene {
638
669
  * Get all unique tags currently in use by objects in the scene.
639
670
  */
640
671
  getAllTags(): string[];
641
- setMousePosition(x: number, _y: number): void;
672
+ /**
673
+ * Set the mouse position for follow behavior.
674
+ * This overrides the browser mouse position for the 'follow' and 'follow-mouse' tags.
675
+ * @deprecated Use setFollowTarget('mouse', x, y) instead
676
+ */
677
+ setMousePosition(x: number, y: number): void;
678
+ /**
679
+ * Set a follow target position. Objects with 'follow-{key}' tag will
680
+ * automatically move toward this target each frame.
681
+ * @param key - The target key (e.g., 'absolute' for 'follow-absolute' tag)
682
+ * @param x - Target X position
683
+ * @param y - Target Y position
684
+ */
685
+ setFollowTarget(key: string, x: number, y: number): void;
686
+ /**
687
+ * Remove a follow target. Objects with the corresponding tag will stop following.
688
+ * @param key - The target key to remove
689
+ */
690
+ removeFollowTarget(key: string): void;
691
+ /**
692
+ * Get all registered follow target keys.
693
+ * @returns Array of follow target keys
694
+ */
695
+ getFollowTargetKeys(): string[];
696
+ /**
697
+ * Apply a force to an object.
698
+ * @param objectId - The ID of the object
699
+ * @param force - The force vector to apply
700
+ */
701
+ applyForce(objectId: string, force: {
702
+ x: number;
703
+ y: number;
704
+ }): void;
705
+ /**
706
+ * Apply a force to all objects with a specific tag.
707
+ * @param tag - The tag to match
708
+ * @param force - The force vector to apply
709
+ */
710
+ applyForceToTag(tag: string, force: {
711
+ x: number;
712
+ y: number;
713
+ }): void;
714
+ /**
715
+ * Set the velocity of an object.
716
+ * @param objectId - The ID of the object
717
+ * @param velocity - The velocity vector to set
718
+ */
719
+ setVelocity(objectId: string, velocity: {
720
+ x: number;
721
+ y: number;
722
+ }): void;
723
+ /**
724
+ * Set the position of an object.
725
+ * @param objectId - The ID of the object
726
+ * @param position - The position to set
727
+ */
728
+ setPosition(objectId: string, position: {
729
+ x: number;
730
+ y: number;
731
+ }): void;
732
+ /**
733
+ * Get the current state of an object.
734
+ * @param id - The ID of the object
735
+ * @returns The object state, or null if not found
736
+ */
737
+ getObject(id: string): ObjectState | null;
738
+ /**
739
+ * Get the current state of all objects with a specific tag.
740
+ * @param tag - The tag to match
741
+ * @returns Array of object states
742
+ */
743
+ getObjectsByTag(tag: string): ObjectState[];
744
+ /**
745
+ * Subscribe to a lifecycle event.
746
+ * @param event - The event type to subscribe to
747
+ * @param callback - The callback to invoke when the event occurs
748
+ */
749
+ on<T extends LifecycleEvent>(event: T, callback: LifecycleCallback<T>): void;
750
+ /**
751
+ * Unsubscribe from a lifecycle event.
752
+ * @param event - The event type to unsubscribe from
753
+ * @param callback - The callback to remove
754
+ */
755
+ off<T extends LifecycleEvent>(event: T, callback: LifecycleCallback<T>): void;
756
+ /** Create ObjectState from an ObjectEntry */
757
+ private toObjectState;
758
+ /** Emit a lifecycle event to all registered callbacks */
759
+ private emitLifecycleEvent;
642
760
  /**
643
761
  * Get the current pressure (number of objects resting) on an obstacle.
644
762
  * @param obstacleId - The ID of the obstacle
@@ -967,4 +1085,4 @@ declare class BackgroundManager {
967
1085
  static clearCache(): void;
968
1086
  }
969
1087
 
970
- export { type BackgroundConfig, type BackgroundImageConfig, type BackgroundImageSizing, BackgroundManager, type BackgroundTransparencyConfig, type BaseEffectConfig, type Bounds, type BurstEffectConfig, type ClickToFallConfig, type ContainerOptions, type DespawnEffectConfig, type DynamicObject, type EffectConfig, type EffectObjectConfig, type EffectType, type FloorConfig, type FontInfo, type FontManifest, type GlyphData, type LoadedFont, type LogLevel, type ObjectConfig, OverlayScene, type OverlaySceneConfig, type PressureThresholdConfig, type RainEffectConfig, type ShadowConfig, type ShapeConfig, type ShapePreset, type StreamEffectConfig, type TTFTextObstacleConfig, type TextAlign, type TextBounds, type TextObstacleConfig, type TextObstacleResult, type UpdateCallback, type UpdateCallbackData, type WeightConfig, clearFontCache, getGlyphData, getKerning, getLogLevel, loadFont, logger, measureText, setLogLevel };
1088
+ export { type BackgroundConfig, type BackgroundImageConfig, type BackgroundImageSizing, BackgroundManager, type BackgroundTransparencyConfig, type BaseEffectConfig, type Bounds, type BurstEffectConfig, type ClickToFallConfig, type ContainerOptions, type DespawnEffectConfig, type DynamicObject, type EffectConfig, type EffectObjectConfig, type EffectType, type FloorConfig, type FontInfo, type FontManifest, type GlyphData, type LifecycleCallback, type LifecycleEvent, type LoadedFont, type LogLevel, type ObjectConfig, type ObjectState, OverlayScene, type OverlaySceneConfig, type PressureThresholdConfig, type RainEffectConfig, type ShadowConfig, type ShapeConfig, type ShapePreset, type StreamEffectConfig, type TTFTextObstacleConfig, type TextAlign, type TextBounds, type TextObstacleConfig, type TextObstacleResult, type UpdateCallback, type UpdateCallbackData, type WeightConfig, clearFontCache, getGlyphData, getKerning, getLogLevel, loadFont, logger, measureText, setLogLevel };
package/dist/index.d.ts CHANGED
@@ -196,6 +196,31 @@ interface DynamicObject {
196
196
  angle: number;
197
197
  tags: string[];
198
198
  }
199
+ /**
200
+ * Extended object state for querying and manipulation.
201
+ * Includes velocity data not present in DynamicObject.
202
+ */
203
+ interface ObjectState {
204
+ id: string;
205
+ x: number;
206
+ y: number;
207
+ velocity: {
208
+ x: number;
209
+ y: number;
210
+ };
211
+ angle: number;
212
+ tags: string[];
213
+ }
214
+ /**
215
+ * Lifecycle events that can be subscribed to.
216
+ */
217
+ type LifecycleEvent = 'objectSpawned' | 'objectRemoved' | 'objectCollision';
218
+ /**
219
+ * Callback type for lifecycle events.
220
+ * - objectSpawned/objectRemoved: receives the affected object
221
+ * - objectCollision: receives both colliding objects
222
+ */
223
+ type LifecycleCallback<T extends LifecycleEvent> = T extends 'objectCollision' ? (a: ObjectState, b: ObjectState) => void : (object: ObjectState) => void;
199
224
  interface UpdateCallbackData {
200
225
  /** All dynamic objects (objects with 'falling' tag) */
201
226
  objects: DynamicObject[];
@@ -509,7 +534,6 @@ declare class OverlayScene {
509
534
  private objects;
510
535
  private boundaries;
511
536
  private updateCallbacks;
512
- private mouseX;
513
537
  private config;
514
538
  private animationFrameId;
515
539
  private mouse;
@@ -525,6 +549,8 @@ declare class OverlayScene {
525
549
  private floorSegmentPressure;
526
550
  private collapsedSegments;
527
551
  private backgroundManager;
552
+ private lifecycleCallbacks;
553
+ private followTargets;
528
554
  static createContainer(parent: HTMLElement, options?: ContainerOptions): {
529
555
  canvas: HTMLCanvasElement;
530
556
  bounds: Bounds;
@@ -544,6 +570,11 @@ declare class OverlayScene {
544
570
  * Draws transparency/frosted glass layer after physics objects.
545
571
  */
546
572
  private handleAfterRender;
573
+ /**
574
+ * Handler for Matter.js collision events.
575
+ * Emits objectCollision lifecycle events.
576
+ */
577
+ private handleCollisionStart;
547
578
  /** Get a display name for an obstacle (letter char or short ID) */
548
579
  private getObstacleDisplayName;
549
580
  /** Update pressure tracking - check which dynamic objects rest on static obstacles */
@@ -638,7 +669,94 @@ declare class OverlayScene {
638
669
  * Get all unique tags currently in use by objects in the scene.
639
670
  */
640
671
  getAllTags(): string[];
641
- setMousePosition(x: number, _y: number): void;
672
+ /**
673
+ * Set the mouse position for follow behavior.
674
+ * This overrides the browser mouse position for the 'follow' and 'follow-mouse' tags.
675
+ * @deprecated Use setFollowTarget('mouse', x, y) instead
676
+ */
677
+ setMousePosition(x: number, y: number): void;
678
+ /**
679
+ * Set a follow target position. Objects with 'follow-{key}' tag will
680
+ * automatically move toward this target each frame.
681
+ * @param key - The target key (e.g., 'absolute' for 'follow-absolute' tag)
682
+ * @param x - Target X position
683
+ * @param y - Target Y position
684
+ */
685
+ setFollowTarget(key: string, x: number, y: number): void;
686
+ /**
687
+ * Remove a follow target. Objects with the corresponding tag will stop following.
688
+ * @param key - The target key to remove
689
+ */
690
+ removeFollowTarget(key: string): void;
691
+ /**
692
+ * Get all registered follow target keys.
693
+ * @returns Array of follow target keys
694
+ */
695
+ getFollowTargetKeys(): string[];
696
+ /**
697
+ * Apply a force to an object.
698
+ * @param objectId - The ID of the object
699
+ * @param force - The force vector to apply
700
+ */
701
+ applyForce(objectId: string, force: {
702
+ x: number;
703
+ y: number;
704
+ }): void;
705
+ /**
706
+ * Apply a force to all objects with a specific tag.
707
+ * @param tag - The tag to match
708
+ * @param force - The force vector to apply
709
+ */
710
+ applyForceToTag(tag: string, force: {
711
+ x: number;
712
+ y: number;
713
+ }): void;
714
+ /**
715
+ * Set the velocity of an object.
716
+ * @param objectId - The ID of the object
717
+ * @param velocity - The velocity vector to set
718
+ */
719
+ setVelocity(objectId: string, velocity: {
720
+ x: number;
721
+ y: number;
722
+ }): void;
723
+ /**
724
+ * Set the position of an object.
725
+ * @param objectId - The ID of the object
726
+ * @param position - The position to set
727
+ */
728
+ setPosition(objectId: string, position: {
729
+ x: number;
730
+ y: number;
731
+ }): void;
732
+ /**
733
+ * Get the current state of an object.
734
+ * @param id - The ID of the object
735
+ * @returns The object state, or null if not found
736
+ */
737
+ getObject(id: string): ObjectState | null;
738
+ /**
739
+ * Get the current state of all objects with a specific tag.
740
+ * @param tag - The tag to match
741
+ * @returns Array of object states
742
+ */
743
+ getObjectsByTag(tag: string): ObjectState[];
744
+ /**
745
+ * Subscribe to a lifecycle event.
746
+ * @param event - The event type to subscribe to
747
+ * @param callback - The callback to invoke when the event occurs
748
+ */
749
+ on<T extends LifecycleEvent>(event: T, callback: LifecycleCallback<T>): void;
750
+ /**
751
+ * Unsubscribe from a lifecycle event.
752
+ * @param event - The event type to unsubscribe from
753
+ * @param callback - The callback to remove
754
+ */
755
+ off<T extends LifecycleEvent>(event: T, callback: LifecycleCallback<T>): void;
756
+ /** Create ObjectState from an ObjectEntry */
757
+ private toObjectState;
758
+ /** Emit a lifecycle event to all registered callbacks */
759
+ private emitLifecycleEvent;
642
760
  /**
643
761
  * Get the current pressure (number of objects resting) on an obstacle.
644
762
  * @param obstacleId - The ID of the obstacle
@@ -967,4 +1085,4 @@ declare class BackgroundManager {
967
1085
  static clearCache(): void;
968
1086
  }
969
1087
 
970
- export { type BackgroundConfig, type BackgroundImageConfig, type BackgroundImageSizing, BackgroundManager, type BackgroundTransparencyConfig, type BaseEffectConfig, type Bounds, type BurstEffectConfig, type ClickToFallConfig, type ContainerOptions, type DespawnEffectConfig, type DynamicObject, type EffectConfig, type EffectObjectConfig, type EffectType, type FloorConfig, type FontInfo, type FontManifest, type GlyphData, type LoadedFont, type LogLevel, type ObjectConfig, OverlayScene, type OverlaySceneConfig, type PressureThresholdConfig, type RainEffectConfig, type ShadowConfig, type ShapeConfig, type ShapePreset, type StreamEffectConfig, type TTFTextObstacleConfig, type TextAlign, type TextBounds, type TextObstacleConfig, type TextObstacleResult, type UpdateCallback, type UpdateCallbackData, type WeightConfig, clearFontCache, getGlyphData, getKerning, getLogLevel, loadFont, logger, measureText, setLogLevel };
1088
+ export { type BackgroundConfig, type BackgroundImageConfig, type BackgroundImageSizing, BackgroundManager, type BackgroundTransparencyConfig, type BaseEffectConfig, type Bounds, type BurstEffectConfig, type ClickToFallConfig, type ContainerOptions, type DespawnEffectConfig, type DynamicObject, type EffectConfig, type EffectObjectConfig, type EffectType, type FloorConfig, type FontInfo, type FontManifest, type GlyphData, type LifecycleCallback, type LifecycleEvent, type LoadedFont, type LogLevel, type ObjectConfig, type ObjectState, OverlayScene, type OverlaySceneConfig, type PressureThresholdConfig, type RainEffectConfig, type ShadowConfig, type ShapeConfig, type ShapePreset, type StreamEffectConfig, type TTFTextObstacleConfig, type TextAlign, type TextBounds, type TextObstacleConfig, type TextObstacleResult, type UpdateCallback, type UpdateCallbackData, type WeightConfig, clearFontCache, getGlyphData, getKerning, getLogLevel, loadFont, logger, measureText, setLogLevel };
package/dist/index.js CHANGED
@@ -854,12 +854,6 @@ function clearFontCache() {
854
854
 
855
855
  // src/entity.ts
856
856
  import Matter3 from "matter-js";
857
- var MOUSE_FORCE = 1e-3;
858
- function applyMouseForce(entity, mouseX, grounded) {
859
- if (!grounded) return;
860
- const direction = Math.sign(mouseX - entity.position.x);
861
- Matter3.Body.applyForce(entity, entity.position, { x: MOUSE_FORCE * direction, y: 0 });
862
- }
863
857
  function wrapHorizontal(entity, bounds) {
864
858
  if (entity.position.x < bounds.left) {
865
859
  Matter3.Body.setPosition(entity, { x: bounds.right, y: entity.position.y });
@@ -1380,7 +1374,6 @@ var OverlayScene = class {
1380
1374
  this.objects = /* @__PURE__ */ new Map();
1381
1375
  this.boundaries = [];
1382
1376
  this.updateCallbacks = [];
1383
- this.mouseX = 0;
1384
1377
  this.animationFrameId = null;
1385
1378
  this.mouse = null;
1386
1379
  this.mouseConstraint = null;
@@ -1397,6 +1390,14 @@ var OverlayScene = class {
1397
1390
  this.floorSegmentPressure = /* @__PURE__ */ new Map();
1398
1391
  // segment index -> object IDs
1399
1392
  this.collapsedSegments = /* @__PURE__ */ new Set();
1393
+ // Lifecycle event callbacks
1394
+ this.lifecycleCallbacks = {
1395
+ objectSpawned: [],
1396
+ objectRemoved: [],
1397
+ objectCollision: []
1398
+ };
1399
+ // Follow targets for follow-{key} tagged objects
1400
+ this.followTargets = /* @__PURE__ */ new Map();
1400
1401
  /** Filter drag events - only allow grabbing objects with 'grabable' tag */
1401
1402
  this.handleStartDrag = (event) => {
1402
1403
  const body = event.body;
@@ -1446,17 +1447,48 @@ var OverlayScene = class {
1446
1447
  this.handleAfterRender = () => {
1447
1448
  this.backgroundManager.renderOverlay();
1448
1449
  };
1450
+ /**
1451
+ * Handler for Matter.js collision events.
1452
+ * Emits objectCollision lifecycle events.
1453
+ */
1454
+ this.handleCollisionStart = (event) => {
1455
+ for (const pair of event.pairs) {
1456
+ const entryA = this.findObjectByBody(pair.bodyA);
1457
+ const entryB = this.findObjectByBody(pair.bodyB);
1458
+ if (entryA && entryB) {
1459
+ this.emitLifecycleEvent(
1460
+ "objectCollision",
1461
+ this.toObjectState(entryA),
1462
+ this.toObjectState(entryB)
1463
+ );
1464
+ }
1465
+ }
1466
+ };
1449
1467
  // ==================== PRIVATE ====================
1450
1468
  this.loop = () => {
1451
1469
  this.effectManager.update();
1452
1470
  this.checkTTLExpiration();
1453
1471
  this.checkDespawnBelowFloor();
1454
1472
  this.updatePressure();
1455
- const mouseX = this.mouse?.position.x ?? this.mouseX;
1473
+ if (!this.followTargets.has("mouse") && this.mouse) {
1474
+ this.followTargets.set("mouse", { x: this.mouse.position.x, y: this.mouse.position.y });
1475
+ }
1456
1476
  for (const entry of this.objects.values()) {
1457
1477
  const isDragging = this.mouseConstraint?.body === entry.body;
1458
- if (!isDragging && entry.tags.includes("follow")) {
1459
- applyMouseForce(entry.body, mouseX, this.isGrounded(entry.body));
1478
+ if (!isDragging) {
1479
+ for (const tag of entry.tags) {
1480
+ const key = tag === "follow" ? "mouse" : tag.startsWith("follow-") ? tag.slice(7) : null;
1481
+ if (key) {
1482
+ const target = this.followTargets.get(key);
1483
+ if (target) {
1484
+ const grounded = this.isGrounded(entry.body);
1485
+ if (grounded) {
1486
+ const direction = Math.sign(target.x - entry.body.position.x);
1487
+ Matter5.Body.applyForce(entry.body, entry.body.position, { x: 1e-3 * direction, y: 0 });
1488
+ }
1489
+ }
1490
+ }
1491
+ }
1460
1492
  }
1461
1493
  if (this.config.wrapHorizontal && entry.tags.includes("falling")) {
1462
1494
  wrapHorizontal(entry.body, this.config.bounds);
@@ -1530,6 +1562,7 @@ var OverlayScene = class {
1530
1562
  }
1531
1563
  Matter5.Events.on(this.render, "beforeRender", this.handleBeforeRender);
1532
1564
  Matter5.Events.on(this.render, "afterRender", this.handleAfterRender);
1565
+ Matter5.Events.on(this.engine, "collisionStart", this.handleCollisionStart);
1533
1566
  }
1534
1567
  static createContainer(parent, options = {}) {
1535
1568
  const canvas = document.createElement("canvas");
@@ -1934,6 +1967,7 @@ var OverlayScene = class {
1934
1967
  this.canvas.removeEventListener("click", this.handleCanvasClick);
1935
1968
  Matter5.Events.off(this.render, "beforeRender", this.handleBeforeRender);
1936
1969
  Matter5.Events.off(this.render, "afterRender", this.handleAfterRender);
1970
+ Matter5.Events.off(this.engine, "collisionStart", this.handleCollisionStart);
1937
1971
  Matter5.Engine.clear(this.engine);
1938
1972
  this.objects.clear();
1939
1973
  this.obstaclePressure.clear();
@@ -1942,6 +1976,10 @@ var OverlayScene = class {
1942
1976
  this.floorSegmentPressure.clear();
1943
1977
  this.collapsedSegments.clear();
1944
1978
  this.updateCallbacks = [];
1979
+ this.lifecycleCallbacks.objectSpawned = [];
1980
+ this.lifecycleCallbacks.objectRemoved = [];
1981
+ this.lifecycleCallbacks.objectCollision = [];
1982
+ this.followTargets.clear();
1945
1983
  }
1946
1984
  setDebug(enabled) {
1947
1985
  this.config.debug = enabled;
@@ -2056,6 +2094,7 @@ var OverlayScene = class {
2056
2094
  if (isStatic && pressureThreshold !== void 0) {
2057
2095
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2058
2096
  }
2097
+ this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
2059
2098
  return id;
2060
2099
  }
2061
2100
  /**
@@ -2111,6 +2150,7 @@ var OverlayScene = class {
2111
2150
  if (isStatic && pressureThreshold !== void 0) {
2112
2151
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2113
2152
  }
2153
+ this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
2114
2154
  return id;
2115
2155
  }
2116
2156
  /**
@@ -2192,6 +2232,7 @@ var OverlayScene = class {
2192
2232
  removeObject(id) {
2193
2233
  const entry = this.objects.get(id);
2194
2234
  if (!entry) return;
2235
+ this.emitLifecycleEvent("objectRemoved", this.toObjectState(entry));
2195
2236
  Matter5.Composite.remove(this.engine.world, entry.body);
2196
2237
  this.objects.delete(id);
2197
2238
  }
@@ -2241,8 +2282,156 @@ var OverlayScene = class {
2241
2282
  }
2242
2283
  return Array.from(tagsSet).sort();
2243
2284
  }
2244
- setMousePosition(x, _y) {
2245
- this.mouseX = x;
2285
+ /**
2286
+ * Set the mouse position for follow behavior.
2287
+ * This overrides the browser mouse position for the 'follow' and 'follow-mouse' tags.
2288
+ * @deprecated Use setFollowTarget('mouse', x, y) instead
2289
+ */
2290
+ setMousePosition(x, y) {
2291
+ this.setFollowTarget("mouse", x, y);
2292
+ }
2293
+ /**
2294
+ * Set a follow target position. Objects with 'follow-{key}' tag will
2295
+ * automatically move toward this target each frame.
2296
+ * @param key - The target key (e.g., 'absolute' for 'follow-absolute' tag)
2297
+ * @param x - Target X position
2298
+ * @param y - Target Y position
2299
+ */
2300
+ setFollowTarget(key, x, y) {
2301
+ this.followTargets.set(key, { x, y });
2302
+ }
2303
+ /**
2304
+ * Remove a follow target. Objects with the corresponding tag will stop following.
2305
+ * @param key - The target key to remove
2306
+ */
2307
+ removeFollowTarget(key) {
2308
+ this.followTargets.delete(key);
2309
+ }
2310
+ /**
2311
+ * Get all registered follow target keys.
2312
+ * @returns Array of follow target keys
2313
+ */
2314
+ getFollowTargetKeys() {
2315
+ return Array.from(this.followTargets.keys());
2316
+ }
2317
+ // ==================== PHYSICS MANIPULATION METHODS ====================
2318
+ /**
2319
+ * Apply a force to an object.
2320
+ * @param objectId - The ID of the object
2321
+ * @param force - The force vector to apply
2322
+ */
2323
+ applyForce(objectId, force) {
2324
+ const entry = this.objects.get(objectId);
2325
+ if (!entry) return;
2326
+ Matter5.Body.applyForce(entry.body, entry.body.position, force);
2327
+ }
2328
+ /**
2329
+ * Apply a force to all objects with a specific tag.
2330
+ * @param tag - The tag to match
2331
+ * @param force - The force vector to apply
2332
+ */
2333
+ applyForceToTag(tag, force) {
2334
+ for (const entry of this.objects.values()) {
2335
+ if (entry.tags.includes(tag)) {
2336
+ Matter5.Body.applyForce(entry.body, entry.body.position, force);
2337
+ }
2338
+ }
2339
+ }
2340
+ /**
2341
+ * Set the velocity of an object.
2342
+ * @param objectId - The ID of the object
2343
+ * @param velocity - The velocity vector to set
2344
+ */
2345
+ setVelocity(objectId, velocity) {
2346
+ const entry = this.objects.get(objectId);
2347
+ if (!entry) return;
2348
+ Matter5.Body.setVelocity(entry.body, velocity);
2349
+ }
2350
+ /**
2351
+ * Set the position of an object.
2352
+ * @param objectId - The ID of the object
2353
+ * @param position - The position to set
2354
+ */
2355
+ setPosition(objectId, position) {
2356
+ const entry = this.objects.get(objectId);
2357
+ if (!entry) return;
2358
+ Matter5.Body.setPosition(entry.body, position);
2359
+ }
2360
+ // ==================== OBJECT STATE METHODS ====================
2361
+ /**
2362
+ * Get the current state of an object.
2363
+ * @param id - The ID of the object
2364
+ * @returns The object state, or null if not found
2365
+ */
2366
+ getObject(id) {
2367
+ const entry = this.objects.get(id);
2368
+ if (!entry) return null;
2369
+ return {
2370
+ id: entry.id,
2371
+ x: entry.body.position.x,
2372
+ y: entry.body.position.y,
2373
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2374
+ angle: entry.body.angle,
2375
+ tags: [...entry.tags]
2376
+ };
2377
+ }
2378
+ /**
2379
+ * Get the current state of all objects with a specific tag.
2380
+ * @param tag - The tag to match
2381
+ * @returns Array of object states
2382
+ */
2383
+ getObjectsByTag(tag) {
2384
+ const result = [];
2385
+ for (const entry of this.objects.values()) {
2386
+ if (entry.tags.includes(tag)) {
2387
+ result.push({
2388
+ id: entry.id,
2389
+ x: entry.body.position.x,
2390
+ y: entry.body.position.y,
2391
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2392
+ angle: entry.body.angle,
2393
+ tags: [...entry.tags]
2394
+ });
2395
+ }
2396
+ }
2397
+ return result;
2398
+ }
2399
+ // ==================== LIFECYCLE EVENTS ====================
2400
+ /**
2401
+ * Subscribe to a lifecycle event.
2402
+ * @param event - The event type to subscribe to
2403
+ * @param callback - The callback to invoke when the event occurs
2404
+ */
2405
+ on(event, callback) {
2406
+ this.lifecycleCallbacks[event].push(callback);
2407
+ }
2408
+ /**
2409
+ * Unsubscribe from a lifecycle event.
2410
+ * @param event - The event type to unsubscribe from
2411
+ * @param callback - The callback to remove
2412
+ */
2413
+ off(event, callback) {
2414
+ const arr = this.lifecycleCallbacks[event];
2415
+ const idx = arr.indexOf(callback);
2416
+ if (idx !== -1) arr.splice(idx, 1);
2417
+ }
2418
+ /** Create ObjectState from an ObjectEntry */
2419
+ toObjectState(entry) {
2420
+ return {
2421
+ id: entry.id,
2422
+ x: entry.body.position.x,
2423
+ y: entry.body.position.y,
2424
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2425
+ angle: entry.body.angle,
2426
+ tags: [...entry.tags]
2427
+ };
2428
+ }
2429
+ /** Emit a lifecycle event to all registered callbacks */
2430
+ emitLifecycleEvent(event, ...args) {
2431
+ const callbacks = this.lifecycleCallbacks[event];
2432
+ for (const cb of callbacks) {
2433
+ cb(...args);
2434
+ }
2246
2435
  }
2247
2436
  // ==================== PRESSURE TRACKING METHODS ====================
2248
2437
  /**