@rpgjs/server 5.0.0-alpha.27 → 5.0.0-alpha.28
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/Player/MoveManager.d.ts +115 -50
- package/dist/Player/Player.d.ts +11 -2
- package/dist/Player/SkillManager.d.ts +157 -22
- package/dist/index.js +976 -174
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/Player/MoveManager.ts +659 -213
- package/src/Player/Player.ts +19 -2
- package/src/Player/SkillManager.ts +401 -73
- package/src/rooms/map.ts +18 -0
- package/tests/battle.spec.ts +375 -0
- package/tests/class.spec.ts +274 -0
- package/tests/effect.spec.ts +219 -0
- package/tests/element.spec.ts +221 -0
- package/tests/gold.spec.ts +99 -0
- package/tests/skill.spec.ts +658 -0
- package/tests/state.spec.ts +467 -0
- package/tests/variable.spec.ts +185 -0
package/dist/index.js
CHANGED
|
@@ -9906,8 +9906,81 @@ class RpgCommonPlayer {
|
|
|
9906
9906
|
this.isConnected = signal(false);
|
|
9907
9907
|
// Store intended movement direction (not synced, only used locally)
|
|
9908
9908
|
this._intendedDirection = null;
|
|
9909
|
+
// Direction and animation locking (server-side only, not synced)
|
|
9910
|
+
this._directionFixed = signal(false);
|
|
9911
|
+
this._animationFixed = signal(false);
|
|
9909
9912
|
this.pendingInputs = [];
|
|
9910
9913
|
}
|
|
9914
|
+
/**
|
|
9915
|
+
* Get whether direction changes are locked
|
|
9916
|
+
*
|
|
9917
|
+
* @returns True if direction is locked and cannot be changed automatically
|
|
9918
|
+
*
|
|
9919
|
+
* @example
|
|
9920
|
+
* ```ts
|
|
9921
|
+
* if (player.directionFixed) {
|
|
9922
|
+
* // Direction is locked, won't change automatically
|
|
9923
|
+
* }
|
|
9924
|
+
* ```
|
|
9925
|
+
*/
|
|
9926
|
+
get directionFixed() {
|
|
9927
|
+
return this._directionFixed();
|
|
9928
|
+
}
|
|
9929
|
+
/**
|
|
9930
|
+
* Set whether direction changes are locked
|
|
9931
|
+
*
|
|
9932
|
+
* When set to true, the player's direction will not change automatically
|
|
9933
|
+
* during movement or from physics engine callbacks.
|
|
9934
|
+
*
|
|
9935
|
+
* @param value - True to lock direction, false to allow automatic changes
|
|
9936
|
+
*
|
|
9937
|
+
* @example
|
|
9938
|
+
* ```ts
|
|
9939
|
+
* // Lock direction during a special animation
|
|
9940
|
+
* player.directionFixed = true;
|
|
9941
|
+
* player.setAnimation('attack');
|
|
9942
|
+
* // ... later
|
|
9943
|
+
* player.directionFixed = false;
|
|
9944
|
+
* ```
|
|
9945
|
+
*/
|
|
9946
|
+
set directionFixed(value) {
|
|
9947
|
+
this._directionFixed.set(value);
|
|
9948
|
+
}
|
|
9949
|
+
/**
|
|
9950
|
+
* Get whether animation changes are locked
|
|
9951
|
+
*
|
|
9952
|
+
* @returns True if animation is locked and cannot be changed automatically
|
|
9953
|
+
*
|
|
9954
|
+
* @example
|
|
9955
|
+
* ```ts
|
|
9956
|
+
* if (player.animationFixed) {
|
|
9957
|
+
* // Animation is locked, won't change automatically
|
|
9958
|
+
* }
|
|
9959
|
+
* ```
|
|
9960
|
+
*/
|
|
9961
|
+
get animationFixed() {
|
|
9962
|
+
return this._animationFixed();
|
|
9963
|
+
}
|
|
9964
|
+
/**
|
|
9965
|
+
* Set whether animation changes are locked
|
|
9966
|
+
*
|
|
9967
|
+
* When set to true, the player's animation will not change automatically
|
|
9968
|
+
* during movement or from physics engine callbacks.
|
|
9969
|
+
*
|
|
9970
|
+
* @param value - True to lock animation, false to allow automatic changes
|
|
9971
|
+
*
|
|
9972
|
+
* @example
|
|
9973
|
+
* ```ts
|
|
9974
|
+
* // Lock animation during a special skill
|
|
9975
|
+
* player.animationFixed = true;
|
|
9976
|
+
* player.setAnimation('skill');
|
|
9977
|
+
* // ... later
|
|
9978
|
+
* player.animationFixed = false;
|
|
9979
|
+
* ```
|
|
9980
|
+
*/
|
|
9981
|
+
set animationFixed(value) {
|
|
9982
|
+
this._animationFixed.set(value);
|
|
9983
|
+
}
|
|
9911
9984
|
/**
|
|
9912
9985
|
* Change the player's facing direction
|
|
9913
9986
|
*
|
|
@@ -9915,6 +9988,8 @@ class RpgCommonPlayer {
|
|
|
9915
9988
|
* and directional abilities. This should be called when the player
|
|
9916
9989
|
* intends to move in a specific direction, not when they are pushed
|
|
9917
9990
|
* by physics or sliding.
|
|
9991
|
+
*
|
|
9992
|
+
* If `directionFixed` is true, this method will not change the direction.
|
|
9918
9993
|
*
|
|
9919
9994
|
* @param direction - The new direction to face
|
|
9920
9995
|
*
|
|
@@ -9922,9 +9997,16 @@ class RpgCommonPlayer {
|
|
|
9922
9997
|
* ```ts
|
|
9923
9998
|
* // Player presses right arrow key
|
|
9924
9999
|
* player.changeDirection(Direction.Right);
|
|
10000
|
+
*
|
|
10001
|
+
* // Lock direction to prevent automatic changes
|
|
10002
|
+
* player.directionFixed = true;
|
|
10003
|
+
* player.changeDirection(Direction.Up); // This will be ignored
|
|
9925
10004
|
* ```
|
|
9926
10005
|
*/
|
|
9927
10006
|
changeDirection(direction) {
|
|
10007
|
+
if (this._directionFixed()) {
|
|
10008
|
+
return;
|
|
10009
|
+
}
|
|
9928
10010
|
this.direction.set(direction);
|
|
9929
10011
|
}
|
|
9930
10012
|
/**
|
|
@@ -10759,6 +10841,8 @@ class Entity {
|
|
|
10759
10841
|
this.enterTileHandlers = /* @__PURE__ */ new Set();
|
|
10760
10842
|
this.leaveTileHandlers = /* @__PURE__ */ new Set();
|
|
10761
10843
|
this.canEnterTileHandlers = /* @__PURE__ */ new Set();
|
|
10844
|
+
this.collisionFilterHandlers = /* @__PURE__ */ new Set();
|
|
10845
|
+
this.resolutionFilterHandlers = /* @__PURE__ */ new Set();
|
|
10762
10846
|
this.wasMoving = this.velocity.lengthSquared() > MOVEMENT_EPSILON_SQ;
|
|
10763
10847
|
}
|
|
10764
10848
|
/**
|
|
@@ -11305,9 +11389,46 @@ class Entity {
|
|
|
11305
11389
|
this.angularVelocity = Math.sign(this.angularVelocity) * this.maxAngularVelocity;
|
|
11306
11390
|
}
|
|
11307
11391
|
}
|
|
11392
|
+
/**
|
|
11393
|
+
* Adds a collision filter to this entity
|
|
11394
|
+
*
|
|
11395
|
+
* Collision filters allow dynamic, conditional collision filtering beyond static bitmasks.
|
|
11396
|
+
* Each filter is called when checking if this entity can collide with another.
|
|
11397
|
+
* If any filter returns `false`, the collision is ignored.
|
|
11398
|
+
*
|
|
11399
|
+
* This enables scenarios like:
|
|
11400
|
+
* - Players passing through other players (`throughOtherPlayer`)
|
|
11401
|
+
* - Entities passing through all characters (`through`)
|
|
11402
|
+
* - Custom game-specific collision rules
|
|
11403
|
+
*
|
|
11404
|
+
* @param filter - Function that returns `true` to allow collision, `false` to ignore
|
|
11405
|
+
* @returns Unsubscribe function to remove the filter
|
|
11406
|
+
*
|
|
11407
|
+
* @example
|
|
11408
|
+
* ```typescript
|
|
11409
|
+
* // Allow entity to pass through other players
|
|
11410
|
+
* const unsubscribe = entity.addCollisionFilter((self, other) => {
|
|
11411
|
+
* const otherOwner = (other as any).owner;
|
|
11412
|
+
* if (otherOwner?.type === 'player') {
|
|
11413
|
+
* return false; // No collision with players
|
|
11414
|
+
* }
|
|
11415
|
+
* return true; // Collide with everything else
|
|
11416
|
+
* });
|
|
11417
|
+
*
|
|
11418
|
+
* // Later, remove the filter
|
|
11419
|
+
* unsubscribe();
|
|
11420
|
+
* ```
|
|
11421
|
+
*/
|
|
11422
|
+
addCollisionFilter(filter) {
|
|
11423
|
+
this.collisionFilterHandlers.add(filter);
|
|
11424
|
+
return () => this.collisionFilterHandlers.delete(filter);
|
|
11425
|
+
}
|
|
11308
11426
|
/**
|
|
11309
11427
|
* Checks if this entity can collide with another entity
|
|
11310
11428
|
*
|
|
11429
|
+
* First checks collision masks (bitmask filtering), then executes all registered
|
|
11430
|
+
* collision filters. If any filter returns `false`, the collision is ignored.
|
|
11431
|
+
*
|
|
11311
11432
|
* @param other - Other entity to check
|
|
11312
11433
|
* @returns True if collision is possible
|
|
11313
11434
|
*/
|
|
@@ -11316,7 +11437,77 @@ class Entity {
|
|
|
11316
11437
|
const maskA = this.collisionMask;
|
|
11317
11438
|
const categoryB = other.collisionCategory;
|
|
11318
11439
|
const maskB = other.collisionMask;
|
|
11319
|
-
|
|
11440
|
+
if ((categoryA & maskB) === 0 || (categoryB & maskA) === 0) {
|
|
11441
|
+
return false;
|
|
11442
|
+
}
|
|
11443
|
+
for (const filter of this.collisionFilterHandlers) {
|
|
11444
|
+
if (!filter(this, other)) {
|
|
11445
|
+
return false;
|
|
11446
|
+
}
|
|
11447
|
+
}
|
|
11448
|
+
for (const filter of other.collisionFilterHandlers) {
|
|
11449
|
+
if (!filter(other, this)) {
|
|
11450
|
+
return false;
|
|
11451
|
+
}
|
|
11452
|
+
}
|
|
11453
|
+
return true;
|
|
11454
|
+
}
|
|
11455
|
+
/**
|
|
11456
|
+
* Adds a resolution filter to this entity
|
|
11457
|
+
*
|
|
11458
|
+
* Resolution filters determine whether a collision should be **resolved** (blocking)
|
|
11459
|
+
* or just **detected** (notification only). Unlike collision filters which prevent
|
|
11460
|
+
* detection entirely, resolution filters allow collision events to fire while
|
|
11461
|
+
* optionally skipping the physical blocking.
|
|
11462
|
+
*
|
|
11463
|
+
* This enables scenarios like:
|
|
11464
|
+
* - Players passing through other players but still triggering touch events
|
|
11465
|
+
* - Entities passing through characters but still calling onPlayerTouch hooks
|
|
11466
|
+
* - Ghost mode where collisions are detected but not resolved
|
|
11467
|
+
*
|
|
11468
|
+
* @param filter - Function that returns `true` to resolve (block), `false` to skip
|
|
11469
|
+
* @returns Unsubscribe function to remove the filter
|
|
11470
|
+
*
|
|
11471
|
+
* @example
|
|
11472
|
+
* ```typescript
|
|
11473
|
+
* // Allow entity to pass through players but still trigger events
|
|
11474
|
+
* const unsubscribe = entity.addResolutionFilter((self, other) => {
|
|
11475
|
+
* const otherOwner = (other as any).owner;
|
|
11476
|
+
* if (otherOwner?.type === 'player') {
|
|
11477
|
+
* return false; // Pass through but events still fire
|
|
11478
|
+
* }
|
|
11479
|
+
* return true; // Block other entities
|
|
11480
|
+
* });
|
|
11481
|
+
*
|
|
11482
|
+
* // Later, remove the filter
|
|
11483
|
+
* unsubscribe();
|
|
11484
|
+
* ```
|
|
11485
|
+
*/
|
|
11486
|
+
addResolutionFilter(filter) {
|
|
11487
|
+
this.resolutionFilterHandlers.add(filter);
|
|
11488
|
+
return () => this.resolutionFilterHandlers.delete(filter);
|
|
11489
|
+
}
|
|
11490
|
+
/**
|
|
11491
|
+
* Checks if this entity should resolve (block) a collision with another entity
|
|
11492
|
+
*
|
|
11493
|
+
* This is called by the CollisionResolver to determine if the collision should
|
|
11494
|
+
* result in physical blocking or just notification.
|
|
11495
|
+
*
|
|
11496
|
+
* @param other - Other entity to check
|
|
11497
|
+
* @returns True if collision should be resolved (blocking), false to pass through
|
|
11498
|
+
*/
|
|
11499
|
+
shouldResolveCollisionWith(other) {
|
|
11500
|
+
for (const filter of this.resolutionFilterHandlers) {
|
|
11501
|
+
if (!filter(this, other)) {
|
|
11502
|
+
return false;
|
|
11503
|
+
}
|
|
11504
|
+
}
|
|
11505
|
+
for (const filter of other.resolutionFilterHandlers) {
|
|
11506
|
+
if (!filter(other, this)) {
|
|
11507
|
+
return false;
|
|
11508
|
+
}
|
|
11509
|
+
}
|
|
11510
|
+
return true;
|
|
11320
11511
|
}
|
|
11321
11512
|
/**
|
|
11322
11513
|
* @internal
|
|
@@ -12967,6 +13158,9 @@ class CollisionResolver {
|
|
|
12967
13158
|
* Resolves a collision
|
|
12968
13159
|
*
|
|
12969
13160
|
* Separates entities and applies collision response.
|
|
13161
|
+
* First checks if the collision should be resolved using resolution filters.
|
|
13162
|
+
* If any entity has a resolution filter that returns false, the collision
|
|
13163
|
+
* is skipped (entities pass through) but events are still fired.
|
|
12970
13164
|
*
|
|
12971
13165
|
* @param collision - Collision information to resolve
|
|
12972
13166
|
*/
|
|
@@ -12975,6 +13169,9 @@ class CollisionResolver {
|
|
|
12975
13169
|
if (depth < this.config.minPenetrationDepth) {
|
|
12976
13170
|
return;
|
|
12977
13171
|
}
|
|
13172
|
+
if (!entityA.shouldResolveCollisionWith(entityB)) {
|
|
13173
|
+
return;
|
|
13174
|
+
}
|
|
12978
13175
|
this.separateEntities(entityA, entityB, normal, depth);
|
|
12979
13176
|
this.resolveVelocities(entityA, entityB, normal);
|
|
12980
13177
|
}
|
|
@@ -14118,20 +14315,64 @@ let MovementManager$1 = class MovementManager {
|
|
|
14118
14315
|
}
|
|
14119
14316
|
/**
|
|
14120
14317
|
* Adds a movement strategy to an entity.
|
|
14318
|
+
*
|
|
14319
|
+
* Returns a Promise that resolves when the movement completes (when `isFinished()` returns true).
|
|
14320
|
+
* If the strategy doesn't implement `isFinished()`, the Promise resolves immediately after adding.
|
|
14121
14321
|
*
|
|
14122
14322
|
* @param target - Entity instance or entity UUID when a resolver is configured
|
|
14123
14323
|
* @param strategy - Strategy to execute
|
|
14324
|
+
* @param options - Optional callbacks for movement lifecycle events
|
|
14325
|
+
* @returns Promise that resolves when the movement completes
|
|
14326
|
+
*
|
|
14327
|
+
* @example
|
|
14328
|
+
* ```typescript
|
|
14329
|
+
* // Simple usage - fire and forget
|
|
14330
|
+
* manager.add(player, new Dash(8, { x: 1, y: 0 }, 200));
|
|
14331
|
+
*
|
|
14332
|
+
* // Wait for completion
|
|
14333
|
+
* await manager.add(player, new Dash(8, { x: 1, y: 0 }, 200));
|
|
14334
|
+
* console.log('Dash finished!');
|
|
14335
|
+
*
|
|
14336
|
+
* // With callbacks
|
|
14337
|
+
* await manager.add(player, new Knockback({ x: -1, y: 0 }, 5, 300), {
|
|
14338
|
+
* onStart: () => {
|
|
14339
|
+
* player.directionFixed = true;
|
|
14340
|
+
* player.animationFixed = true;
|
|
14341
|
+
* },
|
|
14342
|
+
* onComplete: () => {
|
|
14343
|
+
* player.directionFixed = false;
|
|
14344
|
+
* player.animationFixed = false;
|
|
14345
|
+
* }
|
|
14346
|
+
* });
|
|
14347
|
+
* ```
|
|
14124
14348
|
*/
|
|
14125
|
-
add(target, strategy) {
|
|
14349
|
+
add(target, strategy, options) {
|
|
14126
14350
|
const body = this.resolveTarget(target);
|
|
14127
14351
|
const key = body.id;
|
|
14128
14352
|
if (!this.entries.has(key)) {
|
|
14129
14353
|
this.entries.set(key, { body, strategies: [] });
|
|
14130
14354
|
}
|
|
14131
|
-
|
|
14355
|
+
if (!strategy.isFinished) {
|
|
14356
|
+
const entry = { strategy, started: false };
|
|
14357
|
+
if (options) {
|
|
14358
|
+
entry.options = options;
|
|
14359
|
+
}
|
|
14360
|
+
this.entries.get(key).strategies.push(entry);
|
|
14361
|
+
return Promise.resolve();
|
|
14362
|
+
}
|
|
14363
|
+
return new Promise((resolve) => {
|
|
14364
|
+
const entry = { strategy, resolve, started: false };
|
|
14365
|
+
if (options) {
|
|
14366
|
+
entry.options = options;
|
|
14367
|
+
}
|
|
14368
|
+
this.entries.get(key).strategies.push(entry);
|
|
14369
|
+
});
|
|
14132
14370
|
}
|
|
14133
14371
|
/**
|
|
14134
14372
|
* Removes a specific strategy from an entity.
|
|
14373
|
+
*
|
|
14374
|
+
* Note: This will NOT trigger the onComplete callback or resolve the Promise.
|
|
14375
|
+
* Use this when you want to cancel a movement without completion.
|
|
14135
14376
|
*
|
|
14136
14377
|
* @param target - Entity instance or identifier
|
|
14137
14378
|
* @param strategy - Strategy instance to remove
|
|
@@ -14143,7 +14384,7 @@ let MovementManager$1 = class MovementManager {
|
|
|
14143
14384
|
if (!entry) {
|
|
14144
14385
|
return false;
|
|
14145
14386
|
}
|
|
14146
|
-
const index = entry.strategies.
|
|
14387
|
+
const index = entry.strategies.findIndex((e) => e.strategy === strategy);
|
|
14147
14388
|
if (index === -1) {
|
|
14148
14389
|
return false;
|
|
14149
14390
|
}
|
|
@@ -14222,13 +14463,21 @@ let MovementManager$1 = class MovementManager {
|
|
|
14222
14463
|
getStrategies(target) {
|
|
14223
14464
|
const body = this.resolveTarget(target);
|
|
14224
14465
|
const entry = this.entries.get(body.id);
|
|
14225
|
-
return entry ?
|
|
14466
|
+
return entry ? entry.strategies.map((e) => e.strategy) : [];
|
|
14226
14467
|
}
|
|
14227
14468
|
/**
|
|
14228
14469
|
* Updates all registered strategies.
|
|
14229
14470
|
*
|
|
14230
14471
|
* Call this method once per frame before `PhysicsEngine.step()` so that the
|
|
14231
14472
|
* physics simulation integrates the velocities that strategies configure.
|
|
14473
|
+
*
|
|
14474
|
+
* This method handles the movement lifecycle:
|
|
14475
|
+
* - Triggers `onStart` callback on first update
|
|
14476
|
+
* - Calls `strategy.update()` each frame
|
|
14477
|
+
* - When `isFinished()` returns true:
|
|
14478
|
+
* - Calls `strategy.onFinished()` if defined
|
|
14479
|
+
* - Triggers `onComplete` callback
|
|
14480
|
+
* - Resolves the Promise returned by `add()`
|
|
14232
14481
|
*
|
|
14233
14482
|
* @param dt - Time delta in seconds
|
|
14234
14483
|
*/
|
|
@@ -14240,14 +14489,22 @@ let MovementManager$1 = class MovementManager {
|
|
|
14240
14489
|
continue;
|
|
14241
14490
|
}
|
|
14242
14491
|
for (let i = strategies.length - 1; i >= 0; i -= 1) {
|
|
14243
|
-
const
|
|
14244
|
-
if (!
|
|
14492
|
+
const strategyEntry = strategies[i];
|
|
14493
|
+
if (!strategyEntry) {
|
|
14245
14494
|
continue;
|
|
14246
14495
|
}
|
|
14247
|
-
|
|
14248
|
-
if (
|
|
14496
|
+
const { strategy, options, resolve } = strategyEntry;
|
|
14497
|
+
if (!strategyEntry.started) {
|
|
14498
|
+
strategyEntry.started = true;
|
|
14499
|
+
options?.onStart?.();
|
|
14500
|
+
}
|
|
14501
|
+
strategy.update(body, dt);
|
|
14502
|
+
const isFinished = strategy.isFinished?.();
|
|
14503
|
+
if (isFinished) {
|
|
14249
14504
|
strategies.splice(i, 1);
|
|
14250
|
-
|
|
14505
|
+
strategy.onFinished?.();
|
|
14506
|
+
options?.onComplete?.();
|
|
14507
|
+
resolve?.();
|
|
14251
14508
|
}
|
|
14252
14509
|
}
|
|
14253
14510
|
if (strategies.length === 0) {
|
|
@@ -15770,8 +16027,16 @@ class MovementManager {
|
|
|
15770
16027
|
get core() {
|
|
15771
16028
|
return this.physicProvider().getMovementManager();
|
|
15772
16029
|
}
|
|
15773
|
-
|
|
15774
|
-
|
|
16030
|
+
/**
|
|
16031
|
+
* Adds a movement strategy and returns a Promise that resolves when it completes.
|
|
16032
|
+
*
|
|
16033
|
+
* @param id - Entity identifier
|
|
16034
|
+
* @param strategy - Movement strategy to add
|
|
16035
|
+
* @param options - Optional callbacks for movement lifecycle events
|
|
16036
|
+
* @returns Promise that resolves when the movement completes
|
|
16037
|
+
*/
|
|
16038
|
+
add(id, strategy, options) {
|
|
16039
|
+
return this.core.add(id, strategy, options);
|
|
15775
16040
|
}
|
|
15776
16041
|
remove(id, strategy) {
|
|
15777
16042
|
return this.core.remove(id, strategy);
|
|
@@ -16624,11 +16889,14 @@ class RpgCommonMap {
|
|
|
16624
16889
|
const owner2 = entity.owner;
|
|
16625
16890
|
if (!owner2) return;
|
|
16626
16891
|
if (cardinalDirection === "idle") return;
|
|
16892
|
+
if (owner2.directionFixed) return;
|
|
16627
16893
|
owner2.changeDirection(cardinalDirection);
|
|
16628
16894
|
});
|
|
16629
16895
|
entity.onMovementChange(({ isMoving, intensity }) => {
|
|
16896
|
+
if (!("$send" in this)) return;
|
|
16630
16897
|
const owner2 = entity.owner;
|
|
16631
16898
|
if (!owner2) return;
|
|
16899
|
+
if (owner2.animationFixed) return;
|
|
16632
16900
|
const LOW_INTENSITY_THRESHOLD = 10;
|
|
16633
16901
|
const hasSetAnimation = typeof owner2.setAnimation === "function";
|
|
16634
16902
|
const animationNameSignal = owner2.animationName;
|
|
@@ -16665,6 +16933,56 @@ class RpgCommonMap {
|
|
|
16665
16933
|
owner.applyFrames?.();
|
|
16666
16934
|
}
|
|
16667
16935
|
});
|
|
16936
|
+
entity.addResolutionFilter((self, other) => {
|
|
16937
|
+
const selfOwner = self.owner;
|
|
16938
|
+
const otherOwner = other.owner;
|
|
16939
|
+
if (!selfOwner || !otherOwner) {
|
|
16940
|
+
return true;
|
|
16941
|
+
}
|
|
16942
|
+
if (selfOwner.z() !== otherOwner.z()) {
|
|
16943
|
+
return false;
|
|
16944
|
+
}
|
|
16945
|
+
if (typeof selfOwner._through === "function") {
|
|
16946
|
+
try {
|
|
16947
|
+
if (selfOwner._through() === true) {
|
|
16948
|
+
return false;
|
|
16949
|
+
}
|
|
16950
|
+
} catch {
|
|
16951
|
+
}
|
|
16952
|
+
} else if (selfOwner.through === true) {
|
|
16953
|
+
return false;
|
|
16954
|
+
}
|
|
16955
|
+
const playersMap = this.players();
|
|
16956
|
+
const eventsMap = this.events();
|
|
16957
|
+
const isSelfPlayer = !!playersMap[self.uuid];
|
|
16958
|
+
const isOtherPlayer = !!playersMap[other.uuid];
|
|
16959
|
+
const isOtherEvent = !!eventsMap[other.uuid];
|
|
16960
|
+
if (isSelfPlayer && isOtherPlayer) {
|
|
16961
|
+
if (typeof selfOwner._throughOtherPlayer === "function") {
|
|
16962
|
+
try {
|
|
16963
|
+
if (selfOwner._throughOtherPlayer() === true) {
|
|
16964
|
+
return false;
|
|
16965
|
+
}
|
|
16966
|
+
} catch {
|
|
16967
|
+
}
|
|
16968
|
+
} else if (selfOwner.throughOtherPlayer === true) {
|
|
16969
|
+
return false;
|
|
16970
|
+
}
|
|
16971
|
+
}
|
|
16972
|
+
if (isSelfPlayer && isOtherEvent) {
|
|
16973
|
+
if (typeof selfOwner._throughEvent === "function") {
|
|
16974
|
+
try {
|
|
16975
|
+
if (selfOwner._throughEvent() === true) {
|
|
16976
|
+
return false;
|
|
16977
|
+
}
|
|
16978
|
+
} catch {
|
|
16979
|
+
}
|
|
16980
|
+
} else if (selfOwner.throughEvent === true) {
|
|
16981
|
+
return false;
|
|
16982
|
+
}
|
|
16983
|
+
}
|
|
16984
|
+
return true;
|
|
16985
|
+
});
|
|
16668
16986
|
return id;
|
|
16669
16987
|
}
|
|
16670
16988
|
/**
|
|
@@ -18232,6 +18550,37 @@ class Gui {
|
|
|
18232
18550
|
}
|
|
18233
18551
|
}
|
|
18234
18552
|
|
|
18553
|
+
class AdditiveKnockback {
|
|
18554
|
+
constructor(direction, initialSpeed, duration, decayFactor = 0.35) {
|
|
18555
|
+
this.duration = duration;
|
|
18556
|
+
this.decayFactor = decayFactor;
|
|
18557
|
+
this.elapsed = 0;
|
|
18558
|
+
const magnitude = Math.hypot(direction.x, direction.y);
|
|
18559
|
+
this.direction = magnitude > 0 ? { x: direction.x / magnitude, y: direction.y / magnitude } : { x: 1, y: 0 };
|
|
18560
|
+
this.currentSpeed = initialSpeed;
|
|
18561
|
+
}
|
|
18562
|
+
update(body, dt) {
|
|
18563
|
+
this.elapsed += dt;
|
|
18564
|
+
if (this.elapsed > this.duration) {
|
|
18565
|
+
return;
|
|
18566
|
+
}
|
|
18567
|
+
const impulseX = this.direction.x * this.currentSpeed;
|
|
18568
|
+
const impulseY = this.direction.y * this.currentSpeed;
|
|
18569
|
+
body.setVelocity({
|
|
18570
|
+
x: body.velocity.x + impulseX,
|
|
18571
|
+
y: body.velocity.y + impulseY
|
|
18572
|
+
});
|
|
18573
|
+
const decay = Math.max(0, Math.min(1, this.decayFactor));
|
|
18574
|
+
if (decay === 0) {
|
|
18575
|
+
this.currentSpeed = 0;
|
|
18576
|
+
} else if (decay !== 1) {
|
|
18577
|
+
this.currentSpeed *= Math.pow(decay, dt);
|
|
18578
|
+
}
|
|
18579
|
+
}
|
|
18580
|
+
isFinished() {
|
|
18581
|
+
return this.elapsed >= this.duration;
|
|
18582
|
+
}
|
|
18583
|
+
}
|
|
18235
18584
|
function wait(sec) {
|
|
18236
18585
|
return new Promise((resolve) => {
|
|
18237
18586
|
setTimeout(resolve, sec * 1e3);
|
|
@@ -18269,51 +18618,44 @@ class MoveList {
|
|
|
18269
18618
|
// Instance counter for each call to ensure variation
|
|
18270
18619
|
this.callCounter = 0;
|
|
18271
18620
|
}
|
|
18621
|
+
static {
|
|
18622
|
+
// Track player positions and directions to detect stuck state
|
|
18623
|
+
this.playerMoveStates = /* @__PURE__ */ new Map();
|
|
18624
|
+
}
|
|
18625
|
+
static {
|
|
18626
|
+
// Threshold for considering a player as "stuck" (in pixels)
|
|
18627
|
+
this.STUCK_THRESHOLD = 2;
|
|
18628
|
+
}
|
|
18272
18629
|
/**
|
|
18273
|
-
*
|
|
18630
|
+
* Clears the movement state for a specific player
|
|
18631
|
+
*
|
|
18632
|
+
* Should be called when a player changes map or is destroyed to prevent
|
|
18633
|
+
* memory leaks and stale stuck detection data.
|
|
18274
18634
|
*
|
|
18275
|
-
*
|
|
18276
|
-
* fair distribution of directions while maintaining smooth, natural-looking movement patterns.
|
|
18277
|
-
* The hash function guarantees uniform distribution, while Perlin noise adds spatial/temporal coherence.
|
|
18635
|
+
* @param playerId - The ID of the player to clear state for
|
|
18278
18636
|
*
|
|
18279
|
-
* @
|
|
18280
|
-
*
|
|
18281
|
-
*
|
|
18637
|
+
* @example
|
|
18638
|
+
* ```ts
|
|
18639
|
+
* // Clear state when player leaves map
|
|
18640
|
+
* Move.clearPlayerState(player.id);
|
|
18641
|
+
* ```
|
|
18282
18642
|
*/
|
|
18283
|
-
|
|
18284
|
-
MoveList.
|
|
18285
|
-
|
|
18286
|
-
|
|
18287
|
-
|
|
18288
|
-
|
|
18289
|
-
|
|
18290
|
-
|
|
18291
|
-
|
|
18292
|
-
|
|
18293
|
-
|
|
18294
|
-
|
|
18295
|
-
|
|
18296
|
-
|
|
18297
|
-
|
|
18298
|
-
|
|
18299
|
-
let hash1 = (seed * 1103515245 + 12345 & 2147483647) >>> 0;
|
|
18300
|
-
let hash2 = seed * 2654435761 >>> 0;
|
|
18301
|
-
let hash3 = seed ^ seed >>> 16;
|
|
18302
|
-
hash3 = hash3 * 2246822507 >>> 0;
|
|
18303
|
-
let combinedHash = (hash1 ^ hash2 ^ hash3) >>> 0;
|
|
18304
|
-
const hashValue = combinedHash % 1e6 / 1e6;
|
|
18305
|
-
const perlinX = seed * 1e-3;
|
|
18306
|
-
const perlinY = seed * 1.618 * 1e-3;
|
|
18307
|
-
const perlinValue = MoveList.perlinNoise.getNormalized(perlinX, perlinY, 0.3);
|
|
18308
|
-
const finalValue = hashValue * 0.9 + perlinValue * 0.1;
|
|
18309
|
-
const clampedValue = Math.max(0, Math.min(0.999999, finalValue));
|
|
18310
|
-
let directionIndex = Math.floor(clampedValue * 4);
|
|
18311
|
-
directionIndex = Math.max(0, Math.min(3, directionIndex));
|
|
18312
|
-
if (!Number.isFinite(directionIndex) || directionIndex < 0 || directionIndex > 3) {
|
|
18313
|
-
const fallbackIndex = Math.floor(hashValue * 4) % 4;
|
|
18314
|
-
return Math.max(0, Math.min(3, fallbackIndex));
|
|
18315
|
-
}
|
|
18316
|
-
return directionIndex;
|
|
18643
|
+
static clearPlayerState(playerId) {
|
|
18644
|
+
MoveList.playerMoveStates.delete(playerId);
|
|
18645
|
+
}
|
|
18646
|
+
/**
|
|
18647
|
+
* Clears all player movement states
|
|
18648
|
+
*
|
|
18649
|
+
* Useful for cleanup during server shutdown or when resetting game state.
|
|
18650
|
+
*
|
|
18651
|
+
* @example
|
|
18652
|
+
* ```ts
|
|
18653
|
+
* // Clear all states on server shutdown
|
|
18654
|
+
* Move.clearAllPlayerStates();
|
|
18655
|
+
* ```
|
|
18656
|
+
*/
|
|
18657
|
+
static clearAllPlayerStates() {
|
|
18658
|
+
MoveList.playerMoveStates.clear();
|
|
18317
18659
|
}
|
|
18318
18660
|
repeatMove(direction, repeat) {
|
|
18319
18661
|
if (!Number.isFinite(repeat) || repeat < 0 || repeat > 1e4) {
|
|
@@ -18372,30 +18714,12 @@ class MoveList {
|
|
|
18372
18714
|
return wait(sec);
|
|
18373
18715
|
}
|
|
18374
18716
|
random(repeat = 1) {
|
|
18375
|
-
|
|
18376
|
-
|
|
18377
|
-
|
|
18378
|
-
|
|
18379
|
-
|
|
18380
|
-
|
|
18381
|
-
console.warn("Unsafe repeat value in random:", repeat, "using default value 1");
|
|
18382
|
-
repeat = 1;
|
|
18383
|
-
}
|
|
18384
|
-
try {
|
|
18385
|
-
MoveList.randomCounter += repeat;
|
|
18386
|
-
return new Array(repeat).fill(null).map((_, index) => {
|
|
18387
|
-
const directionIndex = this.getRandomDirectionIndex(void 0, index);
|
|
18388
|
-
return [
|
|
18389
|
-
Direction.Right,
|
|
18390
|
-
Direction.Left,
|
|
18391
|
-
Direction.Up,
|
|
18392
|
-
Direction.Down
|
|
18393
|
-
][directionIndex];
|
|
18394
|
-
});
|
|
18395
|
-
} catch (error) {
|
|
18396
|
-
console.error("Error creating random array with repeat:", repeat, error);
|
|
18397
|
-
return [Direction.Down];
|
|
18398
|
-
}
|
|
18717
|
+
return new Array(repeat).fill(null).map(() => [
|
|
18718
|
+
Direction.Right,
|
|
18719
|
+
Direction.Left,
|
|
18720
|
+
Direction.Up,
|
|
18721
|
+
Direction.Down
|
|
18722
|
+
][random(0, 3)]);
|
|
18399
18723
|
}
|
|
18400
18724
|
tileRight(repeat = 1) {
|
|
18401
18725
|
return this.repeatTileMove("right", repeat, "tileWidth");
|
|
@@ -18411,41 +18735,18 @@ class MoveList {
|
|
|
18411
18735
|
}
|
|
18412
18736
|
tileRandom(repeat = 1) {
|
|
18413
18737
|
return (player, map) => {
|
|
18414
|
-
if (!Number.isFinite(repeat) || repeat < 0 || repeat > 1e3) {
|
|
18415
|
-
console.warn("Invalid repeat value in tileRandom:", repeat, "using default value 1");
|
|
18416
|
-
repeat = 1;
|
|
18417
|
-
}
|
|
18418
|
-
repeat = Math.floor(repeat);
|
|
18419
18738
|
let directions = [];
|
|
18420
|
-
const directionFunctions = [
|
|
18421
|
-
this.tileRight(),
|
|
18422
|
-
this.tileLeft(),
|
|
18423
|
-
this.tileUp(),
|
|
18424
|
-
this.tileDown()
|
|
18425
|
-
];
|
|
18426
18739
|
for (let i = 0; i < repeat; i++) {
|
|
18427
|
-
|
|
18428
|
-
|
|
18429
|
-
|
|
18430
|
-
|
|
18431
|
-
|
|
18432
|
-
|
|
18433
|
-
|
|
18434
|
-
|
|
18435
|
-
|
|
18436
|
-
|
|
18437
|
-
try {
|
|
18438
|
-
const newDirections = randFn(player, map);
|
|
18439
|
-
if (Array.isArray(newDirections)) {
|
|
18440
|
-
directions = [...directions, ...newDirections];
|
|
18441
|
-
}
|
|
18442
|
-
} catch (error) {
|
|
18443
|
-
console.warn("Error in tileRandom iteration:", error);
|
|
18444
|
-
}
|
|
18445
|
-
if (directions.length > 1e4) {
|
|
18446
|
-
console.warn("tileRandom generated too many directions, truncating");
|
|
18447
|
-
break;
|
|
18448
|
-
}
|
|
18740
|
+
const randFn = [
|
|
18741
|
+
this.tileRight(),
|
|
18742
|
+
this.tileLeft(),
|
|
18743
|
+
this.tileUp(),
|
|
18744
|
+
this.tileDown()
|
|
18745
|
+
][random(0, 3)];
|
|
18746
|
+
directions = [
|
|
18747
|
+
...directions,
|
|
18748
|
+
...randFn(player, map)
|
|
18749
|
+
];
|
|
18449
18750
|
}
|
|
18450
18751
|
return directions;
|
|
18451
18752
|
};
|
|
@@ -18603,26 +18904,73 @@ function WithMoveManager(Base) {
|
|
|
18603
18904
|
get through() {
|
|
18604
18905
|
return this._through();
|
|
18605
18906
|
}
|
|
18907
|
+
set throughEvent(value) {
|
|
18908
|
+
this._throughEvent.set(value);
|
|
18909
|
+
}
|
|
18910
|
+
get throughEvent() {
|
|
18911
|
+
return this._throughEvent();
|
|
18912
|
+
}
|
|
18606
18913
|
set frequency(value) {
|
|
18607
18914
|
this._frequency.set(value);
|
|
18608
18915
|
}
|
|
18609
18916
|
get frequency() {
|
|
18610
18917
|
return this._frequency();
|
|
18611
18918
|
}
|
|
18612
|
-
|
|
18919
|
+
set directionFixed(value) {
|
|
18920
|
+
this._directionFixed.set(value);
|
|
18921
|
+
}
|
|
18922
|
+
get directionFixed() {
|
|
18923
|
+
return this._directionFixed();
|
|
18924
|
+
}
|
|
18925
|
+
set animationFixed(value) {
|
|
18926
|
+
this._animationFixed.set(value);
|
|
18927
|
+
}
|
|
18928
|
+
get animationFixed() {
|
|
18929
|
+
return this._animationFixed();
|
|
18930
|
+
}
|
|
18931
|
+
/**
|
|
18932
|
+
* Add a movement strategy to this entity
|
|
18933
|
+
*
|
|
18934
|
+
* Returns a Promise that resolves when the movement completes (when `isFinished()` returns true).
|
|
18935
|
+
* If the strategy doesn't implement `isFinished()`, the Promise resolves immediately.
|
|
18936
|
+
*
|
|
18937
|
+
* @param strategy - The movement strategy to add
|
|
18938
|
+
* @param options - Optional callbacks for start and completion events
|
|
18939
|
+
* @returns Promise that resolves when the movement completes
|
|
18940
|
+
*
|
|
18941
|
+
* @example
|
|
18942
|
+
* ```ts
|
|
18943
|
+
* // Fire and forget
|
|
18944
|
+
* player.addMovement(new LinearMove({ x: 1, y: 0 }, 200));
|
|
18945
|
+
*
|
|
18946
|
+
* // Wait for completion
|
|
18947
|
+
* await player.addMovement(new Dash(10, { x: 1, y: 0 }, 200));
|
|
18948
|
+
* console.log('Dash completed!');
|
|
18949
|
+
*
|
|
18950
|
+
* // With callbacks
|
|
18951
|
+
* await player.addMovement(new Knockback({ x: -1, y: 0 }, 5, 300), {
|
|
18952
|
+
* onStart: () => console.log('Knockback started'),
|
|
18953
|
+
* onComplete: () => console.log('Knockback completed')
|
|
18954
|
+
* });
|
|
18955
|
+
* ```
|
|
18956
|
+
*/
|
|
18957
|
+
addMovement(strategy, options) {
|
|
18613
18958
|
const map = this.getCurrentMap();
|
|
18614
|
-
if (!map) return;
|
|
18615
|
-
|
|
18959
|
+
if (!map) return Promise.resolve();
|
|
18960
|
+
const playerId = this.id;
|
|
18961
|
+
return map.moveManager.add(playerId, strategy, options);
|
|
18616
18962
|
}
|
|
18617
18963
|
removeMovement(strategy) {
|
|
18618
18964
|
const map = this.getCurrentMap();
|
|
18619
18965
|
if (!map) return false;
|
|
18620
|
-
|
|
18966
|
+
const playerId = this.id;
|
|
18967
|
+
return map.moveManager.remove(playerId, strategy);
|
|
18621
18968
|
}
|
|
18622
18969
|
clearMovements() {
|
|
18623
18970
|
const map = this.getCurrentMap();
|
|
18624
18971
|
if (!map) return;
|
|
18625
|
-
|
|
18972
|
+
const playerId = this.id;
|
|
18973
|
+
map.moveManager.clear(playerId);
|
|
18626
18974
|
}
|
|
18627
18975
|
hasActiveMovements() {
|
|
18628
18976
|
const map = this.getCurrentMap();
|
|
@@ -18634,15 +18982,45 @@ function WithMoveManager(Base) {
|
|
|
18634
18982
|
if (!map) return [];
|
|
18635
18983
|
return map.moveManager.getStrategies(this.id);
|
|
18636
18984
|
}
|
|
18985
|
+
/**
|
|
18986
|
+
* Move toward a target player or position using AI pathfinding
|
|
18987
|
+
*
|
|
18988
|
+
* Uses the `SeekAvoid` strategy to navigate toward the target while avoiding obstacles.
|
|
18989
|
+
* The movement speed is based on the player's current `speed` property, scaled appropriately.
|
|
18990
|
+
*
|
|
18991
|
+
* @param target - Target player or position `{ x, y }` to move toward
|
|
18992
|
+
*
|
|
18993
|
+
* @example
|
|
18994
|
+
* ```ts
|
|
18995
|
+
* // Move toward another player
|
|
18996
|
+
* player.moveTo(otherPlayer);
|
|
18997
|
+
*
|
|
18998
|
+
* // Move toward a specific position
|
|
18999
|
+
* player.moveTo({ x: 200, y: 150 });
|
|
19000
|
+
* ```
|
|
19001
|
+
*/
|
|
18637
19002
|
moveTo(target) {
|
|
18638
19003
|
const map = this.getCurrentMap();
|
|
18639
19004
|
if (!map) return;
|
|
19005
|
+
const playerId = this.id;
|
|
18640
19006
|
const engine = map.physic;
|
|
19007
|
+
const playerSpeed = this.speed();
|
|
19008
|
+
const existingStrategies = this.getActiveMovements();
|
|
19009
|
+
const conflictingStrategies = existingStrategies.filter(
|
|
19010
|
+
(s) => s instanceof SeekAvoid || s instanceof Dash || s instanceof Knockback || s instanceof LinearRepulsion
|
|
19011
|
+
);
|
|
19012
|
+
if (conflictingStrategies.length > 0) {
|
|
19013
|
+
conflictingStrategies.forEach((s) => this.removeMovement(s));
|
|
19014
|
+
}
|
|
18641
19015
|
if ("id" in target) {
|
|
18642
|
-
const targetProvider = () =>
|
|
19016
|
+
const targetProvider = () => {
|
|
19017
|
+
const body = map.getBody(target.id) ?? null;
|
|
19018
|
+
return body;
|
|
19019
|
+
};
|
|
19020
|
+
const maxSpeed2 = playerSpeed * 45;
|
|
18643
19021
|
map.moveManager.add(
|
|
18644
|
-
|
|
18645
|
-
new SeekAvoid(engine, targetProvider,
|
|
19022
|
+
playerId,
|
|
19023
|
+
new SeekAvoid(engine, targetProvider, maxSpeed2, 140, 80, 48)
|
|
18646
19024
|
);
|
|
18647
19025
|
return;
|
|
18648
19026
|
}
|
|
@@ -18651,37 +19029,275 @@ function WithMoveManager(Base) {
|
|
|
18651
19029
|
mass: Infinity
|
|
18652
19030
|
});
|
|
18653
19031
|
staticTarget.freeze();
|
|
19032
|
+
const maxSpeed = playerSpeed * 20;
|
|
18654
19033
|
map.moveManager.add(
|
|
18655
|
-
|
|
18656
|
-
new SeekAvoid(engine, () => staticTarget,
|
|
19034
|
+
playerId,
|
|
19035
|
+
new SeekAvoid(engine, () => staticTarget, maxSpeed, 140, 80, 48)
|
|
18657
19036
|
);
|
|
18658
19037
|
}
|
|
18659
19038
|
stopMoveTo() {
|
|
18660
19039
|
const map = this.getCurrentMap();
|
|
18661
19040
|
if (!map) return;
|
|
19041
|
+
this.id;
|
|
18662
19042
|
const strategies = this.getActiveMovements();
|
|
18663
|
-
strategies.
|
|
18664
|
-
|
|
19043
|
+
const toRemove = strategies.filter((s) => s instanceof SeekAvoid || s instanceof LinearRepulsion);
|
|
19044
|
+
if (toRemove.length > 0) {
|
|
19045
|
+
toRemove.forEach((strategy) => {
|
|
18665
19046
|
this.removeMovement(strategy);
|
|
18666
|
-
}
|
|
18667
|
-
}
|
|
19047
|
+
});
|
|
19048
|
+
}
|
|
18668
19049
|
}
|
|
18669
|
-
|
|
18670
|
-
|
|
19050
|
+
/**
|
|
19051
|
+
* Perform a dash movement in the specified direction
|
|
19052
|
+
*
|
|
19053
|
+
* Creates a burst of velocity for a fixed duration. The total speed is calculated
|
|
19054
|
+
* by adding the player's base speed (`this.speed()`) to the additional dash speed.
|
|
19055
|
+
* This ensures faster players also dash faster proportionally.
|
|
19056
|
+
*
|
|
19057
|
+
* With default speed=4 and additionalSpeed=4: total = 8 (same as original default)
|
|
19058
|
+
*
|
|
19059
|
+
* @param direction - Normalized direction vector `{ x, y }` for the dash
|
|
19060
|
+
* @param additionalSpeed - Extra speed added on top of base speed (default: 4)
|
|
19061
|
+
* @param duration - Duration in milliseconds (default: 200)
|
|
19062
|
+
* @param options - Optional callbacks for movement events
|
|
19063
|
+
* @returns Promise that resolves when the dash completes
|
|
19064
|
+
*
|
|
19065
|
+
* @example
|
|
19066
|
+
* ```ts
|
|
19067
|
+
* // Dash to the right and wait for completion
|
|
19068
|
+
* await player.dash({ x: 1, y: 0 });
|
|
19069
|
+
*
|
|
19070
|
+
* // Powerful dash with callbacks
|
|
19071
|
+
* await player.dash({ x: 0, y: -1 }, 12, 300, {
|
|
19072
|
+
* onStart: () => console.log('Dash started!'),
|
|
19073
|
+
* onComplete: () => console.log('Dash finished!')
|
|
19074
|
+
* });
|
|
19075
|
+
* ```
|
|
19076
|
+
*/
|
|
19077
|
+
dash(direction, additionalSpeed = 4, duration = 200, options) {
|
|
19078
|
+
const playerSpeed = this.speed();
|
|
19079
|
+
const totalSpeed = playerSpeed + additionalSpeed;
|
|
19080
|
+
const durationSeconds = duration / 1e3;
|
|
19081
|
+
return this.addMovement(new Dash(totalSpeed, direction, durationSeconds), options);
|
|
18671
19082
|
}
|
|
18672
|
-
|
|
18673
|
-
|
|
19083
|
+
/**
|
|
19084
|
+
* Apply knockback effect in the specified direction
|
|
19085
|
+
*
|
|
19086
|
+
* Pushes the entity with an initial force that decays over time.
|
|
19087
|
+
* Returns a Promise that resolves when the knockback completes **or is cancelled**.
|
|
19088
|
+
*
|
|
19089
|
+
* ## Design notes
|
|
19090
|
+
* - The underlying physics `MovementManager` can cancel strategies via `remove()`, `clear()`,
|
|
19091
|
+
* or `stopMovement()` **without resolving the Promise** returned by `add()`.
|
|
19092
|
+
* - For this reason, this method considers the knockback finished when either:
|
|
19093
|
+
* - the `add()` promise resolves (normal completion), or
|
|
19094
|
+
* - the strategy is no longer present in the active movements list (cancellation).
|
|
19095
|
+
* - When multiple knockbacks overlap, `directionFixed` and `animationFixed` are restored
|
|
19096
|
+
* only after **all** knockbacks have finished (including cancellations).
|
|
19097
|
+
*
|
|
19098
|
+
* @param direction - Normalized direction vector `{ x, y }` for the knockback
|
|
19099
|
+
* @param force - Initial knockback force (default: 5)
|
|
19100
|
+
* @param duration - Duration in milliseconds (default: 300)
|
|
19101
|
+
* @param options - Optional callbacks for movement events
|
|
19102
|
+
* @returns Promise that resolves when the knockback completes or is cancelled
|
|
19103
|
+
*
|
|
19104
|
+
* @example
|
|
19105
|
+
* ```ts
|
|
19106
|
+
* // Simple knockback (await is optional)
|
|
19107
|
+
* await player.knockback({ x: 1, y: 0 }, 5, 300);
|
|
19108
|
+
*
|
|
19109
|
+
* // Overlapping knockbacks: flags are restored only after the last one ends
|
|
19110
|
+
* player.knockback({ x: -1, y: 0 }, 5, 300);
|
|
19111
|
+
* player.knockback({ x: 0, y: 1 }, 3, 200);
|
|
19112
|
+
*
|
|
19113
|
+
* // Cancellation (e.g. map change) will still restore fixed flags
|
|
19114
|
+
* // even if the underlying movement strategy promise is never resolved.
|
|
19115
|
+
* ```
|
|
19116
|
+
*/
|
|
19117
|
+
async knockback(direction, force = 5, duration = 300, options) {
|
|
19118
|
+
const durationSeconds = duration / 1e3;
|
|
19119
|
+
const selfAny = this;
|
|
19120
|
+
const lockKey = "__rpg_knockback_lock__";
|
|
19121
|
+
const getLock = () => selfAny[lockKey];
|
|
19122
|
+
const setLock = (lock) => {
|
|
19123
|
+
selfAny[lockKey] = lock;
|
|
19124
|
+
};
|
|
19125
|
+
const clearLock = () => {
|
|
19126
|
+
delete selfAny[lockKey];
|
|
19127
|
+
};
|
|
19128
|
+
const hasActiveKnockback = () => this.getActiveMovements().some((s) => s instanceof Knockback || s instanceof AdditiveKnockback);
|
|
19129
|
+
const setAnimationName = (name) => {
|
|
19130
|
+
if (typeof selfAny.setAnimation === "function") {
|
|
19131
|
+
selfAny.setAnimation(name);
|
|
19132
|
+
return;
|
|
19133
|
+
}
|
|
19134
|
+
const animSignal = selfAny.animationName;
|
|
19135
|
+
if (animSignal && typeof animSignal === "object" && typeof animSignal.set === "function") {
|
|
19136
|
+
animSignal.set(name);
|
|
19137
|
+
}
|
|
19138
|
+
};
|
|
19139
|
+
const getAnimationName = () => {
|
|
19140
|
+
const animSignal = selfAny.animationName;
|
|
19141
|
+
if (typeof animSignal === "function") {
|
|
19142
|
+
try {
|
|
19143
|
+
return animSignal();
|
|
19144
|
+
} catch {
|
|
19145
|
+
return void 0;
|
|
19146
|
+
}
|
|
19147
|
+
}
|
|
19148
|
+
return void 0;
|
|
19149
|
+
};
|
|
19150
|
+
const restore = () => {
|
|
19151
|
+
const lock = getLock();
|
|
19152
|
+
if (!lock) return;
|
|
19153
|
+
this.directionFixed = lock.prevDirectionFixed;
|
|
19154
|
+
const prevAnimFixed = lock.prevAnimationFixed;
|
|
19155
|
+
this.animationFixed = false;
|
|
19156
|
+
if (!prevAnimFixed && lock.prevAnimationName) {
|
|
19157
|
+
setAnimationName(lock.prevAnimationName);
|
|
19158
|
+
}
|
|
19159
|
+
this.animationFixed = prevAnimFixed;
|
|
19160
|
+
clearLock();
|
|
19161
|
+
};
|
|
19162
|
+
const ensureLockInitialized = () => {
|
|
19163
|
+
if (getLock()) return;
|
|
19164
|
+
setLock({
|
|
19165
|
+
prevDirectionFixed: this.directionFixed,
|
|
19166
|
+
prevAnimationFixed: this.animationFixed,
|
|
19167
|
+
prevAnimationName: getAnimationName()
|
|
19168
|
+
});
|
|
19169
|
+
this.directionFixed = true;
|
|
19170
|
+
setAnimationName("stand");
|
|
19171
|
+
this.animationFixed = true;
|
|
19172
|
+
};
|
|
19173
|
+
const waitUntilRemovedOrTimeout = (strategy2) => {
|
|
19174
|
+
return new Promise((resolve) => {
|
|
19175
|
+
const start = Date.now();
|
|
19176
|
+
const maxMs = Math.max(0, duration + 1e3);
|
|
19177
|
+
const intervalId = setInterval(() => {
|
|
19178
|
+
const active = this.getActiveMovements();
|
|
19179
|
+
if (!active.includes(strategy2) || Date.now() - start > maxMs) {
|
|
19180
|
+
clearInterval(intervalId);
|
|
19181
|
+
resolve();
|
|
19182
|
+
}
|
|
19183
|
+
}, 16);
|
|
19184
|
+
});
|
|
19185
|
+
};
|
|
19186
|
+
ensureLockInitialized();
|
|
19187
|
+
const strategy = new AdditiveKnockback(direction, force, durationSeconds);
|
|
19188
|
+
const addPromise = this.addMovement(strategy, options);
|
|
19189
|
+
try {
|
|
19190
|
+
await Promise.race([addPromise, waitUntilRemovedOrTimeout(strategy)]);
|
|
19191
|
+
} finally {
|
|
19192
|
+
if (!hasActiveKnockback()) {
|
|
19193
|
+
restore();
|
|
19194
|
+
}
|
|
19195
|
+
}
|
|
18674
19196
|
}
|
|
18675
|
-
|
|
19197
|
+
/**
|
|
19198
|
+
* Follow a sequence of waypoints
|
|
19199
|
+
*
|
|
19200
|
+
* Makes the entity move through a list of positions at a speed calculated
|
|
19201
|
+
* from the player's base speed. The `speedMultiplier` allows adjusting
|
|
19202
|
+
* the travel speed relative to the player's normal movement speed.
|
|
19203
|
+
*
|
|
19204
|
+
* With default speed=4 and multiplier=0.5: speed = 2 (same as original default)
|
|
19205
|
+
*
|
|
19206
|
+
* @param waypoints - Array of `{ x, y }` positions to follow in order
|
|
19207
|
+
* @param speedMultiplier - Multiplier applied to base speed (default: 0.5)
|
|
19208
|
+
* @param loop - Whether to loop back to start after reaching the end (default: false)
|
|
19209
|
+
*
|
|
19210
|
+
* @example
|
|
19211
|
+
* ```ts
|
|
19212
|
+
* // Follow a patrol path at normal speed
|
|
19213
|
+
* const patrol = [
|
|
19214
|
+
* { x: 100, y: 100 },
|
|
19215
|
+
* { x: 200, y: 100 },
|
|
19216
|
+
* { x: 200, y: 200 }
|
|
19217
|
+
* ];
|
|
19218
|
+
* player.followPath(patrol, 1, true); // Loop at full speed
|
|
19219
|
+
*
|
|
19220
|
+
* // Slow walk through waypoints
|
|
19221
|
+
* player.followPath(waypoints, 0.25, false);
|
|
19222
|
+
* ```
|
|
19223
|
+
*/
|
|
19224
|
+
followPath(waypoints, speedMultiplier = 0.5, loop = false) {
|
|
19225
|
+
const playerSpeed = this.speed();
|
|
19226
|
+
const speed = playerSpeed * speedMultiplier;
|
|
18676
19227
|
this.addMovement(new PathFollow(waypoints, speed, loop));
|
|
18677
19228
|
}
|
|
19229
|
+
/**
|
|
19230
|
+
* Apply oscillating movement pattern
|
|
19231
|
+
*
|
|
19232
|
+
* Creates a back-and-forth movement along the specified axis. The movement
|
|
19233
|
+
* oscillates sinusoidally between -amplitude and +amplitude from the starting position.
|
|
19234
|
+
*
|
|
19235
|
+
* @param direction - Primary oscillation axis (normalized direction vector)
|
|
19236
|
+
* @param amplitude - Maximum distance from center in pixels (default: 50)
|
|
19237
|
+
* @param period - Time for a complete cycle in milliseconds (default: 2000)
|
|
19238
|
+
*
|
|
19239
|
+
* @example
|
|
19240
|
+
* ```ts
|
|
19241
|
+
* // Horizontal oscillation
|
|
19242
|
+
* player.oscillate({ x: 1, y: 0 }, 100, 3000);
|
|
19243
|
+
*
|
|
19244
|
+
* // Diagonal bobbing motion
|
|
19245
|
+
* player.oscillate({ x: 1, y: 1 }, 30, 1000);
|
|
19246
|
+
* ```
|
|
19247
|
+
*/
|
|
18678
19248
|
oscillate(direction, amplitude = 50, period = 2e3) {
|
|
18679
19249
|
this.addMovement(new Oscillate(direction, amplitude, period));
|
|
18680
19250
|
}
|
|
18681
|
-
|
|
19251
|
+
/**
|
|
19252
|
+
* Apply ice movement physics
|
|
19253
|
+
*
|
|
19254
|
+
* Simulates slippery surface physics where the entity accelerates gradually
|
|
19255
|
+
* and has difficulty stopping. The maximum speed is based on the player's
|
|
19256
|
+
* base speed multiplied by a speed factor.
|
|
19257
|
+
*
|
|
19258
|
+
* With default speed=4 and factor=1: maxSpeed = 4 (same as original default)
|
|
19259
|
+
*
|
|
19260
|
+
* @param direction - Target movement direction `{ x, y }`
|
|
19261
|
+
* @param speedFactor - Factor multiplied with base speed for max speed (default: 1.0)
|
|
19262
|
+
*
|
|
19263
|
+
* @example
|
|
19264
|
+
* ```ts
|
|
19265
|
+
* // Normal ice physics
|
|
19266
|
+
* player.applyIceMovement({ x: 1, y: 0 });
|
|
19267
|
+
*
|
|
19268
|
+
* // Fast ice sliding
|
|
19269
|
+
* player.applyIceMovement({ x: 0, y: 1 }, 1.5);
|
|
19270
|
+
* ```
|
|
19271
|
+
*/
|
|
19272
|
+
applyIceMovement(direction, speedFactor = 1) {
|
|
19273
|
+
const playerSpeed = this.speed();
|
|
19274
|
+
const maxSpeed = playerSpeed * speedFactor;
|
|
18682
19275
|
this.addMovement(new IceMovement(direction, maxSpeed));
|
|
18683
19276
|
}
|
|
18684
|
-
|
|
19277
|
+
/**
|
|
19278
|
+
* Shoot a projectile in the specified direction
|
|
19279
|
+
*
|
|
19280
|
+
* Creates a projectile with ballistic trajectory. The speed is calculated
|
|
19281
|
+
* from the player's base speed multiplied by a speed factor.
|
|
19282
|
+
*
|
|
19283
|
+
* With default speed=4 and factor=50: speed = 200 (same as original default)
|
|
19284
|
+
*
|
|
19285
|
+
* @param type - Type of projectile trajectory (`Straight`, `Arc`, or `Bounce`)
|
|
19286
|
+
* @param direction - Normalized direction vector `{ x, y }`
|
|
19287
|
+
* @param speedFactor - Factor multiplied with base speed (default: 50)
|
|
19288
|
+
*
|
|
19289
|
+
* @example
|
|
19290
|
+
* ```ts
|
|
19291
|
+
* // Straight projectile
|
|
19292
|
+
* player.shootProjectile(ProjectileType.Straight, { x: 1, y: 0 });
|
|
19293
|
+
*
|
|
19294
|
+
* // Fast arc projectile
|
|
19295
|
+
* player.shootProjectile(ProjectileType.Arc, { x: 1, y: -0.5 }, 75);
|
|
19296
|
+
* ```
|
|
19297
|
+
*/
|
|
19298
|
+
shootProjectile(type, direction, speedFactor = 50) {
|
|
19299
|
+
const playerSpeed = this.speed();
|
|
19300
|
+
const speed = playerSpeed * speedFactor;
|
|
18685
19301
|
const config = {
|
|
18686
19302
|
speed,
|
|
18687
19303
|
direction,
|
|
@@ -18744,10 +19360,13 @@ function WithMoveManager(Base) {
|
|
|
18744
19360
|
this.stuckThreshold = options2?.stuckThreshold ?? 1;
|
|
18745
19361
|
this.processNextRoute();
|
|
18746
19362
|
}
|
|
19363
|
+
debugLog(message, data) {
|
|
19364
|
+
}
|
|
18747
19365
|
processNextRoute() {
|
|
18748
19366
|
this.waitingForFrequency = false;
|
|
18749
19367
|
this.frequencyWaitStartTime = 0;
|
|
18750
19368
|
if (this.routeIndex >= this.routes.length) {
|
|
19369
|
+
this.debugLog("COMPLETE all routes finished");
|
|
18751
19370
|
this.finished = true;
|
|
18752
19371
|
this.onComplete(true);
|
|
18753
19372
|
return;
|
|
@@ -18760,13 +19379,16 @@ function WithMoveManager(Base) {
|
|
|
18760
19379
|
}
|
|
18761
19380
|
try {
|
|
18762
19381
|
if (typeof currentRoute === "object" && "then" in currentRoute) {
|
|
19382
|
+
this.debugLog(`WAIT for promise (route ${this.routeIndex}/${this.routes.length})`);
|
|
18763
19383
|
this.waitingForPromise = true;
|
|
18764
19384
|
this.promiseStartTime = Date.now();
|
|
18765
19385
|
this.promiseDuration = 1e3;
|
|
18766
19386
|
currentRoute.then(() => {
|
|
19387
|
+
this.debugLog("WAIT promise resolved");
|
|
18767
19388
|
this.waitingForPromise = false;
|
|
18768
19389
|
this.processNextRoute();
|
|
18769
19390
|
}).catch(() => {
|
|
19391
|
+
this.debugLog("WAIT promise rejected");
|
|
18770
19392
|
this.waitingForPromise = false;
|
|
18771
19393
|
this.processNextRoute();
|
|
18772
19394
|
});
|
|
@@ -18791,6 +19413,7 @@ function WithMoveManager(Base) {
|
|
|
18791
19413
|
direction = Direction.Right;
|
|
18792
19414
|
break;
|
|
18793
19415
|
}
|
|
19416
|
+
this.debugLog(`TURN to ${directionStr}`);
|
|
18794
19417
|
if (this.player.changeDirection) {
|
|
18795
19418
|
this.player.changeDirection(direction);
|
|
18796
19419
|
}
|
|
@@ -18852,6 +19475,7 @@ function WithMoveManager(Base) {
|
|
|
18852
19475
|
this.currentTarget = { x: targetX, y: targetY };
|
|
18853
19476
|
this.currentTargetTopLeft = { x: targetTopLeftX, y: targetTopLeftY };
|
|
18854
19477
|
this.currentDirection = { x: 0, y: 0 };
|
|
19478
|
+
this.debugLog(`MOVE direction=${moveDirection} from=(${currentTopLeftX.toFixed(1)}, ${currentTopLeftY.toFixed(1)}) to=(${targetTopLeftX.toFixed(1)}, ${targetTopLeftY.toFixed(1)}) dist=${distance.toFixed(1)}`);
|
|
18855
19479
|
this.lastPosition = null;
|
|
18856
19480
|
this.isCurrentlyStuck = false;
|
|
18857
19481
|
this.stuckCheckStartTime = 0;
|
|
@@ -18921,6 +19545,7 @@ function WithMoveManager(Base) {
|
|
|
18921
19545
|
dy = this.currentTargetTopLeft.y - currentTopLeftY;
|
|
18922
19546
|
distance = Math.hypot(dx, dy);
|
|
18923
19547
|
if (distance <= this.tolerance) {
|
|
19548
|
+
this.debugLog(`TARGET reached at (${currentTopLeftX.toFixed(1)}, ${currentTopLeftY.toFixed(1)})`);
|
|
18924
19549
|
this.currentTarget = null;
|
|
18925
19550
|
this.currentTargetTopLeft = null;
|
|
18926
19551
|
this.currentDirection = { x: 0, y: 0 };
|
|
@@ -18983,21 +19608,30 @@ function WithMoveManager(Base) {
|
|
|
18983
19608
|
this.isCurrentlyStuck = true;
|
|
18984
19609
|
} else {
|
|
18985
19610
|
if (currentTime - this.stuckCheckStartTime >= this.stuckTimeout) {
|
|
19611
|
+
this.debugLog(`STUCK detected at (${currentPosition.x.toFixed(1)}, ${currentPosition.y.toFixed(1)}) target=(${this.currentTarget.x.toFixed(1)}, ${this.currentTarget.y.toFixed(1)})`);
|
|
18986
19612
|
const shouldContinue = this.onStuck(
|
|
18987
19613
|
this.player,
|
|
18988
19614
|
this.currentTarget,
|
|
18989
19615
|
currentPosition
|
|
18990
19616
|
);
|
|
18991
19617
|
if (shouldContinue === false) {
|
|
19618
|
+
this.debugLog("STUCK cancelled route");
|
|
18992
19619
|
this.finished = true;
|
|
18993
19620
|
this.onComplete(false);
|
|
18994
19621
|
body.setVelocity({ x: 0, y: 0 });
|
|
18995
19622
|
return;
|
|
18996
19623
|
}
|
|
19624
|
+
this.currentTarget = null;
|
|
19625
|
+
this.currentTargetTopLeft = null;
|
|
19626
|
+
this.currentDirection = { x: 0, y: 0 };
|
|
19627
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
18997
19628
|
this.isCurrentlyStuck = false;
|
|
18998
19629
|
this.stuckCheckStartTime = 0;
|
|
18999
|
-
this.lastPosition =
|
|
19000
|
-
this.lastDistanceToTarget =
|
|
19630
|
+
this.lastPosition = null;
|
|
19631
|
+
this.lastDistanceToTarget = null;
|
|
19632
|
+
this.stuckCheckInitialized = false;
|
|
19633
|
+
this.processNextRoute();
|
|
19634
|
+
return;
|
|
19001
19635
|
}
|
|
19002
19636
|
}
|
|
19003
19637
|
} else {
|
|
@@ -19066,12 +19700,12 @@ function WithMoveManager(Base) {
|
|
|
19066
19700
|
return acc.concat(item);
|
|
19067
19701
|
}, []);
|
|
19068
19702
|
}
|
|
19069
|
-
infiniteMoveRoute(routes) {
|
|
19703
|
+
infiniteMoveRoute(routes, options) {
|
|
19070
19704
|
this._infiniteRoutes = routes;
|
|
19071
19705
|
this._isInfiniteRouteActive = true;
|
|
19072
19706
|
const executeInfiniteRoute = (isBreaking = false) => {
|
|
19073
19707
|
if (isBreaking || !this._isInfiniteRouteActive) return;
|
|
19074
|
-
this.moveRoutes(routes).then((completed) => {
|
|
19708
|
+
this.moveRoutes(routes, options).then((completed) => {
|
|
19075
19709
|
if (completed && this._isInfiniteRouteActive) {
|
|
19076
19710
|
executeInfiniteRoute();
|
|
19077
19711
|
}
|
|
@@ -20362,64 +20996,208 @@ function WithElementManager(Base) {
|
|
|
20362
20996
|
|
|
20363
20997
|
function WithSkillManager(Base) {
|
|
20364
20998
|
return class extends Base {
|
|
20365
|
-
|
|
20999
|
+
/**
|
|
21000
|
+
* Find the index of a skill in the skills array
|
|
21001
|
+
*
|
|
21002
|
+
* Searches by ID for both string inputs and object/class inputs.
|
|
21003
|
+
*
|
|
21004
|
+
* @param skillInput - Skill ID, class, or object to find
|
|
21005
|
+
* @returns Index of the skill or -1 if not found
|
|
21006
|
+
*/
|
|
21007
|
+
_getSkillIndex(skillInput) {
|
|
21008
|
+
let searchId = "";
|
|
21009
|
+
if (isString(skillInput)) {
|
|
21010
|
+
searchId = skillInput;
|
|
21011
|
+
} else if (typeof skillInput === "function") {
|
|
21012
|
+
searchId = skillInput.id || skillInput.name;
|
|
21013
|
+
} else {
|
|
21014
|
+
searchId = skillInput.id || "";
|
|
21015
|
+
}
|
|
20366
21016
|
return this.skills().findIndex((skill) => {
|
|
20367
|
-
|
|
20368
|
-
|
|
20369
|
-
}
|
|
20370
|
-
if (isString(skillClass)) {
|
|
20371
|
-
return skillClass == (skill.id || skill);
|
|
20372
|
-
}
|
|
20373
|
-
return isInstanceOf(skill, skillClass);
|
|
21017
|
+
const skillId = skill.id || skill.name || "";
|
|
21018
|
+
return skillId === searchId;
|
|
20374
21019
|
});
|
|
20375
21020
|
}
|
|
20376
|
-
|
|
20377
|
-
|
|
21021
|
+
/**
|
|
21022
|
+
* Retrieves a learned skill
|
|
21023
|
+
*
|
|
21024
|
+
* Searches the player's learned skills by ID, class, or object.
|
|
21025
|
+
*
|
|
21026
|
+
* @param skillInput - Skill ID, class, or object to find
|
|
21027
|
+
* @returns The skill data if found, null otherwise
|
|
21028
|
+
*
|
|
21029
|
+
* @example
|
|
21030
|
+
* ```ts
|
|
21031
|
+
* const skill = player.getSkill('fire');
|
|
21032
|
+
* if (skill) {
|
|
21033
|
+
* console.log(`Fire skill costs ${skill.spCost} SP`);
|
|
21034
|
+
* }
|
|
21035
|
+
* ```
|
|
21036
|
+
*/
|
|
21037
|
+
getSkill(skillInput) {
|
|
21038
|
+
const index = this._getSkillIndex(skillInput);
|
|
20378
21039
|
return this.skills()[index] ?? null;
|
|
20379
21040
|
}
|
|
20380
|
-
|
|
21041
|
+
/**
|
|
21042
|
+
* Learn a new skill
|
|
21043
|
+
*
|
|
21044
|
+
* Adds a skill to the player's skill list. Supports three input formats:
|
|
21045
|
+
* - **String ID**: Retrieves the skill from the database
|
|
21046
|
+
* - **Class**: Creates an instance and adds to database if needed
|
|
21047
|
+
* - **Object**: Uses directly and adds to database if needed
|
|
21048
|
+
*
|
|
21049
|
+
* @param skillInput - Skill ID, class, or object to learn
|
|
21050
|
+
* @returns The learned skill data
|
|
21051
|
+
* @throws SkillLog.alreadyLearned if the skill is already known
|
|
21052
|
+
*
|
|
21053
|
+
* @example
|
|
21054
|
+
* ```ts
|
|
21055
|
+
* // From database
|
|
21056
|
+
* player.learnSkill('fire');
|
|
21057
|
+
*
|
|
21058
|
+
* // From class
|
|
21059
|
+
* player.learnSkill(FireSkill);
|
|
21060
|
+
*
|
|
21061
|
+
* // From object
|
|
21062
|
+
* player.learnSkill({
|
|
21063
|
+
* id: 'custom-skill',
|
|
21064
|
+
* name: 'Custom Skill',
|
|
21065
|
+
* spCost: 20,
|
|
21066
|
+
* onLearn(player) {
|
|
21067
|
+
* console.log('Learned custom skill!');
|
|
21068
|
+
* }
|
|
21069
|
+
* });
|
|
21070
|
+
* ```
|
|
21071
|
+
*/
|
|
21072
|
+
learnSkill(skillInput) {
|
|
21073
|
+
const map = this.getCurrentMap() || this.map;
|
|
21074
|
+
let skillId = "";
|
|
21075
|
+
let skillData;
|
|
21076
|
+
if (isString(skillInput)) {
|
|
21077
|
+
skillId = skillInput;
|
|
21078
|
+
skillData = this.databaseById(skillId);
|
|
21079
|
+
} else if (typeof skillInput === "function") {
|
|
21080
|
+
const SkillClassCtor = skillInput;
|
|
21081
|
+
skillId = SkillClassCtor.id || SkillClassCtor.name;
|
|
21082
|
+
const existingData = map?.database()?.[skillId];
|
|
21083
|
+
if (existingData) {
|
|
21084
|
+
skillData = existingData;
|
|
21085
|
+
} else if (map) {
|
|
21086
|
+
map.addInDatabase(skillId, SkillClassCtor);
|
|
21087
|
+
skillData = SkillClassCtor;
|
|
21088
|
+
} else {
|
|
21089
|
+
skillData = SkillClassCtor;
|
|
21090
|
+
}
|
|
21091
|
+
const skillInstance = new SkillClassCtor();
|
|
21092
|
+
skillData = { ...skillData, ...skillInstance, id: skillId };
|
|
21093
|
+
} else {
|
|
21094
|
+
const skillObj = skillInput;
|
|
21095
|
+
skillId = skillObj.id || `skill-${Date.now()}`;
|
|
21096
|
+
skillObj.id = skillId;
|
|
21097
|
+
const existingData = map?.database()?.[skillId];
|
|
21098
|
+
if (existingData) {
|
|
21099
|
+
skillData = { ...existingData, ...skillObj };
|
|
21100
|
+
if (map) {
|
|
21101
|
+
map.addInDatabase(skillId, skillData, { force: true });
|
|
21102
|
+
}
|
|
21103
|
+
} else if (map) {
|
|
21104
|
+
map.addInDatabase(skillId, skillObj);
|
|
21105
|
+
skillData = skillObj;
|
|
21106
|
+
} else {
|
|
21107
|
+
skillData = skillObj;
|
|
21108
|
+
}
|
|
21109
|
+
}
|
|
20381
21110
|
if (this.getSkill(skillId)) {
|
|
20382
|
-
throw SkillLog.alreadyLearned(
|
|
21111
|
+
throw SkillLog.alreadyLearned(skillData);
|
|
20383
21112
|
}
|
|
20384
|
-
|
|
20385
|
-
this
|
|
20386
|
-
|
|
20387
|
-
return instance;
|
|
21113
|
+
this.skills().push(skillData);
|
|
21114
|
+
this["execMethod"]("onLearn", [this], skillData);
|
|
21115
|
+
return skillData;
|
|
20388
21116
|
}
|
|
20389
|
-
|
|
20390
|
-
|
|
20391
|
-
|
|
20392
|
-
|
|
20393
|
-
|
|
21117
|
+
/**
|
|
21118
|
+
* Forget a learned skill
|
|
21119
|
+
*
|
|
21120
|
+
* Removes a skill from the player's skill list.
|
|
21121
|
+
*
|
|
21122
|
+
* @param skillInput - Skill ID, class, or object to forget
|
|
21123
|
+
* @returns The forgotten skill data
|
|
21124
|
+
* @throws SkillLog.notLearned if the skill is not known
|
|
21125
|
+
*
|
|
21126
|
+
* @example
|
|
21127
|
+
* ```ts
|
|
21128
|
+
* player.forgetSkill('fire');
|
|
21129
|
+
* // or
|
|
21130
|
+
* player.forgetSkill(FireSkill);
|
|
21131
|
+
* ```
|
|
21132
|
+
*/
|
|
21133
|
+
forgetSkill(skillInput) {
|
|
21134
|
+
const index = this._getSkillIndex(skillInput);
|
|
21135
|
+
if (index === -1) {
|
|
21136
|
+
let skillData2 = skillInput;
|
|
21137
|
+
if (isString(skillInput)) {
|
|
21138
|
+
try {
|
|
21139
|
+
skillData2 = this.databaseById(skillInput);
|
|
21140
|
+
} catch {
|
|
21141
|
+
skillData2 = { name: skillInput, id: skillInput };
|
|
21142
|
+
}
|
|
21143
|
+
} else if (typeof skillInput === "function") {
|
|
21144
|
+
skillData2 = { name: skillInput.name, id: skillInput.id || skillInput.name };
|
|
21145
|
+
}
|
|
21146
|
+
throw SkillLog.notLearned(skillData2);
|
|
20394
21147
|
}
|
|
20395
|
-
const
|
|
21148
|
+
const skillData = this.skills()[index];
|
|
20396
21149
|
this.skills().splice(index, 1);
|
|
20397
|
-
this["execMethod"]("onForget", [this],
|
|
20398
|
-
return
|
|
21150
|
+
this["execMethod"]("onForget", [this], skillData);
|
|
21151
|
+
return skillData;
|
|
20399
21152
|
}
|
|
20400
|
-
|
|
20401
|
-
|
|
21153
|
+
/**
|
|
21154
|
+
* Use a learned skill
|
|
21155
|
+
*
|
|
21156
|
+
* Executes a skill, consuming SP and applying effects to targets.
|
|
21157
|
+
* The skill must be learned and the player must have enough SP.
|
|
21158
|
+
*
|
|
21159
|
+
* @param skillInput - Skill ID, class, or object to use
|
|
21160
|
+
* @param otherPlayer - Optional target player(s) to apply skill effects to
|
|
21161
|
+
* @returns The used skill data
|
|
21162
|
+
* @throws SkillLog.restriction if player has CAN_NOT_SKILL effect
|
|
21163
|
+
* @throws SkillLog.notLearned if skill is not known
|
|
21164
|
+
* @throws SkillLog.notEnoughSp if not enough SP
|
|
21165
|
+
* @throws SkillLog.chanceToUseFailed if hit rate check fails
|
|
21166
|
+
*
|
|
21167
|
+
* @example
|
|
21168
|
+
* ```ts
|
|
21169
|
+
* // Use skill without target
|
|
21170
|
+
* player.useSkill('fire');
|
|
21171
|
+
*
|
|
21172
|
+
* // Use skill on a target
|
|
21173
|
+
* player.useSkill('fire', enemy);
|
|
21174
|
+
*
|
|
21175
|
+
* // Use skill on multiple targets
|
|
21176
|
+
* player.useSkill('fire', [enemy1, enemy2]);
|
|
21177
|
+
* ```
|
|
21178
|
+
*/
|
|
21179
|
+
useSkill(skillInput, otherPlayer) {
|
|
21180
|
+
const skill = this.getSkill(skillInput);
|
|
20402
21181
|
if (this.hasEffect(Effect.CAN_NOT_SKILL)) {
|
|
20403
|
-
throw SkillLog.restriction(
|
|
21182
|
+
throw SkillLog.restriction(skill || skillInput);
|
|
20404
21183
|
}
|
|
20405
21184
|
if (!skill) {
|
|
20406
|
-
throw SkillLog.notLearned(
|
|
21185
|
+
throw SkillLog.notLearned(skillInput);
|
|
20407
21186
|
}
|
|
20408
|
-
|
|
20409
|
-
|
|
21187
|
+
const spCost = skill.spCost || 0;
|
|
21188
|
+
if (spCost > this.sp) {
|
|
21189
|
+
throw SkillLog.notEnoughSp(skill, spCost, this.sp);
|
|
20410
21190
|
}
|
|
20411
|
-
|
|
21191
|
+
const costMultiplier = this.hasEffect(Effect.HALF_SP_COST) ? 2 : 1;
|
|
21192
|
+
this.sp -= spCost / costMultiplier;
|
|
20412
21193
|
const hitRate = skill.hitRate ?? 1;
|
|
20413
21194
|
if (Math.random() > hitRate) {
|
|
20414
21195
|
this["execMethod"]("onUseFailed", [this, otherPlayer], skill);
|
|
20415
|
-
throw SkillLog.chanceToUseFailed(
|
|
21196
|
+
throw SkillLog.chanceToUseFailed(skill);
|
|
20416
21197
|
}
|
|
20417
21198
|
if (otherPlayer) {
|
|
20418
|
-
|
|
20419
|
-
|
|
20420
|
-
players = [otherPlayer];
|
|
20421
|
-
}
|
|
20422
|
-
for (let player of players) {
|
|
21199
|
+
const players = isArray(otherPlayer) ? otherPlayer : [otherPlayer];
|
|
21200
|
+
for (const player of players) {
|
|
20423
21201
|
this.applyStates(player, skill);
|
|
20424
21202
|
player.applyDamage(this, skill);
|
|
20425
21203
|
}
|
|
@@ -20941,6 +21719,8 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
20941
21719
|
* When `nbTimes` is set to a finite number, the animation will play that many times
|
|
20942
21720
|
* before returning to the previous animation state.
|
|
20943
21721
|
*
|
|
21722
|
+
* If `animationFixed` is true, this method will not change the animation.
|
|
21723
|
+
*
|
|
20944
21724
|
* @param animationName - The name of the animation to play (e.g., 'attack', 'skill', 'walk')
|
|
20945
21725
|
* @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)
|
|
20946
21726
|
*
|
|
@@ -20952,14 +21732,18 @@ const _RpgPlayer = class _RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
20952
21732
|
* // Play attack animation 3 times then return to previous state
|
|
20953
21733
|
* player.setAnimation('attack', 3);
|
|
20954
21734
|
*
|
|
20955
|
-
* //
|
|
20956
|
-
* player.
|
|
21735
|
+
* // Lock animation to prevent automatic changes
|
|
21736
|
+
* player.animationFixed = true;
|
|
21737
|
+
* player.setAnimation('skill'); // This will be ignored
|
|
20957
21738
|
*
|
|
20958
21739
|
* // Set idle/stand animation
|
|
20959
21740
|
* player.setAnimation('stand');
|
|
20960
21741
|
* ```
|
|
20961
21742
|
*/
|
|
20962
21743
|
setAnimation(animationName, nbTimes = Infinity) {
|
|
21744
|
+
if (this.animationFixed) {
|
|
21745
|
+
return;
|
|
21746
|
+
}
|
|
20963
21747
|
const map2 = this.getCurrentMap();
|
|
20964
21748
|
if (!map2) return;
|
|
20965
21749
|
if (nbTimes === Infinity) {
|
|
@@ -21559,9 +22343,16 @@ class RpgEvent extends RpgPlayer {
|
|
|
21559
22343
|
const ret = instance[methodName](...methodData);
|
|
21560
22344
|
return ret;
|
|
21561
22345
|
}
|
|
22346
|
+
/**
|
|
22347
|
+
* Remove this event from the map
|
|
22348
|
+
*
|
|
22349
|
+
* Stops all movements before removing to prevent "unable to resolve entity" errors
|
|
22350
|
+
* from the MovementManager when the entity is destroyed while moving.
|
|
22351
|
+
*/
|
|
21562
22352
|
remove() {
|
|
21563
22353
|
const map2 = this.getCurrentMap();
|
|
21564
22354
|
if (!map2) return;
|
|
22355
|
+
this.stopMoveTo();
|
|
21565
22356
|
map2.removeEvent(this.id);
|
|
21566
22357
|
}
|
|
21567
22358
|
isEvent() {
|
|
@@ -25983,9 +26774,17 @@ let RpgMap = class extends RpgCommonMap {
|
|
|
25983
26774
|
setupCollisionDetection() {
|
|
25984
26775
|
const activeCollisions = /* @__PURE__ */ new Set();
|
|
25985
26776
|
const activeShapeCollisions = /* @__PURE__ */ new Set();
|
|
26777
|
+
const hasDifferentZ = (entityA, entityB) => {
|
|
26778
|
+
const zA = entityA.owner.z();
|
|
26779
|
+
const zB = entityB.owner.z();
|
|
26780
|
+
return zA !== zB;
|
|
26781
|
+
};
|
|
25986
26782
|
this.physic.getEvents().onCollisionEnter((collision) => {
|
|
25987
26783
|
const entityA = collision.entityA;
|
|
25988
26784
|
const entityB = collision.entityB;
|
|
26785
|
+
if (hasDifferentZ(entityA, entityB)) {
|
|
26786
|
+
return;
|
|
26787
|
+
}
|
|
25989
26788
|
const collisionKey = entityA.uuid < entityB.uuid ? `${entityA.uuid}-${entityB.uuid}` : `${entityB.uuid}-${entityA.uuid}`;
|
|
25990
26789
|
if (activeCollisions.has(collisionKey)) {
|
|
25991
26790
|
return;
|
|
@@ -26025,6 +26824,9 @@ let RpgMap = class extends RpgCommonMap {
|
|
|
26025
26824
|
this.physic.getEvents().onCollisionExit((collision) => {
|
|
26026
26825
|
const entityA = collision.entityA;
|
|
26027
26826
|
const entityB = collision.entityB;
|
|
26827
|
+
if (hasDifferentZ(entityA, entityB)) {
|
|
26828
|
+
return;
|
|
26829
|
+
}
|
|
26028
26830
|
const collisionKey = entityA.uuid < entityB.uuid ? `${entityA.uuid}-${entityB.uuid}` : `${entityB.uuid}-${entityA.uuid}`;
|
|
26029
26831
|
const shapeA = this._shapeEntities.get(entityA.uuid);
|
|
26030
26832
|
const shapeB = this._shapeEntities.get(entityB.uuid);
|