@rpgjs/server 5.0.0-beta.5 → 5.0.0-beta.6

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.
@@ -1,4 +1,4 @@
1
- import { a as isMapUpdateAuthorized, c as updateMap, h as injector, i as createMapUpdateHeaders, l as context, n as MAP_UPDATE_TOKEN_ENV, o as readMapUpdateToken, p as setInject, r as MAP_UPDATE_TOKEN_HEADER, s as resolveMapUpdateToken, t as provideServerModules } from "../module-BmvXIvlE.js";
1
+ import { a as isMapUpdateAuthorized, c as updateMap, h as injector, i as createMapUpdateHeaders, l as context, n as MAP_UPDATE_TOKEN_ENV, o as readMapUpdateToken, p as setInject, r as MAP_UPDATE_TOKEN_HEADER, s as resolveMapUpdateToken, t as provideServerModules } from "../module-Dy124Jyk.js";
2
2
  //#region src/node/connection.ts
3
3
  function readEnvVariable(name) {
4
4
  const value = globalThis.process?.env?.[name];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/server",
3
- "version": "5.0.0-beta.5",
3
+ "version": "5.0.0-beta.6",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {
@@ -22,9 +22,9 @@
22
22
  "description": "",
23
23
  "dependencies": {
24
24
  "@canvasengine/tiled": "2.0.0-beta.58",
25
- "@rpgjs/common": "5.0.0-beta.5",
26
- "@rpgjs/physic": "5.0.0-beta.5",
27
- "@rpgjs/testing": "5.0.0-beta.5",
25
+ "@rpgjs/common": "5.0.0-beta.6",
26
+ "@rpgjs/physic": "5.0.0-beta.6",
27
+ "@rpgjs/testing": "5.0.0-beta.6",
28
28
  "@rpgjs/database": "^4.3.0",
29
29
  "@signe/di": "^2.9.0",
30
30
  "@signe/reactive": "^2.9.0",
@@ -461,7 +461,7 @@ export class RpgPlayer extends BasicPlayerMixins(RpgCommonPlayer) {
461
461
  const centerX = positions.x + width / 2;
462
462
  const centerY = positions.y + height / 2;
463
463
 
464
- this.map.physic.teleport(entity, { x: centerX, y: centerY });
464
+ this.map.physic.teleportEntity(entity, { x: centerX, y: centerY });
465
465
  }
466
466
  }
467
467
  this.x.set(positions.x)
package/src/rooms/map.ts CHANGED
@@ -24,7 +24,6 @@ import { Subject } from "rxjs";
24
24
  import { BehaviorSubject } from "rxjs";
25
25
  import { COEFFICIENT_ELEMENTS, DAMAGE_CRITICAL, DAMAGE_PHYSIC, DAMAGE_SKILL } from "../presets";
26
26
  import { z } from "zod";
27
- import { EntityState } from "@rpgjs/physic";
28
27
  import { MapOptions } from "../decorators/map";
29
28
  import { EventMode } from "../decorators/event";
30
29
  import { BaseRoom } from "./BaseRoom";
@@ -1054,10 +1053,20 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
1054
1053
  */
1055
1054
  @Action('action')
1056
1055
  onAction(player: RpgPlayer, action: any) {
1057
- // Get collisions using the helper method from RpgCommonMap
1058
- const collisions = (this as any).getCollisions(player.id);
1059
- const events = collisions
1060
- .map(id => this.getEvent(id))
1056
+ const direction =
1057
+ typeof player.getDirection === "function"
1058
+ ? player.getDirection()
1059
+ : typeof player.direction === "function"
1060
+ ? player.direction()
1061
+ : undefined;
1062
+ const collisions = new Set<string>((this as any).getCollisions(player.id));
1063
+ const interactionCollisions = (this as any).getInteractionCollisions?.(player.id, direction);
1064
+ if (Array.isArray(interactionCollisions)) {
1065
+ interactionCollisions.forEach(id => collisions.add(id));
1066
+ }
1067
+
1068
+ const events = Array.from(collisions)
1069
+ .map(id => this.getEvent<RpgEvent>(id))
1061
1070
  .filter((event): event is RpgEvent => !!event && this.isEventVisibleForPlayer(event, player));
1062
1071
  if (events.length > 0) {
1063
1072
  events.forEach(event => {
@@ -2500,18 +2509,15 @@ export class RpgMap extends RpgCommonMap<RpgPlayer> implements RoomOnJoin {
2500
2509
  const centerX = x + width / 2;
2501
2510
  const centerY = y + height / 2;
2502
2511
 
2503
- // Create static entity (hitbox) in physics engine
2512
+ // Create static obstacle in physics engine
2504
2513
  const entityId = `shape-${name}`;
2505
- const entity = this.physic.createEntity({
2506
- uuid: entityId,
2507
- position: { x: centerX, y: centerY },
2508
- width: width,
2509
- height: height,
2510
- mass: Infinity, // Static entity
2511
- state: EntityState.Static,
2512
- restitution: 0, // No bounce
2514
+ this.physic.createStaticObstacle(entityId, {
2515
+ x: centerX,
2516
+ y: centerY,
2517
+ width,
2518
+ height,
2519
+ restitution: 0,
2513
2520
  });
2514
- entity.freeze(); // Ensure it's frozen
2515
2521
 
2516
2522
  // Build properties object
2517
2523
  const properties: Record<string, any> = {
@@ -0,0 +1,97 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ import { testing, TestingFixture } from "@rpgjs/testing";
3
+ import { Control, createModule, defineModule, Direction } from "@rpgjs/common";
4
+ import { RpgClient } from "../../client/src";
5
+ import { RpgPlayer, RpgServer } from "../src";
6
+
7
+ let actionCount = 0;
8
+
9
+ const serverModule = defineModule<RpgServer>({
10
+ maps: [
11
+ {
12
+ id: "interaction-map",
13
+ file: "",
14
+ },
15
+ ],
16
+ player: {
17
+ async onConnected(player) {
18
+ await player.changeMap("interaction-map", { x: 100, y: 100 });
19
+ },
20
+ },
21
+ });
22
+
23
+ const clientModule = defineModule<RpgClient>({});
24
+
25
+ let fixture: TestingFixture;
26
+ let client: any;
27
+ let player: RpgPlayer;
28
+
29
+ beforeEach(async () => {
30
+ actionCount = 0;
31
+ const myModule = createModule("ActionInteractionTestModule", [
32
+ {
33
+ server: serverModule,
34
+ client: clientModule,
35
+ },
36
+ ]);
37
+
38
+ fixture = await testing(myModule);
39
+ client = await fixture.createClient();
40
+ player = await client.waitForMapChange("interaction-map");
41
+ });
42
+
43
+ afterEach(async () => {
44
+ await fixture.clear();
45
+ });
46
+
47
+ describe("Action interactions", () => {
48
+ test("triggers onAction for an event just in front of the player", async () => {
49
+ const map = player.getCurrentMap() as any;
50
+ const hitbox = player.hitbox();
51
+
52
+ await map.createDynamicEvent({
53
+ id: "front-event",
54
+ x: player.x(),
55
+ y: player.y() + hitbox.h + 2,
56
+ event: {
57
+ onAction() {
58
+ actionCount += 1;
59
+ },
60
+ },
61
+ });
62
+ await fixture.nextTick();
63
+
64
+ player.changeDirection(Direction.Down);
65
+
66
+ expect(map.getCollisions(player.id)).not.toContain("front-event");
67
+ expect(map.getInteractionCollisions(player.id, Direction.Down)).toContain("front-event");
68
+
69
+ map.onAction(player, { action: Control.Action });
70
+ await fixture.wait(0);
71
+
72
+ expect(actionCount).toBe(1);
73
+ });
74
+
75
+ test("ignores a nearby event behind the player", async () => {
76
+ const map = player.getCurrentMap() as any;
77
+ const hitbox = player.hitbox();
78
+
79
+ await map.createDynamicEvent({
80
+ id: "behind-event",
81
+ x: player.x(),
82
+ y: player.y() - hitbox.h - 2,
83
+ event: {
84
+ onAction() {
85
+ actionCount += 1;
86
+ },
87
+ },
88
+ });
89
+ await fixture.nextTick();
90
+
91
+ player.changeDirection(Direction.Down);
92
+ map.onAction(player, { action: Control.Action });
93
+ await fixture.wait(0);
94
+
95
+ expect(actionCount).toBe(0);
96
+ });
97
+ });
@@ -1,10 +1,11 @@
1
1
  import { beforeEach, test, expect, afterEach, describe, vi } from "vitest";
2
2
  import { testing, waitForSyncComplete, TestingFixture } from "@rpgjs/testing";
3
3
  import { defineModule, createModule } from "@rpgjs/common";
4
- import { RpgPlayer, RpgServer } from "../src";
4
+ import { RpgEvent, RpgPlayer, RpgServer } from "../src";
5
5
  import { RpgClient } from "../../client/src";
6
6
  import { ItemLog } from "../src/logs";
7
7
  import type { ItemObject } from "../src/Player/ItemManager";
8
+ import { syncClass } from "@signe/sync";
8
9
 
9
10
  // Define test items as objects for database
10
11
  const TestPotion = {
@@ -124,6 +125,76 @@ describe("Item Management - Basic Operations", () => {
124
125
  expect(item.quantity()).toBe(5);
125
126
  expect(item.name()).toBe("Test Potion");
126
127
  });
128
+
129
+ test("should sync cloneable event items and equipments initialized before sync", () => {
130
+ const event = new RpgEvent();
131
+ event.id = "enemy-event";
132
+ event.setMap(player.getCurrentMap()!);
133
+ event.execMethod = vi.fn() as any;
134
+ event.addItem("TestSword", 1);
135
+ event.equip("TestSword", true);
136
+
137
+ const packets: any[] = [];
138
+ const memory: Record<string, any> = {};
139
+ const setPath = (target: Record<string, any>, path: string, value: any) => {
140
+ const parts = path.split(".");
141
+ let current = target;
142
+ for (let i = 0; i < parts.length - 1; i++) {
143
+ current = current[parts[i]] ??= {};
144
+ }
145
+ current[parts[parts.length - 1]] = value;
146
+ };
147
+
148
+ syncClass(event, {
149
+ onSync(values) {
150
+ const packet: Record<string, any> = {};
151
+ for (const [path, value] of values) {
152
+ setPath(packet, path, value);
153
+ setPath(memory, path, value);
154
+ }
155
+ structuredClone({ type: "sync", value: packet });
156
+ packets.push(packet);
157
+ values.clear();
158
+ },
159
+ });
160
+
161
+ expect(packets).toContainEqual({
162
+ items: {
163
+ "0": expect.objectContaining({
164
+ id: "TestSword",
165
+ name: "Test Sword",
166
+ quantity: 1,
167
+ }),
168
+ },
169
+ });
170
+ expect(packets).toContainEqual({
171
+ equipments: {
172
+ "0": expect.objectContaining({
173
+ id: "TestSword",
174
+ name: "Test Sword",
175
+ quantity: 1,
176
+ }),
177
+ },
178
+ });
179
+ expect(memory).toEqual(
180
+ expect.objectContaining({
181
+ items: {
182
+ "0": expect.objectContaining({
183
+ id: "TestSword",
184
+ name: "Test Sword",
185
+ quantity: 1,
186
+ }),
187
+ },
188
+ equipments: {
189
+ "0": expect.objectContaining({
190
+ id: "TestSword",
191
+ name: "Test Sword",
192
+ quantity: 1,
193
+ }),
194
+ },
195
+ })
196
+ );
197
+ });
127
198
 
128
199
 
129
200
  test("should add item using object", () => {
@@ -48,6 +48,17 @@ afterEach(async () => {
48
48
 
49
49
 
50
50
  describe("Move Routes - Basic Movements", () => {
51
+ test("should create player physics body with rectangular RPG hitbox and speed", async () => {
52
+ const map = player.getCurrentMap() as any;
53
+ const entity = map.getBody(player.id);
54
+ const hitbox = player.hitbox();
55
+
56
+ expect(entity).toBeDefined();
57
+ expect(entity.width).toBe(hitbox.w);
58
+ expect(entity.height).toBe(hitbox.h);
59
+ expect(entity.radius).toBe(0);
60
+ expect(entity.maxLinearVelocity).toBe(player.speed() * 50);
61
+ });
51
62
 
52
63
  test("should move right using Direction enum", async () => {
53
64
  const initialX = player.x();
@@ -117,6 +128,59 @@ describe("Move Routes - Basic Movements", () => {
117
128
  });
118
129
  });
119
130
 
131
+ describe("Map Shapes", () => {
132
+ test("should create static obstacle body for map shape", async () => {
133
+ const map = player.getCurrentMap() as any;
134
+ const shape = map.createShape({
135
+ x: 40,
136
+ y: 56,
137
+ width: 24,
138
+ height: 32,
139
+ name: "test-obstacle",
140
+ });
141
+ const entity = map.physic.getEntityByUUID("shape-test-obstacle");
142
+
143
+ expect(shape.name).toBe("test-obstacle");
144
+ expect(entity).toBeDefined();
145
+ expect(entity.position.x).toBe(52);
146
+ expect(entity.position.y).toBe(72);
147
+ expect(entity.width).toBe(24);
148
+ expect(entity.height).toBe(32);
149
+ expect(entity.isStatic()).toBe(true);
150
+ });
151
+
152
+ test("should detect entities with moving hitbox sensor", async () => {
153
+ const map = player.getCurrentMap() as any;
154
+ const hitbox = player.hitbox();
155
+ const hitsPromise = new Promise<any[]>((resolve, reject) => {
156
+ let subscription: { unsubscribe: () => void };
157
+ const timeout = setTimeout(() => {
158
+ subscription.unsubscribe();
159
+ reject(new Error("moving hitbox did not detect player"));
160
+ }, 500);
161
+ subscription = map.createMovingHitbox([
162
+ {
163
+ x: player.x(),
164
+ y: player.y(),
165
+ width: hitbox.w,
166
+ height: hitbox.h,
167
+ },
168
+ ]).subscribe({
169
+ next: (hits: any[]) => {
170
+ clearTimeout(timeout);
171
+ subscription.unsubscribe();
172
+ resolve(hits);
173
+ },
174
+ error: reject,
175
+ });
176
+ map.physic.getZoneManager().update();
177
+ });
178
+
179
+ const hits = await hitsPromise;
180
+ expect(hits.map((hit) => hit.id)).toContain(player.id);
181
+ });
182
+ });
183
+
120
184
  describe("Move Routes - Move Helper Functions", () => {
121
185
  test("should move right using Move.right()", async () => {
122
186
  const initialX = player.x();
@@ -598,4 +662,4 @@ describe("Move Routes - Stuck Detection", () => {
598
662
  // Player should have moved successfully
599
663
  expect(player.x()).toBeGreaterThan(initialX);
600
664
  });
601
- });
665
+ });
@@ -396,6 +396,10 @@ describe('Map WorldMapsManager Integration', () => {
396
396
  const worldY = player.worldPositionY()
397
397
  expect(worldX).toBe(100)
398
398
  expect(worldY).toBe(200)
399
+ const entity = map?.physic.getEntityByUUID(player.id)
400
+ const hitbox = player.hitbox()
401
+ expect(entity?.position.x).toBe(100 + hitbox.w / 2)
402
+ expect(entity?.position.y).toBe(200 + hitbox.h / 2)
399
403
 
400
404
  // Change to map2 which is at worldX: 1024, worldY: 0
401
405
  await player.changeMap('map2', { x: 50, y: 100 })