@rpgjs/client 5.0.0-beta.10 → 5.0.0-beta.12
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 +21 -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.js +2 -2
- package/dist/Game/Object.js.map +1 -1
- package/dist/Game/Object.spec.d.ts +1 -0
- package/dist/Game/ProjectileManager.d.ts +98 -0
- package/dist/Game/ProjectileManager.js +196 -0
- package/dist/Game/ProjectileManager.js.map +1 -0
- package/dist/Game/ProjectileManager.spec.d.ts +1 -0
- package/dist/RpgClient.d.ts +117 -13
- package/dist/RpgClientEngine.d.ts +82 -4
- package/dist/RpgClientEngine.js +296 -51
- 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/hit.ce.js.map +1 -1
- 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 +140 -40
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/dynamics/bar.ce.js +4 -3
- package/dist/components/dynamics/bar.ce.js.map +1 -1
- package/dist/components/dynamics/image.ce.js +2 -1
- package/dist/components/dynamics/image.ce.js.map +1 -1
- package/dist/components/dynamics/shape.ce.js +3 -2
- package/dist/components/dynamics/shape.ce.js.map +1 -1
- package/dist/components/dynamics/text.ce.js +9 -8
- package/dist/components/dynamics/text.ce.js.map +1 -1
- package/dist/components/gui/dialogbox/index.ce.js +3 -2
- package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
- package/dist/components/gui/gameover.ce.js +3 -2
- package/dist/components/gui/gameover.ce.js.map +1 -1
- package/dist/components/gui/hud/hud.ce.js.map +1 -1
- package/dist/components/gui/menu/equip-menu.ce.js +2 -1
- package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/exit-menu.ce.js +2 -1
- package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/items-menu.ce.js +3 -2
- package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/main-menu.ce.js +3 -2
- package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/components/gui/notification/notification.ce.js.map +1 -1
- package/dist/components/gui/save-load.ce.js +2 -1
- package/dist/components/gui/save-load.ce.js.map +1 -1
- package/dist/components/gui/shop/shop.ce.js +3 -2
- package/dist/components/gui/shop/shop.ce.js.map +1 -1
- package/dist/components/gui/title-screen.ce.js +3 -2
- package/dist/components/gui/title-screen.ce.js.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +1 -0
- package/dist/components/player-components.ce.js +11 -10
- package/dist/components/player-components.ce.js.map +1 -1
- package/dist/components/prebuilt/hp-bar.ce.js +4 -3
- package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
- package/dist/components/prebuilt/light-halo.ce.js +2 -1
- package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
- package/dist/components/scenes/canvas.ce.js +12 -4
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +6 -3
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/components/scenes/event-layer.ce.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +10 -5
- package/dist/module.js +18 -0
- package/dist/module.js.map +1 -1
- package/dist/services/actionInput.d.ts +14 -0
- package/dist/services/actionInput.js +59 -0
- package/dist/services/actionInput.js.map +1 -0
- package/dist/services/actionInput.spec.d.ts +1 -0
- package/dist/services/mmorpg-connection.d.ts +5 -0
- package/dist/services/mmorpg-connection.js +50 -0
- package/dist/services/mmorpg-connection.js.map +1 -0
- package/dist/services/mmorpg-connection.spec.d.ts +1 -0
- package/dist/services/mmorpg.d.ts +10 -4
- package/dist/services/mmorpg.js +48 -30
- package/dist/services/mmorpg.js.map +1 -1
- package/dist/services/pointerContext.d.ts +11 -0
- package/dist/services/pointerContext.js +48 -0
- package/dist/services/pointerContext.js.map +1 -0
- package/dist/services/pointerContext.spec.d.ts +1 -0
- package/dist/services/standalone-message.d.ts +1 -0
- package/dist/services/standalone-message.js +9 -0
- package/dist/services/standalone-message.js.map +1 -0
- package/dist/services/standalone.d.ts +3 -1
- package/dist/services/standalone.js +34 -15
- package/dist/services/standalone.js.map +1 -1
- package/dist/services/standalone.spec.d.ts +1 -0
- 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 +7 -7
- 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 +46 -0
- package/src/Game/Object.ts +2 -2
- package/src/Game/ProjectileManager.spec.ts +449 -0
- package/src/Game/ProjectileManager.ts +346 -0
- package/src/RpgClient.ts +130 -15
- package/src/RpgClientEngine.ts +405 -69
- package/src/components/animations/fx.ce +101 -0
- package/src/components/animations/index.ts +4 -2
- package/src/components/character.ce +185 -40
- package/src/components/dynamics/bar.ce +4 -3
- package/src/components/dynamics/image.ce +2 -1
- package/src/components/dynamics/shape.ce +3 -2
- package/src/components/dynamics/text.ce +9 -8
- package/src/components/gui/dialogbox/index.ce +3 -2
- package/src/components/gui/gameover.ce +2 -1
- package/src/components/gui/menu/equip-menu.ce +2 -1
- package/src/components/gui/menu/exit-menu.ce +2 -1
- package/src/components/gui/menu/items-menu.ce +3 -2
- package/src/components/gui/menu/main-menu.ce +2 -1
- package/src/components/gui/save-load.ce +2 -1
- package/src/components/gui/shop/shop.ce +3 -2
- package/src/components/gui/title-screen.ce +2 -1
- package/src/components/index.ts +2 -1
- package/src/components/player-components.ce +11 -10
- package/src/components/prebuilt/hp-bar.ce +4 -3
- package/src/components/prebuilt/light-halo.ce +2 -2
- package/src/components/scenes/canvas.ce +10 -2
- package/src/components/scenes/draw-map.ce +17 -3
- package/src/index.ts +4 -0
- package/src/module.ts +24 -0
- package/src/services/actionInput.spec.ts +155 -0
- package/src/services/actionInput.ts +120 -0
- package/src/services/mmorpg-connection.spec.ts +99 -0
- package/src/services/mmorpg-connection.ts +69 -0
- package/src/services/mmorpg.ts +60 -34
- package/src/services/pointerContext.spec.ts +36 -0
- package/src/services/pointerContext.ts +84 -0
- package/src/services/standalone-message.ts +7 -0
- package/src/services/standalone.spec.ts +34 -0
- package/src/services/standalone.ts +42 -12
- package/src/utils/mapId.ts +2 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { isNativeSocketEvent, parseSocketMessage, waitForRpgjsConnected } from "./mmorpg-connection";
|
|
3
|
+
|
|
4
|
+
function wait(ms = 0): Promise<void> {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
class FakeConnection extends EventTarget {
|
|
9
|
+
serverMessage(data: any) {
|
|
10
|
+
this.dispatchEvent(new MessageEvent("message", {
|
|
11
|
+
data: typeof data === "string" ? data : JSON.stringify(data),
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
close(reason = "", code = 1008) {
|
|
16
|
+
this.dispatchEvent(new CloseEvent("close", {
|
|
17
|
+
code,
|
|
18
|
+
reason,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
fail(message = "WebSocket connection failed") {
|
|
23
|
+
this.dispatchEvent(new ErrorEvent("error", {
|
|
24
|
+
message,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("MMORPG connection gate", () => {
|
|
30
|
+
test("resolves only after the RPGJS connected packet", async () => {
|
|
31
|
+
const conn = new FakeConnection();
|
|
32
|
+
let resolved = false;
|
|
33
|
+
const promise = waitForRpgjsConnected(conn).then(() => {
|
|
34
|
+
resolved = true;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
conn.serverMessage({ type: "sync", value: { pId: "player-1" } });
|
|
38
|
+
await wait();
|
|
39
|
+
|
|
40
|
+
expect(resolved).toBe(false);
|
|
41
|
+
|
|
42
|
+
conn.serverMessage({ type: "connected", value: { id: "conn-1" } });
|
|
43
|
+
await promise;
|
|
44
|
+
|
|
45
|
+
expect(resolved).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("rejects when the socket closes before the RPGJS connected packet", async () => {
|
|
49
|
+
const conn = new FakeConnection();
|
|
50
|
+
const promise = waitForRpgjsConnected(conn);
|
|
51
|
+
|
|
52
|
+
conn.close("Authentication failed");
|
|
53
|
+
|
|
54
|
+
await expect(promise).rejects.toThrow("Authentication failed");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("can ignore the clean close emitted by PartySocket during manual reconnect", async () => {
|
|
58
|
+
const conn = new FakeConnection();
|
|
59
|
+
const promise = waitForRpgjsConnected(conn, 10000, { ignoreCleanClose: true });
|
|
60
|
+
|
|
61
|
+
conn.close("", 1000);
|
|
62
|
+
await wait();
|
|
63
|
+
|
|
64
|
+
conn.serverMessage({ type: "connected", value: { id: "conn-2" } });
|
|
65
|
+
|
|
66
|
+
await expect(promise).resolves.toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("can ignore PartySocket reconnect close events with non-string reasons", async () => {
|
|
70
|
+
const conn = new FakeConnection();
|
|
71
|
+
const promise = waitForRpgjsConnected(conn, 10000, { ignoreCleanClose: true });
|
|
72
|
+
|
|
73
|
+
conn.dispatchEvent(new CloseEvent("close", {
|
|
74
|
+
code: 1000,
|
|
75
|
+
reason: new Event("close") as any,
|
|
76
|
+
}));
|
|
77
|
+
await wait();
|
|
78
|
+
|
|
79
|
+
conn.serverMessage({ type: "connected", value: { id: "conn-3" } });
|
|
80
|
+
|
|
81
|
+
await expect(promise).resolves.toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("rejects when the socket errors before the RPGJS connected packet", async () => {
|
|
85
|
+
const conn = new FakeConnection();
|
|
86
|
+
const promise = waitForRpgjsConnected(conn);
|
|
87
|
+
|
|
88
|
+
conn.fail("Unauthorized");
|
|
89
|
+
|
|
90
|
+
await expect(promise).rejects.toThrow("Unauthorized");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("parses connection messages and detects native socket events", () => {
|
|
94
|
+
expect(parseSocketMessage(JSON.stringify({ type: "connected" }))).toEqual({ type: "connected" });
|
|
95
|
+
expect(parseSocketMessage("not-json")).toBeUndefined();
|
|
96
|
+
expect(isNativeSocketEvent("open")).toBe(true);
|
|
97
|
+
expect(isNativeSocketEvent("sync")).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export function parseSocketMessage(data: any) {
|
|
2
|
+
if (typeof data !== "string") {
|
|
3
|
+
return data;
|
|
4
|
+
}
|
|
5
|
+
try {
|
|
6
|
+
return JSON.parse(data);
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isNativeSocketEvent(event: string) {
|
|
14
|
+
return event === "open" || event === "close" || event === "error";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function waitForRpgjsConnected(
|
|
18
|
+
conn: any,
|
|
19
|
+
timeoutMs = 10000,
|
|
20
|
+
options: { ignoreCleanClose?: boolean } = {},
|
|
21
|
+
): Promise<void> {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let timeoutId: number | undefined;
|
|
24
|
+
|
|
25
|
+
const cleanup = () => {
|
|
26
|
+
conn.removeEventListener("message", onMessage);
|
|
27
|
+
conn.removeEventListener("close", onClose);
|
|
28
|
+
conn.removeEventListener("error", onError);
|
|
29
|
+
if (timeoutId !== undefined) {
|
|
30
|
+
window.clearTimeout(timeoutId);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
const rejectWith = (error: Error) => {
|
|
34
|
+
cleanup();
|
|
35
|
+
reject(error);
|
|
36
|
+
};
|
|
37
|
+
const onMessage = (event: MessageEvent) => {
|
|
38
|
+
const data = parseSocketMessage(event.data);
|
|
39
|
+
if (data?.type !== "connected") {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
cleanup();
|
|
43
|
+
resolve();
|
|
44
|
+
};
|
|
45
|
+
const onClose = (event: CloseEvent) => {
|
|
46
|
+
const rawReason: unknown = (event as any).reason;
|
|
47
|
+
if (
|
|
48
|
+
options.ignoreCleanClose
|
|
49
|
+
&& (event.code === 1000 || rawReason instanceof Event || typeof rawReason !== "string")
|
|
50
|
+
) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const reason = typeof rawReason === "string" && rawReason
|
|
54
|
+
? rawReason
|
|
55
|
+
: "WebSocket closed before RPGJS connection was accepted";
|
|
56
|
+
rejectWith(new Error(reason));
|
|
57
|
+
};
|
|
58
|
+
const onError = (event: ErrorEvent) => {
|
|
59
|
+
rejectWith(new Error(event.message || "WebSocket connection failed"));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
conn.addEventListener("message", onMessage);
|
|
63
|
+
conn.addEventListener("close", onClose);
|
|
64
|
+
conn.addEventListener("error", onError);
|
|
65
|
+
timeoutId = window.setTimeout(() => {
|
|
66
|
+
rejectWith(new Error("RPGJS connection timeout"));
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
});
|
|
69
|
+
}
|
package/src/services/mmorpg.ts
CHANGED
|
@@ -2,21 +2,25 @@ import { Context } from "@signe/di";
|
|
|
2
2
|
import { connectionRoom } from "@signe/sync/client";
|
|
3
3
|
import { RpgGui } from "../Gui/Gui";
|
|
4
4
|
import { RpgClientEngine } from "../RpgClientEngine";
|
|
5
|
-
import { AbstractWebsocket, SocketUpdateProperties, WebSocketToken } from "./AbstractSocket";
|
|
5
|
+
import { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from "./AbstractSocket";
|
|
6
6
|
import { UpdateMapService, UpdateMapToken } from "@rpgjs/common";
|
|
7
7
|
import { provideKeyboardControls } from "./keyboardControls";
|
|
8
8
|
import { provideSaveClient } from "./save";
|
|
9
|
+
import { isNativeSocketEvent, waitForRpgjsConnected } from "./mmorpg-connection";
|
|
9
10
|
|
|
10
|
-
interface MmorpgOptions {
|
|
11
|
+
export interface MmorpgOptions {
|
|
11
12
|
host?: string;
|
|
12
13
|
connectionId?: string;
|
|
13
14
|
connectionIdScope?: "local" | "session" | "ephemeral";
|
|
15
|
+
query?: SocketQuery | (() => SocketQuery | undefined);
|
|
16
|
+
socketOptions?: Record<string, any>;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
class BridgeWebsocket extends AbstractWebsocket {
|
|
19
|
+
export class BridgeWebsocket extends AbstractWebsocket {
|
|
17
20
|
private socket: any;
|
|
18
21
|
private privateId: string;
|
|
19
22
|
private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];
|
|
23
|
+
private acceptedOpenListeners = new Set<(data: any) => void>();
|
|
20
24
|
private targetRoom = "lobby-1";
|
|
21
25
|
|
|
22
26
|
constructor(protected context: Context, private options: MmorpgOptions = {}) {
|
|
@@ -51,6 +55,14 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
51
55
|
return id;
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
private resolveQuery(): SocketQuery {
|
|
59
|
+
const query = typeof this.options.query === "function"
|
|
60
|
+
? this.options.query()
|
|
61
|
+
: this.options.query;
|
|
62
|
+
|
|
63
|
+
return query ?? {};
|
|
64
|
+
}
|
|
65
|
+
|
|
54
66
|
async connection(listeners?: (data: any) => void) {
|
|
55
67
|
// tmp
|
|
56
68
|
class Room {
|
|
@@ -59,17 +71,28 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
59
71
|
const instance = new Room()
|
|
60
72
|
const host = this.options.host || window.location.host;
|
|
61
73
|
this.socket = await connectionRoom({
|
|
74
|
+
maxRetries: 0,
|
|
75
|
+
...this.options.socketOptions,
|
|
62
76
|
host,
|
|
63
77
|
room: this.targetRoom,
|
|
64
78
|
id: this.privateId,
|
|
65
79
|
query: {
|
|
80
|
+
...this.resolveQuery(),
|
|
66
81
|
id: this.privateId,
|
|
67
82
|
},
|
|
68
83
|
}, instance)
|
|
69
84
|
|
|
70
|
-
|
|
71
|
-
this.pendingOn.forEach(({ event, callback }) => this.socket.on(event, callback));
|
|
85
|
+
const pendingOn = this.pendingOn;
|
|
72
86
|
this.pendingOn = [];
|
|
87
|
+
pendingOn
|
|
88
|
+
.filter(({ event }) => !this.isNativeSocketEvent(event))
|
|
89
|
+
.forEach(({ event, callback }) => this.attachEvent(event, callback));
|
|
90
|
+
await waitForRpgjsConnected(this.socket.conn);
|
|
91
|
+
pendingOn
|
|
92
|
+
.filter(({ event }) => this.isNativeSocketEvent(event))
|
|
93
|
+
.forEach(({ event, callback }) => this.attachEvent(event, callback));
|
|
94
|
+
this.emitAcceptedOpen();
|
|
95
|
+
listeners?.(this.socket)
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
on(key: string, callback: (data: any) => void) {
|
|
@@ -77,11 +100,19 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
77
100
|
this.pendingOn.push({ event: key, callback });
|
|
78
101
|
return;
|
|
79
102
|
}
|
|
80
|
-
this.
|
|
103
|
+
this.attachEvent(key, callback);
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
off(event: string, callback: (data: any) => void) {
|
|
84
107
|
if (!this.socket) return;
|
|
108
|
+
if (event === "open") {
|
|
109
|
+
this.acceptedOpenListeners.delete(callback);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (this.isNativeSocketEvent(event)) {
|
|
113
|
+
this.socket.conn.removeEventListener(event, callback);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
85
116
|
this.socket.off(event, callback);
|
|
86
117
|
}
|
|
87
118
|
|
|
@@ -89,6 +120,23 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
89
120
|
this.socket.emit(event, data);
|
|
90
121
|
}
|
|
91
122
|
|
|
123
|
+
private attachEvent(event: string, callback: (data: any) => void) {
|
|
124
|
+
if (event === "open") {
|
|
125
|
+
this.acceptedOpenListeners.add(callback);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (this.isNativeSocketEvent(event)) {
|
|
129
|
+
this.socket.conn.addEventListener(event, callback);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.socket.on(event, callback);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private emitAcceptedOpen() {
|
|
136
|
+
const event = new Event("open");
|
|
137
|
+
this.acceptedOpenListeners.forEach((callback) => callback(event));
|
|
138
|
+
}
|
|
139
|
+
|
|
92
140
|
updateProperties({ room, host, query }: SocketUpdateProperties) {
|
|
93
141
|
if (!this.socket?.conn) return;
|
|
94
142
|
this.targetRoom = room;
|
|
@@ -97,46 +145,24 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
97
145
|
id: this.privateId,
|
|
98
146
|
host: host || this.options.host || window.location.host,
|
|
99
147
|
query: {
|
|
148
|
+
...this.resolveQuery(),
|
|
100
149
|
...query,
|
|
101
150
|
id: this.privateId,
|
|
102
151
|
},
|
|
103
152
|
})
|
|
104
153
|
}
|
|
105
154
|
|
|
106
|
-
private
|
|
107
|
-
return
|
|
108
|
-
let timeoutId: number | undefined;
|
|
109
|
-
const onOpen = () => {
|
|
110
|
-
cleanup();
|
|
111
|
-
resolve();
|
|
112
|
-
};
|
|
113
|
-
const onError = () => {
|
|
114
|
-
cleanup();
|
|
115
|
-
reject(new Error("WebSocket reconnect failed"));
|
|
116
|
-
};
|
|
117
|
-
const cleanup = () => {
|
|
118
|
-
conn.removeEventListener("open", onOpen);
|
|
119
|
-
conn.removeEventListener("error", onError);
|
|
120
|
-
if (timeoutId !== undefined) {
|
|
121
|
-
window.clearTimeout(timeoutId);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
conn.addEventListener("open", onOpen);
|
|
126
|
-
conn.addEventListener("error", onError);
|
|
127
|
-
timeoutId = window.setTimeout(() => {
|
|
128
|
-
cleanup();
|
|
129
|
-
reject(new Error("WebSocket reconnect timeout"));
|
|
130
|
-
}, timeoutMs);
|
|
131
|
-
});
|
|
155
|
+
private isNativeSocketEvent(event: string) {
|
|
156
|
+
return isNativeSocketEvent(event);
|
|
132
157
|
}
|
|
133
158
|
|
|
134
159
|
async reconnect(_listeners?: (data: any) => void): Promise<void> {
|
|
135
160
|
if (!this.socket?.conn) return;
|
|
136
161
|
const conn = this.socket.conn;
|
|
137
|
-
const
|
|
162
|
+
const connected = waitForRpgjsConnected(conn, 10000, { ignoreCleanClose: true });
|
|
138
163
|
conn.reconnect();
|
|
139
|
-
await
|
|
164
|
+
await connected;
|
|
165
|
+
this.emitAcceptedOpen();
|
|
140
166
|
}
|
|
141
167
|
|
|
142
168
|
getCurrentRoom(): string {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { createClientPointerContext } from "./pointerContext";
|
|
3
|
+
|
|
4
|
+
describe("createClientPointerContext", () => {
|
|
5
|
+
test("returns null before any pointer event", () => {
|
|
6
|
+
const pointer = createClientPointerContext();
|
|
7
|
+
|
|
8
|
+
expect(pointer.screen()).toBeNull();
|
|
9
|
+
expect(pointer.world()).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("stores screen and world coordinates from pointer events", () => {
|
|
13
|
+
const pointer = createClientPointerContext();
|
|
14
|
+
|
|
15
|
+
const world = pointer.updateFromEvent({
|
|
16
|
+
global: { x: 120, y: 80 },
|
|
17
|
+
currentTarget: {
|
|
18
|
+
toLocal(point: { x: number; y: number }) {
|
|
19
|
+
return { x: point.x + 10, y: point.y + 20 };
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(world).toEqual({ x: 130, y: 100 });
|
|
25
|
+
expect(pointer.screen()).toEqual({ x: 120, y: 80 });
|
|
26
|
+
expect(pointer.world()).toEqual({ x: 130, y: 100 });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("ignores pointer events without usable coordinates", () => {
|
|
30
|
+
const pointer = createClientPointerContext();
|
|
31
|
+
|
|
32
|
+
expect(pointer.updateFromEvent({ global: { x: Number.NaN, y: 80 } })).toBeNull();
|
|
33
|
+
expect(pointer.screen()).toBeNull();
|
|
34
|
+
expect(pointer.world()).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export interface ClientPointerPosition {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ClientPointerContext {
|
|
7
|
+
screen(): ClientPointerPosition | null;
|
|
8
|
+
world(): ClientPointerPosition | null;
|
|
9
|
+
update(screen: ClientPointerPosition | null, world?: ClientPointerPosition | null): ClientPointerPosition | null;
|
|
10
|
+
updateFromEvent(event: any): ClientPointerPosition | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toPosition(point: any): ClientPointerPosition | null {
|
|
14
|
+
const x = Number(point?.x);
|
|
15
|
+
const y = Number(point?.y);
|
|
16
|
+
|
|
17
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return { x, y };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractGlobalPoint(event: any): ClientPointerPosition | null {
|
|
25
|
+
return toPosition(event?.global)
|
|
26
|
+
?? toPosition(event?.data?.global)
|
|
27
|
+
?? toPosition(event);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractWorldPoint(event: any, global: ClientPointerPosition | null): ClientPointerPosition | null {
|
|
31
|
+
const target = event?.currentTarget ?? event?.target;
|
|
32
|
+
|
|
33
|
+
if (target && global && typeof target.toLocal === "function") {
|
|
34
|
+
return toPosition(target.toLocal(global));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (typeof event?.getLocalPosition === "function") {
|
|
38
|
+
return toPosition(event.getLocalPosition(target));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (typeof event?.data?.getLocalPosition === "function") {
|
|
42
|
+
return toPosition(event.data.getLocalPosition(target));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return global;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createClientPointerContext(): ClientPointerContext {
|
|
49
|
+
let lastScreen: ClientPointerPosition | null = null;
|
|
50
|
+
let lastWorld: ClientPointerPosition | null = null;
|
|
51
|
+
|
|
52
|
+
const update = (screen: ClientPointerPosition | null, world?: ClientPointerPosition | null) => {
|
|
53
|
+
const nextScreen = toPosition(screen);
|
|
54
|
+
const nextWorld = toPosition(world) ?? nextScreen;
|
|
55
|
+
|
|
56
|
+
if (nextScreen) {
|
|
57
|
+
lastScreen = nextScreen;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (nextWorld) {
|
|
61
|
+
lastWorld = nextWorld;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return lastWorld ? { ...lastWorld } : null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
screen() {
|
|
69
|
+
return lastScreen ? { ...lastScreen } : null;
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
world() {
|
|
73
|
+
return lastWorld ? { ...lastWorld } : null;
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
update,
|
|
77
|
+
|
|
78
|
+
updateFromEvent(event: any) {
|
|
79
|
+
const global = extractGlobalPoint(event);
|
|
80
|
+
const world = extractWorldPoint(event, global);
|
|
81
|
+
return update(global, world);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { normalizeStandaloneMessage } from "./standalone-message";
|
|
3
|
+
|
|
4
|
+
describe("standalone websocket bridge", () => {
|
|
5
|
+
test("dispatches mock room object broadcasts to named listeners", () => {
|
|
6
|
+
const onSpawn = vi.fn();
|
|
7
|
+
const object = normalizeStandaloneMessage({
|
|
8
|
+
type: "projectile:spawnBatch",
|
|
9
|
+
value: {
|
|
10
|
+
projectiles: [{ id: "p1", type: "bolt" }],
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (object.type === "projectile:spawnBatch") {
|
|
15
|
+
onSpawn(object.value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
expect(onSpawn).toHaveBeenCalledWith({
|
|
19
|
+
projectiles: [{ id: "p1", type: "bolt" }],
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("still accepts browser-style string messages", () => {
|
|
24
|
+
expect(normalizeStandaloneMessage({
|
|
25
|
+
data: JSON.stringify({
|
|
26
|
+
type: "projectile:spawnBatch",
|
|
27
|
+
value: { projectiles: [] },
|
|
28
|
+
}),
|
|
29
|
+
})).toEqual({
|
|
30
|
+
type: "projectile:spawnBatch",
|
|
31
|
+
value: { projectiles: [] },
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -7,6 +7,7 @@ import { LoadMapToken } from "./loadMap";
|
|
|
7
7
|
import { RpgGui } from "../Gui/Gui";
|
|
8
8
|
import { provideKeyboardControls } from "./keyboardControls";
|
|
9
9
|
import { provideSaveClient } from "./save";
|
|
10
|
+
import { normalizeStandaloneMessage } from "./standalone-message";
|
|
10
11
|
|
|
11
12
|
type ServerIo = any;
|
|
12
13
|
type ClientIo = any;
|
|
@@ -18,7 +19,12 @@ interface StandaloneOptions {
|
|
|
18
19
|
class BridgeWebsocket extends AbstractWebsocket {
|
|
19
20
|
private room: ServerIo;
|
|
20
21
|
private socket: ClientIo;
|
|
21
|
-
private
|
|
22
|
+
private socketRoom?: ServerIo;
|
|
23
|
+
private listeners: Array<{
|
|
24
|
+
event: string;
|
|
25
|
+
callback: (data: any) => void;
|
|
26
|
+
handler: (event: any) => void;
|
|
27
|
+
}> = [];
|
|
22
28
|
private rooms = {
|
|
23
29
|
partyFn: async (roomId: string) => {
|
|
24
30
|
this.room = new ServerIo(roomId, this.rooms);
|
|
@@ -48,6 +54,7 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
private async _connection(listeners?: (data: any) => void) {
|
|
57
|
+
this.detachCurrentSocket();
|
|
51
58
|
this.serverInstance = this.context.get('server')
|
|
52
59
|
this.socket = new ClientIo(this.serverInstance, 'player-client-id');
|
|
53
60
|
const url = new URL('http://localhost')
|
|
@@ -58,30 +65,43 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
58
65
|
}
|
|
59
66
|
})
|
|
60
67
|
listeners?.(this.socket)
|
|
61
|
-
await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);
|
|
62
68
|
this.room.clients.set(this.socket.id, this.socket);
|
|
63
|
-
this.
|
|
64
|
-
this.
|
|
69
|
+
this.socketRoom = this.room;
|
|
70
|
+
this.listeners.forEach(({ handler }) => {
|
|
71
|
+
this.socket.addEventListener("message", handler);
|
|
72
|
+
});
|
|
73
|
+
await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);
|
|
65
74
|
return this.socket
|
|
66
75
|
}
|
|
67
76
|
|
|
68
77
|
on(key: string, callback: (data: any) => void) {
|
|
78
|
+
if (
|
|
79
|
+
this.listeners.some(
|
|
80
|
+
(listener) => listener.event === key && listener.callback === callback
|
|
81
|
+
)
|
|
82
|
+
) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
69
85
|
const handler = (event) => {
|
|
70
|
-
const object =
|
|
86
|
+
const object = normalizeStandaloneMessage(event);
|
|
71
87
|
if (object.type === key) {
|
|
72
88
|
callback(object.value);
|
|
73
89
|
}
|
|
74
90
|
};
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
this.socket.addEventListener("message", handler);
|
|
91
|
+
this.listeners.push({ event: key, callback, handler });
|
|
92
|
+
this.socket?.addEventListener("message", handler);
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
off(event: string, callback: (data: any) => void) {
|
|
83
|
-
|
|
84
|
-
this.
|
|
96
|
+
const remaining: typeof this.listeners = [];
|
|
97
|
+
for (const listener of this.listeners) {
|
|
98
|
+
if (listener.event === event && listener.callback === callback) {
|
|
99
|
+
this.socket?.removeEventListener("message", listener.handler);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
remaining.push(listener);
|
|
103
|
+
}
|
|
104
|
+
this.listeners = remaining;
|
|
85
105
|
}
|
|
86
106
|
|
|
87
107
|
emit(event: string, data: any) {
|
|
@@ -134,6 +154,16 @@ class BridgeWebsocket extends AbstractWebsocket {
|
|
|
134
154
|
})
|
|
135
155
|
}
|
|
136
156
|
|
|
157
|
+
private detachCurrentSocket() {
|
|
158
|
+
if (!this.socket) return;
|
|
159
|
+
this.listeners.forEach(({ handler }) => {
|
|
160
|
+
this.socket.removeEventListener("message", handler);
|
|
161
|
+
});
|
|
162
|
+
this.socketRoom?.clients?.delete?.(this.socket.id);
|
|
163
|
+
this.socket = undefined as any;
|
|
164
|
+
this.socketRoom = undefined;
|
|
165
|
+
}
|
|
166
|
+
|
|
137
167
|
getServer() {
|
|
138
168
|
return this.serverInstance
|
|
139
169
|
}
|