@rpgjs/server 5.0.0-alpha.8 → 5.0.0-beta.1
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/Gui/DialogGui.d.ts +5 -0
- package/dist/Gui/GameoverGui.d.ts +23 -0
- package/dist/Gui/Gui.d.ts +6 -0
- package/dist/Gui/MenuGui.d.ts +22 -3
- package/dist/Gui/NotificationGui.d.ts +1 -2
- package/dist/Gui/SaveLoadGui.d.ts +13 -0
- package/dist/Gui/ShopGui.d.ts +28 -3
- package/dist/Gui/TitleGui.d.ts +23 -0
- package/dist/Gui/index.d.ts +10 -1
- package/dist/Player/BattleManager.d.ts +44 -32
- package/dist/Player/ClassManager.d.ts +24 -4
- package/dist/Player/ComponentManager.d.ts +100 -7
- package/dist/Player/Components.d.ts +345 -0
- package/dist/Player/EffectManager.d.ts +50 -4
- package/dist/Player/ElementManager.d.ts +77 -4
- package/dist/Player/GoldManager.d.ts +1 -1
- package/dist/Player/GuiManager.d.ts +233 -5
- package/dist/Player/ItemFixture.d.ts +1 -1
- package/dist/Player/ItemManager.d.ts +431 -4
- package/dist/Player/MoveManager.d.ts +301 -34
- package/dist/Player/ParameterManager.d.ts +364 -28
- package/dist/Player/Player.d.ts +558 -14
- package/dist/Player/SkillManager.d.ts +187 -13
- package/dist/Player/StateManager.d.ts +75 -4
- package/dist/Player/VariableManager.d.ts +62 -4
- package/dist/RpgServer.d.ts +278 -63
- package/dist/RpgServerEngine.d.ts +2 -1
- package/dist/decorators/event.d.ts +46 -0
- package/dist/decorators/map.d.ts +299 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +17920 -29866
- package/dist/index.js.map +1 -1
- package/dist/logs/log.d.ts +2 -3
- package/dist/module-CaCW1SDh.js +11018 -0
- package/dist/module-CaCW1SDh.js.map +1 -0
- package/dist/module.d.ts +43 -1
- package/dist/node/connection.d.ts +51 -0
- package/dist/node/index.d.ts +5 -0
- package/dist/node/index.js +551 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/map.d.ts +16 -0
- package/dist/node/room.d.ts +21 -0
- package/dist/node/transport.d.ts +28 -0
- package/dist/node/types.d.ts +47 -0
- package/dist/presets/index.d.ts +0 -9
- package/dist/rooms/BaseRoom.d.ts +132 -0
- package/dist/rooms/lobby.d.ts +10 -2
- package/dist/rooms/map.d.ts +1359 -32
- package/dist/services/save.d.ts +43 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/localStorage.d.ts +23 -0
- package/package.json +25 -10
- package/src/Gui/DialogGui.ts +19 -4
- package/src/Gui/GameoverGui.ts +39 -0
- package/src/Gui/Gui.ts +23 -1
- package/src/Gui/MenuGui.ts +155 -6
- package/src/Gui/NotificationGui.ts +1 -2
- package/src/Gui/SaveLoadGui.ts +60 -0
- package/src/Gui/ShopGui.ts +146 -16
- package/src/Gui/TitleGui.ts +39 -0
- package/src/Gui/index.ts +15 -2
- package/src/Player/BattleManager.ts +39 -56
- package/src/Player/ClassManager.ts +82 -74
- package/src/Player/ComponentManager.ts +401 -37
- package/src/Player/Components.ts +380 -0
- package/src/Player/EffectManager.ts +50 -96
- package/src/Player/ElementManager.ts +74 -152
- package/src/Player/GuiManager.ts +284 -149
- package/src/Player/ItemManager.ts +747 -341
- package/src/Player/MoveManager.ts +1532 -750
- package/src/Player/ParameterManager.ts +636 -106
- package/src/Player/Player.ts +1273 -79
- package/src/Player/SkillManager.ts +558 -197
- package/src/Player/StateManager.ts +131 -258
- package/src/Player/VariableManager.ts +85 -157
- package/src/RpgServer.ts +293 -62
- package/src/decorators/event.ts +61 -0
- package/src/decorators/map.ts +343 -0
- package/src/index.ts +11 -1
- package/src/logs/log.ts +10 -3
- package/src/module.ts +126 -3
- package/src/node/connection.ts +254 -0
- package/src/node/index.ts +22 -0
- package/src/node/map.ts +328 -0
- package/src/node/room.ts +63 -0
- package/src/node/transport.ts +532 -0
- package/src/node/types.ts +61 -0
- package/src/presets/index.ts +1 -10
- package/src/rooms/BaseRoom.ts +232 -0
- package/src/rooms/lobby.ts +25 -7
- package/src/rooms/map.ts +2682 -206
- package/src/services/save.ts +147 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/localStorage.ts +76 -0
- package/tests/battle.spec.ts +375 -0
- package/tests/change-map.spec.ts +72 -0
- package/tests/class.spec.ts +274 -0
- package/tests/custom-websocket.spec.ts +127 -0
- package/tests/effect.spec.ts +219 -0
- package/tests/element.spec.ts +221 -0
- package/tests/event.spec.ts +80 -0
- package/tests/gold.spec.ts +99 -0
- package/tests/item.spec.ts +609 -0
- package/tests/module.spec.ts +38 -0
- package/tests/move.spec.ts +601 -0
- package/tests/node-transport.spec.ts +223 -0
- package/tests/player-param.spec.ts +45 -0
- package/tests/prediction-reconciliation.spec.ts +182 -0
- package/tests/random-move.spec.ts +65 -0
- package/tests/skill.spec.ts +658 -0
- package/tests/state.spec.ts +467 -0
- package/tests/variable.spec.ts +185 -0
- package/tests/world-maps.spec.ts +896 -0
- package/vite.config.ts +36 -3
- package/dist/Player/Event.d.ts +0 -0
- package/src/Player/Event.ts +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { PlayerCtor,
|
|
2
|
-
import { RpgCommonPlayer,
|
|
3
|
-
import {
|
|
4
|
-
MovementManager,
|
|
1
|
+
import { PlayerCtor, ProjectileType } from "@rpgjs/common";
|
|
2
|
+
import { RpgCommonPlayer, Direction, Entity } from "@rpgjs/common";
|
|
3
|
+
import {
|
|
5
4
|
MovementStrategy,
|
|
5
|
+
MovementOptions,
|
|
6
6
|
LinearMove,
|
|
7
7
|
Dash,
|
|
8
8
|
Knockback,
|
|
@@ -13,15 +13,83 @@ import {
|
|
|
13
13
|
LinearRepulsion,
|
|
14
14
|
IceMovement,
|
|
15
15
|
ProjectileMovement,
|
|
16
|
-
ProjectileType,
|
|
17
16
|
random,
|
|
18
17
|
isFunction,
|
|
19
|
-
capitalize
|
|
18
|
+
capitalize,
|
|
19
|
+
PerlinNoise2D
|
|
20
20
|
} from "@rpgjs/common";
|
|
21
|
+
import type { MovementBody } from "@rpgjs/physic";
|
|
21
22
|
import { RpgMap } from "../rooms/map";
|
|
22
|
-
import { Observable, Subscription, takeUntil, Subject, tap, switchMap, of, from } from 'rxjs';
|
|
23
|
+
import { Observable, Subscription, takeUntil, Subject, tap, switchMap, of, from, take } from 'rxjs';
|
|
23
24
|
import { RpgPlayer } from "./Player";
|
|
24
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Additive knockback strategy that **adds** an impulse-like velocity on top of the
|
|
28
|
+
* current velocity, instead of overwriting it.
|
|
29
|
+
*
|
|
30
|
+
* This is designed for A-RPG gameplay where the player should keep control during
|
|
31
|
+
* knockback. Inputs keep setting the base velocity, and this strategy adds a decaying
|
|
32
|
+
* impulse each physics step for a short duration.
|
|
33
|
+
*
|
|
34
|
+
* ## Design
|
|
35
|
+
*
|
|
36
|
+
* - The built-in physics `Knockback` strategy overwrites velocity every frame.
|
|
37
|
+
* When inputs also change velocity, the two systems compete and can cause visible jitter.
|
|
38
|
+
* - This strategy avoids the "tug of war" by reading the current velocity and adding
|
|
39
|
+
* the knockback impulse on top.
|
|
40
|
+
* - When finished, it does **not** force velocity to zero, so player input remains responsive.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* // Add a short impulse to the right, while player can still steer
|
|
45
|
+
* await player.addMovement(new AdditiveKnockback({ x: 1, y: 0 }, 5, 0.3));
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
class AdditiveKnockback implements MovementStrategy {
|
|
49
|
+
private readonly direction: { x: number; y: number };
|
|
50
|
+
private elapsed = 0;
|
|
51
|
+
private currentSpeed: number;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
direction: { x: number; y: number },
|
|
55
|
+
initialSpeed: number,
|
|
56
|
+
private readonly duration: number,
|
|
57
|
+
private readonly decayFactor = 0.35
|
|
58
|
+
) {
|
|
59
|
+
const magnitude = Math.hypot(direction.x, direction.y);
|
|
60
|
+
this.direction = magnitude > 0
|
|
61
|
+
? { x: direction.x / magnitude, y: direction.y / magnitude }
|
|
62
|
+
: { x: 1, y: 0 };
|
|
63
|
+
this.currentSpeed = initialSpeed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
update(body: MovementBody, dt: number): void {
|
|
67
|
+
this.elapsed += dt;
|
|
68
|
+
if (this.elapsed > this.duration) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const impulseX = this.direction.x * this.currentSpeed;
|
|
73
|
+
const impulseY = this.direction.y * this.currentSpeed;
|
|
74
|
+
|
|
75
|
+
body.setVelocity({
|
|
76
|
+
x: body.velocity.x + impulseX,
|
|
77
|
+
y: body.velocity.y + impulseY,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const decay = Math.max(0, Math.min(1, this.decayFactor));
|
|
81
|
+
if (decay === 0) {
|
|
82
|
+
this.currentSpeed = 0;
|
|
83
|
+
} else if (decay !== 1) {
|
|
84
|
+
this.currentSpeed *= Math.pow(decay, dt);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
isFinished(): boolean {
|
|
89
|
+
return this.elapsed >= this.duration;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
25
93
|
|
|
26
94
|
interface PlayerWithMixins extends RpgCommonPlayer {
|
|
27
95
|
getCurrentMap(): RpgMap;
|
|
@@ -35,11 +103,9 @@ interface PlayerWithMixins extends RpgCommonPlayer {
|
|
|
35
103
|
}
|
|
36
104
|
|
|
37
105
|
|
|
38
|
-
|
|
39
|
-
|
|
40
106
|
function wait(sec: number) {
|
|
41
107
|
return new Promise((resolve) => {
|
|
42
|
-
|
|
108
|
+
setTimeout(resolve, sec * 1000)
|
|
43
109
|
})
|
|
44
110
|
}
|
|
45
111
|
|
|
@@ -47,6 +113,51 @@ type CallbackTileMove = (player: RpgPlayer, map) => Direction[]
|
|
|
47
113
|
type CallbackTurnMove = (player: RpgPlayer, map) => string
|
|
48
114
|
type Routes = (string | Promise<any> | Direction | Direction[] | Function)[]
|
|
49
115
|
|
|
116
|
+
// Re-export MovementOptions from @rpgjs/common for convenience
|
|
117
|
+
export type { MovementOptions };
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Options for moveRoutes method
|
|
121
|
+
*/
|
|
122
|
+
export interface MoveRoutesOptions {
|
|
123
|
+
/**
|
|
124
|
+
* Callback function called when the player gets stuck (cannot move towards target)
|
|
125
|
+
*
|
|
126
|
+
* This callback is triggered when the player is trying to move but cannot make progress
|
|
127
|
+
* towards the target position, typically due to obstacles or collisions.
|
|
128
|
+
*
|
|
129
|
+
* @param player - The player instance that is stuck
|
|
130
|
+
* @param target - The target position the player was trying to reach
|
|
131
|
+
* @param currentPosition - The current position of the player
|
|
132
|
+
* @returns If true, the route will continue; if false, the route will be cancelled
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* await player.moveRoutes([Move.right()], {
|
|
137
|
+
* onStuck: (player, target, currentPos) => {
|
|
138
|
+
* console.log('Player is stuck!');
|
|
139
|
+
* return false; // Cancel the route
|
|
140
|
+
* }
|
|
141
|
+
* });
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
onStuck?: (player: RpgPlayer, target: { x: number; y: number }, currentPosition: { x: number; y: number }) => boolean | void;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Time in milliseconds to wait before considering the player stuck (default: 500ms)
|
|
148
|
+
*
|
|
149
|
+
* The player must be unable to make progress for this duration before onStuck is called.
|
|
150
|
+
*/
|
|
151
|
+
stuckTimeout?: number;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Minimum distance change in pixels to consider movement progress (default: 1 pixel)
|
|
155
|
+
*
|
|
156
|
+
* If the player moves less than this distance over the stuckTimeout period, they are considered stuck.
|
|
157
|
+
*/
|
|
158
|
+
stuckThreshold?: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
50
161
|
export enum Frequency {
|
|
51
162
|
Lowest = 600,
|
|
52
163
|
Lower = 400,
|
|
@@ -94,300 +205,317 @@ export enum Speed {
|
|
|
94
205
|
* Move.turnTowardPlayer(player) | Turns in the direction of the designated player
|
|
95
206
|
* @memberof Move
|
|
96
207
|
* */
|
|
208
|
+
/**
|
|
209
|
+
* Tracks player state for stuck detection in random movement
|
|
210
|
+
*
|
|
211
|
+
* Stores the last known position and direction for each player to detect
|
|
212
|
+
* when they are stuck (position hasn't changed) and need a different direction.
|
|
213
|
+
*/
|
|
214
|
+
interface PlayerMoveState {
|
|
215
|
+
lastX: number;
|
|
216
|
+
lastY: number;
|
|
217
|
+
lastDirection: number;
|
|
218
|
+
stuckCount: number;
|
|
219
|
+
}
|
|
220
|
+
|
|
97
221
|
class MoveList {
|
|
222
|
+
// Shared Perlin noise instance for smooth random movement
|
|
223
|
+
private static perlinNoise: PerlinNoise2D = new PerlinNoise2D();
|
|
224
|
+
private static randomCounter: number = 0;
|
|
225
|
+
// Instance counter for each call to ensure variation
|
|
226
|
+
private static callCounter: number = 0;
|
|
227
|
+
// Track player positions and directions to detect stuck state
|
|
228
|
+
private static playerMoveStates: Map<string, PlayerMoveState> = new Map();
|
|
229
|
+
// Threshold for considering a player as "stuck" (in pixels)
|
|
230
|
+
private static readonly STUCK_THRESHOLD = 2;
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Clears the movement state for a specific player
|
|
235
|
+
*
|
|
236
|
+
* Should be called when a player changes map or is destroyed to prevent
|
|
237
|
+
* memory leaks and stale stuck detection data.
|
|
238
|
+
*
|
|
239
|
+
* @param playerId - The ID of the player to clear state for
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```ts
|
|
243
|
+
* // Clear state when player leaves map
|
|
244
|
+
* Move.clearPlayerState(player.id);
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
static clearPlayerState(playerId: string): void {
|
|
248
|
+
MoveList.playerMoveStates.delete(playerId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clears all player movement states
|
|
253
|
+
*
|
|
254
|
+
* Useful for cleanup during server shutdown or when resetting game state.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* ```ts
|
|
258
|
+
* // Clear all states on server shutdown
|
|
259
|
+
* Move.clearAllPlayerStates();
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
static clearAllPlayerStates(): void {
|
|
263
|
+
MoveList.playerMoveStates.clear();
|
|
264
|
+
}
|
|
98
265
|
|
|
99
266
|
repeatMove(direction: Direction, repeat: number): Direction[] {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
267
|
+
// Safety check for valid repeat value
|
|
268
|
+
if (!Number.isFinite(repeat) || repeat < 0 || repeat > 10000) {
|
|
269
|
+
console.warn('Invalid repeat value:', repeat, 'using default value 1');
|
|
270
|
+
repeat = 1;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Ensure repeat is an integer
|
|
274
|
+
repeat = Math.floor(repeat);
|
|
275
|
+
|
|
276
|
+
// Additional safety check - ensure repeat is a safe integer
|
|
277
|
+
if (repeat < 0 || repeat > Number.MAX_SAFE_INTEGER || !Number.isSafeInteger(repeat)) {
|
|
278
|
+
console.warn('Unsafe repeat value:', repeat, 'using default value 1');
|
|
279
|
+
repeat = 1;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
return new Array(repeat).fill(direction);
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('Error creating array with repeat:', repeat, error);
|
|
286
|
+
return [direction]; // Return single direction as fallback
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private repeatTileMove(direction: string, repeat: number, propMap: string): CallbackTileMove {
|
|
291
|
+
return (player: RpgPlayer, map): Direction[] => {
|
|
292
|
+
const playerSpeed = typeof player.speed === 'function' ? player.speed() : player.speed;
|
|
293
|
+
|
|
294
|
+
// Safety checks
|
|
295
|
+
if (!playerSpeed || playerSpeed <= 0) {
|
|
296
|
+
console.warn('Invalid player speed:', playerSpeed, 'using default speed 3');
|
|
297
|
+
return this[direction](repeat);
|
|
104
298
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
repeat = 1;
|
|
299
|
+
|
|
300
|
+
const repeatTile = Math.floor((map[propMap] || 32) / playerSpeed) * repeat;
|
|
301
|
+
|
|
302
|
+
// Additional safety check for the calculated repeat value
|
|
303
|
+
if (!Number.isFinite(repeatTile) || repeatTile < 0 || repeatTile > 10000) {
|
|
304
|
+
console.warn('Calculated repeatTile is invalid:', repeatTile, 'using original repeat:', repeat);
|
|
305
|
+
return this[direction](repeat);
|
|
113
306
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return [direction]; // Return single direction as fallback
|
|
307
|
+
|
|
308
|
+
// Final safety check before calling the method
|
|
309
|
+
if (!Number.isSafeInteger(repeatTile)) {
|
|
310
|
+
console.warn('repeatTile is not a safe integer:', repeatTile, 'using original repeat:', repeat);
|
|
311
|
+
return this[direction](repeat);
|
|
120
312
|
}
|
|
121
|
-
}
|
|
122
313
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (!playerSpeed || playerSpeed <= 0) {
|
|
129
|
-
console.warn('Invalid player speed:', playerSpeed, 'using default speed 3');
|
|
130
|
-
return this[direction](repeat);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const repeatTile = Math.floor((map[propMap] || 32) / playerSpeed) * repeat;
|
|
134
|
-
|
|
135
|
-
// Additional safety check for the calculated repeat value
|
|
136
|
-
if (!Number.isFinite(repeatTile) || repeatTile < 0 || repeatTile > 10000) {
|
|
137
|
-
console.warn('Calculated repeatTile is invalid:', repeatTile, 'using original repeat:', repeat);
|
|
138
|
-
return this[direction](repeat);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// Final safety check before calling the method
|
|
142
|
-
if (!Number.isSafeInteger(repeatTile)) {
|
|
143
|
-
console.warn('repeatTile is not a safe integer:', repeatTile, 'using original repeat:', repeat);
|
|
144
|
-
return this[direction](repeat);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
try {
|
|
148
|
-
return this[direction](repeatTile);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
console.error('Error calling direction method with repeatTile:', repeatTile, error);
|
|
151
|
-
return this[direction](repeat); // Fallback to original repeat
|
|
152
|
-
}
|
|
314
|
+
try {
|
|
315
|
+
return this[direction](repeatTile);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
console.error('Error calling direction method with repeatTile:', repeatTile, error);
|
|
318
|
+
return this[direction](repeat); // Fallback to original repeat
|
|
153
319
|
}
|
|
320
|
+
}
|
|
154
321
|
}
|
|
155
322
|
|
|
156
323
|
right(repeat: number = 1): Direction[] {
|
|
157
|
-
|
|
324
|
+
return this.repeatMove(Direction.Right, repeat)
|
|
158
325
|
}
|
|
159
326
|
|
|
160
327
|
left(repeat: number = 1): Direction[] {
|
|
161
|
-
|
|
328
|
+
return this.repeatMove(Direction.Left, repeat)
|
|
162
329
|
}
|
|
163
330
|
|
|
164
331
|
up(repeat: number = 1): Direction[] {
|
|
165
|
-
|
|
332
|
+
return this.repeatMove(Direction.Up, repeat)
|
|
166
333
|
}
|
|
167
334
|
|
|
168
335
|
down(repeat: number = 1): Direction[] {
|
|
169
|
-
|
|
336
|
+
return this.repeatMove(Direction.Down, repeat)
|
|
170
337
|
}
|
|
171
338
|
|
|
172
339
|
wait(sec: number): Promise<unknown> {
|
|
173
|
-
|
|
340
|
+
return wait(sec)
|
|
174
341
|
}
|
|
175
342
|
|
|
176
343
|
random(repeat: number = 1): Direction[] {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
repeat = Math.floor(repeat);
|
|
185
|
-
|
|
186
|
-
// Additional safety check - ensure repeat is a safe integer
|
|
187
|
-
if (repeat < 0 || repeat > Number.MAX_SAFE_INTEGER || !Number.isSafeInteger(repeat)) {
|
|
188
|
-
console.warn('Unsafe repeat value in random:', repeat, 'using default value 1');
|
|
189
|
-
repeat = 1;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
try {
|
|
193
|
-
return new Array(repeat).fill(null).map(() => [
|
|
194
|
-
Direction.Right,
|
|
195
|
-
Direction.Left,
|
|
196
|
-
Direction.Up,
|
|
197
|
-
Direction.Down
|
|
198
|
-
][random(0, 3)]);
|
|
199
|
-
} catch (error) {
|
|
200
|
-
console.error('Error creating random array with repeat:', repeat, error);
|
|
201
|
-
return [Direction.Down]; // Return single direction as fallback
|
|
202
|
-
}
|
|
203
|
-
}
|
|
344
|
+
return new Array(repeat).fill(null).map(() => [
|
|
345
|
+
Direction.Right,
|
|
346
|
+
Direction.Left,
|
|
347
|
+
Direction.Up,
|
|
348
|
+
Direction.Down
|
|
349
|
+
][random(0, 3)])
|
|
350
|
+
}
|
|
204
351
|
|
|
205
352
|
tileRight(repeat: number = 1): CallbackTileMove {
|
|
206
|
-
|
|
353
|
+
return this.repeatTileMove('right', repeat, 'tileWidth')
|
|
207
354
|
}
|
|
208
355
|
|
|
209
356
|
tileLeft(repeat: number = 1): CallbackTileMove {
|
|
210
|
-
|
|
357
|
+
return this.repeatTileMove('left', repeat, 'tileWidth')
|
|
211
358
|
}
|
|
212
359
|
|
|
213
360
|
tileUp(repeat: number = 1): CallbackTileMove {
|
|
214
|
-
|
|
361
|
+
return this.repeatTileMove('up', repeat, 'tileHeight')
|
|
215
362
|
}
|
|
216
363
|
|
|
217
364
|
tileDown(repeat: number = 1): CallbackTileMove {
|
|
218
|
-
|
|
365
|
+
return this.repeatTileMove('down', repeat, 'tileHeight')
|
|
219
366
|
}
|
|
220
367
|
|
|
221
368
|
tileRandom(repeat: number = 1): CallbackTileMove {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
][random(0, 3)]
|
|
240
|
-
|
|
241
|
-
try {
|
|
242
|
-
const newDirections = randFn(player, map);
|
|
243
|
-
if (Array.isArray(newDirections)) {
|
|
244
|
-
directions = [...directions, ...newDirections];
|
|
245
|
-
}
|
|
246
|
-
} catch (error) {
|
|
247
|
-
console.warn('Error in tileRandom iteration:', error);
|
|
248
|
-
// Continue with next iteration instead of breaking
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Safety check to prevent excessive array growth
|
|
252
|
-
if (directions.length > 10000) {
|
|
253
|
-
console.warn('tileRandom generated too many directions, truncating');
|
|
254
|
-
break;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
return directions
|
|
258
|
-
}
|
|
259
|
-
}
|
|
369
|
+
return (player: RpgPlayer, map): Direction[] => {
|
|
370
|
+
let directions: Direction[] = []
|
|
371
|
+
for (let i = 0; i < repeat; i++) {
|
|
372
|
+
const randFn: CallbackTileMove = [
|
|
373
|
+
this.tileRight(),
|
|
374
|
+
this.tileLeft(),
|
|
375
|
+
this.tileUp(),
|
|
376
|
+
this.tileDown()
|
|
377
|
+
][random(0, 3)]
|
|
378
|
+
directions = [
|
|
379
|
+
...directions,
|
|
380
|
+
...randFn(player, map)
|
|
381
|
+
]
|
|
382
|
+
}
|
|
383
|
+
return directions
|
|
384
|
+
}
|
|
385
|
+
}
|
|
260
386
|
|
|
261
387
|
private _awayFromPlayerDirection(player: RpgPlayer, otherPlayer: RpgPlayer): Direction {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
388
|
+
const directionOtherPlayer = otherPlayer.getDirection()
|
|
389
|
+
let newDirection: Direction = Direction.Down
|
|
390
|
+
|
|
391
|
+
switch (directionOtherPlayer) {
|
|
392
|
+
case Direction.Left:
|
|
393
|
+
case Direction.Right:
|
|
394
|
+
if (otherPlayer.x() > player.x()) {
|
|
395
|
+
newDirection = Direction.Left
|
|
396
|
+
}
|
|
397
|
+
else {
|
|
398
|
+
newDirection = Direction.Right
|
|
399
|
+
}
|
|
400
|
+
break
|
|
401
|
+
case Direction.Up:
|
|
402
|
+
case Direction.Down:
|
|
403
|
+
if (otherPlayer.y() > player.y()) {
|
|
404
|
+
newDirection = Direction.Up
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
newDirection = Direction.Down
|
|
408
|
+
}
|
|
409
|
+
break
|
|
410
|
+
}
|
|
411
|
+
return newDirection
|
|
286
412
|
}
|
|
287
413
|
|
|
288
414
|
private _towardPlayerDirection(player: RpgPlayer, otherPlayer: RpgPlayer): Direction {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
415
|
+
const directionOtherPlayer = otherPlayer.getDirection()
|
|
416
|
+
let newDirection: Direction = Direction.Down
|
|
417
|
+
|
|
418
|
+
switch (directionOtherPlayer) {
|
|
419
|
+
case Direction.Left:
|
|
420
|
+
case Direction.Right:
|
|
421
|
+
if (otherPlayer.x() > player.x()) {
|
|
422
|
+
newDirection = Direction.Right
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
newDirection = Direction.Left
|
|
426
|
+
}
|
|
427
|
+
break
|
|
428
|
+
case Direction.Up:
|
|
429
|
+
case Direction.Down:
|
|
430
|
+
if (otherPlayer.y() > player.y()) {
|
|
431
|
+
newDirection = Direction.Down
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
newDirection = Direction.Up
|
|
435
|
+
}
|
|
436
|
+
break
|
|
437
|
+
}
|
|
438
|
+
return newDirection
|
|
313
439
|
}
|
|
314
440
|
|
|
315
441
|
private _awayFromPlayer({ isTile, typeMov }: { isTile: boolean, typeMov: string }, otherPlayer: RpgPlayer, repeat: number = 1) {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
442
|
+
const method = (dir: Direction) => {
|
|
443
|
+
const direction: string = DirectionNames[dir as any] || 'down'
|
|
444
|
+
return this[isTile ? 'tile' + capitalize(direction) : direction](repeat)
|
|
445
|
+
}
|
|
446
|
+
return (player: RpgPlayer, map) => {
|
|
447
|
+
let newDirection: Direction = Direction.Down
|
|
448
|
+
switch (typeMov) {
|
|
449
|
+
case 'away':
|
|
450
|
+
newDirection = this._awayFromPlayerDirection(player, otherPlayer)
|
|
451
|
+
break;
|
|
452
|
+
case 'toward':
|
|
453
|
+
newDirection = this._towardPlayerDirection(player, otherPlayer)
|
|
454
|
+
break
|
|
319
455
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
case 'away':
|
|
324
|
-
newDirection = this._awayFromPlayerDirection(player, otherPlayer)
|
|
325
|
-
break;
|
|
326
|
-
case 'toward':
|
|
327
|
-
newDirection = this._towardPlayerDirection(player, otherPlayer)
|
|
328
|
-
break
|
|
329
|
-
}
|
|
330
|
-
let direction: any = method(newDirection)
|
|
331
|
-
if (isFunction(direction)) {
|
|
332
|
-
direction = direction(player, map)
|
|
333
|
-
}
|
|
334
|
-
return direction
|
|
456
|
+
let direction: any = method(newDirection)
|
|
457
|
+
if (isFunction(direction)) {
|
|
458
|
+
direction = direction(player, map)
|
|
335
459
|
}
|
|
460
|
+
return direction
|
|
461
|
+
}
|
|
336
462
|
}
|
|
337
463
|
|
|
338
464
|
towardPlayer(player: RpgPlayer, repeat: number = 1) {
|
|
339
|
-
|
|
465
|
+
return this._awayFromPlayer({ isTile: false, typeMov: 'toward' }, player, repeat)
|
|
340
466
|
}
|
|
341
467
|
|
|
342
468
|
tileTowardPlayer(player: RpgPlayer, repeat: number = 1) {
|
|
343
|
-
|
|
469
|
+
return this._awayFromPlayer({ isTile: true, typeMov: 'toward' }, player, repeat)
|
|
344
470
|
}
|
|
345
471
|
|
|
346
472
|
awayFromPlayer(player: RpgPlayer, repeat: number = 1): CallbackTileMove {
|
|
347
|
-
|
|
473
|
+
return this._awayFromPlayer({ isTile: false, typeMov: 'away' }, player, repeat)
|
|
348
474
|
}
|
|
349
475
|
|
|
350
476
|
tileAwayFromPlayer(player: RpgPlayer, repeat: number = 1): CallbackTileMove {
|
|
351
|
-
|
|
477
|
+
return this._awayFromPlayer({ isTile: true, typeMov: 'away' }, player, repeat)
|
|
352
478
|
}
|
|
353
479
|
|
|
354
480
|
turnLeft(): string {
|
|
355
|
-
|
|
481
|
+
return 'turn-' + Direction.Left
|
|
356
482
|
}
|
|
357
483
|
|
|
358
484
|
turnRight(): string {
|
|
359
|
-
|
|
485
|
+
return 'turn-' + Direction.Right
|
|
360
486
|
}
|
|
361
487
|
|
|
362
488
|
turnUp(): string {
|
|
363
|
-
|
|
489
|
+
return 'turn-' + Direction.Up
|
|
364
490
|
}
|
|
365
491
|
|
|
366
492
|
turnDown(): string {
|
|
367
|
-
|
|
493
|
+
return 'turn-' + Direction.Down
|
|
368
494
|
}
|
|
369
495
|
|
|
370
496
|
turnRandom(): string {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
497
|
+
// Use Perlin noise for smooth random turn direction with guaranteed variation
|
|
498
|
+
const directionIndex = this.getRandomDirectionIndex();
|
|
499
|
+
return [
|
|
500
|
+
this.turnRight(),
|
|
501
|
+
this.turnLeft(),
|
|
502
|
+
this.turnUp(),
|
|
503
|
+
this.turnDown()
|
|
504
|
+
][directionIndex]
|
|
377
505
|
}
|
|
378
506
|
|
|
379
507
|
turnAwayFromPlayer(otherPlayer: RpgPlayer): CallbackTurnMove {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
508
|
+
return (player: RpgPlayer) => {
|
|
509
|
+
const direction = this._awayFromPlayerDirection(player, otherPlayer)
|
|
510
|
+
return 'turn-' + direction
|
|
511
|
+
}
|
|
384
512
|
}
|
|
385
513
|
|
|
386
514
|
turnTowardPlayer(otherPlayer: RpgPlayer): CallbackTurnMove {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
515
|
+
return (player: RpgPlayer) => {
|
|
516
|
+
const direction = this._towardPlayerDirection(player, otherPlayer)
|
|
517
|
+
return 'turn-' + direction
|
|
518
|
+
}
|
|
391
519
|
}
|
|
392
520
|
}
|
|
393
521
|
|
|
@@ -412,7 +540,7 @@ export const Move = new MoveList();
|
|
|
412
540
|
* - **Strategy Management**: Add, remove, and query movement strategies
|
|
413
541
|
* - **Predefined Movements**: Quick access to common movement patterns
|
|
414
542
|
* - **Composite Movements**: Combine multiple strategies
|
|
415
|
-
* - **Physics Integration**: Seamless integration with
|
|
543
|
+
* - **Physics Integration**: Seamless integration with the deterministic @rpgjs/physic engine
|
|
416
544
|
*
|
|
417
545
|
* ## Available Movement Strategies
|
|
418
546
|
* - `LinearMove`: Constant velocity movement
|
|
@@ -481,25 +609,25 @@ export const Move = new MoveList();
|
|
|
481
609
|
* ```
|
|
482
610
|
*/
|
|
483
611
|
export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
484
|
-
|
|
485
|
-
|
|
612
|
+
const baseProto = Base.prototype as any;
|
|
613
|
+
class WithMoveManagerClass extends Base {
|
|
614
|
+
setAnimation(animationName: string, nbTimes: number): void {
|
|
615
|
+
if (typeof baseProto.setAnimation === 'function') {
|
|
616
|
+
baseProto.setAnimation.call(this, animationName, nbTimes);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
showComponentAnimation(id: string, params: any): void {
|
|
621
|
+
if (typeof baseProto.showComponentAnimation === 'function') {
|
|
622
|
+
baseProto.showComponentAnimation.call(this, id, params);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
486
626
|
// Properties for infinite route management
|
|
487
627
|
_infiniteRoutes: Routes | null = null;
|
|
488
628
|
_finishRoute: ((value: boolean) => void) | null = null;
|
|
489
629
|
_isInfiniteRouteActive: boolean = false;
|
|
490
630
|
|
|
491
|
-
/**
|
|
492
|
-
* The player passes through the other players (or vice versa). But the player does not go through the events.
|
|
493
|
-
*
|
|
494
|
-
* ```ts
|
|
495
|
-
* player.throughOtherPlayer = true
|
|
496
|
-
* ```
|
|
497
|
-
*
|
|
498
|
-
* @title Go through to other player
|
|
499
|
-
* @prop {boolean} player.throughOtherPlayer
|
|
500
|
-
* @default true
|
|
501
|
-
* @memberof MoveManager
|
|
502
|
-
* */
|
|
503
631
|
set throughOtherPlayer(value: boolean) {
|
|
504
632
|
this._throughOtherPlayer.set(value);
|
|
505
633
|
}
|
|
@@ -508,55 +636,22 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
|
508
636
|
return this._throughOtherPlayer();
|
|
509
637
|
}
|
|
510
638
|
|
|
511
|
-
/**
|
|
512
|
-
* The player goes through the event or the other players (or vice versa)
|
|
513
|
-
*
|
|
514
|
-
* ```ts
|
|
515
|
-
* player.through = true
|
|
516
|
-
* ```
|
|
517
|
-
*
|
|
518
|
-
* @title Go through the player
|
|
519
|
-
* @prop {boolean} player.through
|
|
520
|
-
* @default false
|
|
521
|
-
* @memberof MoveManager
|
|
522
|
-
* */
|
|
523
639
|
set through(value: boolean) {
|
|
524
640
|
this._through.set(value);
|
|
525
641
|
}
|
|
526
|
-
|
|
642
|
+
|
|
527
643
|
get through(): boolean {
|
|
528
644
|
return this._through();
|
|
529
645
|
}
|
|
530
646
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
* You can use Frequency enum
|
|
540
|
-
*
|
|
541
|
-
* ```ts
|
|
542
|
-
* import { Frequency } from '@rpgjs/server'
|
|
543
|
-
* player.frequency = Frequency.Low
|
|
544
|
-
* ```
|
|
545
|
-
*
|
|
546
|
-
* @title Change Frequency
|
|
547
|
-
* @prop {number} player.speed
|
|
548
|
-
* @enum {number}
|
|
549
|
-
*
|
|
550
|
-
* Frequency.Lowest | 600
|
|
551
|
-
* Frequency.Lower | 400
|
|
552
|
-
* Frequency.Low | 200
|
|
553
|
-
* Frequency.High | 100
|
|
554
|
-
* Frequency.Higher | 50
|
|
555
|
-
* Frequency.Highest | 25
|
|
556
|
-
* Frequency.None | 0
|
|
557
|
-
* @default 0
|
|
558
|
-
* @memberof MoveManager
|
|
559
|
-
* */
|
|
647
|
+
set throughEvent(value: boolean) {
|
|
648
|
+
this._throughEvent.set(value);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
get throughEvent(): boolean {
|
|
652
|
+
return this._throughEvent();
|
|
653
|
+
}
|
|
654
|
+
|
|
560
655
|
set frequency(value: number) {
|
|
561
656
|
this._frequency.set(value);
|
|
562
657
|
}
|
|
@@ -564,296 +659,419 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
|
564
659
|
get frequency(): number {
|
|
565
660
|
return this._frequency();
|
|
566
661
|
}
|
|
567
|
-
|
|
662
|
+
|
|
663
|
+
set directionFixed(value: boolean) {
|
|
664
|
+
(this as any)._directionFixed.set(value);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
get directionFixed(): boolean {
|
|
668
|
+
return (this as any)._directionFixed();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
set animationFixed(value: boolean) {
|
|
672
|
+
(this as any)._animationFixed.set(value);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
get animationFixed(): boolean {
|
|
676
|
+
return (this as any)._animationFixed();
|
|
677
|
+
}
|
|
678
|
+
|
|
568
679
|
/**
|
|
569
|
-
* Add a
|
|
680
|
+
* Add a movement strategy to this entity
|
|
570
681
|
*
|
|
571
|
-
*
|
|
572
|
-
*
|
|
682
|
+
* Returns a Promise that resolves when the movement completes (when `isFinished()` returns true).
|
|
683
|
+
* If the strategy doesn't implement `isFinished()`, the Promise resolves immediately.
|
|
573
684
|
*
|
|
574
685
|
* @param strategy - The movement strategy to add
|
|
686
|
+
* @param options - Optional callbacks for start and completion events
|
|
687
|
+
* @returns Promise that resolves when the movement completes
|
|
575
688
|
*
|
|
576
689
|
* @example
|
|
577
690
|
* ```ts
|
|
578
|
-
* //
|
|
579
|
-
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
691
|
+
* // Fire and forget
|
|
692
|
+
* player.addMovement(new LinearMove({ x: 1, y: 0 }, 200));
|
|
693
|
+
*
|
|
694
|
+
* // Wait for completion
|
|
695
|
+
* await player.addMovement(new Dash(10, { x: 1, y: 0 }, 200));
|
|
696
|
+
* console.log('Dash completed!');
|
|
697
|
+
*
|
|
698
|
+
* // With callbacks
|
|
699
|
+
* await player.addMovement(new Knockback({ x: -1, y: 0 }, 5, 300), {
|
|
700
|
+
* onStart: () => console.log('Knockback started'),
|
|
701
|
+
* onComplete: () => console.log('Knockback completed')
|
|
702
|
+
* });
|
|
585
703
|
* ```
|
|
586
704
|
*/
|
|
587
|
-
addMovement(strategy: MovementStrategy): void {
|
|
588
|
-
const map = (this as unknown as PlayerWithMixins).getCurrentMap();
|
|
589
|
-
if (!map) return;
|
|
590
|
-
|
|
591
|
-
|
|
705
|
+
addMovement(strategy: MovementStrategy, options?: MovementOptions): Promise<void> {
|
|
706
|
+
const map = (this as unknown as PlayerWithMixins).getCurrentMap() as any;
|
|
707
|
+
if (!map) return Promise.resolve();
|
|
708
|
+
|
|
709
|
+
const playerId = (this as unknown as PlayerWithMixins).id;
|
|
710
|
+
return map.moveManager.add(playerId, strategy, options);
|
|
592
711
|
}
|
|
593
712
|
|
|
594
|
-
/**
|
|
595
|
-
* Remove a specific movement strategy from this entity
|
|
596
|
-
*
|
|
597
|
-
* @param strategy - The strategy instance to remove
|
|
598
|
-
* @returns True if the strategy was found and removed
|
|
599
|
-
*
|
|
600
|
-
* @example
|
|
601
|
-
* ```ts
|
|
602
|
-
* const dashMove = new Dash(8, { x: 1, y: 0 }, 200);
|
|
603
|
-
* player.addMovement(dashMove);
|
|
604
|
-
*
|
|
605
|
-
* // Later, remove the specific movement
|
|
606
|
-
* const removed = player.removeMovement(dashMove);
|
|
607
|
-
* console.log('Movement removed:', removed);
|
|
608
|
-
* ```
|
|
609
|
-
*/
|
|
610
713
|
removeMovement(strategy: MovementStrategy): boolean {
|
|
611
|
-
const map = (this as unknown as PlayerWithMixins).getCurrentMap();
|
|
714
|
+
const map = (this as unknown as PlayerWithMixins).getCurrentMap() as any;
|
|
612
715
|
if (!map) return false;
|
|
613
|
-
|
|
614
|
-
|
|
716
|
+
|
|
717
|
+
const playerId = (this as unknown as PlayerWithMixins).id;
|
|
718
|
+
return map.moveManager.remove(playerId, strategy);
|
|
615
719
|
}
|
|
616
720
|
|
|
617
|
-
/**
|
|
618
|
-
* Remove all active movement strategies from this entity
|
|
619
|
-
*
|
|
620
|
-
* Stops all current movements immediately.
|
|
621
|
-
*
|
|
622
|
-
* @example
|
|
623
|
-
* ```ts
|
|
624
|
-
* // Stop all movements when player dies
|
|
625
|
-
* player.clearMovements();
|
|
626
|
-
*
|
|
627
|
-
* // Clear movements before applying new ones
|
|
628
|
-
* player.clearMovements();
|
|
629
|
-
* player.dash({ x: 1, y: 0 });
|
|
630
|
-
* ```
|
|
631
|
-
*/
|
|
632
721
|
clearMovements(): void {
|
|
633
|
-
const map = (this as unknown as PlayerWithMixins).getCurrentMap();
|
|
722
|
+
const map = (this as unknown as PlayerWithMixins).getCurrentMap() as any;
|
|
634
723
|
if (!map) return;
|
|
635
|
-
|
|
636
|
-
|
|
724
|
+
|
|
725
|
+
const playerId = (this as unknown as PlayerWithMixins).id;
|
|
726
|
+
map.moveManager.clear(playerId);
|
|
637
727
|
}
|
|
638
728
|
|
|
639
|
-
/**
|
|
640
|
-
* Check if this entity has any active movement strategies
|
|
641
|
-
*
|
|
642
|
-
* @returns True if entity has active movements
|
|
643
|
-
*
|
|
644
|
-
* @example
|
|
645
|
-
* ```ts
|
|
646
|
-
* // Don't accept input while movements are active
|
|
647
|
-
* if (!player.hasActiveMovements()) {
|
|
648
|
-
* player.dash(inputDirection);
|
|
649
|
-
* }
|
|
650
|
-
*
|
|
651
|
-
* // Check before adding new movement
|
|
652
|
-
* if (player.hasActiveMovements()) {
|
|
653
|
-
* player.clearMovements();
|
|
654
|
-
* }
|
|
655
|
-
* ```
|
|
656
|
-
*/
|
|
657
729
|
hasActiveMovements(): boolean {
|
|
658
|
-
const map = (this as unknown as PlayerWithMixins).getCurrentMap();
|
|
730
|
+
const map = (this as unknown as PlayerWithMixins).getCurrentMap() as any;
|
|
659
731
|
if (!map) return false;
|
|
660
|
-
|
|
732
|
+
|
|
661
733
|
return map.moveManager.hasActiveStrategies((this as unknown as PlayerWithMixins).id);
|
|
662
734
|
}
|
|
663
735
|
|
|
664
|
-
/**
|
|
665
|
-
* Get all active movement strategies for this entity
|
|
666
|
-
*
|
|
667
|
-
* @returns Array of active movement strategies
|
|
668
|
-
*
|
|
669
|
-
* @example
|
|
670
|
-
* ```ts
|
|
671
|
-
* // Check what movements are currently active
|
|
672
|
-
* const movements = player.getActiveMovements();
|
|
673
|
-
* console.log(`Player has ${movements.length} active movements`);
|
|
674
|
-
*
|
|
675
|
-
* // Find specific movement type
|
|
676
|
-
* const hasDash = movements.some(m => m instanceof Dash);
|
|
677
|
-
* ```
|
|
678
|
-
*/
|
|
679
736
|
getActiveMovements(): MovementStrategy[] {
|
|
680
|
-
const map = (this as unknown as PlayerWithMixins).getCurrentMap();
|
|
737
|
+
const map = (this as unknown as PlayerWithMixins).getCurrentMap() as any;
|
|
681
738
|
if (!map) return [];
|
|
682
|
-
|
|
739
|
+
|
|
683
740
|
return map.moveManager.getStrategies((this as unknown as PlayerWithMixins).id);
|
|
684
741
|
}
|
|
685
742
|
|
|
686
743
|
/**
|
|
687
744
|
* Move toward a target player or position using AI pathfinding
|
|
688
745
|
*
|
|
689
|
-
* Uses SeekAvoid strategy
|
|
690
|
-
* The
|
|
746
|
+
* Uses the `SeekAvoid` strategy to navigate toward the target while avoiding obstacles.
|
|
747
|
+
* The movement speed is based on the player's current `speed` and `frequency` settings,
|
|
748
|
+
* scaled appropriately.
|
|
691
749
|
*
|
|
692
|
-
* @param target - Target player or position to move toward
|
|
750
|
+
* @param target - Target player or position `{ x, y }` to move toward
|
|
693
751
|
*
|
|
694
752
|
* @example
|
|
695
753
|
* ```ts
|
|
696
754
|
* // Move toward another player
|
|
697
|
-
*
|
|
698
|
-
* player.moveTo(targetPlayer);
|
|
755
|
+
* player.moveTo(otherPlayer);
|
|
699
756
|
*
|
|
700
757
|
* // Move toward a specific position
|
|
701
|
-
* player.moveTo({ x:
|
|
702
|
-
*
|
|
703
|
-
* // Stop the movement later
|
|
704
|
-
* player.stopMoveTo();
|
|
758
|
+
* player.moveTo({ x: 200, y: 150 });
|
|
705
759
|
* ```
|
|
706
760
|
*/
|
|
707
761
|
moveTo(target: RpgCommonPlayer | { x: number, y: number }): void {
|
|
708
|
-
const map = (this as unknown as PlayerWithMixins).getCurrentMap();
|
|
762
|
+
const map = (this as unknown as PlayerWithMixins).getCurrentMap() as any;
|
|
709
763
|
if (!map) return;
|
|
764
|
+
|
|
765
|
+
const playerId = (this as unknown as PlayerWithMixins).id;
|
|
766
|
+
const engine = map.physic;
|
|
767
|
+
|
|
768
|
+
// Calculate maxSpeed based on player's speed and frequency
|
|
769
|
+
// Original values: 180 for player target, 80 for position target (with default speed=4)
|
|
770
|
+
// Factor: 45 for player (180/4), 20 for position (80/4)
|
|
771
|
+
const playerSpeed = (this as any).speed();
|
|
772
|
+
const rawFrequency = (this as any).frequency;
|
|
773
|
+
const playerFrequency = typeof rawFrequency === 'function' ? rawFrequency() : rawFrequency;
|
|
774
|
+
const frequencyScale = playerFrequency > 0 ? Frequency.High / playerFrequency : 1;
|
|
775
|
+
const normalizedFrequencyScale = Number.isFinite(frequencyScale) && frequencyScale > 0 ? frequencyScale : 1;
|
|
776
|
+
|
|
777
|
+
// Remove ALL movement strategies that could interfere with SeekAvoid
|
|
778
|
+
// This includes SeekAvoid, Dash, Knockback, and LinearRepulsion
|
|
779
|
+
const existingStrategies = this.getActiveMovements();
|
|
780
|
+
const conflictingStrategies = existingStrategies.filter(s =>
|
|
781
|
+
s instanceof SeekAvoid ||
|
|
782
|
+
s instanceof Dash ||
|
|
783
|
+
s instanceof Knockback ||
|
|
784
|
+
s instanceof LinearRepulsion
|
|
785
|
+
);
|
|
710
786
|
|
|
711
|
-
|
|
712
|
-
|
|
787
|
+
if (conflictingStrategies.length > 0) {
|
|
788
|
+
conflictingStrategies.forEach(s => this.removeMovement(s));
|
|
789
|
+
}
|
|
790
|
+
|
|
713
791
|
if ('id' in target) {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
792
|
+
const targetProvider = () => {
|
|
793
|
+
const body = (map as any).getBody(target.id) ?? null;
|
|
794
|
+
return body;
|
|
795
|
+
};
|
|
796
|
+
// Factor 45: with speed=4 gives 180 (original value)
|
|
797
|
+
const maxSpeed = playerSpeed * 45 * normalizedFrequencyScale;
|
|
719
798
|
map.moveManager.add(
|
|
720
|
-
|
|
721
|
-
new SeekAvoid(
|
|
799
|
+
playerId,
|
|
800
|
+
new SeekAvoid(engine, targetProvider, maxSpeed, 140, 80, 48)
|
|
722
801
|
);
|
|
723
802
|
return;
|
|
724
803
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
804
|
+
|
|
805
|
+
const staticTarget = new Entity({
|
|
806
|
+
position: { x: target.x, y: target.y },
|
|
807
|
+
mass: Infinity,
|
|
808
|
+
});
|
|
809
|
+
staticTarget.freeze();
|
|
810
|
+
|
|
811
|
+
// Factor 20: with speed=4 gives 80 (original value)
|
|
812
|
+
const maxSpeed = playerSpeed * 20 * normalizedFrequencyScale;
|
|
813
|
+
map.moveManager.add(
|
|
814
|
+
playerId,
|
|
815
|
+
new SeekAvoid(engine, () => staticTarget, maxSpeed, 140, 80, 48)
|
|
816
|
+
);
|
|
732
817
|
}
|
|
733
818
|
|
|
734
|
-
/**
|
|
735
|
-
* Stop the current moveTo behavior
|
|
736
|
-
*
|
|
737
|
-
* Removes any active SeekAvoid strategies.
|
|
738
|
-
*
|
|
739
|
-
* @example
|
|
740
|
-
* ```ts
|
|
741
|
-
* // Start following a target
|
|
742
|
-
* player.moveTo(targetPlayer);
|
|
743
|
-
*
|
|
744
|
-
* // Stop following when target is reached
|
|
745
|
-
* if (distanceToTarget < 10) {
|
|
746
|
-
* player.stopMoveTo();
|
|
747
|
-
* }
|
|
748
|
-
* ```
|
|
749
|
-
*/
|
|
750
819
|
stopMoveTo(): void {
|
|
751
|
-
const map = (this as unknown as PlayerWithMixins).getCurrentMap();
|
|
820
|
+
const map = (this as unknown as PlayerWithMixins).getCurrentMap() as any;
|
|
752
821
|
if (!map) return;
|
|
822
|
+
|
|
823
|
+
let strategies: MovementStrategy[] = [];
|
|
824
|
+
try {
|
|
825
|
+
strategies = this.getActiveMovements();
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
// Teardown race: entity can be removed while AI still clears movements.
|
|
829
|
+
const message = (error as Error | undefined)?.message ?? "";
|
|
830
|
+
if (message.includes("unable to resolve entity")) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
throw error;
|
|
834
|
+
}
|
|
835
|
+
const toRemove = strategies.filter(s => s instanceof SeekAvoid || s instanceof LinearRepulsion);
|
|
753
836
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (strategy instanceof SeekAvoid || strategy instanceof LinearRepulsion) {
|
|
837
|
+
if (toRemove.length > 0) {
|
|
838
|
+
toRemove.forEach(strategy => {
|
|
757
839
|
this.removeMovement(strategy);
|
|
758
|
-
}
|
|
759
|
-
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
760
842
|
}
|
|
761
843
|
|
|
762
844
|
/**
|
|
763
845
|
* Perform a dash movement in the specified direction
|
|
764
846
|
*
|
|
765
|
-
*
|
|
847
|
+
* Creates a burst of velocity for a fixed duration. The total speed is calculated
|
|
848
|
+
* by adding the player's base speed (`this.speed()`) to the additional dash speed.
|
|
849
|
+
* This ensures faster players also dash faster proportionally.
|
|
766
850
|
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
851
|
+
* With default speed=4 and additionalSpeed=4: total = 8 (same as original default)
|
|
852
|
+
*
|
|
853
|
+
* @param direction - Normalized direction vector `{ x, y }` for the dash
|
|
854
|
+
* @param additionalSpeed - Extra speed added on top of base speed (default: 4)
|
|
769
855
|
* @param duration - Duration in milliseconds (default: 200)
|
|
856
|
+
* @param options - Optional callbacks for movement events
|
|
857
|
+
* @returns Promise that resolves when the dash completes
|
|
770
858
|
*
|
|
771
859
|
* @example
|
|
772
860
|
* ```ts
|
|
773
|
-
* // Dash right
|
|
774
|
-
* player.dash({ x: 1, y: 0 });
|
|
775
|
-
*
|
|
776
|
-
* //
|
|
777
|
-
* player.dash({ x: 0
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
861
|
+
* // Dash to the right and wait for completion
|
|
862
|
+
* await player.dash({ x: 1, y: 0 });
|
|
863
|
+
*
|
|
864
|
+
* // Powerful dash with callbacks
|
|
865
|
+
* await player.dash({ x: 0, y: -1 }, 12, 300, {
|
|
866
|
+
* onStart: () => console.log('Dash started!'),
|
|
867
|
+
* onComplete: () => console.log('Dash finished!')
|
|
868
|
+
* });
|
|
781
869
|
* ```
|
|
782
870
|
*/
|
|
783
|
-
dash(direction: { x: number, y: number },
|
|
784
|
-
|
|
871
|
+
dash(direction: { x: number, y: number }, additionalSpeed: number = 4, duration: number = 200, options?: MovementOptions): Promise<void> {
|
|
872
|
+
const playerSpeed = (this as any).speed();
|
|
873
|
+
// Total dash speed = base speed + additional speed
|
|
874
|
+
// With speed=4, additionalSpeed=4: gives 8 (original default value)
|
|
875
|
+
const totalSpeed = playerSpeed + additionalSpeed;
|
|
876
|
+
// Physic strategies expect seconds (dt is in seconds), while the server API exposes milliseconds
|
|
877
|
+
const durationSeconds = duration / 1000;
|
|
878
|
+
return this.addMovement(new Dash(totalSpeed, direction, durationSeconds), options);
|
|
785
879
|
}
|
|
786
880
|
|
|
787
881
|
/**
|
|
788
882
|
* Apply knockback effect in the specified direction
|
|
789
883
|
*
|
|
790
|
-
*
|
|
884
|
+
* Pushes the entity with an initial force that decays over time.
|
|
885
|
+
* Returns a Promise that resolves when the knockback completes **or is cancelled**.
|
|
791
886
|
*
|
|
792
|
-
*
|
|
887
|
+
* ## Design notes
|
|
888
|
+
* - The underlying physics `MovementManager` can cancel strategies via `remove()`, `clear()`,
|
|
889
|
+
* or `stopMovement()` **without resolving the Promise** returned by `add()`.
|
|
890
|
+
* - For this reason, this method considers the knockback finished when either:
|
|
891
|
+
* - the `add()` promise resolves (normal completion), or
|
|
892
|
+
* - the strategy is no longer present in the active movements list (cancellation).
|
|
893
|
+
* - When multiple knockbacks overlap, `directionFixed` and `animationFixed` are restored
|
|
894
|
+
* only after **all** knockbacks have finished (including cancellations).
|
|
895
|
+
*
|
|
896
|
+
* @param direction - Normalized direction vector `{ x, y }` for the knockback
|
|
793
897
|
* @param force - Initial knockback force (default: 5)
|
|
794
898
|
* @param duration - Duration in milliseconds (default: 300)
|
|
899
|
+
* @param options - Optional callbacks for movement events
|
|
900
|
+
* @returns Promise that resolves when the knockback completes or is cancelled
|
|
795
901
|
*
|
|
796
902
|
* @example
|
|
797
903
|
* ```ts
|
|
798
|
-
* //
|
|
799
|
-
*
|
|
800
|
-
*
|
|
801
|
-
*
|
|
802
|
-
*
|
|
803
|
-
* player.knockback(
|
|
904
|
+
* // Simple knockback (await is optional)
|
|
905
|
+
* await player.knockback({ x: 1, y: 0 }, 5, 300);
|
|
906
|
+
*
|
|
907
|
+
* // Overlapping knockbacks: flags are restored only after the last one ends
|
|
908
|
+
* player.knockback({ x: -1, y: 0 }, 5, 300);
|
|
909
|
+
* player.knockback({ x: 0, y: 1 }, 3, 200);
|
|
910
|
+
*
|
|
911
|
+
* // Cancellation (e.g. map change) will still restore fixed flags
|
|
912
|
+
* // even if the underlying movement strategy promise is never resolved.
|
|
804
913
|
* ```
|
|
805
914
|
*/
|
|
806
|
-
knockback(direction: { x: number, y: number }, force: number = 5, duration: number = 300): void {
|
|
807
|
-
|
|
915
|
+
async knockback(direction: { x: number, y: number }, force: number = 5, duration: number = 300, options?: MovementOptions): Promise<void> {
|
|
916
|
+
const durationSeconds = duration / 1000;
|
|
917
|
+
const selfAny = this as any;
|
|
918
|
+
const lockKey = '__rpg_knockback_lock__';
|
|
919
|
+
|
|
920
|
+
type KnockbackLockState = {
|
|
921
|
+
prevDirectionFixed: boolean;
|
|
922
|
+
prevAnimationFixed: boolean;
|
|
923
|
+
prevAnimationName?: string;
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
const getLock = (): KnockbackLockState | undefined => selfAny[lockKey];
|
|
927
|
+
const setLock = (lock: KnockbackLockState): void => {
|
|
928
|
+
selfAny[lockKey] = lock;
|
|
929
|
+
};
|
|
930
|
+
const clearLock = (): void => {
|
|
931
|
+
delete selfAny[lockKey];
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const hasActiveKnockback = (): boolean =>
|
|
935
|
+
this.getActiveMovements().some(s => s instanceof Knockback || s instanceof AdditiveKnockback);
|
|
936
|
+
|
|
937
|
+
const setAnimationName = (name: string): void => {
|
|
938
|
+
if (typeof selfAny.setGraphicAnimation === 'function') {
|
|
939
|
+
selfAny.setGraphicAnimation(name);
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const animSignal = selfAny.animationName;
|
|
943
|
+
if (animSignal && typeof animSignal === 'object' && typeof animSignal.set === 'function') {
|
|
944
|
+
animSignal.set(name);
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
948
|
+
const getAnimationName = (): string | undefined => {
|
|
949
|
+
const animSignal = selfAny.animationName;
|
|
950
|
+
if (typeof animSignal === 'function') {
|
|
951
|
+
try {
|
|
952
|
+
return animSignal();
|
|
953
|
+
} catch {
|
|
954
|
+
return undefined;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return undefined;
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
const restore = (): void => {
|
|
961
|
+
const lock = getLock();
|
|
962
|
+
if (!lock) return;
|
|
963
|
+
|
|
964
|
+
this.directionFixed = lock.prevDirectionFixed;
|
|
965
|
+
|
|
966
|
+
const prevAnimFixed = lock.prevAnimationFixed;
|
|
967
|
+
this.animationFixed = false; // temporarily unlock so we can restore animation
|
|
968
|
+
if (!prevAnimFixed && lock.prevAnimationName) {
|
|
969
|
+
setAnimationName(lock.prevAnimationName);
|
|
970
|
+
}
|
|
971
|
+
this.animationFixed = prevAnimFixed;
|
|
972
|
+
|
|
973
|
+
clearLock();
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const ensureLockInitialized = (): void => {
|
|
977
|
+
if (getLock()) return;
|
|
978
|
+
|
|
979
|
+
setLock({
|
|
980
|
+
prevDirectionFixed: this.directionFixed,
|
|
981
|
+
prevAnimationFixed: this.animationFixed,
|
|
982
|
+
prevAnimationName: getAnimationName(),
|
|
983
|
+
});
|
|
984
|
+
|
|
985
|
+
this.directionFixed = true;
|
|
986
|
+
setAnimationName('stand');
|
|
987
|
+
this.animationFixed = true;
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
const waitUntilRemovedOrTimeout = (strategy: MovementStrategy): Promise<void> => {
|
|
991
|
+
return new Promise<void>((resolve) => {
|
|
992
|
+
const start = Date.now();
|
|
993
|
+
const maxMs = Math.max(0, duration + 1000);
|
|
994
|
+
|
|
995
|
+
const intervalId = setInterval(() => {
|
|
996
|
+
const active = this.getActiveMovements();
|
|
997
|
+
if (!active.includes(strategy) || (Date.now() - start > maxMs)) {
|
|
998
|
+
clearInterval(intervalId);
|
|
999
|
+
resolve();
|
|
1000
|
+
}
|
|
1001
|
+
}, 16);
|
|
1002
|
+
});
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
// First knockback creates the lock and freezes direction/animation.
|
|
1006
|
+
// Next knockbacks reuse the lock and keep the fixed flags enabled.
|
|
1007
|
+
ensureLockInitialized();
|
|
1008
|
+
|
|
1009
|
+
// Use additive knockback to avoid jitter with player inputs
|
|
1010
|
+
const strategy = new AdditiveKnockback(direction, force, durationSeconds);
|
|
1011
|
+
const addPromise = this.addMovement(strategy, options);
|
|
1012
|
+
|
|
1013
|
+
try {
|
|
1014
|
+
await Promise.race([addPromise, waitUntilRemovedOrTimeout(strategy)]);
|
|
1015
|
+
} finally {
|
|
1016
|
+
// Restore only when ALL knockbacks are done (including cancellations).
|
|
1017
|
+
if (!hasActiveKnockback()) {
|
|
1018
|
+
restore();
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
808
1021
|
}
|
|
809
1022
|
|
|
810
1023
|
/**
|
|
811
1024
|
* Follow a sequence of waypoints
|
|
812
1025
|
*
|
|
813
|
-
*
|
|
1026
|
+
* Makes the entity move through a list of positions at a speed calculated
|
|
1027
|
+
* from the player's base speed. The `speedMultiplier` allows adjusting
|
|
1028
|
+
* the travel speed relative to the player's normal movement speed.
|
|
1029
|
+
*
|
|
1030
|
+
* With default speed=4 and multiplier=0.5: speed = 2 (same as original default)
|
|
814
1031
|
*
|
|
815
|
-
* @param waypoints - Array of x,y positions to follow
|
|
816
|
-
* @param
|
|
817
|
-
* @param loop - Whether to loop back to start (default: false)
|
|
1032
|
+
* @param waypoints - Array of `{ x, y }` positions to follow in order
|
|
1033
|
+
* @param speedMultiplier - Multiplier applied to base speed (default: 0.5)
|
|
1034
|
+
* @param loop - Whether to loop back to start after reaching the end (default: false)
|
|
818
1035
|
*
|
|
819
1036
|
* @example
|
|
820
1037
|
* ```ts
|
|
821
|
-
* //
|
|
822
|
-
* const
|
|
1038
|
+
* // Follow a patrol path at normal speed
|
|
1039
|
+
* const patrol = [
|
|
823
1040
|
* { x: 100, y: 100 },
|
|
824
|
-
* { x:
|
|
825
|
-
* { x:
|
|
826
|
-
* { x: 100, y: 300 }
|
|
1041
|
+
* { x: 200, y: 100 },
|
|
1042
|
+
* { x: 200, y: 200 }
|
|
827
1043
|
* ];
|
|
828
|
-
* player.followPath(
|
|
1044
|
+
* player.followPath(patrol, 1, true); // Loop at full speed
|
|
829
1045
|
*
|
|
830
|
-
* //
|
|
831
|
-
* player.followPath(
|
|
1046
|
+
* // Slow walk through waypoints
|
|
1047
|
+
* player.followPath(waypoints, 0.25, false);
|
|
832
1048
|
* ```
|
|
833
1049
|
*/
|
|
834
|
-
followPath(waypoints: Array<{ x: number, y: number }>,
|
|
1050
|
+
followPath(waypoints: Array<{ x: number, y: number }>, speedMultiplier: number = 0.5, loop: boolean = false): void {
|
|
1051
|
+
const playerSpeed = (this as any).speed();
|
|
1052
|
+
// Path follow speed = player base speed * multiplier
|
|
1053
|
+
// With speed=4, multiplier=0.5: gives 2 (original default value)
|
|
1054
|
+
const speed = playerSpeed * speedMultiplier;
|
|
835
1055
|
this.addMovement(new PathFollow(waypoints, speed, loop));
|
|
836
1056
|
}
|
|
837
1057
|
|
|
838
1058
|
/**
|
|
839
1059
|
* Apply oscillating movement pattern
|
|
840
1060
|
*
|
|
841
|
-
*
|
|
1061
|
+
* Creates a back-and-forth movement along the specified axis. The movement
|
|
1062
|
+
* oscillates sinusoidally between -amplitude and +amplitude from the starting position.
|
|
842
1063
|
*
|
|
843
|
-
* @param direction - Primary oscillation axis (normalized)
|
|
844
|
-
* @param amplitude - Maximum distance from center (default: 50)
|
|
845
|
-
* @param period - Time for complete cycle in
|
|
1064
|
+
* @param direction - Primary oscillation axis (normalized direction vector)
|
|
1065
|
+
* @param amplitude - Maximum distance from center in pixels (default: 50)
|
|
1066
|
+
* @param period - Time for a complete cycle in milliseconds (default: 2000)
|
|
846
1067
|
*
|
|
847
1068
|
* @example
|
|
848
1069
|
* ```ts
|
|
849
1070
|
* // Horizontal oscillation
|
|
850
1071
|
* player.oscillate({ x: 1, y: 0 }, 100, 3000);
|
|
851
1072
|
*
|
|
852
|
-
* //
|
|
853
|
-
* player.oscillate({ x:
|
|
854
|
-
*
|
|
855
|
-
* // Diagonal oscillation
|
|
856
|
-
* player.oscillate({ x: 0.7, y: 0.7 }, 75, 2500);
|
|
1073
|
+
* // Diagonal bobbing motion
|
|
1074
|
+
* player.oscillate({ x: 1, y: 1 }, 30, 1000);
|
|
857
1075
|
* ```
|
|
858
1076
|
*/
|
|
859
1077
|
oscillate(direction: { x: number, y: number }, amplitude: number = 50, period: number = 2000): void {
|
|
@@ -863,49 +1081,59 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
|
863
1081
|
/**
|
|
864
1082
|
* Apply ice movement physics
|
|
865
1083
|
*
|
|
866
|
-
*
|
|
867
|
-
*
|
|
1084
|
+
* Simulates slippery surface physics where the entity accelerates gradually
|
|
1085
|
+
* and has difficulty stopping. The maximum speed is based on the player's
|
|
1086
|
+
* base speed multiplied by a speed factor.
|
|
1087
|
+
*
|
|
1088
|
+
* With default speed=4 and factor=1: maxSpeed = 4 (same as original default)
|
|
868
1089
|
*
|
|
869
|
-
* @param direction - Target movement direction
|
|
870
|
-
* @param
|
|
1090
|
+
* @param direction - Target movement direction `{ x, y }`
|
|
1091
|
+
* @param speedFactor - Factor multiplied with base speed for max speed (default: 1.0)
|
|
871
1092
|
*
|
|
872
1093
|
* @example
|
|
873
1094
|
* ```ts
|
|
874
|
-
* //
|
|
875
|
-
*
|
|
876
|
-
* player.applyIceMovement(inputDirection, 5);
|
|
877
|
-
* }
|
|
1095
|
+
* // Normal ice physics
|
|
1096
|
+
* player.applyIceMovement({ x: 1, y: 0 });
|
|
878
1097
|
*
|
|
879
|
-
* //
|
|
880
|
-
*
|
|
1098
|
+
* // Fast ice sliding
|
|
1099
|
+
* player.applyIceMovement({ x: 0, y: 1 }, 1.5);
|
|
881
1100
|
* ```
|
|
882
1101
|
*/
|
|
883
|
-
applyIceMovement(direction: { x: number, y: number },
|
|
1102
|
+
applyIceMovement(direction: { x: number, y: number }, speedFactor: number = 1): void {
|
|
1103
|
+
const playerSpeed = (this as any).speed();
|
|
1104
|
+
// Max ice speed = player base speed * factor
|
|
1105
|
+
// With speed=4, factor=1: gives 4 (original default value)
|
|
1106
|
+
const maxSpeed = playerSpeed * speedFactor;
|
|
884
1107
|
this.addMovement(new IceMovement(direction, maxSpeed));
|
|
885
1108
|
}
|
|
886
1109
|
|
|
887
1110
|
/**
|
|
888
1111
|
* Shoot a projectile in the specified direction
|
|
889
1112
|
*
|
|
890
|
-
* Creates projectile
|
|
1113
|
+
* Creates a projectile with ballistic trajectory. The speed is calculated
|
|
1114
|
+
* from the player's base speed multiplied by a speed factor.
|
|
1115
|
+
*
|
|
1116
|
+
* With default speed=4 and factor=50: speed = 200 (same as original default)
|
|
891
1117
|
*
|
|
892
|
-
* @param type - Type of projectile trajectory
|
|
893
|
-
* @param direction - Normalized direction vector
|
|
894
|
-
* @param
|
|
1118
|
+
* @param type - Type of projectile trajectory (`Straight`, `Arc`, or `Bounce`)
|
|
1119
|
+
* @param direction - Normalized direction vector `{ x, y }`
|
|
1120
|
+
* @param speedFactor - Factor multiplied with base speed (default: 50)
|
|
895
1121
|
*
|
|
896
1122
|
* @example
|
|
897
1123
|
* ```ts
|
|
898
|
-
* //
|
|
899
|
-
* player.shootProjectile(ProjectileType.Straight, { x: 1, y: 0 }
|
|
1124
|
+
* // Straight projectile
|
|
1125
|
+
* player.shootProjectile(ProjectileType.Straight, { x: 1, y: 0 });
|
|
900
1126
|
*
|
|
901
|
-
* //
|
|
902
|
-
* player.shootProjectile(ProjectileType.Arc, { x:
|
|
903
|
-
*
|
|
904
|
-
* // Bouncing projectile
|
|
905
|
-
* player.shootProjectile(ProjectileType.Bounce, { x: 1, y: 0 }, 100);
|
|
1127
|
+
* // Fast arc projectile
|
|
1128
|
+
* player.shootProjectile(ProjectileType.Arc, { x: 1, y: -0.5 }, 75);
|
|
906
1129
|
* ```
|
|
907
1130
|
*/
|
|
908
|
-
shootProjectile(type: ProjectileType, direction: { x: number, y: number },
|
|
1131
|
+
shootProjectile(type: ProjectileType, direction: { x: number, y: number }, speedFactor: number = 50): void {
|
|
1132
|
+
const playerSpeed = (this as any).speed();
|
|
1133
|
+
// Projectile speed = player base speed * factor
|
|
1134
|
+
// With speed=4, factor=50: gives 200 (original default value)
|
|
1135
|
+
const speed = playerSpeed * speedFactor;
|
|
1136
|
+
|
|
909
1137
|
const config = {
|
|
910
1138
|
speed,
|
|
911
1139
|
direction,
|
|
@@ -915,250 +1143,635 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
|
915
1143
|
maxBounces: type === ProjectileType.Bounce ? 3 : undefined,
|
|
916
1144
|
bounciness: type === ProjectileType.Bounce ? 0.6 : undefined
|
|
917
1145
|
};
|
|
918
|
-
|
|
1146
|
+
|
|
919
1147
|
this.addMovement(new ProjectileMovement(type, config));
|
|
920
1148
|
}
|
|
921
1149
|
|
|
922
|
-
|
|
923
|
-
* Give an itinerary to follow using movement strategies
|
|
924
|
-
*
|
|
925
|
-
* Executes a sequence of movements and actions in order. Each route can be:
|
|
926
|
-
* - A Direction enum value for basic movement
|
|
927
|
-
* - A string starting with "turn-" for direction changes
|
|
928
|
-
* - A function that returns directions or actions
|
|
929
|
-
* - A Promise for async operations
|
|
930
|
-
*
|
|
931
|
-
* The method processes routes sequentially, respecting the entity's frequency
|
|
932
|
-
* setting for timing between movements.
|
|
933
|
-
*
|
|
934
|
-
* @param routes - Array of movement instructions to execute
|
|
935
|
-
* @returns Promise that resolves when all routes are completed
|
|
936
|
-
*
|
|
937
|
-
* @example
|
|
938
|
-
* ```ts
|
|
939
|
-
* // Basic directional movements
|
|
940
|
-
* await player.moveRoutes([
|
|
941
|
-
* Direction.Right,
|
|
942
|
-
* Direction.Up,
|
|
943
|
-
* Direction.Left
|
|
944
|
-
* ]);
|
|
945
|
-
*
|
|
946
|
-
* // Mix of movements and turns
|
|
947
|
-
* await player.moveRoutes([
|
|
948
|
-
* Direction.Right,
|
|
949
|
-
* 'turn-' + Direction.Up,
|
|
950
|
-
* Direction.Up
|
|
951
|
-
* ]);
|
|
952
|
-
*
|
|
953
|
-
* // Using functions for dynamic behavior
|
|
954
|
-
* const customMove = (player, map) => [Direction.Right, Direction.Down];
|
|
955
|
-
* await player.moveRoutes([customMove]);
|
|
956
|
-
*
|
|
957
|
-
* // With async operations
|
|
958
|
-
* await player.moveRoutes([
|
|
959
|
-
* Direction.Right,
|
|
960
|
-
* new Promise(resolve => setTimeout(resolve, 1000)), // Wait 1 second
|
|
961
|
-
* Direction.Left
|
|
962
|
-
* ]);
|
|
963
|
-
* ```
|
|
964
|
-
*/
|
|
965
|
-
moveRoutes(routes: Routes): Promise<boolean> {
|
|
966
|
-
let count = 0;
|
|
967
|
-
let frequence = 0;
|
|
1150
|
+
moveRoutes(routes: Routes, options?: MoveRoutesOptions): Promise<boolean> {
|
|
968
1151
|
const player = this as unknown as PlayerWithMixins;
|
|
969
|
-
|
|
1152
|
+
const selfAny = this as any;
|
|
1153
|
+
const routeSequence = (selfAny.__moveRouteSequence__ ?? 0) + 1;
|
|
1154
|
+
selfAny.__moveRouteSequence__ = routeSequence;
|
|
1155
|
+
|
|
1156
|
+
if (this._finishRoute) {
|
|
1157
|
+
this._finishRoute(false);
|
|
1158
|
+
this._finishRoute = null;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
970
1161
|
// Break any existing route movement
|
|
971
1162
|
this.clearMovements();
|
|
972
|
-
|
|
1163
|
+
|
|
973
1164
|
return new Promise(async (resolve) => {
|
|
974
1165
|
// Store the resolve function for potential breaking
|
|
975
1166
|
this._finishRoute = resolve;
|
|
976
|
-
|
|
1167
|
+
|
|
977
1168
|
// Process function routes first
|
|
978
|
-
const processedRoutes =
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1169
|
+
const processedRoutes = await Promise.all(
|
|
1170
|
+
routes.map(async (route: any) => {
|
|
1171
|
+
if (typeof route === 'function') {
|
|
1172
|
+
const map = player.getCurrentMap() as any;
|
|
1173
|
+
if (!map) {
|
|
1174
|
+
return undefined;
|
|
1175
|
+
}
|
|
1176
|
+
return route.apply(route, [player, map]);
|
|
983
1177
|
}
|
|
984
|
-
return route
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1178
|
+
return route;
|
|
1179
|
+
})
|
|
1180
|
+
);
|
|
1181
|
+
|
|
989
1182
|
// Flatten nested arrays
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1183
|
+
// Note: We keep promises in the routes array and handle them in the strategy
|
|
1184
|
+
const finalRoutes = this.flattenRoutes(processedRoutes);
|
|
1185
|
+
|
|
1186
|
+
if (selfAny.__moveRouteSequence__ !== routeSequence) {
|
|
1187
|
+
resolve(false);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// Create a movement strategy that handles all routes
|
|
1192
|
+
class RouteMovementStrategy implements MovementStrategy {
|
|
1193
|
+
private routeIndex = 0;
|
|
1194
|
+
private currentTarget: { x: number; y: number } | null = null; // Center position for physics
|
|
1195
|
+
private currentTargetTopLeft: { x: number; y: number } | null = null; // Top-left position for player.x() comparison
|
|
1196
|
+
private currentDirection: { x: number; y: number } = { x: 0, y: 0 };
|
|
1197
|
+
private finished = false;
|
|
1198
|
+
private waitingForPromise = false;
|
|
1199
|
+
private promiseStartTime = 0;
|
|
1200
|
+
private promiseDuration = 0;
|
|
1201
|
+
private readonly routes: Routes;
|
|
1202
|
+
private readonly player: PlayerWithMixins;
|
|
1203
|
+
private readonly onComplete: (success: boolean) => void;
|
|
1204
|
+
private readonly tileSize: number;
|
|
1205
|
+
private readonly tolerance: number;
|
|
1206
|
+
private readonly onStuck?: MoveRoutesOptions['onStuck'];
|
|
1207
|
+
private readonly stuckTimeout: number;
|
|
1208
|
+
private readonly stuckThreshold: number;
|
|
1209
|
+
private remainingDistance = 0;
|
|
1210
|
+
private segmentDirection: Direction | null = null;
|
|
1211
|
+
private segmentStep = 0;
|
|
1212
|
+
|
|
1213
|
+
// Frequency wait state
|
|
1214
|
+
private waitingForFrequency = false;
|
|
1215
|
+
private frequencyWaitStartTime = 0;
|
|
1216
|
+
private ratioFrequency = 15;
|
|
1217
|
+
|
|
1218
|
+
// Stuck detection state
|
|
1219
|
+
private lastPosition: { x: number; y: number } | null = null;
|
|
1220
|
+
private lastPositionTime: number = 0;
|
|
1221
|
+
private stuckCheckStartTime: number = 0;
|
|
1222
|
+
private lastDistanceToTarget: number | null = null;
|
|
1223
|
+
private isCurrentlyStuck: boolean = false;
|
|
1224
|
+
private stuckCheckInitialized: boolean = false;
|
|
1225
|
+
|
|
1226
|
+
constructor(
|
|
1227
|
+
routes: Routes,
|
|
1228
|
+
player: PlayerWithMixins,
|
|
1229
|
+
onComplete: (success: boolean) => void,
|
|
1230
|
+
options?: MoveRoutesOptions
|
|
1231
|
+
) {
|
|
1232
|
+
this.routes = routes;
|
|
1233
|
+
this.player = player;
|
|
1234
|
+
this.onComplete = onComplete;
|
|
1235
|
+
this.tileSize = player.nbPixelInTile || 32;
|
|
1236
|
+
this.tolerance = 0.5; // Tolerance in pixels for reaching target (reduced for precision)
|
|
1237
|
+
this.onStuck = options?.onStuck;
|
|
1238
|
+
this.stuckTimeout = options?.stuckTimeout ?? 500; // Default 500ms
|
|
1239
|
+
this.stuckThreshold = options?.stuckThreshold ?? 1; // Default 1 pixel
|
|
1240
|
+
|
|
1241
|
+
// Process initial route
|
|
1242
|
+
this.processNextRoute();
|
|
999
1243
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1244
|
+
|
|
1245
|
+
private debugLog(message: string, data?: any): void {
|
|
1246
|
+
// Debug logging disabled - enable if needed for troubleshooting
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
private processNextRoute(): void {
|
|
1250
|
+
// Reset frequency wait state when processing a new route
|
|
1251
|
+
this.waitingForFrequency = false;
|
|
1252
|
+
this.frequencyWaitStartTime = 0;
|
|
1253
|
+
this.remainingDistance = 0;
|
|
1254
|
+
this.segmentDirection = null;
|
|
1255
|
+
this.segmentStep = 0;
|
|
1256
|
+
|
|
1257
|
+
// Check if we've completed all routes
|
|
1258
|
+
if (this.routeIndex >= this.routes.length) {
|
|
1259
|
+
this.debugLog('COMPLETE all routes finished');
|
|
1260
|
+
this.finished = true;
|
|
1261
|
+
this.onComplete(true);
|
|
1006
1262
|
return;
|
|
1007
1263
|
}
|
|
1264
|
+
|
|
1265
|
+
const currentRoute = this.routes[this.routeIndex];
|
|
1266
|
+
this.routeIndex++;
|
|
1267
|
+
|
|
1268
|
+
if (currentRoute === undefined) {
|
|
1269
|
+
this.processNextRoute();
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
try {
|
|
1274
|
+
// Handle different route types
|
|
1275
|
+
if (typeof currentRoute === 'object' && 'then' in currentRoute) {
|
|
1276
|
+
// Handle Promise (like Move.wait())
|
|
1277
|
+
this.debugLog(`WAIT for promise (route ${this.routeIndex}/${this.routes.length})`);
|
|
1278
|
+
this.waitingForPromise = true;
|
|
1279
|
+
this.promiseStartTime = Date.now();
|
|
1280
|
+
|
|
1281
|
+
// Try to get duration from promise if possible (for Move.wait())
|
|
1282
|
+
// Move.wait() creates a promise that resolves after a delay
|
|
1283
|
+
// We'll use a default duration and let the promise resolve naturally
|
|
1284
|
+
this.promiseDuration = 1000; // Default 1 second, will be updated when promise resolves
|
|
1285
|
+
|
|
1286
|
+
// Set up promise resolution handler
|
|
1287
|
+
(currentRoute as Promise<any>).then(() => {
|
|
1288
|
+
this.debugLog('WAIT promise resolved');
|
|
1289
|
+
this.waitingForPromise = false;
|
|
1290
|
+
this.processNextRoute();
|
|
1291
|
+
}).catch(() => {
|
|
1292
|
+
this.debugLog('WAIT promise rejected');
|
|
1293
|
+
this.waitingForPromise = false;
|
|
1294
|
+
this.processNextRoute();
|
|
1295
|
+
});
|
|
1296
|
+
} else if (typeof currentRoute === 'string' && currentRoute.startsWith('turn-')) {
|
|
1297
|
+
// Handle turn commands - just change direction, no movement
|
|
1298
|
+
const directionStr = currentRoute.replace('turn-', '');
|
|
1299
|
+
let direction: Direction = Direction.Down;
|
|
1300
|
+
|
|
1301
|
+
switch (directionStr) {
|
|
1302
|
+
case 'up':
|
|
1303
|
+
case Direction.Up:
|
|
1304
|
+
direction = Direction.Up;
|
|
1305
|
+
break;
|
|
1306
|
+
case 'down':
|
|
1307
|
+
case Direction.Down:
|
|
1308
|
+
direction = Direction.Down;
|
|
1309
|
+
break;
|
|
1310
|
+
case 'left':
|
|
1311
|
+
case Direction.Left:
|
|
1312
|
+
direction = Direction.Left;
|
|
1313
|
+
break;
|
|
1314
|
+
case 'right':
|
|
1315
|
+
case Direction.Right:
|
|
1316
|
+
direction = Direction.Right;
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
this.debugLog(`TURN to ${directionStr}`);
|
|
1321
|
+
if (this.player.changeDirection) {
|
|
1322
|
+
this.player.changeDirection(direction);
|
|
1323
|
+
}
|
|
1324
|
+
// Turn is instant, continue immediately
|
|
1325
|
+
this.processNextRoute();
|
|
1326
|
+
} else if (typeof currentRoute === 'number' || typeof currentRoute === 'string') {
|
|
1327
|
+
// Handle Direction enum values (number or string) - calculate target position
|
|
1328
|
+
const moveDirection = currentRoute as unknown as Direction;
|
|
1329
|
+
const map = this.player.getCurrentMap() as any;
|
|
1330
|
+
if (!map) {
|
|
1331
|
+
this.finished = true;
|
|
1332
|
+
this.onComplete(false);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Get current position (top-left from player, which is what player.x() returns)
|
|
1337
|
+
// We calculate target based on top-left position to match player.x() expectations
|
|
1338
|
+
const currentTopLeftX = typeof this.player.x === 'function' ? this.player.x() : this.player.x;
|
|
1339
|
+
const currentTopLeftY = typeof this.player.y === 'function' ? this.player.y() : this.player.y;
|
|
1340
|
+
|
|
1341
|
+
// Get player speed
|
|
1342
|
+
let playerSpeed = this.player.speed()
|
|
1343
|
+
|
|
1344
|
+
// Use player speed as distance, not tile size
|
|
1345
|
+
let distance = playerSpeed;
|
|
1346
|
+
|
|
1347
|
+
// Merge consecutive routes of same direction
|
|
1348
|
+
const initialDistance = distance;
|
|
1349
|
+
const initialRouteIndex = this.routeIndex;
|
|
1350
|
+
while (this.routeIndex < this.routes.length) {
|
|
1351
|
+
const nextRoute = this.routes[this.routeIndex];
|
|
1352
|
+
if (nextRoute === currentRoute) {
|
|
1353
|
+
distance += playerSpeed;
|
|
1354
|
+
this.routeIndex++;
|
|
1355
|
+
} else {
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Prepare segmented movement (per tile)
|
|
1361
|
+
this.remainingDistance = distance;
|
|
1362
|
+
this.segmentDirection = moveDirection;
|
|
1363
|
+
this.segmentStep = this.getTileStepDistance(playerSpeed);
|
|
1364
|
+
this.setNextSegmentTarget(currentTopLeftX, currentTopLeftY);
|
|
1365
|
+
|
|
1366
|
+
if (this.currentTargetTopLeft) {
|
|
1367
|
+
this.debugLog(`MOVE direction=${moveDirection} from=(${currentTopLeftX.toFixed(1)}, ${currentTopLeftY.toFixed(1)}) to=(${this.currentTargetTopLeft.x.toFixed(1)}, ${this.currentTargetTopLeft.y.toFixed(1)}) dist=${distance.toFixed(1)}`);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Reset stuck detection when starting a new movement
|
|
1371
|
+
this.lastPosition = null;
|
|
1372
|
+
this.isCurrentlyStuck = false;
|
|
1373
|
+
this.stuckCheckStartTime = 0;
|
|
1374
|
+
this.lastDistanceToTarget = null;
|
|
1375
|
+
this.stuckCheckInitialized = false;
|
|
1376
|
+
// Reset frequency wait state when starting a new movement
|
|
1377
|
+
this.waitingForFrequency = false;
|
|
1378
|
+
this.frequencyWaitStartTime = 0;
|
|
1379
|
+
} else if (Array.isArray(currentRoute)) {
|
|
1380
|
+
// Handle array of directions - insert them into routes
|
|
1381
|
+
for (let i = currentRoute.length - 1; i >= 0; i--) {
|
|
1382
|
+
this.routes.splice(this.routeIndex, 0, currentRoute[i]);
|
|
1383
|
+
}
|
|
1384
|
+
this.processNextRoute();
|
|
1385
|
+
} else {
|
|
1386
|
+
// Unknown route type, skip
|
|
1387
|
+
this.processNextRoute();
|
|
1388
|
+
}
|
|
1389
|
+
} catch (error) {
|
|
1390
|
+
console.warn('Error processing route:', error);
|
|
1391
|
+
this.processNextRoute();
|
|
1392
|
+
}
|
|
1008
1393
|
}
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
const currentRoute = flatRoutes[routeIndex];
|
|
1021
|
-
routeIndex++;
|
|
1022
|
-
|
|
1023
|
-
if (currentRoute === undefined) {
|
|
1024
|
-
executeNextRoute();
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
try {
|
|
1029
|
-
// Handle different route types
|
|
1030
|
-
if (typeof currentRoute === 'object' && 'then' in currentRoute) {
|
|
1031
|
-
// Handle Promise
|
|
1032
|
-
await currentRoute;
|
|
1033
|
-
executeNextRoute();
|
|
1034
|
-
} else if (typeof currentRoute === 'string' && currentRoute.startsWith('turn-')) {
|
|
1035
|
-
// Handle turn commands
|
|
1036
|
-
const directionStr = currentRoute.replace('turn-', '');
|
|
1037
|
-
let direction: Direction = Direction.Down;
|
|
1038
|
-
|
|
1039
|
-
// Convert string direction to Direction enum
|
|
1040
|
-
switch (directionStr) {
|
|
1041
|
-
case 'up':
|
|
1042
|
-
case Direction.Up:
|
|
1043
|
-
direction = Direction.Up;
|
|
1044
|
-
break;
|
|
1045
|
-
case 'down':
|
|
1046
|
-
case Direction.Down:
|
|
1047
|
-
direction = Direction.Down;
|
|
1048
|
-
break;
|
|
1049
|
-
case 'left':
|
|
1050
|
-
case Direction.Left:
|
|
1051
|
-
direction = Direction.Left;
|
|
1052
|
-
break;
|
|
1053
|
-
case 'right':
|
|
1054
|
-
case Direction.Right:
|
|
1055
|
-
direction = Direction.Right;
|
|
1056
|
-
break;
|
|
1394
|
+
|
|
1395
|
+
update(body: MovementBody, dt: number): void {
|
|
1396
|
+
// Don't process if waiting for promise
|
|
1397
|
+
if (this.waitingForPromise) {
|
|
1398
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
1399
|
+
// Check if promise wait time has elapsed (fallback)
|
|
1400
|
+
if (Date.now() - this.promiseStartTime > this.promiseDuration) {
|
|
1401
|
+
this.waitingForPromise = false;
|
|
1402
|
+
this.processNextRoute();
|
|
1057
1403
|
}
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// Don't process if waiting for frequency delay
|
|
1408
|
+
if (this.waitingForFrequency) {
|
|
1409
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
1410
|
+
const playerFrequency = this.player.frequency;
|
|
1411
|
+
const frequencyMs = playerFrequency || 0;
|
|
1412
|
+
|
|
1413
|
+
if (frequencyMs > 0 && Date.now() - this.frequencyWaitStartTime >= frequencyMs * this.ratioFrequency) {
|
|
1414
|
+
this.waitingForFrequency = false;
|
|
1415
|
+
if (this.remainingDistance > 0) {
|
|
1416
|
+
const currentTopLeftX = this.player.x();
|
|
1417
|
+
const currentTopLeftY = this.player.y();
|
|
1418
|
+
this.setNextSegmentTarget(currentTopLeftX, currentTopLeftY);
|
|
1419
|
+
} else {
|
|
1420
|
+
this.processNextRoute();
|
|
1421
|
+
}
|
|
1061
1422
|
}
|
|
1062
|
-
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// If no target, try to process next route
|
|
1427
|
+
if (!this.currentTarget) {
|
|
1428
|
+
if (!this.finished) {
|
|
1429
|
+
this.processNextRoute();
|
|
1430
|
+
}
|
|
1431
|
+
if (!this.currentTarget) {
|
|
1432
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
1433
|
+
// Reset stuck detection when no target
|
|
1434
|
+
this.lastPosition = null;
|
|
1435
|
+
this.isCurrentlyStuck = false;
|
|
1436
|
+
this.lastDistanceToTarget = null;
|
|
1437
|
+
this.stuckCheckInitialized = false;
|
|
1438
|
+
this.currentTargetTopLeft = null;
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
const entity = body.getEntity?.();
|
|
1444
|
+
if (!entity) {
|
|
1445
|
+
this.finished = true;
|
|
1446
|
+
this.onComplete(false);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
const currentPosition = { x: entity.position.x, y: entity.position.y };
|
|
1451
|
+
const currentTime = Date.now();
|
|
1452
|
+
|
|
1453
|
+
// Check distance using player's top-left position (what player.x() returns)
|
|
1454
|
+
// This ensures we match the test expectations which compare player.x()
|
|
1455
|
+
const currentTopLeftX = this.player.x();
|
|
1456
|
+
const currentTopLeftY = this.player.y()
|
|
1457
|
+
const currentMap = this.player.getCurrentMap() as any;
|
|
1458
|
+
|
|
1459
|
+
// Calculate direction and distance using top-left position if available
|
|
1460
|
+
let dx: number, dy: number, distance: number;
|
|
1461
|
+
if (this.currentTargetTopLeft) {
|
|
1462
|
+
dx = this.currentTargetTopLeft.x - currentTopLeftX;
|
|
1463
|
+
dy = this.currentTargetTopLeft.y - currentTopLeftY;
|
|
1464
|
+
distance = Math.hypot(dx, dy);
|
|
1465
|
+
const arrivalThreshold = Math.max(this.tolerance, this.player.speed());
|
|
1466
|
+
|
|
1467
|
+
// Snap to the target when the physics step overshoots the discrete route distance.
|
|
1468
|
+
if (distance <= arrivalThreshold) {
|
|
1469
|
+
currentMap?.setBodyPosition?.(this.player.id, this.currentTargetTopLeft.x, this.currentTargetTopLeft.y, "top-left");
|
|
1470
|
+
|
|
1471
|
+
// Target reached, wait for frequency before processing next route
|
|
1472
|
+
this.debugLog(`TARGET reached at (${currentTopLeftX.toFixed(1)}, ${currentTopLeftY.toFixed(1)})`);
|
|
1473
|
+
this.currentTarget = null;
|
|
1474
|
+
this.currentTargetTopLeft = null;
|
|
1475
|
+
this.currentDirection = { x: 0, y: 0 };
|
|
1476
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
1477
|
+
// Reset stuck detection
|
|
1478
|
+
this.lastPosition = null;
|
|
1479
|
+
this.isCurrentlyStuck = false;
|
|
1480
|
+
this.lastDistanceToTarget = null;
|
|
1481
|
+
this.stuckCheckInitialized = false;
|
|
1482
|
+
|
|
1483
|
+
// Wait for frequency before processing next route or segment
|
|
1484
|
+
if (!this.finished) {
|
|
1485
|
+
const playerFrequency = this.player.frequency;
|
|
1486
|
+
if (playerFrequency && playerFrequency > 0) {
|
|
1487
|
+
this.waitingForFrequency = true;
|
|
1488
|
+
this.frequencyWaitStartTime = Date.now();
|
|
1489
|
+
} else if (this.remainingDistance > 0) {
|
|
1490
|
+
const nextTopLeftX = this.player.x();
|
|
1491
|
+
const nextTopLeftY = this.player.y();
|
|
1492
|
+
this.setNextSegmentTarget(nextTopLeftX, nextTopLeftY);
|
|
1493
|
+
} else {
|
|
1494
|
+
// No frequency delay, process immediately
|
|
1495
|
+
this.processNextRoute();
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1500
|
+
} else {
|
|
1501
|
+
// Fallback: use center position distance if top-left target not available
|
|
1502
|
+
dx = this.currentTarget.x - currentPosition.x;
|
|
1503
|
+
dy = this.currentTarget.y - currentPosition.y;
|
|
1504
|
+
distance = Math.hypot(dx, dy);
|
|
1505
|
+
const arrivalThreshold = Math.max(this.tolerance, this.player.speed());
|
|
1506
|
+
|
|
1507
|
+
// Check if we've reached the target (using center position as fallback)
|
|
1508
|
+
if (distance <= arrivalThreshold) {
|
|
1509
|
+
entity.position.set(this.currentTarget.x, this.currentTarget.y);
|
|
1510
|
+
entity.notifyPositionChange?.();
|
|
1511
|
+
|
|
1512
|
+
// Target reached, wait for frequency before processing next route
|
|
1513
|
+
this.currentTarget = null;
|
|
1514
|
+
this.currentTargetTopLeft = null;
|
|
1515
|
+
this.currentDirection = { x: 0, y: 0 };
|
|
1516
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
1517
|
+
// Reset stuck detection
|
|
1518
|
+
this.lastPosition = null;
|
|
1519
|
+
this.isCurrentlyStuck = false;
|
|
1520
|
+
this.lastDistanceToTarget = null;
|
|
1521
|
+
this.stuckCheckInitialized = false;
|
|
1522
|
+
|
|
1523
|
+
// Wait for frequency before processing next route or segment
|
|
1524
|
+
if (!this.finished) {
|
|
1525
|
+
const playerFrequency = player.frequency;
|
|
1526
|
+
if (playerFrequency && playerFrequency > 0) {
|
|
1527
|
+
this.waitingForFrequency = true;
|
|
1528
|
+
this.frequencyWaitStartTime = Date.now();
|
|
1529
|
+
} else if (this.remainingDistance > 0) {
|
|
1530
|
+
const nextTopLeftX = this.player.x();
|
|
1531
|
+
const nextTopLeftY = this.player.y();
|
|
1532
|
+
this.setNextSegmentTarget(nextTopLeftX, nextTopLeftY);
|
|
1533
|
+
} else {
|
|
1534
|
+
// No frequency delay, process immediately
|
|
1535
|
+
this.processNextRoute();
|
|
1536
|
+
}
|
|
1076
1537
|
}
|
|
1077
|
-
this.addMovement(new LinearMove(vx * (player.speed?.() || 3), vy * (player.speed?.() || 3), 100));
|
|
1078
|
-
setTimeout(executeNextRoute, 100);
|
|
1079
1538
|
return;
|
|
1080
1539
|
}
|
|
1081
|
-
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// Stuck detection: check if player is making progress
|
|
1543
|
+
if (this.onStuck && this.currentTarget) {
|
|
1544
|
+
// Initialize tracking on first update
|
|
1545
|
+
if (!this.stuckCheckInitialized) {
|
|
1546
|
+
this.lastPosition = { ...currentPosition };
|
|
1547
|
+
this.lastDistanceToTarget = distance;
|
|
1548
|
+
this.stuckCheckInitialized = true;
|
|
1549
|
+
// Update tracking and continue (don't return early)
|
|
1550
|
+
this.lastPositionTime = currentTime;
|
|
1551
|
+
} else if (this.lastPosition && this.lastDistanceToTarget !== null) {
|
|
1552
|
+
// We have a target, so we're trying to move (regardless of current velocity,
|
|
1553
|
+
// which may be zero due to physics engine collision handling)
|
|
1554
|
+
const positionChanged = Math.hypot(
|
|
1555
|
+
currentPosition.x - this.lastPosition.x,
|
|
1556
|
+
currentPosition.y - this.lastPosition.y
|
|
1557
|
+
) > this.stuckThreshold;
|
|
1558
|
+
|
|
1559
|
+
const distanceImproved = distance < (this.lastDistanceToTarget - this.stuckThreshold);
|
|
1560
|
+
|
|
1561
|
+
// Player is stuck if: not moving AND not getting closer to target
|
|
1562
|
+
if (!positionChanged && !distanceImproved) {
|
|
1563
|
+
// Player is not making progress
|
|
1564
|
+
if (!this.isCurrentlyStuck) {
|
|
1565
|
+
// Start stuck timer
|
|
1566
|
+
this.stuckCheckStartTime = currentTime;
|
|
1567
|
+
this.isCurrentlyStuck = true;
|
|
1568
|
+
} else {
|
|
1569
|
+
// Check if stuck timeout has elapsed
|
|
1570
|
+
if (currentTime - this.stuckCheckStartTime >= this.stuckTimeout) {
|
|
1571
|
+
// Player is stuck, call onStuck callback
|
|
1572
|
+
this.debugLog(`STUCK detected at (${currentPosition.x.toFixed(1)}, ${currentPosition.y.toFixed(1)}) target=(${this.currentTarget.x.toFixed(1)}, ${this.currentTarget.y.toFixed(1)})`);
|
|
1573
|
+
const shouldContinue = this.onStuck(
|
|
1574
|
+
this.player as any,
|
|
1575
|
+
this.currentTarget,
|
|
1576
|
+
currentPosition
|
|
1577
|
+
);
|
|
1578
|
+
|
|
1579
|
+
if (shouldContinue === false) {
|
|
1580
|
+
// Cancel the route
|
|
1581
|
+
this.debugLog('STUCK cancelled route');
|
|
1582
|
+
this.finished = true;
|
|
1583
|
+
this.onComplete(false);
|
|
1584
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
1585
|
+
return;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
// Continue route: abandon the current target and move on.
|
|
1589
|
+
//
|
|
1590
|
+
// ## Why?
|
|
1591
|
+
// When random movement hits an obstacle, the physics engine can keep the entity
|
|
1592
|
+
// at the same position while this strategy keeps trying to reach the same target,
|
|
1593
|
+
// resulting in an infinite "stuck loop". By dropping the current target here,
|
|
1594
|
+
// we allow the route to advance and (in the common case of `infiniteMoveRoute`)
|
|
1595
|
+
// re-evaluate `Move.tileRandom()` on the next cycle, producing a new direction.
|
|
1596
|
+
this.currentTarget = null;
|
|
1597
|
+
this.currentTargetTopLeft = null;
|
|
1598
|
+
this.currentDirection = { x: 0, y: 0 };
|
|
1599
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
1600
|
+
|
|
1601
|
+
// Reset stuck detection to start fresh on the next target
|
|
1602
|
+
this.isCurrentlyStuck = false;
|
|
1603
|
+
this.stuckCheckStartTime = 0;
|
|
1604
|
+
this.lastPosition = null;
|
|
1605
|
+
this.lastDistanceToTarget = null;
|
|
1606
|
+
this.stuckCheckInitialized = false;
|
|
1607
|
+
|
|
1608
|
+
// Advance to next route instruction immediately
|
|
1609
|
+
this.processNextRoute();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
} else {
|
|
1614
|
+
// Player is making progress, reset stuck detection
|
|
1615
|
+
this.isCurrentlyStuck = false;
|
|
1616
|
+
this.stuckCheckStartTime = 0;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Update tracking variables
|
|
1620
|
+
this.lastPosition = { ...currentPosition };
|
|
1621
|
+
this.lastPositionTime = currentTime;
|
|
1622
|
+
this.lastDistanceToTarget = distance;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// Get speed scalar from map (default 50 if not found)
|
|
1627
|
+
const map = currentMap;
|
|
1628
|
+
const speedScalar = map?.speedScalar ?? 50;
|
|
1629
|
+
|
|
1630
|
+
// Calculate direction and speed
|
|
1631
|
+
// Use the distance calculated above (from top-left if available, center otherwise)
|
|
1632
|
+
if (distance > 0) {
|
|
1633
|
+
this.currentDirection = { x: dx / distance, y: dy / distance };
|
|
1082
1634
|
} else {
|
|
1083
|
-
//
|
|
1084
|
-
|
|
1635
|
+
// If distance is 0 or negative, we've reached or passed the target
|
|
1636
|
+
this.currentTarget = null;
|
|
1637
|
+
this.currentTargetTopLeft = null;
|
|
1638
|
+
this.currentDirection = { x: 0, y: 0 };
|
|
1639
|
+
body.setVelocity({ x: 0, y: 0 });
|
|
1640
|
+
if (!this.finished) {
|
|
1641
|
+
const playerFrequency = typeof this.player.frequency === 'function' ? this.player.frequency() : this.player.frequency;
|
|
1642
|
+
if (playerFrequency && playerFrequency > 0) {
|
|
1643
|
+
this.waitingForFrequency = true;
|
|
1644
|
+
this.frequencyWaitStartTime = Date.now();
|
|
1645
|
+
} else if (this.remainingDistance > 0) {
|
|
1646
|
+
const nextTopLeftX = this.player.x();
|
|
1647
|
+
const nextTopLeftY = this.player.y();
|
|
1648
|
+
this.setNextSegmentTarget(nextTopLeftX, nextTopLeftY);
|
|
1649
|
+
} else {
|
|
1650
|
+
// No frequency delay, process immediately
|
|
1651
|
+
this.processNextRoute();
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
return;
|
|
1085
1655
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1656
|
+
|
|
1657
|
+
// Convert vector direction to cardinal direction (like moveBody does)
|
|
1658
|
+
const absX = Math.abs(this.currentDirection.x);
|
|
1659
|
+
const absY = Math.abs(this.currentDirection.y);
|
|
1660
|
+
let cardinalDirection: Direction;
|
|
1661
|
+
|
|
1662
|
+
if (absX >= absY) {
|
|
1663
|
+
cardinalDirection = this.currentDirection.x >= 0 ? Direction.Right : Direction.Left;
|
|
1664
|
+
} else {
|
|
1665
|
+
cardinalDirection = this.currentDirection.y >= 0 ? Direction.Down : Direction.Up;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
map.movePlayer(this.player as any, cardinalDirection)
|
|
1089
1669
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1670
|
+
|
|
1671
|
+
isFinished(): boolean {
|
|
1672
|
+
return this.finished;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
onFinished(): void {
|
|
1676
|
+
this.onComplete(true);
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
private getTileStepDistance(playerSpeed: number): number {
|
|
1680
|
+
if (!Number.isFinite(playerSpeed) || playerSpeed <= 0) {
|
|
1681
|
+
return this.tileSize;
|
|
1682
|
+
}
|
|
1683
|
+
const stepsPerTile = Math.max(1, Math.floor(this.tileSize / playerSpeed));
|
|
1684
|
+
return stepsPerTile * playerSpeed;
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
private setNextSegmentTarget(currentTopLeftX: number, currentTopLeftY: number): void {
|
|
1688
|
+
if (!this.segmentDirection || this.remainingDistance <= 0) {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
const map = this.player.getCurrentMap() as any;
|
|
1693
|
+
if (!map) {
|
|
1694
|
+
this.finished = true;
|
|
1695
|
+
this.onComplete(false);
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
const entity = map.physic.getEntityByUUID(this.player.id);
|
|
1700
|
+
if (!entity) {
|
|
1701
|
+
this.finished = true;
|
|
1702
|
+
this.onComplete(false);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const segmentDistance = Math.min(this.segmentStep || this.remainingDistance, this.remainingDistance);
|
|
1707
|
+
let targetTopLeftX = currentTopLeftX;
|
|
1708
|
+
let targetTopLeftY = currentTopLeftY;
|
|
1709
|
+
|
|
1710
|
+
switch (this.segmentDirection) {
|
|
1711
|
+
case Direction.Right:
|
|
1712
|
+
case 'right' as any:
|
|
1713
|
+
targetTopLeftX = currentTopLeftX + segmentDistance;
|
|
1714
|
+
break;
|
|
1715
|
+
case Direction.Left:
|
|
1716
|
+
case 'left' as any:
|
|
1717
|
+
targetTopLeftX = currentTopLeftX - segmentDistance;
|
|
1718
|
+
break;
|
|
1719
|
+
case Direction.Down:
|
|
1720
|
+
case 'down' as any:
|
|
1721
|
+
targetTopLeftY = currentTopLeftY + segmentDistance;
|
|
1722
|
+
break;
|
|
1723
|
+
case Direction.Up:
|
|
1724
|
+
case 'up' as any:
|
|
1725
|
+
targetTopLeftY = currentTopLeftY - segmentDistance;
|
|
1726
|
+
break;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
const hitbox = this.player.hitbox();
|
|
1730
|
+
const hitboxWidth = hitbox?.w ?? 32;
|
|
1731
|
+
const hitboxHeight = hitbox?.h ?? 32;
|
|
1732
|
+
|
|
1733
|
+
const targetX = targetTopLeftX + hitboxWidth / 2;
|
|
1734
|
+
const targetY = targetTopLeftY + hitboxHeight / 2;
|
|
1735
|
+
|
|
1736
|
+
this.currentTarget = { x: targetX, y: targetY };
|
|
1737
|
+
this.currentTargetTopLeft = { x: targetTopLeftX, y: targetTopLeftY };
|
|
1738
|
+
this.currentDirection = { x: 0, y: 0 };
|
|
1739
|
+
this.remainingDistance -= segmentDistance;
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// Create and add the route movement strategy
|
|
1744
|
+
const routeStrategy = new RouteMovementStrategy(
|
|
1745
|
+
finalRoutes,
|
|
1746
|
+
player,
|
|
1747
|
+
(success: boolean) => {
|
|
1748
|
+
this._finishRoute = null;
|
|
1749
|
+
resolve(success);
|
|
1750
|
+
},
|
|
1751
|
+
options
|
|
1752
|
+
);
|
|
1753
|
+
|
|
1754
|
+
this.addMovement(routeStrategy);
|
|
1093
1755
|
});
|
|
1094
1756
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
* @param routes - Routes array that may contain nested arrays
|
|
1101
|
-
* @returns Flattened array of routes
|
|
1102
|
-
*/
|
|
1103
|
-
flattenRoutes(routes: any[]): any[] {
|
|
1104
|
-
const result: any[] = [];
|
|
1105
|
-
|
|
1106
|
-
for (const route of routes) {
|
|
1107
|
-
if (Array.isArray(route)) {
|
|
1108
|
-
result.push(...this.flattenRoutes(route));
|
|
1109
|
-
} else {
|
|
1110
|
-
result.push(route);
|
|
1757
|
+
|
|
1758
|
+
private flattenRoutes(routes: Routes): Routes {
|
|
1759
|
+
return routes.reduce((acc: Routes, item) => {
|
|
1760
|
+
if (Array.isArray(item)) {
|
|
1761
|
+
return acc.concat(this.flattenRoutes(item));
|
|
1111
1762
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
return result;
|
|
1763
|
+
return acc.concat(item);
|
|
1764
|
+
}, []);
|
|
1115
1765
|
}
|
|
1116
1766
|
|
|
1117
|
-
|
|
1118
|
-
* Give a path that repeats itself in a loop to a character
|
|
1119
|
-
*
|
|
1120
|
-
* Creates an infinite movement pattern that continues until manually stopped.
|
|
1121
|
-
* The routes will repeat in a continuous loop, making it perfect for patrol
|
|
1122
|
-
* patterns, ambient movements, or any repetitive behavior.
|
|
1123
|
-
*
|
|
1124
|
-
* You can stop the movement at any time with `breakRoutes()` and replay it
|
|
1125
|
-
* with `replayRoutes()`.
|
|
1126
|
-
*
|
|
1127
|
-
* @param routes - Array of movement instructions to repeat infinitely
|
|
1128
|
-
*
|
|
1129
|
-
* @example
|
|
1130
|
-
* ```ts
|
|
1131
|
-
* // Create an infinite random movement pattern
|
|
1132
|
-
* player.infiniteMoveRoute([Move.random()]);
|
|
1133
|
-
*
|
|
1134
|
-
* // Create a patrol route
|
|
1135
|
-
* player.infiniteMoveRoute([
|
|
1136
|
-
* Direction.Right,
|
|
1137
|
-
* Direction.Right,
|
|
1138
|
-
* Direction.Down,
|
|
1139
|
-
* Direction.Left,
|
|
1140
|
-
* Direction.Left,
|
|
1141
|
-
* Direction.Up
|
|
1142
|
-
* ]);
|
|
1143
|
-
*
|
|
1144
|
-
* // Mix movements and rotations
|
|
1145
|
-
* player.infiniteMoveRoute([
|
|
1146
|
-
* Move.turnRight(),
|
|
1147
|
-
* Direction.Right,
|
|
1148
|
-
* Move.wait(1),
|
|
1149
|
-
* Move.turnLeft(),
|
|
1150
|
-
* Direction.Left
|
|
1151
|
-
* ]);
|
|
1152
|
-
* ```
|
|
1153
|
-
*/
|
|
1154
|
-
infiniteMoveRoute(routes: Routes): void {
|
|
1767
|
+
infiniteMoveRoute(routes: Routes, options?: MoveRoutesOptions): void {
|
|
1155
1768
|
this._infiniteRoutes = routes;
|
|
1156
1769
|
this._isInfiniteRouteActive = true;
|
|
1157
1770
|
|
|
1158
1771
|
const executeInfiniteRoute = (isBreaking: boolean = false) => {
|
|
1159
1772
|
if (isBreaking || !this._isInfiniteRouteActive) return;
|
|
1160
|
-
|
|
1161
|
-
this.moveRoutes(routes).then((completed) => {
|
|
1773
|
+
|
|
1774
|
+
this.moveRoutes(routes, options).then((completed) => {
|
|
1162
1775
|
// Only continue if the route completed successfully and we're still active
|
|
1163
1776
|
if (completed && this._isInfiniteRouteActive) {
|
|
1164
1777
|
executeInfiniteRoute();
|
|
@@ -1175,37 +1788,14 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
|
1175
1788
|
executeInfiniteRoute();
|
|
1176
1789
|
}
|
|
1177
1790
|
|
|
1178
|
-
/**
|
|
1179
|
-
* Stop an infinite movement
|
|
1180
|
-
*
|
|
1181
|
-
* Works only for infinite movements created with `infiniteMoveRoute()`.
|
|
1182
|
-
* This method stops the current route execution and prevents the next
|
|
1183
|
-
* iteration from starting.
|
|
1184
|
-
*
|
|
1185
|
-
* @param force - Forces the stop of the infinite movement immediately
|
|
1186
|
-
*
|
|
1187
|
-
* @example
|
|
1188
|
-
* ```ts
|
|
1189
|
-
* // Start infinite movement
|
|
1190
|
-
* player.infiniteMoveRoute([Move.random()]);
|
|
1191
|
-
*
|
|
1192
|
-
* // Stop it when player enters combat
|
|
1193
|
-
* if (inCombat) {
|
|
1194
|
-
* player.breakRoutes(true);
|
|
1195
|
-
* }
|
|
1196
|
-
*
|
|
1197
|
-
* // Gentle stop (completes current route first)
|
|
1198
|
-
* player.breakRoutes();
|
|
1199
|
-
* ```
|
|
1200
|
-
*/
|
|
1201
1791
|
breakRoutes(force: boolean = false): void {
|
|
1202
1792
|
this._isInfiniteRouteActive = false;
|
|
1203
|
-
|
|
1793
|
+
|
|
1204
1794
|
if (force) {
|
|
1205
1795
|
// Force stop by clearing all movements immediately
|
|
1206
1796
|
this.clearMovements();
|
|
1207
1797
|
}
|
|
1208
|
-
|
|
1798
|
+
|
|
1209
1799
|
// If there's an active route promise, resolve it
|
|
1210
1800
|
if (this._finishRoute) {
|
|
1211
1801
|
this._finishRoute(force);
|
|
@@ -1213,42 +1803,234 @@ export function WithMoveManager<TBase extends PlayerCtor>(Base: TBase) {
|
|
|
1213
1803
|
}
|
|
1214
1804
|
}
|
|
1215
1805
|
|
|
1216
|
-
/**
|
|
1217
|
-
* Replay an infinite movement
|
|
1218
|
-
*
|
|
1219
|
-
* Works only for infinite movements that were previously created with
|
|
1220
|
-
* `infiniteMoveRoute()`. If the route was stopped with `breakRoutes()`,
|
|
1221
|
-
* you can restart it with this method using the same route configuration.
|
|
1222
|
-
*
|
|
1223
|
-
* @example
|
|
1224
|
-
* ```ts
|
|
1225
|
-
* // Create infinite movement
|
|
1226
|
-
* player.infiniteMoveRoute([Move.random()]);
|
|
1227
|
-
*
|
|
1228
|
-
* // Stop it temporarily
|
|
1229
|
-
* player.breakRoutes(true);
|
|
1230
|
-
*
|
|
1231
|
-
* // Resume the same movement pattern
|
|
1232
|
-
* player.replayRoutes();
|
|
1233
|
-
*
|
|
1234
|
-
* // Stop and start with different conditions
|
|
1235
|
-
* if (playerNearby) {
|
|
1236
|
-
* player.breakRoutes();
|
|
1237
|
-
* } else {
|
|
1238
|
-
* player.replayRoutes();
|
|
1239
|
-
* }
|
|
1240
|
-
* ```
|
|
1241
|
-
*/
|
|
1242
1806
|
replayRoutes(): void {
|
|
1243
1807
|
if (this._infiniteRoutes && !this._isInfiniteRouteActive) {
|
|
1244
1808
|
this.infiniteMoveRoute(this._infiniteRoutes);
|
|
1245
1809
|
}
|
|
1246
1810
|
}
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1811
|
+
}
|
|
1249
1812
|
|
|
1813
|
+
return WithMoveManagerClass as unknown as PlayerCtor;
|
|
1814
|
+
}
|
|
1250
1815
|
/**
|
|
1251
|
-
*
|
|
1252
|
-
*
|
|
1816
|
+
* Interface for Move Manager functionality
|
|
1817
|
+
*
|
|
1818
|
+
* Provides comprehensive movement management capabilities including pathfinding,
|
|
1819
|
+
* physics-based movement, route following, and advanced movement strategies.
|
|
1820
|
+
* This interface defines the public API of the MoveManager mixin.
|
|
1253
1821
|
*/
|
|
1254
|
-
export
|
|
1822
|
+
export interface IMoveManager {
|
|
1823
|
+
/**
|
|
1824
|
+
* Whether the player passes through other players
|
|
1825
|
+
*
|
|
1826
|
+
* When `true`, the player can walk through other player entities without collision.
|
|
1827
|
+
* This is useful for busy areas where players shouldn't block each other.
|
|
1828
|
+
*
|
|
1829
|
+
* @default true
|
|
1830
|
+
*
|
|
1831
|
+
* @example
|
|
1832
|
+
* ```ts
|
|
1833
|
+
* // Disable player-to-player collision
|
|
1834
|
+
* player.throughOtherPlayer = true;
|
|
1835
|
+
*
|
|
1836
|
+
* // Enable player-to-player collision
|
|
1837
|
+
* player.throughOtherPlayer = false;
|
|
1838
|
+
* ```
|
|
1839
|
+
*/
|
|
1840
|
+
throughOtherPlayer: boolean;
|
|
1841
|
+
|
|
1842
|
+
/**
|
|
1843
|
+
* Whether the player goes through all characters (players and events)
|
|
1844
|
+
*
|
|
1845
|
+
* When `true`, the player can walk through all character entities (both players and events)
|
|
1846
|
+
* without collision. Walls and obstacles still block movement.
|
|
1847
|
+
* This takes precedence over `throughOtherPlayer` and `throughEvent`.
|
|
1848
|
+
*
|
|
1849
|
+
* @default false
|
|
1850
|
+
*
|
|
1851
|
+
* @example
|
|
1852
|
+
* ```ts
|
|
1853
|
+
* // Enable ghost mode - pass through all characters
|
|
1854
|
+
* player.through = true;
|
|
1855
|
+
*
|
|
1856
|
+
* // Disable ghost mode
|
|
1857
|
+
* player.through = false;
|
|
1858
|
+
* ```
|
|
1859
|
+
*/
|
|
1860
|
+
through: boolean;
|
|
1861
|
+
|
|
1862
|
+
/**
|
|
1863
|
+
* Whether the player passes through events (NPCs, objects)
|
|
1864
|
+
*
|
|
1865
|
+
* When `true`, the player can walk through event entities without collision.
|
|
1866
|
+
* This is useful for NPCs that shouldn't block player movement.
|
|
1867
|
+
*
|
|
1868
|
+
* @default false
|
|
1869
|
+
*
|
|
1870
|
+
* @example
|
|
1871
|
+
* ```ts
|
|
1872
|
+
* // Allow passing through events
|
|
1873
|
+
* player.throughEvent = true;
|
|
1874
|
+
*
|
|
1875
|
+
* // Block passage through events
|
|
1876
|
+
* player.throughEvent = false;
|
|
1877
|
+
* ```
|
|
1878
|
+
*/
|
|
1879
|
+
throughEvent: boolean;
|
|
1880
|
+
|
|
1881
|
+
/** Frequency for movement timing (milliseconds between movements) */
|
|
1882
|
+
frequency: number;
|
|
1883
|
+
|
|
1884
|
+
/** Whether direction changes are locked (prevents automatic direction changes) */
|
|
1885
|
+
directionFixed: boolean;
|
|
1886
|
+
|
|
1887
|
+
/** Whether animation changes are locked (prevents automatic animation changes) */
|
|
1888
|
+
animationFixed: boolean;
|
|
1889
|
+
|
|
1890
|
+
/**
|
|
1891
|
+
* Add a custom movement strategy to this entity
|
|
1892
|
+
*
|
|
1893
|
+
* Returns a Promise that resolves when the movement completes.
|
|
1894
|
+
*
|
|
1895
|
+
* @param strategy - The movement strategy to add
|
|
1896
|
+
* @param options - Optional callbacks for movement lifecycle events
|
|
1897
|
+
* @returns Promise that resolves when the movement completes
|
|
1898
|
+
*/
|
|
1899
|
+
addMovement(strategy: MovementStrategy, options?: MovementOptions): Promise<void>;
|
|
1900
|
+
|
|
1901
|
+
/**
|
|
1902
|
+
* Remove a specific movement strategy from this entity
|
|
1903
|
+
*
|
|
1904
|
+
* @param strategy - The strategy instance to remove
|
|
1905
|
+
* @returns True if the strategy was found and removed
|
|
1906
|
+
*/
|
|
1907
|
+
removeMovement(strategy: MovementStrategy): boolean;
|
|
1908
|
+
|
|
1909
|
+
/**
|
|
1910
|
+
* Remove all active movement strategies from this entity
|
|
1911
|
+
*/
|
|
1912
|
+
clearMovements(): void;
|
|
1913
|
+
|
|
1914
|
+
/**
|
|
1915
|
+
* Check if this entity has any active movement strategies
|
|
1916
|
+
*
|
|
1917
|
+
* @returns True if entity has active movements
|
|
1918
|
+
*/
|
|
1919
|
+
hasActiveMovements(): boolean;
|
|
1920
|
+
|
|
1921
|
+
/**
|
|
1922
|
+
* Get all active movement strategies for this entity
|
|
1923
|
+
*
|
|
1924
|
+
* @returns Array of active movement strategies
|
|
1925
|
+
*/
|
|
1926
|
+
getActiveMovements(): MovementStrategy[];
|
|
1927
|
+
|
|
1928
|
+
/**
|
|
1929
|
+
* Move toward a target player or position using AI pathfinding
|
|
1930
|
+
*
|
|
1931
|
+
* @param target - Target player or position to move toward
|
|
1932
|
+
*/
|
|
1933
|
+
moveTo(target: RpgCommonPlayer | { x: number, y: number }): void;
|
|
1934
|
+
|
|
1935
|
+
/**
|
|
1936
|
+
* Stop the current moveTo behavior
|
|
1937
|
+
*/
|
|
1938
|
+
stopMoveTo(): void;
|
|
1939
|
+
|
|
1940
|
+
/**
|
|
1941
|
+
* Perform a dash movement in the specified direction
|
|
1942
|
+
*
|
|
1943
|
+
* The total speed is calculated by adding the player's base speed to the additional speed.
|
|
1944
|
+
* Returns a Promise that resolves when the dash completes.
|
|
1945
|
+
*
|
|
1946
|
+
* @param direction - Normalized direction vector
|
|
1947
|
+
* @param additionalSpeed - Extra speed added on top of base speed (default: 4)
|
|
1948
|
+
* @param duration - Duration in milliseconds (default: 200)
|
|
1949
|
+
* @param options - Optional callbacks for movement lifecycle events
|
|
1950
|
+
* @returns Promise that resolves when the dash completes
|
|
1951
|
+
*/
|
|
1952
|
+
dash(direction: { x: number, y: number }, additionalSpeed?: number, duration?: number, options?: MovementOptions): Promise<void>;
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* Apply knockback effect in the specified direction
|
|
1956
|
+
*
|
|
1957
|
+
* The force is scaled by the player's base speed for consistent behavior.
|
|
1958
|
+
* Returns a Promise that resolves when the knockback completes.
|
|
1959
|
+
*
|
|
1960
|
+
* @param direction - Normalized direction vector
|
|
1961
|
+
* @param force - Force multiplier applied to base speed (default: 5)
|
|
1962
|
+
* @param duration - Duration in milliseconds (default: 300)
|
|
1963
|
+
* @param options - Optional callbacks for movement lifecycle events
|
|
1964
|
+
* @returns Promise that resolves when the knockback completes
|
|
1965
|
+
*/
|
|
1966
|
+
knockback(direction: { x: number, y: number }, force?: number, duration?: number, options?: MovementOptions): Promise<void>;
|
|
1967
|
+
|
|
1968
|
+
/**
|
|
1969
|
+
* Follow a sequence of waypoints
|
|
1970
|
+
*
|
|
1971
|
+
* Speed is calculated from the player's base speed multiplied by the speedMultiplier.
|
|
1972
|
+
*
|
|
1973
|
+
* @param waypoints - Array of x,y positions to follow
|
|
1974
|
+
* @param speedMultiplier - Multiplier applied to base speed (default: 0.5)
|
|
1975
|
+
* @param loop - Whether to loop back to start (default: false)
|
|
1976
|
+
*/
|
|
1977
|
+
followPath(waypoints: Array<{ x: number, y: number }>, speedMultiplier?: number, loop?: boolean): void;
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Apply oscillating movement pattern
|
|
1981
|
+
*
|
|
1982
|
+
* @param direction - Primary oscillation axis (normalized)
|
|
1983
|
+
* @param amplitude - Maximum distance from center (default: 50)
|
|
1984
|
+
* @param period - Time for complete cycle in ms (default: 2000)
|
|
1985
|
+
*/
|
|
1986
|
+
oscillate(direction: { x: number, y: number }, amplitude?: number, period?: number): void;
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* Apply ice movement physics
|
|
1990
|
+
*
|
|
1991
|
+
* Max speed is calculated from the player's base speed multiplied by the speedFactor.
|
|
1992
|
+
*
|
|
1993
|
+
* @param direction - Target movement direction
|
|
1994
|
+
* @param speedFactor - Factor multiplied with base speed for max speed (default: 1.0)
|
|
1995
|
+
*/
|
|
1996
|
+
applyIceMovement(direction: { x: number, y: number }, speedFactor?: number): void;
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* Shoot a projectile in the specified direction
|
|
2000
|
+
*
|
|
2001
|
+
* Speed is calculated from the player's base speed multiplied by the speedFactor.
|
|
2002
|
+
*
|
|
2003
|
+
* @param type - Type of projectile trajectory
|
|
2004
|
+
* @param direction - Normalized direction vector
|
|
2005
|
+
* @param speedFactor - Factor multiplied with base speed (default: 50)
|
|
2006
|
+
*/
|
|
2007
|
+
shootProjectile(type: ProjectileType, direction: { x: number, y: number }, speedFactor?: number): void;
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* Give an itinerary to follow using movement strategies
|
|
2011
|
+
*
|
|
2012
|
+
* @param routes - Array of movement instructions to execute
|
|
2013
|
+
* @param options - Optional configuration including onStuck callback
|
|
2014
|
+
* @returns Promise that resolves when all routes are completed
|
|
2015
|
+
*/
|
|
2016
|
+
moveRoutes(routes: Routes, options?: MoveRoutesOptions): Promise<boolean>;
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* Give a path that repeats itself in a loop to a character
|
|
2020
|
+
*
|
|
2021
|
+
* @param routes - Array of movement instructions to repeat infinitely
|
|
2022
|
+
*/
|
|
2023
|
+
infiniteMoveRoute(routes: Routes): void;
|
|
2024
|
+
|
|
2025
|
+
/**
|
|
2026
|
+
* Stop an infinite movement
|
|
2027
|
+
*
|
|
2028
|
+
* @param force - Forces the stop of the infinite movement immediately
|
|
2029
|
+
*/
|
|
2030
|
+
breakRoutes(force?: boolean): void;
|
|
2031
|
+
|
|
2032
|
+
/**
|
|
2033
|
+
* Replay an infinite movement
|
|
2034
|
+
*/
|
|
2035
|
+
replayRoutes(): void;
|
|
2036
|
+
}
|