@rpgjs/server 5.0.0-alpha.9 → 5.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Gui/DialogGui.d.ts +5 -0
- package/dist/Gui/GameoverGui.d.ts +23 -0
- package/dist/Gui/Gui.d.ts +6 -0
- package/dist/Gui/MenuGui.d.ts +22 -3
- package/dist/Gui/NotificationGui.d.ts +1 -2
- package/dist/Gui/SaveLoadGui.d.ts +13 -0
- package/dist/Gui/ShopGui.d.ts +28 -3
- package/dist/Gui/TitleGui.d.ts +23 -0
- package/dist/Gui/index.d.ts +10 -1
- package/dist/Player/BattleManager.d.ts +44 -32
- package/dist/Player/ClassManager.d.ts +24 -4
- package/dist/Player/ComponentManager.d.ts +95 -32
- package/dist/Player/Components.d.ts +345 -0
- package/dist/Player/EffectManager.d.ts +50 -4
- package/dist/Player/ElementManager.d.ts +77 -4
- package/dist/Player/GoldManager.d.ts +1 -1
- package/dist/Player/GuiManager.d.ts +87 -4
- package/dist/Player/ItemFixture.d.ts +1 -1
- package/dist/Player/ItemManager.d.ts +431 -4
- package/dist/Player/MoveManager.d.ts +301 -34
- package/dist/Player/ParameterManager.d.ts +364 -28
- package/dist/Player/Player.d.ts +558 -14
- package/dist/Player/SkillManager.d.ts +187 -13
- package/dist/Player/StateManager.d.ts +75 -4
- package/dist/Player/VariableManager.d.ts +62 -4
- package/dist/RpgServer.d.ts +278 -63
- package/dist/RpgServerEngine.d.ts +2 -1
- package/dist/decorators/event.d.ts +46 -0
- package/dist/decorators/map.d.ts +299 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +17920 -29711
- package/dist/index.js.map +1 -1
- package/dist/logs/log.d.ts +2 -3
- package/dist/module-CaCW1SDh.js +11018 -0
- package/dist/module-CaCW1SDh.js.map +1 -0
- package/dist/module.d.ts +43 -1
- package/dist/node/connection.d.ts +51 -0
- package/dist/node/index.d.ts +5 -0
- package/dist/node/index.js +551 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/map.d.ts +16 -0
- package/dist/node/room.d.ts +21 -0
- package/dist/node/transport.d.ts +28 -0
- package/dist/node/types.d.ts +47 -0
- package/dist/presets/index.d.ts +0 -9
- package/dist/rooms/BaseRoom.d.ts +132 -0
- package/dist/rooms/lobby.d.ts +10 -2
- package/dist/rooms/map.d.ts +1359 -32
- package/dist/services/save.d.ts +43 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/localStorage.d.ts +23 -0
- package/package.json +25 -10
- package/src/Gui/DialogGui.ts +19 -4
- package/src/Gui/GameoverGui.ts +39 -0
- package/src/Gui/Gui.ts +23 -1
- package/src/Gui/MenuGui.ts +155 -6
- package/src/Gui/NotificationGui.ts +1 -2
- package/src/Gui/SaveLoadGui.ts +60 -0
- package/src/Gui/ShopGui.ts +146 -16
- package/src/Gui/TitleGui.ts +39 -0
- package/src/Gui/index.ts +15 -2
- package/src/Player/BattleManager.ts +39 -56
- package/src/Player/ClassManager.ts +82 -74
- package/src/Player/ComponentManager.ts +394 -32
- package/src/Player/Components.ts +380 -0
- package/src/Player/EffectManager.ts +50 -96
- package/src/Player/ElementManager.ts +74 -152
- package/src/Player/GuiManager.ts +125 -14
- package/src/Player/ItemManager.ts +747 -341
- package/src/Player/MoveManager.ts +1532 -750
- package/src/Player/ParameterManager.ts +636 -106
- package/src/Player/Player.ts +1273 -79
- package/src/Player/SkillManager.ts +558 -197
- package/src/Player/StateManager.ts +131 -258
- package/src/Player/VariableManager.ts +85 -157
- package/src/RpgServer.ts +293 -62
- package/src/decorators/event.ts +61 -0
- package/src/decorators/map.ts +343 -0
- package/src/index.ts +11 -1
- package/src/logs/log.ts +10 -3
- package/src/module.ts +126 -3
- package/src/node/connection.ts +254 -0
- package/src/node/index.ts +22 -0
- package/src/node/map.ts +328 -0
- package/src/node/room.ts +63 -0
- package/src/node/transport.ts +532 -0
- package/src/node/types.ts +61 -0
- package/src/presets/index.ts +1 -10
- package/src/rooms/BaseRoom.ts +232 -0
- package/src/rooms/lobby.ts +25 -7
- package/src/rooms/map.ts +2682 -206
- package/src/services/save.ts +147 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/localStorage.ts +76 -0
- package/tests/battle.spec.ts +375 -0
- package/tests/change-map.spec.ts +72 -0
- package/tests/class.spec.ts +274 -0
- package/tests/custom-websocket.spec.ts +127 -0
- package/tests/effect.spec.ts +219 -0
- package/tests/element.spec.ts +221 -0
- package/tests/event.spec.ts +80 -0
- package/tests/gold.spec.ts +99 -0
- package/tests/item.spec.ts +609 -0
- package/tests/module.spec.ts +38 -0
- package/tests/move.spec.ts +601 -0
- package/tests/node-transport.spec.ts +223 -0
- package/tests/player-param.spec.ts +45 -0
- package/tests/prediction-reconciliation.spec.ts +182 -0
- package/tests/random-move.spec.ts +65 -0
- package/tests/skill.spec.ts +658 -0
- package/tests/state.spec.ts +467 -0
- package/tests/variable.spec.ts +185 -0
- package/tests/world-maps.spec.ts +896 -0
- package/vite.config.ts +36 -3
package/src/Player/Player.ts
CHANGED
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
RpgCommonPlayer,
|
|
6
6
|
ShowAnimationParams,
|
|
7
7
|
Constructor,
|
|
8
|
+
Direction,
|
|
9
|
+
AttachShapeOptions,
|
|
10
|
+
RpgShape,
|
|
11
|
+
ShapePositioning,
|
|
8
12
|
} from "@rpgjs/common";
|
|
13
|
+
import { Entity, Vector2 } from "@rpgjs/physic";
|
|
9
14
|
import { IComponentManager, WithComponentManager } from "./ComponentManager";
|
|
10
15
|
import { RpgMap } from "../rooms/map";
|
|
11
16
|
import { Context, inject } from "@signe/di";
|
|
@@ -14,33 +19,33 @@ import { MockConnection } from "@signe/room";
|
|
|
14
19
|
import { IMoveManager, WithMoveManager } from "./MoveManager";
|
|
15
20
|
import { IGoldManager, WithGoldManager } from "./GoldManager";
|
|
16
21
|
import { WithVariableManager, type IVariableManager } from "./VariableManager";
|
|
17
|
-
import { sync } from "@signe/sync";
|
|
18
|
-
import { signal } from "@signe/reactive";
|
|
22
|
+
import { createStatesSnapshotDeep, load, sync, type } from "@signe/sync";
|
|
23
|
+
import { computed, signal } from "@signe/reactive";
|
|
19
24
|
import {
|
|
20
25
|
IParameterManager,
|
|
21
26
|
WithParameterManager,
|
|
22
27
|
} from "./ParameterManager";
|
|
23
28
|
import { WithItemFixture } from "./ItemFixture";
|
|
24
29
|
import { IItemManager, WithItemManager } from "./ItemManager";
|
|
25
|
-
import { lastValueFrom } from "rxjs";
|
|
30
|
+
import { bufferTime, combineLatest, debounceTime, distinctUntilChanged, filter, lastValueFrom, map, Observable, pairwise, sample, throttleTime } from "rxjs";
|
|
26
31
|
import { IEffectManager, WithEffectManager } from "./EffectManager";
|
|
27
|
-
import { AGI,
|
|
32
|
+
import { AGI, DEX, INT, MAXHP, MAXSP, STR } from "@rpgjs/common";
|
|
33
|
+
import { AGI_CURVE, DEX_CURVE, INT_CURVE, MAXHP_CURVE, MAXSP_CURVE, STR_CURVE } from "../presets";
|
|
28
34
|
import { IElementManager, WithElementManager } from "./ElementManager";
|
|
29
35
|
import { ISkillManager, WithSkillManager } from "./SkillManager";
|
|
30
36
|
import { IBattleManager, WithBattleManager } from "./BattleManager";
|
|
31
37
|
import { IClassManager, WithClassManager } from "./ClassManager";
|
|
32
38
|
import { IStateManager, WithStateManager } from "./StateManager";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
39
|
+
import {
|
|
40
|
+
buildSaveSlotMeta,
|
|
41
|
+
resolveAutoSaveStrategy,
|
|
42
|
+
resolveSaveSlot,
|
|
43
|
+
resolveSaveStorageStrategy,
|
|
44
|
+
shouldAutoSave,
|
|
45
|
+
type SaveRequestContext,
|
|
46
|
+
type SaveSlotIndex,
|
|
47
|
+
type SaveSlotMeta,
|
|
48
|
+
} from "../services/save";
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
51
|
* Combines multiple RpgCommonPlayer mixins into one
|
|
@@ -96,36 +101,224 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
96
101
|
map: RpgMap | null = null;
|
|
97
102
|
context?: Context;
|
|
98
103
|
conn: MockConnection | null = null;
|
|
104
|
+
touchSide: boolean = false; // Protection against map change loops
|
|
105
|
+
private _clientListeners = new Map<string, Set<(data: any) => void | Promise<void>>>();
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Computed signal for world X position
|
|
109
|
+
*
|
|
110
|
+
* Calculates the absolute world X position from the map's world position
|
|
111
|
+
* plus the player's local X position. Returns 0 if no map is assigned.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const worldX = player.worldX();
|
|
116
|
+
* console.log(`Player is at world X: ${worldX}`);
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
get worldPositionX() {
|
|
120
|
+
return this._getComputedWorldPosition('x');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Computed signal for world Y position
|
|
125
|
+
*
|
|
126
|
+
* Calculates the absolute world Y position from the map's world position
|
|
127
|
+
* plus the player's local Y position. Returns 0 if no map is assigned.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* const worldY = player.worldY();
|
|
132
|
+
* console.log(`Player is at world Y: ${worldY}`);
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
get worldPositionY() {
|
|
136
|
+
return this._getComputedWorldPosition('y');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private _worldPositionSignals = new WeakMap<any, any>();
|
|
140
|
+
|
|
141
|
+
private _getComputedWorldPosition(axis: 'x' | 'y') {
|
|
142
|
+
// We use a WeakMap to cache the computed signal per instance
|
|
143
|
+
// This ensures that if the player object is copied (e.g. in tests),
|
|
144
|
+
// the new instance gets its own signal bound to itself.
|
|
145
|
+
if (!this._worldPositionSignals) {
|
|
146
|
+
this._worldPositionSignals = new WeakMap();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const key = axis;
|
|
150
|
+
let signals = this._worldPositionSignals.get(this);
|
|
151
|
+
if (!signals) {
|
|
152
|
+
signals = {};
|
|
153
|
+
this._worldPositionSignals.set(this, signals);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (!signals[key]) {
|
|
157
|
+
signals[key] = computed(() => {
|
|
158
|
+
const map = this.map as RpgMap | null;
|
|
159
|
+
const mapWorldPos = map ? (map[axis === 'x' ? 'worldX' : 'worldY'] ?? 0) : 0;
|
|
160
|
+
return mapWorldPos + (this[axis] as any)();
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return signals[key];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Internal: Shapes attached to this player */
|
|
167
|
+
private _attachedShapes: Map<string, RpgShape> = new Map();
|
|
168
|
+
|
|
169
|
+
/** Internal: Shapes where this player is currently located */
|
|
170
|
+
private _inShapes: Set<RpgShape> = new Set();
|
|
171
|
+
/** Last processed client input timestamp for reconciliation */
|
|
172
|
+
lastProcessedInputTs: number = 0;
|
|
173
|
+
/** Last processed client input frame for reconciliation with server tick */
|
|
174
|
+
_lastFramePositions: {
|
|
175
|
+
frame: number;
|
|
176
|
+
position: {
|
|
177
|
+
x: number;
|
|
178
|
+
y: number;
|
|
179
|
+
direction: Direction;
|
|
180
|
+
};
|
|
181
|
+
serverTick?: number; // Server tick at which this position was computed
|
|
182
|
+
} | null = null;
|
|
183
|
+
|
|
184
|
+
frames: { x: number; y: number; ts: number }[] = [];
|
|
99
185
|
|
|
100
186
|
@sync(RpgPlayer) events = signal<RpgEvent[]>([]);
|
|
101
187
|
|
|
102
188
|
constructor() {
|
|
103
189
|
super();
|
|
190
|
+
|
|
191
|
+
const initialX = typeof this.x === "function" ? Number(this.x()) || 0 : 0;
|
|
192
|
+
const initialY = typeof this.y === "function" ? Number(this.y()) || 0 : 0;
|
|
193
|
+
let lastEmitted: { x: number; y: number } | null = { x: initialX, y: initialY };
|
|
194
|
+
let pendingUpdate: { x: number; y: number } | null = null;
|
|
195
|
+
let updateScheduled = false;
|
|
196
|
+
|
|
197
|
+
combineLatest([this.x.observable, this.y.observable])
|
|
198
|
+
.subscribe(([x, y]) => {
|
|
199
|
+
pendingUpdate = { x, y };
|
|
200
|
+
|
|
201
|
+
// Schedule a synchronous update using queueMicrotask
|
|
202
|
+
// This groups multiple rapid changes (x and y in the same tick) into a single frame
|
|
203
|
+
if (!updateScheduled) {
|
|
204
|
+
updateScheduled = true;
|
|
205
|
+
queueMicrotask(() => {
|
|
206
|
+
if (pendingUpdate) {
|
|
207
|
+
const { x, y } = pendingUpdate;
|
|
208
|
+
// Only emit if the values are different from the last emitted frame
|
|
209
|
+
if (!lastEmitted || lastEmitted.x !== x || lastEmitted.y !== y) {
|
|
210
|
+
this.frames = [...this.frames, {
|
|
211
|
+
x: x,
|
|
212
|
+
y: y,
|
|
213
|
+
ts: Date.now(),
|
|
214
|
+
}];
|
|
215
|
+
lastEmitted = { x, y };
|
|
216
|
+
}
|
|
217
|
+
pendingUpdate = null;
|
|
218
|
+
}
|
|
219
|
+
updateScheduled = false;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private _getClientListenerBucket(key: string) {
|
|
226
|
+
let listeners = this._clientListeners.get(key);
|
|
227
|
+
if (!listeners) {
|
|
228
|
+
listeners = new Set();
|
|
229
|
+
this._clientListeners.set(key, listeners);
|
|
230
|
+
}
|
|
231
|
+
return listeners;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async _dispatchClientEvent(key: string, data: any) {
|
|
235
|
+
const listeners = [...(this._clientListeners.get(key) ?? [])];
|
|
236
|
+
for (const callback of listeners) {
|
|
237
|
+
await callback(data);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_onInit() {
|
|
242
|
+
this.hooks.callHooks("server-playerProps-load", this).subscribe();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Apply the built-in default parameter curves to this player.
|
|
247
|
+
*
|
|
248
|
+
* Use this when you want RPGJS to provide the initial parameter setup
|
|
249
|
+
* instead of restoring values from your own database or a saved snapshot.
|
|
250
|
+
*
|
|
251
|
+
* This method only defines the parameter curves and related defaults.
|
|
252
|
+
* It does not restore custom persisted data for you.
|
|
253
|
+
*
|
|
254
|
+
* @method player.applyDefaultParameters()
|
|
255
|
+
* @returns {void}
|
|
256
|
+
*/
|
|
257
|
+
applyDefaultParameters() {
|
|
104
258
|
// Use type assertion to access mixin properties
|
|
105
259
|
(this as any).expCurve = {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
260
|
+
basis: 30,
|
|
261
|
+
extra: 20,
|
|
262
|
+
accelerationA: 30,
|
|
263
|
+
accelerationB: 30
|
|
110
264
|
};
|
|
111
265
|
|
|
112
|
-
(this as any).addParameter(MAXHP, MAXHP_CURVE);
|
|
266
|
+
;(this as any).addParameter(MAXHP, MAXHP_CURVE);
|
|
113
267
|
(this as any).addParameter(MAXSP, MAXSP_CURVE);
|
|
114
268
|
(this as any).addParameter(STR, STR_CURVE);
|
|
115
269
|
(this as any).addParameter(INT, INT_CURVE);
|
|
116
270
|
(this as any).addParameter(DEX, DEX_CURVE);
|
|
117
271
|
(this as any).addParameter(AGI, AGI_CURVE);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Initialize the built-in default player stats.
|
|
276
|
+
*
|
|
277
|
+
* This applies the default parameter curves and then restores HP/SP to their
|
|
278
|
+
* current maximum values so the client receives coherent bars on first load.
|
|
279
|
+
*
|
|
280
|
+
* Call this manually in `onConnected()` or `onStart()` when your game relies
|
|
281
|
+
* on the built-in defaults. Do not call it after loading a snapshot or
|
|
282
|
+
* hydrating player data from your own database unless you explicitly want to
|
|
283
|
+
* overwrite those values.
|
|
284
|
+
*
|
|
285
|
+
* @method player.initializeDefaultStats()
|
|
286
|
+
* @returns {void}
|
|
287
|
+
*/
|
|
288
|
+
initializeDefaultStats() {
|
|
289
|
+
this.applyDefaultParameters();
|
|
118
290
|
(this as any).allRecovery();
|
|
119
291
|
}
|
|
120
292
|
|
|
293
|
+
get hooks() {
|
|
294
|
+
return inject<Hooks>(this.context as any, ModulesToken);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// compatibility with v4
|
|
298
|
+
get server() {
|
|
299
|
+
return this.map
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
setMap(map: RpgMap) {
|
|
303
|
+
this.map = map;
|
|
304
|
+
// Prevent immediate ping-pong map transfers when spawning near a border.
|
|
305
|
+
this.touchSide = true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
applyFrames() {
|
|
309
|
+
this._frames.set(this.frames)
|
|
310
|
+
this.frames = []
|
|
311
|
+
}
|
|
312
|
+
|
|
121
313
|
async execMethod(method: string, methodData: any[] = [], target?: any) {
|
|
122
314
|
let ret: any;
|
|
123
315
|
if (target) {
|
|
124
|
-
|
|
316
|
+
if (typeof target[method] === 'function') {
|
|
317
|
+
ret = await target[method](...methodData);
|
|
318
|
+
}
|
|
125
319
|
}
|
|
126
320
|
else {
|
|
127
|
-
|
|
128
|
-
ret = await lastValueFrom(hooks
|
|
321
|
+
ret = await lastValueFrom(this.hooks
|
|
129
322
|
.callHooks(`server-player-${method}`, target ?? this, ...methodData));
|
|
130
323
|
}
|
|
131
324
|
this.syncChanges()
|
|
@@ -152,23 +345,169 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
152
345
|
mapId: string,
|
|
153
346
|
positions?: { x: number; y: number; z?: number } | string
|
|
154
347
|
): Promise<any | null | boolean> {
|
|
348
|
+
const realMapId = 'map-' + mapId;
|
|
349
|
+
const room = this.getCurrentMap();
|
|
350
|
+
|
|
351
|
+
const canChange: boolean[] = await lastValueFrom(this.hooks.callHooks("server-player-canChangeMap", this, {
|
|
352
|
+
id: mapId,
|
|
353
|
+
}));
|
|
354
|
+
if (canChange.some(v => v === false)) return false;
|
|
355
|
+
|
|
356
|
+
if (positions && typeof positions === 'object') {
|
|
357
|
+
await this.teleport(positions)
|
|
358
|
+
}
|
|
359
|
+
const transferToken = await room?.$sessionTransfer(this.conn, realMapId);
|
|
155
360
|
this.emit("changeMap", {
|
|
156
|
-
mapId:
|
|
361
|
+
mapId: realMapId,
|
|
157
362
|
positions,
|
|
363
|
+
transferToken: typeof transferToken === 'string' ? transferToken : undefined,
|
|
158
364
|
});
|
|
159
365
|
return true;
|
|
160
366
|
}
|
|
161
367
|
|
|
368
|
+
async autoChangeMap(nextPosition: Vector2): Promise<boolean> {
|
|
369
|
+
const map = this.getCurrentMap()
|
|
370
|
+
const worldMaps = map?.getInWorldMaps()
|
|
371
|
+
let ret: boolean = false
|
|
372
|
+
if (worldMaps && map) {
|
|
373
|
+
const direction = this.getDirection()
|
|
374
|
+
const marginLeftRight = map.tileWidth / 2
|
|
375
|
+
const marginTopDown = map.tileHeight / 2
|
|
376
|
+
const hitbox = this.hitbox()
|
|
377
|
+
const currentX = this.x()
|
|
378
|
+
const currentY = this.y()
|
|
379
|
+
const nearBorder =
|
|
380
|
+
currentX < marginLeftRight ||
|
|
381
|
+
currentX > map.widthPx - hitbox.w - marginLeftRight ||
|
|
382
|
+
currentY < marginTopDown ||
|
|
383
|
+
currentY > map.heightPx - hitbox.h - marginTopDown
|
|
384
|
+
|
|
385
|
+
if (this.touchSide) {
|
|
386
|
+
if (nearBorder) {
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
389
|
+
this.touchSide = false
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const changeMap = async (adjacent, to) => {
|
|
393
|
+
const [nextMap] = worldMaps.getAdjacentMaps(map, adjacent)
|
|
394
|
+
if (!nextMap) {
|
|
395
|
+
return false
|
|
396
|
+
}
|
|
397
|
+
const id = nextMap.id as string
|
|
398
|
+
const nextMapInfo = worldMaps.getMapInfo(id)
|
|
399
|
+
const changed = !!(await this.changeMap(id, to(nextMapInfo)))
|
|
400
|
+
if (changed) {
|
|
401
|
+
this.touchSide = true
|
|
402
|
+
}
|
|
403
|
+
return changed
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (nextPosition.x < marginLeftRight && direction == Direction.Left) {
|
|
407
|
+
ret = await changeMap({
|
|
408
|
+
x: map.worldX - 1,
|
|
409
|
+
y: this.worldPositionY() + 1
|
|
410
|
+
}, nextMapInfo => ({
|
|
411
|
+
x: (nextMapInfo.width) - this.hitbox().w - marginLeftRight,
|
|
412
|
+
y: map.worldY - nextMapInfo.y + nextPosition.y
|
|
413
|
+
}))
|
|
414
|
+
}
|
|
415
|
+
else if (nextPosition.x > map.widthPx - this.hitbox().w - marginLeftRight && direction == Direction.Right) {
|
|
416
|
+
ret = await changeMap({
|
|
417
|
+
x: map.worldX + map.widthPx + 1,
|
|
418
|
+
y: this.worldPositionY() + 1
|
|
419
|
+
}, nextMapInfo => ({
|
|
420
|
+
x: marginLeftRight,
|
|
421
|
+
y: map.worldY - nextMapInfo.y + nextPosition.y
|
|
422
|
+
}))
|
|
423
|
+
}
|
|
424
|
+
else if (nextPosition.y < marginTopDown && direction == Direction.Up) {
|
|
425
|
+
ret = await changeMap({
|
|
426
|
+
x: this.worldPositionX() + 1,
|
|
427
|
+
y: map.worldY - 1
|
|
428
|
+
}, nextMapInfo => ({
|
|
429
|
+
x: map.worldX - nextMapInfo.x + nextPosition.x,
|
|
430
|
+
y: (nextMapInfo.height) - this.hitbox().h - marginTopDown,
|
|
431
|
+
}))
|
|
432
|
+
}
|
|
433
|
+
else if (nextPosition.y > map.heightPx - this.hitbox().h - marginTopDown && direction == Direction.Down) {
|
|
434
|
+
ret = await changeMap({
|
|
435
|
+
x: this.worldPositionX() + 1,
|
|
436
|
+
y: map.worldY + map.heightPx + 1
|
|
437
|
+
}, nextMapInfo => ({
|
|
438
|
+
x: map.worldX - nextMapInfo.x + nextPosition.x,
|
|
439
|
+
y: marginTopDown,
|
|
440
|
+
}))
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
this.touchSide = false
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return ret
|
|
447
|
+
}
|
|
448
|
+
|
|
162
449
|
async teleport(positions: { x: number; y: number }) {
|
|
163
450
|
if (!this.map) return false;
|
|
164
|
-
|
|
165
|
-
|
|
451
|
+
if (this.map && this.map.physic) {
|
|
452
|
+
// Skip collision check for teleportation (allow teleporting through walls)
|
|
453
|
+
const entity = this.map.physic.getEntityByUUID(this.id);
|
|
454
|
+
if (entity) {
|
|
455
|
+
const hitbox = typeof this.hitbox === "function" ? this.hitbox() : this.hitbox;
|
|
456
|
+
const width = hitbox?.w ?? 32;
|
|
457
|
+
const height = hitbox?.h ?? 32;
|
|
458
|
+
|
|
459
|
+
// Convert top-left position to center position for physics engine
|
|
460
|
+
// positions.x/y are TOP-LEFT coordinates, but physic.teleport expects CENTER coordinates
|
|
461
|
+
const centerX = positions.x + width / 2;
|
|
462
|
+
const centerY = positions.y + height / 2;
|
|
463
|
+
|
|
464
|
+
this.map.physic.teleport(entity, { x: centerX, y: centerY });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
this.x.set(positions.x)
|
|
468
|
+
this.y.set(positions.y)
|
|
469
|
+
// Wait for the frame to be added before applying frames
|
|
470
|
+
// This ensures the frame is added before applyFrames() is called
|
|
471
|
+
queueMicrotask(() => {
|
|
472
|
+
this.applyFrames()
|
|
473
|
+
})
|
|
166
474
|
}
|
|
167
475
|
|
|
168
476
|
getCurrentMap<T extends RpgMap = RpgMap>(): T | null {
|
|
169
477
|
return this.map as T | null;
|
|
170
478
|
}
|
|
171
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Send a custom event to the current player's client.
|
|
482
|
+
*
|
|
483
|
+
* Use this to push arbitrary websocket payloads to one client only.
|
|
484
|
+
* On the client side, receive the event by injecting `WebSocketToken`
|
|
485
|
+
* and subscribing with `socket.on(...)`.
|
|
486
|
+
*
|
|
487
|
+
* @method player.emit(type, value)
|
|
488
|
+
* @param type - Custom event name sent to the client
|
|
489
|
+
* @param value - Payload sent with the event
|
|
490
|
+
* @returns {void}
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* player.emit("inventory:updated", {
|
|
495
|
+
* slots: player.items().length,
|
|
496
|
+
* });
|
|
497
|
+
* ```
|
|
498
|
+
*
|
|
499
|
+
* @example
|
|
500
|
+
* ```ts
|
|
501
|
+
* import { inject } from "@rpgjs/client";
|
|
502
|
+
* import { WebSocketToken, type AbstractWebsocket } from "@rpgjs/client";
|
|
503
|
+
*
|
|
504
|
+
* const socket = inject<AbstractWebsocket>(WebSocketToken);
|
|
505
|
+
*
|
|
506
|
+
* socket.on("inventory:updated", (payload) => {
|
|
507
|
+
* console.log(payload.slots);
|
|
508
|
+
* });
|
|
509
|
+
* ```
|
|
510
|
+
*/
|
|
172
511
|
emit(type: string, value?: any) {
|
|
173
512
|
const map = this.getCurrentMap();
|
|
174
513
|
if (!map || !this.conn) return;
|
|
@@ -178,7 +517,272 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
178
517
|
});
|
|
179
518
|
}
|
|
180
519
|
|
|
181
|
-
|
|
520
|
+
snapshot() {
|
|
521
|
+
const snapshot = createStatesSnapshotDeep(this);
|
|
522
|
+
const expCurve = (this as any).expCurve;
|
|
523
|
+
if (expCurve) {
|
|
524
|
+
snapshot.expCurve = { ...expCurve };
|
|
525
|
+
}
|
|
526
|
+
return snapshot;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async applySnapshot(snapshot: string | object) {
|
|
530
|
+
const data = typeof snapshot === "string" ? JSON.parse(snapshot) : snapshot;
|
|
531
|
+
const withItems = (this as any).resolveItemsSnapshot?.(data) ?? data;
|
|
532
|
+
const withSkills = (this as any).resolveSkillsSnapshot?.(withItems) ?? withItems;
|
|
533
|
+
const withStates = (this as any).resolveStatesSnapshot?.(withSkills) ?? withSkills;
|
|
534
|
+
const withClass = (this as any).resolveClassSnapshot?.(withStates) ?? withStates;
|
|
535
|
+
const resolvedSnapshot = (this as any).resolveEquipmentsSnapshot?.(withClass) ?? withClass;
|
|
536
|
+
load(this, resolvedSnapshot);
|
|
537
|
+
if (resolvedSnapshot.expCurve) {
|
|
538
|
+
(this as any).expCurve = resolvedSnapshot.expCurve;
|
|
539
|
+
}
|
|
540
|
+
if (Array.isArray(resolvedSnapshot.items)) {
|
|
541
|
+
this.items.set(resolvedSnapshot.items);
|
|
542
|
+
}
|
|
543
|
+
if (Array.isArray(resolvedSnapshot.skills)) {
|
|
544
|
+
this.skills.set(resolvedSnapshot.skills);
|
|
545
|
+
}
|
|
546
|
+
if (Array.isArray(resolvedSnapshot.states)) {
|
|
547
|
+
this.states.set(resolvedSnapshot.states);
|
|
548
|
+
}
|
|
549
|
+
if (resolvedSnapshot._class != null && this._class?.set) {
|
|
550
|
+
this._class.set(resolvedSnapshot._class);
|
|
551
|
+
}
|
|
552
|
+
if (Array.isArray(resolvedSnapshot.equipments)) {
|
|
553
|
+
this.equipments.set(resolvedSnapshot.equipments);
|
|
554
|
+
}
|
|
555
|
+
await lastValueFrom(this.hooks.callHooks("server-player-onLoad", this, resolvedSnapshot));
|
|
556
|
+
return resolvedSnapshot;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async save(slot: SaveSlotIndex = "auto", meta: SaveSlotMeta = {}, context: SaveRequestContext = {}) {
|
|
560
|
+
const policy = resolveAutoSaveStrategy();
|
|
561
|
+
if (policy.canSave && !policy.canSave(this, context)) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
const resolvedSlot = resolveSaveSlot(slot, policy, this, context);
|
|
565
|
+
if (resolvedSlot === null) {
|
|
566
|
+
return null;
|
|
567
|
+
}
|
|
568
|
+
const snapshot = this.snapshot();
|
|
569
|
+
await lastValueFrom(this.hooks.callHooks("server-player-onSave", this, snapshot));
|
|
570
|
+
const storage = resolveSaveStorageStrategy();
|
|
571
|
+
const finalMeta = buildSaveSlotMeta(this, meta);
|
|
572
|
+
await storage.save(this, resolvedSlot, JSON.stringify(snapshot), finalMeta);
|
|
573
|
+
return { index: resolvedSlot, meta: finalMeta };
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async load(
|
|
577
|
+
slot: SaveSlotIndex = "auto",
|
|
578
|
+
context: SaveRequestContext = {},
|
|
579
|
+
options: { changeMap?: boolean } = {}
|
|
580
|
+
) {
|
|
581
|
+
const policy = resolveAutoSaveStrategy();
|
|
582
|
+
if (policy.canLoad && !policy.canLoad(this, context)) {
|
|
583
|
+
return { ok: false };
|
|
584
|
+
}
|
|
585
|
+
const resolvedSlot = resolveSaveSlot(slot, policy, this, context);
|
|
586
|
+
if (resolvedSlot === null) {
|
|
587
|
+
return { ok: false };
|
|
588
|
+
}
|
|
589
|
+
const storage = resolveSaveStorageStrategy();
|
|
590
|
+
const slotData = await storage.get(this, resolvedSlot);
|
|
591
|
+
if (!slotData?.snapshot) {
|
|
592
|
+
return { ok: false };
|
|
593
|
+
}
|
|
594
|
+
await this.applySnapshot(slotData.snapshot);
|
|
595
|
+
const { snapshot, ...meta } = slotData;
|
|
596
|
+
if (options.changeMap !== false && meta.map) {
|
|
597
|
+
await this.changeMap(meta.map);
|
|
598
|
+
}
|
|
599
|
+
return { ok: true, slot: meta, index: resolvedSlot };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* @deprecated Use setGraphicAnimation instead.
|
|
605
|
+
* @param animationName - The name of the animation to play (e.g., 'attack', 'skill', 'walk')
|
|
606
|
+
* @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)
|
|
607
|
+
*/
|
|
608
|
+
setAnimation(animationName: string, nbTimes: number = Infinity) {
|
|
609
|
+
console.warn('setAnimation is deprecated. Use setGraphicAnimation instead.');
|
|
610
|
+
this.setGraphicAnimation(animationName, nbTimes);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* @deprecated Use setGraphicAnimation instead.
|
|
615
|
+
* @param graphic - The graphic to use for the animation (e.g., 'attack', 'skill', 'walk')
|
|
616
|
+
* @param animationName - The name of the animation to play (e.g., 'attack', 'skill', 'walk')
|
|
617
|
+
* @param replaceGraphic - Whether to replace the player's graphic (default: false)
|
|
618
|
+
*/
|
|
619
|
+
showAnimation(graphic: string, animationName: string, replaceGraphic: boolean = false) {
|
|
620
|
+
if (replaceGraphic) {
|
|
621
|
+
console.warn('showAnimation is deprecated. Use player.setGraphicAnimation instead.');
|
|
622
|
+
this.setGraphicAnimation(animationName, graphic);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
console.warn('showAnimation is deprecated. Use map.showAnimation instead.');
|
|
626
|
+
const map = this.getCurrentMap();
|
|
627
|
+
map?.showAnimation({ x: this.x(), y: this.y() }, graphic, animationName);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Listen to custom data sent by the current player's client.
|
|
633
|
+
*
|
|
634
|
+
* This listens to websocket actions emitted from the client with
|
|
635
|
+
* `socket.emit(key, data)`. It is intended for custom client events
|
|
636
|
+
* that are not already handled by built-in server actions such as
|
|
637
|
+
* `move`, `action`, or GUI interactions.
|
|
638
|
+
*
|
|
639
|
+
* @title Listen to data from the client
|
|
640
|
+
* @method player.on(key, cb)
|
|
641
|
+
* @param key - Event name emitted by the client
|
|
642
|
+
* @param cb - Callback invoked with the payload sent by the client
|
|
643
|
+
* @returns {void}
|
|
644
|
+
* @since 3.0.0-beta.5
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```ts
|
|
648
|
+
* player.on("chat:message", ({ text }) => {
|
|
649
|
+
* console.log("Client says:", text);
|
|
650
|
+
* });
|
|
651
|
+
* ```
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```ts
|
|
655
|
+
* import { inject } from "@rpgjs/client";
|
|
656
|
+
* import { WebSocketToken, type AbstractWebsocket } from "@rpgjs/client";
|
|
657
|
+
*
|
|
658
|
+
* const socket = inject<AbstractWebsocket>(WebSocketToken);
|
|
659
|
+
* socket.emit("chat:message", { text: "Hello server" });
|
|
660
|
+
* ```
|
|
661
|
+
*/
|
|
662
|
+
on(key: string, cb: (data: any) => void | Promise<void>) {
|
|
663
|
+
this._getClientListenerBucket(key).add(cb);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Listen one time to custom data sent by the current player's client.
|
|
668
|
+
*
|
|
669
|
+
* After the first matching event is received, the listener is removed
|
|
670
|
+
* automatically.
|
|
671
|
+
*
|
|
672
|
+
* @title Listen one-time to data from the client
|
|
673
|
+
* @method player.once(key, cb)
|
|
674
|
+
* @param key - Event name emitted by the client
|
|
675
|
+
* @param cb - Callback invoked only once with the payload sent by the client
|
|
676
|
+
* @returns {void}
|
|
677
|
+
* @since 3.0.0-beta.5
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* ```ts
|
|
681
|
+
* player.once("tutorial:ready", (payload) => {
|
|
682
|
+
* console.log("Ready once:", payload.step);
|
|
683
|
+
* });
|
|
684
|
+
* ```
|
|
685
|
+
*/
|
|
686
|
+
once(key: string, cb: (data: any) => void | Promise<void>) {
|
|
687
|
+
const onceCallback = async (data: any) => {
|
|
688
|
+
this._clientListeners.get(key)?.delete(onceCallback);
|
|
689
|
+
await cb(data);
|
|
690
|
+
};
|
|
691
|
+
this.on(key, onceCallback);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Remove all listeners for a custom client event on this player.
|
|
696
|
+
*
|
|
697
|
+
* @title Remove listeners of the client event
|
|
698
|
+
* @method player.off(key)
|
|
699
|
+
* @param key - Event name to clear
|
|
700
|
+
* @returns {void}
|
|
701
|
+
* @since 3.0.0-beta.5
|
|
702
|
+
*
|
|
703
|
+
* @example
|
|
704
|
+
* ```ts
|
|
705
|
+
* player.off("chat:message");
|
|
706
|
+
* ```
|
|
707
|
+
*/
|
|
708
|
+
off(key: string) {
|
|
709
|
+
this._clientListeners.delete(key);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Set the current animation of the player's sprite
|
|
714
|
+
*
|
|
715
|
+
* This method changes the animation state of the player's current sprite.
|
|
716
|
+
* It's used to trigger character animations like attack, skill, or custom movements.
|
|
717
|
+
* When `nbTimes` is set to a finite number, the animation will play that many times
|
|
718
|
+
* before returning to the previous animation state.
|
|
719
|
+
*
|
|
720
|
+
* If `animationFixed` is true, this method will not change the animation.
|
|
721
|
+
*
|
|
722
|
+
* @param animationName - The name of the animation to play (e.g., 'attack', 'skill', 'walk')
|
|
723
|
+
* @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)
|
|
724
|
+
*/
|
|
725
|
+
setGraphicAnimation(animationName: string, nbTimes: number): void;
|
|
726
|
+
/**
|
|
727
|
+
* Set the current animation of the player's sprite with a temporary graphic change
|
|
728
|
+
*
|
|
729
|
+
* This method changes the animation state of the player's current sprite and temporarily
|
|
730
|
+
* changes the player's graphic (sprite sheet) during the animation. The graphic is
|
|
731
|
+
* automatically reset when the animation finishes.
|
|
732
|
+
*
|
|
733
|
+
* When `nbTimes` is set to a finite number, the animation will play that many times
|
|
734
|
+
* before returning to the previous animation state and graphic.
|
|
735
|
+
*
|
|
736
|
+
* If `animationFixed` is true, this method will not change the animation.
|
|
737
|
+
*
|
|
738
|
+
* @param animationName - The name of the animation to play (e.g., 'attack', 'skill', 'walk')
|
|
739
|
+
* @param graphic - The graphic(s) to temporarily use during the animation
|
|
740
|
+
* @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)
|
|
741
|
+
*/
|
|
742
|
+
setGraphicAnimation(animationName: string, graphic: string | string[], nbTimes: number): void;
|
|
743
|
+
setGraphicAnimation(animationName: string, graphic: string | string[]): void;
|
|
744
|
+
setGraphicAnimation(animationName: string, graphicOrNbTimes?: string | string[] | number, nbTimes: number = 1): void {
|
|
745
|
+
// Don't change animation if it's locked
|
|
746
|
+
if (this.animationFixed) {
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let graphic: string | string[] | undefined;
|
|
751
|
+
let finalNbTimes: number = Infinity;
|
|
752
|
+
|
|
753
|
+
// Handle overloads
|
|
754
|
+
if (typeof graphicOrNbTimes === 'number') {
|
|
755
|
+
// setGraphicAnimation(animationName, nbTimes)
|
|
756
|
+
finalNbTimes = graphicOrNbTimes;
|
|
757
|
+
} else if (graphicOrNbTimes !== undefined) {
|
|
758
|
+
// setGraphicAnimation(animationName, graphic, nbTimes)
|
|
759
|
+
graphic = graphicOrNbTimes;
|
|
760
|
+
finalNbTimes = nbTimes ?? Infinity;
|
|
761
|
+
} else {
|
|
762
|
+
// setGraphicAnimation(animationName) - nbTimes remains Infinity
|
|
763
|
+
finalNbTimes = Infinity;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const map = this.getCurrentMap();
|
|
767
|
+
if (!map) return;
|
|
768
|
+
|
|
769
|
+
if (finalNbTimes === Infinity) {
|
|
770
|
+
if (graphic) this.setGraphic(graphic);
|
|
771
|
+
this.animationName.set(animationName);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
map.$broadcast({
|
|
775
|
+
type: "setAnimation",
|
|
776
|
+
value: {
|
|
777
|
+
animationName,
|
|
778
|
+
graphic,
|
|
779
|
+
nbTimes: finalNbTimes,
|
|
780
|
+
object: this.id,
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
182
786
|
|
|
183
787
|
/**
|
|
184
788
|
* Run the change detection cycle. Normally, as soon as a hook is called in a class, the cycle is started. But you can start it manually
|
|
@@ -191,11 +795,15 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
191
795
|
*/
|
|
192
796
|
syncChanges() {
|
|
193
797
|
this._eventChanges();
|
|
798
|
+
if (shouldAutoSave(this, { reason: "auto", source: "syncChanges" })) {
|
|
799
|
+
void this.save("auto", {}, { reason: "auto", source: "syncChanges" });
|
|
800
|
+
}
|
|
194
801
|
}
|
|
195
802
|
|
|
196
803
|
databaseById(id: string) {
|
|
197
|
-
|
|
198
|
-
|
|
804
|
+
// Use this.map directly to support both RpgMap and LobbyRoom
|
|
805
|
+
const map = this.map as any;
|
|
806
|
+
if (!map || !map.database) return;
|
|
199
807
|
const data = map.database()[id];
|
|
200
808
|
if (!data)
|
|
201
809
|
throw new Error(
|
|
@@ -208,58 +816,315 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
208
816
|
const map = this.getCurrentMap();
|
|
209
817
|
if (!map) return;
|
|
210
818
|
const { events } = map;
|
|
819
|
+
const visibleMapEvents = Object.values(events?.() ?? {}).filter((event: any) =>
|
|
820
|
+
map.isEventVisibleForPlayer?.(event, this) ?? true
|
|
821
|
+
);
|
|
211
822
|
const arrayEvents: any[] = [
|
|
212
823
|
...Object.values(this.events()),
|
|
213
|
-
...
|
|
824
|
+
...visibleMapEvents,
|
|
214
825
|
];
|
|
215
826
|
for (let event of arrayEvents) {
|
|
216
827
|
if (event.onChanges) event.onChanges(this);
|
|
217
828
|
}
|
|
218
829
|
}
|
|
219
830
|
|
|
220
|
-
|
|
831
|
+
/**
|
|
832
|
+
* Attach a zone shape to this player using the physic zone system
|
|
833
|
+
*
|
|
834
|
+
* This method creates a zone attached to the player's entity in the physics engine.
|
|
835
|
+
* The zone can be circular or cone-shaped and will detect other entities (players/events)
|
|
836
|
+
* entering or exiting the zone.
|
|
837
|
+
*
|
|
838
|
+
* @param id - Optional zone identifier. If not provided, a unique ID will be generated
|
|
839
|
+
* @param options - Zone configuration options
|
|
840
|
+
*
|
|
841
|
+
* @example
|
|
842
|
+
* ```ts
|
|
843
|
+
* // Create a circular detection zone
|
|
844
|
+
* player.attachShape("vision", {
|
|
845
|
+
* radius: 150,
|
|
846
|
+
* angle: 360,
|
|
847
|
+
* });
|
|
848
|
+
*
|
|
849
|
+
* // Create a cone-shaped vision zone
|
|
850
|
+
* player.attachShape("vision", {
|
|
851
|
+
* radius: 200,
|
|
852
|
+
* angle: 120,
|
|
853
|
+
* direction: Direction.Right,
|
|
854
|
+
* limitedByWalls: true,
|
|
855
|
+
* });
|
|
856
|
+
*
|
|
857
|
+
* // Create a zone with width/height (radius calculated automatically)
|
|
858
|
+
* player.attachShape({
|
|
859
|
+
* width: 100,
|
|
860
|
+
* height: 100,
|
|
861
|
+
* positioning: "center",
|
|
862
|
+
* });
|
|
863
|
+
* ```
|
|
864
|
+
*/
|
|
865
|
+
attachShape(idOrOptions: string | AttachShapeOptions, options?: AttachShapeOptions): RpgShape | undefined {
|
|
221
866
|
const map = this.getCurrentMap();
|
|
222
|
-
if (!map) return;
|
|
867
|
+
if (!map) return undefined;
|
|
223
868
|
|
|
224
|
-
|
|
869
|
+
// Handle overloaded signature: attachShape(options) or attachShape(id, options)
|
|
870
|
+
let zoneId: string;
|
|
871
|
+
let shapeOptions: AttachShapeOptions;
|
|
225
872
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
873
|
+
if (typeof idOrOptions === 'string') {
|
|
874
|
+
zoneId = idOrOptions;
|
|
875
|
+
if (!options) {
|
|
876
|
+
console.warn('attachShape: options must be provided when id is specified');
|
|
877
|
+
return undefined;
|
|
878
|
+
}
|
|
879
|
+
shapeOptions = options;
|
|
880
|
+
} else {
|
|
881
|
+
zoneId = `zone-${this.id}-${Date.now()}`;
|
|
882
|
+
shapeOptions = idOrOptions;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Get player entity from physic engine
|
|
886
|
+
const playerEntity = map.physic.getEntityByUUID(this.id);
|
|
887
|
+
if (!playerEntity) {
|
|
888
|
+
console.warn(`Player entity not found in physic engine for player ${this.id}`);
|
|
889
|
+
return undefined;
|
|
890
|
+
}
|
|
230
891
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
892
|
+
// Calculate radius from width/height if not provided
|
|
893
|
+
let radius: number;
|
|
894
|
+
if (shapeOptions.radius !== undefined) {
|
|
895
|
+
radius = shapeOptions.radius;
|
|
896
|
+
} else if (shapeOptions.width && shapeOptions.height) {
|
|
897
|
+
// Use the larger dimension as radius, or calculate from area
|
|
898
|
+
radius = Math.max(shapeOptions.width, shapeOptions.height) / 2;
|
|
899
|
+
} else {
|
|
900
|
+
console.warn('attachShape: radius or width/height must be provided');
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Calculate offset based on positioning
|
|
905
|
+
let offset: Vector2 = new Vector2(0, 0);
|
|
906
|
+
const positioning: ShapePositioning = shapeOptions.positioning || "default";
|
|
907
|
+
if (shapeOptions.positioning) {
|
|
908
|
+
const playerWidth = playerEntity.width || playerEntity.radius * 2 || 32;
|
|
909
|
+
const playerHeight = playerEntity.height || playerEntity.radius * 2 || 32;
|
|
910
|
+
|
|
911
|
+
switch (shapeOptions.positioning) {
|
|
912
|
+
case 'top':
|
|
913
|
+
offset = new Vector2(0, -playerHeight / 2);
|
|
914
|
+
break;
|
|
915
|
+
case 'bottom':
|
|
916
|
+
offset = new Vector2(0, playerHeight / 2);
|
|
917
|
+
break;
|
|
918
|
+
case 'left':
|
|
919
|
+
offset = new Vector2(-playerWidth / 2, 0);
|
|
920
|
+
break;
|
|
921
|
+
case 'right':
|
|
922
|
+
offset = new Vector2(playerWidth / 2, 0);
|
|
923
|
+
break;
|
|
924
|
+
case 'center':
|
|
925
|
+
default:
|
|
926
|
+
offset = new Vector2(0, 0);
|
|
927
|
+
break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Get zone manager and create attached zone
|
|
932
|
+
const zoneManager = map.physic.getZoneManager();
|
|
933
|
+
|
|
934
|
+
// Convert direction from Direction enum to string if needed
|
|
935
|
+
// Direction enum values are already strings ("up", "down", "left", "right")
|
|
936
|
+
let direction: 'up' | 'down' | 'left' | 'right' = 'down';
|
|
937
|
+
if (shapeOptions.direction !== undefined) {
|
|
938
|
+
if (typeof shapeOptions.direction === 'string') {
|
|
939
|
+
direction = shapeOptions.direction as 'up' | 'down' | 'left' | 'right';
|
|
940
|
+
} else {
|
|
941
|
+
// Direction enum value is already a string, just cast it
|
|
942
|
+
direction = String(shapeOptions.direction) as 'up' | 'down' | 'left' | 'right';
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Create zone with metadata for name and properties
|
|
947
|
+
const metadata: Record<string, any> = {};
|
|
948
|
+
if (shapeOptions.name) {
|
|
949
|
+
metadata.name = shapeOptions.name;
|
|
950
|
+
}
|
|
951
|
+
if (shapeOptions.properties) {
|
|
952
|
+
metadata.properties = shapeOptions.properties;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// Get initial position
|
|
956
|
+
const initialX = playerEntity.position.x + offset.x;
|
|
957
|
+
const initialY = playerEntity.position.y + offset.y;
|
|
958
|
+
|
|
959
|
+
const physicZoneId = zoneManager.createAttachedZone(
|
|
960
|
+
playerEntity,
|
|
961
|
+
{
|
|
962
|
+
radius,
|
|
963
|
+
angle: shapeOptions.angle ?? 360,
|
|
964
|
+
direction,
|
|
965
|
+
limitedByWalls: shapeOptions.limitedByWalls ?? false,
|
|
966
|
+
offset,
|
|
967
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
243
968
|
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
event.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
969
|
+
{
|
|
970
|
+
onEnter: (entities: Entity[]) => {
|
|
971
|
+
entities.forEach((entity) => {
|
|
972
|
+
const event = map.getEvent<RpgEvent>(entity.uuid);
|
|
973
|
+
const player = map.getPlayer(entity.uuid);
|
|
974
|
+
|
|
975
|
+
if (event && (!map.isEventVisibleForPlayer || map.isEventVisibleForPlayer(event, this))) {
|
|
976
|
+
event.execMethod("onInShape", [shape, this]);
|
|
977
|
+
// Track that this event is in the shape
|
|
978
|
+
if ((event as any)._inShapes) {
|
|
979
|
+
(event as any)._inShapes.add(shape);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
if (player) {
|
|
983
|
+
this.execMethod("onDetectInShape", [player, shape]);
|
|
984
|
+
// Track that this player is in the shape
|
|
985
|
+
if (player._inShapes) {
|
|
986
|
+
player._inShapes.add(shape);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
},
|
|
991
|
+
onExit: (entities: Entity[]) => {
|
|
992
|
+
entities.forEach((entity) => {
|
|
993
|
+
const event = map.getEvent<RpgEvent>(entity.uuid);
|
|
994
|
+
const player = map.getPlayer(entity.uuid);
|
|
995
|
+
|
|
996
|
+
if (event && (!map.isEventVisibleForPlayer || map.isEventVisibleForPlayer(event, this))) {
|
|
997
|
+
event.execMethod("onOutShape", [shape, this]);
|
|
998
|
+
// Remove from tracking
|
|
999
|
+
if ((event as any)._inShapes) {
|
|
1000
|
+
(event as any)._inShapes.delete(shape);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
if (player) {
|
|
1004
|
+
this.execMethod("onDetectOutShape", [player, shape]);
|
|
1005
|
+
// Remove from tracking
|
|
1006
|
+
if (player._inShapes) {
|
|
1007
|
+
player._inShapes.delete(shape);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
},
|
|
254
1012
|
}
|
|
255
1013
|
);
|
|
1014
|
+
|
|
1015
|
+
// Create RpgShape instance
|
|
1016
|
+
const shape = new RpgShape({
|
|
1017
|
+
name: shapeOptions.name || zoneId,
|
|
1018
|
+
positioning,
|
|
1019
|
+
width: shapeOptions.width || radius * 2,
|
|
1020
|
+
height: shapeOptions.height || radius * 2,
|
|
1021
|
+
x: initialX,
|
|
1022
|
+
y: initialY,
|
|
1023
|
+
properties: shapeOptions.properties || {},
|
|
1024
|
+
playerOwner: this,
|
|
1025
|
+
physicZoneId: physicZoneId,
|
|
1026
|
+
map: map,
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
// Store mapping from zoneId to physicZoneId for future reference
|
|
1030
|
+
(this as any)._zoneIdMap = (this as any)._zoneIdMap || new Map();
|
|
1031
|
+
(this as any)._zoneIdMap.set(zoneId, physicZoneId);
|
|
1032
|
+
|
|
1033
|
+
// Store the shape
|
|
1034
|
+
this._attachedShapes.set(zoneId, shape);
|
|
1035
|
+
|
|
1036
|
+
// Update shape position when player moves
|
|
1037
|
+
const updateShapePosition = () => {
|
|
1038
|
+
const currentEntity = map.physic.getEntityByUUID(this.id);
|
|
1039
|
+
if (currentEntity) {
|
|
1040
|
+
const zoneInfo = zoneManager.getZone(physicZoneId);
|
|
1041
|
+
if (zoneInfo) {
|
|
1042
|
+
shape._updatePosition(zoneInfo.position.x, zoneInfo.position.y);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
// Listen to position changes to update shape position
|
|
1048
|
+
playerEntity.onPositionChange(() => {
|
|
1049
|
+
updateShapePosition();
|
|
1050
|
+
});
|
|
1051
|
+
|
|
1052
|
+
return shape;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Get all shapes attached to this player
|
|
1057
|
+
*
|
|
1058
|
+
* Returns all shapes that were created using `attachShape()` on this player.
|
|
1059
|
+
*
|
|
1060
|
+
* @returns Array of RpgShape instances attached to this player
|
|
1061
|
+
*
|
|
1062
|
+
* @example
|
|
1063
|
+
* ```ts
|
|
1064
|
+
* player.attachShape("vision", { radius: 150 });
|
|
1065
|
+
* player.attachShape("detection", { radius: 100 });
|
|
1066
|
+
*
|
|
1067
|
+
* const shapes = player.getShapes();
|
|
1068
|
+
* console.log(shapes.length); // 2
|
|
1069
|
+
* ```
|
|
1070
|
+
*/
|
|
1071
|
+
getShapes(): RpgShape[] {
|
|
1072
|
+
return Array.from(this._attachedShapes.values());
|
|
256
1073
|
}
|
|
257
1074
|
|
|
258
|
-
|
|
1075
|
+
/**
|
|
1076
|
+
* Get all shapes where this player is currently located
|
|
1077
|
+
*
|
|
1078
|
+
* Returns all shapes (from any player/event) where this player is currently inside.
|
|
1079
|
+
* This is updated automatically when the player enters or exits shapes.
|
|
1080
|
+
*
|
|
1081
|
+
* @returns Array of RpgShape instances where this player is located
|
|
1082
|
+
*
|
|
1083
|
+
* @example
|
|
1084
|
+
* ```ts
|
|
1085
|
+
* // Another player has a detection zone
|
|
1086
|
+
* otherPlayer.attachShape("detection", { radius: 200 });
|
|
1087
|
+
*
|
|
1088
|
+
* // Check if this player is in any shape
|
|
1089
|
+
* const inShapes = player.getInShapes();
|
|
1090
|
+
* if (inShapes.length > 0) {
|
|
1091
|
+
* console.log("Player is being detected!");
|
|
1092
|
+
* }
|
|
1093
|
+
* ```
|
|
1094
|
+
*/
|
|
1095
|
+
getInShapes(): RpgShape[] {
|
|
1096
|
+
return Array.from(this._inShapes);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
/**
|
|
1100
|
+
* Show a temporary component animation on this player
|
|
1101
|
+
*
|
|
1102
|
+
* This method broadcasts a component animation to all clients, allowing
|
|
1103
|
+
* temporary visual effects like hit indicators, spell effects, or status animations
|
|
1104
|
+
* to be displayed on the player.
|
|
1105
|
+
*
|
|
1106
|
+
* @param id - The ID of the component animation to display
|
|
1107
|
+
* @param params - Parameters to pass to the component animation
|
|
1108
|
+
*
|
|
1109
|
+
* @example
|
|
1110
|
+
* ```ts
|
|
1111
|
+
* // Show a hit animation with damage text
|
|
1112
|
+
* player.showComponentAnimation("hit", {
|
|
1113
|
+
* text: "150",
|
|
1114
|
+
* color: "red"
|
|
1115
|
+
* });
|
|
1116
|
+
*
|
|
1117
|
+
* // Show a heal animation
|
|
1118
|
+
* player.showComponentAnimation("heal", {
|
|
1119
|
+
* amount: 50
|
|
1120
|
+
* });
|
|
1121
|
+
* ```
|
|
1122
|
+
*/
|
|
1123
|
+
showComponentAnimation(id: string, params: any = {}) {
|
|
259
1124
|
const map = this.getCurrentMap();
|
|
260
1125
|
if (!map) return;
|
|
261
1126
|
map.$broadcast({
|
|
262
|
-
type: "
|
|
1127
|
+
type: "showComponentAnimation",
|
|
263
1128
|
value: {
|
|
264
1129
|
id,
|
|
265
1130
|
params,
|
|
@@ -269,17 +1134,333 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
269
1134
|
}
|
|
270
1135
|
|
|
271
1136
|
showHit(text: string) {
|
|
272
|
-
this.
|
|
1137
|
+
this.showComponentAnimation("hit", {
|
|
273
1138
|
text,
|
|
274
1139
|
direction: this.direction(),
|
|
275
1140
|
});
|
|
276
1141
|
}
|
|
1142
|
+
|
|
1143
|
+
/**
|
|
1144
|
+
* Play a sound on the client side for this player only
|
|
1145
|
+
*
|
|
1146
|
+
* This method emits an event to play a sound only for this specific player.
|
|
1147
|
+
* The sound must be defined on the client side (in the client module configuration).
|
|
1148
|
+
*
|
|
1149
|
+
* ## Design
|
|
1150
|
+
*
|
|
1151
|
+
* The sound is sent only to this player's client connection, making it ideal
|
|
1152
|
+
* for personal feedback sounds like UI interactions, notifications, or personal
|
|
1153
|
+
* achievements. For map-wide sounds that all players should hear, use `map.playSound()` instead.
|
|
1154
|
+
*
|
|
1155
|
+
* @param soundId - Sound identifier, defined on the client side
|
|
1156
|
+
* @param options - Optional sound configuration
|
|
1157
|
+
* @param options.volume - Volume level (0.0 to 1.0, default: 1.0)
|
|
1158
|
+
* @param options.loop - Whether the sound should loop (default: false)
|
|
1159
|
+
*
|
|
1160
|
+
* @example
|
|
1161
|
+
* ```ts
|
|
1162
|
+
* // Play a sound for this player only (default behavior)
|
|
1163
|
+
* player.playSound("item-pickup");
|
|
1164
|
+
*
|
|
1165
|
+
* // Play a sound with volume and loop
|
|
1166
|
+
* player.playSound("background-music", {
|
|
1167
|
+
* volume: 0.5,
|
|
1168
|
+
* loop: true
|
|
1169
|
+
* });
|
|
1170
|
+
*
|
|
1171
|
+
* // Play a notification sound at low volume
|
|
1172
|
+
* player.playSound("notification", { volume: 0.3 });
|
|
1173
|
+
* ```
|
|
1174
|
+
*/
|
|
1175
|
+
playSound(soundId: string, options?: { volume?: number; loop?: boolean }): void {
|
|
1176
|
+
const map = this.getCurrentMap();
|
|
1177
|
+
if (!map) return;
|
|
1178
|
+
|
|
1179
|
+
const data: any = {
|
|
1180
|
+
soundId,
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
if (options) {
|
|
1184
|
+
if (options.volume !== undefined) {
|
|
1185
|
+
data.volume = Math.max(0, Math.min(1, options.volume));
|
|
1186
|
+
}
|
|
1187
|
+
if (options.loop !== undefined) {
|
|
1188
|
+
data.loop = options.loop;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Send only to this player
|
|
1193
|
+
this.emit("playSound", data);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Stop a sound that is currently playing for this player
|
|
1198
|
+
*
|
|
1199
|
+
* This method stops a sound that was previously started with `playSound()`.
|
|
1200
|
+
* The sound must be defined on the client side.
|
|
1201
|
+
*
|
|
1202
|
+
* @param soundId - Sound identifier to stop
|
|
1203
|
+
*
|
|
1204
|
+
* @example
|
|
1205
|
+
* ```ts
|
|
1206
|
+
* // Start a looping background music
|
|
1207
|
+
* player.playSound("background-music", { loop: true });
|
|
1208
|
+
*
|
|
1209
|
+
* // Later, stop it
|
|
1210
|
+
* player.stopSound("background-music");
|
|
1211
|
+
* ```
|
|
1212
|
+
*/
|
|
1213
|
+
stopSound(soundId: string): void {
|
|
1214
|
+
const map = this.getCurrentMap();
|
|
1215
|
+
if (!map) return;
|
|
1216
|
+
|
|
1217
|
+
const data = {
|
|
1218
|
+
soundId,
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
// Send stop command only to this player
|
|
1222
|
+
this.emit("stopSound", data);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
/**
|
|
1226
|
+
* Stop all currently playing sounds for this player
|
|
1227
|
+
*
|
|
1228
|
+
* This method stops all sounds that are currently playing for the player.
|
|
1229
|
+
* Useful when changing maps to prevent sound overlap.
|
|
1230
|
+
*
|
|
1231
|
+
* @example
|
|
1232
|
+
* ```ts
|
|
1233
|
+
* // Stop all sounds before changing map
|
|
1234
|
+
* player.stopAllSounds();
|
|
1235
|
+
* await player.changeMap("new-map");
|
|
1236
|
+
* ```
|
|
1237
|
+
*/
|
|
1238
|
+
stopAllSounds(): void {
|
|
1239
|
+
const map = this.getCurrentMap();
|
|
1240
|
+
if (!map) return;
|
|
1241
|
+
|
|
1242
|
+
// Send stop all command only to this player
|
|
1243
|
+
this.emit("stopAllSounds", {});
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* Make the camera follow another player or event
|
|
1248
|
+
*
|
|
1249
|
+
* This method sends an instruction to the client to fix the viewport on another sprite.
|
|
1250
|
+
* The camera will follow the specified player or event, with optional smooth animation.
|
|
1251
|
+
*
|
|
1252
|
+
* ## Design
|
|
1253
|
+
*
|
|
1254
|
+
* The camera follow instruction is sent only to this player's client connection.
|
|
1255
|
+
* This allows each player to have their own camera target, useful for cutscenes,
|
|
1256
|
+
* following NPCs, or focusing on specific events.
|
|
1257
|
+
*
|
|
1258
|
+
* @param otherPlayer - The player or event that the camera should follow
|
|
1259
|
+
* @param options - Camera follow options
|
|
1260
|
+
* @param options.smoothMove - Enable smooth animation. Can be a boolean (default: true) or an object with animation parameters
|
|
1261
|
+
* @param options.smoothMove.time - Time duration for the animation in milliseconds (optional)
|
|
1262
|
+
* @param options.smoothMove.ease - Easing function name. Visit https://easings.net for available functions (optional)
|
|
1263
|
+
*
|
|
1264
|
+
* @example
|
|
1265
|
+
* ```ts
|
|
1266
|
+
* // Follow another player with default smooth animation
|
|
1267
|
+
* player.cameraFollow(otherPlayer, { smoothMove: true });
|
|
1268
|
+
*
|
|
1269
|
+
* // Follow an event with custom smooth animation
|
|
1270
|
+
* player.cameraFollow(npcEvent, {
|
|
1271
|
+
* smoothMove: {
|
|
1272
|
+
* time: 1000,
|
|
1273
|
+
* ease: "easeInOutQuad"
|
|
1274
|
+
* }
|
|
1275
|
+
* });
|
|
1276
|
+
*
|
|
1277
|
+
* // Follow without animation (instant)
|
|
1278
|
+
* player.cameraFollow(targetPlayer, { smoothMove: false });
|
|
1279
|
+
* ```
|
|
1280
|
+
*/
|
|
1281
|
+
cameraFollow(
|
|
1282
|
+
otherPlayer: RpgPlayer | RpgEvent,
|
|
1283
|
+
options?: {
|
|
1284
|
+
smoothMove?: boolean | { time?: number; ease?: string };
|
|
1285
|
+
}
|
|
1286
|
+
): void {
|
|
1287
|
+
const map = this.getCurrentMap();
|
|
1288
|
+
if (!map) return;
|
|
1289
|
+
|
|
1290
|
+
const data: any = {
|
|
1291
|
+
targetId: otherPlayer.id,
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
// Handle smoothMove option
|
|
1295
|
+
if (options?.smoothMove !== undefined) {
|
|
1296
|
+
if (typeof options.smoothMove === "boolean") {
|
|
1297
|
+
data.smoothMove = options.smoothMove;
|
|
1298
|
+
} else {
|
|
1299
|
+
// smoothMove is an object
|
|
1300
|
+
data.smoothMove = {
|
|
1301
|
+
enabled: true,
|
|
1302
|
+
...options.smoothMove,
|
|
1303
|
+
};
|
|
1304
|
+
}
|
|
1305
|
+
} else {
|
|
1306
|
+
// Default to true if not specified
|
|
1307
|
+
data.smoothMove = true;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Send camera follow instruction only to this player
|
|
1311
|
+
this.emit("cameraFollow", data);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Trigger a flash animation on this player
|
|
1317
|
+
*
|
|
1318
|
+
* This method sends a flash animation event to the client, creating a visual
|
|
1319
|
+
* feedback effect on the player's sprite. The flash can be configured with
|
|
1320
|
+
* various options including type (alpha, tint, or both), duration, cycles, and color.
|
|
1321
|
+
*
|
|
1322
|
+
* ## Design
|
|
1323
|
+
*
|
|
1324
|
+
* The flash is sent as a broadcast event to all clients viewing this player.
|
|
1325
|
+
* This is useful for visual feedback when the player takes damage, receives
|
|
1326
|
+
* a buff, or when an important event occurs.
|
|
1327
|
+
*
|
|
1328
|
+
* @param options - Flash configuration options
|
|
1329
|
+
* @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')
|
|
1330
|
+
* @param options.duration - Duration of the flash animation in milliseconds (default: 300)
|
|
1331
|
+
* @param options.cycles - Number of flash cycles (flash on/off) (default: 1)
|
|
1332
|
+
* @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)
|
|
1333
|
+
* @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)
|
|
1334
|
+
*
|
|
1335
|
+
* @example
|
|
1336
|
+
* ```ts
|
|
1337
|
+
* // Simple flash with default settings (alpha flash)
|
|
1338
|
+
* player.flash();
|
|
1339
|
+
*
|
|
1340
|
+
* // Flash with red tint when taking damage
|
|
1341
|
+
* player.flash({ type: 'tint', tint: 0xff0000 });
|
|
1342
|
+
*
|
|
1343
|
+
* // Flash with both alpha and tint for dramatic effect
|
|
1344
|
+
* player.flash({
|
|
1345
|
+
* type: 'both',
|
|
1346
|
+
* alpha: 0.5,
|
|
1347
|
+
* tint: 0xff0000,
|
|
1348
|
+
* duration: 200,
|
|
1349
|
+
* cycles: 2
|
|
1350
|
+
* });
|
|
1351
|
+
*
|
|
1352
|
+
* // Quick damage flash
|
|
1353
|
+
* player.flash({
|
|
1354
|
+
* type: 'tint',
|
|
1355
|
+
* tint: 'red',
|
|
1356
|
+
* duration: 150,
|
|
1357
|
+
* cycles: 1
|
|
1358
|
+
* });
|
|
1359
|
+
* ```
|
|
1360
|
+
*/
|
|
1361
|
+
flash(options?: {
|
|
1362
|
+
type?: 'alpha' | 'tint' | 'both';
|
|
1363
|
+
duration?: number;
|
|
1364
|
+
cycles?: number;
|
|
1365
|
+
alpha?: number;
|
|
1366
|
+
tint?: number | string;
|
|
1367
|
+
}): void {
|
|
1368
|
+
const map = this.getCurrentMap();
|
|
1369
|
+
if (!map) return;
|
|
1370
|
+
|
|
1371
|
+
const flashOptions = {
|
|
1372
|
+
type: options?.type || 'alpha',
|
|
1373
|
+
duration: options?.duration ?? 300,
|
|
1374
|
+
cycles: options?.cycles ?? 1,
|
|
1375
|
+
alpha: options?.alpha ?? 0.3,
|
|
1376
|
+
tint: options?.tint ?? 0xffffff,
|
|
1377
|
+
};
|
|
1378
|
+
|
|
1379
|
+
map.$broadcast({
|
|
1380
|
+
type: "flash",
|
|
1381
|
+
value: {
|
|
1382
|
+
object: this.id,
|
|
1383
|
+
...flashOptions,
|
|
1384
|
+
},
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* Set the hitbox of the player for collision detection
|
|
1390
|
+
*
|
|
1391
|
+
* This method defines the hitbox used for collision detection in the physics engine.
|
|
1392
|
+
* The hitbox can be smaller or larger than the visual representation of the player,
|
|
1393
|
+
* allowing for precise collision detection.
|
|
1394
|
+
*
|
|
1395
|
+
* ## Design
|
|
1396
|
+
*
|
|
1397
|
+
* The hitbox is used by the physics engine to detect collisions with other entities,
|
|
1398
|
+
* static obstacles, and shapes. Changing the hitbox will immediately update the
|
|
1399
|
+
* collision detection without affecting the visual appearance of the player.
|
|
1400
|
+
*
|
|
1401
|
+
* @param width - Width of the hitbox in pixels
|
|
1402
|
+
* @param height - Height of the hitbox in pixels
|
|
1403
|
+
*
|
|
1404
|
+
* @example
|
|
1405
|
+
* ```ts
|
|
1406
|
+
* // Set a 20x20 hitbox for precise collision detection
|
|
1407
|
+
* player.setHitbox(20, 20);
|
|
1408
|
+
*
|
|
1409
|
+
* // Set a larger hitbox for easier collision detection
|
|
1410
|
+
* player.setHitbox(40, 40);
|
|
1411
|
+
* ```
|
|
1412
|
+
*/
|
|
1413
|
+
setHitbox(width: number, height: number): void {
|
|
1414
|
+
// Validate inputs
|
|
1415
|
+
if (typeof width !== 'number' || width <= 0) {
|
|
1416
|
+
throw new Error('setHitbox: width must be a positive number');
|
|
1417
|
+
}
|
|
1418
|
+
if (typeof height !== 'number' || height <= 0) {
|
|
1419
|
+
throw new Error('setHitbox: height must be a positive number');
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// Update hitbox signal
|
|
1423
|
+
this.hitbox.set({
|
|
1424
|
+
w: width,
|
|
1425
|
+
h: height,
|
|
1426
|
+
});
|
|
1427
|
+
|
|
1428
|
+
// Update physics entity if map exists
|
|
1429
|
+
const map = this.getCurrentMap();
|
|
1430
|
+
if (map && map.physic) {
|
|
1431
|
+
const topLeftX = this.x();
|
|
1432
|
+
const topLeftY = this.y();
|
|
1433
|
+
map.updateHitbox(this.id, topLeftX, topLeftY, width, height);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
/**
|
|
1438
|
+
* Set the sync schema for the map
|
|
1439
|
+
* @param schema - The schema to set
|
|
1440
|
+
*/
|
|
1441
|
+
setSync(schema: any) {
|
|
1442
|
+
for (let key in schema) {
|
|
1443
|
+
this[key] = type(signal(null), key, {
|
|
1444
|
+
syncWithClient: schema[key]?.$syncWithClient,
|
|
1445
|
+
persist: schema[key]?.$permanent,
|
|
1446
|
+
}, this)
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
isEvent(): boolean {
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
277
1453
|
}
|
|
278
1454
|
|
|
279
1455
|
export class RpgEvent extends RpgPlayer {
|
|
1456
|
+
|
|
1457
|
+
constructor() {
|
|
1458
|
+
super();
|
|
1459
|
+
this.initializeDefaultStats()
|
|
1460
|
+
}
|
|
1461
|
+
|
|
280
1462
|
override async execMethod(methodName: string, methodData: any[] = [], instance = this) {
|
|
281
|
-
|
|
282
|
-
await lastValueFrom(hooks
|
|
1463
|
+
await lastValueFrom(this.hooks
|
|
283
1464
|
.callHooks(`server-event-${methodName}`, instance, ...methodData));
|
|
284
1465
|
if (!instance[methodName]) {
|
|
285
1466
|
return;
|
|
@@ -288,11 +1469,25 @@ export class RpgEvent extends RpgPlayer {
|
|
|
288
1469
|
return ret;
|
|
289
1470
|
}
|
|
290
1471
|
|
|
1472
|
+
/**
|
|
1473
|
+
* Remove this event from the map
|
|
1474
|
+
*
|
|
1475
|
+
* Stops all movements before removing to prevent "unable to resolve entity" errors
|
|
1476
|
+
* from the MovementManager when the entity is destroyed while moving.
|
|
1477
|
+
*/
|
|
291
1478
|
remove() {
|
|
292
1479
|
const map = this.getCurrentMap();
|
|
293
1480
|
if (!map) return;
|
|
1481
|
+
|
|
1482
|
+
// Stop all movements before removing to prevent MovementManager errors
|
|
1483
|
+
this.stopMoveTo();
|
|
1484
|
+
|
|
294
1485
|
map.removeEvent(this.id);
|
|
295
1486
|
}
|
|
1487
|
+
|
|
1488
|
+
override isEvent(): boolean {
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
296
1491
|
}
|
|
297
1492
|
|
|
298
1493
|
|
|
@@ -302,18 +1497,17 @@ export class RpgEvent extends RpgPlayer {
|
|
|
302
1497
|
* Extends the RpgPlayer class with additional interfaces from mixins.
|
|
303
1498
|
* This provides proper TypeScript support for all mixin methods and properties.
|
|
304
1499
|
*/
|
|
305
|
-
export interface RpgPlayer extends
|
|
306
|
-
IVariableManager,
|
|
307
|
-
IMoveManager,
|
|
308
|
-
IGoldManager,
|
|
309
|
-
IComponentManager,
|
|
310
|
-
IGuiManager,
|
|
311
|
-
IItemManager,
|
|
312
|
-
IEffectManager,
|
|
313
|
-
IParameterManager,
|
|
314
|
-
IElementManager,
|
|
315
|
-
ISkillManager,
|
|
316
|
-
IBattleManager,
|
|
317
|
-
IClassManager,
|
|
318
|
-
IStateManager
|
|
319
|
-
{}
|
|
1500
|
+
export interface RpgPlayer extends
|
|
1501
|
+
IVariableManager,
|
|
1502
|
+
IMoveManager,
|
|
1503
|
+
IGoldManager,
|
|
1504
|
+
IComponentManager,
|
|
1505
|
+
IGuiManager,
|
|
1506
|
+
IItemManager,
|
|
1507
|
+
IEffectManager,
|
|
1508
|
+
IParameterManager,
|
|
1509
|
+
IElementManager,
|
|
1510
|
+
ISkillManager,
|
|
1511
|
+
IBattleManager,
|
|
1512
|
+
IClassManager,
|
|
1513
|
+
IStateManager { }
|