@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,196 @@
|
|
|
1
|
+
import { normalizeRoomMapId } from "../utils/mapId.js";
|
|
2
|
+
import { computed, signal } from "canvasengine";
|
|
3
|
+
//#region src/Game/ProjectileManager.ts
|
|
4
|
+
var ProjectileManager = class {
|
|
5
|
+
constructor(hooks, predictionResolver) {
|
|
6
|
+
this.hooks = hooks;
|
|
7
|
+
this.predictionResolver = predictionResolver;
|
|
8
|
+
this.components = /* @__PURE__ */ new Map();
|
|
9
|
+
this.projectiles = /* @__PURE__ */ new Map();
|
|
10
|
+
this.version = signal(0);
|
|
11
|
+
this.impactDurationMs = 350;
|
|
12
|
+
this.current = computed(() => {
|
|
13
|
+
this.version();
|
|
14
|
+
const now = Date.now();
|
|
15
|
+
const rendered = [];
|
|
16
|
+
for (const projectile of this.projectiles.values()) {
|
|
17
|
+
const props = this.toProps(projectile, now);
|
|
18
|
+
if (!props) continue;
|
|
19
|
+
rendered.push({
|
|
20
|
+
id: projectile.spawn.id,
|
|
21
|
+
type: projectile.spawn.type,
|
|
22
|
+
component: projectile.component,
|
|
23
|
+
props
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return rendered;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
register(type, component) {
|
|
30
|
+
this.components.set(type, component);
|
|
31
|
+
return component;
|
|
32
|
+
}
|
|
33
|
+
get(type) {
|
|
34
|
+
return this.components.get(type);
|
|
35
|
+
}
|
|
36
|
+
setMapId(mapId) {
|
|
37
|
+
const normalizedMapId = normalizeRoomMapId(mapId);
|
|
38
|
+
if (this.mapId === normalizedMapId) return;
|
|
39
|
+
this.mapId = normalizedMapId;
|
|
40
|
+
this.clear();
|
|
41
|
+
}
|
|
42
|
+
getMapId() {
|
|
43
|
+
return this.mapId;
|
|
44
|
+
}
|
|
45
|
+
spawnBatch(projectiles, clock = {}) {
|
|
46
|
+
if (!this.acceptsMap(clock.mapId)) return;
|
|
47
|
+
const now = clock.now ?? Date.now();
|
|
48
|
+
for (const projectile of projectiles) {
|
|
49
|
+
const component = this.components.get(projectile.type);
|
|
50
|
+
if (!component) continue;
|
|
51
|
+
const runtime = {
|
|
52
|
+
spawn: {
|
|
53
|
+
...projectile,
|
|
54
|
+
delay: projectile.delay ?? 0,
|
|
55
|
+
index: projectile.index ?? 0,
|
|
56
|
+
count: projectile.count ?? 1
|
|
57
|
+
},
|
|
58
|
+
component,
|
|
59
|
+
createdAt: now
|
|
60
|
+
};
|
|
61
|
+
this.setPredictedImpact(runtime);
|
|
62
|
+
this.projectiles.set(projectile.id, runtime);
|
|
63
|
+
this.hooks.callHooks("client-projectiles-onSpawn", runtime.spawn).subscribe();
|
|
64
|
+
}
|
|
65
|
+
this.touch();
|
|
66
|
+
}
|
|
67
|
+
impactBatch(impacts, context = {}) {
|
|
68
|
+
if (!this.acceptsMap(context.mapId)) return;
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
for (const impact of impacts) {
|
|
71
|
+
const projectile = this.projectiles.get(impact.id);
|
|
72
|
+
if (!projectile) continue;
|
|
73
|
+
this.setImpact(projectile, impact, now);
|
|
74
|
+
this.hooks.callHooks("client-projectiles-onImpact", this.toProps(projectile, now)).subscribe();
|
|
75
|
+
}
|
|
76
|
+
this.touch();
|
|
77
|
+
}
|
|
78
|
+
destroyBatch(projectiles, context = {}) {
|
|
79
|
+
if (!this.acceptsMap(context.mapId)) return;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
for (const destroyed of projectiles) {
|
|
82
|
+
const projectile = this.projectiles.get(destroyed.id);
|
|
83
|
+
if (!projectile) continue;
|
|
84
|
+
if (destroyed.reason === "hit") {
|
|
85
|
+
const current = this.toProps(projectile, now);
|
|
86
|
+
this.setImpact(projectile, {
|
|
87
|
+
id: destroyed.id,
|
|
88
|
+
targetId: destroyed.targetId ?? projectile.impact?.targetId,
|
|
89
|
+
x: destroyed.x ?? projectile.impact?.x ?? current?.x ?? projectile.spawn.origin.x,
|
|
90
|
+
y: destroyed.y ?? projectile.impact?.y ?? current?.y ?? projectile.spawn.origin.y,
|
|
91
|
+
distance: destroyed.distance ?? projectile.impact?.distance ?? current?.distance
|
|
92
|
+
}, now);
|
|
93
|
+
}
|
|
94
|
+
projectile.destroyReason = destroyed.reason;
|
|
95
|
+
projectile.destroyAt = projectile.destroyAt ?? (projectile.impact && projectile.impactStartedAt !== void 0 ? projectile.impactStartedAt + this.impactDurationMs : now);
|
|
96
|
+
this.hooks.callHooks("client-projectiles-onDestroy", this.toProps(projectile, now)).subscribe();
|
|
97
|
+
}
|
|
98
|
+
this.touch();
|
|
99
|
+
}
|
|
100
|
+
clear() {
|
|
101
|
+
this.projectiles.clear();
|
|
102
|
+
this.touch();
|
|
103
|
+
}
|
|
104
|
+
step() {
|
|
105
|
+
const now = Date.now();
|
|
106
|
+
let changed = false;
|
|
107
|
+
for (const [id, projectile] of this.projectiles) if (!this.toProps(projectile, now) && !this.isWaitingForDelay(projectile, now) || projectile.destroyAt !== void 0 && now >= projectile.destroyAt) {
|
|
108
|
+
this.projectiles.delete(id);
|
|
109
|
+
changed = true;
|
|
110
|
+
}
|
|
111
|
+
this.touch(changed || this.projectiles.size > 0);
|
|
112
|
+
}
|
|
113
|
+
toProps(projectile, now) {
|
|
114
|
+
const spawn = projectile.spawn;
|
|
115
|
+
const delayMs = (spawn.delay ?? 0) * 1e3;
|
|
116
|
+
const elapsedMs = now - projectile.createdAt - delayMs;
|
|
117
|
+
if (elapsedMs < 0) return null;
|
|
118
|
+
const elapsed = elapsedMs / 1e3;
|
|
119
|
+
const ttl = Math.max(.001, spawn.ttl);
|
|
120
|
+
const rawDistance = Math.min(spawn.speed * elapsed, spawn.range);
|
|
121
|
+
const predictedImpact = this.getActivePredictedImpact(projectile, now, rawDistance);
|
|
122
|
+
const visualImpact = projectile.visualImpact ?? projectile.impact;
|
|
123
|
+
const distance = visualImpact?.distance ?? predictedImpact?.distance ?? rawDistance;
|
|
124
|
+
const progress = Math.min(1, distance / spawn.range);
|
|
125
|
+
const x = visualImpact?.x ?? predictedImpact?.x ?? spawn.origin.x + spawn.direction.x * distance;
|
|
126
|
+
const y = visualImpact?.y ?? predictedImpact?.y ?? spawn.origin.y + spawn.direction.y * distance;
|
|
127
|
+
const impactElapsedMs = projectile.impactStartedAt !== void 0 ? Math.max(0, now - projectile.impactStartedAt) : void 0;
|
|
128
|
+
return {
|
|
129
|
+
...spawn,
|
|
130
|
+
x,
|
|
131
|
+
y,
|
|
132
|
+
angle: Math.atan2(spawn.direction.y, spawn.direction.x),
|
|
133
|
+
distance,
|
|
134
|
+
elapsed,
|
|
135
|
+
progress,
|
|
136
|
+
impact: projectile.impact,
|
|
137
|
+
impactElapsed: impactElapsedMs === void 0 ? void 0 : impactElapsedMs / 1e3,
|
|
138
|
+
impactProgress: impactElapsedMs === void 0 ? void 0 : Math.min(1, impactElapsedMs / this.impactDurationMs),
|
|
139
|
+
destroyed: projectile.destroyAt !== void 0,
|
|
140
|
+
ttl
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
acceptsMap(mapId) {
|
|
144
|
+
const normalizedMapId = normalizeRoomMapId(mapId);
|
|
145
|
+
return !normalizedMapId || !this.mapId || normalizedMapId === this.mapId;
|
|
146
|
+
}
|
|
147
|
+
isWaitingForDelay(projectile, now) {
|
|
148
|
+
const delayMs = (projectile.spawn.delay ?? 0) * 1e3;
|
|
149
|
+
return now - projectile.createdAt - delayMs < 0;
|
|
150
|
+
}
|
|
151
|
+
setPredictedImpact(projectile) {
|
|
152
|
+
if (projectile.spawn.predictImpact === false) return;
|
|
153
|
+
const impact = this.predictionResolver?.(projectile.spawn);
|
|
154
|
+
if (!impact || !Number.isFinite(impact.x) || !Number.isFinite(impact.y)) return;
|
|
155
|
+
const distance = typeof impact.distance === "number" && Number.isFinite(impact.distance) ? impact.distance : Math.hypot(impact.x - projectile.spawn.origin.x, impact.y - projectile.spawn.origin.y);
|
|
156
|
+
if (!Number.isFinite(distance) || distance < 0 || distance > projectile.spawn.range) return;
|
|
157
|
+
projectile.predictedImpact = {
|
|
158
|
+
...impact,
|
|
159
|
+
distance
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
getActivePredictedImpact(projectile, now, rawDistance) {
|
|
163
|
+
if (!projectile.predictedImpact || projectile.impact) return;
|
|
164
|
+
const distance = projectile.predictedImpact.distance;
|
|
165
|
+
if (distance === void 0 || rawDistance < distance) return;
|
|
166
|
+
return projectile.predictedImpact;
|
|
167
|
+
}
|
|
168
|
+
setImpact(projectile, impact, now) {
|
|
169
|
+
projectile.visualImpact = this.resolveVisualImpact(projectile, impact, now);
|
|
170
|
+
projectile.impact = impact;
|
|
171
|
+
projectile.predictedImpact = void 0;
|
|
172
|
+
projectile.impactStartedAt = projectile.impactStartedAt ?? now;
|
|
173
|
+
const impactDestroyAt = projectile.impactStartedAt + this.impactDurationMs;
|
|
174
|
+
projectile.destroyAt = Math.max(projectile.destroyAt ?? 0, impactDestroyAt);
|
|
175
|
+
}
|
|
176
|
+
resolveVisualImpact(projectile, impact, now) {
|
|
177
|
+
const predicted = projectile.predictedImpact;
|
|
178
|
+
if (!predicted || !this.isSameTarget(predicted, impact)) return impact;
|
|
179
|
+
const distance = predicted.distance;
|
|
180
|
+
if (distance === void 0) return impact;
|
|
181
|
+
const delayMs = (projectile.spawn.delay ?? 0) * 1e3;
|
|
182
|
+
const elapsedMs = now - projectile.createdAt - delayMs;
|
|
183
|
+
if (elapsedMs < 0) return impact;
|
|
184
|
+
return Math.min(projectile.spawn.speed * (elapsedMs / 1e3), projectile.spawn.range) >= distance ? predicted : impact;
|
|
185
|
+
}
|
|
186
|
+
isSameTarget(a, b) {
|
|
187
|
+
return a.targetId !== void 0 && a.targetId === b.targetId;
|
|
188
|
+
}
|
|
189
|
+
touch(force = true) {
|
|
190
|
+
if (force) this.version.update((value) => value + 1);
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
//#endregion
|
|
194
|
+
export { ProjectileManager };
|
|
195
|
+
|
|
196
|
+
//# sourceMappingURL=ProjectileManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProjectileManager.js","names":[],"sources":["../../src/Game/ProjectileManager.ts"],"sourcesContent":["import { computed, signal } from \"canvasengine\";\nimport { Hooks } from \"@rpgjs/common\";\nimport { normalizeRoomMapId } from \"../utils/mapId\";\n\nexport interface ClientProjectileSpawn {\n id: string;\n type: string;\n ownerId?: string;\n origin: { x: number; y: number };\n direction: { x: number; y: number };\n speed: number;\n range: number;\n ttl: number;\n spawnTick: number;\n delay?: number;\n index?: number;\n count?: number;\n params?: Record<string, unknown>;\n collisionMask?: number;\n ignoreOwner?: boolean;\n predictImpact?: boolean;\n}\n\nexport interface ClientProjectileImpact {\n id: string;\n targetId?: string;\n x: number;\n y: number;\n distance?: number;\n}\n\nexport interface ClientProjectileDestroy {\n id: string;\n reason?: string;\n targetId?: string;\n x?: number;\n y?: number;\n distance?: number;\n}\n\nexport interface RenderedProjectileProps extends ClientProjectileSpawn {\n x: number;\n y: number;\n angle: number;\n distance: number;\n elapsed: number;\n progress: number;\n impact?: ClientProjectileImpact;\n impactElapsed?: number;\n impactProgress?: number;\n destroyed?: boolean;\n}\n\nexport interface RenderedProjectile {\n id: string;\n type: string;\n component: any;\n props: RenderedProjectileProps;\n}\n\nexport type ProjectilePredictionResolver = (\n projectile: ClientProjectileSpawn,\n) => ClientProjectileImpact | null | undefined;\n\nexport interface ProjectileSpawnClock {\n now?: number;\n currentServerTick?: number;\n tickDurationMs?: number;\n mapId?: string;\n}\n\ninterface RuntimeProjectile {\n spawn: ClientProjectileSpawn;\n component: any;\n createdAt: number;\n impact?: ClientProjectileImpact;\n visualImpact?: ClientProjectileImpact;\n predictedImpact?: ClientProjectileImpact;\n impactStartedAt?: number;\n destroyAt?: number;\n destroyReason?: string;\n}\n\nexport class ProjectileManager {\n private readonly components = new Map<string, any>();\n private readonly projectiles = new Map<string, RuntimeProjectile>();\n private readonly version = signal(0);\n private readonly impactDurationMs = 350;\n private mapId?: string;\n\n constructor(\n private readonly hooks: Hooks,\n private readonly predictionResolver?: ProjectilePredictionResolver,\n ) {}\n\n current = computed<RenderedProjectile[]>(() => {\n this.version();\n const now = Date.now();\n const rendered: RenderedProjectile[] = [];\n for (const projectile of this.projectiles.values()) {\n const props = this.toProps(projectile, now);\n if (!props) {\n continue;\n }\n rendered.push({\n id: projectile.spawn.id,\n type: projectile.spawn.type,\n component: projectile.component,\n props,\n });\n }\n return rendered;\n });\n\n register(type: string, component: any): any {\n this.components.set(type, component);\n return component;\n }\n\n get(type: string): any {\n return this.components.get(type);\n }\n\n setMapId(mapId: string | undefined): void {\n const normalizedMapId = normalizeRoomMapId(mapId);\n if (this.mapId === normalizedMapId) return;\n this.mapId = normalizedMapId;\n this.clear();\n }\n\n getMapId(): string | undefined {\n return this.mapId;\n }\n\n spawnBatch(projectiles: ClientProjectileSpawn[], clock: ProjectileSpawnClock = {}): void {\n if (!this.acceptsMap(clock.mapId)) return;\n const now = clock.now ?? Date.now();\n for (const projectile of projectiles) {\n const component = this.components.get(projectile.type);\n if (!component) {\n continue;\n }\n const runtime: RuntimeProjectile = {\n spawn: {\n ...projectile,\n delay: projectile.delay ?? 0,\n index: projectile.index ?? 0,\n count: projectile.count ?? 1,\n },\n component,\n createdAt: now,\n };\n this.setPredictedImpact(runtime);\n this.projectiles.set(projectile.id, runtime);\n this.hooks.callHooks(\"client-projectiles-onSpawn\", runtime.spawn).subscribe();\n }\n this.touch();\n }\n\n impactBatch(impacts: ClientProjectileImpact[], context: { mapId?: string } = {}): void {\n if (!this.acceptsMap(context.mapId)) return;\n const now = Date.now();\n for (const impact of impacts) {\n const projectile = this.projectiles.get(impact.id);\n if (!projectile) {\n continue;\n }\n this.setImpact(projectile, impact, now);\n this.hooks.callHooks(\"client-projectiles-onImpact\", this.toProps(projectile, now)).subscribe();\n }\n this.touch();\n }\n\n destroyBatch(projectiles: ClientProjectileDestroy[], context: { mapId?: string } = {}): void {\n if (!this.acceptsMap(context.mapId)) return;\n const now = Date.now();\n for (const destroyed of projectiles) {\n const projectile = this.projectiles.get(destroyed.id);\n if (!projectile) {\n continue;\n }\n if (destroyed.reason === \"hit\") {\n const current = this.toProps(projectile, now);\n this.setImpact(projectile, {\n id: destroyed.id,\n targetId: destroyed.targetId ?? projectile.impact?.targetId,\n x: destroyed.x ?? projectile.impact?.x ?? current?.x ?? projectile.spawn.origin.x,\n y: destroyed.y ?? projectile.impact?.y ?? current?.y ?? projectile.spawn.origin.y,\n distance: destroyed.distance ?? projectile.impact?.distance ?? current?.distance,\n }, now);\n }\n projectile.destroyReason = destroyed.reason;\n projectile.destroyAt = projectile.destroyAt ?? (\n projectile.impact && projectile.impactStartedAt !== undefined\n ? projectile.impactStartedAt + this.impactDurationMs\n : now\n );\n this.hooks.callHooks(\"client-projectiles-onDestroy\", this.toProps(projectile, now)).subscribe();\n }\n this.touch();\n }\n\n clear(): void {\n this.projectiles.clear();\n this.touch();\n }\n\n step(): void {\n const now = Date.now();\n let changed = false;\n for (const [id, projectile] of this.projectiles) {\n const props = this.toProps(projectile, now);\n if (\n (!props && !this.isWaitingForDelay(projectile, now)) ||\n (projectile.destroyAt !== undefined && now >= projectile.destroyAt)\n ) {\n this.projectiles.delete(id);\n changed = true;\n }\n }\n this.touch(changed || this.projectiles.size > 0);\n }\n\n private toProps(projectile: RuntimeProjectile, now: number): RenderedProjectileProps | null {\n const spawn = projectile.spawn;\n const delayMs = (spawn.delay ?? 0) * 1000;\n const elapsedMs = now - projectile.createdAt - delayMs;\n if (elapsedMs < 0) {\n return null;\n }\n const elapsed = elapsedMs / 1000;\n const ttl = Math.max(0.001, spawn.ttl);\n const rawDistance = Math.min(spawn.speed * elapsed, spawn.range);\n const predictedImpact = this.getActivePredictedImpact(projectile, now, rawDistance);\n const visualImpact = projectile.visualImpact ?? projectile.impact;\n const distance = visualImpact?.distance ?? predictedImpact?.distance ?? rawDistance;\n const progress = Math.min(1, distance / spawn.range);\n const x = visualImpact?.x ?? predictedImpact?.x ?? spawn.origin.x + spawn.direction.x * distance;\n const y = visualImpact?.y ?? predictedImpact?.y ?? spawn.origin.y + spawn.direction.y * distance;\n const impactElapsedMs = projectile.impactStartedAt !== undefined\n ? Math.max(0, now - projectile.impactStartedAt)\n : undefined;\n return {\n ...spawn,\n x,\n y,\n angle: Math.atan2(spawn.direction.y, spawn.direction.x),\n distance,\n elapsed,\n progress,\n impact: projectile.impact,\n impactElapsed: impactElapsedMs === undefined ? undefined : impactElapsedMs / 1000,\n impactProgress: impactElapsedMs === undefined\n ? undefined\n : Math.min(1, impactElapsedMs / this.impactDurationMs),\n destroyed: projectile.destroyAt !== undefined,\n ttl,\n };\n }\n\n private acceptsMap(mapId: string | undefined): boolean {\n const normalizedMapId = normalizeRoomMapId(mapId);\n return !normalizedMapId || !this.mapId || normalizedMapId === this.mapId;\n }\n\n private isWaitingForDelay(projectile: RuntimeProjectile, now: number): boolean {\n const delayMs = (projectile.spawn.delay ?? 0) * 1000;\n return now - projectile.createdAt - delayMs < 0;\n }\n\n private setPredictedImpact(projectile: RuntimeProjectile): void {\n if (projectile.spawn.predictImpact === false) {\n return;\n }\n const impact = this.predictionResolver?.(projectile.spawn);\n if (!impact || !Number.isFinite(impact.x) || !Number.isFinite(impact.y)) {\n return;\n }\n const distance = typeof impact.distance === \"number\" && Number.isFinite(impact.distance)\n ? impact.distance\n : Math.hypot(impact.x - projectile.spawn.origin.x, impact.y - projectile.spawn.origin.y);\n if (!Number.isFinite(distance) || distance < 0 || distance > projectile.spawn.range) {\n return;\n }\n projectile.predictedImpact = {\n ...impact,\n distance,\n };\n }\n\n private getActivePredictedImpact(\n projectile: RuntimeProjectile,\n now: number,\n rawDistance: number,\n ): ClientProjectileImpact | undefined {\n if (!projectile.predictedImpact || projectile.impact) {\n return undefined;\n }\n const distance = projectile.predictedImpact.distance;\n if (distance === undefined || rawDistance < distance) {\n return undefined;\n }\n return projectile.predictedImpact;\n }\n\n private setImpact(projectile: RuntimeProjectile, impact: ClientProjectileImpact, now: number): void {\n projectile.visualImpact = this.resolveVisualImpact(projectile, impact, now);\n projectile.impact = impact;\n projectile.predictedImpact = undefined;\n projectile.impactStartedAt = projectile.impactStartedAt ?? now;\n const impactDestroyAt = projectile.impactStartedAt + this.impactDurationMs;\n projectile.destroyAt = Math.max(projectile.destroyAt ?? 0, impactDestroyAt);\n }\n\n private resolveVisualImpact(\n projectile: RuntimeProjectile,\n impact: ClientProjectileImpact,\n now: number,\n ): ClientProjectileImpact {\n const predicted = projectile.predictedImpact;\n if (!predicted || !this.isSameTarget(predicted, impact)) {\n return impact;\n }\n const distance = predicted.distance;\n if (distance === undefined) {\n return impact;\n }\n const delayMs = (projectile.spawn.delay ?? 0) * 1000;\n const elapsedMs = now - projectile.createdAt - delayMs;\n if (elapsedMs < 0) {\n return impact;\n }\n const rawDistance = Math.min(projectile.spawn.speed * (elapsedMs / 1000), projectile.spawn.range);\n return rawDistance >= distance ? predicted : impact;\n }\n\n private isSameTarget(a: ClientProjectileImpact, b: ClientProjectileImpact): boolean {\n return a.targetId !== undefined && a.targetId === b.targetId;\n }\n\n private touch(force = true): void {\n if (force) {\n this.version.update((value) => value + 1);\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/RpgClient.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ 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 { RpgClientEvent } from './Game/Event';
|
|
6
|
+
import { MapPhysicsEntityContext, MapPhysicsInitContext, RpgActionName } from '@rpgjs/common';
|
|
7
|
+
import { ClientProjectileSpawn, RenderedProjectileProps } from './Game/ProjectileManager';
|
|
8
|
+
import { ClientVisualMap } from './Game/ClientVisuals';
|
|
6
9
|
type RpgComponent = RpgClientObject;
|
|
7
10
|
type SceneMap = Container;
|
|
8
11
|
export type SpriteComponentConfig = ComponentFunction | {
|
|
@@ -11,6 +14,14 @@ export type SpriteComponentConfig = ComponentFunction | {
|
|
|
11
14
|
data?: Record<string, any> | ((object: RpgClientObject) => Record<string, any>);
|
|
12
15
|
dependencies?: (object: RpgClientObject) => any[];
|
|
13
16
|
};
|
|
17
|
+
export type EventComponentSprite = RpgClientEvent & Record<string, any>;
|
|
18
|
+
export type EventComponentConfig = ComponentFunction | {
|
|
19
|
+
component: ComponentFunction;
|
|
20
|
+
props?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>);
|
|
21
|
+
data?: Record<string, any> | ((event: EventComponentSprite) => Record<string, any>);
|
|
22
|
+
dependencies?: (event: EventComponentSprite) => any[];
|
|
23
|
+
renderGraphic?: boolean;
|
|
24
|
+
};
|
|
14
25
|
export interface RpgSpriteBeforeRemoveContext {
|
|
15
26
|
reason?: string;
|
|
16
27
|
data?: any;
|
|
@@ -40,15 +51,18 @@ export interface RpgClientEngineHooks {
|
|
|
40
51
|
/**
|
|
41
52
|
* Recover keys from the pressed keyboard
|
|
42
53
|
*
|
|
43
|
-
* @prop { (engine: RpgClientEngine, obj: { input: string, playerId: number }) => any } [onInput]
|
|
54
|
+
* @prop { (engine: RpgClientEngine, obj: { input: string | number, action?: string | number, data?: any, playerId: number }) => any } [onInput]
|
|
44
55
|
* @memberof RpgEngineHooks
|
|
45
56
|
*/
|
|
46
57
|
onInput?: (engine: RpgClientEngine, obj: {
|
|
47
|
-
input:
|
|
58
|
+
input: RpgActionName;
|
|
59
|
+
action?: RpgActionName;
|
|
60
|
+
data?: any;
|
|
48
61
|
playerId: number;
|
|
49
62
|
}) => any;
|
|
50
63
|
/**
|
|
51
|
-
* Called when the user is connected to the server
|
|
64
|
+
* Called when the user is connected to the server. In MMORPG mode, this
|
|
65
|
+
* runs after the server sends the RPGJS connection acceptance packet.
|
|
52
66
|
*
|
|
53
67
|
* @prop { (engine: RpgClientEngine, socket: any) => any } [onConnected]
|
|
54
68
|
* @memberof RpgEngineHooks
|
|
@@ -62,7 +76,8 @@ export interface RpgClientEngineHooks {
|
|
|
62
76
|
*/
|
|
63
77
|
onDisconnect?: (engine: RpgClientEngine, reason: any, socket: any) => any;
|
|
64
78
|
/**
|
|
65
|
-
* Called when there was a connection error
|
|
79
|
+
* Called when there was a connection error. In MMORPG mode, this also runs
|
|
80
|
+
* when server-side auth refuses the connection.
|
|
66
81
|
*
|
|
67
82
|
* @prop { (engine: RpgClientEngine, err: any, socket: any) => any } [onConnectError]
|
|
68
83
|
* @memberof RpgEngineHooks
|
|
@@ -126,6 +141,30 @@ export interface RpgSpriteHooks {
|
|
|
126
141
|
* ```
|
|
127
142
|
*/
|
|
128
143
|
components?: Record<string, ComponentFunction>;
|
|
144
|
+
/**
|
|
145
|
+
* Resolve a custom CanvasEngine component for a specific event.
|
|
146
|
+
*
|
|
147
|
+
* The component always receives the synced event object as the `sprite` prop.
|
|
148
|
+
* Custom props are merged in addition to `sprite`, but cannot replace it.
|
|
149
|
+
* Return `null` or `undefined` to keep the default graphic renderer.
|
|
150
|
+
*
|
|
151
|
+
* @prop { (event: EventComponentSprite) => EventComponentConfig | null | undefined } [eventComponent]
|
|
152
|
+
* @memberof RpgSpriteHooks
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* import ChestEvent from './components/chest-event.ce'
|
|
156
|
+
*
|
|
157
|
+
* const sprite: RpgSpriteHooks = {
|
|
158
|
+
* eventComponent(sprite) {
|
|
159
|
+
* if (sprite.name === 'CHEST') {
|
|
160
|
+
* return ChestEvent
|
|
161
|
+
* }
|
|
162
|
+
* return null
|
|
163
|
+
* }
|
|
164
|
+
* }
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
eventComponent?: (event: EventComponentSprite) => EventComponentConfig | null | undefined;
|
|
129
168
|
/**
|
|
130
169
|
* As soon as the sprite is initialized
|
|
131
170
|
*
|
|
@@ -222,6 +261,13 @@ export interface RpgSceneHooks<Scene> {
|
|
|
222
261
|
onDraw?: (scene: Scene, t: number) => any;
|
|
223
262
|
}
|
|
224
263
|
export interface RpgSceneMapHooks extends RpgSceneHooks<SceneMap> {
|
|
264
|
+
/**
|
|
265
|
+
* Root CanvasEngine component used to render the RPG scene map.
|
|
266
|
+
*
|
|
267
|
+
* Use the exported `SceneMap` component inside your custom component to
|
|
268
|
+
* keep the default map rendering and compose additional scene children.
|
|
269
|
+
*/
|
|
270
|
+
component?: ComponentFunction;
|
|
225
271
|
/**
|
|
226
272
|
* The map and resources are being loaded
|
|
227
273
|
*
|
|
@@ -261,6 +307,24 @@ export interface RpgSceneMapHooks extends RpgSceneHooks<SceneMap> {
|
|
|
261
307
|
*/
|
|
262
308
|
onPhysicsReset?: (scene: SceneMap) => any;
|
|
263
309
|
}
|
|
310
|
+
export interface RpgProjectileHooks {
|
|
311
|
+
/**
|
|
312
|
+
* CanvasEngine components used to render server-authoritative projectiles.
|
|
313
|
+
*/
|
|
314
|
+
components?: Record<string, ComponentFunction>;
|
|
315
|
+
/**
|
|
316
|
+
* Called when a projectile spawn batch is received from the server.
|
|
317
|
+
*/
|
|
318
|
+
onSpawn?: (projectile: ClientProjectileSpawn) => any;
|
|
319
|
+
/**
|
|
320
|
+
* Called when the server confirms a projectile impact.
|
|
321
|
+
*/
|
|
322
|
+
onImpact?: (projectile: RenderedProjectileProps | null) => any;
|
|
323
|
+
/**
|
|
324
|
+
* Called when the server destroys a projectile.
|
|
325
|
+
*/
|
|
326
|
+
onDestroy?: (projectile: RenderedProjectileProps | null) => any;
|
|
327
|
+
}
|
|
264
328
|
export interface RpgClient {
|
|
265
329
|
/**
|
|
266
330
|
* Add hooks to the player or engine. All modules can listen to the hook
|
|
@@ -588,30 +652,33 @@ export interface RpgClient {
|
|
|
588
652
|
* */
|
|
589
653
|
sprite?: RpgSpriteHooks;
|
|
590
654
|
/**
|
|
591
|
-
* Reference the scenes of the game.
|
|
655
|
+
* Reference the scenes of the game.
|
|
592
656
|
*
|
|
593
657
|
* ```ts
|
|
594
658
|
* import { RpgSceneMapHooks, RpgClient, defineModule } from '@rpgjs/client'
|
|
659
|
+
* import MyScene from './my-scene.ce'
|
|
595
660
|
*
|
|
596
661
|
* export const sceneMap: RpgSceneMapHooks = {
|
|
597
|
-
*
|
|
662
|
+
* component: MyScene
|
|
598
663
|
* }
|
|
599
664
|
*
|
|
600
665
|
* defineModule<RpgClient>({
|
|
601
|
-
*
|
|
602
|
-
* // If you put the RpgSceneMap scene, Thhe key is called mandatory `map`
|
|
603
|
-
* map: sceneMap
|
|
604
|
-
* }
|
|
666
|
+
* sceneMap
|
|
605
667
|
* })
|
|
606
668
|
* ```
|
|
607
669
|
*
|
|
608
|
-
* @prop {
|
|
670
|
+
* @prop {RpgSceneMapHooks} [sceneMap]
|
|
609
671
|
* @memberof RpgClient
|
|
610
672
|
* */
|
|
673
|
+
sceneMap?: RpgSceneMapHooks;
|
|
674
|
+
/**
|
|
675
|
+
* Legacy scene map hook container.
|
|
676
|
+
*
|
|
677
|
+
* Prefer `sceneMap` for new code.
|
|
678
|
+
*/
|
|
611
679
|
scenes?: {
|
|
612
680
|
map: RpgSceneMapHooks;
|
|
613
681
|
};
|
|
614
|
-
sceneMap?: RpgSceneMapHooks;
|
|
615
682
|
/**
|
|
616
683
|
* Array containing the list of component animations
|
|
617
684
|
* Each element defines a temporary component to display for animations like hits, effects, etc.
|
|
@@ -642,5 +709,42 @@ export interface RpgClient {
|
|
|
642
709
|
id: string;
|
|
643
710
|
component: ComponentFunction;
|
|
644
711
|
}[];
|
|
712
|
+
/**
|
|
713
|
+
* Named client-side visual macros.
|
|
714
|
+
*
|
|
715
|
+
* Use client visuals when the server needs to trigger a group of existing
|
|
716
|
+
* client visual primitives at once, such as a flash, damage text, sound,
|
|
717
|
+
* component animation, and camera shake. The server sends only the visual
|
|
718
|
+
* name and a serializable payload; the rendering details live on the client.
|
|
719
|
+
*
|
|
720
|
+
* For a single sound, flash, or component animation, prefer the direct
|
|
721
|
+
* server APIs (`playSound`, `flash`, `showComponentAnimation`). Client
|
|
722
|
+
* visuals are meant to group several visual operations and reduce bandwidth.
|
|
723
|
+
*
|
|
724
|
+
* ```ts
|
|
725
|
+
* import { defineModule, RpgClient } from '@rpgjs/client'
|
|
726
|
+
*
|
|
727
|
+
* export default defineModule<RpgClient>({
|
|
728
|
+
* clientVisuals: {
|
|
729
|
+
* hit({ target, data }, helpers) {
|
|
730
|
+
* helpers.flash(target, { type: 'tint', tint: 'red' })
|
|
731
|
+
* helpers.showHit(target, `-${data.damage}`)
|
|
732
|
+
* helpers.sound('hit')
|
|
733
|
+
* }
|
|
734
|
+
* }
|
|
735
|
+
* })
|
|
736
|
+
* ```
|
|
737
|
+
*
|
|
738
|
+
* @prop {Record<string, ClientVisualHandler>} [clientVisuals]
|
|
739
|
+
* @memberof RpgClient
|
|
740
|
+
*/
|
|
741
|
+
clientVisuals?: ClientVisualMap;
|
|
742
|
+
/**
|
|
743
|
+
* Client-side projectile rendering configuration.
|
|
744
|
+
*
|
|
745
|
+
* Register a CanvasEngine component per projectile type. The server sends
|
|
746
|
+
* compact spawn/impact/destroy events and the client predicts x/y locally.
|
|
747
|
+
*/
|
|
748
|
+
projectiles?: RpgProjectileHooks;
|
|
645
749
|
}
|
|
646
750
|
export {};
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Trigger } from 'canvasengine';
|
|
2
2
|
import { AbstractWebsocket } from './services/AbstractSocket';
|
|
3
|
-
import { Direction } from '@rpgjs/common';
|
|
3
|
+
import { Direction, RpgActionInput, RpgActionName } from '@rpgjs/common';
|
|
4
|
+
import { EventComponentConfig } from './RpgClient';
|
|
5
|
+
import { RpgClientEvent } from './Game/Event';
|
|
4
6
|
import { RpgClientMap } from './Game/Map';
|
|
5
7
|
import { AnimationManager } from './Game/AnimationManager';
|
|
6
8
|
import { Observable } from 'rxjs';
|
|
9
|
+
import { ProjectileManager } from './Game/ProjectileManager';
|
|
10
|
+
import { ClientVisualRegistry, ClientVisualHandler, ClientVisualMap, ClientVisualPacket } from './Game/ClientVisuals';
|
|
11
|
+
import { ClientPointerContext } from './services/pointerContext';
|
|
12
|
+
import { EventComponentResolver } from './Game/EventComponentResolver';
|
|
7
13
|
import * as PIXI from "pixi.js";
|
|
8
14
|
type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
|
|
9
15
|
start(config?: T): Promise<void>;
|
|
@@ -24,12 +30,16 @@ export declare class RpgClientEngine<T = any> {
|
|
|
24
30
|
private selector;
|
|
25
31
|
globalConfig: T;
|
|
26
32
|
sceneComponent: any;
|
|
33
|
+
sceneMapComponent: any;
|
|
27
34
|
stopProcessingInput: boolean;
|
|
28
35
|
width: import('canvasengine').WritableSignal<string>;
|
|
29
36
|
height: import('canvasengine').WritableSignal<string>;
|
|
30
37
|
spritesheets: Map<string | number, any>;
|
|
31
38
|
sounds: Map<string, any>;
|
|
32
39
|
componentAnimations: any[];
|
|
40
|
+
clientVisuals: ClientVisualRegistry;
|
|
41
|
+
projectiles: ProjectileManager;
|
|
42
|
+
pointer: ClientPointerContext;
|
|
33
43
|
private spritesheetResolver?;
|
|
34
44
|
private soundResolver?;
|
|
35
45
|
particleSettings: {
|
|
@@ -43,6 +53,7 @@ export declare class RpgClientEngine<T = any> {
|
|
|
43
53
|
spriteComponentsBehind: import('canvasengine').WritableArraySignal<any[]>;
|
|
44
54
|
spriteComponentsInFront: import('canvasengine').WritableArraySignal<any[]>;
|
|
45
55
|
spriteComponents: Map<string, any>;
|
|
56
|
+
private eventComponentResolvers;
|
|
46
57
|
/** ID of the sprite that the camera should follow. null means follow the current player */
|
|
47
58
|
cameraFollowTargetId: import('canvasengine').WritableSignal<string | null>;
|
|
48
59
|
/** Trigger for map shake animation */
|
|
@@ -56,6 +67,8 @@ export declare class RpgClientEngine<T = any> {
|
|
|
56
67
|
private pendingPredictionFrames;
|
|
57
68
|
private lastClientPhysicsStepAt;
|
|
58
69
|
private frameOffset;
|
|
70
|
+
private latestServerTick?;
|
|
71
|
+
private latestServerTickAt;
|
|
59
72
|
private rtt;
|
|
60
73
|
private pingInterval;
|
|
61
74
|
private readonly PING_INTERVAL_MS;
|
|
@@ -70,8 +83,14 @@ export declare class RpgClientEngine<T = any> {
|
|
|
70
83
|
private eventsReceived$;
|
|
71
84
|
private onAfterLoadingSubscription?;
|
|
72
85
|
private sceneResetQueued;
|
|
86
|
+
private mapTransitionInProgress;
|
|
87
|
+
private currentMapRoomId?;
|
|
88
|
+
private socketListenersInitialized;
|
|
73
89
|
private tickSubscriptions;
|
|
74
90
|
private resizeHandler?;
|
|
91
|
+
private pointerMoveHandler?;
|
|
92
|
+
private pointerCanvas?;
|
|
93
|
+
private pendingSyncPackets;
|
|
75
94
|
private notificationManager;
|
|
76
95
|
constructor(context: any);
|
|
77
96
|
/**
|
|
@@ -109,9 +128,18 @@ export declare class RpgClientEngine<T = any> {
|
|
|
109
128
|
*/
|
|
110
129
|
setKeyboardControls(controlInstance: any): void;
|
|
111
130
|
start(): Promise<void>;
|
|
131
|
+
private resolveSceneMapComponent;
|
|
132
|
+
private setupPointerTracking;
|
|
133
|
+
private findViewportInstance;
|
|
112
134
|
private prepareSyncPayload;
|
|
113
135
|
private normalizeAckWithSyncState;
|
|
114
136
|
private initListeners;
|
|
137
|
+
private beginMapTransfer;
|
|
138
|
+
private clearComponentAnimations;
|
|
139
|
+
private shouldProcessProjectilePacket;
|
|
140
|
+
private callConnectError;
|
|
141
|
+
private flushPendingSyncPackets;
|
|
142
|
+
private applySyncPacket;
|
|
115
143
|
/**
|
|
116
144
|
* Start periodic ping/pong for client-server synchronization
|
|
117
145
|
*
|
|
@@ -486,6 +514,52 @@ export declare class RpgClientEngine<T = any> {
|
|
|
486
514
|
* @returns The CanvasEngine component, or undefined when missing
|
|
487
515
|
*/
|
|
488
516
|
getSpriteComponent(id: string): any;
|
|
517
|
+
/**
|
|
518
|
+
* Register a custom event component resolver.
|
|
519
|
+
*
|
|
520
|
+
* The last resolver returning a component wins. This lets later modules
|
|
521
|
+
* override earlier defaults without replacing the whole map scene.
|
|
522
|
+
*
|
|
523
|
+
* @param resolver - Function receiving the synced event object
|
|
524
|
+
* @returns The registered resolver
|
|
525
|
+
*/
|
|
526
|
+
addEventComponentResolver(resolver: EventComponentResolver): EventComponentResolver;
|
|
527
|
+
/**
|
|
528
|
+
* Resolve the custom CanvasEngine component for an event, if any.
|
|
529
|
+
*
|
|
530
|
+
* @param event - Synced client event object
|
|
531
|
+
* @returns The component/config returned by the last matching resolver
|
|
532
|
+
*/
|
|
533
|
+
resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null;
|
|
534
|
+
registerProjectileComponent(type: string, component: any): any;
|
|
535
|
+
getProjectileComponent(type: string): any;
|
|
536
|
+
/**
|
|
537
|
+
* Register a named client visual macro.
|
|
538
|
+
*
|
|
539
|
+
* Client visuals are small client-side functions that group existing visual
|
|
540
|
+
* primitives such as flash, sound, component animations, sprite animation, or
|
|
541
|
+
* map shake. The server sends only the visual name and a serializable payload.
|
|
542
|
+
*
|
|
543
|
+
* @param name - Stable visual name sent by the server
|
|
544
|
+
* @param handler - Client-side visual handler
|
|
545
|
+
* @returns The registered handler
|
|
546
|
+
*/
|
|
547
|
+
registerClientVisual(name: string, handler: ClientVisualHandler): ClientVisualHandler;
|
|
548
|
+
/**
|
|
549
|
+
* Register several named client visual macros.
|
|
550
|
+
*
|
|
551
|
+
* @param visuals - Map of visual names to client-side handlers
|
|
552
|
+
*/
|
|
553
|
+
registerClientVisuals(visuals: ClientVisualMap): void;
|
|
554
|
+
/**
|
|
555
|
+
* Play a registered client visual locally.
|
|
556
|
+
*
|
|
557
|
+
* This is also used by the websocket listener when the server calls
|
|
558
|
+
* `player.clientVisual()` or `map.clientVisual()`.
|
|
559
|
+
*
|
|
560
|
+
* @param packet - Visual name and serializable payload
|
|
561
|
+
*/
|
|
562
|
+
playClientVisual(packet: ClientVisualPacket): Promise<void>;
|
|
489
563
|
/**
|
|
490
564
|
* Add a component animation to the engine
|
|
491
565
|
*
|
|
@@ -564,14 +638,18 @@ export declare class RpgClientEngine<T = any> {
|
|
|
564
638
|
processInput({ input }: {
|
|
565
639
|
input: Direction;
|
|
566
640
|
}): Promise<void>;
|
|
567
|
-
processAction(
|
|
568
|
-
|
|
569
|
-
}): void;
|
|
641
|
+
processAction(action: RpgActionName, data?: any): void;
|
|
642
|
+
processAction(action: RpgActionInput): void;
|
|
570
643
|
get PIXI(): typeof PIXI;
|
|
571
644
|
get socket(): AbstractWebsocket;
|
|
572
645
|
get playerId(): string | null;
|
|
573
646
|
get scene(): RpgClientMap;
|
|
647
|
+
getObjectById(id: string): any;
|
|
574
648
|
private getPhysicsTick;
|
|
649
|
+
private getPhysicsTickDurationMs;
|
|
650
|
+
private updateServerTickEstimate;
|
|
651
|
+
private estimateServerTick;
|
|
652
|
+
private predictProjectileImpact;
|
|
575
653
|
private ensureCurrentPlayerBody;
|
|
576
654
|
private stepClientPhysicsTick;
|
|
577
655
|
private flushPendingPredictedStates;
|