@rpgjs/client 5.0.0-beta.14 → 5.0.0-beta.16
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 +17 -0
- package/dist/Game/Map.js +15 -4
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.js +4 -3
- package/dist/Game/Object.js.map +1 -1
- package/dist/RpgClientEngine.d.ts +14 -0
- package/dist/RpgClientEngine.js +89 -6
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/services/AbstractSocket.d.ts +2 -0
- package/dist/services/AbstractSocket.js.map +1 -1
- package/dist/services/mmorpg.d.ts +1 -0
- package/dist/services/mmorpg.js +1 -0
- package/dist/services/mmorpg.js.map +1 -1
- package/dist/services/standalone.d.ts +1 -0
- package/dist/services/standalone.js +1 -0
- package/dist/services/standalone.js.map +1 -1
- package/package.json +4 -4
- package/src/Game/Map.ts +37 -4
- package/src/Game/Object.ts +4 -3
- package/src/RpgClientEngine.ts +141 -7
- package/src/services/AbstractSocket.ts +3 -0
- package/src/services/mmorpg.ts +2 -0
- package/src/services/standalone.spec.ts +20 -0
- package/src/services/standalone.ts +2 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AbstractSocket.js","names":[],"sources":["../../src/services/AbstractSocket.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\n\nexport const WebSocketToken = \"websocket\";\n\nexport type SocketQueryValue = string | null | undefined;\nexport type SocketQuery = Record<string, SocketQueryValue>;\nexport type SocketUpdateProperties = {\n room: string;\n host?: string;\n query?: SocketQuery;\n};\n\nexport abstract class AbstractWebsocket {\n constructor(protected context: Context) {}\n\n abstract connection(listeners?: (data: any) => void): Promise<void>;\n abstract emit(event: string, data: any): void;\n abstract on(event: string, callback: (data: any) => void): void;\n abstract off(event: string, callback: (data: any) => void): void;\n abstract updateProperties(params: SocketUpdateProperties): void;\n abstract reconnect(listeners?: (data: any) => void): Promise<void>;\n}\n"],"mappings":";AAEA,IAAa,iBAAiB;
|
|
1
|
+
{"version":3,"file":"AbstractSocket.js","names":[],"sources":["../../src/services/AbstractSocket.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\n\nexport const WebSocketToken = \"websocket\";\n\nexport type SocketQueryValue = string | null | undefined;\nexport type SocketQuery = Record<string, SocketQueryValue>;\nexport type SocketUpdateProperties = {\n room: string;\n host?: string;\n query?: SocketQuery;\n};\nexport type WebSocketMode = \"standalone\" | \"mmorpg\";\n\nexport abstract class AbstractWebsocket {\n readonly mode?: WebSocketMode;\n\n constructor(protected context: Context) {}\n\n abstract connection(listeners?: (data: any) => void): Promise<void>;\n abstract emit(event: string, data: any): void;\n abstract on(event: string, callback: (data: any) => void): void;\n abstract off(event: string, callback: (data: any) => void): void;\n abstract updateProperties(params: SocketUpdateProperties): void;\n abstract reconnect(listeners?: (data: any) => void): Promise<void>;\n}\n"],"mappings":";AAEA,IAAa,iBAAiB;AAW9B,IAAsB,oBAAtB,MAAwC;CAGtC,YAAY,SAA4B;EAAlB,KAAA,UAAA;CAAmB;AAQ3C"}
|
package/dist/services/mmorpg.js
CHANGED
|
@@ -12,6 +12,7 @@ var BridgeWebsocket = class extends AbstractWebsocket {
|
|
|
12
12
|
super(context);
|
|
13
13
|
this.context = context;
|
|
14
14
|
this.options = options;
|
|
15
|
+
this.mode = "mmorpg";
|
|
15
16
|
this.pendingOn = [];
|
|
16
17
|
this.acceptedOpenListeners = /* @__PURE__ */ new Set();
|
|
17
18
|
this.targetRoom = "lobby-1";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mmorpg.js","names":[],"sources":["../../src/services/mmorpg.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\nimport { connectionRoom } from \"@signe/sync/client\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { isNativeSocketEvent, waitForRpgjsConnected } from \"./mmorpg-connection\";\n\nexport interface MmorpgOptions {\n host?: string;\n connectionId?: string;\n connectionIdScope?: \"local\" | \"session\" | \"ephemeral\";\n query?: SocketQuery | (() => SocketQuery | undefined);\n socketOptions?: Record<string, any>;\n}\n\nexport class BridgeWebsocket extends AbstractWebsocket {\n private socket: any;\n private privateId: string;\n private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];\n private acceptedOpenListeners = new Set<(data: any) => void>();\n private targetRoom = \"lobby-1\";\n\n constructor(protected context: Context, private options: MmorpgOptions = {}) {\n super(context);\n this.privateId = this.resolveConnectionId();\n }\n\n private resolveConnectionId(): string {\n if (this.options.connectionId) {\n return this.options.connectionId;\n }\n\n const scope = this.options.connectionIdScope ?? \"local\";\n const key = \"rpgjs-user-id\";\n\n if (scope === \"ephemeral\") {\n return crypto.randomUUID();\n }\n\n const storage =\n scope === \"session\"\n ? window.sessionStorage\n : window.localStorage;\n\n const existing = storage.getItem(key);\n if (existing) {\n return existing;\n }\n\n const id = crypto.randomUUID();\n storage.setItem(key, id);\n return id;\n }\n\n private resolveQuery(): SocketQuery {\n const query = typeof this.options.query === \"function\"\n ? this.options.query()\n : this.options.query;\n\n return query ?? {};\n }\n\n async connection(listeners?: (data: any) => void) {\n // tmp\n class Room {\n \n }\n const instance = new Room()\n const host = this.options.host || window.location.host;\n this.socket = await connectionRoom({\n maxRetries: 0,\n ...this.options.socketOptions,\n host,\n room: this.targetRoom,\n id: this.privateId,\n query: {\n ...this.resolveQuery(),\n id: this.privateId,\n },\n }, instance)\n\n const pendingOn = this.pendingOn;\n this.pendingOn = [];\n pendingOn\n .filter(({ event }) => !this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n await waitForRpgjsConnected(this.socket.conn);\n pendingOn\n .filter(({ event }) => this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n this.emitAcceptedOpen();\n listeners?.(this.socket)\n }\n\n on(key: string, callback: (data: any) => void) {\n if (!this.socket) {\n this.pendingOn.push({ event: key, callback });\n return;\n }\n this.attachEvent(key, callback);\n }\n\n off(event: string, callback: (data: any) => void) {\n if (!this.socket) return;\n if (event === \"open\") {\n this.acceptedOpenListeners.delete(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.removeEventListener(event, callback);\n return;\n }\n this.socket.off(event, callback);\n }\n\n emit(event: string, data: any) {\n this.socket.emit(event, data);\n }\n\n private attachEvent(event: string, callback: (data: any) => void) {\n if (event === \"open\") {\n this.acceptedOpenListeners.add(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.addEventListener(event, callback);\n return;\n }\n this.socket.on(event, callback);\n }\n\n private emitAcceptedOpen() {\n const event = new Event(\"open\");\n this.acceptedOpenListeners.forEach((callback) => callback(event));\n }\n\n updateProperties({ room, host, query }: SocketUpdateProperties) {\n if (!this.socket?.conn) return;\n this.targetRoom = room;\n this.socket.conn.updateProperties({\n room,\n id: this.privateId,\n host: host || this.options.host || window.location.host,\n query: {\n ...this.resolveQuery(),\n ...query,\n id: this.privateId,\n },\n })\n }\n\n private isNativeSocketEvent(event: string) {\n return isNativeSocketEvent(event);\n }\n\n async reconnect(_listeners?: (data: any) => void): Promise<void> {\n if (!this.socket?.conn) return;\n const conn = this.socket.conn;\n const connected = waitForRpgjsConnected(conn, 10000, { ignoreCleanClose: true });\n conn.reconnect();\n await connected;\n this.emitAcceptedOpen();\n }\n\n getCurrentRoom(): string {\n return this.targetRoom || this.socket?.conn?.room || \"lobby-1\";\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n constructor(protected context: Context, private _options: MmorpgOptions) {\n super(context);\n }\n\n async update(_map: any) {\n // In MMORPG mode, clients are untrusted and must not push map definitions.\n // Map bootstrap/update is handled server-side by @rpgjs/vite.\n return;\n }\n}\n\nexport function provideMmorpg(options: MmorpgOptions) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, options),\n },\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => new UpdateMapStandaloneService(context, options),\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":";;;;;;;;;AAkBA,IAAa,kBAAb,cAAqC,kBAAkB;
|
|
1
|
+
{"version":3,"file":"mmorpg.js","names":[],"sources":["../../src/services/mmorpg.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\nimport { connectionRoom } from \"@signe/sync/client\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { isNativeSocketEvent, waitForRpgjsConnected } from \"./mmorpg-connection\";\n\nexport interface MmorpgOptions {\n host?: string;\n connectionId?: string;\n connectionIdScope?: \"local\" | \"session\" | \"ephemeral\";\n query?: SocketQuery | (() => SocketQuery | undefined);\n socketOptions?: Record<string, any>;\n}\n\nexport class BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"mmorpg\" as const;\n\n private socket: any;\n private privateId: string;\n private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];\n private acceptedOpenListeners = new Set<(data: any) => void>();\n private targetRoom = \"lobby-1\";\n\n constructor(protected context: Context, private options: MmorpgOptions = {}) {\n super(context);\n this.privateId = this.resolveConnectionId();\n }\n\n private resolveConnectionId(): string {\n if (this.options.connectionId) {\n return this.options.connectionId;\n }\n\n const scope = this.options.connectionIdScope ?? \"local\";\n const key = \"rpgjs-user-id\";\n\n if (scope === \"ephemeral\") {\n return crypto.randomUUID();\n }\n\n const storage =\n scope === \"session\"\n ? window.sessionStorage\n : window.localStorage;\n\n const existing = storage.getItem(key);\n if (existing) {\n return existing;\n }\n\n const id = crypto.randomUUID();\n storage.setItem(key, id);\n return id;\n }\n\n private resolveQuery(): SocketQuery {\n const query = typeof this.options.query === \"function\"\n ? this.options.query()\n : this.options.query;\n\n return query ?? {};\n }\n\n async connection(listeners?: (data: any) => void) {\n // tmp\n class Room {\n \n }\n const instance = new Room()\n const host = this.options.host || window.location.host;\n this.socket = await connectionRoom({\n maxRetries: 0,\n ...this.options.socketOptions,\n host,\n room: this.targetRoom,\n id: this.privateId,\n query: {\n ...this.resolveQuery(),\n id: this.privateId,\n },\n }, instance)\n\n const pendingOn = this.pendingOn;\n this.pendingOn = [];\n pendingOn\n .filter(({ event }) => !this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n await waitForRpgjsConnected(this.socket.conn);\n pendingOn\n .filter(({ event }) => this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n this.emitAcceptedOpen();\n listeners?.(this.socket)\n }\n\n on(key: string, callback: (data: any) => void) {\n if (!this.socket) {\n this.pendingOn.push({ event: key, callback });\n return;\n }\n this.attachEvent(key, callback);\n }\n\n off(event: string, callback: (data: any) => void) {\n if (!this.socket) return;\n if (event === \"open\") {\n this.acceptedOpenListeners.delete(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.removeEventListener(event, callback);\n return;\n }\n this.socket.off(event, callback);\n }\n\n emit(event: string, data: any) {\n this.socket.emit(event, data);\n }\n\n private attachEvent(event: string, callback: (data: any) => void) {\n if (event === \"open\") {\n this.acceptedOpenListeners.add(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.addEventListener(event, callback);\n return;\n }\n this.socket.on(event, callback);\n }\n\n private emitAcceptedOpen() {\n const event = new Event(\"open\");\n this.acceptedOpenListeners.forEach((callback) => callback(event));\n }\n\n updateProperties({ room, host, query }: SocketUpdateProperties) {\n if (!this.socket?.conn) return;\n this.targetRoom = room;\n this.socket.conn.updateProperties({\n room,\n id: this.privateId,\n host: host || this.options.host || window.location.host,\n query: {\n ...this.resolveQuery(),\n ...query,\n id: this.privateId,\n },\n })\n }\n\n private isNativeSocketEvent(event: string) {\n return isNativeSocketEvent(event);\n }\n\n async reconnect(_listeners?: (data: any) => void): Promise<void> {\n if (!this.socket?.conn) return;\n const conn = this.socket.conn;\n const connected = waitForRpgjsConnected(conn, 10000, { ignoreCleanClose: true });\n conn.reconnect();\n await connected;\n this.emitAcceptedOpen();\n }\n\n getCurrentRoom(): string {\n return this.targetRoom || this.socket?.conn?.room || \"lobby-1\";\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n constructor(protected context: Context, private _options: MmorpgOptions) {\n super(context);\n }\n\n async update(_map: any) {\n // In MMORPG mode, clients are untrusted and must not push map definitions.\n // Map bootstrap/update is handled server-side by @rpgjs/vite.\n return;\n }\n}\n\nexport function provideMmorpg(options: MmorpgOptions) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, options),\n },\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => new UpdateMapStandaloneService(context, options),\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
|
|
@@ -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 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":";;;;;;;;;AAkBA,IAAM,kBAAN,cAA8B,kBAAkB;
|
|
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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"}
|
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.16",
|
|
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",
|
|
@@ -22,9 +22,9 @@
|
|
|
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.15",
|
|
26
|
+
"@rpgjs/server": "5.0.0-beta.16",
|
|
27
|
+
"@rpgjs/ui-css": "5.0.0-beta.13",
|
|
28
28
|
"@signe/di": "3.0.1",
|
|
29
29
|
"@signe/room": "3.0.1",
|
|
30
30
|
"@signe/sync": "3.0.1",
|
package/src/Game/Map.ts
CHANGED
|
@@ -24,6 +24,34 @@ type TestGlobalScope = typeof globalThis & {
|
|
|
24
24
|
__RPGJS_TEST__?: boolean;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
+
const lightingColorsEqual = (
|
|
28
|
+
left: LightSpot["color"],
|
|
29
|
+
right: LightSpot["color"],
|
|
30
|
+
): boolean => {
|
|
31
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
32
|
+
return Array.isArray(left)
|
|
33
|
+
&& Array.isArray(right)
|
|
34
|
+
&& left.length === right.length
|
|
35
|
+
&& left.every((value, index) => value === right[index]);
|
|
36
|
+
}
|
|
37
|
+
return left === right;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const lightSpotsEqual = (left: LightSpot | undefined, right: LightSpot): boolean => {
|
|
41
|
+
if (!left) return false;
|
|
42
|
+
return left.id === right.id
|
|
43
|
+
&& left.x === right.x
|
|
44
|
+
&& left.y === right.y
|
|
45
|
+
&& left.radius === right.radius
|
|
46
|
+
&& left.intensity === right.intensity
|
|
47
|
+
&& lightingColorsEqual(left.color, right.color)
|
|
48
|
+
&& left.flicker === right.flicker
|
|
49
|
+
&& left.flickerSpeed === right.flickerSpeed
|
|
50
|
+
&& left.pulse === right.pulse
|
|
51
|
+
&& left.pulseSpeed === right.pulseSpeed
|
|
52
|
+
&& left.phase === right.phase;
|
|
53
|
+
};
|
|
54
|
+
|
|
27
55
|
export class RpgClientMap extends RpgCommonMap<any> {
|
|
28
56
|
engine: RpgClientEngine = inject(RpgClientEngine)
|
|
29
57
|
@users(RpgClientPlayer) players = signal<Record<string, RpgClientPlayer>>({});
|
|
@@ -132,10 +160,15 @@ export class RpgClientMap extends RpgCommonMap<any> {
|
|
|
132
160
|
if (!nextSpot) {
|
|
133
161
|
return;
|
|
134
162
|
}
|
|
135
|
-
this.localLightSpots.update((spots) =>
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
this.localLightSpots.update((spots) => {
|
|
164
|
+
if (lightSpotsEqual(spots[id], nextSpot)) {
|
|
165
|
+
return spots;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
...spots,
|
|
169
|
+
[id]: nextSpot,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
139
172
|
}
|
|
140
173
|
|
|
141
174
|
patchLightSpot(id: string, patch: Partial<LightSpot>): void {
|
package/src/Game/Object.ts
CHANGED
|
@@ -64,6 +64,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
64
64
|
|
|
65
65
|
constructor() {
|
|
66
66
|
super();
|
|
67
|
+
const engine = this.engine;
|
|
67
68
|
this.hooks.callHooks("client-sprite-onInit", this).subscribe();
|
|
68
69
|
|
|
69
70
|
this._frames.observable.subscribe(({ items }) => {
|
|
@@ -84,7 +85,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
84
85
|
const graphicRefs = Array.isArray(graphics) ? graphics : [];
|
|
85
86
|
if (graphicRefs.length === 0) return of([]);
|
|
86
87
|
return from(Promise.all(graphicRefs.map(async (graphic) => {
|
|
87
|
-
const spritesheet = await
|
|
88
|
+
const spritesheet = await engine.getSpriteSheet(graphic);
|
|
88
89
|
return withGraphicDisplayScale(spritesheet, scale);
|
|
89
90
|
})));
|
|
90
91
|
})
|
|
@@ -93,7 +94,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
93
94
|
this.graphicsSignals.set(sheets);
|
|
94
95
|
});
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
engine.tick
|
|
97
98
|
.pipe
|
|
98
99
|
//throttleTime(10)
|
|
99
100
|
()
|
|
@@ -101,7 +102,7 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
101
102
|
const frame = this.frames.shift();
|
|
102
103
|
if (frame) {
|
|
103
104
|
if (typeof frame.x !== "number" || typeof frame.y !== "number") return;
|
|
104
|
-
|
|
105
|
+
engine.scene.setBodyPosition(
|
|
105
106
|
this.id,
|
|
106
107
|
frame.x,
|
|
107
108
|
frame.y,
|
package/src/RpgClientEngine.ts
CHANGED
|
@@ -52,6 +52,11 @@ interface MovementTrajectoryPoint {
|
|
|
52
52
|
direction?: Direction;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
interface CanvasResizeSize {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
const DEFAULT_DASH_ADDITIONAL_SPEED = 8;
|
|
56
61
|
const DEFAULT_DASH_DURATION_MS = 180;
|
|
57
62
|
const DEFAULT_DASH_COOLDOWN_MS = 450;
|
|
@@ -187,6 +192,9 @@ export class RpgClientEngine<T = any> {
|
|
|
187
192
|
private predictionEnabled = false;
|
|
188
193
|
private prediction?: PredictionController<RpgMovementInput, Direction>;
|
|
189
194
|
private readonly SERVER_CORRECTION_THRESHOLD = 30;
|
|
195
|
+
private localMovementAuthority = false;
|
|
196
|
+
private lastLocalMovementInputAt = 0;
|
|
197
|
+
private readonly LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS = 250;
|
|
190
198
|
private inputFrameCounter = 0;
|
|
191
199
|
private pendingPredictionFrames: number[] = [];
|
|
192
200
|
private lastClientPhysicsStepAt = 0;
|
|
@@ -213,6 +221,8 @@ export class RpgClientEngine<T = any> {
|
|
|
213
221
|
private mapTransitionInProgress = false;
|
|
214
222
|
private currentMapRoomId?: string;
|
|
215
223
|
private socketListenersInitialized = false;
|
|
224
|
+
private clientReadyForMapChanges = false;
|
|
225
|
+
private pendingMapChanges: any[] = [];
|
|
216
226
|
|
|
217
227
|
// Store subscriptions and event listeners for cleanup
|
|
218
228
|
private tickSubscriptions: any[] = [];
|
|
@@ -265,9 +275,36 @@ export class RpgClientEngine<T = any> {
|
|
|
265
275
|
this.registerSpriteComponent("rpg:image", ImageComponent);
|
|
266
276
|
|
|
267
277
|
this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;
|
|
278
|
+
this.localMovementAuthority = this.resolveLocalMovementAuthority();
|
|
268
279
|
this.initializePredictionController();
|
|
269
280
|
}
|
|
270
281
|
|
|
282
|
+
private resolveLocalMovementAuthority(): boolean {
|
|
283
|
+
const predictionConfig = (this.globalConfig as any)?.prediction;
|
|
284
|
+
const configured =
|
|
285
|
+
(this.globalConfig as any)?.movementAuthority ??
|
|
286
|
+
predictionConfig?.movementAuthority ??
|
|
287
|
+
predictionConfig?.authority ??
|
|
288
|
+
predictionConfig?.mode;
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
configured === "server" ||
|
|
292
|
+
configured === "network" ||
|
|
293
|
+
configured === false
|
|
294
|
+
) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
configured === "client" ||
|
|
299
|
+
configured === "local" ||
|
|
300
|
+
configured === true
|
|
301
|
+
) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return this.webSocket.mode === "standalone";
|
|
306
|
+
}
|
|
307
|
+
|
|
271
308
|
setLocale(locale: string) {
|
|
272
309
|
this.locale = locale;
|
|
273
310
|
}
|
|
@@ -357,6 +394,7 @@ export class RpgClientEngine<T = any> {
|
|
|
357
394
|
Canvas,
|
|
358
395
|
bootstrapOptions
|
|
359
396
|
);
|
|
397
|
+
this.installCanvasResizeGuard(app);
|
|
360
398
|
this.canvasApp = app;
|
|
361
399
|
this.canvasElement = canvasElement;
|
|
362
400
|
this.renderer = app.renderer as unknown as PIXI.Renderer;
|
|
@@ -390,6 +428,8 @@ export class RpgClientEngine<T = any> {
|
|
|
390
428
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
391
429
|
|
|
392
430
|
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
431
|
+
this.clientReadyForMapChanges = true;
|
|
432
|
+
this.flushPendingMapChanges();
|
|
393
433
|
|
|
394
434
|
// wondow is resize
|
|
395
435
|
this.resizeHandler = () => {
|
|
@@ -416,6 +456,56 @@ export class RpgClientEngine<T = any> {
|
|
|
416
456
|
this.startPingPong();
|
|
417
457
|
}
|
|
418
458
|
|
|
459
|
+
private installCanvasResizeGuard(app: any) {
|
|
460
|
+
if (!app || typeof app.resize !== "function") return;
|
|
461
|
+
|
|
462
|
+
const originalResize = app.resize.bind(app);
|
|
463
|
+
app.resize = () => {
|
|
464
|
+
const targetSize = this.readCanvasResizeTargetSize(app);
|
|
465
|
+
const rendererSize = this.readCanvasRendererSize(app);
|
|
466
|
+
|
|
467
|
+
if (
|
|
468
|
+
targetSize &&
|
|
469
|
+
rendererSize &&
|
|
470
|
+
targetSize.width === rendererSize.width &&
|
|
471
|
+
targetSize.height === rendererSize.height
|
|
472
|
+
) {
|
|
473
|
+
this.cancelCanvasResizeFrame(app);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
originalResize();
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private readCanvasResizeTargetSize(app: any): CanvasResizeSize | null {
|
|
482
|
+
const resizeTarget = app?.resizeTo;
|
|
483
|
+
if (!resizeTarget || typeof window === "undefined") return null;
|
|
484
|
+
|
|
485
|
+
const rawWidth = resizeTarget === window ? window.innerWidth : resizeTarget.clientWidth;
|
|
486
|
+
const rawHeight = resizeTarget === window ? window.innerHeight : resizeTarget.clientHeight;
|
|
487
|
+
const width = Math.round(Number(rawWidth));
|
|
488
|
+
const height = Math.round(Number(rawHeight));
|
|
489
|
+
|
|
490
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) return null;
|
|
491
|
+
return { width, height };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private readCanvasRendererSize(app: any): CanvasResizeSize | null {
|
|
495
|
+
const screen = app?.renderer?.screen;
|
|
496
|
+
const width = Math.round(Number(screen?.width));
|
|
497
|
+
const height = Math.round(Number(screen?.height));
|
|
498
|
+
|
|
499
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
500
|
+
return { width, height };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private cancelCanvasResizeFrame(app: any) {
|
|
504
|
+
if (typeof app?._cancelResize === "function") {
|
|
505
|
+
app._cancelResize();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
419
509
|
private resolveSceneMapComponent() {
|
|
420
510
|
const components = this.hooks.getHookFunctions("client-sceneMap-component");
|
|
421
511
|
const component = components[components.length - 1];
|
|
@@ -523,8 +613,8 @@ export class RpgClientEngine<T = any> {
|
|
|
523
613
|
|
|
524
614
|
const myId = this.playerIdSignal();
|
|
525
615
|
const players = payload.players;
|
|
526
|
-
const
|
|
527
|
-
|
|
616
|
+
const localPatch = myId && players ? players[myId] : undefined;
|
|
617
|
+
const shouldMaskLocalPosition = this.shouldPreserveLocalPlayerPosition(localPatch);
|
|
528
618
|
if (shouldMaskLocalPosition && myId && players && players[myId]) {
|
|
529
619
|
const localPatch = { ...players[myId] };
|
|
530
620
|
delete localPatch.x;
|
|
@@ -540,6 +630,31 @@ export class RpgClientEngine<T = any> {
|
|
|
540
630
|
return payload;
|
|
541
631
|
}
|
|
542
632
|
|
|
633
|
+
private shouldPreserveLocalPlayerPosition(localPatch?: any): boolean {
|
|
634
|
+
if (!localPatch) {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
if (this.predictionEnabled && !!this.prediction?.hasPendingInputs()) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
return this.shouldKeepLocalPlayerMovement();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private shouldKeepLocalPlayerMovement(): boolean {
|
|
644
|
+
if (!this.localMovementAuthority || this.mapTransitionInProgress) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
const myId = this.playerIdSignal();
|
|
648
|
+
const player = myId ? this.sceneMap?.players?.()?.[myId] : undefined;
|
|
649
|
+
if (!player) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
if (this.prediction?.hasPendingInputs()) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
return Date.now() - this.lastLocalMovementInputAt <= this.LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS;
|
|
656
|
+
}
|
|
657
|
+
|
|
543
658
|
private normalizeAckWithSyncState(
|
|
544
659
|
ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },
|
|
545
660
|
syncData: any,
|
|
@@ -594,10 +709,11 @@ export class RpgClientEngine<T = any> {
|
|
|
594
709
|
});
|
|
595
710
|
|
|
596
711
|
this.webSocket.on("changeMap", (data) => {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
712
|
+
if (!this.clientReadyForMapChanges) {
|
|
713
|
+
this.pendingMapChanges.push(data);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
this.handleChangeMap(data);
|
|
601
717
|
});
|
|
602
718
|
|
|
603
719
|
this.webSocket.on("showComponentAnimation", (data) => {
|
|
@@ -795,6 +911,19 @@ export class RpgClientEngine<T = any> {
|
|
|
795
911
|
packets.forEach((packet) => this.applySyncPacket(packet));
|
|
796
912
|
}
|
|
797
913
|
|
|
914
|
+
private flushPendingMapChanges() {
|
|
915
|
+
const packets = this.pendingMapChanges;
|
|
916
|
+
this.pendingMapChanges = [];
|
|
917
|
+
packets.forEach((packet) => this.handleChangeMap(packet));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private handleChangeMap(data: any) {
|
|
921
|
+
const nextMapId = typeof data?.mapId === "string" ? data.mapId : undefined;
|
|
922
|
+
this.beginMapTransfer(nextMapId);
|
|
923
|
+
const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
|
|
924
|
+
this.loadScene(data.mapId, transferToken);
|
|
925
|
+
}
|
|
926
|
+
|
|
798
927
|
private applySyncPacket(data: any) {
|
|
799
928
|
if (data.pId) {
|
|
800
929
|
this.playerIdSignal.set(data.pId);
|
|
@@ -1715,6 +1844,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1715
1844
|
if (timestamp < this.dashLockedUntil) return;
|
|
1716
1845
|
this.dashLockedUntil = timestamp + cooldown;
|
|
1717
1846
|
}
|
|
1847
|
+
this.lastLocalMovementInputAt = timestamp;
|
|
1718
1848
|
|
|
1719
1849
|
let frame: number;
|
|
1720
1850
|
let tick: number;
|
|
@@ -2230,12 +2360,13 @@ export class RpgClientEngine<T = any> {
|
|
|
2230
2360
|
|
|
2231
2361
|
private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
|
|
2232
2362
|
this.updateServerTickEstimate(ack.serverTick);
|
|
2363
|
+
const keepLocalMovement = this.shouldKeepLocalPlayerMovement();
|
|
2233
2364
|
if (this.predictionEnabled && this.prediction) {
|
|
2234
2365
|
const result = this.prediction.applyServerAck({
|
|
2235
2366
|
frame: ack.frame,
|
|
2236
2367
|
serverTick: ack.serverTick,
|
|
2237
2368
|
state:
|
|
2238
|
-
typeof ack.x === "number" && typeof ack.y === "number"
|
|
2369
|
+
!keepLocalMovement && typeof ack.x === "number" && typeof ack.y === "number"
|
|
2239
2370
|
? { x: ack.x, y: ack.y, direction: ack.direction }
|
|
2240
2371
|
: undefined,
|
|
2241
2372
|
});
|
|
@@ -2248,6 +2379,9 @@ export class RpgClientEngine<T = any> {
|
|
|
2248
2379
|
if (typeof ack.x !== "number" || typeof ack.y !== "number") {
|
|
2249
2380
|
return;
|
|
2250
2381
|
}
|
|
2382
|
+
if (keepLocalMovement) {
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2251
2385
|
const player = this.getCurrentPlayer() as any;
|
|
2252
2386
|
const myId = this.playerIdSignal();
|
|
2253
2387
|
if (!player || !myId) {
|
|
@@ -9,8 +9,11 @@ export type SocketUpdateProperties = {
|
|
|
9
9
|
host?: string;
|
|
10
10
|
query?: SocketQuery;
|
|
11
11
|
};
|
|
12
|
+
export type WebSocketMode = "standalone" | "mmorpg";
|
|
12
13
|
|
|
13
14
|
export abstract class AbstractWebsocket {
|
|
15
|
+
readonly mode?: WebSocketMode;
|
|
16
|
+
|
|
14
17
|
constructor(protected context: Context) {}
|
|
15
18
|
|
|
16
19
|
abstract connection(listeners?: (data: any) => void): Promise<void>;
|
package/src/services/mmorpg.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface MmorpgOptions {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export class BridgeWebsocket extends AbstractWebsocket {
|
|
20
|
+
readonly mode = "mmorpg" as const;
|
|
21
|
+
|
|
20
22
|
private socket: any;
|
|
21
23
|
private privateId: string;
|
|
22
24
|
private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
1
3
|
import { describe, expect, test, vi } from "vitest";
|
|
4
|
+
import { Context } from "@signe/di";
|
|
5
|
+
import { WebSocketToken } from "./AbstractSocket";
|
|
6
|
+
import { provideMmorpg } from "./mmorpg";
|
|
7
|
+
import { provideRpg } from "./standalone";
|
|
2
8
|
import { normalizeStandaloneMessage } from "./standalone-message";
|
|
3
9
|
|
|
4
10
|
describe("standalone websocket bridge", () => {
|
|
@@ -31,4 +37,18 @@ describe("standalone websocket bridge", () => {
|
|
|
31
37
|
value: { projectiles: [] },
|
|
32
38
|
});
|
|
33
39
|
});
|
|
40
|
+
|
|
41
|
+
test("marks standalone and MMORPG websocket providers with their runtime mode", () => {
|
|
42
|
+
class Server {}
|
|
43
|
+
const context = new Context();
|
|
44
|
+
const standaloneProvider = provideRpg(Server).find(
|
|
45
|
+
(provider: any) => provider.provide === WebSocketToken,
|
|
46
|
+
) as any;
|
|
47
|
+
const mmorpgProvider = provideMmorpg({ connectionId: "test-client" }).find(
|
|
48
|
+
(provider: any) => provider.provide === WebSocketToken,
|
|
49
|
+
) as any;
|
|
50
|
+
|
|
51
|
+
expect(standaloneProvider.useFactory(context).mode).toBe("standalone");
|
|
52
|
+
expect(mmorpgProvider.useFactory(context).mode).toBe("mmorpg");
|
|
53
|
+
});
|
|
34
54
|
});
|