@rpgjs/client 5.0.0-beta.12 → 5.0.0-beta.14
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 +18 -0
- package/dist/Game/Object.d.ts +2 -0
- package/dist/Game/Object.js +20 -6
- package/dist/Game/Object.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 +21 -1
- package/dist/RpgClientEngine.d.ts +20 -2
- package/dist/RpgClientEngine.js +182 -17
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/character.ce.js +84 -9
- 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 +66 -33
- 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/components/scenes/event-layer.ce.js +42 -3
- package/dist/components/scenes/event-layer.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 +2 -0
- package/dist/index.js +3 -1
- package/dist/module.js +23 -3
- package/dist/module.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/loadMap.d.ts +3 -0
- package/dist/services/loadMap.js.map +1 -1
- package/package.json +4 -4
- package/src/Game/Object.spec.ts +14 -1
- package/src/Game/Object.ts +34 -10
- package/src/Gui/Gui.spec.ts +67 -0
- package/src/Gui/Gui.ts +24 -7
- package/src/RpgClient.ts +28 -1
- package/src/RpgClientEngine.ts +254 -29
- package/src/components/character.ce +92 -9
- 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 +68 -31
- package/src/components/scenes/draw-map.ce +16 -5
- package/src/components/scenes/event-layer.ce +54 -2
- package/src/i18n.spec.ts +39 -0
- package/src/i18n.ts +59 -0
- package/src/index.ts +2 -0
- package/src/module.ts +32 -10
- 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/loadMap.ts +3 -1
package/src/RpgClientEngine.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
|
|
|
6
6
|
import { LoadMapService, LoadMapToken } from "./services/loadMap";
|
|
7
7
|
import { RpgSound } from "./Sound";
|
|
8
8
|
import { RpgResource } from "./Resource";
|
|
9
|
-
import { Hooks, ModulesToken, Direction, normalizeLightingState, Vector2 } from "@rpgjs/common";
|
|
9
|
+
import { getOrCreateI18nService, Hooks, ModulesToken, Direction, normalizeLightingState, Vector2, type I18nParams, type I18nService } from "@rpgjs/common";
|
|
10
10
|
import type { EventComponentConfig } from "./RpgClient";
|
|
11
11
|
import type { RpgClientEvent } from "./Game/Event";
|
|
12
12
|
import { load } from "@signe/sync";
|
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
type PredictionState,
|
|
28
28
|
type RpgActionInput,
|
|
29
29
|
type RpgActionName,
|
|
30
|
+
type RpgDashInput,
|
|
31
|
+
type RpgMovementInput,
|
|
30
32
|
} from "@rpgjs/common";
|
|
31
33
|
import { NotificationManager } from "./Gui/NotificationManager";
|
|
32
34
|
import { SaveClientService } from "./services/save";
|
|
@@ -35,19 +37,97 @@ import { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSp
|
|
|
35
37
|
import { ClientVisualRegistry, type ClientVisualHandler, type ClientVisualMap, type ClientVisualPacket } from "./Game/ClientVisuals";
|
|
36
38
|
import { normalizeActionInput } from "./services/actionInput";
|
|
37
39
|
import { createClientPointerContext, type ClientPointerContext } from "./services/pointerContext";
|
|
40
|
+
import { RpgClientInteractions } from "./services/interactions";
|
|
38
41
|
import { normalizeRoomMapId } from "./utils/mapId";
|
|
39
42
|
import { EventComponentResolverRegistry, type EventComponentResolver } from "./Game/EventComponentResolver";
|
|
43
|
+
import { RpgClientBuiltinI18n } from "./i18n";
|
|
40
44
|
|
|
41
45
|
interface MovementTrajectoryPoint {
|
|
42
46
|
frame: number;
|
|
43
47
|
tick: number;
|
|
44
48
|
timestamp: number;
|
|
45
|
-
input:
|
|
49
|
+
input: RpgMovementInput;
|
|
46
50
|
x: number;
|
|
47
51
|
y: number;
|
|
48
52
|
direction?: Direction;
|
|
49
53
|
}
|
|
50
54
|
|
|
55
|
+
const DEFAULT_DASH_ADDITIONAL_SPEED = 8;
|
|
56
|
+
const DEFAULT_DASH_DURATION_MS = 180;
|
|
57
|
+
const DEFAULT_DASH_COOLDOWN_MS = 450;
|
|
58
|
+
|
|
59
|
+
const isDashInput = (input: RpgMovementInput): input is RpgDashInput =>
|
|
60
|
+
typeof input === "object" && input !== null && input.type === "dash";
|
|
61
|
+
|
|
62
|
+
const isMoveInput = (
|
|
63
|
+
input: RpgMovementInput
|
|
64
|
+
): input is { type: "move"; direction: Direction } =>
|
|
65
|
+
typeof input === "object" && input !== null && input.type === "move";
|
|
66
|
+
|
|
67
|
+
const resolveMoveDirection = (input: RpgMovementInput): Direction | undefined => {
|
|
68
|
+
if (isMoveInput(input)) return input.direction;
|
|
69
|
+
if (typeof input === "string" || typeof input === "number") {
|
|
70
|
+
return input as Direction;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const directionToVector = (direction: Direction | undefined) => {
|
|
76
|
+
switch (direction) {
|
|
77
|
+
case Direction.Left:
|
|
78
|
+
return { x: -1, y: 0 };
|
|
79
|
+
case Direction.Right:
|
|
80
|
+
return { x: 1, y: 0 };
|
|
81
|
+
case Direction.Up:
|
|
82
|
+
return { x: 0, y: -1 };
|
|
83
|
+
case Direction.Down:
|
|
84
|
+
default:
|
|
85
|
+
return { x: 0, y: 1 };
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const vectorToDirection = (direction: { x: number; y: number }): Direction => {
|
|
90
|
+
if (Math.abs(direction.x) > Math.abs(direction.y)) {
|
|
91
|
+
return direction.x < 0 ? Direction.Left : Direction.Right;
|
|
92
|
+
}
|
|
93
|
+
return direction.y < 0 ? Direction.Up : Direction.Down;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const normalizeDashInput = (
|
|
97
|
+
input: Partial<RpgDashInput>,
|
|
98
|
+
fallbackDirection: Direction | undefined
|
|
99
|
+
): RpgDashInput | null => {
|
|
100
|
+
const rawDirection = input.direction ?? directionToVector(fallbackDirection);
|
|
101
|
+
const rawX = Number(rawDirection?.x ?? 0);
|
|
102
|
+
const rawY = Number(rawDirection?.y ?? 0);
|
|
103
|
+
const magnitude = Math.hypot(rawX, rawY);
|
|
104
|
+
if (!Number.isFinite(magnitude) || magnitude <= 0) return null;
|
|
105
|
+
|
|
106
|
+
const additionalSpeed =
|
|
107
|
+
typeof input.additionalSpeed === "number" && Number.isFinite(input.additionalSpeed)
|
|
108
|
+
? Math.max(0, Math.min(input.additionalSpeed, 64))
|
|
109
|
+
: DEFAULT_DASH_ADDITIONAL_SPEED;
|
|
110
|
+
const duration =
|
|
111
|
+
typeof input.duration === "number" && Number.isFinite(input.duration)
|
|
112
|
+
? Math.max(1, Math.min(input.duration, 1000))
|
|
113
|
+
: DEFAULT_DASH_DURATION_MS;
|
|
114
|
+
const cooldown =
|
|
115
|
+
typeof input.cooldown === "number" && Number.isFinite(input.cooldown)
|
|
116
|
+
? Math.max(0, Math.min(input.cooldown, 5000))
|
|
117
|
+
: DEFAULT_DASH_COOLDOWN_MS;
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
type: "dash",
|
|
121
|
+
direction: {
|
|
122
|
+
x: rawX / magnitude,
|
|
123
|
+
y: rawY / magnitude,
|
|
124
|
+
},
|
|
125
|
+
additionalSpeed,
|
|
126
|
+
duration,
|
|
127
|
+
cooldown,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
51
131
|
type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
|
|
52
132
|
start(config?: T): Promise<void>;
|
|
53
133
|
};
|
|
@@ -73,11 +153,13 @@ export class RpgClientEngine<T = any> {
|
|
|
73
153
|
width = signal("100%");
|
|
74
154
|
height = signal("100%");
|
|
75
155
|
spritesheets: Map<string | number, any> = new Map();
|
|
156
|
+
private spritesheetPromises: Map<string | number, Promise<any>> = new Map();
|
|
76
157
|
sounds: Map<string, any> = new Map();
|
|
77
158
|
componentAnimations: any[] = [];
|
|
78
159
|
clientVisuals = new ClientVisualRegistry();
|
|
79
160
|
projectiles: ProjectileManager;
|
|
80
161
|
pointer: ClientPointerContext = createClientPointerContext();
|
|
162
|
+
interactions: RpgClientInteractions = new RpgClientInteractions(this);
|
|
81
163
|
private spritesheetResolver?: (id: string | number) => any | Promise<any>;
|
|
82
164
|
private soundResolver?: (id: string) => any | Promise<any>;
|
|
83
165
|
particleSettings: {
|
|
@@ -103,7 +185,7 @@ export class RpgClientEngine<T = any> {
|
|
|
103
185
|
gamePause = signal(false);
|
|
104
186
|
|
|
105
187
|
private predictionEnabled = false;
|
|
106
|
-
private prediction?: PredictionController<Direction>;
|
|
188
|
+
private prediction?: PredictionController<RpgMovementInput, Direction>;
|
|
107
189
|
private readonly SERVER_CORRECTION_THRESHOLD = 30;
|
|
108
190
|
private inputFrameCounter = 0;
|
|
109
191
|
private pendingPredictionFrames: number[] = [];
|
|
@@ -111,6 +193,7 @@ export class RpgClientEngine<T = any> {
|
|
|
111
193
|
private frameOffset = 0;
|
|
112
194
|
private latestServerTick?: number;
|
|
113
195
|
private latestServerTickAt = 0;
|
|
196
|
+
private dashLockedUntil = 0;
|
|
114
197
|
// Ping/Pong for RTT measurement
|
|
115
198
|
private rtt: number = 0; // Round-trip time in ms
|
|
116
199
|
private pingInterval: any = null;
|
|
@@ -135,15 +218,21 @@ export class RpgClientEngine<T = any> {
|
|
|
135
218
|
private tickSubscriptions: any[] = [];
|
|
136
219
|
private resizeHandler?: () => void;
|
|
137
220
|
private pointerMoveHandler?: (event: PointerEvent) => void;
|
|
221
|
+
private pointerUpHandler?: (event: PointerEvent) => void;
|
|
222
|
+
private pointerCancelHandler?: (event: PointerEvent) => void;
|
|
138
223
|
private pointerCanvas?: HTMLCanvasElement;
|
|
139
224
|
private pendingSyncPackets: any[] = [];
|
|
140
225
|
private notificationManager: NotificationManager = new NotificationManager();
|
|
226
|
+
private i18nService: I18nService;
|
|
227
|
+
private locale?: string;
|
|
141
228
|
|
|
142
229
|
constructor(public context) {
|
|
143
230
|
this.webSocket = inject(WebSocketToken);
|
|
144
231
|
this.guiService = inject(RpgGui);
|
|
145
232
|
this.loadMapService = inject(LoadMapToken);
|
|
146
233
|
this.hooks = inject<Hooks>(ModulesToken);
|
|
234
|
+
this.i18nService = getOrCreateI18nService(context);
|
|
235
|
+
this.i18nService.addMessages(RpgClientBuiltinI18n, "rpgjs-client", 0);
|
|
147
236
|
this.projectiles = new ProjectileManager(
|
|
148
237
|
this.hooks,
|
|
149
238
|
(projectile) => this.predictProjectileImpact(projectile),
|
|
@@ -179,6 +268,25 @@ export class RpgClientEngine<T = any> {
|
|
|
179
268
|
this.initializePredictionController();
|
|
180
269
|
}
|
|
181
270
|
|
|
271
|
+
setLocale(locale: string) {
|
|
272
|
+
this.locale = locale;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
getLocale(): string {
|
|
276
|
+
return this.locale || this.i18nService.defaultLocale;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
t(key: string, params?: I18nParams): string {
|
|
280
|
+
return this.i18nService.t(key, params, this.getLocale());
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
i18n() {
|
|
284
|
+
return {
|
|
285
|
+
locale: this.getLocale(),
|
|
286
|
+
t: (key: string, params?: I18nParams) => this.t(key, params),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
182
290
|
/**
|
|
183
291
|
* Assigns a CanvasEngine KeyboardControls instance to the dependency injection context
|
|
184
292
|
*
|
|
@@ -254,7 +362,6 @@ export class RpgClientEngine<T = any> {
|
|
|
254
362
|
this.renderer = app.renderer as unknown as PIXI.Renderer;
|
|
255
363
|
this.setupPointerTracking();
|
|
256
364
|
this.tick = canvasElement?.propObservables?.context['tick'].observable
|
|
257
|
-
this.flushPendingSyncPackets();
|
|
258
365
|
|
|
259
366
|
const inputCheckSubscription = this.tick.subscribe(() => {
|
|
260
367
|
if (Date.now() - this.lastInputTime > 100) {
|
|
@@ -268,6 +375,7 @@ export class RpgClientEngine<T = any> {
|
|
|
268
375
|
|
|
269
376
|
this.hooks.callHooks("client-spritesheets-load", this).subscribe();
|
|
270
377
|
this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
|
|
378
|
+
this.flushPendingSyncPackets();
|
|
271
379
|
this.hooks.callHooks("client-sounds-load", this).subscribe();
|
|
272
380
|
this.hooks.callHooks("client-soundResolver-load", this).subscribe();
|
|
273
381
|
|
|
@@ -278,6 +386,7 @@ export class RpgClientEngine<T = any> {
|
|
|
278
386
|
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
279
387
|
this.hooks.callHooks("client-clientVisuals-load", this).subscribe();
|
|
280
388
|
this.hooks.callHooks("client-projectiles-load", this).subscribe();
|
|
389
|
+
this.hooks.callHooks("client-interactions-load", this).subscribe();
|
|
281
390
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
282
391
|
|
|
283
392
|
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
@@ -324,7 +433,7 @@ export class RpgClientEngine<T = any> {
|
|
|
324
433
|
}
|
|
325
434
|
|
|
326
435
|
this.pointerCanvas = canvas;
|
|
327
|
-
|
|
436
|
+
const updatePointer = (event: PointerEvent) => {
|
|
328
437
|
const rect = canvas.getBoundingClientRect();
|
|
329
438
|
const screen = {
|
|
330
439
|
x: event.clientX - rect.left,
|
|
@@ -344,16 +453,67 @@ export class RpgClientEngine<T = any> {
|
|
|
344
453
|
this.pointer.update(screen, world);
|
|
345
454
|
};
|
|
346
455
|
|
|
456
|
+
this.pointerMoveHandler = (event: PointerEvent) => {
|
|
457
|
+
updatePointer(event);
|
|
458
|
+
this.interactions.handlePointerMove(event);
|
|
459
|
+
};
|
|
460
|
+
this.pointerUpHandler = (event: PointerEvent) => {
|
|
461
|
+
updatePointer(event);
|
|
462
|
+
this.interactions.handlePointerUp(event);
|
|
463
|
+
};
|
|
464
|
+
this.pointerCancelHandler = (event: PointerEvent) => {
|
|
465
|
+
updatePointer(event);
|
|
466
|
+
this.interactions.cancelDrag(event);
|
|
467
|
+
};
|
|
468
|
+
|
|
347
469
|
canvas.addEventListener("pointermove", this.pointerMoveHandler);
|
|
348
470
|
canvas.addEventListener("pointerdown", this.pointerMoveHandler);
|
|
471
|
+
canvas.addEventListener("pointerup", this.pointerUpHandler);
|
|
472
|
+
canvas.addEventListener("pointercancel", this.pointerCancelHandler);
|
|
473
|
+
canvas.addEventListener("pointerleave", this.pointerCancelHandler);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
updatePointerFromInteractionEvent(event: any): void {
|
|
477
|
+
const global = event?.global ?? event?.data?.global;
|
|
478
|
+
|
|
479
|
+
if (!global) {
|
|
480
|
+
this.pointer.updateFromEvent(event);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const screen = {
|
|
485
|
+
x: Number(global.x),
|
|
486
|
+
y: Number(global.y),
|
|
487
|
+
};
|
|
488
|
+
if (!Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {
|
|
489
|
+
this.pointer.updateFromEvent(event);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const viewport = this.findViewportInstance();
|
|
494
|
+
if (viewport && typeof viewport.toWorld === "function") {
|
|
495
|
+
const point = viewport.toWorld(screen.x, screen.y);
|
|
496
|
+
this.pointer.update(screen, { x: Number(point.x), y: Number(point.y) });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
this.pointer.update(screen);
|
|
349
501
|
}
|
|
350
502
|
|
|
351
503
|
private findViewportInstance(): any {
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
typeof
|
|
355
|
-
|
|
356
|
-
|
|
504
|
+
const find = (node: any): any => {
|
|
505
|
+
if (!node) return undefined;
|
|
506
|
+
if (typeof node?.toWorld === "function" || node?.constructor?.name === "Viewport") {
|
|
507
|
+
return node;
|
|
508
|
+
}
|
|
509
|
+
for (const child of node.children ?? []) {
|
|
510
|
+
const viewport = find(child);
|
|
511
|
+
if (viewport) return viewport;
|
|
512
|
+
}
|
|
513
|
+
return undefined;
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
return find((this.canvasApp as any)?.stage);
|
|
357
517
|
}
|
|
358
518
|
|
|
359
519
|
private prepareSyncPayload(data: any): any {
|
|
@@ -793,6 +953,12 @@ export class RpgClientEngine<T = any> {
|
|
|
793
953
|
throw error;
|
|
794
954
|
}
|
|
795
955
|
const res = await this.loadMapService.load(mapId)
|
|
956
|
+
const loadedLighting = typeof res?.lighting !== "undefined"
|
|
957
|
+
? res.lighting
|
|
958
|
+
: res?.data?.lighting;
|
|
959
|
+
if (typeof loadedLighting !== "undefined") {
|
|
960
|
+
this.sceneMap.lightingState.set(normalizeLightingState(loadedLighting));
|
|
961
|
+
}
|
|
796
962
|
this.sceneMap.data.set(res)
|
|
797
963
|
|
|
798
964
|
// Check if playerId is already present
|
|
@@ -883,17 +1049,29 @@ export class RpgClientEngine<T = any> {
|
|
|
883
1049
|
|
|
884
1050
|
// If not in cache and resolver exists, use it
|
|
885
1051
|
if (this.spritesheetResolver) {
|
|
1052
|
+
if (this.spritesheetPromises.has(id)) {
|
|
1053
|
+
return this.spritesheetPromises.get(id);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
886
1056
|
const result = this.spritesheetResolver(id);
|
|
887
1057
|
|
|
888
1058
|
// Check if result is a Promise
|
|
889
1059
|
if (result instanceof Promise) {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1060
|
+
const promise = result
|
|
1061
|
+
.then((spritesheet) => {
|
|
1062
|
+
if (spritesheet) {
|
|
1063
|
+
// Cache the resolved spritesheet
|
|
1064
|
+
this.spritesheets.set(id, spritesheet);
|
|
1065
|
+
}
|
|
1066
|
+
this.spritesheetPromises.delete(id);
|
|
1067
|
+
return spritesheet;
|
|
1068
|
+
})
|
|
1069
|
+
.catch((error) => {
|
|
1070
|
+
this.spritesheetPromises.delete(id);
|
|
1071
|
+
throw error;
|
|
1072
|
+
});
|
|
1073
|
+
this.spritesheetPromises.set(id, promise);
|
|
1074
|
+
return promise;
|
|
897
1075
|
} else {
|
|
898
1076
|
// Synchronous result
|
|
899
1077
|
if (result) {
|
|
@@ -1515,7 +1693,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1515
1693
|
});
|
|
1516
1694
|
}
|
|
1517
1695
|
|
|
1518
|
-
async processInput({ input }: { input:
|
|
1696
|
+
async processInput({ input }: { input: RpgMovementInput }) {
|
|
1519
1697
|
if (this.stopProcessingInput) return;
|
|
1520
1698
|
|
|
1521
1699
|
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
@@ -1528,10 +1706,20 @@ export class RpgClientEngine<T = any> {
|
|
|
1528
1706
|
}
|
|
1529
1707
|
|
|
1530
1708
|
const timestamp = Date.now();
|
|
1709
|
+
const movementInput = isDashInput(input)
|
|
1710
|
+
? normalizeDashInput(input, currentPlayer?.direction?.())
|
|
1711
|
+
: input;
|
|
1712
|
+
if (!movementInput) return;
|
|
1713
|
+
if (isDashInput(movementInput)) {
|
|
1714
|
+
const cooldown = movementInput.cooldown ?? DEFAULT_DASH_COOLDOWN_MS;
|
|
1715
|
+
if (timestamp < this.dashLockedUntil) return;
|
|
1716
|
+
this.dashLockedUntil = timestamp + cooldown;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1531
1719
|
let frame: number;
|
|
1532
1720
|
let tick: number;
|
|
1533
1721
|
if (this.predictionEnabled && this.prediction) {
|
|
1534
|
-
const meta = this.prediction.recordInput(
|
|
1722
|
+
const meta = this.prediction.recordInput(movementInput, timestamp);
|
|
1535
1723
|
frame = meta.frame;
|
|
1536
1724
|
tick = meta.tick;
|
|
1537
1725
|
} else {
|
|
@@ -1539,12 +1727,11 @@ export class RpgClientEngine<T = any> {
|
|
|
1539
1727
|
tick = this.getPhysicsTick();
|
|
1540
1728
|
}
|
|
1541
1729
|
this.inputFrameCounter = frame;
|
|
1542
|
-
this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
|
|
1730
|
+
this.hooks.callHooks("client-engine-onInput", this, { input: movementInput, playerId: this.playerId }).subscribe();
|
|
1543
1731
|
|
|
1544
1732
|
const bodyReady = this.ensureCurrentPlayerBody();
|
|
1545
1733
|
if (currentPlayer && bodyReady) {
|
|
1546
|
-
|
|
1547
|
-
(this.sceneMap as any).moveBody(currentPlayer, input);
|
|
1734
|
+
this.applyPredictedMovementInput(currentPlayer, movementInput);
|
|
1548
1735
|
if (this.predictionEnabled && this.prediction) {
|
|
1549
1736
|
this.pendingPredictionFrames.push(frame);
|
|
1550
1737
|
if (this.pendingPredictionFrames.length > 240) {
|
|
@@ -1553,8 +1740,21 @@ export class RpgClientEngine<T = any> {
|
|
|
1553
1740
|
}
|
|
1554
1741
|
}
|
|
1555
1742
|
|
|
1556
|
-
this.emitMovePacket(
|
|
1557
|
-
this.lastInputTime =
|
|
1743
|
+
this.emitMovePacket(movementInput, frame, tick, timestamp, true);
|
|
1744
|
+
this.lastInputTime = isDashInput(movementInput)
|
|
1745
|
+
? Date.now() + (movementInput.duration ?? DEFAULT_DASH_DURATION_MS)
|
|
1746
|
+
: Date.now();
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
async processDash(input: Partial<RpgDashInput> = {}) {
|
|
1750
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1751
|
+
const fallbackDirection =
|
|
1752
|
+
typeof currentPlayer?.direction === "function"
|
|
1753
|
+
? currentPlayer.direction()
|
|
1754
|
+
: currentPlayer?.direction;
|
|
1755
|
+
const dashInput = normalizeDashInput(input, fallbackDirection);
|
|
1756
|
+
if (!dashInput) return;
|
|
1757
|
+
await this.processInput({ input: dashInput });
|
|
1558
1758
|
}
|
|
1559
1759
|
|
|
1560
1760
|
processAction(action: RpgActionName, data?: any): void;
|
|
@@ -1730,7 +1930,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1730
1930
|
input: entry.direction,
|
|
1731
1931
|
x: state.x,
|
|
1732
1932
|
y: state.y,
|
|
1733
|
-
direction: state.direction ?? entry.direction,
|
|
1933
|
+
direction: state.direction ?? resolveMoveDirection(entry.direction),
|
|
1734
1934
|
});
|
|
1735
1935
|
}
|
|
1736
1936
|
if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
|
|
@@ -1740,7 +1940,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1740
1940
|
}
|
|
1741
1941
|
|
|
1742
1942
|
private emitMovePacket(
|
|
1743
|
-
input:
|
|
1943
|
+
input: RpgMovementInput,
|
|
1744
1944
|
frame: number,
|
|
1745
1945
|
tick: number,
|
|
1746
1946
|
timestamp: number,
|
|
@@ -1795,6 +1995,22 @@ export class RpgClientEngine<T = any> {
|
|
|
1795
1995
|
this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
|
|
1796
1996
|
}
|
|
1797
1997
|
|
|
1998
|
+
private applyPredictedMovementInput(
|
|
1999
|
+
player: any,
|
|
2000
|
+
input: RpgMovementInput
|
|
2001
|
+
): boolean {
|
|
2002
|
+
if (isDashInput(input)) {
|
|
2003
|
+
const direction = vectorToDirection(input.direction);
|
|
2004
|
+
player.changeDirection(direction);
|
|
2005
|
+
return Boolean((this.sceneMap as any).dashBody?.(player, input));
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
const direction = resolveMoveDirection(input);
|
|
2009
|
+
if (!direction) return false;
|
|
2010
|
+
player.changeDirection(direction);
|
|
2011
|
+
return Boolean((this.sceneMap as any).moveBody?.(player, direction));
|
|
2012
|
+
}
|
|
2013
|
+
|
|
1798
2014
|
private getLocalPlayerState(): PredictionState<Direction> {
|
|
1799
2015
|
const currentPlayer = this.sceneMap?.getCurrentPlayer();
|
|
1800
2016
|
if (!currentPlayer) {
|
|
@@ -1838,7 +2054,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1838
2054
|
? configuredMaxEntries
|
|
1839
2055
|
: Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
|
|
1840
2056
|
this.sceneMap?.configureClientPrediction?.(true);
|
|
1841
|
-
this.prediction = new PredictionController<Direction>({
|
|
2057
|
+
this.prediction = new PredictionController<RpgMovementInput, Direction>({
|
|
1842
2058
|
correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
|
|
1843
2059
|
historyTtlMs,
|
|
1844
2060
|
maxHistoryEntries,
|
|
@@ -2053,7 +2269,7 @@ export class RpgClientEngine<T = any> {
|
|
|
2053
2269
|
|
|
2054
2270
|
private reconcilePrediction(
|
|
2055
2271
|
authoritativeState: PredictionState<Direction>,
|
|
2056
|
-
pendingInputs: PredictionHistoryEntry<Direction>[],
|
|
2272
|
+
pendingInputs: PredictionHistoryEntry<RpgMovementInput, Direction>[],
|
|
2057
2273
|
): void {
|
|
2058
2274
|
const player = this.getCurrentPlayer() as any;
|
|
2059
2275
|
if (!player) {
|
|
@@ -2075,7 +2291,7 @@ export class RpgClientEngine<T = any> {
|
|
|
2075
2291
|
const replayInputs = pendingInputs.slice(-600);
|
|
2076
2292
|
for (const entry of replayInputs) {
|
|
2077
2293
|
if (!entry?.direction) continue;
|
|
2078
|
-
|
|
2294
|
+
this.applyPredictedMovementInput(player, entry.direction);
|
|
2079
2295
|
this.sceneMap.stepPredictionTick();
|
|
2080
2296
|
this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
|
|
2081
2297
|
}
|
|
@@ -2178,7 +2394,16 @@ export class RpgClientEngine<T = any> {
|
|
|
2178
2394
|
if (this.pointerMoveHandler && this.pointerCanvas) {
|
|
2179
2395
|
this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);
|
|
2180
2396
|
this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);
|
|
2397
|
+
if (this.pointerUpHandler) {
|
|
2398
|
+
this.pointerCanvas.removeEventListener('pointerup', this.pointerUpHandler);
|
|
2399
|
+
}
|
|
2400
|
+
if (this.pointerCancelHandler) {
|
|
2401
|
+
this.pointerCanvas.removeEventListener('pointercancel', this.pointerCancelHandler);
|
|
2402
|
+
this.pointerCanvas.removeEventListener('pointerleave', this.pointerCancelHandler);
|
|
2403
|
+
}
|
|
2181
2404
|
this.pointerMoveHandler = undefined;
|
|
2405
|
+
this.pointerUpHandler = undefined;
|
|
2406
|
+
this.pointerCancelHandler = undefined;
|
|
2182
2407
|
this.pointerCanvas = undefined;
|
|
2183
2408
|
}
|
|
2184
2409
|
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
<Container
|
|
1
|
+
<Container
|
|
2
|
+
x={smoothX}
|
|
3
|
+
y={smoothY}
|
|
4
|
+
zIndex={z}
|
|
5
|
+
viewportFollow={shouldFollowCamera}
|
|
6
|
+
controls
|
|
7
|
+
onBeforeDestroy
|
|
8
|
+
visible
|
|
9
|
+
cursor={interactionCursor}
|
|
10
|
+
pointerover={interactionPointerOver}
|
|
11
|
+
pointerout={interactionPointerOut}
|
|
12
|
+
pointerdown={interactionPointerDown}
|
|
13
|
+
pointerup={interactionPointerUp}
|
|
14
|
+
pointermove={interactionPointerMove}
|
|
15
|
+
click={interactionClick}
|
|
16
|
+
>
|
|
2
17
|
@for (compConfig of normalizedComponentsBehind) {
|
|
3
18
|
<Container>
|
|
4
19
|
<compConfig.component object={sprite} ...compConfig.props />
|
|
@@ -34,11 +49,17 @@
|
|
|
34
49
|
<compConfig.component object={sprite} ...compConfig.props />
|
|
35
50
|
</Container>
|
|
36
51
|
}
|
|
52
|
+
<InteractionComponents
|
|
53
|
+
object={sprite}
|
|
54
|
+
bounds={graphicBounds}
|
|
55
|
+
hitboxBounds={hitboxBounds}
|
|
56
|
+
graphicBounds={graphicBounds}
|
|
57
|
+
/>
|
|
37
58
|
@for (attachedGui of attachedGuis) {
|
|
38
59
|
@if (shouldDisplayAttachedGui) {
|
|
39
60
|
<Container>
|
|
40
|
-
<attachedGui.component ...attachedGui.data() dependencies={attachedGui.dependencies} object={sprite} onFinish={(data) => {
|
|
41
|
-
onAttachedGuiFinish(attachedGui, data)
|
|
61
|
+
<attachedGui.component ...attachedGui.data() dependencies={attachedGui.dependencies} object={sprite} guiOpenId={attachedGui.openId} onFinish={(data, guiOpenId) => {
|
|
62
|
+
onAttachedGuiFinish(attachedGui, data, guiOpenId)
|
|
42
63
|
}} onInteraction={(name, data) => {
|
|
43
64
|
onAttachedGuiInteraction(attachedGui, name, data)
|
|
44
65
|
}} />
|
|
@@ -53,13 +74,14 @@
|
|
|
53
74
|
|
|
54
75
|
import { lastValueFrom, combineLatest, pairwise, filter, map, startWith } from "rxjs";
|
|
55
76
|
import { Particle } from "@canvasengine/presets";
|
|
56
|
-
import { GameEngineToken, ModulesToken } from "@rpgjs/common";
|
|
77
|
+
import { GameEngineToken, ModulesToken, shouldRenderLightingShadows } from "@rpgjs/common";
|
|
57
78
|
import { RpgClientEngine } from "../RpgClientEngine";
|
|
58
79
|
import { inject } from "../core/inject";
|
|
59
80
|
import { Direction, Animation } from "@rpgjs/common";
|
|
60
81
|
import { normalizeEventComponent } from "../Game/EventComponentResolver";
|
|
61
82
|
import Hit from "./effects/hit.ce";
|
|
62
83
|
import PlayerComponents from "./player-components.ce";
|
|
84
|
+
import InteractionComponents from "./interaction-components.ce";
|
|
63
85
|
import { RpgGui } from "../Gui/Gui";
|
|
64
86
|
import { getCanMoveValue } from "../utils/readPropValue";
|
|
65
87
|
import {
|
|
@@ -91,7 +113,7 @@
|
|
|
91
113
|
const isMe = computed(isCurrentPlayer);
|
|
92
114
|
const shadowsEnabled = computed(() => {
|
|
93
115
|
const lighting = client.sceneMap?.lighting?.();
|
|
94
|
-
return
|
|
116
|
+
return shouldRenderLightingShadows(lighting);
|
|
95
117
|
});
|
|
96
118
|
|
|
97
119
|
/**
|
|
@@ -352,6 +374,20 @@
|
|
|
352
374
|
return direction();
|
|
353
375
|
};
|
|
354
376
|
|
|
377
|
+
const directionToDashVector = (currentDirection) => {
|
|
378
|
+
switch (currentDirection) {
|
|
379
|
+
case Direction.Left:
|
|
380
|
+
return { x: -1, y: 0 };
|
|
381
|
+
case Direction.Right:
|
|
382
|
+
return { x: 1, y: 0 };
|
|
383
|
+
case Direction.Up:
|
|
384
|
+
return { x: 0, y: -1 };
|
|
385
|
+
case Direction.Down:
|
|
386
|
+
default:
|
|
387
|
+
return { x: 0, y: 1 };
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
355
391
|
const withCurrentDirection = (payload) => {
|
|
356
392
|
if (payload.action !== 'action') return payload;
|
|
357
393
|
const data = payload.data && typeof payload.data === 'object'
|
|
@@ -393,6 +429,13 @@
|
|
|
393
429
|
playPredictedWalkAnimation();
|
|
394
430
|
};
|
|
395
431
|
|
|
432
|
+
const processDashInput = () => {
|
|
433
|
+
if (!canControls()) return;
|
|
434
|
+
client.processDash({
|
|
435
|
+
direction: directionToDashVector(resolveSpriteDirection()),
|
|
436
|
+
});
|
|
437
|
+
};
|
|
438
|
+
|
|
396
439
|
const actionBind = () => getKeyboardControlBind(keyboardControls.action);
|
|
397
440
|
const keyboardEventId = (event) => `${event.keyCode}:${event.code}:${event.key}`;
|
|
398
441
|
|
|
@@ -463,6 +506,12 @@
|
|
|
463
506
|
}
|
|
464
507
|
},
|
|
465
508
|
},
|
|
509
|
+
dash: {
|
|
510
|
+
bind: keyboardControls.dash,
|
|
511
|
+
keyDown() {
|
|
512
|
+
processDashInput()
|
|
513
|
+
},
|
|
514
|
+
},
|
|
466
515
|
escape: {
|
|
467
516
|
bind: keyboardControls.escape,
|
|
468
517
|
keyDown() {
|
|
@@ -512,7 +561,7 @@
|
|
|
512
561
|
}
|
|
513
562
|
|
|
514
563
|
const graphicScale = (graphicObject) => {
|
|
515
|
-
const scale = graphicObject?.scale;
|
|
564
|
+
const scale = graphicObject?.displayScale ?? graphicObject?.scale;
|
|
516
565
|
if (Array.isArray(scale)) return scale;
|
|
517
566
|
if (typeof scale === 'number') return [scale, scale];
|
|
518
567
|
if (scale && typeof scale === 'object') {
|
|
@@ -824,6 +873,31 @@
|
|
|
824
873
|
};
|
|
825
874
|
});
|
|
826
875
|
|
|
876
|
+
const interactionBounds = () => ({
|
|
877
|
+
bounds: graphicBounds(),
|
|
878
|
+
hitbox: hitboxBounds(),
|
|
879
|
+
graphic: graphicBounds()
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
const interactionCursor = computed(() =>
|
|
883
|
+
client.interactions.cursorFor(sprite, interactionBounds())
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const handleInteraction = (type) => (event) => {
|
|
887
|
+
client.updatePointerFromInteractionEvent(event);
|
|
888
|
+
client.interactions.handle(sprite, type, {
|
|
889
|
+
event,
|
|
890
|
+
bounds: interactionBounds()
|
|
891
|
+
});
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const interactionPointerOver = handleInteraction('pointerover');
|
|
895
|
+
const interactionPointerOut = handleInteraction('pointerout');
|
|
896
|
+
const interactionPointerDown = handleInteraction('pointerdown');
|
|
897
|
+
const interactionPointerUp = handleInteraction('pointerup');
|
|
898
|
+
const interactionPointerMove = handleInteraction('pointermove');
|
|
899
|
+
const interactionClick = handleInteraction('click');
|
|
900
|
+
|
|
827
901
|
// Combine animation change detection with movement state from smoothX/smoothY
|
|
828
902
|
const movementAnimations = ['walk', 'stand'];
|
|
829
903
|
const epsilon = 0; // movement threshold to consider the easing still running
|
|
@@ -897,7 +971,8 @@
|
|
|
897
971
|
const isTemporaryAnimationPlaying =
|
|
898
972
|
sprite.animationIsPlaying && sprite.animationIsPlaying();
|
|
899
973
|
|
|
900
|
-
if (sprite.animationFixed && isMovementAnimation
|
|
974
|
+
if (sprite.animationFixed && isMovementAnimation) {
|
|
975
|
+
realAnimationName.set(curr);
|
|
901
976
|
return;
|
|
902
977
|
}
|
|
903
978
|
|
|
@@ -1001,8 +1076,16 @@
|
|
|
1001
1076
|
* @param gui - The GUI instance
|
|
1002
1077
|
* @param data - Data passed from the GUI component
|
|
1003
1078
|
*/
|
|
1004
|
-
const
|
|
1005
|
-
|
|
1079
|
+
const normalizeOpenId = (value) => {
|
|
1080
|
+
const resolved = typeof value === "function" ? value() : value;
|
|
1081
|
+
return typeof resolved === "string" && resolved.length > 0 ? resolved : undefined;
|
|
1082
|
+
};
|
|
1083
|
+
|
|
1084
|
+
const onAttachedGuiFinish = (gui, data, guiOpenId) => {
|
|
1085
|
+
const completedOpenId = normalizeOpenId(guiOpenId);
|
|
1086
|
+
const currentOpenId = normalizeOpenId(gui.openId);
|
|
1087
|
+
if (completedOpenId && currentOpenId && completedOpenId !== currentOpenId) return;
|
|
1088
|
+
guiService.guiClose(gui.name, data, completedOpenId ?? currentOpenId);
|
|
1006
1089
|
};
|
|
1007
1090
|
|
|
1008
1091
|
/**
|