@rpgjs/server 5.0.0-alpha.25 → 5.0.0-alpha.27
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/MoveManager.d.ts +62 -1
- package/dist/Player/Player.d.ts +33 -22
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1827 -365
- 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 +10 -10
- package/src/Player/ItemManager.ts +50 -15
- package/src/Player/MoveManager.ts +654 -112
- package/src/Player/Player.ts +179 -136
- package/src/index.ts +2 -1
- 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 +70 -146
- package/tests/change-map.spec.ts +2 -2
- package/tests/event.spec.ts +80 -0
- package/tests/item.spec.ts +455 -441
- package/tests/move.spec.ts +601 -0
- package/tests/random-move.spec.ts +65 -0
- 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,90 @@ 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
|
-
|
|
307
|
+
const direction = this.getDirection()
|
|
308
|
+
const marginLeftRight = map.tileWidth / 2
|
|
309
|
+
const marginTopDown = map.tileHeight / 2
|
|
268
310
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
this.
|
|
372
|
+
const hitbox = typeof this.hitbox === "function" ? this.hitbox() : this.hitbox;
|
|
373
|
+
const width = hitbox?.w ?? 32;
|
|
374
|
+
const height = hitbox?.h ?? 32;
|
|
375
|
+
|
|
376
|
+
// Convert top-left position to center position for physics engine
|
|
377
|
+
// positions.x/y are TOP-LEFT coordinates, but physic.teleport expects CENTER coordinates
|
|
378
|
+
const centerX = positions.x + width / 2;
|
|
379
|
+
const centerY = positions.y + height / 2;
|
|
380
|
+
|
|
381
|
+
this.map.physic.teleport(entity, { x: centerX, y: centerY });
|
|
345
382
|
}
|
|
346
383
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
this.y.set(positions.y)
|
|
350
|
-
}
|
|
384
|
+
this.x.set(positions.x)
|
|
385
|
+
this.y.set(positions.y)
|
|
351
386
|
// Wait for the frame to be added before applying frames
|
|
352
387
|
// This ensures the frame is added before applyFrames() is called
|
|
353
388
|
queueMicrotask(() => {
|
|
@@ -440,8 +475,9 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
440
475
|
}
|
|
441
476
|
|
|
442
477
|
databaseById(id: string) {
|
|
443
|
-
|
|
444
|
-
|
|
478
|
+
// Use this.map directly to support both RpgMap and LobbyRoom
|
|
479
|
+
const map = this.map as any;
|
|
480
|
+
if (!map || !map.database) return;
|
|
445
481
|
const data = map.database()[id];
|
|
446
482
|
if (!data)
|
|
447
483
|
throw new Error(
|
|
@@ -504,7 +540,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
504
540
|
// Handle overloaded signature: attachShape(options) or attachShape(id, options)
|
|
505
541
|
let zoneId: string;
|
|
506
542
|
let shapeOptions: AttachShapeOptions;
|
|
507
|
-
|
|
543
|
+
|
|
508
544
|
if (typeof idOrOptions === 'string') {
|
|
509
545
|
zoneId = idOrOptions;
|
|
510
546
|
if (!options) {
|
|
@@ -542,7 +578,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
542
578
|
if (shapeOptions.positioning) {
|
|
543
579
|
const playerWidth = playerEntity.width || playerEntity.radius * 2 || 32;
|
|
544
580
|
const playerHeight = playerEntity.height || playerEntity.radius * 2 || 32;
|
|
545
|
-
|
|
581
|
+
|
|
546
582
|
switch (shapeOptions.positioning) {
|
|
547
583
|
case 'top':
|
|
548
584
|
offset = new Vector2(0, -playerHeight / 2);
|
|
@@ -565,7 +601,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
565
601
|
|
|
566
602
|
// Get zone manager and create attached zone
|
|
567
603
|
const zoneManager = map.physic.getZoneManager();
|
|
568
|
-
|
|
604
|
+
|
|
569
605
|
// Convert direction from Direction enum to string if needed
|
|
570
606
|
// Direction enum values are already strings ("up", "down", "left", "right")
|
|
571
607
|
let direction: 'up' | 'down' | 'left' | 'right' = 'down';
|
|
@@ -606,7 +642,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
606
642
|
entities.forEach((entity) => {
|
|
607
643
|
const event = map.getEvent<RpgEvent>(entity.uuid);
|
|
608
644
|
const player = map.getPlayer(entity.uuid);
|
|
609
|
-
|
|
645
|
+
|
|
610
646
|
if (event) {
|
|
611
647
|
event.execMethod("onInShape", [shape, this]);
|
|
612
648
|
// Track that this event is in the shape
|
|
@@ -627,7 +663,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
627
663
|
entities.forEach((entity) => {
|
|
628
664
|
const event = map.getEvent<RpgEvent>(entity.uuid);
|
|
629
665
|
const player = map.getPlayer(entity.uuid);
|
|
630
|
-
|
|
666
|
+
|
|
631
667
|
if (event) {
|
|
632
668
|
event.execMethod("onOutShape", [shape, this]);
|
|
633
669
|
// Remove from tracking
|
|
@@ -664,10 +700,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
664
700
|
// Store mapping from zoneId to physicZoneId for future reference
|
|
665
701
|
(this as any)._zoneIdMap = (this as any)._zoneIdMap || new Map();
|
|
666
702
|
(this as any)._zoneIdMap.set(zoneId, physicZoneId);
|
|
667
|
-
|
|
703
|
+
|
|
668
704
|
// Store the shape
|
|
669
705
|
this._attachedShapes.set(zoneId, shape);
|
|
670
|
-
|
|
706
|
+
|
|
671
707
|
// Update shape position when player moves
|
|
672
708
|
const updateShapePosition = () => {
|
|
673
709
|
const currentEntity = map.physic.getEntityByUUID(this.id);
|
|
@@ -678,7 +714,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
678
714
|
}
|
|
679
715
|
}
|
|
680
716
|
};
|
|
681
|
-
|
|
717
|
+
|
|
682
718
|
// Listen to position changes to update shape position
|
|
683
719
|
playerEntity.onPositionChange(() => {
|
|
684
720
|
updateShapePosition();
|
|
@@ -686,7 +722,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
686
722
|
|
|
687
723
|
return shape;
|
|
688
724
|
}
|
|
689
|
-
|
|
725
|
+
|
|
690
726
|
/**
|
|
691
727
|
* Get all shapes attached to this player
|
|
692
728
|
*
|
|
@@ -706,7 +742,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
706
742
|
getShapes(): RpgShape[] {
|
|
707
743
|
return Array.from(this._attachedShapes.values());
|
|
708
744
|
}
|
|
709
|
-
|
|
745
|
+
|
|
710
746
|
/**
|
|
711
747
|
* Get all shapes where this player is currently located
|
|
712
748
|
*
|
|
@@ -1053,13 +1089,13 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
1053
1089
|
if (typeof height !== 'number' || height <= 0) {
|
|
1054
1090
|
throw new Error('setHitbox: height must be a positive number');
|
|
1055
1091
|
}
|
|
1056
|
-
|
|
1092
|
+
|
|
1057
1093
|
// Update hitbox signal
|
|
1058
1094
|
this.hitbox.set({
|
|
1059
1095
|
w: width,
|
|
1060
1096
|
h: height,
|
|
1061
1097
|
});
|
|
1062
|
-
|
|
1098
|
+
|
|
1063
1099
|
// Update physics entity if map exists
|
|
1064
1100
|
const map = this.getCurrentMap();
|
|
1065
1101
|
if (map && map.physic) {
|
|
@@ -1081,6 +1117,10 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
|
|
|
1081
1117
|
}, this)
|
|
1082
1118
|
}
|
|
1083
1119
|
}
|
|
1120
|
+
|
|
1121
|
+
isEvent(): boolean {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1084
1124
|
}
|
|
1085
1125
|
|
|
1086
1126
|
export class RpgEvent extends RpgPlayer {
|
|
@@ -1099,6 +1139,10 @@ export class RpgEvent extends RpgPlayer {
|
|
|
1099
1139
|
if (!map) return;
|
|
1100
1140
|
map.removeEvent(this.id);
|
|
1101
1141
|
}
|
|
1142
|
+
|
|
1143
|
+
override isEvent(): boolean {
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1102
1146
|
}
|
|
1103
1147
|
|
|
1104
1148
|
|
|
@@ -1108,18 +1152,17 @@ export class RpgEvent extends RpgPlayer {
|
|
|
1108
1152
|
* Extends the RpgPlayer class with additional interfaces from mixins.
|
|
1109
1153
|
* This provides proper TypeScript support for all mixin methods and properties.
|
|
1110
1154
|
*/
|
|
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
|
-
{}
|
|
1155
|
+
export interface RpgPlayer extends
|
|
1156
|
+
IVariableManager,
|
|
1157
|
+
IMoveManager,
|
|
1158
|
+
IGoldManager,
|
|
1159
|
+
IComponentManager,
|
|
1160
|
+
IGuiManager,
|
|
1161
|
+
IItemManager,
|
|
1162
|
+
IEffectManager,
|
|
1163
|
+
IParameterManager,
|
|
1164
|
+
IElementManager,
|
|
1165
|
+
ISkillManager,
|
|
1166
|
+
IBattleManager,
|
|
1167
|
+
IClassManager,
|
|
1168
|
+
IStateManager { }
|
package/src/index.ts
CHANGED
|
@@ -12,4 +12,5 @@ export * from "@signe/reactive";
|
|
|
12
12
|
export * from "./Gui";
|
|
13
13
|
export { RpgShape, RpgModule } from "@rpgjs/common";
|
|
14
14
|
export * from "./decorators/event";
|
|
15
|
-
export * from "./decorators/map";
|
|
15
|
+
export * from "./decorators/map";
|
|
16
|
+
export * from "./Player/MoveManager";
|
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;
|