@rpgjs/server 5.0.0-alpha.16 → 5.0.0-alpha.18

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.js CHANGED
@@ -11298,6 +11298,11 @@ class RpgCommonPlayer {
11298
11298
  this._throughEvent = signal(false);
11299
11299
  this._frequency = signal(0);
11300
11300
  this._frames = signal([]);
11301
+ this.componentsTop = signal(null);
11302
+ this.componentsBottom = signal(null);
11303
+ this.componentsCenter = signal(null);
11304
+ this.componentsLeft = signal(null);
11305
+ this.componentsRight = signal(null);
11301
11306
  this.isConnected = signal(false);
11302
11307
  // Store intended movement direction (not synced, only used locally)
11303
11308
  this._intendedDirection = null;
@@ -11349,10 +11354,10 @@ __decorateClass$3([
11349
11354
  sync()
11350
11355
  ], RpgCommonPlayer.prototype, "type");
11351
11356
  __decorateClass$3([
11352
- persist()
11357
+ sync()
11353
11358
  ], RpgCommonPlayer.prototype, "x");
11354
11359
  __decorateClass$3([
11355
- persist()
11360
+ sync()
11356
11361
  ], RpgCommonPlayer.prototype, "y");
11357
11362
  __decorateClass$3([
11358
11363
  sync()
@@ -11423,11 +11428,95 @@ __decorateClass$3([
11423
11428
  __decorateClass$3([
11424
11429
  sync()
11425
11430
  ], RpgCommonPlayer.prototype, "_frames");
11431
+ __decorateClass$3([
11432
+ sync()
11433
+ ], RpgCommonPlayer.prototype, "componentsTop");
11434
+ __decorateClass$3([
11435
+ sync()
11436
+ ], RpgCommonPlayer.prototype, "componentsBottom");
11437
+ __decorateClass$3([
11438
+ sync()
11439
+ ], RpgCommonPlayer.prototype, "componentsCenter");
11440
+ __decorateClass$3([
11441
+ sync()
11442
+ ], RpgCommonPlayer.prototype, "componentsLeft");
11443
+ __decorateClass$3([
11444
+ sync()
11445
+ ], RpgCommonPlayer.prototype, "componentsRight");
11426
11446
  __decorateClass$3([
11427
11447
  connected()
11428
11448
  ], RpgCommonPlayer.prototype, "isConnected");
11429
11449
 
11430
- const _Vector2 = class _Vector2 {
11450
+ class RpgShape {
11451
+ /**
11452
+ * Creates a new RpgShape instance
11453
+ *
11454
+ * @param config - Shape configuration
11455
+ */
11456
+ constructor(config) {
11457
+ this.name = config.name;
11458
+ this.positioning = config.positioning;
11459
+ this.width = config.width;
11460
+ this.height = config.height;
11461
+ this.x = config.x;
11462
+ this.y = config.y;
11463
+ this.properties = config.properties;
11464
+ this._playerOwner = config.playerOwner;
11465
+ this._physicZoneId = config.physicZoneId;
11466
+ this._map = config.map;
11467
+ }
11468
+ /**
11469
+ * Checks if a player is currently inside this shape
11470
+ *
11471
+ * @param player - The player to check
11472
+ * @returns True if the player is inside the shape
11473
+ *
11474
+ * @example
11475
+ * ```ts
11476
+ * const shape = player.attachShape("detection", { radius: 100 });
11477
+ * if (shape.playerIsIn(otherPlayer)) {
11478
+ * console.log("Player detected in zone");
11479
+ * }
11480
+ * ```
11481
+ */
11482
+ playerIsIn(player) {
11483
+ if (!this._map) return false;
11484
+ const zoneManager = this._map.physic.getZoneManager();
11485
+ const entities = zoneManager.getEntitiesInZone(this._physicZoneId);
11486
+ if (!entities || entities.length === 0) return false;
11487
+ const playerEntity = this._map.physic.getEntityByUUID(player.id);
11488
+ if (!playerEntity) return false;
11489
+ return entities.some((entity) => entity.uuid === playerEntity.uuid);
11490
+ }
11491
+ /**
11492
+ * Gets the player that owns this shape
11493
+ *
11494
+ * Returns the player on which `attachShape()` was called to create this shape.
11495
+ *
11496
+ * @returns The player owner or undefined if not available
11497
+ *
11498
+ * @example
11499
+ * ```ts
11500
+ * const shape = player.attachShape("vision", { radius: 150 });
11501
+ * const owner = shape.getPlayerOwner();
11502
+ * console.log(owner?.name); // Player's name
11503
+ * ```
11504
+ */
11505
+ getPlayerOwner() {
11506
+ return this._playerOwner;
11507
+ }
11508
+ /**
11509
+ * Updates the shape's position
11510
+ *
11511
+ * @internal
11512
+ */
11513
+ _updatePosition(x, y) {
11514
+ this.x = x;
11515
+ this.y = y;
11516
+ }
11517
+ }
11518
+
11519
+ const _Vector2$1 = class _Vector2 {
11431
11520
  /**
11432
11521
  * Creates a new Vector2
11433
11522
  *
@@ -11697,10 +11786,10 @@ const _Vector2 = class _Vector2 {
11697
11786
  return `Vector2(${this.x}, ${this.y})`;
11698
11787
  }
11699
11788
  };
11700
- _Vector2.ZERO = new _Vector2(0, 0);
11701
- _Vector2.UNIT_X = new _Vector2(1, 0);
11702
- _Vector2.UNIT_Y = new _Vector2(0, 1);
11703
- let Vector2 = _Vector2;
11789
+ _Vector2$1.ZERO = new _Vector2$1(0, 0);
11790
+ _Vector2$1.UNIT_X = new _Vector2$1(1, 0);
11791
+ _Vector2$1.UNIT_Y = new _Vector2$1(0, 1);
11792
+ let Vector2$1 = _Vector2$1;
11704
11793
 
11705
11794
  class AABB {
11706
11795
  /**
@@ -11790,7 +11879,7 @@ class AABB {
11790
11879
  * @returns Center point
11791
11880
  */
11792
11881
  getCenter() {
11793
- return new Vector2(
11882
+ return new Vector2$1(
11794
11883
  (this.minX + this.maxX) / 2,
11795
11884
  (this.minY + this.maxY) / 2
11796
11885
  );
@@ -11968,20 +12057,20 @@ class AABB {
11968
12057
  * @returns Clamped point
11969
12058
  */
11970
12059
  clamp(point) {
11971
- return new Vector2(
12060
+ return new Vector2$1(
11972
12061
  Math.max(this.minX, Math.min(this.maxX, point.x)),
11973
12062
  Math.max(this.minY, Math.min(this.maxY, point.y))
11974
12063
  );
11975
12064
  }
11976
12065
  }
11977
12066
 
11978
- var EntityState = /* @__PURE__ */ ((EntityState2) => {
12067
+ var EntityState$1 = /* @__PURE__ */ ((EntityState2) => {
11979
12068
  EntityState2[EntityState2["Static"] = 1] = "Static";
11980
12069
  EntityState2[EntityState2["Dynamic"] = 2] = "Dynamic";
11981
12070
  EntityState2[EntityState2["Sleeping"] = 4] = "Sleeping";
11982
12071
  EntityState2[EntityState2["Kinematic"] = 8] = "Kinematic";
11983
12072
  return EntityState2;
11984
- })(EntityState || {});
12073
+ })(EntityState$1 || {});
11985
12074
 
11986
12075
  function generateUUID() {
11987
12076
  return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
@@ -12004,20 +12093,20 @@ class Entity {
12004
12093
  constructor(config = {}) {
12005
12094
  this.lastCardinalDirection = "idle";
12006
12095
  this.uuid = config.uuid ?? generateUUID();
12007
- if (config.position instanceof Vector2) {
12096
+ if (config.position instanceof Vector2$1) {
12008
12097
  this.position = config.position.clone();
12009
12098
  } else if (config.position) {
12010
- this.position = new Vector2(config.position.x, config.position.y);
12099
+ this.position = new Vector2$1(config.position.x, config.position.y);
12011
12100
  } else {
12012
- this.position = new Vector2(0, 0);
12101
+ this.position = new Vector2$1(0, 0);
12013
12102
  }
12014
- this.currentTile = new Vector2(0, 0);
12015
- if (config.velocity instanceof Vector2) {
12103
+ this.currentTile = new Vector2$1(0, 0);
12104
+ if (config.velocity instanceof Vector2$1) {
12016
12105
  this.velocity = config.velocity.clone();
12017
12106
  } else if (config.velocity) {
12018
- this.velocity = new Vector2(config.velocity.x, config.velocity.y);
12107
+ this.velocity = new Vector2$1(config.velocity.x, config.velocity.y);
12019
12108
  } else {
12020
- this.velocity = new Vector2(0, 0);
12109
+ this.velocity = new Vector2$1(0, 0);
12021
12110
  }
12022
12111
  this.rotation = config.rotation ?? 0;
12023
12112
  this.angularVelocity = config.angularVelocity ?? 0;
@@ -12030,14 +12119,14 @@ class Entity {
12030
12119
  this.capsule = config.capsule;
12031
12120
  }
12032
12121
  this.continuous = config.continuous ?? false;
12033
- this.state = config.state ?? EntityState.Dynamic;
12122
+ this.state = config.state ?? EntityState$1.Dynamic;
12034
12123
  this.restitution = config.restitution ?? 0.2;
12035
12124
  this.friction = config.friction ?? 0.3;
12036
12125
  this.linearDamping = config.linearDamping ?? 0.01;
12037
12126
  this.angularDamping = config.angularDamping ?? 0.01;
12038
12127
  this.maxLinearVelocity = config.maxLinearVelocity ?? Infinity;
12039
12128
  this.maxAngularVelocity = config.maxAngularVelocity ?? Infinity;
12040
- this.force = new Vector2(0, 0);
12129
+ this.force = new Vector2$1(0, 0);
12041
12130
  this.torque = 0;
12042
12131
  this.collisionMask = config.collisionMask ?? 4294967295;
12043
12132
  this.collisionCategory = config.collisionCategory ?? 1;
@@ -12172,7 +12261,7 @@ class Entity {
12172
12261
  */
12173
12262
  notifyDirectionChange() {
12174
12263
  const isMoving = this.velocity.lengthSquared() > DIRECTION_CHANGE_THRESHOLD_SQ;
12175
- const direction = isMoving ? this.velocity.clone().normalize() : new Vector2(0, 0);
12264
+ const direction = isMoving ? this.velocity.clone().normalize() : new Vector2$1(0, 0);
12176
12265
  const cardinalDirection = this.computeCardinalDirection(direction);
12177
12266
  if (cardinalDirection !== "idle") {
12178
12267
  this.lastCardinalDirection = cardinalDirection;
@@ -12415,7 +12504,7 @@ class Entity {
12415
12504
  * @returns This entity for chaining
12416
12505
  */
12417
12506
  teleport(position) {
12418
- if (position instanceof Vector2) {
12507
+ if (position instanceof Vector2$1) {
12419
12508
  this.position.copyFrom(position);
12420
12509
  } else {
12421
12510
  this.position.set(position.x, position.y);
@@ -12432,14 +12521,14 @@ class Entity {
12432
12521
  */
12433
12522
  setVelocity(velocity) {
12434
12523
  const oldVelocity = this.velocity.clone();
12435
- if (velocity instanceof Vector2) {
12524
+ if (velocity instanceof Vector2$1) {
12436
12525
  this.velocity.copyFrom(velocity);
12437
12526
  } else {
12438
12527
  this.velocity.set(velocity.x, velocity.y);
12439
12528
  }
12440
12529
  this.wakeUp();
12441
- const oldDirection = oldVelocity.lengthSquared() > MOVEMENT_EPSILON_SQ ? oldVelocity.clone().normalize() : new Vector2(0, 0);
12442
- const newDirection = this.velocity.lengthSquared() > MOVEMENT_EPSILON_SQ ? this.velocity.clone().normalize() : new Vector2(0, 0);
12530
+ const oldDirection = oldVelocity.lengthSquared() > MOVEMENT_EPSILON_SQ ? oldVelocity.clone().normalize() : new Vector2$1(0, 0);
12531
+ const newDirection = this.velocity.lengthSquared() > MOVEMENT_EPSILON_SQ ? this.velocity.clone().normalize() : new Vector2$1(0, 0);
12443
12532
  const oldCardinal = this.computeCardinalDirection(oldDirection);
12444
12533
  const newCardinal = this.computeCardinalDirection(newDirection);
12445
12534
  if (oldCardinal !== newCardinal || Math.abs(oldDirection.dot(newDirection) - 1) > 0.01) {
@@ -12454,7 +12543,7 @@ class Entity {
12454
12543
  * @returns This entity for chaining
12455
12544
  */
12456
12545
  freeze() {
12457
- this.state = EntityState.Static;
12546
+ this.state = EntityState$1.Static;
12458
12547
  this.velocity.set(0, 0);
12459
12548
  this.angularVelocity = 0;
12460
12549
  this.force.set(0, 0);
@@ -12469,7 +12558,7 @@ class Entity {
12469
12558
  */
12470
12559
  unfreeze() {
12471
12560
  if (this.mass > 0) {
12472
- this.state = EntityState.Dynamic;
12561
+ this.state = EntityState$1.Dynamic;
12473
12562
  }
12474
12563
  return this;
12475
12564
  }
@@ -12480,7 +12569,7 @@ class Entity {
12480
12569
  */
12481
12570
  sleep() {
12482
12571
  if (!this.isStatic()) {
12483
- this.state |= EntityState.Sleeping;
12572
+ this.state |= EntityState$1.Sleeping;
12484
12573
  this.velocity.set(0, 0);
12485
12574
  this.angularVelocity = 0;
12486
12575
  this.force.set(0, 0);
@@ -12495,7 +12584,7 @@ class Entity {
12495
12584
  * @returns This entity for chaining
12496
12585
  */
12497
12586
  wakeUp() {
12498
- this.state &= ~EntityState.Sleeping;
12587
+ this.state &= ~EntityState$1.Sleeping;
12499
12588
  this.timeSinceMovement = 0;
12500
12589
  return this;
12501
12590
  }
@@ -12510,7 +12599,7 @@ class Entity {
12510
12599
  * @returns True if static
12511
12600
  */
12512
12601
  isStatic() {
12513
- return (this.state & EntityState.Static) !== 0 || this.invMass === 0;
12602
+ return (this.state & EntityState$1.Static) !== 0 || this.invMass === 0;
12514
12603
  }
12515
12604
  /**
12516
12605
  * Checks if the entity is dynamic
@@ -12518,7 +12607,7 @@ class Entity {
12518
12607
  * @returns True if dynamic
12519
12608
  */
12520
12609
  isDynamic() {
12521
- return (this.state & EntityState.Dynamic) !== 0 && this.mass > 0;
12610
+ return (this.state & EntityState$1.Dynamic) !== 0 && this.mass > 0;
12522
12611
  }
12523
12612
  /**
12524
12613
  * Checks if the entity is sleeping
@@ -12526,7 +12615,7 @@ class Entity {
12526
12615
  * @returns True if sleeping
12527
12616
  */
12528
12617
  isSleeping() {
12529
- return (this.state & EntityState.Sleeping) !== 0;
12618
+ return (this.state & EntityState$1.Sleeping) !== 0;
12530
12619
  }
12531
12620
  /**
12532
12621
  * Checks if the entity is kinematic
@@ -12534,7 +12623,7 @@ class Entity {
12534
12623
  * @returns True if kinematic
12535
12624
  */
12536
12625
  isKinematic() {
12537
- return (this.state & EntityState.Kinematic) !== 0;
12626
+ return (this.state & EntityState$1.Kinematic) !== 0;
12538
12627
  }
12539
12628
  /**
12540
12629
  * Resets accumulated forces and torques
@@ -12739,7 +12828,7 @@ class Integrator {
12739
12828
  */
12740
12829
  constructor(config) {
12741
12830
  this.config = config;
12742
- this.gravity = config.gravity?.clone() ?? new Vector2(0, 0);
12831
+ this.gravity = config.gravity?.clone() ?? new Vector2$1(0, 0);
12743
12832
  }
12744
12833
  /**
12745
12834
  * Integrates an entity's motion
@@ -12949,10 +13038,10 @@ class AABBCollider {
12949
13038
  let depth;
12950
13039
  if (overlapX < overlapY) {
12951
13040
  depth = overlapX;
12952
- normal = aabbA.getCenterX() < aabbB.getCenterX() ? new Vector2(1, 0) : new Vector2(-1, 0);
13041
+ normal = aabbA.getCenterX() < aabbB.getCenterX() ? new Vector2$1(1, 0) : new Vector2$1(-1, 0);
12953
13042
  } else {
12954
13043
  depth = overlapY;
12955
- normal = aabbA.getCenterY() < aabbB.getCenterY() ? new Vector2(0, 1) : new Vector2(0, -1);
13044
+ normal = aabbA.getCenterY() < aabbB.getCenterY() ? new Vector2$1(0, 1) : new Vector2$1(0, -1);
12956
13045
  }
12957
13046
  const contactPoint = intersection.getCenter();
12958
13047
  return {
@@ -13006,7 +13095,7 @@ class AABBCollider {
13006
13095
  const t = tNear < 0 ? tFar : tNear;
13007
13096
  if (t < 0) return null;
13008
13097
  const point = ray.getPoint(t);
13009
- let normal = new Vector2(0, 0);
13098
+ let normal = new Vector2$1(0, 0);
13010
13099
  if (Math.abs(point.x - bounds.minX) < 1e-5) normal.set(-1, 0);
13011
13100
  else if (Math.abs(point.x - bounds.maxX) < 1e-5) normal.set(1, 0);
13012
13101
  else if (Math.abs(point.y - bounds.minY) < 1e-5) normal.set(0, -1);
@@ -13088,7 +13177,7 @@ class CircleCollider {
13088
13177
  const depth = minDistance - distance;
13089
13178
  let normal;
13090
13179
  if (distance < 1e-5) {
13091
- normal = new Vector2(1, 0);
13180
+ normal = new Vector2$1(1, 0);
13092
13181
  } else {
13093
13182
  normal = centerB.sub(centerA).normalize();
13094
13183
  }
@@ -13133,18 +13222,18 @@ class CircleCollider {
13133
13222
  const distToTop = aabb.maxY - circleCenter.y;
13134
13223
  const minDist = Math.min(distToLeft, distToRight, distToBottom, distToTop);
13135
13224
  if (minDist === distToLeft) {
13136
- normal = new Vector2(-1, 0);
13225
+ normal = new Vector2$1(-1, 0);
13137
13226
  } else if (minDist === distToRight) {
13138
- normal = new Vector2(1, 0);
13227
+ normal = new Vector2$1(1, 0);
13139
13228
  } else if (minDist === distToBottom) {
13140
- normal = new Vector2(0, -1);
13229
+ normal = new Vector2$1(0, -1);
13141
13230
  } else {
13142
- normal = new Vector2(0, 1);
13231
+ normal = new Vector2$1(0, 1);
13143
13232
  }
13144
13233
  } else {
13145
- normal = new Vector2(closestX - circleCenter.x, closestY - circleCenter.y).normalize();
13234
+ normal = new Vector2$1(closestX - circleCenter.x, closestY - circleCenter.y).normalize();
13146
13235
  }
13147
- const contactPoint = new Vector2(closestX, closestY);
13236
+ const contactPoint = new Vector2$1(closestX, closestY);
13148
13237
  return {
13149
13238
  entityA: this.entity,
13150
13239
  entityB: other.getEntity(),
@@ -13233,7 +13322,7 @@ class CapsuleCollider {
13233
13322
  const t = tNear < 0 ? tFar : tNear;
13234
13323
  if (t < 0) return null;
13235
13324
  const point = ray.getPoint(t);
13236
- let normal = new Vector2(0, 0);
13325
+ let normal = new Vector2$1(0, 0);
13237
13326
  if (Math.abs(point.x - bounds.minX) < 1e-5) normal.set(-1, 0);
13238
13327
  else if (Math.abs(point.x - bounds.maxX) < 1e-5) normal.set(1, 0);
13239
13328
  else if (Math.abs(point.y - bounds.minY) < 1e-5) normal.set(0, -1);
@@ -13270,8 +13359,8 @@ class CapsuleCollider {
13270
13359
  const pos = this.entity.position;
13271
13360
  const halfSegment = Math.max(0, height / 2 - radius);
13272
13361
  return {
13273
- a: new Vector2(pos.x, pos.y - halfSegment),
13274
- b: new Vector2(pos.x, pos.y + halfSegment)
13362
+ a: new Vector2$1(pos.x, pos.y - halfSegment),
13363
+ b: new Vector2$1(pos.x, pos.y + halfSegment)
13275
13364
  };
13276
13365
  }
13277
13366
  testCircle(circle) {
@@ -13286,7 +13375,7 @@ class CapsuleCollider {
13286
13375
  return null;
13287
13376
  }
13288
13377
  const dist = Math.sqrt(distSq);
13289
- const normal = dist > 0 ? circleCenter.sub(closest).normalize() : new Vector2(1, 0);
13378
+ const normal = dist > 0 ? circleCenter.sub(closest).normalize() : new Vector2$1(1, 0);
13290
13379
  const depth = minDist - dist;
13291
13380
  return {
13292
13381
  entityA: this.entity,
@@ -13318,7 +13407,7 @@ class CapsuleCollider {
13318
13407
  return null;
13319
13408
  }
13320
13409
  const dist = Math.sqrt(distSq);
13321
- const normal = dist > 0 ? closestOnBox.sub(closestOnSeg).normalize() : new Vector2(0, 1);
13410
+ const normal = dist > 0 ? closestOnBox.sub(closestOnSeg).normalize() : new Vector2$1(0, 1);
13322
13411
  const depth = capRadius - dist;
13323
13412
  return {
13324
13413
  entityA: this.entity,
@@ -13344,7 +13433,7 @@ class CapsuleCollider {
13344
13433
  return null;
13345
13434
  }
13346
13435
  const dist = Math.sqrt(distSq);
13347
- const normal = dist > 0 ? p2.sub(p1).normalize() : new Vector2(1, 0);
13436
+ const normal = dist > 0 ? p2.sub(p1).normalize() : new Vector2$1(1, 0);
13348
13437
  const depth = minDist - dist;
13349
13438
  return {
13350
13439
  entityA: this.entity,
@@ -13488,12 +13577,12 @@ class PolygonCollider {
13488
13577
  const b = other.getBounds();
13489
13578
  const halfW = (b.maxX - b.minX) / 2;
13490
13579
  const halfH = (b.maxY - b.minY) / 2;
13491
- const center = new Vector2((b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2);
13580
+ const center = new Vector2$1((b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2);
13492
13581
  const aabbPolyLocal = [
13493
- new Vector2(-halfW, -halfH),
13494
- new Vector2(halfW, -halfH),
13495
- new Vector2(halfW, halfH),
13496
- new Vector2(-halfW, halfH)
13582
+ new Vector2$1(-halfW, -halfH),
13583
+ new Vector2$1(halfW, -halfH),
13584
+ new Vector2$1(halfW, halfH),
13585
+ new Vector2$1(-halfW, halfH)
13497
13586
  ];
13498
13587
  const tempEntity = new Entity({ position: center, rotation: 0, mass: 0 });
13499
13588
  entityToPolygonConfig.set(tempEntity, { vertices: aabbPolyLocal, isConvex: true });
@@ -13529,7 +13618,7 @@ class PolygonCollider {
13529
13618
  if (!closestPoint) continue;
13530
13619
  const dist = Math.sqrt(minDistSq);
13531
13620
  if (dist <= r) {
13532
- const normal = dist > 1e-6 ? center.sub(closestPoint).normalize() : new Vector2(1, 0);
13621
+ const normal = dist > 1e-6 ? center.sub(closestPoint).normalize() : new Vector2$1(1, 0);
13533
13622
  const depth = r - dist;
13534
13623
  const info = {
13535
13624
  entityA: this.entity,
@@ -13637,12 +13726,12 @@ class PolygonCollider {
13637
13726
  const t = ((v1.x - v3.x) * (v3.y - v4.y) - (v1.y - v3.y) * (v3.x - v4.x)) / den;
13638
13727
  const u = -((v1.x - v2.x) * (v1.y - v3.y) - (v1.y - v2.y) * (v1.x - v3.x)) / den;
13639
13728
  if (t >= 0 && t <= ray.length && u >= 0 && u <= 1) {
13640
- const point = new Vector2(
13729
+ const point = new Vector2$1(
13641
13730
  v1.x + t * (v2.x - v1.x),
13642
13731
  v1.y + t * (v2.y - v1.y)
13643
13732
  );
13644
13733
  const segmentDir = p2.sub(p1).normalize();
13645
- let normal = new Vector2(-segmentDir.y, segmentDir.x);
13734
+ let normal = new Vector2$1(-segmentDir.y, segmentDir.x);
13646
13735
  if (normal.dot(ray.direction) > 0) {
13647
13736
  normal = normal.mul(-1);
13648
13737
  }
@@ -13667,7 +13756,7 @@ class PolygonCollider {
13667
13756
  for (let i = 0; i < part.length; i++) {
13668
13757
  const v = part[i];
13669
13758
  if (!v) continue;
13670
- w[i] = new Vector2(v.x * cos - v.y * sin + px, v.x * sin + v.y * cos + py);
13759
+ w[i] = new Vector2$1(v.x * cos - v.y * sin + px, v.x * sin + v.y * cos + py);
13671
13760
  }
13672
13761
  worldParts.push(w);
13673
13762
  }
@@ -13687,7 +13776,7 @@ function gatherSATAxes(a, b) {
13687
13776
  const p1 = p[(i + 1) % p.length];
13688
13777
  if (!p0 || !p1) return;
13689
13778
  const edge = p1.sub(p0);
13690
- const axis = new Vector2(-edge.y, edge.x).normalize();
13779
+ const axis = new Vector2$1(-edge.y, edge.x).normalize();
13691
13780
  axes.push(axis);
13692
13781
  };
13693
13782
  for (let i = 0; i < a.length; i++) pushAxis(a, i);
@@ -13715,7 +13804,7 @@ function polygonCentroid(poly) {
13715
13804
  cy += v.y;
13716
13805
  }
13717
13806
  const n = poly.length > 0 ? poly.length : 1;
13718
- return new Vector2(cx / n, cy / n);
13807
+ return new Vector2$1(cx / n, cy / n);
13719
13808
  }
13720
13809
 
13721
13810
  const colliderCache = /* @__PURE__ */ new WeakMap();
@@ -14132,7 +14221,7 @@ class Ray {
14132
14221
  }
14133
14222
 
14134
14223
  function raycast(partition, origin, direction, maxDistance, mask, filter) {
14135
- const dir = direction.length() > 0 ? direction.normalize() : new Vector2(1, 0);
14224
+ const dir = direction.length() > 0 ? direction.normalize() : new Vector2$1(1, 0);
14136
14225
  const end = origin.add(dir.mul(maxDistance));
14137
14226
  const candidates = partition.raycast(new Ray(origin, dir, maxDistance), mask, filter);
14138
14227
  if (candidates) return candidates;
@@ -14196,8 +14285,8 @@ function raycastAABB(box, origin, dir, maxDistance) {
14196
14285
  if (tmax < tmin || tmin < 0 || tmin > maxDistance) return null;
14197
14286
  const point = origin.add(dir.mul(tmin));
14198
14287
  let normal;
14199
- if (tmin === tminX) normal = new Vector2(dir.x > 0 ? -1 : 1, 0);
14200
- else normal = new Vector2(0, dir.y > 0 ? -1 : 1);
14288
+ if (tmin === tminX) normal = new Vector2$1(dir.x > 0 ? -1 : 1, 0);
14289
+ else normal = new Vector2$1(0, dir.y > 0 ? -1 : 1);
14201
14290
  return { entity: box.getEntity(), point, normal, distance: tmin };
14202
14291
  }
14203
14292
  function raycastPolygon(poly, origin, dir, maxDistance) {
@@ -14219,7 +14308,7 @@ function raycastPolygon(poly, origin, dir, maxDistance) {
14219
14308
  bestT = t;
14220
14309
  bestPoint = hit.point;
14221
14310
  const edge = b.sub(a);
14222
- const n = new Vector2(-edge.y, edge.x).normalize();
14311
+ const n = new Vector2$1(-edge.y, edge.x).normalize();
14223
14312
  bestNormal = n;
14224
14313
  }
14225
14314
  }
@@ -14230,13 +14319,13 @@ function raycastPolygon(poly, origin, dir, maxDistance) {
14230
14319
  function segmentRayIntersection(a, b, r0, r1) {
14231
14320
  const v1 = r0.sub(a);
14232
14321
  const v2 = b.sub(a);
14233
- const v3 = new Vector2(-(r1.y - r0.y), r1.x - r0.x);
14322
+ const v3 = new Vector2$1(-(r1.y - r0.y), r1.x - r0.x);
14234
14323
  const denom = v2.dot(v3);
14235
14324
  if (Math.abs(denom) < 1e-9) return null;
14236
14325
  const t1 = v2.cross(v1) / denom;
14237
14326
  const t2 = v1.dot(v3) / denom;
14238
14327
  if (t1 >= 0 && t1 <= 1 && t2 >= 0 && t2 <= 1) {
14239
- const hitPoint = new Vector2(r0.x + (r1.x - r0.x) * t1, r0.y + (r1.y - r0.y) * t1);
14328
+ const hitPoint = new Vector2$1(r0.x + (r1.x - r0.x) * t1, r0.y + (r1.y - r0.y) * t1);
14240
14329
  const dist = hitPoint.sub(r0).length();
14241
14330
  return { point: hitPoint, distance: dist };
14242
14331
  }
@@ -14274,6 +14363,11 @@ class CollisionResolver {
14274
14363
  /**
14275
14364
  * Separates two entities by moving them apart
14276
14365
  *
14366
+ * This method applies position corrections to resolve penetration between
14367
+ * colliding entities. After applying corrections, it notifies position change
14368
+ * handlers to ensure proper synchronization with game logic (e.g., updating
14369
+ * owner.x/y signals for network sync).
14370
+ *
14277
14371
  * @param entityA - First entity
14278
14372
  * @param entityB - Second entity
14279
14373
  * @param normal - Separation normal (from A to B)
@@ -14292,9 +14386,11 @@ class CollisionResolver {
14292
14386
  const correctionB = normal.mul(correction * (entityB.invMass / totalInvMass));
14293
14387
  if (!entityA.isStatic()) {
14294
14388
  entityA.position.addInPlace(correctionA);
14389
+ entityA.notifyPositionChange();
14295
14390
  }
14296
14391
  if (!entityB.isStatic()) {
14297
14392
  entityB.position.addInPlace(correctionB);
14393
+ entityB.notifyPositionChange();
14298
14394
  }
14299
14395
  }
14300
14396
  /**
@@ -14413,7 +14509,7 @@ function sweepCircleAABB(circle, box, delta) {
14413
14509
  const p0 = circle.getCenter();
14414
14510
  const dir = delta;
14415
14511
  const maxDist = 1;
14416
- const origin = new Vector2(0, 0);
14512
+ const origin = new Vector2$1(0, 0);
14417
14513
  const bb = new AABB(expanded.minX - p0.x, expanded.minY - p0.y, expanded.maxX - p0.x, expanded.maxY - p0.y);
14418
14514
  const res = rayVsAABB(origin, dir, bb, maxDist);
14419
14515
  if (!res) return null;
@@ -14436,11 +14532,11 @@ function sweepAABBAABB(a, b, delta) {
14436
14532
  if (tEntry > tExit || tEntry < 0 || tEntry > 1) return null;
14437
14533
  let normal;
14438
14534
  if (Math.min(xEntry, xExit) > Math.min(yEntry, yExit)) {
14439
- normal = new Vector2(delta.x > 0 ? -1 : 1, 0);
14535
+ normal = new Vector2$1(delta.x > 0 ? -1 : 1, 0);
14440
14536
  } else {
14441
- normal = new Vector2(0, delta.y > 0 ? -1 : 1);
14537
+ normal = new Vector2$1(0, delta.y > 0 ? -1 : 1);
14442
14538
  }
14443
- const hitPoint = new Vector2(
14539
+ const hitPoint = new Vector2$1(
14444
14540
  delta.x !== 0 ? delta.x > 0 ? A.maxX : A.minX : (A.minX + A.maxX) * 0.5,
14445
14541
  delta.y !== 0 ? delta.y > 0 ? A.maxY : A.minY : (A.minY + A.maxY) * 0.5
14446
14542
  );
@@ -14462,7 +14558,7 @@ function rayVsAABB(origin, dir, b, maxT) {
14462
14558
  tmin = Math.max(tmin, Math.max(tminX, tminY));
14463
14559
  tmax = Math.min(tmax, Math.min(tmaxX, tmaxY));
14464
14560
  if (tmax < tmin || tmin < 0 || tmin > maxT) return null;
14465
- const normal = tmin === tminX ? new Vector2(dir.x > 0 ? -1 : 1, 0) : new Vector2(0, dir.y > 0 ? -1 : 1);
14561
+ const normal = tmin === tminX ? new Vector2$1(dir.x > 0 ? -1 : 1, 0) : new Vector2$1(0, dir.y > 0 ? -1 : 1);
14466
14562
  return { t: tmin, normal };
14467
14563
  }
14468
14564
 
@@ -15367,7 +15463,7 @@ class EntityMovementBody {
15367
15463
  this.entity.setVelocity(velocity);
15368
15464
  }
15369
15465
  translate(delta) {
15370
- this.entity.position.addInPlace(new Vector2(delta.x, delta.y));
15466
+ this.entity.position.addInPlace(new Vector2$1(delta.x, delta.y));
15371
15467
  }
15372
15468
  isStatic() {
15373
15469
  return this.entity.isStatic();
@@ -15624,19 +15720,19 @@ class ZoneManager {
15624
15720
  attachedEntity = config.entity;
15625
15721
  const entityPos = attachedEntity.position;
15626
15722
  const offsetValue = config.offset ?? { x: 0, y: 0 };
15627
- if (offsetValue instanceof Vector2) {
15723
+ if (offsetValue instanceof Vector2$1) {
15628
15724
  offset = offsetValue.clone();
15629
15725
  } else {
15630
- offset = new Vector2(offsetValue.x, offsetValue.y);
15726
+ offset = new Vector2$1(offsetValue.x, offsetValue.y);
15631
15727
  }
15632
- position = new Vector2(entityPos.x + offset.x, entityPos.y + offset.y);
15728
+ position = new Vector2$1(entityPos.x + offset.x, entityPos.y + offset.y);
15633
15729
  } else {
15634
15730
  type = "static";
15635
15731
  const pos = config.position;
15636
- if (pos instanceof Vector2) {
15732
+ if (pos instanceof Vector2$1) {
15637
15733
  position = pos.clone();
15638
15734
  } else {
15639
- position = new Vector2(pos.x, pos.y);
15735
+ position = new Vector2$1(pos.x, pos.y);
15640
15736
  }
15641
15737
  }
15642
15738
  const record = {
@@ -15714,10 +15810,10 @@ class ZoneManager {
15714
15810
  }
15715
15811
  if ("offset" in updates && updates.offset !== void 0 && zone.type === "attached") {
15716
15812
  const offsetValue = updates.offset;
15717
- if (offsetValue instanceof Vector2) {
15813
+ if (offsetValue instanceof Vector2$1) {
15718
15814
  zone.offset = offsetValue.clone();
15719
15815
  } else {
15720
- zone.offset = new Vector2(offsetValue.x, offsetValue.y);
15816
+ zone.offset = new Vector2$1(offsetValue.x, offsetValue.y);
15721
15817
  }
15722
15818
  }
15723
15819
  return true;
@@ -15821,7 +15917,7 @@ class ZoneManager {
15821
15917
  for (const zone of this.zones.values()) {
15822
15918
  if (zone.type === "attached" && zone.attachedEntity) {
15823
15919
  const entityPos = zone.attachedEntity.position;
15824
- const offset = zone.offset ?? new Vector2(0, 0);
15920
+ const offset = zone.offset ?? new Vector2$1(0, 0);
15825
15921
  zone.position.set(entityPos.x + offset.x, entityPos.y + offset.y);
15826
15922
  }
15827
15923
  const aabb = new AABB(
@@ -16620,7 +16716,7 @@ class LinearRepulsion {
16620
16716
  throw new Error("LinearRepulsion requires a movement body backed by a physics entity.");
16621
16717
  }
16622
16718
  const targetPosition = this.targetProvider();
16623
- const toTarget = new Vector2(targetPosition.x - entity.position.x, targetPosition.y - entity.position.y);
16719
+ const toTarget = new Vector2$1(targetPosition.x - entity.position.x, targetPosition.y - entity.position.y);
16624
16720
  const distance = toTarget.length();
16625
16721
  if (distance > 0) {
16626
16722
  toTarget.divInPlace(distance);
@@ -16630,12 +16726,12 @@ class LinearRepulsion {
16630
16726
  const bounds = AABB.fromCenterSize(entity.position.x, entity.position.y, this.repulseRadius * 2, this.repulseRadius * 2);
16631
16727
  const neighbors = this.engine.queryAABB(bounds);
16632
16728
  const ignored = this.ignoredEntity?.();
16633
- const push = new Vector2(0, 0);
16729
+ const push = new Vector2$1(0, 0);
16634
16730
  for (const other of neighbors) {
16635
16731
  if (other === entity || other === ignored || other.isStatic()) {
16636
16732
  continue;
16637
16733
  }
16638
- const diff = new Vector2(entity.position.x - other.position.x, entity.position.y - other.position.y);
16734
+ const diff = new Vector2$1(entity.position.x - other.position.x, entity.position.y - other.position.y);
16639
16735
  const d2 = diff.lengthSquared();
16640
16736
  if (d2 > this.repulseRadiusSq || d2 === 0) {
16641
16737
  continue;
@@ -16876,7 +16972,7 @@ class ProjectileMovement {
16876
16972
  return;
16877
16973
  }
16878
16974
  if (this.startPosition === null) {
16879
- this.startPosition = new Vector2(body.position.x, body.position.y);
16975
+ this.startPosition = new Vector2$1(body.position.x, body.position.y);
16880
16976
  }
16881
16977
  this.elapsed += dt;
16882
16978
  if (this.options.lifetime !== void 0 && this.elapsed >= this.options.lifetime) {
@@ -16895,7 +16991,7 @@ class ProjectileMovement {
16895
16991
  return;
16896
16992
  }
16897
16993
  if (this.startPosition) {
16898
- this.distanceTraveled = new Vector2(body.position.x, body.position.y).distanceTo(this.startPosition);
16994
+ this.distanceTraveled = new Vector2$1(body.position.x, body.position.y).distanceTo(this.startPosition);
16899
16995
  if (this.options.maxRange !== void 0 && this.distanceTraveled >= this.options.maxRange) {
16900
16996
  this.finish(body);
16901
16997
  }
@@ -17008,7 +17104,7 @@ class SeekAvoid {
17008
17104
  body.setVelocity({ x: 0, y: 0 });
17009
17105
  return;
17010
17106
  }
17011
- const toTarget = new Vector2(target.position.x - entity.position.x, target.position.y - entity.position.y);
17107
+ const toTarget = new Vector2$1(target.position.x - entity.position.x, target.position.y - entity.position.y);
17012
17108
  const distSq = toTarget.lengthSquared();
17013
17109
  let arrived = false;
17014
17110
  if (distSq <= this.arriveRadiusSq) {
@@ -17019,13 +17115,13 @@ class SeekAvoid {
17019
17115
  }
17020
17116
  const bounds = AABB.fromCenterSize(entity.position.x, entity.position.y, this.repulseRadius * 2, this.repulseRadius * 2);
17021
17117
  const neighbors = this.engine.queryAABB(bounds);
17022
- const push = new Vector2(0, 0);
17118
+ const push = new Vector2$1(0, 0);
17023
17119
  if (!arrived) {
17024
17120
  for (const other of neighbors) {
17025
17121
  if (other === entity || other === target || other.isStatic()) {
17026
17122
  continue;
17027
17123
  }
17028
- const diff = new Vector2(entity.position.x - other.position.x, entity.position.y - other.position.y);
17124
+ const diff = new Vector2$1(entity.position.x - other.position.x, entity.position.y - other.position.y);
17029
17125
  let d2 = diff.lengthSquared();
17030
17126
  if (d2 > this.repulseRadiusSq) {
17031
17127
  continue;
@@ -17109,7 +17205,7 @@ class RpgCommonMap {
17109
17205
  this.data = signal(null);
17110
17206
  this.physic = new PhysicsEngine({
17111
17207
  timeStep: 1 / 60,
17112
- gravity: new Vector2(0, 0),
17208
+ gravity: new Vector2$1(0, 0),
17113
17209
  enableSleep: false
17114
17210
  });
17115
17211
  this.moveManager = new MovementManager(() => this.physic);
@@ -17123,12 +17219,33 @@ class RpgCommonMap {
17123
17219
  * It's shared using the share() operator, meaning that all subscribers will receive
17124
17220
  * events from a single interval rather than creating multiple intervals.
17125
17221
  *
17222
+ * ## Physics Loop Architecture
17223
+ *
17224
+ * The physics simulation is centralized in this game loop:
17225
+ *
17226
+ * 1. **Input Processing** (`processInput`): Only updates entity velocities, does NOT step physics
17227
+ * 2. **Game Loop** (`tick$` -> `runFixedTicks`): Executes physics simulation with fixed timestep
17228
+ * 3. **Fixed Timestep Pattern**: Accumulator-based approach ensures deterministic physics
17229
+ *
17230
+ * ```
17231
+ * Input Events ─────────────────────────────────────────────────────────────►
17232
+ * │
17233
+ * ▼ (update velocity only)
17234
+ * ┌─────────────────────────────────────────────────────────────────────────┐
17235
+ * │ Game Loop (tick$) │
17236
+ * │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
17237
+ * │ │ updateMovements │ → │ stepOneTick │ → │ postTickUpdates │ │
17238
+ * │ │ (apply velocity)│ │ (physics step) │ │ (zones, sync) │ │
17239
+ * │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
17240
+ * └─────────────────────────────────────────────────────────────────────────┘
17241
+ * ```
17242
+ *
17126
17243
  * @example
17127
17244
  * ```ts
17128
- * // Subscribe to the game tick to update entity positions
17129
- * map.tick$.subscribe(timestamp => {
17130
- * // Update game entities based on elapsed time
17131
- * this.updateEntities(timestamp);
17245
+ * // Subscribe to the game tick for custom updates
17246
+ * map.tick$.subscribe(({ delta, timestamp }) => {
17247
+ * // Custom game logic runs alongside physics
17248
+ * this.updateCustomEntities(delta);
17132
17249
  * });
17133
17250
  * ```
17134
17251
  */
@@ -17304,6 +17421,43 @@ class RpgCommonMap {
17304
17421
  getObjectById(id) {
17305
17422
  return this.players()[id] ?? this.events()[id];
17306
17423
  }
17424
+ /**
17425
+ * Execute physics simulation with fixed timestep
17426
+ *
17427
+ * This method runs the physics engine using a fixed timestep accumulator pattern.
17428
+ * It ensures deterministic physics regardless of frame rate by:
17429
+ * 1. Accumulating delta time
17430
+ * 2. Running fixed-size physics steps until the accumulator is depleted
17431
+ * 3. Calling `updateMovements()` before each step to apply velocity changes
17432
+ * 4. Running post-tick updates (zones, callbacks) after each step
17433
+ *
17434
+ * ## Architecture
17435
+ *
17436
+ * The physics loop is centralized here and called from `tick$` subscription.
17437
+ * Input processing (`processInput`) only updates entity velocities - it does NOT
17438
+ * step the physics. This ensures:
17439
+ * - Consistent physics timing (60fps fixed timestep)
17440
+ * - No double-stepping when inputs are processed
17441
+ * - Proper accumulator-based interpolation support
17442
+ *
17443
+ * @param deltaMs - Time elapsed since last call in milliseconds
17444
+ * @param hooks - Optional callbacks for before/after each physics step
17445
+ * @returns Number of physics ticks executed
17446
+ *
17447
+ * @example
17448
+ * ```ts
17449
+ * // Called automatically by tick$ subscription
17450
+ * this.tickSubscription = this.tick$.subscribe(({ delta }) => {
17451
+ * this.runFixedTicks(delta);
17452
+ * });
17453
+ *
17454
+ * // Or manually with hooks for debugging
17455
+ * this.runFixedTicks(16, {
17456
+ * beforeStep: () => console.log('Before physics step'),
17457
+ * afterStep: (tick) => console.log(`Physics tick ${tick} completed`)
17458
+ * });
17459
+ * ```
17460
+ */
17307
17461
  runFixedTicks(deltaMs, hooks) {
17308
17462
  if (!Number.isFinite(deltaMs) || deltaMs <= 0) {
17309
17463
  return 0;
@@ -17314,12 +17468,46 @@ class RpgCommonMap {
17314
17468
  while (this.physicsAccumulatorMs >= fixedStepMs) {
17315
17469
  this.physicsAccumulatorMs -= fixedStepMs;
17316
17470
  hooks?.beforeStep?.();
17471
+ this.physic.updateMovements();
17317
17472
  const tick = this.physic.stepOneTick();
17318
17473
  executed += 1;
17474
+ this.runPostTickUpdates();
17319
17475
  hooks?.afterStep?.(tick);
17320
17476
  }
17321
17477
  return executed;
17322
17478
  }
17479
+ /**
17480
+ * Force a single physics tick outside of the normal game loop
17481
+ *
17482
+ * This method is primarily used for **client-side prediction** where the client
17483
+ * needs to immediately simulate physics in response to local input, rather than
17484
+ * waiting for the next game loop tick.
17485
+ *
17486
+ * ## Use Cases
17487
+ *
17488
+ * - **Client-side prediction**: Immediately simulate player movement for responsive feel
17489
+ * - **Testing**: Force a physics step in unit tests
17490
+ * - **Special effects**: Immediate physics response for specific game events
17491
+ *
17492
+ * ## Important
17493
+ *
17494
+ * This method should NOT be used on the server for normal input processing.
17495
+ * Server-side physics is handled by `runFixedTicks` in the main game loop to ensure
17496
+ * deterministic simulation.
17497
+ *
17498
+ * @param hooks - Optional callbacks for before/after the physics step
17499
+ * @returns The physics tick number
17500
+ *
17501
+ * @example
17502
+ * ```ts
17503
+ * // Client-side: immediately simulate predicted movement
17504
+ * class RpgClientMap extends RpgCommonMap {
17505
+ * stepPredictionTick(): void {
17506
+ * this.forceSingleTick();
17507
+ * }
17508
+ * }
17509
+ * ```
17510
+ */
17323
17511
  forceSingleTick(hooks) {
17324
17512
  hooks?.beforeStep?.();
17325
17513
  this.physic.updateMovements();
@@ -17613,13 +17801,13 @@ class RpgCommonMap {
17613
17801
  width: boxWidth,
17614
17802
  height: boxHeight,
17615
17803
  mass: Infinity,
17616
- state: EntityState.Static,
17804
+ state: EntityState$1.Static,
17617
17805
  restitution: 0
17618
17806
  });
17619
17807
  entity.freeze();
17620
17808
  const localVertices = points.map((point) => {
17621
17809
  const [px, py] = point;
17622
- return new Vector2(px - centerX, py - centerY);
17810
+ return new Vector2$1(px - centerX, py - centerY);
17623
17811
  });
17624
17812
  assignPolygonCollider(entity, { vertices: localVertices });
17625
17813
  } else {
@@ -17636,7 +17824,7 @@ class RpgCommonMap {
17636
17824
  width: boxWidth,
17637
17825
  height: boxHeight,
17638
17826
  mass: Infinity,
17639
- state: EntityState.Static,
17827
+ state: EntityState$1.Static,
17640
17828
  restitution: 0
17641
17829
  });
17642
17830
  entity.freeze();
@@ -17647,23 +17835,60 @@ class RpgCommonMap {
17647
17835
  * Add a character to the physics world
17648
17836
  * @private
17649
17837
  */
17838
+ /**
17839
+ * Add a character entity to the physics world
17840
+ *
17841
+ * Creates a physics entity for a character (player or NPC) with proper position handling.
17842
+ * The owner's x/y signals represent **top-left** coordinates, while the physics entity
17843
+ * uses **center** coordinates internally.
17844
+ *
17845
+ * ## Position System
17846
+ *
17847
+ * - `owner.x()` / `owner.y()` → **top-left** corner of the character's hitbox
17848
+ * - `entity.position` → **center** of the physics collider
17849
+ * - Conversion: `center = topLeft + (size / 2)`
17850
+ *
17851
+ * @param options - Character configuration
17852
+ * @returns The character's unique ID
17853
+ *
17854
+ * @example
17855
+ * ```ts
17856
+ * // Player at top-left position (100, 100) with 32x32 hitbox
17857
+ * // Physics entity will be at center (116, 116)
17858
+ * this.addCharacter({
17859
+ * owner: player,
17860
+ * x: 116, // center X (ignored, uses owner.x())
17861
+ * y: 116, // center Y (ignored, uses owner.y())
17862
+ * kind: "hero"
17863
+ * });
17864
+ * ```
17865
+ *
17866
+ * @private
17867
+ */
17650
17868
  addCharacter(options) {
17651
17869
  if (!options || typeof options.owner?.id !== "string") {
17652
17870
  throw new Error("Character requires an owner object with a string id");
17653
17871
  }
17654
17872
  const owner = options.owner;
17655
17873
  const id = owner.id;
17656
- const radius = owner.hitbox?.w ?? options.radius ?? 25;
17657
- const diameter = radius * 2;
17658
- const topLeftX = owner.x() - radius;
17659
- const topLeftY = owner.y() - radius;
17660
- const centerX = topLeftX + diameter / 2;
17661
- const centerY = topLeftY + diameter / 2;
17874
+ const hitbox = typeof owner.hitbox === "function" ? owner.hitbox() : owner.hitbox;
17875
+ const width = hitbox?.w ?? 32;
17876
+ const height = hitbox?.h ?? 32;
17877
+ const radius = Math.max(width, height) / 2;
17878
+ const topLeftX = owner.x();
17879
+ const topLeftY = owner.y();
17880
+ const centerX = topLeftX + width / 2;
17881
+ const centerY = topLeftY + height / 2;
17662
17882
  const isStatic = !!options.isStatic;
17663
17883
  const entity = this.physic.createEntity({
17664
17884
  uuid: id,
17665
17885
  position: { x: centerX, y: centerY },
17886
+ // Use radius for circular collision detection
17666
17887
  radius: Math.max(radius, 1),
17888
+ // Also store explicit width/height for consistent position conversions
17889
+ // This ensures getBodyPosition/setBodyPosition use the same dimensions
17890
+ width,
17891
+ height,
17667
17892
  mass: options.mass ?? (isStatic ? Infinity : 1),
17668
17893
  friction: options.friction ?? 0.4,
17669
17894
  linearDamping: isStatic ? 1 : 0.2,
@@ -17691,17 +17916,21 @@ class RpgCommonMap {
17691
17916
  owner.animationName.set("stand");
17692
17917
  }
17693
17918
  });
17919
+ const entityWidth = width;
17920
+ const entityHeight = height;
17694
17921
  entity.onPositionChange(({ x, y }) => {
17695
- const width = entity.width || (entity.radius ? entity.radius * 2 : 32);
17696
- const height = entity.height || (entity.radius ? entity.radius * 2 : 32);
17697
- const topLeftX2 = x - width / 2;
17698
- const topLeftY2 = y - height / 2;
17922
+ const topLeftX2 = x - entityWidth / 2;
17923
+ const topLeftY2 = y - entityHeight / 2;
17924
+ let changed = false;
17699
17925
  if (typeof owner.x === "function" && typeof owner.x.set === "function") {
17700
17926
  owner.x.set(Math.round(topLeftX2));
17701
- owner.applyFrames?.();
17927
+ changed = true;
17702
17928
  }
17703
17929
  if (typeof owner.y === "function" && typeof owner.y.set === "function") {
17704
17930
  owner.y.set(Math.round(topLeftY2));
17931
+ changed = true;
17932
+ }
17933
+ if (changed) {
17705
17934
  owner.applyFrames?.();
17706
17935
  }
17707
17936
  });
@@ -17917,11 +18146,11 @@ class RpgCommonMap {
17917
18146
  setBodyPosition(id, x, y, mode = "center") {
17918
18147
  const entity = this.physic.getEntityByUUID(id);
17919
18148
  if (!entity) return;
18149
+ const width = entity.width || (entity.radius ? entity.radius * 2 : 32);
18150
+ const height = entity.height || (entity.radius ? entity.radius * 2 : 32);
17920
18151
  let centerX = x;
17921
18152
  let centerY = y;
17922
18153
  if (mode === "top-left") {
17923
- const width = entity.width || (entity.radius ? entity.radius * 2 : 32);
17924
- const height = entity.height || (entity.radius ? entity.radius * 2 : 32);
17925
18154
  centerX = x + width / 2;
17926
18155
  centerY = y + height / 2;
17927
18156
  }
@@ -18387,113 +18616,664 @@ var PrebuiltGui = /* @__PURE__ */ ((PrebuiltGui2) => {
18387
18616
  return PrebuiltGui2;
18388
18617
  })(PrebuiltGui || {});
18389
18618
 
18390
- function WithComponentManager(Base) {
18391
- return class extends Base {
18392
- setGraphic(graphic) {
18393
- if (Array.isArray(graphic)) {
18394
- this.graphics.set(graphic);
18395
- } else {
18396
- this.graphics.set([graphic]);
18397
- }
18398
- }
18399
- };
18400
- }
18401
-
18402
- var __defProp$3 = Object.defineProperty;
18403
- var __name = (target, value) => __defProp$3(target, "name", { value, configurable: true });
18404
-
18405
- // src/inject.ts
18406
- function provide(context, name, value) {
18407
- context.set("inject:" + name, value);
18408
- return value;
18409
- }
18410
- __name(provide, "provide");
18411
- function isInjected(context, name) {
18412
- return context.get("injected:" + name) === true;
18413
- }
18414
- __name(isInjected, "isInjected");
18415
- function isProvided(context, name) {
18416
- return context.get("inject:" + name) !== void 0;
18417
- }
18418
- __name(isProvided, "isProvided");
18419
- function inject$1(context, service, args = []) {
18420
- const isClass = typeof service === "function";
18421
- const name = isClass ? service.name : service;
18422
- const value = context.get("inject:" + name);
18423
- if (value) {
18424
- context.set("injected:" + name, true);
18425
- return value;
18619
+ const _Vector2 = class _Vector2 {
18620
+ /**
18621
+ * Creates a new Vector2
18622
+ *
18623
+ * @param x - X component (default: 0)
18624
+ * @param y - Y component (default: 0)
18625
+ */
18626
+ constructor(x = 0, y = 0) {
18627
+ this.x = x;
18628
+ this.y = y;
18426
18629
  }
18427
- throw new Error(`Injection provider ${name} not found`);
18428
- }
18429
- __name(inject$1, "inject");
18430
- function override(providers, newProvider, options) {
18431
- let { upsert = false, key } = options ?? {};
18432
- if (!key) {
18433
- key = typeof newProvider === "function" ? newProvider.name : newProvider.provide;
18630
+ /**
18631
+ * Creates a copy of this vector
18632
+ *
18633
+ * @returns A new Vector2 with the same values
18634
+ */
18635
+ clone() {
18636
+ return new _Vector2(this.x, this.y);
18434
18637
  }
18435
- const flatProviders = providers.flat();
18436
- const exists = flatProviders.some((provider) => {
18437
- if (typeof provider === "function") {
18438
- return provider.name === key;
18439
- } else if (typeof provider === "object") {
18440
- return provider.provide === key;
18441
- }
18442
- return false;
18443
- });
18444
- const mappedProviders = flatProviders.map((provider) => {
18445
- if (typeof provider === "function" && provider.name === key) {
18446
- return newProvider;
18447
- } else if (typeof provider === "object" && provider.provide === key) {
18448
- return newProvider;
18449
- }
18450
- return provider;
18451
- });
18452
- if (upsert && !exists) {
18453
- mappedProviders.push(newProvider);
18638
+ /**
18639
+ * Sets the components of this vector
18640
+ *
18641
+ * @param x - X component
18642
+ * @param y - Y component
18643
+ * @returns This vector for chaining
18644
+ */
18645
+ set(x, y) {
18646
+ this.x = x;
18647
+ this.y = y;
18648
+ return this;
18454
18649
  }
18455
- return mappedProviders;
18456
- }
18457
- __name(override, "override");
18458
- function findProviders(providers, name) {
18459
- const results = [];
18460
- for (const provider of providers) {
18461
- if (Array.isArray(provider)) {
18462
- results.push(...findProviders(provider, name));
18463
- } else if (findProvider(provider, name)) {
18464
- results.push(provider);
18465
- }
18650
+ /**
18651
+ * Copies values from another vector
18652
+ *
18653
+ * @param other - Vector to copy from
18654
+ * @returns This vector for chaining
18655
+ */
18656
+ copyFrom(other) {
18657
+ this.x = other.x;
18658
+ this.y = other.y;
18659
+ return this;
18466
18660
  }
18467
- return results;
18468
- }
18469
- __name(findProviders, "findProviders");
18470
- function findProvider(providers, name) {
18471
- if (!Array.isArray(providers)) {
18472
- if (typeof providers === "object" && "provide" in providers) {
18473
- const provider = providers;
18474
- const providerName = typeof provider.provide === "function" ? provider.provide.name : provider.provide;
18475
- if (name instanceof RegExp) {
18476
- if (name.test(providerName)) return providers;
18477
- } else {
18478
- if (providerName === name) return providers;
18479
- }
18480
- }
18481
- return null;
18661
+ /**
18662
+ * Adds another vector to this vector (immutable)
18663
+ *
18664
+ * @param other - Vector to add
18665
+ * @returns New vector with the result
18666
+ */
18667
+ add(other) {
18668
+ return new _Vector2(this.x + other.x, this.y + other.y);
18482
18669
  }
18483
- for (const provider of providers) {
18484
- if (Array.isArray(provider)) {
18485
- const found = findProvider(provider, name);
18486
- if (found) return found;
18487
- continue;
18488
- }
18489
- if (typeof provider === "object" && "provide" in provider) {
18490
- const providerName = typeof provider.provide === "function" ? provider.provide.name : provider.provide;
18491
- if (name instanceof RegExp) {
18492
- if (name.test(providerName)) return provider;
18493
- } else {
18494
- if (providerName === name) return provider;
18495
- }
18496
- }
18670
+ /**
18671
+ * Adds another vector to this vector (in-place)
18672
+ *
18673
+ * @param other - Vector to add
18674
+ * @returns This vector for chaining
18675
+ */
18676
+ addInPlace(other) {
18677
+ this.x += other.x;
18678
+ this.y += other.y;
18679
+ return this;
18680
+ }
18681
+ /**
18682
+ * Subtracts another vector from this vector (immutable)
18683
+ *
18684
+ * @param other - Vector to subtract
18685
+ * @returns New vector with the result
18686
+ */
18687
+ sub(other) {
18688
+ return new _Vector2(this.x - other.x, this.y - other.y);
18689
+ }
18690
+ /**
18691
+ * Subtracts another vector from this vector (in-place)
18692
+ *
18693
+ * @param other - Vector to subtract
18694
+ * @returns This vector for chaining
18695
+ */
18696
+ subInPlace(other) {
18697
+ this.x -= other.x;
18698
+ this.y -= other.y;
18699
+ return this;
18700
+ }
18701
+ /**
18702
+ * Multiplies this vector by a scalar (immutable)
18703
+ *
18704
+ * @param scalar - Scalar value
18705
+ * @returns New vector with the result
18706
+ */
18707
+ mul(scalar) {
18708
+ return new _Vector2(this.x * scalar, this.y * scalar);
18709
+ }
18710
+ /**
18711
+ * Multiplies this vector by a scalar (in-place)
18712
+ *
18713
+ * @param scalar - Scalar value
18714
+ * @returns This vector for chaining
18715
+ */
18716
+ mulInPlace(scalar) {
18717
+ this.x *= scalar;
18718
+ this.y *= scalar;
18719
+ return this;
18720
+ }
18721
+ /**
18722
+ * Divides this vector by a scalar (immutable)
18723
+ *
18724
+ * @param scalar - Scalar value (must not be zero)
18725
+ * @returns New vector with the result
18726
+ */
18727
+ div(scalar) {
18728
+ return new _Vector2(this.x / scalar, this.y / scalar);
18729
+ }
18730
+ /**
18731
+ * Divides this vector by a scalar (in-place)
18732
+ *
18733
+ * @param scalar - Scalar value (must not be zero)
18734
+ * @returns This vector for chaining
18735
+ */
18736
+ divInPlace(scalar) {
18737
+ this.x /= scalar;
18738
+ this.y /= scalar;
18739
+ return this;
18740
+ }
18741
+ /**
18742
+ * Calculates the dot product with another vector
18743
+ *
18744
+ * @param other - Vector to dot with
18745
+ * @returns Dot product value
18746
+ */
18747
+ dot(other) {
18748
+ return this.x * other.x + this.y * other.y;
18749
+ }
18750
+ /**
18751
+ * Calculates the 2D cross product (scalar result)
18752
+ *
18753
+ * @param other - Vector to cross with
18754
+ * @returns Cross product value (z-component of 3D cross product)
18755
+ */
18756
+ cross(other) {
18757
+ return this.x * other.y - this.y * other.x;
18758
+ }
18759
+ /**
18760
+ * Calculates the squared length (faster than length, avoids sqrt)
18761
+ *
18762
+ * @returns Squared length
18763
+ */
18764
+ lengthSquared() {
18765
+ return this.x * this.x + this.y * this.y;
18766
+ }
18767
+ /**
18768
+ * Calculates the length (magnitude) of the vector
18769
+ *
18770
+ * @returns Length
18771
+ */
18772
+ length() {
18773
+ return Math.sqrt(this.lengthSquared());
18774
+ }
18775
+ /**
18776
+ * Normalizes this vector to unit length (immutable)
18777
+ *
18778
+ * @returns New normalized vector
18779
+ */
18780
+ normalize() {
18781
+ const len = this.length();
18782
+ if (len === 0) {
18783
+ return new _Vector2(0, 0);
18784
+ }
18785
+ return this.div(len);
18786
+ }
18787
+ /**
18788
+ * Normalizes this vector to unit length (in-place)
18789
+ *
18790
+ * @returns This vector for chaining
18791
+ */
18792
+ normalizeInPlace() {
18793
+ const len = this.length();
18794
+ if (len === 0) {
18795
+ this.x = 0;
18796
+ this.y = 0;
18797
+ } else {
18798
+ this.divInPlace(len);
18799
+ }
18800
+ return this;
18801
+ }
18802
+ /**
18803
+ * Calculates the distance to another vector
18804
+ *
18805
+ * @param other - Target vector
18806
+ * @returns Distance
18807
+ */
18808
+ distanceTo(other) {
18809
+ return this.sub(other).length();
18810
+ }
18811
+ /**
18812
+ * Calculates the squared distance to another vector (faster)
18813
+ *
18814
+ * @param other - Target vector
18815
+ * @returns Squared distance
18816
+ */
18817
+ distanceToSquared(other) {
18818
+ return this.sub(other).lengthSquared();
18819
+ }
18820
+ /**
18821
+ * Rotates this vector by an angle in radians (immutable)
18822
+ *
18823
+ * @param angle - Angle in radians
18824
+ * @returns New rotated vector
18825
+ */
18826
+ rotate(angle) {
18827
+ const cos = Math.cos(angle);
18828
+ const sin = Math.sin(angle);
18829
+ return new _Vector2(
18830
+ this.x * cos - this.y * sin,
18831
+ this.x * sin + this.y * cos
18832
+ );
18833
+ }
18834
+ /**
18835
+ * Rotates this vector by an angle in radians (in-place)
18836
+ *
18837
+ * @param angle - Angle in radians
18838
+ * @returns This vector for chaining
18839
+ */
18840
+ rotateInPlace(angle) {
18841
+ const cos = Math.cos(angle);
18842
+ const sin = Math.sin(angle);
18843
+ const x = this.x * cos - this.y * sin;
18844
+ const y = this.x * sin + this.y * cos;
18845
+ this.x = x;
18846
+ this.y = y;
18847
+ return this;
18848
+ }
18849
+ /**
18850
+ * Calculates the angle of this vector in radians
18851
+ *
18852
+ * @returns Angle in radians (range: -π to π)
18853
+ */
18854
+ angle() {
18855
+ return Math.atan2(this.y, this.x);
18856
+ }
18857
+ /**
18858
+ * Linearly interpolates between this vector and another
18859
+ *
18860
+ * @param other - Target vector
18861
+ * @param t - Interpolation factor (0 to 1)
18862
+ * @returns New interpolated vector
18863
+ */
18864
+ lerp(other, t) {
18865
+ return new _Vector2(
18866
+ this.x + (other.x - this.x) * t,
18867
+ this.y + (other.y - this.y) * t
18868
+ );
18869
+ }
18870
+ /**
18871
+ * Checks if this vector equals another (with epsilon tolerance)
18872
+ *
18873
+ * @param other - Vector to compare
18874
+ * @param epsilon - Tolerance for comparison (default: 1e-5)
18875
+ * @returns True if vectors are approximately equal
18876
+ */
18877
+ equals(other, epsilon = 1e-5) {
18878
+ return Math.abs(this.x - other.x) < epsilon && Math.abs(this.y - other.y) < epsilon;
18879
+ }
18880
+ /**
18881
+ * Returns a string representation of this vector
18882
+ *
18883
+ * @returns String representation
18884
+ */
18885
+ toString() {
18886
+ return `Vector2(${this.x}, ${this.y})`;
18887
+ }
18888
+ };
18889
+ _Vector2.ZERO = new _Vector2(0, 0);
18890
+ _Vector2.UNIT_X = new _Vector2(1, 0);
18891
+ _Vector2.UNIT_Y = new _Vector2(0, 1);
18892
+ let Vector2 = _Vector2;
18893
+
18894
+ var EntityState = /* @__PURE__ */ ((EntityState2) => {
18895
+ EntityState2[EntityState2["Static"] = 1] = "Static";
18896
+ EntityState2[EntityState2["Dynamic"] = 2] = "Dynamic";
18897
+ EntityState2[EntityState2["Sleeping"] = 4] = "Sleeping";
18898
+ EntityState2[EntityState2["Kinematic"] = 8] = "Kinematic";
18899
+ return EntityState2;
18900
+ })(EntityState || {});
18901
+
18902
+ function WithComponentManager(Base) {
18903
+ return class extends Base {
18904
+ setGraphic(graphic) {
18905
+ if (Array.isArray(graphic)) {
18906
+ this.graphics.set(graphic);
18907
+ } else {
18908
+ this.graphics.set([graphic]);
18909
+ }
18910
+ }
18911
+ /**
18912
+ * Set components to display above the player graphic
18913
+ *
18914
+ * Components are displayed above the player's sprite and can include
18915
+ * text, bars, shapes, or any combination. The components are synchronized
18916
+ * to all clients on the map.
18917
+ *
18918
+ * @param layout - Component(s) to display, can be single, array, or 2D array
18919
+ * @param options - Optional layout options for positioning and sizing
18920
+ * @returns void
18921
+ *
18922
+ * @example
18923
+ * ```ts
18924
+ * // Single text component
18925
+ * player.setComponentsTop(Components.text('{name}'));
18926
+ *
18927
+ * // Multiple components vertically
18928
+ * player.setComponentsTop([
18929
+ * Components.text('HP: {hp}'),
18930
+ * Components.text('{name}')
18931
+ * ]);
18932
+ *
18933
+ * // Table layout (columns)
18934
+ * player.setComponentsTop([
18935
+ * [Components.text('{hp}'), Components.text('{name}')]
18936
+ * ]);
18937
+ *
18938
+ * // With layout options
18939
+ * player.setComponentsTop([
18940
+ * Components.text('HP: {hp}'),
18941
+ * Components.text('{name}')
18942
+ * ], {
18943
+ * width: 100,
18944
+ * height: 30,
18945
+ * marginBottom: -10
18946
+ * });
18947
+ * ```
18948
+ */
18949
+ setComponentsTop(layout, options) {
18950
+ const normalized = this.normalizeComponents(layout);
18951
+ const data = {
18952
+ components: normalized,
18953
+ layout: options || {}
18954
+ };
18955
+ this.componentsTop.set(JSON.stringify(data));
18956
+ }
18957
+ /**
18958
+ * Set components to display below the player graphic
18959
+ *
18960
+ * Components are displayed below the player's sprite and can include
18961
+ * text, bars, shapes, or any combination. The components are synchronized
18962
+ * to all clients on the map.
18963
+ *
18964
+ * @param layout - Component(s) to display, can be single, array, or 2D array
18965
+ * @param options - Optional layout options for positioning and sizing
18966
+ * @returns void
18967
+ *
18968
+ * @example
18969
+ * ```ts
18970
+ * player.setComponentsBottom(Components.shape({
18971
+ * fill: '#ff0000',
18972
+ * type: 'rectangle',
18973
+ * width: 32,
18974
+ * height: 32
18975
+ * }), {
18976
+ * marginBottom: 16
18977
+ * });
18978
+ * ```
18979
+ */
18980
+ setComponentsBottom(layout, options) {
18981
+ const normalized = this.normalizeComponents(layout);
18982
+ const data = {
18983
+ components: normalized,
18984
+ layout: options || {}
18985
+ };
18986
+ this.componentsBottom.set(JSON.stringify(data));
18987
+ }
18988
+ /**
18989
+ * Set components to display at the center of the player graphic
18990
+ *
18991
+ * Components are displayed at the center of the player's sprite.
18992
+ * Be careful: if you assign, it deletes the graphics and if the lines are superimposed.
18993
+ *
18994
+ * @param layout - Component(s) to display, can be single, array, or 2D array
18995
+ * @param options - Optional layout options for positioning and sizing
18996
+ * @returns void
18997
+ *
18998
+ * @example
18999
+ * ```ts
19000
+ * player.setComponentsCenter([
19001
+ * Components.text('{name}'),
19002
+ * Components.hpBar()
19003
+ * ]);
19004
+ * ```
19005
+ */
19006
+ setComponentsCenter(layout, options) {
19007
+ const normalized = this.normalizeComponents(layout);
19008
+ const data = {
19009
+ components: normalized,
19010
+ layout: options || {}
19011
+ };
19012
+ this.componentsCenter.set(JSON.stringify(data));
19013
+ }
19014
+ /**
19015
+ * Set components to display to the left of the player graphic
19016
+ *
19017
+ * Components are displayed to the left of the player's sprite.
19018
+ *
19019
+ * @param layout - Component(s) to display, can be single, array, or 2D array
19020
+ * @param options - Optional layout options for positioning and sizing
19021
+ * @returns void
19022
+ *
19023
+ * @example
19024
+ * ```ts
19025
+ * player.setComponentsLeft([
19026
+ * Components.text('{name}'),
19027
+ * Components.hpBar()
19028
+ * ]);
19029
+ * ```
19030
+ */
19031
+ setComponentsLeft(layout, options) {
19032
+ const normalized = this.normalizeComponents(layout);
19033
+ const data = {
19034
+ components: normalized,
19035
+ layout: options || {}
19036
+ };
19037
+ this.componentsLeft.set(JSON.stringify(data));
19038
+ }
19039
+ /**
19040
+ * Set components to display to the right of the player graphic
19041
+ *
19042
+ * Components are displayed to the right of the player's sprite.
19043
+ *
19044
+ * @param layout - Component(s) to display, can be single, array, or 2D array
19045
+ * @param options - Optional layout options for positioning and sizing
19046
+ * @returns void
19047
+ *
19048
+ * @example
19049
+ * ```ts
19050
+ * player.setComponentsRight([
19051
+ * Components.text('{name}'),
19052
+ * Components.hpBar()
19053
+ * ]);
19054
+ * ```
19055
+ */
19056
+ setComponentsRight(layout, options) {
19057
+ const normalized = this.normalizeComponents(layout);
19058
+ const data = {
19059
+ components: normalized,
19060
+ layout: options || {}
19061
+ };
19062
+ this.componentsRight.set(JSON.stringify(data));
19063
+ }
19064
+ /**
19065
+ * Remove components from a specific position
19066
+ *
19067
+ * Deletes all components at the specified position.
19068
+ *
19069
+ * @param position - Position of the components: 'top', 'center', 'bottom', 'left', or 'right'
19070
+ * @returns void
19071
+ *
19072
+ * @example
19073
+ * ```ts
19074
+ * player.removeComponents('top');
19075
+ * ```
19076
+ */
19077
+ removeComponents(position) {
19078
+ switch (position) {
19079
+ case "top":
19080
+ this.componentsTop.set(null);
19081
+ break;
19082
+ case "center":
19083
+ this.componentsCenter.set(null);
19084
+ break;
19085
+ case "bottom":
19086
+ this.componentsBottom.set(null);
19087
+ break;
19088
+ case "left":
19089
+ this.componentsLeft.set(null);
19090
+ break;
19091
+ case "right":
19092
+ this.componentsRight.set(null);
19093
+ break;
19094
+ }
19095
+ }
19096
+ /**
19097
+ * Merge components with existing components at a specific position
19098
+ *
19099
+ * Merges new components with existing components at the specified position.
19100
+ *
19101
+ * @param position - Position of the components: 'top', 'center', 'bottom', 'left', or 'right'
19102
+ * @param layout - Component(s) to merge, can be single, array, or 2D array
19103
+ * @param options - Optional layout options for positioning and sizing
19104
+ * @returns void
19105
+ *
19106
+ * @example
19107
+ * ```ts
19108
+ * // First set some components
19109
+ * player.setComponentsTop([Components.text('{name}')]);
19110
+ *
19111
+ * // Then merge additional components
19112
+ * player.mergeComponents('top', [Components.hpBar()], {
19113
+ * width: 100
19114
+ * });
19115
+ * ```
19116
+ */
19117
+ mergeComponents(position, layout, options) {
19118
+ const normalized = this.normalizeComponents(layout);
19119
+ let existingData = null;
19120
+ let signal = null;
19121
+ switch (position) {
19122
+ case "top":
19123
+ signal = this.componentsTop;
19124
+ break;
19125
+ case "center":
19126
+ signal = this.componentsCenter;
19127
+ break;
19128
+ case "bottom":
19129
+ signal = this.componentsBottom;
19130
+ break;
19131
+ case "left":
19132
+ signal = this.componentsLeft;
19133
+ break;
19134
+ case "right":
19135
+ signal = this.componentsRight;
19136
+ break;
19137
+ }
19138
+ const existingJson = signal();
19139
+ if (existingJson) {
19140
+ try {
19141
+ existingData = JSON.parse(existingJson);
19142
+ } catch (e) {
19143
+ existingData = null;
19144
+ }
19145
+ }
19146
+ const existingComponents = existingData?.components || [];
19147
+ const mergedComponents = [...existingComponents, ...normalized];
19148
+ const mergedLayout = {
19149
+ ...existingData?.layout || {},
19150
+ ...options || {}
19151
+ };
19152
+ const data = {
19153
+ components: mergedComponents,
19154
+ layout: mergedLayout
19155
+ };
19156
+ signal.set(JSON.stringify(data));
19157
+ }
19158
+ /**
19159
+ * Normalize component input to a consistent structure
19160
+ *
19161
+ * Converts various input formats (single component, array, 2D array)
19162
+ * into a normalized 2D array structure for consistent rendering.
19163
+ *
19164
+ * @param components - Component input in any format
19165
+ * @returns Normalized 2D array of components
19166
+ */
19167
+ normalizeComponents(components) {
19168
+ if (!components) {
19169
+ return [];
19170
+ }
19171
+ if (!Array.isArray(components)) {
19172
+ return [[components]];
19173
+ }
19174
+ if (components.length > 0 && Array.isArray(components[0])) {
19175
+ return components;
19176
+ }
19177
+ return components.map((comp) => [comp]);
19178
+ }
19179
+ };
19180
+ }
19181
+
19182
+ var __defProp$3 = Object.defineProperty;
19183
+ var __name = (target, value) => __defProp$3(target, "name", { value, configurable: true });
19184
+
19185
+ // src/inject.ts
19186
+ function provide(context, name, value) {
19187
+ context.set("inject:" + name, value);
19188
+ return value;
19189
+ }
19190
+ __name(provide, "provide");
19191
+ function isInjected(context, name) {
19192
+ return context.get("injected:" + name) === true;
19193
+ }
19194
+ __name(isInjected, "isInjected");
19195
+ function isProvided(context, name) {
19196
+ return context.get("inject:" + name) !== void 0;
19197
+ }
19198
+ __name(isProvided, "isProvided");
19199
+ function inject$1(context, service, args = []) {
19200
+ const isClass = typeof service === "function";
19201
+ const name = isClass ? service.name : service;
19202
+ const value = context.get("inject:" + name);
19203
+ if (value) {
19204
+ context.set("injected:" + name, true);
19205
+ return value;
19206
+ }
19207
+ throw new Error(`Injection provider ${name} not found`);
19208
+ }
19209
+ __name(inject$1, "inject");
19210
+ function override(providers, newProvider, options) {
19211
+ let { upsert = false, key } = options ?? {};
19212
+ if (!key) {
19213
+ key = typeof newProvider === "function" ? newProvider.name : newProvider.provide;
19214
+ }
19215
+ const flatProviders = providers.flat();
19216
+ const exists = flatProviders.some((provider) => {
19217
+ if (typeof provider === "function") {
19218
+ return provider.name === key;
19219
+ } else if (typeof provider === "object") {
19220
+ return provider.provide === key;
19221
+ }
19222
+ return false;
19223
+ });
19224
+ const mappedProviders = flatProviders.map((provider) => {
19225
+ if (typeof provider === "function" && provider.name === key) {
19226
+ return newProvider;
19227
+ } else if (typeof provider === "object" && provider.provide === key) {
19228
+ return newProvider;
19229
+ }
19230
+ return provider;
19231
+ });
19232
+ if (upsert && !exists) {
19233
+ mappedProviders.push(newProvider);
19234
+ }
19235
+ return mappedProviders;
19236
+ }
19237
+ __name(override, "override");
19238
+ function findProviders(providers, name) {
19239
+ const results = [];
19240
+ for (const provider of providers) {
19241
+ if (Array.isArray(provider)) {
19242
+ results.push(...findProviders(provider, name));
19243
+ } else if (findProvider(provider, name)) {
19244
+ results.push(provider);
19245
+ }
19246
+ }
19247
+ return results;
19248
+ }
19249
+ __name(findProviders, "findProviders");
19250
+ function findProvider(providers, name) {
19251
+ if (!Array.isArray(providers)) {
19252
+ if (typeof providers === "object" && "provide" in providers) {
19253
+ const provider = providers;
19254
+ const providerName = typeof provider.provide === "function" ? provider.provide.name : provider.provide;
19255
+ if (name instanceof RegExp) {
19256
+ if (name.test(providerName)) return providers;
19257
+ } else {
19258
+ if (providerName === name) return providers;
19259
+ }
19260
+ }
19261
+ return null;
19262
+ }
19263
+ for (const provider of providers) {
19264
+ if (Array.isArray(provider)) {
19265
+ const found = findProvider(provider, name);
19266
+ if (found) return found;
19267
+ continue;
19268
+ }
19269
+ if (typeof provider === "object" && "provide" in provider) {
19270
+ const providerName = typeof provider.provide === "function" ? provider.provide.name : provider.provide;
19271
+ if (name instanceof RegExp) {
19272
+ if (name.test(providerName)) return provider;
19273
+ } else {
19274
+ if (providerName === name) return provider;
19275
+ }
19276
+ }
18497
19277
  }
18498
19278
  return null;
18499
19279
  }
@@ -19173,7 +19953,7 @@ function WithMoveManager(Base) {
19173
19953
  staticTarget.freeze();
19174
19954
  map.moveManager.add(
19175
19955
  this.id,
19176
- new SeekAvoid(engine, () => staticTarget, 3, 50, 5)
19956
+ new SeekAvoid(engine, () => staticTarget, 80, 140, 80, 48)
19177
19957
  );
19178
19958
  }
19179
19959
  /**
@@ -22312,6 +23092,10 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22312
23092
  this.conn = null;
22313
23093
  this.touchSide = false;
22314
23094
  // Protection against map change loops
23095
+ /** Internal: Shapes attached to this player */
23096
+ this._attachedShapes = /* @__PURE__ */ new Map();
23097
+ /** Internal: Shapes where this player is currently located */
23098
+ this._inShapes = /* @__PURE__ */ new Set();
22315
23099
  /** Last processed client input timestamp for reconciliation */
22316
23100
  this.lastProcessedInputTs = 0;
22317
23101
  /** Last processed client input frame for reconciliation with server tick */
@@ -22331,12 +23115,29 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22331
23115
  this.addParameter(DEX, DEX_CURVE);
22332
23116
  this.addParameter(AGI, AGI_CURVE);
22333
23117
  this.allRecovery();
23118
+ let lastEmitted = null;
23119
+ let pendingUpdate = null;
23120
+ let updateScheduled = false;
22334
23121
  combineLatest$1([this.x.observable, this.y.observable]).subscribe(([x, y]) => {
22335
- this.frames = [...this.frames, {
22336
- x,
22337
- y,
22338
- ts: Date.now()
22339
- }];
23122
+ pendingUpdate = { x, y };
23123
+ if (!updateScheduled) {
23124
+ updateScheduled = true;
23125
+ queueMicrotask(() => {
23126
+ if (pendingUpdate) {
23127
+ const { x: x2, y: y2 } = pendingUpdate;
23128
+ if (!lastEmitted || lastEmitted.x !== x2 || lastEmitted.y !== y2) {
23129
+ this.frames = [...this.frames, {
23130
+ x: x2,
23131
+ y: y2,
23132
+ ts: Date.now()
23133
+ }];
23134
+ lastEmitted = { x: x2, y: y2 };
23135
+ }
23136
+ pendingUpdate = null;
23137
+ }
23138
+ updateScheduled = false;
23139
+ });
23140
+ }
22340
23141
  });
22341
23142
  }
22342
23143
  _onInit() {
@@ -22411,22 +23212,22 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22411
23212
  * ```
22412
23213
  */
22413
23214
  async autoChangeMap(nextPosition, forcedDirection) {
22414
- const map = this.getCurrentMap();
22415
- if (!map) return false;
22416
- const worldMaps = map.getWorldMapsManager?.();
23215
+ const map2 = this.getCurrentMap();
23216
+ if (!map2) return false;
23217
+ const worldMaps = map2.getWorldMapsManager?.();
22417
23218
  let ret = false;
22418
- if (worldMaps && map) {
23219
+ if (worldMaps && map2) {
22419
23220
  const direction = forcedDirection ?? this.getDirection();
22420
- const marginLeftRight = (map.tileWidth ?? 32) / 2;
22421
- const marginTopDown = (map.tileHeight ?? 32) / 2;
22422
- const worldPositionX = (map.worldX ?? 0) + this.x();
22423
- const worldPositionY = (map.worldY ?? 0) + this.y();
23221
+ const marginLeftRight = (map2.tileWidth ?? 32) / 2;
23222
+ const marginTopDown = (map2.tileHeight ?? 32) / 2;
23223
+ const worldPositionX = (map2.worldX ?? 0) + this.x();
23224
+ const worldPositionY = (map2.worldY ?? 0) + this.y();
22424
23225
  const changeMap = async (adjacentCoords, positionCalculator) => {
22425
23226
  if (this.touchSide) {
22426
23227
  return false;
22427
23228
  }
22428
23229
  this.touchSide = true;
22429
- const [nextMap] = worldMaps.getAdjacentMaps(map, adjacentCoords);
23230
+ const [nextMap] = worldMaps.getAdjacentMaps(map2, adjacentCoords);
22430
23231
  if (!nextMap) {
22431
23232
  this.touchSide = false;
22432
23233
  return false;
@@ -22446,34 +23247,34 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22446
23247
  };
22447
23248
  if (nextPosition.x < marginLeftRight && direction === Direction.Left) {
22448
23249
  ret = await changeMap({
22449
- x: (map.worldX ?? 0) - 1,
23250
+ x: (map2.worldX ?? 0) - 1,
22450
23251
  y: worldPositionY
22451
23252
  }, (nextMapInfo) => ({
22452
23253
  x: nextMapInfo.width - this.hitbox().w - marginLeftRight,
22453
- y: (map.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
23254
+ y: (map2.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
22454
23255
  }));
22455
- } else if (nextPosition.x > map.widthPx - this.hitbox().w - marginLeftRight && direction === Direction.Right) {
23256
+ } else if (nextPosition.x > map2.widthPx - this.hitbox().w - marginLeftRight && direction === Direction.Right) {
22456
23257
  ret = await changeMap({
22457
- x: (map.worldX ?? 0) + map.widthPx + 1,
23258
+ x: (map2.worldX ?? 0) + map2.widthPx + 1,
22458
23259
  y: worldPositionY
22459
23260
  }, (nextMapInfo) => ({
22460
23261
  x: marginLeftRight,
22461
- y: (map.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
23262
+ y: (map2.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
22462
23263
  }));
22463
23264
  } else if (nextPosition.y < marginTopDown && direction === Direction.Up) {
22464
23265
  ret = await changeMap({
22465
23266
  x: worldPositionX,
22466
- y: (map.worldY ?? 0) - 1
23267
+ y: (map2.worldY ?? 0) - 1
22467
23268
  }, (nextMapInfo) => ({
22468
- x: (map.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
23269
+ x: (map2.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
22469
23270
  y: nextMapInfo.height - this.hitbox().h - marginTopDown
22470
23271
  }));
22471
- } else if (nextPosition.y > map.heightPx - this.hitbox().h - marginTopDown && direction === Direction.Down) {
23272
+ } else if (nextPosition.y > map2.heightPx - this.hitbox().h - marginTopDown && direction === Direction.Down) {
22472
23273
  ret = await changeMap({
22473
23274
  x: worldPositionX,
22474
- y: (map.worldY ?? 0) + map.heightPx + 1
23275
+ y: (map2.worldY ?? 0) + map2.heightPx + 1
22475
23276
  }, (nextMapInfo) => ({
22476
- x: (map.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
23277
+ x: (map2.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
22477
23278
  y: marginTopDown
22478
23279
  }));
22479
23280
  } else {
@@ -22493,15 +23294,17 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22493
23294
  this.x.set(positions.x);
22494
23295
  this.y.set(positions.y);
22495
23296
  }
22496
- this._frames.set(this.frames);
23297
+ queueMicrotask(() => {
23298
+ this.applyFrames();
23299
+ });
22497
23300
  }
22498
23301
  getCurrentMap() {
22499
23302
  return this.map;
22500
23303
  }
22501
23304
  emit(type2, value) {
22502
- const map = this.getCurrentMap();
22503
- if (!map || !this.conn) return;
22504
- map.$send(this.conn, {
23305
+ const map2 = this.getCurrentMap();
23306
+ if (!map2 || !this.conn) return;
23307
+ map2.$send(this.conn, {
22505
23308
  type: type2,
22506
23309
  value
22507
23310
  });
@@ -22544,12 +23347,12 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22544
23347
  * ```
22545
23348
  */
22546
23349
  setAnimation(animationName, nbTimes = Infinity) {
22547
- const map = this.getCurrentMap();
22548
- if (!map) return;
23350
+ const map2 = this.getCurrentMap();
23351
+ if (!map2) return;
22549
23352
  if (nbTimes === Infinity) {
22550
23353
  this.animationName.set(animationName);
22551
23354
  } else {
22552
- map.$broadcast({
23355
+ map2.$broadcast({
22553
23356
  type: "setAnimation",
22554
23357
  value: {
22555
23358
  animationName,
@@ -22572,9 +23375,9 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22572
23375
  this._eventChanges();
22573
23376
  }
22574
23377
  databaseById(id) {
22575
- const map = this.getCurrentMap();
22576
- if (!map) return;
22577
- const data = map.database()[id];
23378
+ const map2 = this.getCurrentMap();
23379
+ if (!map2) return;
23380
+ const data = map2.database()[id];
22578
23381
  if (!data)
22579
23382
  throw new Error(
22580
23383
  `The ID=${id} data is not found in the database. Add the data in the property "database"`
@@ -22582,9 +23385,9 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22582
23385
  return data;
22583
23386
  }
22584
23387
  _eventChanges() {
22585
- const map = this.getCurrentMap();
22586
- if (!map) return;
22587
- const { events } = map;
23388
+ const map2 = this.getCurrentMap();
23389
+ if (!map2) return;
23390
+ const { events } = map2;
22588
23391
  const arrayEvents = [
22589
23392
  ...Object.values(this.events()),
22590
23393
  ...Object.values(events?.() ?? {})
@@ -22593,39 +23396,231 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22593
23396
  if (event.onChanges) event.onChanges(this);
22594
23397
  }
22595
23398
  }
22596
- attachShape(id, options) {
22597
- const map = this.getCurrentMap();
22598
- if (!map) return;
22599
- const physic = map.physic;
22600
- const zoneId = physic.addZone(id, {
22601
- linkedTo: this.id,
22602
- ...options
22603
- });
22604
- physic.registerZoneEvents(
22605
- id,
22606
- (hitIds) => {
22607
- hitIds.forEach((id2) => {
22608
- const event = map.getEvent(id2);
22609
- const player = map.getPlayer(id2);
22610
- const zone = physic.getZone(zoneId);
22611
- if (event) {
22612
- event.execMethod("onInShape", [zone, this]);
22613
- }
22614
- if (player) this.execMethod("onDetectInShape", [player, zone]);
22615
- });
23399
+ /**
23400
+ * Attach a zone shape to this player using the physic zone system
23401
+ *
23402
+ * This method creates a zone attached to the player's entity in the physics engine.
23403
+ * The zone can be circular or cone-shaped and will detect other entities (players/events)
23404
+ * entering or exiting the zone.
23405
+ *
23406
+ * @param id - Optional zone identifier. If not provided, a unique ID will be generated
23407
+ * @param options - Zone configuration options
23408
+ *
23409
+ * @example
23410
+ * ```ts
23411
+ * // Create a circular detection zone
23412
+ * player.attachShape("vision", {
23413
+ * radius: 150,
23414
+ * angle: 360,
23415
+ * });
23416
+ *
23417
+ * // Create a cone-shaped vision zone
23418
+ * player.attachShape("vision", {
23419
+ * radius: 200,
23420
+ * angle: 120,
23421
+ * direction: Direction.Right,
23422
+ * limitedByWalls: true,
23423
+ * });
23424
+ *
23425
+ * // Create a zone with width/height (radius calculated automatically)
23426
+ * player.attachShape({
23427
+ * width: 100,
23428
+ * height: 100,
23429
+ * positioning: "center",
23430
+ * });
23431
+ * ```
23432
+ */
23433
+ attachShape(idOrOptions, options) {
23434
+ const map2 = this.getCurrentMap();
23435
+ if (!map2) return void 0;
23436
+ let zoneId;
23437
+ let shapeOptions;
23438
+ if (typeof idOrOptions === "string") {
23439
+ zoneId = idOrOptions;
23440
+ if (!options) {
23441
+ console.warn("attachShape: options must be provided when id is specified");
23442
+ return void 0;
23443
+ }
23444
+ shapeOptions = options;
23445
+ } else {
23446
+ zoneId = `zone-${this.id}-${Date.now()}`;
23447
+ shapeOptions = idOrOptions;
23448
+ }
23449
+ const playerEntity = map2.physic.getEntityByUUID(this.id);
23450
+ if (!playerEntity) {
23451
+ console.warn(`Player entity not found in physic engine for player ${this.id}`);
23452
+ return void 0;
23453
+ }
23454
+ let radius;
23455
+ if (shapeOptions.radius !== void 0) {
23456
+ radius = shapeOptions.radius;
23457
+ } else if (shapeOptions.width && shapeOptions.height) {
23458
+ radius = Math.max(shapeOptions.width, shapeOptions.height) / 2;
23459
+ } else {
23460
+ console.warn("attachShape: radius or width/height must be provided");
23461
+ return void 0;
23462
+ }
23463
+ let offset = new Vector2(0, 0);
23464
+ const positioning = shapeOptions.positioning || "default";
23465
+ if (shapeOptions.positioning) {
23466
+ const playerWidth = playerEntity.width || playerEntity.radius * 2 || 32;
23467
+ const playerHeight = playerEntity.height || playerEntity.radius * 2 || 32;
23468
+ switch (shapeOptions.positioning) {
23469
+ case "top":
23470
+ offset = new Vector2(0, -playerHeight / 2);
23471
+ break;
23472
+ case "bottom":
23473
+ offset = new Vector2(0, playerHeight / 2);
23474
+ break;
23475
+ case "left":
23476
+ offset = new Vector2(-playerWidth / 2, 0);
23477
+ break;
23478
+ case "right":
23479
+ offset = new Vector2(playerWidth / 2, 0);
23480
+ break;
23481
+ case "center":
23482
+ default:
23483
+ offset = new Vector2(0, 0);
23484
+ break;
23485
+ }
23486
+ }
23487
+ const zoneManager = map2.physic.getZoneManager();
23488
+ let direction = "down";
23489
+ if (shapeOptions.direction !== void 0) {
23490
+ if (typeof shapeOptions.direction === "string") {
23491
+ direction = shapeOptions.direction;
23492
+ } else {
23493
+ direction = String(shapeOptions.direction);
23494
+ }
23495
+ }
23496
+ const metadata = {};
23497
+ if (shapeOptions.name) {
23498
+ metadata.name = shapeOptions.name;
23499
+ }
23500
+ if (shapeOptions.properties) {
23501
+ metadata.properties = shapeOptions.properties;
23502
+ }
23503
+ const initialX = playerEntity.position.x + offset.x;
23504
+ const initialY = playerEntity.position.y + offset.y;
23505
+ const physicZoneId = zoneManager.createAttachedZone(
23506
+ playerEntity,
23507
+ {
23508
+ radius,
23509
+ angle: shapeOptions.angle ?? 360,
23510
+ direction,
23511
+ limitedByWalls: shapeOptions.limitedByWalls ?? false,
23512
+ offset,
23513
+ metadata: Object.keys(metadata).length > 0 ? metadata : void 0
22616
23514
  },
22617
- (hitIds) => {
22618
- hitIds.forEach((id2) => {
22619
- const event = map.getEvent(id2);
22620
- const zone = physic.getZone(zoneId);
22621
- const player = map.getPlayer(id2);
22622
- if (event) {
22623
- event.execMethod("onOutShape", [zone, this]);
22624
- }
22625
- if (player) this.execMethod("onDetectOutShape", [player, zone]);
22626
- });
23515
+ {
23516
+ onEnter: (entities) => {
23517
+ entities.forEach((entity) => {
23518
+ const event = map2.getEvent(entity.uuid);
23519
+ const player = map2.getPlayer(entity.uuid);
23520
+ if (event) {
23521
+ event.execMethod("onInShape", [shape, this]);
23522
+ if (event._inShapes) {
23523
+ event._inShapes.add(shape);
23524
+ }
23525
+ }
23526
+ if (player) {
23527
+ this.execMethod("onDetectInShape", [player, shape]);
23528
+ if (player._inShapes) {
23529
+ player._inShapes.add(shape);
23530
+ }
23531
+ }
23532
+ });
23533
+ },
23534
+ onExit: (entities) => {
23535
+ entities.forEach((entity) => {
23536
+ const event = map2.getEvent(entity.uuid);
23537
+ const player = map2.getPlayer(entity.uuid);
23538
+ if (event) {
23539
+ event.execMethod("onOutShape", [shape, this]);
23540
+ if (event._inShapes) {
23541
+ event._inShapes.delete(shape);
23542
+ }
23543
+ }
23544
+ if (player) {
23545
+ this.execMethod("onDetectOutShape", [player, shape]);
23546
+ if (player._inShapes) {
23547
+ player._inShapes.delete(shape);
23548
+ }
23549
+ }
23550
+ });
23551
+ }
22627
23552
  }
22628
23553
  );
23554
+ const shape = new RpgShape({
23555
+ name: shapeOptions.name || zoneId,
23556
+ positioning,
23557
+ width: shapeOptions.width || radius * 2,
23558
+ height: shapeOptions.height || radius * 2,
23559
+ x: initialX,
23560
+ y: initialY,
23561
+ properties: shapeOptions.properties || {},
23562
+ playerOwner: this,
23563
+ physicZoneId,
23564
+ map: map2
23565
+ });
23566
+ this._zoneIdMap = this._zoneIdMap || /* @__PURE__ */ new Map();
23567
+ this._zoneIdMap.set(zoneId, physicZoneId);
23568
+ this._attachedShapes.set(zoneId, shape);
23569
+ const updateShapePosition = () => {
23570
+ const currentEntity = map2.physic.getEntityByUUID(this.id);
23571
+ if (currentEntity) {
23572
+ const zoneInfo = zoneManager.getZone(physicZoneId);
23573
+ if (zoneInfo) {
23574
+ shape._updatePosition(zoneInfo.position.x, zoneInfo.position.y);
23575
+ }
23576
+ }
23577
+ };
23578
+ playerEntity.onPositionChange(() => {
23579
+ updateShapePosition();
23580
+ });
23581
+ return shape;
23582
+ }
23583
+ /**
23584
+ * Get all shapes attached to this player
23585
+ *
23586
+ * Returns all shapes that were created using `attachShape()` on this player.
23587
+ *
23588
+ * @returns Array of RpgShape instances attached to this player
23589
+ *
23590
+ * @example
23591
+ * ```ts
23592
+ * player.attachShape("vision", { radius: 150 });
23593
+ * player.attachShape("detection", { radius: 100 });
23594
+ *
23595
+ * const shapes = player.getShapes();
23596
+ * console.log(shapes.length); // 2
23597
+ * ```
23598
+ */
23599
+ getShapes() {
23600
+ return Array.from(this._attachedShapes.values());
23601
+ }
23602
+ /**
23603
+ * Get all shapes where this player is currently located
23604
+ *
23605
+ * Returns all shapes (from any player/event) where this player is currently inside.
23606
+ * This is updated automatically when the player enters or exits shapes.
23607
+ *
23608
+ * @returns Array of RpgShape instances where this player is located
23609
+ *
23610
+ * @example
23611
+ * ```ts
23612
+ * // Another player has a detection zone
23613
+ * otherPlayer.attachShape("detection", { radius: 200 });
23614
+ *
23615
+ * // Check if this player is in any shape
23616
+ * const inShapes = player.getInShapes();
23617
+ * if (inShapes.length > 0) {
23618
+ * console.log("Player is being detected!");
23619
+ * }
23620
+ * ```
23621
+ */
23622
+ getInShapes() {
23623
+ return Array.from(this._inShapes);
22629
23624
  }
22630
23625
  /**
22631
23626
  * Show a temporary component animation on this player
@@ -22645,29 +23640,102 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
22645
23640
  * color: "red"
22646
23641
  * });
22647
23642
  *
22648
- * // Show a heal animation
22649
- * player.showComponentAnimation("heal", {
22650
- * amount: 50
22651
- * });
23643
+ * // Show a heal animation
23644
+ * player.showComponentAnimation("heal", {
23645
+ * amount: 50
23646
+ * });
23647
+ * ```
23648
+ */
23649
+ showComponentAnimation(id, params = {}) {
23650
+ const map2 = this.getCurrentMap();
23651
+ if (!map2) return;
23652
+ map2.$broadcast({
23653
+ type: "showComponentAnimation",
23654
+ value: {
23655
+ id,
23656
+ params,
23657
+ object: this.id
23658
+ }
23659
+ });
23660
+ }
23661
+ showHit(text) {
23662
+ this.showComponentAnimation("hit", {
23663
+ text,
23664
+ direction: this.direction()
23665
+ });
23666
+ }
23667
+ /**
23668
+ * Play a sound on the client side for this player only
23669
+ *
23670
+ * This method emits an event to play a sound only for this specific player.
23671
+ * The sound must be defined on the client side (in the client module configuration).
23672
+ *
23673
+ * ## Design
23674
+ *
23675
+ * The sound is sent only to this player's client connection, making it ideal
23676
+ * for personal feedback sounds like UI interactions, notifications, or personal
23677
+ * achievements. For map-wide sounds that all players should hear, use `map.playSound()` instead.
23678
+ *
23679
+ * @param soundId - Sound identifier, defined on the client side
23680
+ * @param options - Optional sound configuration
23681
+ * @param options.volume - Volume level (0.0 to 1.0, default: 1.0)
23682
+ * @param options.loop - Whether the sound should loop (default: false)
23683
+ *
23684
+ * @example
23685
+ * ```ts
23686
+ * // Play a sound for this player only (default behavior)
23687
+ * player.playSound("item-pickup");
23688
+ *
23689
+ * // Play a sound with volume and loop
23690
+ * player.playSound("background-music", {
23691
+ * volume: 0.5,
23692
+ * loop: true
23693
+ * });
23694
+ *
23695
+ * // Play a notification sound at low volume
23696
+ * player.playSound("notification", { volume: 0.3 });
23697
+ * ```
23698
+ */
23699
+ playSound(soundId, options) {
23700
+ const map2 = this.getCurrentMap();
23701
+ if (!map2) return;
23702
+ const data = {
23703
+ soundId
23704
+ };
23705
+ if (options) {
23706
+ if (options.volume !== void 0) {
23707
+ data.volume = Math.max(0, Math.min(1, options.volume));
23708
+ }
23709
+ if (options.loop !== void 0) {
23710
+ data.loop = options.loop;
23711
+ }
23712
+ }
23713
+ this.emit("playSound", data);
23714
+ }
23715
+ /**
23716
+ * Stop a sound that is currently playing for this player
23717
+ *
23718
+ * This method stops a sound that was previously started with `playSound()`.
23719
+ * The sound must be defined on the client side.
23720
+ *
23721
+ * @param soundId - Sound identifier to stop
23722
+ *
23723
+ * @example
23724
+ * ```ts
23725
+ * // Start a looping background music
23726
+ * player.playSound("background-music", { loop: true });
23727
+ *
23728
+ * // Later, stop it
23729
+ * player.stopSound("background-music");
22652
23730
  * ```
22653
23731
  */
22654
- showComponentAnimation(id, params = {}) {
22655
- const map = this.getCurrentMap();
22656
- if (!map) return;
22657
- map.$broadcast({
22658
- type: "showComponentAnimation",
22659
- value: {
22660
- id,
22661
- params,
22662
- object: this.id
22663
- }
22664
- });
22665
- }
22666
- showHit(text) {
22667
- this.showComponentAnimation("hit", {
22668
- text,
22669
- direction: this.direction()
22670
- });
23732
+ stopSound(soundId) {
23733
+ const map2 = this.getCurrentMap();
23734
+ if (!map2) return;
23735
+ const data = {
23736
+ soundId
23737
+ };
23738
+ this.emit("stopSound", data);
22671
23739
  }
22672
23740
  /**
22673
23741
  * Set the sync schema for the map
@@ -22696,9 +23764,9 @@ class RpgEvent extends RpgPlayer {
22696
23764
  return ret;
22697
23765
  }
22698
23766
  remove() {
22699
- const map = this.getCurrentMap();
22700
- if (!map) return;
22701
- map.removeEvent(this.id);
23767
+ const map2 = this.getCurrentMap();
23768
+ if (!map2) return;
23769
+ map2.removeEvent(this.id);
22702
23770
  }
22703
23771
  }
22704
23772
 
@@ -26027,12 +27095,126 @@ let RpgMap = class extends RpgCommonMap {
26027
27095
  this.dataIsReady$ = new BehaviorSubject$1(void 0);
26028
27096
  this.globalConfig = {};
26029
27097
  this.damageFormulas = {};
27098
+ /** Internal: Map of shapes by name */
27099
+ this._shapes = /* @__PURE__ */ new Map();
27100
+ /** Internal: Map of shape entity UUIDs to RpgShape instances */
27101
+ this._shapeEntities = /* @__PURE__ */ new Map();
26030
27102
  this.hooks.callHooks("server-map-onStart", this).subscribe();
26031
27103
  this.throttleSync = this.isStandalone ? 0 : 50;
26032
27104
  this.throttleStorage = this.isStandalone ? 0 : 1e3;
26033
27105
  this.sessionExpiryTime = 1e3 * 60 * 5;
27106
+ this.setupCollisionDetection();
26034
27107
  this.loop();
26035
27108
  }
27109
+ /**
27110
+ * Setup collision detection between players, events, and shapes
27111
+ *
27112
+ * This method listens to physics collision events and triggers hooks:
27113
+ * - `onPlayerTouch` on events when a player collides with them
27114
+ * - `onInShape` on players and events when they enter a shape
27115
+ * - `onOutShape` on players and events when they exit a shape
27116
+ *
27117
+ * ## Architecture
27118
+ *
27119
+ * Uses the physics engine's collision event system to detect when entities collide.
27120
+ * When a collision is detected:
27121
+ * - Between a player and an event: triggers `onPlayerTouch` on the event
27122
+ * - Between a player/event and a shape: triggers `onInShape`/`onOutShape` hooks
27123
+ *
27124
+ * @example
27125
+ * ```ts
27126
+ * // Event with onPlayerTouch hook
27127
+ * map.createDynamicEvent({
27128
+ * x: 100,
27129
+ * y: 200,
27130
+ * event: {
27131
+ * onPlayerTouch(player) {
27132
+ * console.log(`Player ${player.id} touched this event!`);
27133
+ * }
27134
+ * }
27135
+ * });
27136
+ *
27137
+ * // Player with onInShape hook
27138
+ * const player: RpgPlayerHooks = {
27139
+ * onInShape(player: RpgPlayer, shape: RpgShape) {
27140
+ * console.log('in', player.name, shape.name);
27141
+ * },
27142
+ * onOutShape(player: RpgPlayer, shape: RpgShape) {
27143
+ * console.log('out', player.name, shape.name);
27144
+ * }
27145
+ * };
27146
+ * ```
27147
+ */
27148
+ setupCollisionDetection() {
27149
+ const activeCollisions = /* @__PURE__ */ new Set();
27150
+ const activeShapeCollisions = /* @__PURE__ */ new Set();
27151
+ this.physic.getEvents().onCollisionEnter((collision) => {
27152
+ const entityA = collision.entityA;
27153
+ const entityB = collision.entityB;
27154
+ const collisionKey = entityA.uuid < entityB.uuid ? `${entityA.uuid}-${entityB.uuid}` : `${entityB.uuid}-${entityA.uuid}`;
27155
+ if (activeCollisions.has(collisionKey)) {
27156
+ return;
27157
+ }
27158
+ const shapeA = this._shapeEntities.get(entityA.uuid);
27159
+ const shapeB = this._shapeEntities.get(entityB.uuid);
27160
+ if (shapeA || shapeB) {
27161
+ const shape = shapeA || shapeB;
27162
+ const otherEntity = shapeA ? entityB : entityA;
27163
+ if (shape) {
27164
+ const shapeKey = `${otherEntity.uuid}-${shape.name}`;
27165
+ if (!activeShapeCollisions.has(shapeKey)) {
27166
+ activeShapeCollisions.add(shapeKey);
27167
+ const player2 = this.getPlayer(otherEntity.uuid);
27168
+ const event2 = this.getEvent(otherEntity.uuid);
27169
+ if (player2) {
27170
+ player2.execMethod("onInShape", [player2, shape]);
27171
+ }
27172
+ if (event2) {
27173
+ event2.execMethod("onInShape", [shape, player2 || event2]);
27174
+ }
27175
+ }
27176
+ }
27177
+ return;
27178
+ }
27179
+ const player = this.getPlayer(entityA.uuid) || this.getPlayer(entityB.uuid);
27180
+ if (!player) {
27181
+ return;
27182
+ }
27183
+ const eventId = player.id === entityA.uuid ? entityB.uuid : entityA.uuid;
27184
+ const event = this.getEvent(eventId);
27185
+ if (event) {
27186
+ activeCollisions.add(collisionKey);
27187
+ event.execMethod("onPlayerTouch", [player]);
27188
+ }
27189
+ });
27190
+ this.physic.getEvents().onCollisionExit((collision) => {
27191
+ const entityA = collision.entityA;
27192
+ const entityB = collision.entityB;
27193
+ const collisionKey = entityA.uuid < entityB.uuid ? `${entityA.uuid}-${entityB.uuid}` : `${entityB.uuid}-${entityA.uuid}`;
27194
+ const shapeA = this._shapeEntities.get(entityA.uuid);
27195
+ const shapeB = this._shapeEntities.get(entityB.uuid);
27196
+ if (shapeA || shapeB) {
27197
+ const shape = shapeA || shapeB;
27198
+ const otherEntity = shapeA ? entityB : entityA;
27199
+ if (shape) {
27200
+ const shapeKey = `${otherEntity.uuid}-${shape.name}`;
27201
+ if (activeShapeCollisions.has(shapeKey)) {
27202
+ activeShapeCollisions.delete(shapeKey);
27203
+ const player = this.getPlayer(otherEntity.uuid);
27204
+ const event = this.getEvent(otherEntity.uuid);
27205
+ if (player) {
27206
+ player.execMethod("onOutShape", [player, shape]);
27207
+ }
27208
+ if (event) {
27209
+ event.execMethod("onOutShape", [shape, player || event]);
27210
+ }
27211
+ }
27212
+ }
27213
+ return;
27214
+ }
27215
+ activeCollisions.delete(collisionKey);
27216
+ });
27217
+ }
26036
27218
  // autoload by @signe/room
26037
27219
  interceptorPacket(player, packet, conn) {
26038
27220
  let obj = {};
@@ -26069,7 +27251,6 @@ let RpgMap = class extends RpgCommonMap {
26069
27251
  player._onInit();
26070
27252
  this.dataIsReady$.pipe(
26071
27253
  finalize$1(() => {
26072
- player.applyFrames();
26073
27254
  this.hooks.callHooks("server-player-onJoinMap", player, this).subscribe();
26074
27255
  })
26075
27256
  ).subscribe();
@@ -26189,8 +27370,16 @@ let RpgMap = class extends RpgCommonMap {
26189
27370
  * This method processes all pending inputs for a player while performing
26190
27371
  * anti-cheat validation to prevent time manipulation and frame skipping.
26191
27372
  * It validates the time deltas between inputs and ensures they are within
26192
- * acceptable ranges. After processing, it saves the last frame position
26193
- * for use in packet interception.
27373
+ * acceptable ranges.
27374
+ *
27375
+ * ## Architecture
27376
+ *
27377
+ * **Important**: This method only updates entity velocities - it does NOT step
27378
+ * the physics engine. Physics simulation is handled centrally by the game loop
27379
+ * (`tick$` -> `runFixedTicks`). This ensures:
27380
+ * - Consistent physics timing (60fps fixed timestep)
27381
+ * - No double-stepping when multiple inputs are processed
27382
+ * - Deterministic physics regardless of input frequency
26194
27383
  *
26195
27384
  * @param playerId - The ID of the player to process inputs for
26196
27385
  * @param controls - Optional anti-cheat configuration
@@ -26272,7 +27461,6 @@ let RpgMap = class extends RpgCommonMap {
26272
27461
  lastProcessedFrame = input.frame;
26273
27462
  }
26274
27463
  if (hasProcessedInputs) {
26275
- this.forceSingleTick();
26276
27464
  player.lastProcessedInputTs = lastProcessedTime;
26277
27465
  } else {
26278
27466
  const idleTimeout = Math.max(config.minTimeBetweenInputs * 4, 50);
@@ -26282,7 +27470,6 @@ let RpgMap = class extends RpgCommonMap {
26282
27470
  player.lastProcessedInputTs = 0;
26283
27471
  }
26284
27472
  }
26285
- player.applyFrames();
26286
27473
  return {
26287
27474
  player,
26288
27475
  inputs: processedInputs
@@ -26419,7 +27606,6 @@ let RpgMap = class extends RpgCommonMap {
26419
27606
  eventInstance.context = context$1;
26420
27607
  eventInstance.x.set(x);
26421
27608
  eventInstance.y.set(y);
26422
- eventInstance.applyFrames();
26423
27609
  if (event.name) eventInstance.name.set(event.name);
26424
27610
  this.events()[id] = eventInstance;
26425
27611
  await eventInstance.execMethod("onInit");
@@ -26544,6 +27730,277 @@ let RpgMap = class extends RpgCommonMap {
26544
27730
  }, holder);
26545
27731
  }
26546
27732
  }
27733
+ /**
27734
+ * Create a shape dynamically on the map
27735
+ *
27736
+ * This method creates a static hitbox on the map that can be used for
27737
+ * collision detection, area triggers, or visual boundaries. The shape is
27738
+ * backed by the physics engine's static entity system for accurate collision detection.
27739
+ *
27740
+ * ## Architecture
27741
+ *
27742
+ * Creates a static entity (hitbox) in the physics engine at the specified position and size.
27743
+ * The shape is stored internally and can be retrieved by name. When players or events
27744
+ * collide with this hitbox, the `onInShape` and `onOutShape` hooks are automatically
27745
+ * triggered on both the player and the event.
27746
+ *
27747
+ * @param obj - Shape configuration object
27748
+ * @param obj.x - X position of the shape (top-left corner) (required)
27749
+ * @param obj.y - Y position of the shape (top-left corner) (required)
27750
+ * @param obj.width - Width of the shape in pixels (required)
27751
+ * @param obj.height - Height of the shape in pixels (required)
27752
+ * @param obj.name - Name of the shape (optional, auto-generated if not provided)
27753
+ * @param obj.z - Z position/depth for rendering (optional)
27754
+ * @param obj.color - Color in hexadecimal format, shared with client (optional)
27755
+ * @param obj.collision - Whether the shape has collision (optional)
27756
+ * @param obj.properties - Additional custom properties (optional)
27757
+ * @returns The created RpgShape instance
27758
+ *
27759
+ * @example
27760
+ * ```ts
27761
+ * // Create a simple rectangular shape
27762
+ * const shape = map.createShape({
27763
+ * x: 100,
27764
+ * y: 200,
27765
+ * width: 50,
27766
+ * height: 50,
27767
+ * name: "spawn-zone"
27768
+ * });
27769
+ *
27770
+ * // Create a shape with visual properties
27771
+ * const triggerZone = map.createShape({
27772
+ * x: 300,
27773
+ * y: 400,
27774
+ * width: 100,
27775
+ * height: 100,
27776
+ * name: "treasure-area",
27777
+ * color: "#FFD700",
27778
+ * z: 1,
27779
+ * collision: false,
27780
+ * properties: {
27781
+ * type: "treasure",
27782
+ * value: 100
27783
+ * }
27784
+ * });
27785
+ *
27786
+ * // Player hooks will be triggered automatically
27787
+ * const player: RpgPlayerHooks = {
27788
+ * onInShape(player: RpgPlayer, shape: RpgShape) {
27789
+ * console.log('in', player.name, shape.name);
27790
+ * },
27791
+ * onOutShape(player: RpgPlayer, shape: RpgShape) {
27792
+ * console.log('out', player.name, shape.name);
27793
+ * }
27794
+ * };
27795
+ * ```
27796
+ */
27797
+ createShape(obj) {
27798
+ const { x, y, width, height } = obj;
27799
+ if (typeof x !== "number" || typeof y !== "number") {
27800
+ throw new Error("Shape x and y must be numbers");
27801
+ }
27802
+ if (typeof width !== "number" || width <= 0) {
27803
+ throw new Error("Shape width must be a positive number");
27804
+ }
27805
+ if (typeof height !== "number" || height <= 0) {
27806
+ throw new Error("Shape height must be a positive number");
27807
+ }
27808
+ const name = obj.name || generateShortUUID$2();
27809
+ if (this._shapes.has(name)) {
27810
+ throw new Error(`Shape with name "${name}" already exists`);
27811
+ }
27812
+ const centerX = x + width / 2;
27813
+ const centerY = y + height / 2;
27814
+ const entityId = `shape-${name}`;
27815
+ const entity = this.physic.createEntity({
27816
+ uuid: entityId,
27817
+ position: { x: centerX, y: centerY },
27818
+ width,
27819
+ height,
27820
+ mass: Infinity,
27821
+ // Static entity
27822
+ state: EntityState.Static,
27823
+ restitution: 0
27824
+ // No bounce
27825
+ });
27826
+ entity.freeze();
27827
+ const properties = {
27828
+ ...obj.properties || {}
27829
+ };
27830
+ if (obj.z !== void 0) properties.z = obj.z;
27831
+ if (obj.color !== void 0) properties.color = obj.color;
27832
+ if (obj.collision !== void 0) properties.collision = obj.collision;
27833
+ const shape = new RpgShape({
27834
+ name,
27835
+ positioning: "default",
27836
+ width,
27837
+ height,
27838
+ x: centerX,
27839
+ y: centerY,
27840
+ properties,
27841
+ playerOwner: void 0,
27842
+ // Static shapes are not attached to players
27843
+ physicZoneId: entityId,
27844
+ // Store entity UUID for reference
27845
+ map: this
27846
+ });
27847
+ this._shapes.set(name, shape);
27848
+ this._shapeEntities.set(entityId, shape);
27849
+ return shape;
27850
+ }
27851
+ /**
27852
+ * Delete a shape from the map
27853
+ *
27854
+ * Removes a shape by its name and cleans up the associated static hitbox entity.
27855
+ * If the shape doesn't exist, the method does nothing.
27856
+ *
27857
+ * @param name - Name of the shape to remove
27858
+ * @returns void
27859
+ *
27860
+ * @example
27861
+ * ```ts
27862
+ * // Create and then remove a shape
27863
+ * const shape = map.createShape({
27864
+ * x: 100,
27865
+ * y: 200,
27866
+ * width: 50,
27867
+ * height: 50,
27868
+ * name: "temp-zone"
27869
+ * });
27870
+ *
27871
+ * // Later, remove it
27872
+ * map.removeShape("temp-zone");
27873
+ * ```
27874
+ */
27875
+ removeShape(name) {
27876
+ const shape = this._shapes.get(name);
27877
+ if (!shape) {
27878
+ return;
27879
+ }
27880
+ const entityId = shape._physicZoneId;
27881
+ const entity = this.physic.getEntityByUUID(entityId);
27882
+ if (entity) {
27883
+ this.physic.removeEntity(entity);
27884
+ }
27885
+ this._shapes.delete(name);
27886
+ this._shapeEntities.delete(entityId);
27887
+ }
27888
+ /**
27889
+ * Get all shapes on the map
27890
+ *
27891
+ * Returns an array of all shapes that have been created on this map,
27892
+ * regardless of whether they are static shapes or player-attached shapes.
27893
+ *
27894
+ * @returns Array of RpgShape instances
27895
+ *
27896
+ * @example
27897
+ * ```ts
27898
+ * // Create multiple shapes
27899
+ * map.createShape({ x: 0, y: 0, width: 50, height: 50, name: "zone1" });
27900
+ * map.createShape({ x: 100, y: 100, width: 50, height: 50, name: "zone2" });
27901
+ *
27902
+ * // Get all shapes
27903
+ * const allShapes = map.getShapes();
27904
+ * console.log(allShapes.length); // 2
27905
+ * ```
27906
+ */
27907
+ getShapes() {
27908
+ return Array.from(this._shapes.values());
27909
+ }
27910
+ /**
27911
+ * Get a shape by its name
27912
+ *
27913
+ * Returns a shape with the specified name, or undefined if no shape
27914
+ * with that name exists on the map.
27915
+ *
27916
+ * @param name - Name of the shape to retrieve
27917
+ * @returns The RpgShape instance, or undefined if not found
27918
+ *
27919
+ * @example
27920
+ * ```ts
27921
+ * // Create a shape with a specific name
27922
+ * map.createShape({
27923
+ * x: 100,
27924
+ * y: 200,
27925
+ * width: 50,
27926
+ * height: 50,
27927
+ * name: "spawn-point"
27928
+ * });
27929
+ *
27930
+ * // Retrieve it later
27931
+ * const spawnZone = map.getShape("spawn-point");
27932
+ * if (spawnZone) {
27933
+ * console.log(`Spawn zone at (${spawnZone.x}, ${spawnZone.y})`);
27934
+ * }
27935
+ * ```
27936
+ */
27937
+ getShape(name) {
27938
+ return this._shapes.get(name);
27939
+ }
27940
+ /**
27941
+ * Play a sound for all players on the map
27942
+ *
27943
+ * This method plays a sound for all players currently on the map by iterating
27944
+ * over each player and calling `player.playSound()`. The sound must be defined
27945
+ * on the client side (in the client module configuration).
27946
+ * This is ideal for environmental sounds, battle music, or map-wide events that
27947
+ * all players should hear simultaneously.
27948
+ *
27949
+ * ## Design
27950
+ *
27951
+ * Iterates over all players on the map and calls `player.playSound()` for each one.
27952
+ * This avoids code duplication and reuses the existing player sound logic.
27953
+ * For player-specific sounds, use `player.playSound()` directly.
27954
+ *
27955
+ * @param soundId - Sound identifier, defined on the client side
27956
+ * @param options - Optional sound configuration
27957
+ * @param options.volume - Volume level (0.0 to 1.0, default: 1.0)
27958
+ * @param options.loop - Whether the sound should loop (default: false)
27959
+ *
27960
+ * @example
27961
+ * ```ts
27962
+ * // Play a sound for all players on the map
27963
+ * map.playSound("explosion");
27964
+ *
27965
+ * // Play background music for everyone with volume and loop
27966
+ * map.playSound("battle-theme", {
27967
+ * volume: 0.7,
27968
+ * loop: true
27969
+ * });
27970
+ *
27971
+ * // Play a door opening sound at low volume
27972
+ * map.playSound("door-open", { volume: 0.4 });
27973
+ * ```
27974
+ */
27975
+ playSound(soundId, options) {
27976
+ const players = this.getPlayers();
27977
+ players.forEach((player) => {
27978
+ player.playSound(soundId, options);
27979
+ });
27980
+ }
27981
+ /**
27982
+ * Stop a sound for all players on the map
27983
+ *
27984
+ * This method stops a sound that was previously started with `map.playSound()`
27985
+ * for all players on the map by iterating over each player and calling `player.stopSound()`.
27986
+ *
27987
+ * @param soundId - Sound identifier to stop
27988
+ *
27989
+ * @example
27990
+ * ```ts
27991
+ * // Start background music for everyone
27992
+ * map.playSound("battle-theme", { loop: true });
27993
+ *
27994
+ * // Later, stop it for everyone
27995
+ * map.stopSound("battle-theme");
27996
+ * ```
27997
+ */
27998
+ stopSound(soundId) {
27999
+ const players = this.getPlayers();
28000
+ players.forEach((player) => {
28001
+ player.stopSound(soundId);
28002
+ });
28003
+ }
26547
28004
  };
26548
28005
  __decorateClass$1([
26549
28006
  users$1(RpgPlayer)
@@ -26646,6 +28103,234 @@ function createServer(options) {
26646
28103
  };
26647
28104
  }
26648
28105
 
28106
+ const Components = {
28107
+ /**
28108
+ * Create a text component
28109
+ *
28110
+ * Creates a text component that displays text with optional styling.
28111
+ * Supports template strings with placeholders like {name}, {hp}, etc.
28112
+ * that are replaced with actual player property values.
28113
+ *
28114
+ * ## Design
28115
+ *
28116
+ * Text components use template strings to allow dynamic content without
28117
+ * resending the entire component structure when values change. Only the
28118
+ * property values are synchronized, reducing bandwidth usage.
28119
+ *
28120
+ * @param text - Text to display, can include placeholders like {name}, {hp}
28121
+ * @param options - Text styling options
28122
+ * @returns Component definition for text
28123
+ *
28124
+ * @example
28125
+ * ```ts
28126
+ * // Simple text
28127
+ * Components.text('Player Name');
28128
+ *
28129
+ * // Text with placeholder
28130
+ * Components.text('{name}');
28131
+ *
28132
+ * // Text with styling
28133
+ * Components.text('{name}', {
28134
+ * fill: '#000000',
28135
+ * fontSize: 20
28136
+ * });
28137
+ * ```
28138
+ */
28139
+ text(value, style) {
28140
+ return {
28141
+ type: "text",
28142
+ value,
28143
+ style
28144
+ };
28145
+ },
28146
+ /**
28147
+ * Create an HP bar component
28148
+ *
28149
+ * Creates a health point bar that automatically displays the player's
28150
+ * current HP relative to their maximum HP. The bar updates automatically
28151
+ * as HP changes.
28152
+ *
28153
+ * ## Design
28154
+ *
28155
+ * HP bars read from the player's hp and param.maxHp properties. The
28156
+ * bar can optionally display text above it showing current, max, or
28157
+ * percentage values.
28158
+ *
28159
+ * @param options - Bar styling options
28160
+ * @param text - Optional text to display above the bar. Can use placeholders:
28161
+ * - {$current} - Current HP value
28162
+ * - {$max} - Maximum HP value
28163
+ * - {$percent} - Percentage value
28164
+ * Set to null to hide text
28165
+ * @returns Component definition for HP bar
28166
+ *
28167
+ * @example
28168
+ * ```ts
28169
+ * // Simple HP bar
28170
+ * Components.hpBar();
28171
+ *
28172
+ * // HP bar with percentage text
28173
+ * Components.hpBar({}, '{$percent}%');
28174
+ *
28175
+ * // HP bar with custom styling
28176
+ * Components.hpBar({
28177
+ * fillColor: '#ff0000',
28178
+ * height: 8
28179
+ * });
28180
+ * ```
28181
+ */
28182
+ hpBar(style, text) {
28183
+ return {
28184
+ type: "hpBar",
28185
+ style,
28186
+ text: text ?? void 0
28187
+ };
28188
+ },
28189
+ /**
28190
+ * Create an SP bar component
28191
+ *
28192
+ * Creates a skill point bar that automatically displays the player's
28193
+ * current SP relative to their maximum SP. The bar updates automatically
28194
+ * as SP changes.
28195
+ *
28196
+ * @param style - Bar styling options
28197
+ * @param text - Optional text to display above the bar. Can use placeholders:
28198
+ * - {$current} - Current SP value
28199
+ * - {$max} - Maximum SP value
28200
+ * - {$percent} - Percentage value
28201
+ * Set to null to hide text
28202
+ * @returns Component definition for SP bar
28203
+ *
28204
+ * @example
28205
+ * ```ts
28206
+ * // Simple SP bar
28207
+ * Components.spBar();
28208
+ *
28209
+ * // SP bar with text
28210
+ * Components.spBar({}, 'SP: {$current}/{$max}');
28211
+ * ```
28212
+ */
28213
+ spBar(style, text) {
28214
+ return {
28215
+ type: "spBar",
28216
+ style,
28217
+ text: text ?? void 0
28218
+ };
28219
+ },
28220
+ /**
28221
+ * Create a custom bar component
28222
+ *
28223
+ * Creates a bar that displays a custom property value relative to a maximum.
28224
+ * Useful for displaying custom resources like wood, mana, energy, etc.
28225
+ *
28226
+ * @param current - Property path for current value (e.g., 'wood', 'mana')
28227
+ * @param max - Property path for maximum value (e.g., 'param.maxWood', 'param.maxMana')
28228
+ * @param style - Bar styling options
28229
+ * @param text - Optional text to display above the bar. Can use placeholders:
28230
+ * - {$current} - Current value
28231
+ * - {$max} - Maximum value
28232
+ * - {$percent} - Percentage value
28233
+ * Set to null to hide text
28234
+ * @returns Component definition for custom bar
28235
+ *
28236
+ * @example
28237
+ * ```ts
28238
+ * // Bar for custom property
28239
+ * Components.bar('wood', 'param.maxWood');
28240
+ *
28241
+ * // Bar with text
28242
+ * Components.bar('mana', 'param.maxMana', {}, 'Mana: {$current}/{$max}');
28243
+ * ```
28244
+ */
28245
+ bar(current, max, style, text) {
28246
+ return {
28247
+ type: "bar",
28248
+ current,
28249
+ max,
28250
+ style,
28251
+ text: text ?? void 0
28252
+ };
28253
+ },
28254
+ /**
28255
+ * Create a shape component
28256
+ *
28257
+ * Creates a geometric shape that can be displayed above or below the player.
28258
+ * Useful for visual indicators, backgrounds, or decorative elements.
28259
+ *
28260
+ * @param value - Shape configuration options
28261
+ * @returns Component definition for shape
28262
+ *
28263
+ * @example
28264
+ * ```ts
28265
+ * // Circle shape
28266
+ * Components.shape({
28267
+ * fill: '#ffffff',
28268
+ * type: 'circle',
28269
+ * radius: 10
28270
+ * });
28271
+ *
28272
+ * // Rectangle shape
28273
+ * Components.shape({
28274
+ * fill: '#ff0000',
28275
+ * type: 'rectangle',
28276
+ * width: 32,
28277
+ * height: 32
28278
+ * });
28279
+ *
28280
+ * // Using parameters
28281
+ * Components.shape({
28282
+ * fill: '#ffffff',
28283
+ * type: 'circle',
28284
+ * radius: 'hp' // radius will be the same as hp value
28285
+ * });
28286
+ * ```
28287
+ */
28288
+ shape(value) {
28289
+ return {
28290
+ type: "shape",
28291
+ value
28292
+ };
28293
+ },
28294
+ /**
28295
+ * Create an image component
28296
+ *
28297
+ * Displays an image from a URL or spritesheet identifier.
28298
+ *
28299
+ * @param value - Image source URL or spritesheet identifier
28300
+ * @returns Component definition for image
28301
+ *
28302
+ * @example
28303
+ * ```ts
28304
+ * Components.image('mygraphic.png');
28305
+ * ```
28306
+ */
28307
+ image(value) {
28308
+ return {
28309
+ type: "image",
28310
+ value
28311
+ };
28312
+ },
28313
+ /**
28314
+ * Create a tile component
28315
+ *
28316
+ * Displays a tile from a tileset by ID.
28317
+ *
28318
+ * @param value - Tile ID in the tileset
28319
+ * @returns Component definition for tile
28320
+ *
28321
+ * @example
28322
+ * ```ts
28323
+ * Components.tile(3); // Use tile #3
28324
+ * ```
28325
+ */
28326
+ tile(value) {
28327
+ return {
28328
+ type: "tile",
28329
+ value
28330
+ };
28331
+ }
28332
+ };
28333
+
26649
28334
  function provideServerModules(modules) {
26650
28335
  return provideModules(modules, "server", (modules2, context) => {
26651
28336
  const mainModuleServer = findModules(context, "Server");
@@ -26696,5 +28381,5 @@ function provideServerModules(modules) {
26696
28381
  });
26697
28382
  }
26698
28383
 
26699
- export { AGI, AGI_CURVE, ATK, ArraySubject$1 as ArraySubject, COEFFICIENT_ELEMENTS, DAMAGE_CRITICAL, DAMAGE_GUARD, DAMAGE_PHYSIC, DAMAGE_SKILL, DEX, DEX_CURVE, DialogGui, DialogPosition, Frequency, Gui, INT, INT_CURVE, MAXHP, MAXHP_CURVE, MAXSP, MAXSP_CURVE, MenuGui, Move, NotificationGui, ObjectSubject$1 as ObjectSubject, PDEF, RpgEvent, RpgMap, RpgPlayer, RpgServerEngine, SDEF, STR, STR_CURVE, ShopGui, Speed, WithMoveManager, clearInject, computed$1 as computed, context, createServer, effect$1 as effect, inject, isArraySubject$1 as isArraySubject, isComputed$1 as isComputed, isObjectSubject$1 as isObjectSubject, isSignal$1 as isSignal, provideServerModules, setInject, signal$1 as signal, untracked$1 as untracked };
28384
+ export { AGI, AGI_CURVE, ATK, ArraySubject$1 as ArraySubject, COEFFICIENT_ELEMENTS, Components, DAMAGE_CRITICAL, DAMAGE_GUARD, DAMAGE_PHYSIC, DAMAGE_SKILL, DEX, DEX_CURVE, DialogGui, DialogPosition, Frequency, Gui, INT, INT_CURVE, MAXHP, MAXHP_CURVE, MAXSP, MAXSP_CURVE, MenuGui, Move, NotificationGui, ObjectSubject$1 as ObjectSubject, PDEF, RpgEvent, RpgMap, RpgPlayer, RpgServerEngine, RpgShape, SDEF, STR, STR_CURVE, ShopGui, Speed, WithMoveManager, clearInject, computed$1 as computed, context, createServer, effect$1 as effect, inject, isArraySubject$1 as isArraySubject, isComputed$1 as isComputed, isObjectSubject$1 as isObjectSubject, isSignal$1 as isSignal, provideServerModules, setInject, signal$1 as signal, untracked$1 as untracked };
26700
28385
  //# sourceMappingURL=index.js.map