@rpgjs/client 5.0.0-beta.11 → 5.0.0-beta.13
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 +19 -0
- package/dist/Game/AnimationManager.d.ts +1 -0
- package/dist/Game/AnimationManager.js +3 -0
- package/dist/Game/AnimationManager.js.map +1 -1
- package/dist/Game/ClientVisuals.d.ts +61 -0
- package/dist/Game/ClientVisuals.js +96 -0
- package/dist/Game/ClientVisuals.js.map +1 -0
- package/dist/Game/ClientVisuals.spec.d.ts +1 -0
- package/dist/Game/EventComponentResolver.d.ts +16 -0
- package/dist/Game/EventComponentResolver.js +52 -0
- package/dist/Game/EventComponentResolver.js.map +1 -0
- package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
- package/dist/Game/Map.js +9 -0
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.d.ts +2 -0
- package/dist/Game/Object.js +22 -8
- package/dist/Game/Object.js.map +1 -1
- package/dist/Game/Object.spec.d.ts +1 -0
- package/dist/Game/ProjectileManager.d.ts +11 -2
- package/dist/Game/ProjectileManager.js +19 -2
- package/dist/Game/ProjectileManager.js.map +1 -1
- package/dist/Gui/Gui.d.ts +3 -2
- package/dist/Gui/Gui.js +18 -6
- package/dist/Gui/Gui.js.map +1 -1
- package/dist/RpgClient.d.ts +85 -1
- package/dist/RpgClientEngine.d.ts +77 -2
- package/dist/RpgClientEngine.js +290 -31
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/animations/fx.ce.js +58 -0
- package/dist/components/animations/fx.ce.js.map +1 -0
- package/dist/components/animations/index.d.ts +1 -0
- package/dist/components/animations/index.js +3 -1
- package/dist/components/animations/index.js.map +1 -1
- package/dist/components/character.ce.js +192 -19
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/gui/dialogbox/index.ce.js +27 -12
- package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
- package/dist/components/gui/gameover.ce.js +4 -3
- package/dist/components/gui/gameover.ce.js.map +1 -1
- package/dist/components/gui/menu/equip-menu.ce.js +9 -8
- package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/exit-menu.ce.js +7 -5
- package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/items-menu.ce.js +8 -7
- package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/main-menu.ce.js +12 -11
- package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/options-menu.ce.js +7 -5
- package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/skills-menu.ce.js +4 -2
- package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
- package/dist/components/gui/notification/notification.ce.js +4 -1
- package/dist/components/gui/notification/notification.ce.js.map +1 -1
- package/dist/components/gui/save-load.ce.js +10 -9
- package/dist/components/gui/save-load.ce.js.map +1 -1
- package/dist/components/gui/shop/shop.ce.js +17 -16
- package/dist/components/gui/shop/shop.ce.js.map +1 -1
- package/dist/components/gui/title-screen.ce.js +4 -3
- package/dist/components/gui/title-screen.ce.js.map +1 -1
- package/dist/components/interaction-components.ce.js +20 -0
- package/dist/components/interaction-components.ce.js.map +1 -0
- package/dist/components/scenes/canvas.ce.js +12 -7
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +18 -13
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/i18n.d.ts +55 -0
- package/dist/i18n.js +60 -0
- package/dist/i18n.js.map +1 -0
- package/dist/i18n.spec.d.ts +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -2
- package/dist/module.js +30 -3
- package/dist/module.js.map +1 -1
- package/dist/services/actionInput.d.ts +3 -1
- package/dist/services/actionInput.js +33 -1
- package/dist/services/actionInput.js.map +1 -1
- package/dist/services/interactions.d.ts +159 -0
- package/dist/services/interactions.js +460 -0
- package/dist/services/interactions.js.map +1 -0
- package/dist/services/interactions.spec.d.ts +1 -0
- package/dist/services/keyboardControls.d.ts +1 -0
- package/dist/services/keyboardControls.js +1 -0
- package/dist/services/keyboardControls.js.map +1 -1
- package/dist/services/standalone.d.ts +3 -1
- package/dist/services/standalone.js +31 -13
- package/dist/services/standalone.js.map +1 -1
- package/dist/utils/mapId.d.ts +1 -0
- package/dist/utils/mapId.js +6 -0
- package/dist/utils/mapId.js.map +1 -0
- package/package.json +4 -4
- package/src/Game/AnimationManager.ts +4 -0
- package/src/Game/ClientVisuals.spec.ts +56 -0
- package/src/Game/ClientVisuals.ts +184 -0
- package/src/Game/EventComponentResolver.spec.ts +84 -0
- package/src/Game/EventComponentResolver.ts +74 -0
- package/src/Game/Map.ts +10 -0
- package/src/Game/Object.spec.ts +59 -0
- package/src/Game/Object.ts +36 -12
- package/src/Game/ProjectileManager.spec.ts +111 -0
- package/src/Game/ProjectileManager.ts +24 -2
- package/src/Gui/Gui.spec.ts +67 -0
- package/src/Gui/Gui.ts +24 -7
- package/src/RpgClient.ts +96 -1
- package/src/RpgClientEngine.ts +378 -45
- package/src/components/animations/fx.ce +101 -0
- package/src/components/animations/index.ts +4 -2
- package/src/components/character.ce +243 -17
- package/src/components/gui/dialogbox/index.ce +35 -14
- package/src/components/gui/gameover.ce +4 -3
- package/src/components/gui/menu/equip-menu.ce +9 -8
- package/src/components/gui/menu/exit-menu.ce +4 -3
- package/src/components/gui/menu/items-menu.ce +8 -7
- package/src/components/gui/menu/main-menu.ce +12 -11
- package/src/components/gui/menu/options-menu.ce +4 -3
- package/src/components/gui/menu/skills-menu.ce +2 -1
- package/src/components/gui/notification/notification.ce +7 -1
- package/src/components/gui/save-load.ce +11 -10
- package/src/components/gui/shop/shop.ce +17 -16
- package/src/components/gui/title-screen.ce +4 -3
- package/src/components/interaction-components.ce +23 -0
- package/src/components/scenes/canvas.ce +12 -7
- package/src/components/scenes/draw-map.ce +16 -5
- package/src/i18n.spec.ts +39 -0
- package/src/i18n.ts +59 -0
- package/src/index.ts +3 -0
- package/src/module.ts +43 -10
- package/src/services/actionInput.spec.ts +54 -0
- package/src/services/actionInput.ts +68 -1
- package/src/services/interactions.spec.ts +175 -0
- package/src/services/interactions.ts +722 -0
- package/src/services/keyboardControls.ts +2 -1
- package/src/services/standalone.ts +39 -10
- package/src/utils/mapId.ts +2 -0
|
@@ -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
|
|
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":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const normalizeRoomMapId: (mapId: string | undefined) => string | undefined;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mapId.js","names":[],"sources":["../../src/utils/mapId.ts"],"sourcesContent":["export const normalizeRoomMapId = (mapId: string | undefined): string | undefined =>\n typeof mapId === \"string\" ? mapId.replace(/^map-/, \"\") : undefined;\n"],"mappings":";AAAA,IAAa,sBAAsB,UACjC,OAAO,UAAU,WAAW,MAAM,QAAQ,SAAS,EAAE,IAAI,KAAA"}
|
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.13",
|
|
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.13",
|
|
26
|
+
"@rpgjs/server": "5.0.0-beta.13",
|
|
27
|
+
"@rpgjs/ui-css": "5.0.0-beta.12",
|
|
28
28
|
"@signe/di": "3.0.1",
|
|
29
29
|
"@signe/room": "3.0.1",
|
|
30
30
|
"@signe/sync": "3.0.1",
|
|
@@ -4,6 +4,10 @@ import { signal } from "canvasengine";
|
|
|
4
4
|
export class AnimationManager {
|
|
5
5
|
current = signal<any[]>([]);
|
|
6
6
|
|
|
7
|
+
clear(): void {
|
|
8
|
+
this.current.set([]);
|
|
9
|
+
}
|
|
10
|
+
|
|
7
11
|
displayEffect(params: any, player: RpgCommonPlayer | { x: number, y: number }): Promise<void> {
|
|
8
12
|
const id = generateUID();
|
|
9
13
|
const effectParams = params ?? {};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { ClientVisualRegistry } from "./ClientVisuals";
|
|
3
|
+
|
|
4
|
+
describe("ClientVisualRegistry", () => {
|
|
5
|
+
test("plays registered visual handlers with resolved objects and helpers", async () => {
|
|
6
|
+
const target = {
|
|
7
|
+
flash: vi.fn(),
|
|
8
|
+
showHit: vi.fn(),
|
|
9
|
+
setAnimation: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
const componentAnimation = {
|
|
12
|
+
displayEffect: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
const engine = {
|
|
15
|
+
scene: {},
|
|
16
|
+
getObjectById: vi.fn(() => target),
|
|
17
|
+
getComponentAnimation: vi.fn(() => componentAnimation),
|
|
18
|
+
playSound: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
mapShakeTrigger: {
|
|
20
|
+
start: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
} as any;
|
|
23
|
+
const registry = new ClientVisualRegistry();
|
|
24
|
+
|
|
25
|
+
registry.register("hit", ({ target, data }, helpers) => {
|
|
26
|
+
helpers.flash(target, { type: "tint", tint: "red" });
|
|
27
|
+
helpers.showHit(target, `-${data.damage}`);
|
|
28
|
+
helpers.component("hit-spark", target, { scale: 2 });
|
|
29
|
+
helpers.sound("hit");
|
|
30
|
+
helpers.animation(target, "hurt");
|
|
31
|
+
helpers.shake({ intensity: 2 });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await registry.play(
|
|
35
|
+
{
|
|
36
|
+
name: "hit",
|
|
37
|
+
data: {
|
|
38
|
+
targetId: "enemy-1",
|
|
39
|
+
damage: 12,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
engine
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
expect(engine.getObjectById).toHaveBeenCalledWith("enemy-1");
|
|
46
|
+
expect(target.flash).toHaveBeenCalledWith({ type: "tint", tint: "red" });
|
|
47
|
+
expect(target.showHit).toHaveBeenCalledWith("-12");
|
|
48
|
+
expect(componentAnimation.displayEffect).toHaveBeenCalledWith(
|
|
49
|
+
{ scale: 2 },
|
|
50
|
+
target
|
|
51
|
+
);
|
|
52
|
+
expect(engine.playSound).toHaveBeenCalledWith("hit", undefined);
|
|
53
|
+
expect(target.setAnimation).toHaveBeenCalledWith("hurt", 1);
|
|
54
|
+
expect(engine.mapShakeTrigger.start).toHaveBeenCalledWith({ intensity: 2 });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { RpgClientEngine } from "../RpgClientEngine";
|
|
2
|
+
import type { RpgClientMap } from "./Map";
|
|
3
|
+
|
|
4
|
+
export type ClientVisualPosition = {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
z?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ClientVisualPayload = Record<string, any> & {
|
|
11
|
+
source?: string;
|
|
12
|
+
sourceId?: string;
|
|
13
|
+
target?: string;
|
|
14
|
+
targetId?: string;
|
|
15
|
+
object?: string;
|
|
16
|
+
objectId?: string;
|
|
17
|
+
position?: ClientVisualPosition;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ClientVisualPacket = {
|
|
21
|
+
name: string;
|
|
22
|
+
data?: ClientVisualPayload;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type ClientVisualObjectTarget = string | Record<string, any> | undefined | null;
|
|
26
|
+
export type ClientVisualComponentTarget =
|
|
27
|
+
| ClientVisualObjectTarget
|
|
28
|
+
| ClientVisualPosition;
|
|
29
|
+
|
|
30
|
+
export type ClientVisualContext = {
|
|
31
|
+
name: string;
|
|
32
|
+
data: ClientVisualPayload;
|
|
33
|
+
engine: RpgClientEngine;
|
|
34
|
+
scene: RpgClientMap;
|
|
35
|
+
source?: any;
|
|
36
|
+
target?: any;
|
|
37
|
+
object?: any;
|
|
38
|
+
position?: ClientVisualPosition;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ClientVisualHelpers = {
|
|
42
|
+
getObject(id?: string | null): any;
|
|
43
|
+
flash(target?: ClientVisualObjectTarget, options?: Record<string, any>): void;
|
|
44
|
+
showHit(target: ClientVisualObjectTarget, text: string): void;
|
|
45
|
+
component(
|
|
46
|
+
id: string,
|
|
47
|
+
target?: ClientVisualComponentTarget,
|
|
48
|
+
params?: Record<string, any>
|
|
49
|
+
): void;
|
|
50
|
+
sound(id: string, options?: { volume?: number; loop?: boolean }): Promise<void>;
|
|
51
|
+
animation(
|
|
52
|
+
target: ClientVisualObjectTarget,
|
|
53
|
+
animationName: string,
|
|
54
|
+
options?: { graphic?: string | string[]; repeat?: number }
|
|
55
|
+
): void;
|
|
56
|
+
shake(options?: {
|
|
57
|
+
intensity?: number;
|
|
58
|
+
duration?: number;
|
|
59
|
+
frequency?: number;
|
|
60
|
+
direction?: string;
|
|
61
|
+
}): void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type ClientVisualHandler = (
|
|
65
|
+
context: ClientVisualContext,
|
|
66
|
+
helpers: ClientVisualHelpers
|
|
67
|
+
) => void | Promise<void>;
|
|
68
|
+
|
|
69
|
+
export type ClientVisualMap = Record<string, ClientVisualHandler>;
|
|
70
|
+
|
|
71
|
+
const isPosition = (value: any): value is ClientVisualPosition =>
|
|
72
|
+
value &&
|
|
73
|
+
typeof value === "object" &&
|
|
74
|
+
typeof value.x === "number" &&
|
|
75
|
+
typeof value.y === "number";
|
|
76
|
+
|
|
77
|
+
const resolvePosition = (data: ClientVisualPayload): ClientVisualPosition | undefined => {
|
|
78
|
+
if (isPosition(data.position)) return data.position;
|
|
79
|
+
if (typeof data.x === "number" && typeof data.y === "number") {
|
|
80
|
+
return { x: data.x, y: data.y, z: data.z };
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const resolveObject = (engine: RpgClientEngine, target: ClientVisualObjectTarget) => {
|
|
86
|
+
if (!target) return undefined;
|
|
87
|
+
if (typeof target === "string") return engine.getObjectById(target);
|
|
88
|
+
return target;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const resolvePayloadObject = (
|
|
92
|
+
engine: RpgClientEngine,
|
|
93
|
+
data: ClientVisualPayload,
|
|
94
|
+
keys: string[]
|
|
95
|
+
) => {
|
|
96
|
+
const id = keys
|
|
97
|
+
.map((key) => data[key])
|
|
98
|
+
.find((value) => typeof value === "string");
|
|
99
|
+
return resolveObject(engine, id);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const createClientVisualHelpers = (
|
|
103
|
+
engine: RpgClientEngine
|
|
104
|
+
): ClientVisualHelpers => ({
|
|
105
|
+
getObject(id) {
|
|
106
|
+
if (!id) return undefined;
|
|
107
|
+
return engine.getObjectById(id);
|
|
108
|
+
},
|
|
109
|
+
flash(target, options = {}) {
|
|
110
|
+
const object = resolveObject(engine, target);
|
|
111
|
+
object?.flash?.(options);
|
|
112
|
+
},
|
|
113
|
+
showHit(target, text) {
|
|
114
|
+
const object = resolveObject(engine, target);
|
|
115
|
+
object?.showHit?.(text);
|
|
116
|
+
},
|
|
117
|
+
component(id, target, params = {}) {
|
|
118
|
+
const object = isPosition(target) ? undefined : resolveObject(engine, target);
|
|
119
|
+
const position = isPosition(target) ? target : undefined;
|
|
120
|
+
const anchor = object ?? position;
|
|
121
|
+
if (!anchor) return;
|
|
122
|
+
engine.getComponentAnimation(id).displayEffect(params, anchor);
|
|
123
|
+
},
|
|
124
|
+
sound(id, options) {
|
|
125
|
+
return engine.playSound(id, options);
|
|
126
|
+
},
|
|
127
|
+
animation(target, animationName, options = {}) {
|
|
128
|
+
const object = resolveObject(engine, target);
|
|
129
|
+
if (!object?.setAnimation) return;
|
|
130
|
+
if (options.graphic !== undefined) {
|
|
131
|
+
object.setAnimation(animationName, options.graphic, options.repeat ?? 1);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
object.setAnimation(animationName, options.repeat ?? 1);
|
|
135
|
+
},
|
|
136
|
+
shake(options = {}) {
|
|
137
|
+
engine.mapShakeTrigger.start(options);
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export class ClientVisualRegistry {
|
|
142
|
+
private readonly handlers = new Map<string, ClientVisualHandler>();
|
|
143
|
+
|
|
144
|
+
register(name: string, handler: ClientVisualHandler) {
|
|
145
|
+
this.handlers.set(name, handler);
|
|
146
|
+
return handler;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
registerMany(visuals: ClientVisualMap) {
|
|
150
|
+
Object.entries(visuals).forEach(([name, handler]) => {
|
|
151
|
+
this.register(name, handler);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get(name: string) {
|
|
156
|
+
return this.handlers.get(name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async play(packet: ClientVisualPacket, engine: RpgClientEngine) {
|
|
160
|
+
const handler = this.handlers.get(packet.name);
|
|
161
|
+
if (!handler) {
|
|
162
|
+
console.warn(`Client visual "${packet.name}" is not registered`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = packet.data ?? {};
|
|
167
|
+
const context: ClientVisualContext = {
|
|
168
|
+
name: packet.name,
|
|
169
|
+
data,
|
|
170
|
+
engine,
|
|
171
|
+
scene: engine.scene,
|
|
172
|
+
source: resolvePayloadObject(engine, data, ["sourceId", "source"]),
|
|
173
|
+
target: resolvePayloadObject(engine, data, ["targetId", "target"]),
|
|
174
|
+
object: resolvePayloadObject(engine, data, ["objectId", "object"]),
|
|
175
|
+
position: resolvePosition(data),
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
await handler(context, createClientVisualHelpers(engine));
|
|
180
|
+
} catch (error) {
|
|
181
|
+
console.error(`Client visual "${packet.name}" failed`, error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { EventComponentResolverRegistry, normalizeEventComponent } from "./EventComponentResolver";
|
|
3
|
+
|
|
4
|
+
describe("EventComponentResolverRegistry", () => {
|
|
5
|
+
test("uses the last resolver returning a custom event component", () => {
|
|
6
|
+
const registry = new EventComponentResolverRegistry();
|
|
7
|
+
const firstComponent = (() => null) as any;
|
|
8
|
+
const secondComponent = (() => null) as any;
|
|
9
|
+
const event = { id: "event-1" } as any;
|
|
10
|
+
|
|
11
|
+
registry.add(() => firstComponent);
|
|
12
|
+
registry.add(() => null);
|
|
13
|
+
registry.add(() => ({ component: secondComponent, props: { variant: "open" } }));
|
|
14
|
+
|
|
15
|
+
expect(registry.resolve(event)).toEqual({
|
|
16
|
+
component: secondComponent,
|
|
17
|
+
props: { variant: "open" }
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("returns null when no resolver matches", () => {
|
|
22
|
+
const registry = new EventComponentResolverRegistry();
|
|
23
|
+
|
|
24
|
+
registry.add(() => undefined);
|
|
25
|
+
registry.add(() => null);
|
|
26
|
+
|
|
27
|
+
expect(registry.resolve({ id: "event-1" } as any)).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("can clear registered resolvers", () => {
|
|
31
|
+
const registry = new EventComponentResolverRegistry();
|
|
32
|
+
const component = (() => null) as any;
|
|
33
|
+
|
|
34
|
+
registry.add(() => component);
|
|
35
|
+
registry.clear();
|
|
36
|
+
|
|
37
|
+
expect(registry.resolve({ id: "event-1" } as any)).toBeNull();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("always injects the real sprite prop for direct components", () => {
|
|
41
|
+
const component = (() => null) as any;
|
|
42
|
+
const sprite = { id: "event-1" } as any;
|
|
43
|
+
|
|
44
|
+
expect(normalizeEventComponent(component, sprite)).toEqual({
|
|
45
|
+
component,
|
|
46
|
+
props: { sprite },
|
|
47
|
+
dependencies: [],
|
|
48
|
+
renderGraphic: false
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("keeps custom props from replacing the sprite prop", () => {
|
|
53
|
+
const component = (() => null) as any;
|
|
54
|
+
const sprite = { id: "real-event" } as any;
|
|
55
|
+
const spoofedSprite = { id: "spoofed-event" };
|
|
56
|
+
|
|
57
|
+
const normalized = normalizeEventComponent({
|
|
58
|
+
component,
|
|
59
|
+
props: { sprite: spoofedSprite, variant: "wood" },
|
|
60
|
+
dependencies: () => ["ready"],
|
|
61
|
+
renderGraphic: true
|
|
62
|
+
}, sprite);
|
|
63
|
+
|
|
64
|
+
expect(normalized).toEqual({
|
|
65
|
+
component,
|
|
66
|
+
props: { variant: "wood", sprite },
|
|
67
|
+
dependencies: ["ready"],
|
|
68
|
+
renderGraphic: true
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("supports dynamic props", () => {
|
|
73
|
+
const component = (() => null) as any;
|
|
74
|
+
const sprite = { id: "event-1", name: "CHEST" } as any;
|
|
75
|
+
|
|
76
|
+
expect(normalizeEventComponent({
|
|
77
|
+
component,
|
|
78
|
+
props: (event) => ({ label: (event as any).name })
|
|
79
|
+
}, sprite)?.props).toEqual({
|
|
80
|
+
label: "CHEST",
|
|
81
|
+
sprite
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { EventComponentConfig, EventComponentSprite } from "../RpgClient";
|
|
2
|
+
import type { RpgClientEvent } from "./Event";
|
|
3
|
+
|
|
4
|
+
export type EventComponentResolver = (event: EventComponentSprite) => EventComponentConfig | null | undefined;
|
|
5
|
+
|
|
6
|
+
export interface NormalizedEventComponentConfig {
|
|
7
|
+
component: any;
|
|
8
|
+
props: Record<string, any>;
|
|
9
|
+
dependencies: any[];
|
|
10
|
+
renderGraphic: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class EventComponentResolverRegistry {
|
|
14
|
+
private resolvers: EventComponentResolver[] = [];
|
|
15
|
+
|
|
16
|
+
add(resolver: EventComponentResolver) {
|
|
17
|
+
this.resolvers.push(resolver);
|
|
18
|
+
return resolver;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
resolve(event: RpgClientEvent): EventComponentConfig | null {
|
|
22
|
+
let resolved: EventComponentConfig | null = null;
|
|
23
|
+
this.resolvers.forEach((resolver) => {
|
|
24
|
+
const result = resolver(event as EventComponentSprite);
|
|
25
|
+
if (result !== undefined && result !== null) {
|
|
26
|
+
resolved = result;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
return resolved;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clear() {
|
|
33
|
+
this.resolvers = [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function withoutReservedSpriteProp(props: unknown): Record<string, any> {
|
|
38
|
+
if (!props || typeof props !== "object") return {};
|
|
39
|
+
const { sprite: _ignoredSprite, ...safeProps } = props as Record<string, any>;
|
|
40
|
+
return safeProps;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeEventComponent(
|
|
44
|
+
componentConfig: EventComponentConfig | null | undefined,
|
|
45
|
+
sprite: RpgClientEvent
|
|
46
|
+
): NormalizedEventComponentConfig | null {
|
|
47
|
+
if (!componentConfig) return null;
|
|
48
|
+
|
|
49
|
+
if (typeof componentConfig === "object" && "component" in componentConfig) {
|
|
50
|
+
const propsValue = componentConfig.props !== undefined
|
|
51
|
+
? componentConfig.props
|
|
52
|
+
: componentConfig.data;
|
|
53
|
+
const props = typeof propsValue === "function"
|
|
54
|
+
? propsValue(sprite as EventComponentSprite)
|
|
55
|
+
: propsValue;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
component: componentConfig.component,
|
|
59
|
+
props: {
|
|
60
|
+
...withoutReservedSpriteProp(props),
|
|
61
|
+
sprite
|
|
62
|
+
},
|
|
63
|
+
dependencies: componentConfig.dependencies ? componentConfig.dependencies(sprite as EventComponentSprite) : [],
|
|
64
|
+
renderGraphic: componentConfig.renderGraphic === true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
component: componentConfig,
|
|
70
|
+
props: { sprite },
|
|
71
|
+
dependencies: [],
|
|
72
|
+
renderGraphic: false
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/Game/Map.ts
CHANGED
|
@@ -88,6 +88,16 @@ export class RpgClientMap extends RpgCommonMap<any> {
|
|
|
88
88
|
const currentPlayer = !force && currentPlayerId
|
|
89
89
|
? this.players()[currentPlayerId]
|
|
90
90
|
: undefined;
|
|
91
|
+
const players = this.players();
|
|
92
|
+
const events = this.events();
|
|
93
|
+
|
|
94
|
+
Object.entries(players).forEach(([id, player]) => {
|
|
95
|
+
if (!player || (!force && id === currentPlayerId)) return;
|
|
96
|
+
(player as any).resetAnimationState?.();
|
|
97
|
+
});
|
|
98
|
+
Object.values(events).forEach((event) => {
|
|
99
|
+
(event as any)?.resetAnimationState?.();
|
|
100
|
+
});
|
|
91
101
|
|
|
92
102
|
this.players.set(
|
|
93
103
|
currentPlayerId && currentPlayer ? { [currentPlayerId]: currentPlayer } : {}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { signal } from "canvasengine";
|
|
3
|
+
import { appendFramePayload, RpgClientObject, withGraphicDisplayScale } from "./Object";
|
|
4
|
+
|
|
5
|
+
vi.mock("../RpgClientEngine", () => ({
|
|
6
|
+
RpgClientEngine: class RpgClientEngine {},
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("../core/inject", () => ({
|
|
10
|
+
inject: () => ({}),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
function createObject() {
|
|
14
|
+
const object = Object.create(RpgClientObject.prototype) as RpgClientObject;
|
|
15
|
+
object.animationName = signal("stand");
|
|
16
|
+
object.graphics = signal(["hero"]) as any;
|
|
17
|
+
object.animationCurrentIndex = signal(0);
|
|
18
|
+
object.animationIsPlaying = signal(false);
|
|
19
|
+
return object;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("RpgClientObject animations", () => {
|
|
23
|
+
test("accepts a single frame payload without requiring iterable spread", () => {
|
|
24
|
+
expect(
|
|
25
|
+
appendFramePayload({ stale: true }, { x: 10, y: 20, ts: 1 }),
|
|
26
|
+
).toEqual([{ x: 10, y: 20, ts: 1 }]);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("keeps instance scale outside the spritesheet transform scale", () => {
|
|
30
|
+
expect(withGraphicDisplayScale({ id: "hero" }, 0.5)).toEqual({
|
|
31
|
+
id: "hero",
|
|
32
|
+
displayScale: 0.5,
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("marks temporary animation as finished before restoring locomotion animation", async () => {
|
|
37
|
+
const object = createObject();
|
|
38
|
+
const animationChanges: Array<{ name: string; isPlaying: boolean }> = [];
|
|
39
|
+
|
|
40
|
+
object.animationName.observable.subscribe((name) => {
|
|
41
|
+
animationChanges.push({
|
|
42
|
+
name,
|
|
43
|
+
isPlaying: object.animationIsPlaying(),
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const done = object.setAnimation("attack", 1, { timeoutMs: 10000 });
|
|
48
|
+
object.animationCurrentIndex.set(1);
|
|
49
|
+
|
|
50
|
+
await done;
|
|
51
|
+
|
|
52
|
+
expect(object.animationName()).toBe("stand");
|
|
53
|
+
expect(object.animationIsPlaying()).toBe(false);
|
|
54
|
+
expect(animationChanges).toContainEqual({
|
|
55
|
+
name: "stand",
|
|
56
|
+
isPlaying: false,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
package/src/Game/Object.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Hooks, ModulesToken, RpgCommonPlayer } from "@rpgjs/common";
|
|
2
2
|
import { trigger, signal, type Trigger } from "canvasengine";
|
|
3
|
-
import { from, map, of, Subscription, switchMap } from "rxjs";
|
|
3
|
+
import { combineLatest, from, map, of, startWith, Subscription, switchMap } from "rxjs";
|
|
4
4
|
import { inject } from "../core/inject";
|
|
5
5
|
import { RpgClientEngine } from "../RpgClientEngine";
|
|
6
6
|
type Frame = { x: number; y: number; ts: number };
|
|
@@ -29,6 +29,24 @@ type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
|
|
|
29
29
|
start(config?: T): Promise<void>;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
export const withGraphicDisplayScale = (spritesheet: any, scale: unknown): any => {
|
|
33
|
+
if (!spritesheet || typeof spritesheet !== "object") return spritesheet;
|
|
34
|
+
if (scale === undefined || scale === null) return spritesheet;
|
|
35
|
+
return {
|
|
36
|
+
...spritesheet,
|
|
37
|
+
displayScale: scale,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const appendFramePayload = (current: unknown, items: unknown): Frame[] => {
|
|
42
|
+
const frameItems = Array.isArray(items) ? items : items ? [items] : [];
|
|
43
|
+
const nextFrames = frameItems.flatMap((item): Frame[] =>
|
|
44
|
+
Array.isArray(item) ? item : [item as Frame]
|
|
45
|
+
);
|
|
46
|
+
const currentFrames = Array.isArray(current) ? current as Frame[] : [];
|
|
47
|
+
return currentFrames.concat(nextFrames);
|
|
48
|
+
};
|
|
49
|
+
|
|
32
50
|
export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
33
51
|
abstract _type: string;
|
|
34
52
|
emitParticleTrigger = trigger();
|
|
@@ -51,18 +69,24 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
51
69
|
this._frames.observable.subscribe(({ items }) => {
|
|
52
70
|
if (!this.id) return;
|
|
53
71
|
//if (this.id == this.engine.playerIdSignal()!) return;
|
|
54
|
-
|
|
55
|
-
Array.isArray(item) ? item : [item]
|
|
56
|
-
);
|
|
57
|
-
this.frames = [...this.frames, ...nextFrames];
|
|
72
|
+
this.frames = appendFramePayload(this.frames, items);
|
|
58
73
|
});
|
|
59
74
|
|
|
60
|
-
this.graphics.observable
|
|
75
|
+
const graphics$ = this.graphics.observable.pipe(map(({ items }) => items));
|
|
76
|
+
const graphicScale$ = this._graphicScale.observable.pipe(
|
|
77
|
+
startWith({ value: this._graphicScale() }),
|
|
78
|
+
map((payload: any) => payload?.value ?? payload),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
combineLatest([graphics$, graphicScale$])
|
|
61
82
|
.pipe(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
return from(Promise.all(
|
|
83
|
+
switchMap(([graphics, scale]) => {
|
|
84
|
+
const graphicRefs = Array.isArray(graphics) ? graphics : [];
|
|
85
|
+
if (graphicRefs.length === 0) return of([]);
|
|
86
|
+
return from(Promise.all(graphicRefs.map(async (graphic) => {
|
|
87
|
+
const spritesheet = await this.engine.getSpriteSheet(graphic);
|
|
88
|
+
return withGraphicDisplayScale(spritesheet, scale);
|
|
89
|
+
})));
|
|
66
90
|
})
|
|
67
91
|
)
|
|
68
92
|
.subscribe((sheets) => {
|
|
@@ -130,12 +154,12 @@ export abstract class RpgClientObject extends RpgCommonPlayer {
|
|
|
130
154
|
const restoreState = this.animationRestoreState;
|
|
131
155
|
this.clearAnimationControls();
|
|
132
156
|
this.animationCurrentIndex.set(0);
|
|
157
|
+
this.animationRestoreState = undefined;
|
|
158
|
+
this.animationIsPlaying.set(false);
|
|
133
159
|
if (restoreState) {
|
|
134
160
|
this.animationName.set(restoreState.animationName);
|
|
135
161
|
this.graphics.set([...restoreState.graphics]);
|
|
136
162
|
}
|
|
137
|
-
this.animationRestoreState = undefined;
|
|
138
|
-
this.animationIsPlaying.set(false);
|
|
139
163
|
this.resolveAnimationWait();
|
|
140
164
|
}
|
|
141
165
|
|