@rpgjs/server 5.0.0-alpha.16 → 5.0.0-alpha.17
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 +170 -26
- package/dist/index.js.map +1 -1
- package/dist/rooms/map.d.ts +10 -2
- package/package.json +3 -3
- package/src/Player/MoveManager.ts +1 -1
- package/src/rooms/map.ts +14 -13
package/dist/index.js
CHANGED
|
@@ -11349,10 +11349,10 @@ __decorateClass$3([
|
|
|
11349
11349
|
sync()
|
|
11350
11350
|
], RpgCommonPlayer.prototype, "type");
|
|
11351
11351
|
__decorateClass$3([
|
|
11352
|
-
|
|
11352
|
+
sync()
|
|
11353
11353
|
], RpgCommonPlayer.prototype, "x");
|
|
11354
11354
|
__decorateClass$3([
|
|
11355
|
-
|
|
11355
|
+
sync()
|
|
11356
11356
|
], RpgCommonPlayer.prototype, "y");
|
|
11357
11357
|
__decorateClass$3([
|
|
11358
11358
|
sync()
|
|
@@ -14274,6 +14274,11 @@ class CollisionResolver {
|
|
|
14274
14274
|
/**
|
|
14275
14275
|
* Separates two entities by moving them apart
|
|
14276
14276
|
*
|
|
14277
|
+
* This method applies position corrections to resolve penetration between
|
|
14278
|
+
* colliding entities. After applying corrections, it notifies position change
|
|
14279
|
+
* handlers to ensure proper synchronization with game logic (e.g., updating
|
|
14280
|
+
* owner.x/y signals for network sync).
|
|
14281
|
+
*
|
|
14277
14282
|
* @param entityA - First entity
|
|
14278
14283
|
* @param entityB - Second entity
|
|
14279
14284
|
* @param normal - Separation normal (from A to B)
|
|
@@ -14292,9 +14297,11 @@ class CollisionResolver {
|
|
|
14292
14297
|
const correctionB = normal.mul(correction * (entityB.invMass / totalInvMass));
|
|
14293
14298
|
if (!entityA.isStatic()) {
|
|
14294
14299
|
entityA.position.addInPlace(correctionA);
|
|
14300
|
+
entityA.notifyPositionChange();
|
|
14295
14301
|
}
|
|
14296
14302
|
if (!entityB.isStatic()) {
|
|
14297
14303
|
entityB.position.addInPlace(correctionB);
|
|
14304
|
+
entityB.notifyPositionChange();
|
|
14298
14305
|
}
|
|
14299
14306
|
}
|
|
14300
14307
|
/**
|
|
@@ -17123,12 +17130,33 @@ class RpgCommonMap {
|
|
|
17123
17130
|
* It's shared using the share() operator, meaning that all subscribers will receive
|
|
17124
17131
|
* events from a single interval rather than creating multiple intervals.
|
|
17125
17132
|
*
|
|
17133
|
+
* ## Physics Loop Architecture
|
|
17134
|
+
*
|
|
17135
|
+
* The physics simulation is centralized in this game loop:
|
|
17136
|
+
*
|
|
17137
|
+
* 1. **Input Processing** (`processInput`): Only updates entity velocities, does NOT step physics
|
|
17138
|
+
* 2. **Game Loop** (`tick$` -> `runFixedTicks`): Executes physics simulation with fixed timestep
|
|
17139
|
+
* 3. **Fixed Timestep Pattern**: Accumulator-based approach ensures deterministic physics
|
|
17140
|
+
*
|
|
17141
|
+
* ```
|
|
17142
|
+
* Input Events ─────────────────────────────────────────────────────────────►
|
|
17143
|
+
* │
|
|
17144
|
+
* ▼ (update velocity only)
|
|
17145
|
+
* ┌─────────────────────────────────────────────────────────────────────────┐
|
|
17146
|
+
* │ Game Loop (tick$) │
|
|
17147
|
+
* │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
|
17148
|
+
* │ │ updateMovements │ → │ stepOneTick │ → │ postTickUpdates │ │
|
|
17149
|
+
* │ │ (apply velocity)│ │ (physics step) │ │ (zones, sync) │ │
|
|
17150
|
+
* │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
|
17151
|
+
* └─────────────────────────────────────────────────────────────────────────┘
|
|
17152
|
+
* ```
|
|
17153
|
+
*
|
|
17126
17154
|
* @example
|
|
17127
17155
|
* ```ts
|
|
17128
|
-
* // Subscribe to the game tick
|
|
17129
|
-
* map.tick$.subscribe(timestamp => {
|
|
17130
|
-
* //
|
|
17131
|
-
* this.
|
|
17156
|
+
* // Subscribe to the game tick for custom updates
|
|
17157
|
+
* map.tick$.subscribe(({ delta, timestamp }) => {
|
|
17158
|
+
* // Custom game logic runs alongside physics
|
|
17159
|
+
* this.updateCustomEntities(delta);
|
|
17132
17160
|
* });
|
|
17133
17161
|
* ```
|
|
17134
17162
|
*/
|
|
@@ -17304,6 +17332,43 @@ class RpgCommonMap {
|
|
|
17304
17332
|
getObjectById(id) {
|
|
17305
17333
|
return this.players()[id] ?? this.events()[id];
|
|
17306
17334
|
}
|
|
17335
|
+
/**
|
|
17336
|
+
* Execute physics simulation with fixed timestep
|
|
17337
|
+
*
|
|
17338
|
+
* This method runs the physics engine using a fixed timestep accumulator pattern.
|
|
17339
|
+
* It ensures deterministic physics regardless of frame rate by:
|
|
17340
|
+
* 1. Accumulating delta time
|
|
17341
|
+
* 2. Running fixed-size physics steps until the accumulator is depleted
|
|
17342
|
+
* 3. Calling `updateMovements()` before each step to apply velocity changes
|
|
17343
|
+
* 4. Running post-tick updates (zones, callbacks) after each step
|
|
17344
|
+
*
|
|
17345
|
+
* ## Architecture
|
|
17346
|
+
*
|
|
17347
|
+
* The physics loop is centralized here and called from `tick$` subscription.
|
|
17348
|
+
* Input processing (`processInput`) only updates entity velocities - it does NOT
|
|
17349
|
+
* step the physics. This ensures:
|
|
17350
|
+
* - Consistent physics timing (60fps fixed timestep)
|
|
17351
|
+
* - No double-stepping when inputs are processed
|
|
17352
|
+
* - Proper accumulator-based interpolation support
|
|
17353
|
+
*
|
|
17354
|
+
* @param deltaMs - Time elapsed since last call in milliseconds
|
|
17355
|
+
* @param hooks - Optional callbacks for before/after each physics step
|
|
17356
|
+
* @returns Number of physics ticks executed
|
|
17357
|
+
*
|
|
17358
|
+
* @example
|
|
17359
|
+
* ```ts
|
|
17360
|
+
* // Called automatically by tick$ subscription
|
|
17361
|
+
* this.tickSubscription = this.tick$.subscribe(({ delta }) => {
|
|
17362
|
+
* this.runFixedTicks(delta);
|
|
17363
|
+
* });
|
|
17364
|
+
*
|
|
17365
|
+
* // Or manually with hooks for debugging
|
|
17366
|
+
* this.runFixedTicks(16, {
|
|
17367
|
+
* beforeStep: () => console.log('Before physics step'),
|
|
17368
|
+
* afterStep: (tick) => console.log(`Physics tick ${tick} completed`)
|
|
17369
|
+
* });
|
|
17370
|
+
* ```
|
|
17371
|
+
*/
|
|
17307
17372
|
runFixedTicks(deltaMs, hooks) {
|
|
17308
17373
|
if (!Number.isFinite(deltaMs) || deltaMs <= 0) {
|
|
17309
17374
|
return 0;
|
|
@@ -17314,12 +17379,46 @@ class RpgCommonMap {
|
|
|
17314
17379
|
while (this.physicsAccumulatorMs >= fixedStepMs) {
|
|
17315
17380
|
this.physicsAccumulatorMs -= fixedStepMs;
|
|
17316
17381
|
hooks?.beforeStep?.();
|
|
17382
|
+
this.physic.updateMovements();
|
|
17317
17383
|
const tick = this.physic.stepOneTick();
|
|
17318
17384
|
executed += 1;
|
|
17385
|
+
this.runPostTickUpdates();
|
|
17319
17386
|
hooks?.afterStep?.(tick);
|
|
17320
17387
|
}
|
|
17321
17388
|
return executed;
|
|
17322
17389
|
}
|
|
17390
|
+
/**
|
|
17391
|
+
* Force a single physics tick outside of the normal game loop
|
|
17392
|
+
*
|
|
17393
|
+
* This method is primarily used for **client-side prediction** where the client
|
|
17394
|
+
* needs to immediately simulate physics in response to local input, rather than
|
|
17395
|
+
* waiting for the next game loop tick.
|
|
17396
|
+
*
|
|
17397
|
+
* ## Use Cases
|
|
17398
|
+
*
|
|
17399
|
+
* - **Client-side prediction**: Immediately simulate player movement for responsive feel
|
|
17400
|
+
* - **Testing**: Force a physics step in unit tests
|
|
17401
|
+
* - **Special effects**: Immediate physics response for specific game events
|
|
17402
|
+
*
|
|
17403
|
+
* ## Important
|
|
17404
|
+
*
|
|
17405
|
+
* This method should NOT be used on the server for normal input processing.
|
|
17406
|
+
* Server-side physics is handled by `runFixedTicks` in the main game loop to ensure
|
|
17407
|
+
* deterministic simulation.
|
|
17408
|
+
*
|
|
17409
|
+
* @param hooks - Optional callbacks for before/after the physics step
|
|
17410
|
+
* @returns The physics tick number
|
|
17411
|
+
*
|
|
17412
|
+
* @example
|
|
17413
|
+
* ```ts
|
|
17414
|
+
* // Client-side: immediately simulate predicted movement
|
|
17415
|
+
* class RpgClientMap extends RpgCommonMap {
|
|
17416
|
+
* stepPredictionTick(): void {
|
|
17417
|
+
* this.forceSingleTick();
|
|
17418
|
+
* }
|
|
17419
|
+
* }
|
|
17420
|
+
* ```
|
|
17421
|
+
*/
|
|
17323
17422
|
forceSingleTick(hooks) {
|
|
17324
17423
|
hooks?.beforeStep?.();
|
|
17325
17424
|
this.physic.updateMovements();
|
|
@@ -17647,23 +17746,60 @@ class RpgCommonMap {
|
|
|
17647
17746
|
* Add a character to the physics world
|
|
17648
17747
|
* @private
|
|
17649
17748
|
*/
|
|
17749
|
+
/**
|
|
17750
|
+
* Add a character entity to the physics world
|
|
17751
|
+
*
|
|
17752
|
+
* Creates a physics entity for a character (player or NPC) with proper position handling.
|
|
17753
|
+
* The owner's x/y signals represent **top-left** coordinates, while the physics entity
|
|
17754
|
+
* uses **center** coordinates internally.
|
|
17755
|
+
*
|
|
17756
|
+
* ## Position System
|
|
17757
|
+
*
|
|
17758
|
+
* - `owner.x()` / `owner.y()` → **top-left** corner of the character's hitbox
|
|
17759
|
+
* - `entity.position` → **center** of the physics collider
|
|
17760
|
+
* - Conversion: `center = topLeft + (size / 2)`
|
|
17761
|
+
*
|
|
17762
|
+
* @param options - Character configuration
|
|
17763
|
+
* @returns The character's unique ID
|
|
17764
|
+
*
|
|
17765
|
+
* @example
|
|
17766
|
+
* ```ts
|
|
17767
|
+
* // Player at top-left position (100, 100) with 32x32 hitbox
|
|
17768
|
+
* // Physics entity will be at center (116, 116)
|
|
17769
|
+
* this.addCharacter({
|
|
17770
|
+
* owner: player,
|
|
17771
|
+
* x: 116, // center X (ignored, uses owner.x())
|
|
17772
|
+
* y: 116, // center Y (ignored, uses owner.y())
|
|
17773
|
+
* kind: "hero"
|
|
17774
|
+
* });
|
|
17775
|
+
* ```
|
|
17776
|
+
*
|
|
17777
|
+
* @private
|
|
17778
|
+
*/
|
|
17650
17779
|
addCharacter(options) {
|
|
17651
17780
|
if (!options || typeof options.owner?.id !== "string") {
|
|
17652
17781
|
throw new Error("Character requires an owner object with a string id");
|
|
17653
17782
|
}
|
|
17654
17783
|
const owner = options.owner;
|
|
17655
17784
|
const id = owner.id;
|
|
17656
|
-
const
|
|
17657
|
-
const
|
|
17658
|
-
const
|
|
17659
|
-
const
|
|
17660
|
-
const
|
|
17661
|
-
const
|
|
17785
|
+
const hitbox = typeof owner.hitbox === "function" ? owner.hitbox() : owner.hitbox;
|
|
17786
|
+
const width = hitbox?.w ?? 32;
|
|
17787
|
+
const height = hitbox?.h ?? 32;
|
|
17788
|
+
const radius = Math.max(width, height) / 2;
|
|
17789
|
+
const topLeftX = owner.x();
|
|
17790
|
+
const topLeftY = owner.y();
|
|
17791
|
+
const centerX = topLeftX + width / 2;
|
|
17792
|
+
const centerY = topLeftY + height / 2;
|
|
17662
17793
|
const isStatic = !!options.isStatic;
|
|
17663
17794
|
const entity = this.physic.createEntity({
|
|
17664
17795
|
uuid: id,
|
|
17665
17796
|
position: { x: centerX, y: centerY },
|
|
17797
|
+
// Use radius for circular collision detection
|
|
17666
17798
|
radius: Math.max(radius, 1),
|
|
17799
|
+
// Also store explicit width/height for consistent position conversions
|
|
17800
|
+
// This ensures getBodyPosition/setBodyPosition use the same dimensions
|
|
17801
|
+
width,
|
|
17802
|
+
height,
|
|
17667
17803
|
mass: options.mass ?? (isStatic ? Infinity : 1),
|
|
17668
17804
|
friction: options.friction ?? 0.4,
|
|
17669
17805
|
linearDamping: isStatic ? 1 : 0.2,
|
|
@@ -17691,17 +17827,21 @@ class RpgCommonMap {
|
|
|
17691
17827
|
owner.animationName.set("stand");
|
|
17692
17828
|
}
|
|
17693
17829
|
});
|
|
17830
|
+
const entityWidth = width;
|
|
17831
|
+
const entityHeight = height;
|
|
17694
17832
|
entity.onPositionChange(({ x, y }) => {
|
|
17695
|
-
const
|
|
17696
|
-
const
|
|
17697
|
-
|
|
17698
|
-
const topLeftY2 = y - height / 2;
|
|
17833
|
+
const topLeftX2 = x - entityWidth / 2;
|
|
17834
|
+
const topLeftY2 = y - entityHeight / 2;
|
|
17835
|
+
let changed = false;
|
|
17699
17836
|
if (typeof owner.x === "function" && typeof owner.x.set === "function") {
|
|
17700
17837
|
owner.x.set(Math.round(topLeftX2));
|
|
17701
|
-
|
|
17838
|
+
changed = true;
|
|
17702
17839
|
}
|
|
17703
17840
|
if (typeof owner.y === "function" && typeof owner.y.set === "function") {
|
|
17704
17841
|
owner.y.set(Math.round(topLeftY2));
|
|
17842
|
+
changed = true;
|
|
17843
|
+
}
|
|
17844
|
+
if (changed) {
|
|
17705
17845
|
owner.applyFrames?.();
|
|
17706
17846
|
}
|
|
17707
17847
|
});
|
|
@@ -17917,11 +18057,11 @@ class RpgCommonMap {
|
|
|
17917
18057
|
setBodyPosition(id, x, y, mode = "center") {
|
|
17918
18058
|
const entity = this.physic.getEntityByUUID(id);
|
|
17919
18059
|
if (!entity) return;
|
|
18060
|
+
const width = entity.width || (entity.radius ? entity.radius * 2 : 32);
|
|
18061
|
+
const height = entity.height || (entity.radius ? entity.radius * 2 : 32);
|
|
17920
18062
|
let centerX = x;
|
|
17921
18063
|
let centerY = y;
|
|
17922
18064
|
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
18065
|
centerX = x + width / 2;
|
|
17926
18066
|
centerY = y + height / 2;
|
|
17927
18067
|
}
|
|
@@ -19173,7 +19313,7 @@ function WithMoveManager(Base) {
|
|
|
19173
19313
|
staticTarget.freeze();
|
|
19174
19314
|
map.moveManager.add(
|
|
19175
19315
|
this.id,
|
|
19176
|
-
new SeekAvoid(engine, () => staticTarget,
|
|
19316
|
+
new SeekAvoid(engine, () => staticTarget, 80, 140, 80, 48)
|
|
19177
19317
|
);
|
|
19178
19318
|
}
|
|
19179
19319
|
/**
|
|
@@ -26069,7 +26209,6 @@ let RpgMap = class extends RpgCommonMap {
|
|
|
26069
26209
|
player._onInit();
|
|
26070
26210
|
this.dataIsReady$.pipe(
|
|
26071
26211
|
finalize$1(() => {
|
|
26072
|
-
player.applyFrames();
|
|
26073
26212
|
this.hooks.callHooks("server-player-onJoinMap", player, this).subscribe();
|
|
26074
26213
|
})
|
|
26075
26214
|
).subscribe();
|
|
@@ -26189,8 +26328,16 @@ let RpgMap = class extends RpgCommonMap {
|
|
|
26189
26328
|
* This method processes all pending inputs for a player while performing
|
|
26190
26329
|
* anti-cheat validation to prevent time manipulation and frame skipping.
|
|
26191
26330
|
* It validates the time deltas between inputs and ensures they are within
|
|
26192
|
-
* acceptable ranges.
|
|
26193
|
-
*
|
|
26331
|
+
* acceptable ranges.
|
|
26332
|
+
*
|
|
26333
|
+
* ## Architecture
|
|
26334
|
+
*
|
|
26335
|
+
* **Important**: This method only updates entity velocities - it does NOT step
|
|
26336
|
+
* the physics engine. Physics simulation is handled centrally by the game loop
|
|
26337
|
+
* (`tick$` -> `runFixedTicks`). This ensures:
|
|
26338
|
+
* - Consistent physics timing (60fps fixed timestep)
|
|
26339
|
+
* - No double-stepping when multiple inputs are processed
|
|
26340
|
+
* - Deterministic physics regardless of input frequency
|
|
26194
26341
|
*
|
|
26195
26342
|
* @param playerId - The ID of the player to process inputs for
|
|
26196
26343
|
* @param controls - Optional anti-cheat configuration
|
|
@@ -26272,7 +26419,6 @@ let RpgMap = class extends RpgCommonMap {
|
|
|
26272
26419
|
lastProcessedFrame = input.frame;
|
|
26273
26420
|
}
|
|
26274
26421
|
if (hasProcessedInputs) {
|
|
26275
|
-
this.forceSingleTick();
|
|
26276
26422
|
player.lastProcessedInputTs = lastProcessedTime;
|
|
26277
26423
|
} else {
|
|
26278
26424
|
const idleTimeout = Math.max(config.minTimeBetweenInputs * 4, 50);
|
|
@@ -26282,7 +26428,6 @@ let RpgMap = class extends RpgCommonMap {
|
|
|
26282
26428
|
player.lastProcessedInputTs = 0;
|
|
26283
26429
|
}
|
|
26284
26430
|
}
|
|
26285
|
-
player.applyFrames();
|
|
26286
26431
|
return {
|
|
26287
26432
|
player,
|
|
26288
26433
|
inputs: processedInputs
|
|
@@ -26419,7 +26564,6 @@ let RpgMap = class extends RpgCommonMap {
|
|
|
26419
26564
|
eventInstance.context = context$1;
|
|
26420
26565
|
eventInstance.x.set(x);
|
|
26421
26566
|
eventInstance.y.set(y);
|
|
26422
|
-
eventInstance.applyFrames();
|
|
26423
26567
|
if (event.name) eventInstance.name.set(event.name);
|
|
26424
26568
|
this.events()[id] = eventInstance;
|
|
26425
26569
|
await eventInstance.execMethod("onInit");
|