@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
|
@@ -35,6 +35,117 @@ describe("ProjectileManager", () => {
|
|
|
35
35
|
expect(onSpawn).toHaveBeenCalledWith(expect.objectContaining({ id: "p1", type: "fireball" }));
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
+
test("ignores projectile packets from another map", () => {
|
|
39
|
+
const hooks = new Hooks([], "client");
|
|
40
|
+
const manager = new ProjectileManager(hooks);
|
|
41
|
+
const component = () => null;
|
|
42
|
+
|
|
43
|
+
manager.register("fireball", component);
|
|
44
|
+
manager.setMapId("map-town");
|
|
45
|
+
manager.spawnBatch([
|
|
46
|
+
{
|
|
47
|
+
id: "old-map-projectile",
|
|
48
|
+
type: "fireball",
|
|
49
|
+
origin: { x: 10, y: 20 },
|
|
50
|
+
direction: { x: 1, y: 0 },
|
|
51
|
+
speed: 100,
|
|
52
|
+
range: 500,
|
|
53
|
+
ttl: 5,
|
|
54
|
+
spawnTick: 1,
|
|
55
|
+
},
|
|
56
|
+
], { mapId: "map-dungeon" });
|
|
57
|
+
|
|
58
|
+
expect(manager.current()).toHaveLength(0);
|
|
59
|
+
|
|
60
|
+
manager.spawnBatch([
|
|
61
|
+
{
|
|
62
|
+
id: "current-map-projectile",
|
|
63
|
+
type: "fireball",
|
|
64
|
+
origin: { x: 10, y: 20 },
|
|
65
|
+
direction: { x: 1, y: 0 },
|
|
66
|
+
speed: 100,
|
|
67
|
+
range: 500,
|
|
68
|
+
ttl: 5,
|
|
69
|
+
spawnTick: 1,
|
|
70
|
+
},
|
|
71
|
+
], { mapId: "map-town" });
|
|
72
|
+
|
|
73
|
+
expect(manager.current()).toHaveLength(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("accepts server map ids without the client room prefix", () => {
|
|
77
|
+
const hooks = new Hooks([], "client");
|
|
78
|
+
const manager = new ProjectileManager(hooks);
|
|
79
|
+
const component = () => null;
|
|
80
|
+
|
|
81
|
+
manager.register("fireball", component);
|
|
82
|
+
manager.setMapId("map-town");
|
|
83
|
+
manager.spawnBatch([
|
|
84
|
+
{
|
|
85
|
+
id: "server-map-projectile",
|
|
86
|
+
type: "fireball",
|
|
87
|
+
origin: { x: 10, y: 20 },
|
|
88
|
+
direction: { x: 1, y: 0 },
|
|
89
|
+
speed: 100,
|
|
90
|
+
range: 500,
|
|
91
|
+
ttl: 5,
|
|
92
|
+
spawnTick: 1,
|
|
93
|
+
},
|
|
94
|
+
], { mapId: "town" });
|
|
95
|
+
|
|
96
|
+
expect(manager.getMapId()).toBe("town");
|
|
97
|
+
expect(manager.current()).toHaveLength(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("accepts prefixed map ids when the manager stores the logical map id", () => {
|
|
101
|
+
const hooks = new Hooks([], "client");
|
|
102
|
+
const manager = new ProjectileManager(hooks);
|
|
103
|
+
const component = () => null;
|
|
104
|
+
|
|
105
|
+
manager.register("fireball", component);
|
|
106
|
+
manager.setMapId("town");
|
|
107
|
+
manager.spawnBatch([
|
|
108
|
+
{
|
|
109
|
+
id: "prefixed-map-projectile",
|
|
110
|
+
type: "fireball",
|
|
111
|
+
origin: { x: 10, y: 20 },
|
|
112
|
+
direction: { x: 1, y: 0 },
|
|
113
|
+
speed: 100,
|
|
114
|
+
range: 500,
|
|
115
|
+
ttl: 5,
|
|
116
|
+
spawnTick: 1,
|
|
117
|
+
},
|
|
118
|
+
], { mapId: "map-town" });
|
|
119
|
+
|
|
120
|
+
expect(manager.current()).toHaveLength(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("clears projectiles when switching map ids", () => {
|
|
124
|
+
const hooks = new Hooks([], "client");
|
|
125
|
+
const manager = new ProjectileManager(hooks);
|
|
126
|
+
|
|
127
|
+
manager.register("fireball", () => null);
|
|
128
|
+
manager.setMapId("map-town");
|
|
129
|
+
manager.spawnBatch([
|
|
130
|
+
{
|
|
131
|
+
id: "p1",
|
|
132
|
+
type: "fireball",
|
|
133
|
+
origin: { x: 10, y: 20 },
|
|
134
|
+
direction: { x: 1, y: 0 },
|
|
135
|
+
speed: 100,
|
|
136
|
+
range: 500,
|
|
137
|
+
ttl: 5,
|
|
138
|
+
spawnTick: 1,
|
|
139
|
+
},
|
|
140
|
+
], { mapId: "map-town" });
|
|
141
|
+
|
|
142
|
+
expect(manager.current()).toHaveLength(1);
|
|
143
|
+
|
|
144
|
+
manager.setMapId("map-dungeon");
|
|
145
|
+
|
|
146
|
+
expect(manager.current()).toHaveLength(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
38
149
|
test("starts visuals at the spawn origin even when a server tick estimate exists", () => {
|
|
39
150
|
vi.useFakeTimers();
|
|
40
151
|
vi.setSystemTime(2000);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { computed, signal } from "canvasengine";
|
|
2
2
|
import { Hooks } from "@rpgjs/common";
|
|
3
|
+
import { normalizeRoomMapId } from "../utils/mapId";
|
|
3
4
|
|
|
4
5
|
export interface ClientProjectileSpawn {
|
|
5
6
|
id: string;
|
|
@@ -65,6 +66,7 @@ export interface ProjectileSpawnClock {
|
|
|
65
66
|
now?: number;
|
|
66
67
|
currentServerTick?: number;
|
|
67
68
|
tickDurationMs?: number;
|
|
69
|
+
mapId?: string;
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
interface RuntimeProjectile {
|
|
@@ -84,6 +86,7 @@ export class ProjectileManager {
|
|
|
84
86
|
private readonly projectiles = new Map<string, RuntimeProjectile>();
|
|
85
87
|
private readonly version = signal(0);
|
|
86
88
|
private readonly impactDurationMs = 350;
|
|
89
|
+
private mapId?: string;
|
|
87
90
|
|
|
88
91
|
constructor(
|
|
89
92
|
private readonly hooks: Hooks,
|
|
@@ -118,7 +121,19 @@ export class ProjectileManager {
|
|
|
118
121
|
return this.components.get(type);
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
setMapId(mapId: string | undefined): void {
|
|
125
|
+
const normalizedMapId = normalizeRoomMapId(mapId);
|
|
126
|
+
if (this.mapId === normalizedMapId) return;
|
|
127
|
+
this.mapId = normalizedMapId;
|
|
128
|
+
this.clear();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getMapId(): string | undefined {
|
|
132
|
+
return this.mapId;
|
|
133
|
+
}
|
|
134
|
+
|
|
121
135
|
spawnBatch(projectiles: ClientProjectileSpawn[], clock: ProjectileSpawnClock = {}): void {
|
|
136
|
+
if (!this.acceptsMap(clock.mapId)) return;
|
|
122
137
|
const now = clock.now ?? Date.now();
|
|
123
138
|
for (const projectile of projectiles) {
|
|
124
139
|
const component = this.components.get(projectile.type);
|
|
@@ -142,7 +157,8 @@ export class ProjectileManager {
|
|
|
142
157
|
this.touch();
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
impactBatch(impacts: ClientProjectileImpact[]): void {
|
|
160
|
+
impactBatch(impacts: ClientProjectileImpact[], context: { mapId?: string } = {}): void {
|
|
161
|
+
if (!this.acceptsMap(context.mapId)) return;
|
|
146
162
|
const now = Date.now();
|
|
147
163
|
for (const impact of impacts) {
|
|
148
164
|
const projectile = this.projectiles.get(impact.id);
|
|
@@ -155,7 +171,8 @@ export class ProjectileManager {
|
|
|
155
171
|
this.touch();
|
|
156
172
|
}
|
|
157
173
|
|
|
158
|
-
destroyBatch(projectiles: ClientProjectileDestroy[]): void {
|
|
174
|
+
destroyBatch(projectiles: ClientProjectileDestroy[], context: { mapId?: string } = {}): void {
|
|
175
|
+
if (!this.acceptsMap(context.mapId)) return;
|
|
159
176
|
const now = Date.now();
|
|
160
177
|
for (const destroyed of projectiles) {
|
|
161
178
|
const projectile = this.projectiles.get(destroyed.id);
|
|
@@ -241,6 +258,11 @@ export class ProjectileManager {
|
|
|
241
258
|
};
|
|
242
259
|
}
|
|
243
260
|
|
|
261
|
+
private acceptsMap(mapId: string | undefined): boolean {
|
|
262
|
+
const normalizedMapId = normalizeRoomMapId(mapId);
|
|
263
|
+
return !normalizedMapId || !this.mapId || normalizedMapId === this.mapId;
|
|
264
|
+
}
|
|
265
|
+
|
|
244
266
|
private isWaitingForDelay(projectile: RuntimeProjectile, now: number): boolean {
|
|
245
267
|
const delayMs = (projectile.spawn.delay ?? 0) * 1000;
|
|
246
268
|
return now - projectile.createdAt - delayMs < 0;
|
package/src/Gui/Gui.spec.ts
CHANGED
|
@@ -64,6 +64,73 @@ const VueTooltip = {
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
describe("RpgGui Vue integration", () => {
|
|
67
|
+
test("tracks GUI open ids and sends them back when closing", async () => {
|
|
68
|
+
const { gui, socket } = await createGui();
|
|
69
|
+
await gui._initialize();
|
|
70
|
+
const openHandler = socket.on.mock.calls.find(([event]) => event === "gui.open")?.[1];
|
|
71
|
+
|
|
72
|
+
openHandler({
|
|
73
|
+
guiId: PrebuiltGui.Dialog,
|
|
74
|
+
guiOpenId: "open-1",
|
|
75
|
+
data: { message: "Hello" },
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(gui.get(PrebuiltGui.Dialog)?.openId).toBe("open-1");
|
|
79
|
+
|
|
80
|
+
gui.guiClose(PrebuiltGui.Dialog, 0, gui.get(PrebuiltGui.Dialog)?.openId);
|
|
81
|
+
|
|
82
|
+
expect(socket.emit).toHaveBeenCalledWith("gui.exit", {
|
|
83
|
+
guiId: PrebuiltGui.Dialog,
|
|
84
|
+
guiOpenId: "open-1",
|
|
85
|
+
data: 0,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("does not emit malformed GUI open ids", async () => {
|
|
90
|
+
const { gui, socket } = await createGui();
|
|
91
|
+
|
|
92
|
+
gui.guiClose(PrebuiltGui.Dialog, 0, (() => "open-1") as any);
|
|
93
|
+
|
|
94
|
+
expect(socket.emit).toHaveBeenCalledWith("gui.exit", {
|
|
95
|
+
guiId: PrebuiltGui.Dialog,
|
|
96
|
+
guiOpenId: undefined,
|
|
97
|
+
data: 0,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("ignores stale server close events for previous GUI opens", async () => {
|
|
102
|
+
const { gui, socket } = await createGui();
|
|
103
|
+
await gui._initialize();
|
|
104
|
+
const openHandler = socket.on.mock.calls.find(([event]) => event === "gui.open")?.[1];
|
|
105
|
+
const exitHandler = socket.on.mock.calls.find(([event]) => event === "gui.exit")?.[1];
|
|
106
|
+
|
|
107
|
+
openHandler({
|
|
108
|
+
guiId: PrebuiltGui.Dialog,
|
|
109
|
+
guiOpenId: "open-1",
|
|
110
|
+
data: { message: "First" },
|
|
111
|
+
});
|
|
112
|
+
openHandler({
|
|
113
|
+
guiId: PrebuiltGui.Dialog,
|
|
114
|
+
guiOpenId: "open-2",
|
|
115
|
+
data: { message: "Second" },
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
exitHandler({
|
|
119
|
+
guiId: PrebuiltGui.Dialog,
|
|
120
|
+
guiOpenId: "open-1",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(gui.isDisplaying(PrebuiltGui.Dialog)).toBe(true);
|
|
124
|
+
expect(gui.get(PrebuiltGui.Dialog)?.data()).toEqual({ message: "Second" });
|
|
125
|
+
|
|
126
|
+
exitHandler({
|
|
127
|
+
guiId: PrebuiltGui.Dialog,
|
|
128
|
+
guiOpenId: "open-2",
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(gui.isDisplaying(PrebuiltGui.Dialog)).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
67
134
|
test("separates CanvasEngine and Vue GUI registries", async () => {
|
|
68
135
|
const { gui } = await createGui();
|
|
69
136
|
|
package/src/Gui/Gui.ts
CHANGED
|
@@ -39,6 +39,7 @@ export interface GuiInstance {
|
|
|
39
39
|
component: any;
|
|
40
40
|
display: WritableSignal<boolean>;
|
|
41
41
|
data: WritableSignal<any>;
|
|
42
|
+
openId?: string;
|
|
42
43
|
autoDisplay: boolean;
|
|
43
44
|
dependencies?: Signal[];
|
|
44
45
|
subscription?: Subscription;
|
|
@@ -50,6 +51,7 @@ type GuiState = {
|
|
|
50
51
|
component: any;
|
|
51
52
|
display: boolean;
|
|
52
53
|
data: any;
|
|
54
|
+
openId?: string;
|
|
53
55
|
attachToSprite: boolean;
|
|
54
56
|
};
|
|
55
57
|
|
|
@@ -179,12 +181,18 @@ export class RpgGui {
|
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
async _initialize() {
|
|
182
|
-
this.webSocket.on("gui.open", (data: { guiId: string; data: any }) => {
|
|
184
|
+
this.webSocket.on("gui.open", (data: { guiId: string; data: any; guiOpenId?: string }) => {
|
|
183
185
|
this.clearPendingActions(data.guiId);
|
|
184
|
-
this.display(data.guiId, data.data);
|
|
186
|
+
this.display(data.guiId, data.data, [], data.guiOpenId);
|
|
185
187
|
});
|
|
186
188
|
|
|
187
|
-
this.webSocket.on("gui.exit", (guiId: string) => {
|
|
189
|
+
this.webSocket.on("gui.exit", (payload: string | { guiId: string; guiOpenId?: string }) => {
|
|
190
|
+
const guiId = typeof payload === "string" ? payload : payload.guiId;
|
|
191
|
+
const guiOpenId = typeof payload === "string" ? undefined : payload.guiOpenId;
|
|
192
|
+
const current = this.get(guiId);
|
|
193
|
+
if (guiOpenId && current?.openId && current.openId !== guiOpenId) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
188
196
|
this.hide(guiId);
|
|
189
197
|
});
|
|
190
198
|
|
|
@@ -256,9 +264,12 @@ export class RpgGui {
|
|
|
256
264
|
});
|
|
257
265
|
}
|
|
258
266
|
|
|
259
|
-
guiClose(guiId: string, data?: any) {
|
|
267
|
+
guiClose(guiId: string, data?: any, guiOpenId?: unknown) {
|
|
268
|
+
const normalizedOpenId =
|
|
269
|
+
typeof guiOpenId === "string" && guiOpenId.length > 0 ? guiOpenId : undefined;
|
|
260
270
|
this.webSocket.emit("gui.exit", {
|
|
261
271
|
guiId,
|
|
272
|
+
guiOpenId: normalizedOpenId,
|
|
262
273
|
data,
|
|
263
274
|
});
|
|
264
275
|
}
|
|
@@ -308,6 +319,7 @@ export class RpgGui {
|
|
|
308
319
|
component,
|
|
309
320
|
display: signal<boolean>(gui.display || false),
|
|
310
321
|
data: signal<any>(gui.data || {}),
|
|
322
|
+
openId: undefined,
|
|
311
323
|
autoDisplay: gui.autoDisplay || false,
|
|
312
324
|
dependencies: gui.dependencies ? gui.dependencies() : [],
|
|
313
325
|
attachToSprite,
|
|
@@ -431,7 +443,7 @@ export class RpgGui {
|
|
|
431
443
|
* gui.display('shop', { shopId: 1 }, [playerSignal, shopSignal]);
|
|
432
444
|
* ```
|
|
433
445
|
*/
|
|
434
|
-
display(id: string, data = {}, dependencies: Signal[] = []) {
|
|
446
|
+
display(id: string, data = {}, dependencies: Signal[] = [], openId?: string) {
|
|
435
447
|
if (!this.exists(id)) {
|
|
436
448
|
throw throwError(id);
|
|
437
449
|
}
|
|
@@ -443,8 +455,9 @@ export class RpgGui {
|
|
|
443
455
|
|
|
444
456
|
if (isVueComponent) {
|
|
445
457
|
// Handle Vue component display
|
|
446
|
-
this._handleVueComponentDisplay(id, data, dependencies, guiInstance);
|
|
458
|
+
this._handleVueComponentDisplay(id, data, dependencies, guiInstance, openId);
|
|
447
459
|
} else {
|
|
460
|
+
guiInstance.openId = openId;
|
|
448
461
|
guiInstance.data.set(data);
|
|
449
462
|
guiInstance.display.set(true);
|
|
450
463
|
}
|
|
@@ -464,7 +477,7 @@ export class RpgGui {
|
|
|
464
477
|
* @param dependencies - Runtime dependencies
|
|
465
478
|
* @param guiInstance - GUI instance
|
|
466
479
|
*/
|
|
467
|
-
private _handleVueComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance) {
|
|
480
|
+
private _handleVueComponentDisplay(id: string, data: any, dependencies: Signal[], guiInstance: GuiInstance, openId?: string) {
|
|
468
481
|
// Unsubscribe from previous subscription if exists
|
|
469
482
|
if (guiInstance.subscription) {
|
|
470
483
|
guiInstance.subscription.unsubscribe();
|
|
@@ -482,6 +495,7 @@ export class RpgGui {
|
|
|
482
495
|
deps.map(dependency => dependency.observable)
|
|
483
496
|
).subscribe((values) => {
|
|
484
497
|
if (values.every(value => value !== undefined)) {
|
|
498
|
+
guiInstance.openId = openId;
|
|
485
499
|
guiInstance.data.set(data);
|
|
486
500
|
guiInstance.display.set(true);
|
|
487
501
|
this._notifyVueGui(id, true, data);
|
|
@@ -491,6 +505,7 @@ export class RpgGui {
|
|
|
491
505
|
}
|
|
492
506
|
|
|
493
507
|
// No dependencies, display immediately
|
|
508
|
+
guiInstance.openId = openId;
|
|
494
509
|
guiInstance.data.set(data);
|
|
495
510
|
guiInstance.display.set(true);
|
|
496
511
|
this._notifyVueGui(id, true, data);
|
|
@@ -523,6 +538,7 @@ export class RpgGui {
|
|
|
523
538
|
}
|
|
524
539
|
|
|
525
540
|
guiInstance.display.set(false)
|
|
541
|
+
guiInstance.openId = undefined;
|
|
526
542
|
|
|
527
543
|
// Check if it's a Vue component and notify VueGui
|
|
528
544
|
const isVueComponent = this.extraGuis.some(gui => gui.name === id);
|
|
@@ -573,6 +589,7 @@ export class RpgGui {
|
|
|
573
589
|
component: gui.component,
|
|
574
590
|
display,
|
|
575
591
|
data,
|
|
592
|
+
openId: gui.openId,
|
|
576
593
|
attachToSprite: gui.attachToSprite || false,
|
|
577
594
|
};
|
|
578
595
|
}
|
package/src/RpgClient.ts
CHANGED
|
@@ -2,11 +2,17 @@ import { ComponentFunction, Signal } from 'canvasengine'
|
|
|
2
2
|
import { RpgClientEngine } from './RpgClientEngine'
|
|
3
3
|
import { Loader, Container } from 'pixi.js'
|
|
4
4
|
import { RpgClientObject } from './Game/Object'
|
|
5
|
-
import
|
|
5
|
+
import type { RpgClientEvent } from './Game/Event'
|
|
6
|
+
import { type I18nMessages, type MapPhysicsEntityContext, type MapPhysicsInitContext, type RpgActionName } from '@rpgjs/common'
|
|
6
7
|
import type {
|
|
7
8
|
ClientProjectileSpawn,
|
|
8
9
|
RenderedProjectileProps,
|
|
9
10
|
} from './Game/ProjectileManager'
|
|
11
|
+
import type { ClientVisualMap } from './Game/ClientVisuals'
|
|
12
|
+
import type {
|
|
13
|
+
RpgInteractionBehavior,
|
|
14
|
+
RpgInteractionMatcher,
|
|
15
|
+
} from './services/interactions'
|
|
10
16
|
|
|
11
17
|
type RpgClass<T = any> = new (...args: any[]) => T
|
|
12
18
|
type RpgComponent = RpgClientObject
|
|
@@ -18,6 +24,16 @@ export type SpriteComponentConfig = ComponentFunction | {
|
|
|
18
24
|
dependencies?: (object: RpgClientObject) => any[]
|
|
19
25
|
}
|
|
20
26
|
|
|
27
|
+
export type EventComponentSprite = RpgClientEvent & Record<string, any>
|
|
28
|
+
|
|
29
|
+
export type EventComponentConfig = ComponentFunction | {
|
|
30
|
+
component: ComponentFunction
|
|
31
|
+
props?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>)
|
|
32
|
+
data?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>)
|
|
33
|
+
dependencies?: (event: EventComponentSprite) => any[]
|
|
34
|
+
renderGraphic?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
export interface RpgSpriteBeforeRemoveContext {
|
|
22
38
|
reason?: string
|
|
23
39
|
data?: any
|
|
@@ -142,6 +158,31 @@ export interface RpgSpriteHooks {
|
|
|
142
158
|
* ```
|
|
143
159
|
*/
|
|
144
160
|
components?: Record<string, ComponentFunction>
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a custom CanvasEngine component for a specific event.
|
|
164
|
+
*
|
|
165
|
+
* The component always receives the synced event object as the `sprite` prop.
|
|
166
|
+
* Custom props are merged in addition to `sprite`, but cannot replace it.
|
|
167
|
+
* Return `null` or `undefined` to keep the default graphic renderer.
|
|
168
|
+
*
|
|
169
|
+
* @prop { (event: EventComponentSprite) => EventComponentConfig | null | undefined } [eventComponent]
|
|
170
|
+
* @memberof RpgSpriteHooks
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* import ChestEvent from './components/chest-event.ce'
|
|
174
|
+
*
|
|
175
|
+
* const sprite: RpgSpriteHooks = {
|
|
176
|
+
* eventComponent(sprite) {
|
|
177
|
+
* if (sprite.name === 'CHEST') {
|
|
178
|
+
* return ChestEvent
|
|
179
|
+
* }
|
|
180
|
+
* return null
|
|
181
|
+
* }
|
|
182
|
+
* }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
eventComponent?: (event: EventComponentSprite) => EventComponentConfig | null | undefined
|
|
145
186
|
|
|
146
187
|
/**
|
|
147
188
|
* As soon as the sprite is initialized
|
|
@@ -326,6 +367,14 @@ export interface RpgProjectileHooks {
|
|
|
326
367
|
}
|
|
327
368
|
|
|
328
369
|
export interface RpgClient {
|
|
370
|
+
/**
|
|
371
|
+
* Default translations owned by this client module.
|
|
372
|
+
*
|
|
373
|
+
* Game-level translations provided with `provideI18n()` override module
|
|
374
|
+
* translations when they share the same locale and key.
|
|
375
|
+
*/
|
|
376
|
+
i18n?: I18nMessages
|
|
377
|
+
|
|
329
378
|
/**
|
|
330
379
|
* Add hooks to the player or engine. All modules can listen to the hook
|
|
331
380
|
*
|
|
@@ -721,6 +770,37 @@ export interface RpgClient {
|
|
|
721
770
|
component: ComponentFunction
|
|
722
771
|
}[]
|
|
723
772
|
|
|
773
|
+
/**
|
|
774
|
+
* Named client-side visual macros.
|
|
775
|
+
*
|
|
776
|
+
* Use client visuals when the server needs to trigger a group of existing
|
|
777
|
+
* client visual primitives at once, such as a flash, damage text, sound,
|
|
778
|
+
* component animation, and camera shake. The server sends only the visual
|
|
779
|
+
* name and a serializable payload; the rendering details live on the client.
|
|
780
|
+
*
|
|
781
|
+
* For a single sound, flash, or component animation, prefer the direct
|
|
782
|
+
* server APIs (`playSound`, `flash`, `showComponentAnimation`). Client
|
|
783
|
+
* visuals are meant to group several visual operations and reduce bandwidth.
|
|
784
|
+
*
|
|
785
|
+
* ```ts
|
|
786
|
+
* import { defineModule, RpgClient } from '@rpgjs/client'
|
|
787
|
+
*
|
|
788
|
+
* export default defineModule<RpgClient>({
|
|
789
|
+
* clientVisuals: {
|
|
790
|
+
* hit({ target, data }, helpers) {
|
|
791
|
+
* helpers.flash(target, { type: 'tint', tint: 'red' })
|
|
792
|
+
* helpers.showHit(target, `-${data.damage}`)
|
|
793
|
+
* helpers.sound('hit')
|
|
794
|
+
* }
|
|
795
|
+
* }
|
|
796
|
+
* })
|
|
797
|
+
* ```
|
|
798
|
+
*
|
|
799
|
+
* @prop {Record<string, ClientVisualHandler>} [clientVisuals]
|
|
800
|
+
* @memberof RpgClient
|
|
801
|
+
*/
|
|
802
|
+
clientVisuals?: ClientVisualMap
|
|
803
|
+
|
|
724
804
|
/**
|
|
725
805
|
* Client-side projectile rendering configuration.
|
|
726
806
|
*
|
|
@@ -728,4 +808,19 @@ export interface RpgClient {
|
|
|
728
808
|
* compact spawn/impact/destroy events and the client predicts x/y locally.
|
|
729
809
|
*/
|
|
730
810
|
projectiles?: RpgProjectileHooks
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* Client-only pointer interactions attached to sprites.
|
|
814
|
+
*
|
|
815
|
+
* Use this for hover popovers, selection, drag previews, cursor changes, and
|
|
816
|
+
* explicit mouse-driven gameplay actions. Pointer feedback stays local unless
|
|
817
|
+
* the behavior calls `ctx.action(...)`.
|
|
818
|
+
*/
|
|
819
|
+
interactions?:
|
|
820
|
+
| ((engine: RpgClientEngine) => void)
|
|
821
|
+
| {
|
|
822
|
+
setup?: (engine: RpgClientEngine) => void
|
|
823
|
+
load?: (engine: RpgClientEngine) => void
|
|
824
|
+
use?: Array<[RpgInteractionMatcher, RpgInteractionBehavior | ComponentFunction]>
|
|
825
|
+
}
|
|
731
826
|
}
|