@rpgjs/server 5.0.0-alpha.25 → 5.0.0-alpha.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Player/Player.d.ts +31 -22
- package/dist/index.js +412 -196
- package/dist/index.js.map +1 -1
- package/dist/rooms/BaseRoom.d.ts +95 -0
- package/dist/rooms/lobby.d.ts +4 -1
- package/dist/rooms/map.d.ts +17 -75
- package/package.json +8 -8
- package/src/Player/ItemManager.ts +50 -15
- package/src/Player/Player.ts +161 -135
- package/src/module.ts +13 -0
- package/src/rooms/BaseRoom.ts +120 -0
- package/src/rooms/lobby.ts +11 -1
- package/src/rooms/map.ts +68 -144
- package/tests/item.spec.ts +455 -441
- package/tests/world-maps.spec.ts +43 -81
package/src/Player/Player.ts
CHANGED
|
@@ -91,10 +91,69 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
91
91
|
context?: Context;
|
|
92
92
|
conn: MockConnection | null = null;
|
|
93
93
|
touchSide: boolean = false; // Protection against map change loops
|
|
94
|
-
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Computed signal for world X position
|
|
97
|
+
*
|
|
98
|
+
* Calculates the absolute world X position from the map's world position
|
|
99
|
+
* plus the player's local X position. Returns 0 if no map is assigned.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const worldX = player.worldX();
|
|
104
|
+
* console.log(`Player is at world X: ${worldX}`);
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
get worldPositionX() {
|
|
108
|
+
return this._getComputedWorldPosition('x');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Computed signal for world Y position
|
|
113
|
+
*
|
|
114
|
+
* Calculates the absolute world Y position from the map's world position
|
|
115
|
+
* plus the player's local Y position. Returns 0 if no map is assigned.
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* ```ts
|
|
119
|
+
* const worldY = player.worldY();
|
|
120
|
+
* console.log(`Player is at world Y: ${worldY}`);
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
get worldPositionY() {
|
|
124
|
+
return this._getComputedWorldPosition('y');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private _worldPositionSignals = new WeakMap<any, any>();
|
|
128
|
+
|
|
129
|
+
private _getComputedWorldPosition(axis: 'x' | 'y') {
|
|
130
|
+
// We use a WeakMap to cache the computed signal per instance
|
|
131
|
+
// This ensures that if the player object is copied (e.g. in tests),
|
|
132
|
+
// the new instance gets its own signal bound to itself.
|
|
133
|
+
if (!this._worldPositionSignals) {
|
|
134
|
+
this._worldPositionSignals = new WeakMap();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const key = axis;
|
|
138
|
+
let signals = this._worldPositionSignals.get(this);
|
|
139
|
+
if (!signals) {
|
|
140
|
+
signals = {};
|
|
141
|
+
this._worldPositionSignals.set(this, signals);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!signals[key]) {
|
|
145
|
+
signals[key] = computed(() => {
|
|
146
|
+
const map = this.map as RpgMap | null;
|
|
147
|
+
const mapWorldPos = map ? (map[axis === 'x' ? 'worldX' : 'worldY'] ?? 0) : 0;
|
|
148
|
+
return mapWorldPos + (this[axis] as any)();
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return signals[key];
|
|
152
|
+
}
|
|
153
|
+
|
|
95
154
|
/** Internal: Shapes attached to this player */
|
|
96
155
|
private _attachedShapes: Map<string, RpgShape> = new Map();
|
|
97
|
-
|
|
156
|
+
|
|
98
157
|
/** Internal: Shapes where this player is currently located */
|
|
99
158
|
private _inShapes: Set<RpgShape> = new Set();
|
|
100
159
|
/** Last processed client input timestamp for reconciliation */
|
|
@@ -118,10 +177,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
118
177
|
super();
|
|
119
178
|
// Use type assertion to access mixin properties
|
|
120
179
|
(this as any).expCurve = {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
180
|
+
basis: 30,
|
|
181
|
+
extra: 20,
|
|
182
|
+
accelerationA: 30,
|
|
183
|
+
accelerationB: 30
|
|
125
184
|
};
|
|
126
185
|
|
|
127
186
|
(this as any).addParameter(MAXHP, MAXHP_CURVE);
|
|
@@ -139,7 +198,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
139
198
|
combineLatest([this.x.observable, this.y.observable])
|
|
140
199
|
.subscribe(([x, y]) => {
|
|
141
200
|
pendingUpdate = { x, y };
|
|
142
|
-
|
|
201
|
+
|
|
143
202
|
// Schedule a synchronous update using queueMicrotask
|
|
144
203
|
// This groups multiple rapid changes (x and y in the same tick) into a single frame
|
|
145
204
|
if (!updateScheduled) {
|
|
@@ -163,7 +222,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
163
222
|
}
|
|
164
223
|
})
|
|
165
224
|
}
|
|
166
|
-
|
|
225
|
+
|
|
167
226
|
_onInit() {
|
|
168
227
|
this.hooks.callHooks("server-playerProps-load", this).subscribe();
|
|
169
228
|
}
|
|
@@ -177,6 +236,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
177
236
|
return this.map
|
|
178
237
|
}
|
|
179
238
|
|
|
239
|
+
setMap(map: RpgMap) {
|
|
240
|
+
this.map = map;
|
|
241
|
+
}
|
|
242
|
+
|
|
180
243
|
applyFrames() {
|
|
181
244
|
this._frames.set(this.frames)
|
|
182
245
|
this.frames = []
|
|
@@ -219,12 +282,12 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
219
282
|
): Promise<any | null | boolean> {
|
|
220
283
|
const realMapId = 'map-' + mapId;
|
|
221
284
|
const room = this.getCurrentMap();
|
|
222
|
-
|
|
285
|
+
|
|
223
286
|
const canChange: boolean[] = await lastValueFrom(this.hooks.callHooks("server-player-canChangeMap", this, {
|
|
224
287
|
id: mapId,
|
|
225
288
|
}));
|
|
226
289
|
if (canChange.some(v => v === false)) return false;
|
|
227
|
-
|
|
290
|
+
|
|
228
291
|
if (positions && typeof positions === 'object') {
|
|
229
292
|
this.teleport(positions)
|
|
230
293
|
}
|
|
@@ -236,118 +299,81 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
236
299
|
return true;
|
|
237
300
|
}
|
|
238
301
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
* and automatically performs a change to the adjacent map if it exists.
|
|
244
|
-
*
|
|
245
|
-
* @param nextPosition - The next position of the player
|
|
246
|
-
* @returns Promise<boolean> - true if a map change occurred
|
|
247
|
-
*
|
|
248
|
-
* @example
|
|
249
|
-
* ```ts
|
|
250
|
-
* // Called automatically by the movement system
|
|
251
|
-
* const changed = await player.autoChangeMap({ x: newX, y: newY });
|
|
252
|
-
* if (changed) {
|
|
253
|
-
* console.log('Player changed map automatically');
|
|
254
|
-
* }
|
|
255
|
-
* ```
|
|
256
|
-
*/
|
|
257
|
-
async autoChangeMap(nextPosition: { x: number; y: number }, forcedDirection?: any): Promise<boolean> {
|
|
258
|
-
const map = this.getCurrentMap() as RpgMap; // Cast to access extended properties
|
|
259
|
-
if (!map) return false;
|
|
260
|
-
|
|
261
|
-
const worldMaps = map.getWorldMapsManager?.();
|
|
262
|
-
let ret: boolean = false;
|
|
263
|
-
|
|
302
|
+
async autoChangeMap(nextPosition: Vector2): Promise<boolean> {
|
|
303
|
+
const map = this.getCurrentMap()
|
|
304
|
+
const worldMaps = map?.getInWorldMaps()
|
|
305
|
+
let ret: boolean = false
|
|
264
306
|
if (worldMaps && map) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
// Current world position of the player
|
|
270
|
-
const worldPositionX = (map.worldX ?? 0) + this.x();
|
|
271
|
-
const worldPositionY = (map.worldY ?? 0) + this.y();
|
|
307
|
+
const direction = this.getDirection()
|
|
308
|
+
const marginLeftRight = map.tileWidth / 2
|
|
309
|
+
const marginTopDown = map.tileHeight / 2
|
|
272
310
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
311
|
+
const changeMap = async (adjacent, to) => {
|
|
312
|
+
if (this.touchSide) {
|
|
313
|
+
return false
|
|
314
|
+
}
|
|
315
|
+
this.touchSide = true
|
|
316
|
+
const [nextMap] = worldMaps.getAdjacentMaps(map, adjacent)
|
|
317
|
+
if (!nextMap) return false
|
|
318
|
+
const id = nextMap.id as string
|
|
319
|
+
const nextMapInfo = worldMaps.getMapInfo(id)
|
|
320
|
+
return !!(await this.changeMap(id, to(nextMapInfo)))
|
|
276
321
|
}
|
|
277
|
-
this.touchSide = true;
|
|
278
322
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
323
|
+
if (nextPosition.x < marginLeftRight && direction == Direction.Left) {
|
|
324
|
+
ret = await changeMap({
|
|
325
|
+
x: map.worldX - 1,
|
|
326
|
+
y: this.worldPositionY() + 1
|
|
327
|
+
}, nextMapInfo => ({
|
|
328
|
+
x: (nextMapInfo.width) - this.hitbox().w - marginLeftRight,
|
|
329
|
+
y: map.worldY - nextMapInfo.y + nextPosition.y
|
|
330
|
+
}))
|
|
283
331
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
332
|
+
else if (nextPosition.x > map.widthPx - this.hitbox().w - marginLeftRight && direction == Direction.Right) {
|
|
333
|
+
ret = await changeMap({
|
|
334
|
+
x: map.worldX + map.widthPx + 1,
|
|
335
|
+
y: this.worldPositionY() + 1
|
|
336
|
+
}, nextMapInfo => ({
|
|
337
|
+
x: marginLeftRight,
|
|
338
|
+
y: map.worldY - nextMapInfo.y + nextPosition.y
|
|
339
|
+
}))
|
|
340
|
+
}
|
|
341
|
+
else if (nextPosition.y < marginTopDown && direction == Direction.Up) {
|
|
342
|
+
ret = await changeMap({
|
|
343
|
+
x: this.worldPositionX() + 1,
|
|
344
|
+
y: map.worldY - 1
|
|
345
|
+
}, nextMapInfo => ({
|
|
346
|
+
x: map.worldX - nextMapInfo.x + nextPosition.x,
|
|
347
|
+
y: (nextMapInfo.height) - this.hitbox().h - marginTopDown,
|
|
348
|
+
}))
|
|
349
|
+
}
|
|
350
|
+
else if (nextPosition.y > map.heightPx - this.hitbox().h - marginTopDown && direction == Direction.Down) {
|
|
351
|
+
ret = await changeMap({
|
|
352
|
+
x: this.worldPositionX() + 1,
|
|
353
|
+
y: map.worldY + map.heightPx + 1
|
|
354
|
+
}, nextMapInfo => ({
|
|
355
|
+
x: map.worldX - nextMapInfo.x + nextPosition.x,
|
|
356
|
+
y: marginTopDown,
|
|
357
|
+
}))
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
this.touchSide = false
|
|
290
361
|
}
|
|
291
|
-
|
|
292
|
-
const newPosition = positionCalculator(nextMapInfo);
|
|
293
|
-
const success = await this.changeMap(id, newPosition);
|
|
294
|
-
|
|
295
|
-
// Reset touchSide after a delay to allow the change
|
|
296
|
-
setTimeout(() => {
|
|
297
|
-
this.touchSide = false;
|
|
298
|
-
}, 100);
|
|
299
|
-
|
|
300
|
-
return !!success;
|
|
301
|
-
};
|
|
302
|
-
// Check left border
|
|
303
|
-
if (nextPosition.x < marginLeftRight && direction === Direction.Left) {
|
|
304
|
-
ret = await changeMap(2, nextMapInfo => ({
|
|
305
|
-
x: nextMapInfo.width - (this.hitbox().w) - marginLeftRight,
|
|
306
|
-
y: (map.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
|
|
307
|
-
}));
|
|
308
|
-
}
|
|
309
|
-
// Check right border
|
|
310
|
-
else if (nextPosition.x > map.widthPx - this.hitbox().w - marginLeftRight && direction === Direction.Right) {
|
|
311
|
-
ret = await changeMap(3, nextMapInfo => ({
|
|
312
|
-
x: marginLeftRight,
|
|
313
|
-
y: (map.worldY ?? 0) - (nextMapInfo.y ?? 0) + nextPosition.y
|
|
314
|
-
}));
|
|
315
|
-
}
|
|
316
|
-
// Check top border
|
|
317
|
-
else if (nextPosition.y < marginTopDown && direction === Direction.Up) {
|
|
318
|
-
ret = await changeMap(0, nextMapInfo => ({
|
|
319
|
-
x: (map.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
|
|
320
|
-
y: nextMapInfo.height - this.hitbox().h - marginTopDown
|
|
321
|
-
}));
|
|
322
|
-
}
|
|
323
|
-
// Check bottom border
|
|
324
|
-
else if (nextPosition.y > map.heightPx - this.hitbox().h - marginTopDown && direction === Direction.Down) {
|
|
325
|
-
ret = await changeMap(1, nextMapInfo => ({
|
|
326
|
-
x: (map.worldX ?? 0) - (nextMapInfo.x ?? 0) + nextPosition.x,
|
|
327
|
-
y: marginTopDown
|
|
328
|
-
}));
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
this.touchSide = false;
|
|
332
|
-
}
|
|
333
362
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
}
|
|
363
|
+
return ret
|
|
364
|
+
}
|
|
337
365
|
|
|
338
366
|
async teleport(positions: { x: number; y: number }) {
|
|
339
367
|
if (!this.map) return false;
|
|
340
|
-
if (this.map.physic) {
|
|
368
|
+
if (this.map && this.map.physic) {
|
|
341
369
|
// Skip collision check for teleportation (allow teleporting through walls)
|
|
342
370
|
const entity = this.map.physic.getEntityByUUID(this.id);
|
|
343
371
|
if (entity) {
|
|
344
372
|
this.map.physic.teleport(entity, { x: positions.x, y: positions.y });
|
|
345
373
|
}
|
|
346
374
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
this.y.set(positions.y)
|
|
350
|
-
}
|
|
375
|
+
this.x.set(positions.x)
|
|
376
|
+
this.y.set(positions.y)
|
|
351
377
|
// Wait for the frame to be added before applying frames
|
|
352
378
|
// This ensures the frame is added before applyFrames() is called
|
|
353
379
|
queueMicrotask(() => {
|
|
@@ -440,8 +466,9 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
440
466
|
}
|
|
441
467
|
|
|
442
468
|
databaseById(id: string) {
|
|
443
|
-
|
|
444
|
-
|
|
469
|
+
// Use this.map directly to support both RpgMap and LobbyRoom
|
|
470
|
+
const map = this.map as any;
|
|
471
|
+
if (!map || !map.database) return;
|
|
445
472
|
const data = map.database()[id];
|
|
446
473
|
if (!data)
|
|
447
474
|
throw new Error(
|
|
@@ -504,7 +531,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
504
531
|
// Handle overloaded signature: attachShape(options) or attachShape(id, options)
|
|
505
532
|
let zoneId: string;
|
|
506
533
|
let shapeOptions: AttachShapeOptions;
|
|
507
|
-
|
|
534
|
+
|
|
508
535
|
if (typeof idOrOptions === 'string') {
|
|
509
536
|
zoneId = idOrOptions;
|
|
510
537
|
if (!options) {
|
|
@@ -542,7 +569,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
542
569
|
if (shapeOptions.positioning) {
|
|
543
570
|
const playerWidth = playerEntity.width || playerEntity.radius * 2 || 32;
|
|
544
571
|
const playerHeight = playerEntity.height || playerEntity.radius * 2 || 32;
|
|
545
|
-
|
|
572
|
+
|
|
546
573
|
switch (shapeOptions.positioning) {
|
|
547
574
|
case 'top':
|
|
548
575
|
offset = new Vector2(0, -playerHeight / 2);
|
|
@@ -565,7 +592,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
565
592
|
|
|
566
593
|
// Get zone manager and create attached zone
|
|
567
594
|
const zoneManager = map.physic.getZoneManager();
|
|
568
|
-
|
|
595
|
+
|
|
569
596
|
// Convert direction from Direction enum to string if needed
|
|
570
597
|
// Direction enum values are already strings ("up", "down", "left", "right")
|
|
571
598
|
let direction: 'up' | 'down' | 'left' | 'right' = 'down';
|
|
@@ -606,7 +633,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
606
633
|
entities.forEach((entity) => {
|
|
607
634
|
const event = map.getEvent<RpgEvent>(entity.uuid);
|
|
608
635
|
const player = map.getPlayer(entity.uuid);
|
|
609
|
-
|
|
636
|
+
|
|
610
637
|
if (event) {
|
|
611
638
|
event.execMethod("onInShape", [shape, this]);
|
|
612
639
|
// Track that this event is in the shape
|
|
@@ -627,7 +654,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
627
654
|
entities.forEach((entity) => {
|
|
628
655
|
const event = map.getEvent<RpgEvent>(entity.uuid);
|
|
629
656
|
const player = map.getPlayer(entity.uuid);
|
|
630
|
-
|
|
657
|
+
|
|
631
658
|
if (event) {
|
|
632
659
|
event.execMethod("onOutShape", [shape, this]);
|
|
633
660
|
// Remove from tracking
|
|
@@ -664,10 +691,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
664
691
|
// Store mapping from zoneId to physicZoneId for future reference
|
|
665
692
|
(this as any)._zoneIdMap = (this as any)._zoneIdMap || new Map();
|
|
666
693
|
(this as any)._zoneIdMap.set(zoneId, physicZoneId);
|
|
667
|
-
|
|
694
|
+
|
|
668
695
|
// Store the shape
|
|
669
696
|
this._attachedShapes.set(zoneId, shape);
|
|
670
|
-
|
|
697
|
+
|
|
671
698
|
// Update shape position when player moves
|
|
672
699
|
const updateShapePosition = () => {
|
|
673
700
|
const currentEntity = map.physic.getEntityByUUID(this.id);
|
|
@@ -678,7 +705,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
678
705
|
}
|
|
679
706
|
}
|
|
680
707
|
};
|
|
681
|
-
|
|
708
|
+
|
|
682
709
|
// Listen to position changes to update shape position
|
|
683
710
|
playerEntity.onPositionChange(() => {
|
|
684
711
|
updateShapePosition();
|
|
@@ -686,7 +713,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
686
713
|
|
|
687
714
|
return shape;
|
|
688
715
|
}
|
|
689
|
-
|
|
716
|
+
|
|
690
717
|
/**
|
|
691
718
|
* Get all shapes attached to this player
|
|
692
719
|
*
|
|
@@ -706,7 +733,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
706
733
|
getShapes(): RpgShape[] {
|
|
707
734
|
return Array.from(this._attachedShapes.values());
|
|
708
735
|
}
|
|
709
|
-
|
|
736
|
+
|
|
710
737
|
/**
|
|
711
738
|
* Get all shapes where this player is currently located
|
|
712
739
|
*
|
|
@@ -1053,13 +1080,13 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
1053
1080
|
if (typeof height !== 'number' || height <= 0) {
|
|
1054
1081
|
throw new Error('setHitbox: height must be a positive number');
|
|
1055
1082
|
}
|
|
1056
|
-
|
|
1083
|
+
|
|
1057
1084
|
// Update hitbox signal
|
|
1058
1085
|
this.hitbox.set({
|
|
1059
1086
|
w: width,
|
|
1060
1087
|
h: height,
|
|
1061
1088
|
});
|
|
1062
|
-
|
|
1089
|
+
|
|
1063
1090
|
// Update physics entity if map exists
|
|
1064
1091
|
const map = this.getCurrentMap();
|
|
1065
1092
|
if (map && map.physic) {
|
|
@@ -1108,18 +1135,17 @@ export class RpgEvent extends RpgPlayer {
|
|
|
1108
1135
|
* Extends the RpgPlayer class with additional interfaces from mixins.
|
|
1109
1136
|
* This provides proper TypeScript support for all mixin methods and properties.
|
|
1110
1137
|
*/
|
|
1111
|
-
export interface RpgPlayer extends
|
|
1112
|
-
IVariableManager,
|
|
1113
|
-
IMoveManager,
|
|
1114
|
-
IGoldManager,
|
|
1115
|
-
IComponentManager,
|
|
1116
|
-
IGuiManager,
|
|
1117
|
-
IItemManager,
|
|
1118
|
-
IEffectManager,
|
|
1119
|
-
IParameterManager,
|
|
1120
|
-
IElementManager,
|
|
1121
|
-
ISkillManager,
|
|
1122
|
-
IBattleManager,
|
|
1123
|
-
IClassManager,
|
|
1124
|
-
IStateManager
|
|
1125
|
-
{}
|
|
1138
|
+
export interface RpgPlayer extends
|
|
1139
|
+
IVariableManager,
|
|
1140
|
+
IMoveManager,
|
|
1141
|
+
IGoldManager,
|
|
1142
|
+
IComponentManager,
|
|
1143
|
+
IGuiManager,
|
|
1144
|
+
IItemManager,
|
|
1145
|
+
IEffectManager,
|
|
1146
|
+
IParameterManager,
|
|
1147
|
+
IElementManager,
|
|
1148
|
+
ISkillManager,
|
|
1149
|
+
IBattleManager,
|
|
1150
|
+
IClassManager,
|
|
1151
|
+
IStateManager { }
|
package/src/module.ts
CHANGED
|
@@ -127,6 +127,19 @@ export function provideServerModules(modules: RpgServerModule[]): FactoryProvide
|
|
|
127
127
|
}
|
|
128
128
|
};
|
|
129
129
|
}
|
|
130
|
+
if (module.database && typeof module.database === 'object') {
|
|
131
|
+
const database = {...module.database};
|
|
132
|
+
module = {
|
|
133
|
+
...module,
|
|
134
|
+
databaseHooks: {
|
|
135
|
+
load: (engine: RpgMap) => {
|
|
136
|
+
for (const key in database) {
|
|
137
|
+
engine.addInDatabase(key, database[key]);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
130
143
|
return module;
|
|
131
144
|
})
|
|
132
145
|
return modules
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { signal } from "@signe/reactive";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base class for rooms that need database functionality
|
|
5
|
+
*
|
|
6
|
+
* This class provides common database management functionality that is shared
|
|
7
|
+
* between RpgMap and LobbyRoom. It includes methods for adding and managing
|
|
8
|
+
* items, classes, and other game data in the room's database.
|
|
9
|
+
*
|
|
10
|
+
* ## Architecture
|
|
11
|
+
*
|
|
12
|
+
* Both RpgMap and LobbyRoom need to store game entities (items, classes, skills, etc.)
|
|
13
|
+
* in a database. This base class provides the common implementation to avoid code duplication.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* class MyCustomRoom extends BaseRoom {
|
|
18
|
+
* // Your custom room implementation
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export abstract class BaseRoom {
|
|
23
|
+
/**
|
|
24
|
+
* Signal containing the room's database of items, classes, and other game data
|
|
25
|
+
*
|
|
26
|
+
* This database can be dynamically populated using `addInDatabase()` and
|
|
27
|
+
* `removeInDatabase()` methods. It's used to store game entities like items,
|
|
28
|
+
* classes, skills, etc. that are available in this room.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Add data to database
|
|
33
|
+
* room.addInDatabase('Potion', PotionClass);
|
|
34
|
+
*
|
|
35
|
+
* // Access database
|
|
36
|
+
* const potion = room.database()['Potion'];
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
database = signal({});
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Add data to the room's database
|
|
43
|
+
*
|
|
44
|
+
* Adds an item, class, or other game entity to the room's database.
|
|
45
|
+
* If the ID already exists and `force` is not enabled, the addition is ignored.
|
|
46
|
+
*
|
|
47
|
+
* ## Architecture
|
|
48
|
+
*
|
|
49
|
+
* This method is used by the item management system to store item definitions
|
|
50
|
+
* in the room's database. When a player adds an item, the system first checks
|
|
51
|
+
* if the item exists in the database, and if not, adds it using this method.
|
|
52
|
+
*
|
|
53
|
+
* @param id - Unique identifier for the data
|
|
54
|
+
* @param data - The data to add (can be a class, object, etc.)
|
|
55
|
+
* @param options - Optional configuration
|
|
56
|
+
* @param options.force - If true, overwrites existing data with the same ID
|
|
57
|
+
* @returns `true` if data was added, `false` if it was ignored (ID already exists)
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* // Add a class to the database
|
|
62
|
+
* room.addInDatabase('Potion', PotionClass);
|
|
63
|
+
*
|
|
64
|
+
* // Add an item object to the database
|
|
65
|
+
* room.addInDatabase('custom-item', {
|
|
66
|
+
* name: 'Custom Item',
|
|
67
|
+
* price: 100
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* // Force overwrite existing data
|
|
71
|
+
* room.addInDatabase('Potion', UpdatedPotionClass, { force: true });
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
addInDatabase(id: string, data: any, options?: { force?: boolean }): boolean {
|
|
75
|
+
const database = this.database();
|
|
76
|
+
|
|
77
|
+
// Check if ID already exists
|
|
78
|
+
if (database[id] !== undefined && !options?.force) {
|
|
79
|
+
// Ignore the addition if ID exists and force is not enabled
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add or overwrite the data
|
|
84
|
+
database[id] = data;
|
|
85
|
+
this.database.set(database);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Remove data from the room's database
|
|
91
|
+
*
|
|
92
|
+
* This method allows you to remove items or data from the room's database.
|
|
93
|
+
*
|
|
94
|
+
* @param id - Unique identifier of the data to remove
|
|
95
|
+
* @returns `true` if data was removed, `false` if ID didn't exist
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* ```ts
|
|
99
|
+
* // Remove an item from the database
|
|
100
|
+
* room.removeInDatabase('Potion');
|
|
101
|
+
*
|
|
102
|
+
* // Check if removal was successful
|
|
103
|
+
* const removed = room.removeInDatabase('custom-item');
|
|
104
|
+
* if (removed) {
|
|
105
|
+
* console.log('Item removed successfully');
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
removeInDatabase(id: string): boolean {
|
|
110
|
+
const database = this.database();
|
|
111
|
+
|
|
112
|
+
if (database[id] === undefined) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
delete database[id];
|
|
117
|
+
this.database.set(database);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
package/src/rooms/lobby.ts
CHANGED
|
@@ -5,12 +5,22 @@ import { context } from "../core/context";
|
|
|
5
5
|
import { users } from "@signe/sync";
|
|
6
6
|
import { signal } from "@signe/reactive";
|
|
7
7
|
import { RpgPlayer } from "../Player/Player";
|
|
8
|
+
import { BaseRoom } from "./BaseRoom";
|
|
8
9
|
|
|
9
10
|
@Room({
|
|
10
11
|
path: "lobby-{id}",
|
|
11
12
|
})
|
|
12
|
-
export class LobbyRoom {
|
|
13
|
+
export class LobbyRoom extends BaseRoom {
|
|
13
14
|
@users(RpgPlayer) players = signal({});
|
|
15
|
+
autoSync: boolean = true;
|
|
16
|
+
|
|
17
|
+
constructor(room) {
|
|
18
|
+
super();
|
|
19
|
+
const isTest = room.env.TEST === 'true' ? true : false;
|
|
20
|
+
if (isTest) {
|
|
21
|
+
this.autoSync = false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
14
24
|
|
|
15
25
|
onJoin(player: RpgPlayer, conn: MockConnection) {
|
|
16
26
|
player.map = this;
|