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