@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.cjs CHANGED
@@ -899,12 +899,6 @@ function clearFontCache() {
899
899
 
900
900
  // src/entity.ts
901
901
  var import_matter_js3 = __toESM(require("matter-js"), 1);
902
- var MOUSE_FORCE = 1e-3;
903
- function applyMouseForce(entity, mouseX, grounded) {
904
- if (!grounded) return;
905
- const direction = Math.sign(mouseX - entity.position.x);
906
- import_matter_js3.default.Body.applyForce(entity, entity.position, { x: MOUSE_FORCE * direction, y: 0 });
907
- }
908
902
  function wrapHorizontal(entity, bounds) {
909
903
  if (entity.position.x < bounds.left) {
910
904
  import_matter_js3.default.Body.setPosition(entity, { x: bounds.right, y: entity.position.y });
@@ -1425,7 +1419,6 @@ var OverlayScene = class {
1425
1419
  this.objects = /* @__PURE__ */ new Map();
1426
1420
  this.boundaries = [];
1427
1421
  this.updateCallbacks = [];
1428
- this.mouseX = 0;
1429
1422
  this.animationFrameId = null;
1430
1423
  this.mouse = null;
1431
1424
  this.mouseConstraint = null;
@@ -1442,6 +1435,14 @@ var OverlayScene = class {
1442
1435
  this.floorSegmentPressure = /* @__PURE__ */ new Map();
1443
1436
  // segment index -> object IDs
1444
1437
  this.collapsedSegments = /* @__PURE__ */ new Set();
1438
+ // Lifecycle event callbacks
1439
+ this.lifecycleCallbacks = {
1440
+ objectSpawned: [],
1441
+ objectRemoved: [],
1442
+ objectCollision: []
1443
+ };
1444
+ // Follow targets for follow-{key} tagged objects
1445
+ this.followTargets = /* @__PURE__ */ new Map();
1445
1446
  /** Filter drag events - only allow grabbing objects with 'grabable' tag */
1446
1447
  this.handleStartDrag = (event) => {
1447
1448
  const body = event.body;
@@ -1491,17 +1492,48 @@ var OverlayScene = class {
1491
1492
  this.handleAfterRender = () => {
1492
1493
  this.backgroundManager.renderOverlay();
1493
1494
  };
1495
+ /**
1496
+ * Handler for Matter.js collision events.
1497
+ * Emits objectCollision lifecycle events.
1498
+ */
1499
+ this.handleCollisionStart = (event) => {
1500
+ for (const pair of event.pairs) {
1501
+ const entryA = this.findObjectByBody(pair.bodyA);
1502
+ const entryB = this.findObjectByBody(pair.bodyB);
1503
+ if (entryA && entryB) {
1504
+ this.emitLifecycleEvent(
1505
+ "objectCollision",
1506
+ this.toObjectState(entryA),
1507
+ this.toObjectState(entryB)
1508
+ );
1509
+ }
1510
+ }
1511
+ };
1494
1512
  // ==================== PRIVATE ====================
1495
1513
  this.loop = () => {
1496
1514
  this.effectManager.update();
1497
1515
  this.checkTTLExpiration();
1498
1516
  this.checkDespawnBelowFloor();
1499
1517
  this.updatePressure();
1500
- const mouseX = this.mouse?.position.x ?? this.mouseX;
1518
+ if (!this.followTargets.has("mouse") && this.mouse) {
1519
+ this.followTargets.set("mouse", { x: this.mouse.position.x, y: this.mouse.position.y });
1520
+ }
1501
1521
  for (const entry of this.objects.values()) {
1502
1522
  const isDragging = this.mouseConstraint?.body === entry.body;
1503
- if (!isDragging && entry.tags.includes("follow")) {
1504
- applyMouseForce(entry.body, mouseX, this.isGrounded(entry.body));
1523
+ if (!isDragging) {
1524
+ for (const tag of entry.tags) {
1525
+ const key = tag === "follow" ? "mouse" : tag.startsWith("follow-") ? tag.slice(7) : null;
1526
+ if (key) {
1527
+ const target = this.followTargets.get(key);
1528
+ if (target) {
1529
+ const grounded = this.isGrounded(entry.body);
1530
+ if (grounded) {
1531
+ const direction = Math.sign(target.x - entry.body.position.x);
1532
+ import_matter_js5.default.Body.applyForce(entry.body, entry.body.position, { x: 1e-3 * direction, y: 0 });
1533
+ }
1534
+ }
1535
+ }
1536
+ }
1505
1537
  }
1506
1538
  if (this.config.wrapHorizontal && entry.tags.includes("falling")) {
1507
1539
  wrapHorizontal(entry.body, this.config.bounds);
@@ -1575,6 +1607,7 @@ var OverlayScene = class {
1575
1607
  }
1576
1608
  import_matter_js5.default.Events.on(this.render, "beforeRender", this.handleBeforeRender);
1577
1609
  import_matter_js5.default.Events.on(this.render, "afterRender", this.handleAfterRender);
1610
+ import_matter_js5.default.Events.on(this.engine, "collisionStart", this.handleCollisionStart);
1578
1611
  }
1579
1612
  static createContainer(parent, options = {}) {
1580
1613
  const canvas = document.createElement("canvas");
@@ -1979,6 +2012,7 @@ var OverlayScene = class {
1979
2012
  this.canvas.removeEventListener("click", this.handleCanvasClick);
1980
2013
  import_matter_js5.default.Events.off(this.render, "beforeRender", this.handleBeforeRender);
1981
2014
  import_matter_js5.default.Events.off(this.render, "afterRender", this.handleAfterRender);
2015
+ import_matter_js5.default.Events.off(this.engine, "collisionStart", this.handleCollisionStart);
1982
2016
  import_matter_js5.default.Engine.clear(this.engine);
1983
2017
  this.objects.clear();
1984
2018
  this.obstaclePressure.clear();
@@ -1987,6 +2021,10 @@ var OverlayScene = class {
1987
2021
  this.floorSegmentPressure.clear();
1988
2022
  this.collapsedSegments.clear();
1989
2023
  this.updateCallbacks = [];
2024
+ this.lifecycleCallbacks.objectSpawned = [];
2025
+ this.lifecycleCallbacks.objectRemoved = [];
2026
+ this.lifecycleCallbacks.objectCollision = [];
2027
+ this.followTargets.clear();
1990
2028
  }
1991
2029
  setDebug(enabled) {
1992
2030
  this.config.debug = enabled;
@@ -2101,6 +2139,7 @@ var OverlayScene = class {
2101
2139
  if (isStatic && pressureThreshold !== void 0) {
2102
2140
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2103
2141
  }
2142
+ this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
2104
2143
  return id;
2105
2144
  }
2106
2145
  /**
@@ -2156,6 +2195,7 @@ var OverlayScene = class {
2156
2195
  if (isStatic && pressureThreshold !== void 0) {
2157
2196
  this.obstaclePressure.set(id, /* @__PURE__ */ new Set());
2158
2197
  }
2198
+ this.emitLifecycleEvent("objectSpawned", this.toObjectState(entry));
2159
2199
  return id;
2160
2200
  }
2161
2201
  /**
@@ -2237,6 +2277,7 @@ var OverlayScene = class {
2237
2277
  removeObject(id) {
2238
2278
  const entry = this.objects.get(id);
2239
2279
  if (!entry) return;
2280
+ this.emitLifecycleEvent("objectRemoved", this.toObjectState(entry));
2240
2281
  import_matter_js5.default.Composite.remove(this.engine.world, entry.body);
2241
2282
  this.objects.delete(id);
2242
2283
  }
@@ -2286,8 +2327,156 @@ var OverlayScene = class {
2286
2327
  }
2287
2328
  return Array.from(tagsSet).sort();
2288
2329
  }
2289
- setMousePosition(x, _y) {
2290
- this.mouseX = x;
2330
+ /**
2331
+ * Set the mouse position for follow behavior.
2332
+ * This overrides the browser mouse position for the 'follow' and 'follow-mouse' tags.
2333
+ * @deprecated Use setFollowTarget('mouse', x, y) instead
2334
+ */
2335
+ setMousePosition(x, y) {
2336
+ this.setFollowTarget("mouse", x, y);
2337
+ }
2338
+ /**
2339
+ * Set a follow target position. Objects with 'follow-{key}' tag will
2340
+ * automatically move toward this target each frame.
2341
+ * @param key - The target key (e.g., 'absolute' for 'follow-absolute' tag)
2342
+ * @param x - Target X position
2343
+ * @param y - Target Y position
2344
+ */
2345
+ setFollowTarget(key, x, y) {
2346
+ this.followTargets.set(key, { x, y });
2347
+ }
2348
+ /**
2349
+ * Remove a follow target. Objects with the corresponding tag will stop following.
2350
+ * @param key - The target key to remove
2351
+ */
2352
+ removeFollowTarget(key) {
2353
+ this.followTargets.delete(key);
2354
+ }
2355
+ /**
2356
+ * Get all registered follow target keys.
2357
+ * @returns Array of follow target keys
2358
+ */
2359
+ getFollowTargetKeys() {
2360
+ return Array.from(this.followTargets.keys());
2361
+ }
2362
+ // ==================== PHYSICS MANIPULATION METHODS ====================
2363
+ /**
2364
+ * Apply a force to an object.
2365
+ * @param objectId - The ID of the object
2366
+ * @param force - The force vector to apply
2367
+ */
2368
+ applyForce(objectId, force) {
2369
+ const entry = this.objects.get(objectId);
2370
+ if (!entry) return;
2371
+ import_matter_js5.default.Body.applyForce(entry.body, entry.body.position, force);
2372
+ }
2373
+ /**
2374
+ * Apply a force to all objects with a specific tag.
2375
+ * @param tag - The tag to match
2376
+ * @param force - The force vector to apply
2377
+ */
2378
+ applyForceToTag(tag, force) {
2379
+ for (const entry of this.objects.values()) {
2380
+ if (entry.tags.includes(tag)) {
2381
+ import_matter_js5.default.Body.applyForce(entry.body, entry.body.position, force);
2382
+ }
2383
+ }
2384
+ }
2385
+ /**
2386
+ * Set the velocity of an object.
2387
+ * @param objectId - The ID of the object
2388
+ * @param velocity - The velocity vector to set
2389
+ */
2390
+ setVelocity(objectId, velocity) {
2391
+ const entry = this.objects.get(objectId);
2392
+ if (!entry) return;
2393
+ import_matter_js5.default.Body.setVelocity(entry.body, velocity);
2394
+ }
2395
+ /**
2396
+ * Set the position of an object.
2397
+ * @param objectId - The ID of the object
2398
+ * @param position - The position to set
2399
+ */
2400
+ setPosition(objectId, position) {
2401
+ const entry = this.objects.get(objectId);
2402
+ if (!entry) return;
2403
+ import_matter_js5.default.Body.setPosition(entry.body, position);
2404
+ }
2405
+ // ==================== OBJECT STATE METHODS ====================
2406
+ /**
2407
+ * Get the current state of an object.
2408
+ * @param id - The ID of the object
2409
+ * @returns The object state, or null if not found
2410
+ */
2411
+ getObject(id) {
2412
+ const entry = this.objects.get(id);
2413
+ if (!entry) return null;
2414
+ return {
2415
+ id: entry.id,
2416
+ x: entry.body.position.x,
2417
+ y: entry.body.position.y,
2418
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2419
+ angle: entry.body.angle,
2420
+ tags: [...entry.tags]
2421
+ };
2422
+ }
2423
+ /**
2424
+ * Get the current state of all objects with a specific tag.
2425
+ * @param tag - The tag to match
2426
+ * @returns Array of object states
2427
+ */
2428
+ getObjectsByTag(tag) {
2429
+ const result = [];
2430
+ for (const entry of this.objects.values()) {
2431
+ if (entry.tags.includes(tag)) {
2432
+ result.push({
2433
+ id: entry.id,
2434
+ x: entry.body.position.x,
2435
+ y: entry.body.position.y,
2436
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2437
+ angle: entry.body.angle,
2438
+ tags: [...entry.tags]
2439
+ });
2440
+ }
2441
+ }
2442
+ return result;
2443
+ }
2444
+ // ==================== LIFECYCLE EVENTS ====================
2445
+ /**
2446
+ * Subscribe to a lifecycle event.
2447
+ * @param event - The event type to subscribe to
2448
+ * @param callback - The callback to invoke when the event occurs
2449
+ */
2450
+ on(event, callback) {
2451
+ this.lifecycleCallbacks[event].push(callback);
2452
+ }
2453
+ /**
2454
+ * Unsubscribe from a lifecycle event.
2455
+ * @param event - The event type to unsubscribe from
2456
+ * @param callback - The callback to remove
2457
+ */
2458
+ off(event, callback) {
2459
+ const arr = this.lifecycleCallbacks[event];
2460
+ const idx = arr.indexOf(callback);
2461
+ if (idx !== -1) arr.splice(idx, 1);
2462
+ }
2463
+ /** Create ObjectState from an ObjectEntry */
2464
+ toObjectState(entry) {
2465
+ return {
2466
+ id: entry.id,
2467
+ x: entry.body.position.x,
2468
+ y: entry.body.position.y,
2469
+ velocity: { x: entry.body.velocity.x, y: entry.body.velocity.y },
2470
+ angle: entry.body.angle,
2471
+ tags: [...entry.tags]
2472
+ };
2473
+ }
2474
+ /** Emit a lifecycle event to all registered callbacks */
2475
+ emitLifecycleEvent(event, ...args) {
2476
+ const callbacks = this.lifecycleCallbacks[event];
2477
+ for (const cb of callbacks) {
2478
+ cb(...args);
2479
+ }
2291
2480
  }
2292
2481
  // ==================== PRESSURE TRACKING METHODS ====================
2293
2482
  /**