@rpgjs/client 5.0.0-beta.20 → 5.0.0-beta.23
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/CHANGELOG.md +16 -0
- package/dist/Game/Object.d.ts +1 -0
- package/dist/Game/Object.js +10 -0
- package/dist/Game/Object.js.map +1 -1
- package/dist/Gui/Gui.js +37 -4
- package/dist/Gui/Gui.js.map +1 -1
- package/dist/RpgClientEngine.d.ts +18 -9
- package/dist/RpgClientEngine.js +40 -13
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/character-hitbox.d.ts +13 -0
- package/dist/components/character-hitbox.js +38 -0
- package/dist/components/character-hitbox.js.map +1 -0
- package/dist/components/character-hitbox.spec.d.ts +1 -0
- package/dist/components/character.ce.js +179 -36
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/gui/mobile/index.d.ts +51 -2
- package/dist/components/gui/mobile/index.js +12 -4
- package/dist/components/gui/mobile/index.js.map +1 -1
- package/dist/components/gui/mobile/index.spec.d.ts +1 -0
- package/dist/components/gui/mobile/mobile.ce.js +303 -55
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/services/cameraFollow.d.ts +51 -0
- package/dist/services/cameraFollow.js +134 -0
- package/dist/services/cameraFollow.js.map +1 -0
- package/dist/services/cameraFollow.spec.d.ts +1 -0
- package/dist/services/standalone.d.ts +6 -1
- package/dist/services/standalone.js +35 -17
- package/dist/services/standalone.js.map +1 -1
- package/dist/utils/syncHitbox.d.ts +1 -0
- package/dist/utils/syncHitbox.js +69 -0
- package/dist/utils/syncHitbox.js.map +1 -0
- package/dist/utils/syncHitbox.spec.d.ts +1 -0
- package/package.json +5 -5
- package/src/Game/Object.spec.ts +44 -4
- package/src/Game/Object.ts +19 -0
- package/src/Gui/Gui.spec.ts +19 -0
- package/src/Gui/Gui.ts +50 -11
- package/src/RpgClientEngine.ts +56 -24
- package/src/components/character-hitbox.spec.ts +33 -0
- package/src/components/character-hitbox.ts +72 -0
- package/src/components/character.ce +207 -48
- package/src/components/gui/mobile/index.spec.ts +94 -0
- package/src/components/gui/mobile/index.ts +74 -6
- package/src/components/gui/mobile/mobile.ce +347 -65
- package/src/index.ts +12 -0
- package/src/services/cameraFollow.spec.ts +220 -0
- package/src/services/cameraFollow.ts +222 -0
- package/src/services/standalone.spec.ts +47 -0
- package/src/services/standalone.ts +53 -20
- package/src/utils/syncHitbox.spec.ts +79 -0
- package/src/utils/syncHitbox.ts +104 -0
|
@@ -13,6 +13,9 @@ declare class BridgeWebsocket extends AbstractWebsocket {
|
|
|
13
13
|
private room;
|
|
14
14
|
private socket;
|
|
15
15
|
private socketRoom?;
|
|
16
|
+
private roomId;
|
|
17
|
+
private query?;
|
|
18
|
+
private readonly privateId;
|
|
16
19
|
private listeners;
|
|
17
20
|
private rooms;
|
|
18
21
|
private serverInstance;
|
|
@@ -38,7 +41,7 @@ declare class BridgeWebsocket extends AbstractWebsocket {
|
|
|
38
41
|
* await websocket.reconnect()
|
|
39
42
|
* ```
|
|
40
43
|
*/
|
|
41
|
-
updateProperties(
|
|
44
|
+
updateProperties({ room, query }: SocketUpdateProperties): void;
|
|
42
45
|
/**
|
|
43
46
|
* Reconnect the client to the current Party room
|
|
44
47
|
*
|
|
@@ -57,6 +60,8 @@ declare class BridgeWebsocket extends AbstractWebsocket {
|
|
|
57
60
|
* ```
|
|
58
61
|
*/
|
|
59
62
|
reconnect(listeners?: (data: any) => void): Promise<void>;
|
|
63
|
+
private connectToRoom;
|
|
64
|
+
private normalizeQuery;
|
|
60
65
|
private detachCurrentSocket;
|
|
61
66
|
getServer(): any;
|
|
62
67
|
getSocket(): any;
|
|
@@ -13,14 +13,15 @@ var BridgeWebsocket = class extends AbstractWebsocket {
|
|
|
13
13
|
this.context = context;
|
|
14
14
|
this.server = server;
|
|
15
15
|
this.mode = "standalone";
|
|
16
|
+
this.roomId = "lobby-1";
|
|
17
|
+
this.privateId = "player-client-id";
|
|
16
18
|
this.listeners = [];
|
|
17
19
|
this.rooms = {
|
|
18
20
|
partyFn: async (roomId) => {
|
|
19
|
-
|
|
20
|
-
const server = new this.server(
|
|
21
|
+
const room = new ServerIo(roomId, this.rooms);
|
|
22
|
+
const server = new this.server(room);
|
|
21
23
|
await server.onStart();
|
|
22
24
|
await server.subRoom.onStart();
|
|
23
|
-
this.context.set("server", server);
|
|
24
25
|
return server;
|
|
25
26
|
},
|
|
26
27
|
env: {}
|
|
@@ -37,20 +38,8 @@ var BridgeWebsocket = class extends AbstractWebsocket {
|
|
|
37
38
|
}
|
|
38
39
|
async _connection(listeners) {
|
|
39
40
|
this.detachCurrentSocket();
|
|
40
|
-
|
|
41
|
-
this.socket = new ClientIo(this.serverInstance, "player-client-id");
|
|
42
|
-
const url = new URL("http://localhost");
|
|
43
|
-
const request = new Request(url.toString(), {
|
|
44
|
-
method: "GET",
|
|
45
|
-
headers: { "Content-Type": "application/json" }
|
|
46
|
-
});
|
|
41
|
+
await this.connectToRoom();
|
|
47
42
|
listeners?.(this.socket);
|
|
48
|
-
this.room.clients.set(this.socket.id, this.socket);
|
|
49
|
-
this.socketRoom = this.room;
|
|
50
|
-
this.listeners.forEach(({ handler }) => {
|
|
51
|
-
this.socket.addEventListener("message", handler);
|
|
52
|
-
});
|
|
53
|
-
await this.serverInstance.onConnect(this.socket.conn, { request });
|
|
54
43
|
return this.socket;
|
|
55
44
|
}
|
|
56
45
|
on(key, callback) {
|
|
@@ -99,7 +88,10 @@ var BridgeWebsocket = class extends AbstractWebsocket {
|
|
|
99
88
|
* await websocket.reconnect()
|
|
100
89
|
* ```
|
|
101
90
|
*/
|
|
102
|
-
updateProperties(
|
|
91
|
+
updateProperties({ room, query }) {
|
|
92
|
+
this.roomId = room;
|
|
93
|
+
this.query = query;
|
|
94
|
+
}
|
|
103
95
|
/**
|
|
104
96
|
* Reconnect the client to the current Party room
|
|
105
97
|
*
|
|
@@ -122,6 +114,32 @@ var BridgeWebsocket = class extends AbstractWebsocket {
|
|
|
122
114
|
listeners?.(socket);
|
|
123
115
|
});
|
|
124
116
|
}
|
|
117
|
+
async connectToRoom() {
|
|
118
|
+
if (this.serverInstance?.room?.id !== this.roomId) {
|
|
119
|
+
const party = await this.room.context.parties.main.get(this.roomId);
|
|
120
|
+
this.serverInstance = party.server;
|
|
121
|
+
this.context.set("server", this.serverInstance);
|
|
122
|
+
this.room = this.serverInstance.room;
|
|
123
|
+
} else this.serverInstance = this.context.get("server");
|
|
124
|
+
this.socket = new ClientIo(this.serverInstance, this.privateId);
|
|
125
|
+
const url = new URL("http://localhost");
|
|
126
|
+
const query = this.normalizeQuery(this.query);
|
|
127
|
+
if (query) for (const [key, value] of Object.entries(query)) url.searchParams.set(key, value);
|
|
128
|
+
const request = new Request(url.toString(), {
|
|
129
|
+
method: "GET",
|
|
130
|
+
headers: { "Content-Type": "application/json" }
|
|
131
|
+
});
|
|
132
|
+
this.room.clients.set(this.socket.id, this.socket);
|
|
133
|
+
this.socketRoom = this.room;
|
|
134
|
+
this.listeners.forEach(({ handler }) => {
|
|
135
|
+
this.socket.addEventListener("message", handler);
|
|
136
|
+
});
|
|
137
|
+
await this.serverInstance.onConnect(this.socket.conn, { request });
|
|
138
|
+
}
|
|
139
|
+
normalizeQuery(query) {
|
|
140
|
+
if (!query) return;
|
|
141
|
+
return Object.fromEntries(Object.entries(query).filter((entry) => typeof entry[1] === "string"));
|
|
142
|
+
}
|
|
125
143
|
detachCurrentSocket() {
|
|
126
144
|
if (!this.socket) return;
|
|
127
145
|
this.listeners.forEach(({ handler }) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standalone.js","names":[],"sources":["../../src/services/standalone.ts"],"sourcesContent":["import { AbstractWebsocket, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { ClientIo, ServerIo } from \"@signe/room\";\nimport { Context } from \"@signe/di\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { LoadMapToken } from \"./loadMap\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { normalizeStandaloneMessage } from \"./standalone-message\";\n\ntype ServerIo = any;\ntype ClientIo = any;\n\ninterface StandaloneOptions {\n env?: Record<string, any>;\n}\n\nclass BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"standalone\" as const;\n\n private room: ServerIo;\n private socket: ClientIo;\n private socketRoom?: ServerIo;\n private listeners: Array<{\n event: string;\n callback: (data: any) => void;\n handler: (event: any) => void;\n }> = [];\n private rooms = {\n partyFn: async (roomId: string) => {\n this.room = new ServerIo(roomId, this.rooms);\n const server = new this.server(this.room)\n await server.onStart();\n await server.subRoom.onStart()\n this.context.set('server', server)\n return server\n },\n env: {}\n }\n private serverInstance: any;\n\n constructor(protected context: Context, private server: any, options: StandaloneOptions = {}) {\n super(context);\n // fake room\n this.rooms.env = options.env || {};\n this.room = new ServerIo(\"lobby-1\", this.rooms);\n }\n\n async connection(listeners?: (data: any) => void) {\n this.serverInstance = new this.server(this.room);\n await this.serverInstance.onStart();\n await this.serverInstance.subRoom.onStart()\n this.context.set('server', this.serverInstance)\n return this._connection(listeners)\n }\n\n private async _connection(listeners?: (data: any) => void) {\n this.detachCurrentSocket();\n this.serverInstance = this.context.get('server')\n this.socket = new ClientIo(this.serverInstance, 'player-client-id');\n const url = new URL('http://localhost')\n const request = new Request(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json'\n }\n })\n listeners?.(this.socket)\n this.room.clients.set(this.socket.id, this.socket);\n this.socketRoom = this.room;\n this.listeners.forEach(({ handler }) => {\n this.socket.addEventListener(\"message\", handler);\n });\n await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);\n return this.socket\n }\n\n on(key: string, callback: (data: any) => void) {\n if (\n this.listeners.some(\n (listener) => listener.event === key && listener.callback === callback\n )\n ) {\n return;\n }\n const handler = (event) => {\n const object = normalizeStandaloneMessage(event);\n if (object.type === key) {\n callback(object.value);\n }\n };\n this.listeners.push({ event: key, callback, handler });\n this.socket?.addEventListener(\"message\", handler);\n }\n\n off(event: string, callback: (data: any) => void) {\n const remaining: typeof this.listeners = [];\n for (const listener of this.listeners) {\n if (listener.event === event && listener.callback === callback) {\n this.socket?.removeEventListener(\"message\", listener.handler);\n continue;\n }\n remaining.push(listener);\n }\n this.listeners = remaining;\n }\n\n emit(event: string, data: any) {\n this.socket.send({\n action: event,\n value: data,\n });\n }\n\n /**\n * Update underlying connection properties before a reconnect\n *\n * Design\n * - Dynamically register a factory for the requested room to ensure a fresh server instance\n * - Swap the internal ServerIo to target the new room\n *\n * @param params - Properties to update\n * @param params.room - The target room id (e.g. `map-simplemap2`)\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-simplemap2' })\n * await websocket.reconnect()\n * ```\n */\n updateProperties(_params: SocketUpdateProperties) {\n // empty\n }\n\n /**\n * Reconnect the client to the current Party room\n *\n * Design\n * - Must be called after `updateProperties()` when switching rooms\n * - Rebuilds the client <-> server bridge and re-triggers connection listeners\n *\n * @param listeners - Optional callback to re-bind event handlers on the new socket\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-dungeon' })\n * await websocket.reconnect((socket) => {\n * // re-bind events here\n * })\n * ```\n */\n async reconnect(listeners?: (data: any) => void): Promise<void> {\n await this._connection((socket) => {\n listeners?.(socket)\n })\n }\n\n private detachCurrentSocket() {\n if (!this.socket) return;\n this.listeners.forEach(({ handler }) => {\n this.socket.removeEventListener(\"message\", handler);\n });\n this.socketRoom?.clients?.delete?.(this.socket.id);\n this.socket = undefined as any;\n this.socketRoom = undefined;\n }\n\n getServer() {\n return this.serverInstance\n }\n\n getSocket() {\n return this.socket\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n private server: any;\n\n /**\n * Update the current room map data on the server side\n *\n * Design\n * - Uses the in-memory server instance stored in context (standalone mode)\n * - Builds a local HTTP-like request to the current Party room endpoint\n *\n * @param map - The map payload to apply on the server\n *\n * @example\n * ```ts\n * await updateMapService.update({ width: 1024, height: 768, events: [] })\n * ```\n */\n async update(map: any) {\n this.server = this.context.get('server')\n const roomId = this.server?.room?.id ?? 'lobby-1'\n const req = {\n url: `http://localhost/parties/main/${roomId}/map/update`,\n method: 'POST',\n headers: new Headers({}),\n json: async () => {\n return map;\n }\n };\n await this.server.onRequest(req)\n }\n}\n\nexport function provideRpg(server: any, options: StandaloneOptions = {}) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, server, options),\n },\n {\n provide: UpdateMapToken,\n useClass: UpdateMapStandaloneService,\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"standalone.js","names":[],"sources":["../../src/services/standalone.ts"],"sourcesContent":["import { AbstractWebsocket, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { ClientIo, ServerIo } from \"@signe/room\";\nimport { Context } from \"@signe/di\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { LoadMapToken } from \"./loadMap\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { normalizeStandaloneMessage } from \"./standalone-message\";\nimport type { SocketQuery } from \"./AbstractSocket\";\n\ntype ServerIo = any;\ntype ClientIo = any;\n\ninterface StandaloneOptions {\n env?: Record<string, any>;\n}\n\nclass BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"standalone\" as const;\n\n private room: ServerIo;\n private socket: ClientIo;\n private socketRoom?: ServerIo;\n private roomId = \"lobby-1\";\n private query?: SocketQuery;\n private readonly privateId = \"player-client-id\";\n private listeners: Array<{\n event: string;\n callback: (data: any) => void;\n handler: (event: any) => void;\n }> = [];\n private rooms = {\n partyFn: async (roomId: string) => {\n const room = new ServerIo(roomId, this.rooms);\n const server = new this.server(room)\n await server.onStart();\n await server.subRoom.onStart()\n return server\n },\n env: {}\n }\n private serverInstance: any;\n\n constructor(protected context: Context, private server: any, options: StandaloneOptions = {}) {\n super(context);\n // fake room\n this.rooms.env = options.env || {};\n this.room = new ServerIo(\"lobby-1\", this.rooms);\n }\n\n async connection(listeners?: (data: any) => void) {\n this.serverInstance = new this.server(this.room);\n await this.serverInstance.onStart();\n await this.serverInstance.subRoom.onStart()\n this.context.set('server', this.serverInstance)\n return this._connection(listeners)\n }\n\n private async _connection(listeners?: (data: any) => void) {\n this.detachCurrentSocket();\n await this.connectToRoom();\n listeners?.(this.socket)\n return this.socket\n }\n\n on(key: string, callback: (data: any) => void) {\n if (\n this.listeners.some(\n (listener) => listener.event === key && listener.callback === callback\n )\n ) {\n return;\n }\n const handler = (event) => {\n const object = normalizeStandaloneMessage(event);\n if (object.type === key) {\n callback(object.value);\n }\n };\n this.listeners.push({ event: key, callback, handler });\n this.socket?.addEventListener(\"message\", handler);\n }\n\n off(event: string, callback: (data: any) => void) {\n const remaining: typeof this.listeners = [];\n for (const listener of this.listeners) {\n if (listener.event === event && listener.callback === callback) {\n this.socket?.removeEventListener(\"message\", listener.handler);\n continue;\n }\n remaining.push(listener);\n }\n this.listeners = remaining;\n }\n\n emit(event: string, data: any) {\n this.socket.send({\n action: event,\n value: data,\n });\n }\n\n /**\n * Update underlying connection properties before a reconnect\n *\n * Design\n * - Dynamically register a factory for the requested room to ensure a fresh server instance\n * - Swap the internal ServerIo to target the new room\n *\n * @param params - Properties to update\n * @param params.room - The target room id (e.g. `map-simplemap2`)\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-simplemap2' })\n * await websocket.reconnect()\n * ```\n */\n updateProperties({ room, query }: SocketUpdateProperties) {\n this.roomId = room;\n this.query = query;\n }\n\n /**\n * Reconnect the client to the current Party room\n *\n * Design\n * - Must be called after `updateProperties()` when switching rooms\n * - Rebuilds the client <-> server bridge and re-triggers connection listeners\n *\n * @param listeners - Optional callback to re-bind event handlers on the new socket\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-dungeon' })\n * await websocket.reconnect((socket) => {\n * // re-bind events here\n * })\n * ```\n */\n async reconnect(listeners?: (data: any) => void): Promise<void> {\n await this._connection((socket) => {\n listeners?.(socket)\n })\n }\n\n private async connectToRoom() {\n if (this.serverInstance?.room?.id !== this.roomId) {\n const party = await this.room.context.parties.main.get(this.roomId);\n this.serverInstance = party.server;\n this.context.set('server', this.serverInstance);\n this.room = this.serverInstance.room;\n }\n else {\n this.serverInstance = this.context.get('server');\n }\n\n this.socket = new ClientIo(this.serverInstance, this.privateId);\n const url = new URL('http://localhost');\n const query = this.normalizeQuery(this.query);\n if (query) {\n for (const [key, value] of Object.entries(query)) {\n url.searchParams.set(key, value);\n }\n }\n const request = new Request(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json'\n }\n });\n this.room.clients.set(this.socket.id, this.socket);\n this.socketRoom = this.room;\n this.listeners.forEach(({ handler }) => {\n this.socket.addEventListener(\"message\", handler);\n });\n await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);\n }\n\n private normalizeQuery(query?: SocketQuery): Record<string, string> | undefined {\n if (!query) {\n return undefined;\n }\n return Object.fromEntries(\n Object.entries(query)\n .filter((entry): entry is [string, string] => typeof entry[1] === \"string\")\n );\n }\n\n private detachCurrentSocket() {\n if (!this.socket) return;\n this.listeners.forEach(({ handler }) => {\n this.socket.removeEventListener(\"message\", handler);\n });\n this.socketRoom?.clients?.delete?.(this.socket.id);\n this.socket = undefined as any;\n this.socketRoom = undefined;\n }\n\n getServer() {\n return this.serverInstance\n }\n\n getSocket() {\n return this.socket\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n private server: any;\n\n /**\n * Update the current room map data on the server side\n *\n * Design\n * - Uses the in-memory server instance stored in context (standalone mode)\n * - Builds a local HTTP-like request to the current Party room endpoint\n *\n * @param map - The map payload to apply on the server\n *\n * @example\n * ```ts\n * await updateMapService.update({ width: 1024, height: 768, events: [] })\n * ```\n */\n async update(map: any) {\n this.server = this.context.get('server')\n const roomId = this.server?.room?.id ?? 'lobby-1'\n const req = {\n url: `http://localhost/parties/main/${roomId}/map/update`,\n method: 'POST',\n headers: new Headers({}),\n json: async () => {\n return map;\n }\n };\n await this.server.onRequest(req)\n }\n}\n\nexport function provideRpg(server: any, options: StandaloneOptions = {}) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, server, options),\n },\n {\n provide: UpdateMapToken,\n useClass: UpdateMapStandaloneService,\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const applySyncedHitboxPayload: (sceneMap: any, payload: any) => void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//#region src/utils/syncHitbox.ts
|
|
2
|
+
var toPositiveNumber = (value) => {
|
|
3
|
+
const numberValue = typeof value === "string" ? Number(value) : value;
|
|
4
|
+
return typeof numberValue === "number" && Number.isFinite(numberValue) && numberValue > 0 ? Math.round(numberValue) : null;
|
|
5
|
+
};
|
|
6
|
+
var readSignalValue = (value) => {
|
|
7
|
+
if (typeof value === "function") try {
|
|
8
|
+
return value();
|
|
9
|
+
} catch {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
14
|
+
var normalizeHitbox = (source) => {
|
|
15
|
+
if (!source || typeof source !== "object") return null;
|
|
16
|
+
const hitbox = source;
|
|
17
|
+
const width = toPositiveNumber(hitbox.w ?? hitbox.width);
|
|
18
|
+
const height = toPositiveNumber(hitbox.h ?? hitbox.height);
|
|
19
|
+
return width && height ? {
|
|
20
|
+
width,
|
|
21
|
+
height
|
|
22
|
+
} : null;
|
|
23
|
+
};
|
|
24
|
+
var readObjectHitbox = (object) => {
|
|
25
|
+
return normalizeHitbox(readSignalValue(object?.hitbox));
|
|
26
|
+
};
|
|
27
|
+
var readCoordinate = (object, key) => {
|
|
28
|
+
const value = readSignalValue(object?.[key]);
|
|
29
|
+
const numberValue = typeof value === "string" ? Number(value) : value;
|
|
30
|
+
return typeof numberValue === "number" && Number.isFinite(numberValue) ? numberValue : null;
|
|
31
|
+
};
|
|
32
|
+
var getObjectById = (sceneMap, id) => {
|
|
33
|
+
return sceneMap?.getObjectById?.(id) ?? sceneMap?.players?.()?.[id] ?? sceneMap?.events?.()?.[id];
|
|
34
|
+
};
|
|
35
|
+
var applyHitboxToObject = (sceneMap, id, object, hitbox) => {
|
|
36
|
+
if (!object) return;
|
|
37
|
+
const current = readObjectHitbox(object);
|
|
38
|
+
if (current?.width !== hitbox.width || current?.height !== hitbox.height) {
|
|
39
|
+
if (typeof object.hitbox?.set === "function") object.hitbox.set({
|
|
40
|
+
w: hitbox.width,
|
|
41
|
+
h: hitbox.height
|
|
42
|
+
});
|
|
43
|
+
else if (typeof object.setHitbox === "function") {
|
|
44
|
+
object.setHitbox(hitbox.width, hitbox.height);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const objectId = typeof object.id === "string" && object.id ? object.id : id;
|
|
49
|
+
const x = readCoordinate(object, "x");
|
|
50
|
+
const y = readCoordinate(object, "y");
|
|
51
|
+
if (objectId && x !== null && y !== null) sceneMap?.updateHitbox?.(objectId, x, y, hitbox.width, hitbox.height);
|
|
52
|
+
};
|
|
53
|
+
var applyHitboxCollection = (sceneMap, collection) => {
|
|
54
|
+
if (!collection || typeof collection !== "object") return;
|
|
55
|
+
for (const [id, patch] of Object.entries(collection)) {
|
|
56
|
+
if (!patch || typeof patch !== "object") continue;
|
|
57
|
+
const hitbox = normalizeHitbox(patch.hitbox);
|
|
58
|
+
if (!hitbox) continue;
|
|
59
|
+
applyHitboxToObject(sceneMap, id, getObjectById(sceneMap, id), hitbox);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
var applySyncedHitboxPayload = (sceneMap, payload) => {
|
|
63
|
+
applyHitboxCollection(sceneMap, payload?.players);
|
|
64
|
+
applyHitboxCollection(sceneMap, payload?.events);
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
export { applySyncedHitboxPayload };
|
|
68
|
+
|
|
69
|
+
//# sourceMappingURL=syncHitbox.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"syncHitbox.js","names":[],"sources":["../../src/utils/syncHitbox.ts"],"sourcesContent":["interface HitboxSize {\n width: number;\n height: number;\n}\n\nconst toPositiveNumber = (value: unknown): number | null => {\n const numberValue = typeof value === \"string\" ? Number(value) : value;\n return typeof numberValue === \"number\" && Number.isFinite(numberValue) && numberValue > 0\n ? Math.round(numberValue)\n : null;\n};\n\nconst readSignalValue = (value: any): any => {\n if (typeof value === \"function\") {\n try {\n return value();\n }\n catch {\n return undefined;\n }\n }\n return value;\n};\n\nconst normalizeHitbox = (source: unknown): HitboxSize | null => {\n if (!source || typeof source !== \"object\") {\n return null;\n }\n\n const hitbox = source as any;\n const width = toPositiveNumber(hitbox.w ?? hitbox.width);\n const height = toPositiveNumber(hitbox.h ?? hitbox.height);\n\n return width && height ? { width, height } : null;\n};\n\nconst readObjectHitbox = (object: any): HitboxSize | null => {\n return normalizeHitbox(readSignalValue(object?.hitbox));\n};\n\nconst readCoordinate = (object: any, key: \"x\" | \"y\"): number | null => {\n const value = readSignalValue(object?.[key]);\n const numberValue = typeof value === \"string\" ? Number(value) : value;\n return typeof numberValue === \"number\" && Number.isFinite(numberValue)\n ? numberValue\n : null;\n};\n\nconst getObjectById = (sceneMap: any, id: string): any => {\n return sceneMap?.getObjectById?.(id)\n ?? sceneMap?.players?.()?.[id]\n ?? sceneMap?.events?.()?.[id];\n};\n\nconst applyHitboxToObject = (\n sceneMap: any,\n id: string,\n object: any,\n hitbox: HitboxSize,\n): void => {\n if (!object) {\n return;\n }\n\n const current = readObjectHitbox(object);\n if (current?.width !== hitbox.width || current?.height !== hitbox.height) {\n if (typeof object.hitbox?.set === \"function\") {\n object.hitbox.set({ w: hitbox.width, h: hitbox.height });\n }\n else if (typeof object.setHitbox === \"function\") {\n object.setHitbox(hitbox.width, hitbox.height);\n return;\n }\n }\n\n const objectId = typeof object.id === \"string\" && object.id ? object.id : id;\n const x = readCoordinate(object, \"x\");\n const y = readCoordinate(object, \"y\");\n if (objectId && x !== null && y !== null) {\n sceneMap?.updateHitbox?.(objectId, x, y, hitbox.width, hitbox.height);\n }\n};\n\nconst applyHitboxCollection = (sceneMap: any, collection: unknown): void => {\n if (!collection || typeof collection !== \"object\") {\n return;\n }\n\n for (const [id, patch] of Object.entries(collection)) {\n if (!patch || typeof patch !== \"object\") {\n continue;\n }\n const hitbox = normalizeHitbox((patch as any).hitbox);\n if (!hitbox) {\n continue;\n }\n applyHitboxToObject(sceneMap, id, getObjectById(sceneMap, id), hitbox);\n }\n};\n\nexport const applySyncedHitboxPayload = (sceneMap: any, payload: any): void => {\n applyHitboxCollection(sceneMap, payload?.players);\n applyHitboxCollection(sceneMap, payload?.events);\n};\n"],"mappings":";AAKA,IAAM,oBAAoB,UAAkC;CAC1D,MAAM,cAAc,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI;CAChE,OAAO,OAAO,gBAAgB,YAAY,OAAO,SAAS,WAAW,KAAK,cAAc,IACpF,KAAK,MAAM,WAAW,IACtB;AACN;AAEA,IAAM,mBAAmB,UAAoB;CAC3C,IAAI,OAAO,UAAU,YACnB,IAAI;EACF,OAAO,MAAM;CACf,QACM;EACJ;CACF;CAEF,OAAO;AACT;AAEA,IAAM,mBAAmB,WAAuC;CAC9D,IAAI,CAAC,UAAU,OAAO,WAAW,UAC/B,OAAO;CAGT,MAAM,SAAS;CACf,MAAM,QAAQ,iBAAiB,OAAO,KAAK,OAAO,KAAK;CACvD,MAAM,SAAS,iBAAiB,OAAO,KAAK,OAAO,MAAM;CAEzD,OAAO,SAAS,SAAS;EAAE;EAAO;CAAO,IAAI;AAC/C;AAEA,IAAM,oBAAoB,WAAmC;CAC3D,OAAO,gBAAgB,gBAAgB,QAAQ,MAAM,CAAC;AACxD;AAEA,IAAM,kBAAkB,QAAa,QAAkC;CACrE,MAAM,QAAQ,gBAAgB,SAAS,IAAI;CAC3C,MAAM,cAAc,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI;CAChE,OAAO,OAAO,gBAAgB,YAAY,OAAO,SAAS,WAAW,IACjE,cACA;AACN;AAEA,IAAM,iBAAiB,UAAe,OAAoB;CACxD,OAAO,UAAU,gBAAgB,EAAE,KAC9B,UAAU,UAAU,IAAI,OACxB,UAAU,SAAS,IAAI;AAC9B;AAEA,IAAM,uBACJ,UACA,IACA,QACA,WACS;CACT,IAAI,CAAC,QACH;CAGF,MAAM,UAAU,iBAAiB,MAAM;CACvC,IAAI,SAAS,UAAU,OAAO,SAAS,SAAS,WAAW,OAAO;MAC5D,OAAO,OAAO,QAAQ,QAAQ,YAChC,OAAO,OAAO,IAAI;GAAE,GAAG,OAAO;GAAO,GAAG,OAAO;EAAO,CAAC;OAEpD,IAAI,OAAO,OAAO,cAAc,YAAY;GAC/C,OAAO,UAAU,OAAO,OAAO,OAAO,MAAM;GAC5C;EACF;;CAGF,MAAM,WAAW,OAAO,OAAO,OAAO,YAAY,OAAO,KAAK,OAAO,KAAK;CAC1E,MAAM,IAAI,eAAe,QAAQ,GAAG;CACpC,MAAM,IAAI,eAAe,QAAQ,GAAG;CACpC,IAAI,YAAY,MAAM,QAAQ,MAAM,MAClC,UAAU,eAAe,UAAU,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAExE;AAEA,IAAM,yBAAyB,UAAe,eAA8B;CAC1E,IAAI,CAAC,cAAc,OAAO,eAAe,UACvC;CAGF,KAAK,MAAM,CAAC,IAAI,UAAU,OAAO,QAAQ,UAAU,GAAG;EACpD,IAAI,CAAC,SAAS,OAAO,UAAU,UAC7B;EAEF,MAAM,SAAS,gBAAiB,MAAc,MAAM;EACpD,IAAI,CAAC,QACH;EAEF,oBAAoB,UAAU,IAAI,cAAc,UAAU,EAAE,GAAG,MAAM;CACvE;AACF;AAEA,IAAa,4BAA4B,UAAe,YAAuB;CAC7E,sBAAsB,UAAU,SAAS,OAAO;CAChD,sBAAsB,UAAU,SAAS,MAAM;AACjD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/client",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.23",
|
|
4
4
|
"description": "RPGJS is a framework for creating RPG/MMORPG games",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@canvasengine/presets": "^2.0.0",
|
|
21
|
-
"canvasengine": "^2.0.
|
|
21
|
+
"canvasengine": "^2.0.1",
|
|
22
22
|
"pixi.js": "^8.9.2"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rpgjs/common": "5.0.0-beta.
|
|
26
|
-
"@rpgjs/server": "5.0.0-beta.
|
|
27
|
-
"@rpgjs/ui-css": "5.0.0-beta.
|
|
25
|
+
"@rpgjs/common": "5.0.0-beta.22",
|
|
26
|
+
"@rpgjs/server": "5.0.0-beta.23",
|
|
27
|
+
"@rpgjs/ui-css": "5.0.0-beta.21",
|
|
28
28
|
"@signe/di": "3.1.0",
|
|
29
29
|
"@signe/room": "3.1.0",
|
|
30
30
|
"@signe/sync": "3.1.0",
|
package/src/Game/Object.spec.ts
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
|
-
import { describe, expect, test, vi } from "vitest";
|
|
1
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import { signal } from "canvasengine";
|
|
3
3
|
import { appendFramePayload, RpgClientObject, withGraphicDisplayScale } from "./Object";
|
|
4
|
+
import { RpgClientEvent } from "./Event";
|
|
5
|
+
import { RpgClientPlayer } from "./Player";
|
|
6
|
+
|
|
7
|
+
const injected = vi.hoisted(() => ({
|
|
8
|
+
engine: {} as any,
|
|
9
|
+
}));
|
|
4
10
|
|
|
5
11
|
vi.mock("../RpgClientEngine", () => ({
|
|
6
12
|
RpgClientEngine: class RpgClientEngine {},
|
|
7
13
|
}));
|
|
8
14
|
|
|
9
15
|
vi.mock("../core/inject", () => ({
|
|
10
|
-
inject: () =>
|
|
16
|
+
inject: () => injected.engine,
|
|
11
17
|
}));
|
|
12
18
|
|
|
13
|
-
function createObject() {
|
|
14
|
-
const object = Object.create(
|
|
19
|
+
function createObject(prototype: object = RpgClientObject.prototype) {
|
|
20
|
+
const object = Object.create(prototype) as RpgClientObject;
|
|
21
|
+
object.id = "object-1";
|
|
22
|
+
object.x = signal(10) as any;
|
|
23
|
+
object.y = signal(20) as any;
|
|
15
24
|
object.animationName = signal("stand");
|
|
16
25
|
object.graphics = signal(["hero"]) as any;
|
|
17
26
|
object.animationCurrentIndex = signal(0);
|
|
18
27
|
object.animationIsPlaying = signal(false);
|
|
28
|
+
object.hitbox = signal({ w: 32, h: 32 }) as any;
|
|
19
29
|
return object;
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
describe("RpgClientObject animations", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
injected.engine = {};
|
|
35
|
+
});
|
|
36
|
+
|
|
23
37
|
test("accepts a single frame payload without requiring iterable spread", () => {
|
|
24
38
|
expect(
|
|
25
39
|
appendFramePayload({ stale: true }, { x: 10, y: 20, ts: 1 }),
|
|
@@ -33,6 +47,32 @@ describe("RpgClientObject animations", () => {
|
|
|
33
47
|
});
|
|
34
48
|
});
|
|
35
49
|
|
|
50
|
+
test("updates the client-side hitbox signal", () => {
|
|
51
|
+
const object = createObject();
|
|
52
|
+
|
|
53
|
+
object.setHitbox(56, 50);
|
|
54
|
+
|
|
55
|
+
expect(object.hitbox()).toEqual({ w: 56, h: 50 });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test.each([
|
|
59
|
+
["player", RpgClientPlayer.prototype],
|
|
60
|
+
["event", RpgClientEvent.prototype],
|
|
61
|
+
])("updates the client-side %s physics body", (_kind, prototype) => {
|
|
62
|
+
const updateHitbox = vi.fn();
|
|
63
|
+
injected.engine = {
|
|
64
|
+
sceneMap: {
|
|
65
|
+
updateHitbox,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
const object = createObject(prototype);
|
|
69
|
+
|
|
70
|
+
object.setHitbox(56, 50);
|
|
71
|
+
|
|
72
|
+
expect(object.hitbox()).toEqual({ w: 56, h: 50 });
|
|
73
|
+
expect(updateHitbox).toHaveBeenCalledWith("object-1", 10, 20, 56, 50);
|
|
74
|
+
});
|
|
75
|
+
|
|
36
76
|
test("marks temporary animation as finished before restoring locomotion animation", async () => {
|
|
37
77
|
const object = createObject();
|
|
38
78
|
const animationChanges: Array<{ name: string; isPlaying: boolean }> = [];
|
package/src/Game/Object.ts
CHANGED
|
@@ -130,6 +130,25 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
130
130
|
return inject(RpgClientEngine);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
setHitbox(width: number, height: number): void {
|
|
134
|
+
if (typeof width !== "number" || width <= 0) {
|
|
135
|
+
throw new Error("setHitbox: width must be a positive number");
|
|
136
|
+
}
|
|
137
|
+
if (typeof height !== "number" || height <= 0) {
|
|
138
|
+
throw new Error("setHitbox: height must be a positive number");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
this.hitbox.set({
|
|
142
|
+
w: width,
|
|
143
|
+
h: height,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const sceneMap = (this.engine as any)?.sceneMap;
|
|
147
|
+
if (sceneMap && this.id) {
|
|
148
|
+
sceneMap.updateHitbox?.(this.id, this.x(), this.y(), width, height);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
133
152
|
private animationSubscription?: Subscription;
|
|
134
153
|
private animationResetTimeout?: ReturnType<typeof setTimeout>;
|
|
135
154
|
private animationWaitResolve?: () => void;
|
package/src/Gui/Gui.spec.ts
CHANGED
|
@@ -98,6 +98,25 @@ describe("RpgGui Vue integration", () => {
|
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
test("waits for CanvasEngine GUI dependencies before displaying", async () => {
|
|
102
|
+
const { gui } = await createGui();
|
|
103
|
+
const enabled = signal(true);
|
|
104
|
+
const ready = signal<boolean | undefined>(undefined);
|
|
105
|
+
|
|
106
|
+
gui.add({
|
|
107
|
+
id: "canvas-dependency",
|
|
108
|
+
component: CanvasGui,
|
|
109
|
+
autoDisplay: true,
|
|
110
|
+
dependencies: () => [enabled, ready],
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(gui.isDisplaying("canvas-dependency")).toBe(false);
|
|
114
|
+
|
|
115
|
+
ready.set(true);
|
|
116
|
+
|
|
117
|
+
expect(gui.isDisplaying("canvas-dependency")).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
101
120
|
test("ignores stale server close events for previous GUI opens", async () => {
|
|
102
121
|
const { gui, socket } = await createGui();
|
|
103
122
|
await gui._initialize();
|
package/src/Gui/Gui.ts
CHANGED
|
@@ -449,18 +449,48 @@ export class RpgGui {
|
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
const guiInstance = this.get(id)!;
|
|
452
|
-
|
|
453
|
-
// Check if it's a Vue component (in extraGuis)
|
|
454
452
|
const isVueComponent = this.extraGuis.some(gui => gui.name === id);
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
}
|
|
453
|
+
|
|
454
|
+
if (guiInstance.subscription) {
|
|
455
|
+
guiInstance.subscription.unsubscribe();
|
|
456
|
+
guiInstance.subscription = undefined;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const show = () => {
|
|
460
460
|
guiInstance.openId = openId;
|
|
461
461
|
guiInstance.data.set(data);
|
|
462
462
|
guiInstance.display.set(true);
|
|
463
|
+
if (isVueComponent) {
|
|
464
|
+
this._notifyVueGui(id, true, data);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const deps = dependencies.length > 0
|
|
469
|
+
? dependencies
|
|
470
|
+
: (guiInstance.dependencies ?? []);
|
|
471
|
+
|
|
472
|
+
if (deps.length > 0) {
|
|
473
|
+
const values = deps.map(dependency => dependency());
|
|
474
|
+
const subscription = new Subscription();
|
|
475
|
+
const showIfReady = () => {
|
|
476
|
+
if (values.every(value => value !== undefined)) {
|
|
477
|
+
show();
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
deps.forEach((dependency, index) => {
|
|
482
|
+
subscription.add(dependency.observable.subscribe((value) => {
|
|
483
|
+
values[index] = value;
|
|
484
|
+
showIfReady();
|
|
485
|
+
}));
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
guiInstance.subscription = subscription;
|
|
489
|
+
showIfReady();
|
|
490
|
+
return;
|
|
463
491
|
}
|
|
492
|
+
|
|
493
|
+
show();
|
|
464
494
|
}
|
|
465
495
|
|
|
466
496
|
isDisplaying(id: string): boolean {
|
|
@@ -490,17 +520,26 @@ export class RpgGui {
|
|
|
490
520
|
: (guiInstance.dependencies ?? []);
|
|
491
521
|
|
|
492
522
|
if (deps.length > 0) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
).subscribe((values) => {
|
|
523
|
+
const values = deps.map(dependency => dependency());
|
|
524
|
+
const subscription = new Subscription();
|
|
525
|
+
const showIfReady = () => {
|
|
497
526
|
if (values.every(value => value !== undefined)) {
|
|
498
527
|
guiInstance.openId = openId;
|
|
499
528
|
guiInstance.data.set(data);
|
|
500
529
|
guiInstance.display.set(true);
|
|
501
530
|
this._notifyVueGui(id, true, data);
|
|
502
531
|
}
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
deps.forEach((dependency, index) => {
|
|
535
|
+
subscription.add(dependency.observable.subscribe((value) => {
|
|
536
|
+
values[index] = value;
|
|
537
|
+
showIfReady();
|
|
538
|
+
}));
|
|
503
539
|
});
|
|
540
|
+
|
|
541
|
+
guiInstance.subscription = subscription;
|
|
542
|
+
showIfReady();
|
|
504
543
|
return;
|
|
505
544
|
}
|
|
506
545
|
|