@rpgjs/server 5.0.0-alpha.2 → 5.0.0-alpha.20
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 +4 -0
- package/dist/Gui/index.d.ts +1 -0
- package/dist/Player/BattleManager.d.ts +32 -22
- package/dist/Player/ClassManager.d.ts +31 -18
- package/dist/Player/ComponentManager.d.ts +123 -0
- package/dist/Player/Components.d.ts +345 -0
- package/dist/Player/EffectManager.d.ts +40 -0
- package/dist/Player/ElementManager.d.ts +31 -0
- package/dist/Player/GoldManager.d.ts +22 -0
- package/dist/Player/GuiManager.d.ts +176 -0
- package/dist/Player/ItemFixture.d.ts +6 -0
- package/dist/Player/ItemManager.d.ts +164 -10
- package/dist/Player/MoveManager.d.ts +32 -44
- package/dist/Player/ParameterManager.d.ts +343 -14
- package/dist/Player/Player.d.ts +266 -8
- package/dist/Player/SkillManager.d.ts +27 -19
- package/dist/Player/StateManager.d.ts +28 -35
- package/dist/Player/VariableManager.d.ts +30 -0
- package/dist/RpgServer.d.ts +227 -1
- package/dist/decorators/event.d.ts +46 -0
- package/dist/decorators/map.d.ts +177 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +17436 -18167
- package/dist/index.js.map +1 -1
- package/dist/rooms/map.d.ts +486 -8
- package/package.json +17 -15
- package/src/Gui/DialogGui.ts +7 -2
- package/src/Gui/index.ts +3 -1
- package/src/Player/BattleManager.ts +97 -38
- package/src/Player/ClassManager.ts +95 -35
- package/src/Player/ComponentManager.ts +425 -19
- package/src/Player/Components.ts +380 -0
- package/src/Player/EffectManager.ts +110 -27
- package/src/Player/ElementManager.ts +126 -25
- package/src/Player/GoldManager.ts +32 -35
- package/src/Player/GuiManager.ts +187 -140
- package/src/Player/ItemFixture.ts +4 -5
- package/src/Player/ItemManager.ts +363 -48
- package/src/Player/MoveManager.ts +323 -308
- package/src/Player/ParameterManager.ts +499 -99
- package/src/Player/Player.ts +719 -80
- package/src/Player/SkillManager.ts +44 -23
- package/src/Player/StateManager.ts +210 -95
- package/src/Player/VariableManager.ts +180 -48
- package/src/RpgServer.ts +236 -1
- package/src/core/context.ts +1 -0
- package/src/decorators/event.ts +61 -0
- package/src/decorators/map.ts +198 -0
- package/src/index.ts +5 -1
- package/src/module.ts +24 -0
- package/src/rooms/map.ts +1054 -54
- package/dist/Player/Event.d.ts +0 -0
- package/src/Player/Event.ts +0 -0
package/src/Player/Player.ts
CHANGED
|
@@ -5,33 +5,36 @@ import {
|
|
|
5
5
|
RpgCommonPlayer,
|
|
6
6
|
ShowAnimationParams,
|
|
7
7
|
Constructor,
|
|
8
|
-
|
|
8
|
+
Direction,
|
|
9
|
+
AttachShapeOptions,
|
|
10
|
+
RpgShape,
|
|
11
|
+
ShapePositioning,
|
|
9
12
|
} from "@rpgjs/common";
|
|
10
|
-
import {
|
|
13
|
+
import { Entity, Vector2 } from "@rpgjs/physic";
|
|
14
|
+
import { IComponentManager, WithComponentManager } from "./ComponentManager";
|
|
11
15
|
import { RpgMap } from "../rooms/map";
|
|
12
16
|
import { Context, inject } from "@signe/di";
|
|
13
17
|
import { IGuiManager, WithGuiManager } from "./GuiManager";
|
|
14
18
|
import { MockConnection } from "@signe/room";
|
|
15
19
|
import { IMoveManager, WithMoveManager } from "./MoveManager";
|
|
16
20
|
import { IGoldManager, WithGoldManager } from "./GoldManager";
|
|
17
|
-
import {
|
|
18
|
-
import { sync } from "@signe/sync";
|
|
19
|
-
import { signal } from "@signe/reactive";
|
|
21
|
+
import { WithVariableManager, type IVariableManager } from "./VariableManager";
|
|
22
|
+
import { createStatesSnapshot, load, sync, type } from "@signe/sync";
|
|
23
|
+
import { computed, signal } from "@signe/reactive";
|
|
20
24
|
import {
|
|
21
|
-
|
|
25
|
+
IParameterManager,
|
|
22
26
|
WithParameterManager,
|
|
23
27
|
} from "./ParameterManager";
|
|
24
28
|
import { WithItemFixture } from "./ItemFixture";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import { WithBattleManager } from "./BattleManager";
|
|
29
|
-
import { WithEffectManager } from "./EffectManager";
|
|
30
|
-
import { WithSkillManager, IWithSkillManager } from "./SkillManager";
|
|
29
|
+
import { IItemManager, WithItemManager } from "./ItemManager";
|
|
30
|
+
import { bufferTime, combineLatest, debounceTime, distinctUntilChanged, filter, lastValueFrom, map, Observable, pairwise, sample, throttleTime } from "rxjs";
|
|
31
|
+
import { IEffectManager, WithEffectManager } from "./EffectManager";
|
|
31
32
|
import { AGI, AGI_CURVE, DEX, DEX_CURVE, INT, INT_CURVE, MAXHP, MAXHP_CURVE, MAXSP, MAXSP_CURVE, STR, STR_CURVE } from "../presets";
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
|
|
33
|
+
import { IElementManager, WithElementManager } from "./ElementManager";
|
|
34
|
+
import { ISkillManager, WithSkillManager } from "./SkillManager";
|
|
35
|
+
import { IBattleManager, WithBattleManager } from "./BattleManager";
|
|
36
|
+
import { IClassManager, WithClassManager } from "./ClassManager";
|
|
37
|
+
import { IStateManager, WithStateManager } from "./StateManager";
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* Combines multiple RpgCommonPlayer mixins into one
|
|
@@ -46,49 +49,132 @@ function combinePlayerMixins<T extends Constructor<RpgCommonPlayer>>(
|
|
|
46
49
|
mixins.reduce((ExtendedClass, mixin) => mixin(ExtendedClass), Base);
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
// Start with basic mixins that work
|
|
53
|
+
const BasicPlayerMixins = combinePlayerMixins([
|
|
50
54
|
WithComponentManager,
|
|
51
55
|
WithEffectManager,
|
|
52
56
|
WithGuiManager,
|
|
53
57
|
WithMoveManager,
|
|
54
58
|
WithGoldManager,
|
|
55
|
-
WithVariableManager,
|
|
56
59
|
WithParameterManager,
|
|
57
60
|
WithItemFixture,
|
|
58
|
-
WithStateManager,
|
|
59
61
|
WithItemManager,
|
|
60
|
-
|
|
62
|
+
WithElementManager,
|
|
63
|
+
WithVariableManager,
|
|
64
|
+
WithStateManager,
|
|
61
65
|
WithClassManager,
|
|
66
|
+
WithSkillManager,
|
|
62
67
|
WithBattleManager,
|
|
63
|
-
WithElementManager,
|
|
64
68
|
]);
|
|
65
69
|
|
|
66
70
|
/**
|
|
67
71
|
* RPG Player class with component management capabilities
|
|
72
|
+
*
|
|
73
|
+
* Combines all player mixins to provide a complete player implementation
|
|
74
|
+
* with graphics, movement, inventory, skills, and battle capabilities.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* // Create a new player
|
|
79
|
+
* const player = new RpgPlayer();
|
|
80
|
+
*
|
|
81
|
+
* // Set player graphics
|
|
82
|
+
* player.setGraphic("hero");
|
|
83
|
+
*
|
|
84
|
+
* // Add parameters and items
|
|
85
|
+
* player.addParameter("strength", { start: 10, end: 100 });
|
|
86
|
+
* player.addItem(sword);
|
|
87
|
+
* ```
|
|
68
88
|
*/
|
|
69
|
-
export class RpgPlayer extends
|
|
89
|
+
export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
70
90
|
map: RpgMap | null = null;
|
|
71
91
|
context?: Context;
|
|
72
92
|
conn: MockConnection | null = null;
|
|
93
|
+
touchSide: boolean = false; // Protection against map change loops
|
|
94
|
+
|
|
95
|
+
/** Internal: Shapes attached to this player */
|
|
96
|
+
private _attachedShapes: Map<string, RpgShape> = new Map();
|
|
97
|
+
|
|
98
|
+
/** Internal: Shapes where this player is currently located */
|
|
99
|
+
private _inShapes: Set<RpgShape> = new Set();
|
|
100
|
+
/** Last processed client input timestamp for reconciliation */
|
|
101
|
+
lastProcessedInputTs: number = 0;
|
|
102
|
+
/** Last processed client input frame for reconciliation with server tick */
|
|
103
|
+
_lastFramePositions: {
|
|
104
|
+
frame: number;
|
|
105
|
+
position: {
|
|
106
|
+
x: number;
|
|
107
|
+
y: number;
|
|
108
|
+
direction: Direction;
|
|
109
|
+
};
|
|
110
|
+
serverTick?: number; // Server tick at which this position was computed
|
|
111
|
+
} | null = null;
|
|
112
|
+
|
|
113
|
+
frames: { x: number; y: number; ts: number }[] = [];
|
|
73
114
|
|
|
74
115
|
@sync(RpgPlayer) events = signal<RpgEvent[]>([]);
|
|
75
116
|
|
|
76
117
|
constructor() {
|
|
77
118
|
super();
|
|
78
|
-
|
|
119
|
+
// Use type assertion to access mixin properties
|
|
120
|
+
(this as any).expCurve = {
|
|
79
121
|
basis: 30,
|
|
80
122
|
extra: 20,
|
|
81
123
|
accelerationA: 30,
|
|
82
124
|
accelerationB: 30
|
|
83
|
-
}
|
|
125
|
+
};
|
|
84
126
|
|
|
85
|
-
this.addParameter(MAXHP, MAXHP_CURVE)
|
|
86
|
-
this.addParameter(MAXSP, MAXSP_CURVE)
|
|
87
|
-
this.addParameter(STR, STR_CURVE)
|
|
88
|
-
this.addParameter(INT, INT_CURVE)
|
|
89
|
-
this.addParameter(DEX, DEX_CURVE)
|
|
90
|
-
this.addParameter(AGI, AGI_CURVE)
|
|
91
|
-
this.allRecovery()
|
|
127
|
+
(this as any).addParameter(MAXHP, MAXHP_CURVE);
|
|
128
|
+
(this as any).addParameter(MAXSP, MAXSP_CURVE);
|
|
129
|
+
(this as any).addParameter(STR, STR_CURVE);
|
|
130
|
+
(this as any).addParameter(INT, INT_CURVE);
|
|
131
|
+
(this as any).addParameter(DEX, DEX_CURVE);
|
|
132
|
+
(this as any).addParameter(AGI, AGI_CURVE);
|
|
133
|
+
(this as any).allRecovery();
|
|
134
|
+
|
|
135
|
+
let lastEmitted: { x: number; y: number } | null = null;
|
|
136
|
+
let pendingUpdate: { x: number; y: number } | null = null;
|
|
137
|
+
let updateScheduled = false;
|
|
138
|
+
|
|
139
|
+
combineLatest([this.x.observable, this.y.observable])
|
|
140
|
+
.subscribe(([x, y]) => {
|
|
141
|
+
pendingUpdate = { x, y };
|
|
142
|
+
|
|
143
|
+
// Schedule a synchronous update using queueMicrotask
|
|
144
|
+
// This groups multiple rapid changes (x and y in the same tick) into a single frame
|
|
145
|
+
if (!updateScheduled) {
|
|
146
|
+
updateScheduled = true;
|
|
147
|
+
queueMicrotask(() => {
|
|
148
|
+
if (pendingUpdate) {
|
|
149
|
+
const { x, y } = pendingUpdate;
|
|
150
|
+
// Only emit if the values are different from the last emitted frame
|
|
151
|
+
if (!lastEmitted || lastEmitted.x !== x || lastEmitted.y !== y) {
|
|
152
|
+
this.frames = [...this.frames, {
|
|
153
|
+
x: x,
|
|
154
|
+
y: y,
|
|
155
|
+
ts: Date.now(),
|
|
156
|
+
}];
|
|
157
|
+
lastEmitted = { x, y };
|
|
158
|
+
}
|
|
159
|
+
pendingUpdate = null;
|
|
160
|
+
}
|
|
161
|
+
updateScheduled = false;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
_onInit() {
|
|
168
|
+
this.hooks.callHooks("server-playerProps-load", this).subscribe();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
get hooks() {
|
|
172
|
+
return inject<Hooks>(this.context as any, ModulesToken);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
applyFrames() {
|
|
176
|
+
this._frames.set(this.frames)
|
|
177
|
+
this.frames = []
|
|
92
178
|
}
|
|
93
179
|
|
|
94
180
|
async execMethod(method: string, methodData: any[] = [], target?: any) {
|
|
@@ -97,8 +183,7 @@ export class RpgPlayer extends PlayerMixins(RpgCommonPlayer) {
|
|
|
97
183
|
ret = await target[method](...methodData);
|
|
98
184
|
}
|
|
99
185
|
else {
|
|
100
|
-
|
|
101
|
-
ret = await lastValueFrom(hooks
|
|
186
|
+
ret = await lastValueFrom(this.hooks
|
|
102
187
|
.callHooks(`server-player-${method}`, target ?? this, ...methodData));
|
|
103
188
|
}
|
|
104
189
|
this.syncChanges()
|
|
@@ -125,17 +210,154 @@ export class RpgPlayer extends PlayerMixins(RpgCommonPlayer) {
|
|
|
125
210
|
mapId: string,
|
|
126
211
|
positions?: { x: number; y: number; z?: number } | string
|
|
127
212
|
): Promise<any | null | boolean> {
|
|
213
|
+
const realMapId = 'map-' + mapId;
|
|
214
|
+
const room = this.getCurrentMap();
|
|
215
|
+
|
|
216
|
+
const canChange: boolean[] = await lastValueFrom(this.hooks.callHooks("server-player-canChangeMap", this, {
|
|
217
|
+
id: mapId,
|
|
218
|
+
}));
|
|
219
|
+
if (canChange.some(v => v === false)) return false;
|
|
220
|
+
|
|
221
|
+
if (positions && typeof positions === 'object') {
|
|
222
|
+
this.teleport(positions)
|
|
223
|
+
}
|
|
224
|
+
await room?.$sessionTransfer(this.conn, realMapId);
|
|
128
225
|
this.emit("changeMap", {
|
|
129
|
-
mapId:
|
|
226
|
+
mapId: realMapId,
|
|
130
227
|
positions,
|
|
131
228
|
});
|
|
132
229
|
return true;
|
|
133
230
|
}
|
|
134
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Auto change map when player touches map borders
|
|
234
|
+
*
|
|
235
|
+
* This method checks if the player touches the current map borders
|
|
236
|
+
* and automatically performs a change to the adjacent map if it exists.
|
|
237
|
+
*
|
|
238
|
+
* @param nextPosition - The next position of the player
|
|
239
|
+
* @returns Promise<boolean> - true if a map change occurred
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```ts
|
|
243
|
+
* // Called automatically by the movement system
|
|
244
|
+
* const changed = await player.autoChangeMap({ x: newX, y: newY });
|
|
245
|
+
* if (changed) {
|
|
246
|
+
* console.log('Player changed map automatically');
|
|
247
|
+
* }
|
|
248
|
+
* ```
|
|
249
|
+
*/
|
|
250
|
+
async autoChangeMap(nextPosition: { x: number; y: number }, forcedDirection?: any): Promise<boolean> {
|
|
251
|
+
const map = this.getCurrentMap() as RpgMap; // Cast to access extended properties
|
|
252
|
+
if (!map) return false;
|
|
253
|
+
|
|
254
|
+
const worldMaps = map.getWorldMapsManager?.();
|
|
255
|
+
let ret: boolean = false;
|
|
256
|
+
|
|
257
|
+
if (worldMaps && map) {
|
|
258
|
+
const direction = forcedDirection ?? this.getDirection();
|
|
259
|
+
const marginLeftRight = (map.tileWidth ?? 32) / 2;
|
|
260
|
+
const marginTopDown = (map.tileHeight ?? 32) / 2;
|
|
261
|
+
|
|
262
|
+
// Current world position of the player
|
|
263
|
+
const worldPositionX = (map.worldX ?? 0) + this.x();
|
|
264
|
+
const worldPositionY = (map.worldY ?? 0) + this.y();
|
|
265
|
+
|
|
266
|
+
const changeMap = async (adjacentCoords: {x: number, y: number}, positionCalculator: (nextMapInfo: any) => {x: number, y: number}) => {
|
|
267
|
+
if (this.touchSide) {
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
this.touchSide = true;
|
|
271
|
+
|
|
272
|
+
const [nextMap] = worldMaps.getAdjacentMaps(map, adjacentCoords);
|
|
273
|
+
if (!nextMap) {
|
|
274
|
+
this.touchSide = false;
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const id = nextMap.id as string;
|
|
279
|
+
const nextMapInfo = worldMaps.getMapInfo(id);
|
|
280
|
+
if (!nextMapInfo) {
|
|
281
|
+
this.touchSide = false;
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const newPosition = positionCalculator(nextMapInfo);
|
|
286
|
+
const success = await this.changeMap(id, newPosition);
|
|
287
|
+
|
|
288
|
+
// Reset touchSide after a delay to allow the change
|
|
289
|
+
setTimeout(() => {
|
|
290
|
+
this.touchSide = false;
|
|
291
|
+
}, 100);
|
|
292
|
+
|
|
293
|
+
return !!success;
|
|
294
|
+
};
|
|
295
|
+
// Check left border
|
|
296
|
+
if (nextPosition.x < marginLeftRight && direction === Direction.Left) {
|
|
297
|
+
ret = await changeMap({
|
|
298
|
+
x: (map.worldX ?? 0) - 1,
|
|
299
|
+
y: worldPositionY
|
|
300
|
+
}, nextMapInfo => ({
|
|
301
|
+
x: nextMapInfo.width - (this.hitbox().w) - marginLeftRight,
|
|
302
|
+
y: (map.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
305
|
+
// Check right border
|
|
306
|
+
else if (nextPosition.x > map.widthPx - this.hitbox().w - marginLeftRight && direction === Direction.Right) {
|
|
307
|
+
ret = await changeMap({
|
|
308
|
+
x: (map.worldX ?? 0) + map.widthPx + 1,
|
|
309
|
+
y: worldPositionY
|
|
310
|
+
}, nextMapInfo => ({
|
|
311
|
+
x: marginLeftRight,
|
|
312
|
+
y: (map.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
// Check top border
|
|
316
|
+
else if (nextPosition.y < marginTopDown && direction === Direction.Up) {
|
|
317
|
+
ret = await changeMap({
|
|
318
|
+
x: worldPositionX,
|
|
319
|
+
y: (map.worldY ?? 0) - 1
|
|
320
|
+
}, nextMapInfo => ({
|
|
321
|
+
x: (map.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
|
|
322
|
+
y: nextMapInfo.height - this.hitbox().h - marginTopDown
|
|
323
|
+
}));
|
|
324
|
+
}
|
|
325
|
+
// Check bottom border
|
|
326
|
+
else if (nextPosition.y > map.heightPx - this.hitbox().h - marginTopDown && direction === Direction.Down) {
|
|
327
|
+
ret = await changeMap({
|
|
328
|
+
x: worldPositionX,
|
|
329
|
+
y: (map.worldY ?? 0) + map.heightPx + 1
|
|
330
|
+
}, nextMapInfo => ({
|
|
331
|
+
x: (map.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
|
|
332
|
+
y: marginTopDown
|
|
333
|
+
}));
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
this.touchSide = false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return ret;
|
|
341
|
+
}
|
|
342
|
+
|
|
135
343
|
async teleport(positions: { x: number; y: number }) {
|
|
136
344
|
if (!this.map) return false;
|
|
137
|
-
|
|
138
|
-
|
|
345
|
+
if (this.map.physic) {
|
|
346
|
+
// Skip collision check for teleportation (allow teleporting through walls)
|
|
347
|
+
const entity = this.map.physic.getEntityByUUID(this.id);
|
|
348
|
+
if (entity) {
|
|
349
|
+
this.map.physic.teleport(entity, { x: positions.x, y: positions.y });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
this.x.set(positions.x)
|
|
354
|
+
this.y.set(positions.y)
|
|
355
|
+
}
|
|
356
|
+
// Wait for the frame to be added before applying frames
|
|
357
|
+
// This ensures the frame is added before applyFrames() is called
|
|
358
|
+
queueMicrotask(() => {
|
|
359
|
+
this.applyFrames()
|
|
360
|
+
})
|
|
139
361
|
}
|
|
140
362
|
|
|
141
363
|
getCurrentMap<T extends RpgMap = RpgMap>(): T | null {
|
|
@@ -151,7 +373,63 @@ export class RpgPlayer extends PlayerMixins(RpgCommonPlayer) {
|
|
|
151
373
|
});
|
|
152
374
|
}
|
|
153
375
|
|
|
154
|
-
|
|
376
|
+
async save() {
|
|
377
|
+
const snapshot = createStatesSnapshot(this)
|
|
378
|
+
await lastValueFrom(this.hooks.callHooks("server-player-onSave", this, snapshot))
|
|
379
|
+
return JSON.stringify(snapshot)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async load(snapshot: string) {
|
|
383
|
+
const data = JSON.parse(snapshot)
|
|
384
|
+
const dataLoaded = load(this, data)
|
|
385
|
+
await lastValueFrom(this.hooks.callHooks("server-player-onLoad", this, dataLoaded))
|
|
386
|
+
return dataLoaded
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Set the current animation of the player's sprite
|
|
391
|
+
*
|
|
392
|
+
* This method changes the animation state of the player's current sprite.
|
|
393
|
+
* It's used to trigger character animations like attack, skill, or custom movements.
|
|
394
|
+
* When `nbTimes` is set to a finite number, the animation will play that many times
|
|
395
|
+
* before returning to the previous animation state.
|
|
396
|
+
*
|
|
397
|
+
* @param animationName - The name of the animation to play (e.g., 'attack', 'skill', 'walk')
|
|
398
|
+
* @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```ts
|
|
402
|
+
* // Set continuous walk animation
|
|
403
|
+
* player.setAnimation('walk');
|
|
404
|
+
*
|
|
405
|
+
* // Play attack animation 3 times then return to previous state
|
|
406
|
+
* player.setAnimation('attack', 3);
|
|
407
|
+
*
|
|
408
|
+
* // Play skill animation once
|
|
409
|
+
* player.setAnimation('skill', 1);
|
|
410
|
+
*
|
|
411
|
+
* // Set idle/stand animation
|
|
412
|
+
* player.setAnimation('stand');
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
setAnimation(animationName: string, nbTimes: number = Infinity) {
|
|
416
|
+
const map = this.getCurrentMap();
|
|
417
|
+
if (!map) return;
|
|
418
|
+
if (nbTimes === Infinity) {
|
|
419
|
+
this.animationName.set(animationName);
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
map.$broadcast({
|
|
423
|
+
type: "setAnimation",
|
|
424
|
+
value: {
|
|
425
|
+
animationName,
|
|
426
|
+
nbTimes,
|
|
427
|
+
object: this.id,
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
155
433
|
|
|
156
434
|
/**
|
|
157
435
|
* 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
|
|
@@ -183,56 +461,310 @@ export class RpgPlayer extends PlayerMixins(RpgCommonPlayer) {
|
|
|
183
461
|
const { events } = map;
|
|
184
462
|
const arrayEvents: any[] = [
|
|
185
463
|
...Object.values(this.events()),
|
|
186
|
-
...Object.values(events()),
|
|
464
|
+
...Object.values(events?.() ?? {}),
|
|
187
465
|
];
|
|
188
466
|
for (let event of arrayEvents) {
|
|
189
467
|
if (event.onChanges) event.onChanges(this);
|
|
190
468
|
}
|
|
191
469
|
}
|
|
192
470
|
|
|
193
|
-
|
|
471
|
+
/**
|
|
472
|
+
* Attach a zone shape to this player using the physic zone system
|
|
473
|
+
*
|
|
474
|
+
* This method creates a zone attached to the player's entity in the physics engine.
|
|
475
|
+
* The zone can be circular or cone-shaped and will detect other entities (players/events)
|
|
476
|
+
* entering or exiting the zone.
|
|
477
|
+
*
|
|
478
|
+
* @param id - Optional zone identifier. If not provided, a unique ID will be generated
|
|
479
|
+
* @param options - Zone configuration options
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```ts
|
|
483
|
+
* // Create a circular detection zone
|
|
484
|
+
* player.attachShape("vision", {
|
|
485
|
+
* radius: 150,
|
|
486
|
+
* angle: 360,
|
|
487
|
+
* });
|
|
488
|
+
*
|
|
489
|
+
* // Create a cone-shaped vision zone
|
|
490
|
+
* player.attachShape("vision", {
|
|
491
|
+
* radius: 200,
|
|
492
|
+
* angle: 120,
|
|
493
|
+
* direction: Direction.Right,
|
|
494
|
+
* limitedByWalls: true,
|
|
495
|
+
* });
|
|
496
|
+
*
|
|
497
|
+
* // Create a zone with width/height (radius calculated automatically)
|
|
498
|
+
* player.attachShape({
|
|
499
|
+
* width: 100,
|
|
500
|
+
* height: 100,
|
|
501
|
+
* positioning: "center",
|
|
502
|
+
* });
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
attachShape(idOrOptions: string | AttachShapeOptions, options?: AttachShapeOptions): RpgShape | undefined {
|
|
194
506
|
const map = this.getCurrentMap();
|
|
195
|
-
if (!map) return;
|
|
507
|
+
if (!map) return undefined;
|
|
508
|
+
|
|
509
|
+
// Handle overloaded signature: attachShape(options) or attachShape(id, options)
|
|
510
|
+
let zoneId: string;
|
|
511
|
+
let shapeOptions: AttachShapeOptions;
|
|
512
|
+
|
|
513
|
+
if (typeof idOrOptions === 'string') {
|
|
514
|
+
zoneId = idOrOptions;
|
|
515
|
+
if (!options) {
|
|
516
|
+
console.warn('attachShape: options must be provided when id is specified');
|
|
517
|
+
return undefined;
|
|
518
|
+
}
|
|
519
|
+
shapeOptions = options;
|
|
520
|
+
} else {
|
|
521
|
+
zoneId = `zone-${this.id}-${Date.now()}`;
|
|
522
|
+
shapeOptions = idOrOptions;
|
|
523
|
+
}
|
|
196
524
|
|
|
197
|
-
|
|
525
|
+
// Get player entity from physic engine
|
|
526
|
+
const playerEntity = map.physic.getEntityByUUID(this.id);
|
|
527
|
+
if (!playerEntity) {
|
|
528
|
+
console.warn(`Player entity not found in physic engine for player ${this.id}`);
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
198
531
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
532
|
+
// Calculate radius from width/height if not provided
|
|
533
|
+
let radius: number;
|
|
534
|
+
if (shapeOptions.radius !== undefined) {
|
|
535
|
+
radius = shapeOptions.radius;
|
|
536
|
+
} else if (shapeOptions.width && shapeOptions.height) {
|
|
537
|
+
// Use the larger dimension as radius, or calculate from area
|
|
538
|
+
radius = Math.max(shapeOptions.width, shapeOptions.height) / 2;
|
|
539
|
+
} else {
|
|
540
|
+
console.warn('attachShape: radius or width/height must be provided');
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
203
543
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
544
|
+
// Calculate offset based on positioning
|
|
545
|
+
let offset: Vector2 = new Vector2(0, 0);
|
|
546
|
+
const positioning: ShapePositioning = shapeOptions.positioning || "default";
|
|
547
|
+
if (shapeOptions.positioning) {
|
|
548
|
+
const playerWidth = playerEntity.width || playerEntity.radius * 2 || 32;
|
|
549
|
+
const playerHeight = playerEntity.height || playerEntity.radius * 2 || 32;
|
|
550
|
+
|
|
551
|
+
switch (shapeOptions.positioning) {
|
|
552
|
+
case 'top':
|
|
553
|
+
offset = new Vector2(0, -playerHeight / 2);
|
|
554
|
+
break;
|
|
555
|
+
case 'bottom':
|
|
556
|
+
offset = new Vector2(0, playerHeight / 2);
|
|
557
|
+
break;
|
|
558
|
+
case 'left':
|
|
559
|
+
offset = new Vector2(-playerWidth / 2, 0);
|
|
560
|
+
break;
|
|
561
|
+
case 'right':
|
|
562
|
+
offset = new Vector2(playerWidth / 2, 0);
|
|
563
|
+
break;
|
|
564
|
+
case 'center':
|
|
565
|
+
default:
|
|
566
|
+
offset = new Vector2(0, 0);
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Get zone manager and create attached zone
|
|
572
|
+
const zoneManager = map.physic.getZoneManager();
|
|
573
|
+
|
|
574
|
+
// Convert direction from Direction enum to string if needed
|
|
575
|
+
// Direction enum values are already strings ("up", "down", "left", "right")
|
|
576
|
+
let direction: 'up' | 'down' | 'left' | 'right' = 'down';
|
|
577
|
+
if (shapeOptions.direction !== undefined) {
|
|
578
|
+
if (typeof shapeOptions.direction === 'string') {
|
|
579
|
+
direction = shapeOptions.direction as 'up' | 'down' | 'left' | 'right';
|
|
580
|
+
} else {
|
|
581
|
+
// Direction enum value is already a string, just cast it
|
|
582
|
+
direction = String(shapeOptions.direction) as 'up' | 'down' | 'left' | 'right';
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// Create zone with metadata for name and properties
|
|
587
|
+
const metadata: Record<string, any> = {};
|
|
588
|
+
if (shapeOptions.name) {
|
|
589
|
+
metadata.name = shapeOptions.name;
|
|
590
|
+
}
|
|
591
|
+
if (shapeOptions.properties) {
|
|
592
|
+
metadata.properties = shapeOptions.properties;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Get initial position
|
|
596
|
+
const initialX = playerEntity.position.x + offset.x;
|
|
597
|
+
const initialY = playerEntity.position.y + offset.y;
|
|
598
|
+
|
|
599
|
+
const physicZoneId = zoneManager.createAttachedZone(
|
|
600
|
+
playerEntity,
|
|
601
|
+
{
|
|
602
|
+
radius,
|
|
603
|
+
angle: shapeOptions.angle ?? 360,
|
|
604
|
+
direction,
|
|
605
|
+
limitedByWalls: shapeOptions.limitedByWalls ?? false,
|
|
606
|
+
offset,
|
|
607
|
+
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
|
|
216
608
|
},
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
event
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
609
|
+
{
|
|
610
|
+
onEnter: (entities: Entity[]) => {
|
|
611
|
+
entities.forEach((entity) => {
|
|
612
|
+
const event = map.getEvent<RpgEvent>(entity.uuid);
|
|
613
|
+
const player = map.getPlayer(entity.uuid);
|
|
614
|
+
|
|
615
|
+
if (event) {
|
|
616
|
+
event.execMethod("onInShape", [shape, this]);
|
|
617
|
+
// Track that this event is in the shape
|
|
618
|
+
if ((event as any)._inShapes) {
|
|
619
|
+
(event as any)._inShapes.add(shape);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (player) {
|
|
623
|
+
this.execMethod("onDetectInShape", [player, shape]);
|
|
624
|
+
// Track that this player is in the shape
|
|
625
|
+
if (player._inShapes) {
|
|
626
|
+
player._inShapes.add(shape);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
},
|
|
631
|
+
onExit: (entities: Entity[]) => {
|
|
632
|
+
entities.forEach((entity) => {
|
|
633
|
+
const event = map.getEvent<RpgEvent>(entity.uuid);
|
|
634
|
+
const player = map.getPlayer(entity.uuid);
|
|
635
|
+
|
|
636
|
+
if (event) {
|
|
637
|
+
event.execMethod("onOutShape", [shape, this]);
|
|
638
|
+
// Remove from tracking
|
|
639
|
+
if ((event as any)._inShapes) {
|
|
640
|
+
(event as any)._inShapes.delete(shape);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (player) {
|
|
644
|
+
this.execMethod("onDetectOutShape", [player, shape]);
|
|
645
|
+
// Remove from tracking
|
|
646
|
+
if (player._inShapes) {
|
|
647
|
+
player._inShapes.delete(shape);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
},
|
|
227
652
|
}
|
|
228
653
|
);
|
|
654
|
+
|
|
655
|
+
// Create RpgShape instance
|
|
656
|
+
const shape = new RpgShape({
|
|
657
|
+
name: shapeOptions.name || zoneId,
|
|
658
|
+
positioning,
|
|
659
|
+
width: shapeOptions.width || radius * 2,
|
|
660
|
+
height: shapeOptions.height || radius * 2,
|
|
661
|
+
x: initialX,
|
|
662
|
+
y: initialY,
|
|
663
|
+
properties: shapeOptions.properties || {},
|
|
664
|
+
playerOwner: this,
|
|
665
|
+
physicZoneId: physicZoneId,
|
|
666
|
+
map: map,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
// Store mapping from zoneId to physicZoneId for future reference
|
|
670
|
+
(this as any)._zoneIdMap = (this as any)._zoneIdMap || new Map();
|
|
671
|
+
(this as any)._zoneIdMap.set(zoneId, physicZoneId);
|
|
672
|
+
|
|
673
|
+
// Store the shape
|
|
674
|
+
this._attachedShapes.set(zoneId, shape);
|
|
675
|
+
|
|
676
|
+
// Update shape position when player moves
|
|
677
|
+
const updateShapePosition = () => {
|
|
678
|
+
const currentEntity = map.physic.getEntityByUUID(this.id);
|
|
679
|
+
if (currentEntity) {
|
|
680
|
+
const zoneInfo = zoneManager.getZone(physicZoneId);
|
|
681
|
+
if (zoneInfo) {
|
|
682
|
+
shape._updatePosition(zoneInfo.position.x, zoneInfo.position.y);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// Listen to position changes to update shape position
|
|
688
|
+
playerEntity.onPositionChange(() => {
|
|
689
|
+
updateShapePosition();
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
return shape;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Get all shapes attached to this player
|
|
697
|
+
*
|
|
698
|
+
* Returns all shapes that were created using `attachShape()` on this player.
|
|
699
|
+
*
|
|
700
|
+
* @returns Array of RpgShape instances attached to this player
|
|
701
|
+
*
|
|
702
|
+
* @example
|
|
703
|
+
* ```ts
|
|
704
|
+
* player.attachShape("vision", { radius: 150 });
|
|
705
|
+
* player.attachShape("detection", { radius: 100 });
|
|
706
|
+
*
|
|
707
|
+
* const shapes = player.getShapes();
|
|
708
|
+
* console.log(shapes.length); // 2
|
|
709
|
+
* ```
|
|
710
|
+
*/
|
|
711
|
+
getShapes(): RpgShape[] {
|
|
712
|
+
return Array.from(this._attachedShapes.values());
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Get all shapes where this player is currently located
|
|
717
|
+
*
|
|
718
|
+
* Returns all shapes (from any player/event) where this player is currently inside.
|
|
719
|
+
* This is updated automatically when the player enters or exits shapes.
|
|
720
|
+
*
|
|
721
|
+
* @returns Array of RpgShape instances where this player is located
|
|
722
|
+
*
|
|
723
|
+
* @example
|
|
724
|
+
* ```ts
|
|
725
|
+
* // Another player has a detection zone
|
|
726
|
+
* otherPlayer.attachShape("detection", { radius: 200 });
|
|
727
|
+
*
|
|
728
|
+
* // Check if this player is in any shape
|
|
729
|
+
* const inShapes = player.getInShapes();
|
|
730
|
+
* if (inShapes.length > 0) {
|
|
731
|
+
* console.log("Player is being detected!");
|
|
732
|
+
* }
|
|
733
|
+
* ```
|
|
734
|
+
*/
|
|
735
|
+
getInShapes(): RpgShape[] {
|
|
736
|
+
return Array.from(this._inShapes);
|
|
229
737
|
}
|
|
230
738
|
|
|
231
|
-
|
|
739
|
+
/**
|
|
740
|
+
* Show a temporary component animation on this player
|
|
741
|
+
*
|
|
742
|
+
* This method broadcasts a component animation to all clients, allowing
|
|
743
|
+
* temporary visual effects like hit indicators, spell effects, or status animations
|
|
744
|
+
* to be displayed on the player.
|
|
745
|
+
*
|
|
746
|
+
* @param id - The ID of the component animation to display
|
|
747
|
+
* @param params - Parameters to pass to the component animation
|
|
748
|
+
*
|
|
749
|
+
* @example
|
|
750
|
+
* ```ts
|
|
751
|
+
* // Show a hit animation with damage text
|
|
752
|
+
* player.showComponentAnimation("hit", {
|
|
753
|
+
* text: "150",
|
|
754
|
+
* color: "red"
|
|
755
|
+
* });
|
|
756
|
+
*
|
|
757
|
+
* // Show a heal animation
|
|
758
|
+
* player.showComponentAnimation("heal", {
|
|
759
|
+
* amount: 50
|
|
760
|
+
* });
|
|
761
|
+
* ```
|
|
762
|
+
*/
|
|
763
|
+
showComponentAnimation(id: string, params: any = {}) {
|
|
232
764
|
const map = this.getCurrentMap();
|
|
233
765
|
if (!map) return;
|
|
234
766
|
map.$broadcast({
|
|
235
|
-
type: "
|
|
767
|
+
type: "showComponentAnimation",
|
|
236
768
|
value: {
|
|
237
769
|
id,
|
|
238
770
|
params,
|
|
@@ -242,17 +774,111 @@ export class RpgPlayer extends PlayerMixins(RpgCommonPlayer) {
|
|
|
242
774
|
}
|
|
243
775
|
|
|
244
776
|
showHit(text: string) {
|
|
245
|
-
this.
|
|
777
|
+
this.showComponentAnimation("hit", {
|
|
246
778
|
text,
|
|
247
779
|
direction: this.direction(),
|
|
248
780
|
});
|
|
249
781
|
}
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Play a sound on the client side for this player only
|
|
785
|
+
*
|
|
786
|
+
* This method emits an event to play a sound only for this specific player.
|
|
787
|
+
* The sound must be defined on the client side (in the client module configuration).
|
|
788
|
+
*
|
|
789
|
+
* ## Design
|
|
790
|
+
*
|
|
791
|
+
* The sound is sent only to this player's client connection, making it ideal
|
|
792
|
+
* for personal feedback sounds like UI interactions, notifications, or personal
|
|
793
|
+
* achievements. For map-wide sounds that all players should hear, use `map.playSound()` instead.
|
|
794
|
+
*
|
|
795
|
+
* @param soundId - Sound identifier, defined on the client side
|
|
796
|
+
* @param options - Optional sound configuration
|
|
797
|
+
* @param options.volume - Volume level (0.0 to 1.0, default: 1.0)
|
|
798
|
+
* @param options.loop - Whether the sound should loop (default: false)
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* ```ts
|
|
802
|
+
* // Play a sound for this player only (default behavior)
|
|
803
|
+
* player.playSound("item-pickup");
|
|
804
|
+
*
|
|
805
|
+
* // Play a sound with volume and loop
|
|
806
|
+
* player.playSound("background-music", {
|
|
807
|
+
* volume: 0.5,
|
|
808
|
+
* loop: true
|
|
809
|
+
* });
|
|
810
|
+
*
|
|
811
|
+
* // Play a notification sound at low volume
|
|
812
|
+
* player.playSound("notification", { volume: 0.3 });
|
|
813
|
+
* ```
|
|
814
|
+
*/
|
|
815
|
+
playSound(soundId: string, options?: { volume?: number; loop?: boolean }): void {
|
|
816
|
+
const map = this.getCurrentMap();
|
|
817
|
+
if (!map) return;
|
|
818
|
+
|
|
819
|
+
const data: any = {
|
|
820
|
+
soundId,
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
if (options) {
|
|
824
|
+
if (options.volume !== undefined) {
|
|
825
|
+
data.volume = Math.max(0, Math.min(1, options.volume));
|
|
826
|
+
}
|
|
827
|
+
if (options.loop !== undefined) {
|
|
828
|
+
data.loop = options.loop;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// Send only to this player
|
|
833
|
+
this.emit("playSound", data);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Stop a sound that is currently playing for this player
|
|
838
|
+
*
|
|
839
|
+
* This method stops a sound that was previously started with `playSound()`.
|
|
840
|
+
* The sound must be defined on the client side.
|
|
841
|
+
*
|
|
842
|
+
* @param soundId - Sound identifier to stop
|
|
843
|
+
*
|
|
844
|
+
* @example
|
|
845
|
+
* ```ts
|
|
846
|
+
* // Start a looping background music
|
|
847
|
+
* player.playSound("background-music", { loop: true });
|
|
848
|
+
*
|
|
849
|
+
* // Later, stop it
|
|
850
|
+
* player.stopSound("background-music");
|
|
851
|
+
* ```
|
|
852
|
+
*/
|
|
853
|
+
stopSound(soundId: string): void {
|
|
854
|
+
const map = this.getCurrentMap();
|
|
855
|
+
if (!map) return;
|
|
856
|
+
|
|
857
|
+
const data = {
|
|
858
|
+
soundId,
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// Send stop command only to this player
|
|
862
|
+
this.emit("stopSound", data);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Set the sync schema for the map
|
|
867
|
+
* @param schema - The schema to set
|
|
868
|
+
*/
|
|
869
|
+
setSync(schema: any) {
|
|
870
|
+
for (let key in schema) {
|
|
871
|
+
this[key] = type(signal(null), key, {
|
|
872
|
+
syncWithClient: schema[key]?.$syncWithClient,
|
|
873
|
+
persist: schema[key]?.$permanent,
|
|
874
|
+
}, this)
|
|
875
|
+
}
|
|
876
|
+
}
|
|
250
877
|
}
|
|
251
878
|
|
|
252
879
|
export class RpgEvent extends RpgPlayer {
|
|
253
880
|
override async execMethod(methodName: string, methodData: any[] = [], instance = this) {
|
|
254
|
-
|
|
255
|
-
await lastValueFrom(hooks
|
|
881
|
+
await lastValueFrom(this.hooks
|
|
256
882
|
.callHooks(`server-event-${methodName}`, instance, ...methodData));
|
|
257
883
|
if (!instance[methodName]) {
|
|
258
884
|
return;
|
|
@@ -268,12 +894,25 @@ export class RpgEvent extends RpgPlayer {
|
|
|
268
894
|
}
|
|
269
895
|
}
|
|
270
896
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Interface extension for RpgPlayer
|
|
900
|
+
*
|
|
901
|
+
* Extends the RpgPlayer class with additional interfaces from mixins.
|
|
902
|
+
* This provides proper TypeScript support for all mixin methods and properties.
|
|
903
|
+
*/
|
|
904
|
+
export interface RpgPlayer extends
|
|
905
|
+
IVariableManager,
|
|
906
|
+
IMoveManager,
|
|
907
|
+
IGoldManager,
|
|
908
|
+
IComponentManager,
|
|
909
|
+
IGuiManager,
|
|
910
|
+
IItemManager,
|
|
911
|
+
IEffectManager,
|
|
912
|
+
IParameterManager,
|
|
913
|
+
IElementManager,
|
|
914
|
+
ISkillManager,
|
|
915
|
+
IBattleManager,
|
|
916
|
+
IClassManager,
|
|
917
|
+
IStateManager
|
|
918
|
+
{}
|