@rpgjs/client 5.0.0-beta.12 → 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 +10 -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 +180 -17
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/character.ce.js +82 -7
- 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 +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/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 +248 -29
- package/src/components/character.ce +90 -7
- 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 +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/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 {
|
|
@@ -883,17 +1043,29 @@ export class RpgClientEngine<T = any> {
|
|
|
883
1043
|
|
|
884
1044
|
// If not in cache and resolver exists, use it
|
|
885
1045
|
if (this.spritesheetResolver) {
|
|
1046
|
+
if (this.spritesheetPromises.has(id)) {
|
|
1047
|
+
return this.spritesheetPromises.get(id);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
886
1050
|
const result = this.spritesheetResolver(id);
|
|
887
1051
|
|
|
888
1052
|
// Check if result is a Promise
|
|
889
1053
|
if (result instanceof Promise) {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
1054
|
+
const promise = result
|
|
1055
|
+
.then((spritesheet) => {
|
|
1056
|
+
if (spritesheet) {
|
|
1057
|
+
// Cache the resolved spritesheet
|
|
1058
|
+
this.spritesheets.set(id, spritesheet);
|
|
1059
|
+
}
|
|
1060
|
+
this.spritesheetPromises.delete(id);
|
|
1061
|
+
return spritesheet;
|
|
1062
|
+
})
|
|
1063
|
+
.catch((error) => {
|
|
1064
|
+
this.spritesheetPromises.delete(id);
|
|
1065
|
+
throw error;
|
|
1066
|
+
});
|
|
1067
|
+
this.spritesheetPromises.set(id, promise);
|
|
1068
|
+
return promise;
|
|
897
1069
|
} else {
|
|
898
1070
|
// Synchronous result
|
|
899
1071
|
if (result) {
|
|
@@ -1515,7 +1687,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1515
1687
|
});
|
|
1516
1688
|
}
|
|
1517
1689
|
|
|
1518
|
-
async processInput({ input }: { input:
|
|
1690
|
+
async processInput({ input }: { input: RpgMovementInput }) {
|
|
1519
1691
|
if (this.stopProcessingInput) return;
|
|
1520
1692
|
|
|
1521
1693
|
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
@@ -1528,10 +1700,20 @@ export class RpgClientEngine<T = any> {
|
|
|
1528
1700
|
}
|
|
1529
1701
|
|
|
1530
1702
|
const timestamp = Date.now();
|
|
1703
|
+
const movementInput = isDashInput(input)
|
|
1704
|
+
? normalizeDashInput(input, currentPlayer?.direction?.())
|
|
1705
|
+
: input;
|
|
1706
|
+
if (!movementInput) return;
|
|
1707
|
+
if (isDashInput(movementInput)) {
|
|
1708
|
+
const cooldown = movementInput.cooldown ?? DEFAULT_DASH_COOLDOWN_MS;
|
|
1709
|
+
if (timestamp < this.dashLockedUntil) return;
|
|
1710
|
+
this.dashLockedUntil = timestamp + cooldown;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1531
1713
|
let frame: number;
|
|
1532
1714
|
let tick: number;
|
|
1533
1715
|
if (this.predictionEnabled && this.prediction) {
|
|
1534
|
-
const meta = this.prediction.recordInput(
|
|
1716
|
+
const meta = this.prediction.recordInput(movementInput, timestamp);
|
|
1535
1717
|
frame = meta.frame;
|
|
1536
1718
|
tick = meta.tick;
|
|
1537
1719
|
} else {
|
|
@@ -1539,12 +1721,11 @@ export class RpgClientEngine<T = any> {
|
|
|
1539
1721
|
tick = this.getPhysicsTick();
|
|
1540
1722
|
}
|
|
1541
1723
|
this.inputFrameCounter = frame;
|
|
1542
|
-
this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
|
|
1724
|
+
this.hooks.callHooks("client-engine-onInput", this, { input: movementInput, playerId: this.playerId }).subscribe();
|
|
1543
1725
|
|
|
1544
1726
|
const bodyReady = this.ensureCurrentPlayerBody();
|
|
1545
1727
|
if (currentPlayer && bodyReady) {
|
|
1546
|
-
|
|
1547
|
-
(this.sceneMap as any).moveBody(currentPlayer, input);
|
|
1728
|
+
this.applyPredictedMovementInput(currentPlayer, movementInput);
|
|
1548
1729
|
if (this.predictionEnabled && this.prediction) {
|
|
1549
1730
|
this.pendingPredictionFrames.push(frame);
|
|
1550
1731
|
if (this.pendingPredictionFrames.length > 240) {
|
|
@@ -1553,8 +1734,21 @@ export class RpgClientEngine<T = any> {
|
|
|
1553
1734
|
}
|
|
1554
1735
|
}
|
|
1555
1736
|
|
|
1556
|
-
this.emitMovePacket(
|
|
1557
|
-
this.lastInputTime =
|
|
1737
|
+
this.emitMovePacket(movementInput, frame, tick, timestamp, true);
|
|
1738
|
+
this.lastInputTime = isDashInput(movementInput)
|
|
1739
|
+
? Date.now() + (movementInput.duration ?? DEFAULT_DASH_DURATION_MS)
|
|
1740
|
+
: Date.now();
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
async processDash(input: Partial<RpgDashInput> = {}) {
|
|
1744
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1745
|
+
const fallbackDirection =
|
|
1746
|
+
typeof currentPlayer?.direction === "function"
|
|
1747
|
+
? currentPlayer.direction()
|
|
1748
|
+
: currentPlayer?.direction;
|
|
1749
|
+
const dashInput = normalizeDashInput(input, fallbackDirection);
|
|
1750
|
+
if (!dashInput) return;
|
|
1751
|
+
await this.processInput({ input: dashInput });
|
|
1558
1752
|
}
|
|
1559
1753
|
|
|
1560
1754
|
processAction(action: RpgActionName, data?: any): void;
|
|
@@ -1730,7 +1924,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1730
1924
|
input: entry.direction,
|
|
1731
1925
|
x: state.x,
|
|
1732
1926
|
y: state.y,
|
|
1733
|
-
direction: state.direction ?? entry.direction,
|
|
1927
|
+
direction: state.direction ?? resolveMoveDirection(entry.direction),
|
|
1734
1928
|
});
|
|
1735
1929
|
}
|
|
1736
1930
|
if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
|
|
@@ -1740,7 +1934,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1740
1934
|
}
|
|
1741
1935
|
|
|
1742
1936
|
private emitMovePacket(
|
|
1743
|
-
input:
|
|
1937
|
+
input: RpgMovementInput,
|
|
1744
1938
|
frame: number,
|
|
1745
1939
|
tick: number,
|
|
1746
1940
|
timestamp: number,
|
|
@@ -1795,6 +1989,22 @@ export class RpgClientEngine<T = any> {
|
|
|
1795
1989
|
this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
|
|
1796
1990
|
}
|
|
1797
1991
|
|
|
1992
|
+
private applyPredictedMovementInput(
|
|
1993
|
+
player: any,
|
|
1994
|
+
input: RpgMovementInput
|
|
1995
|
+
): boolean {
|
|
1996
|
+
if (isDashInput(input)) {
|
|
1997
|
+
const direction = vectorToDirection(input.direction);
|
|
1998
|
+
player.changeDirection(direction);
|
|
1999
|
+
return Boolean((this.sceneMap as any).dashBody?.(player, input));
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
const direction = resolveMoveDirection(input);
|
|
2003
|
+
if (!direction) return false;
|
|
2004
|
+
player.changeDirection(direction);
|
|
2005
|
+
return Boolean((this.sceneMap as any).moveBody?.(player, direction));
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1798
2008
|
private getLocalPlayerState(): PredictionState<Direction> {
|
|
1799
2009
|
const currentPlayer = this.sceneMap?.getCurrentPlayer();
|
|
1800
2010
|
if (!currentPlayer) {
|
|
@@ -1838,7 +2048,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1838
2048
|
? configuredMaxEntries
|
|
1839
2049
|
: Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
|
|
1840
2050
|
this.sceneMap?.configureClientPrediction?.(true);
|
|
1841
|
-
this.prediction = new PredictionController<Direction>({
|
|
2051
|
+
this.prediction = new PredictionController<RpgMovementInput, Direction>({
|
|
1842
2052
|
correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
|
|
1843
2053
|
historyTtlMs,
|
|
1844
2054
|
maxHistoryEntries,
|
|
@@ -2053,7 +2263,7 @@ export class RpgClientEngine<T = any> {
|
|
|
2053
2263
|
|
|
2054
2264
|
private reconcilePrediction(
|
|
2055
2265
|
authoritativeState: PredictionState<Direction>,
|
|
2056
|
-
pendingInputs: PredictionHistoryEntry<Direction>[],
|
|
2266
|
+
pendingInputs: PredictionHistoryEntry<RpgMovementInput, Direction>[],
|
|
2057
2267
|
): void {
|
|
2058
2268
|
const player = this.getCurrentPlayer() as any;
|
|
2059
2269
|
if (!player) {
|
|
@@ -2075,7 +2285,7 @@ export class RpgClientEngine<T = any> {
|
|
|
2075
2285
|
const replayInputs = pendingInputs.slice(-600);
|
|
2076
2286
|
for (const entry of replayInputs) {
|
|
2077
2287
|
if (!entry?.direction) continue;
|
|
2078
|
-
|
|
2288
|
+
this.applyPredictedMovementInput(player, entry.direction);
|
|
2079
2289
|
this.sceneMap.stepPredictionTick();
|
|
2080
2290
|
this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
|
|
2081
2291
|
}
|
|
@@ -2178,7 +2388,16 @@ export class RpgClientEngine<T = any> {
|
|
|
2178
2388
|
if (this.pointerMoveHandler && this.pointerCanvas) {
|
|
2179
2389
|
this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);
|
|
2180
2390
|
this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);
|
|
2391
|
+
if (this.pointerUpHandler) {
|
|
2392
|
+
this.pointerCanvas.removeEventListener('pointerup', this.pointerUpHandler);
|
|
2393
|
+
}
|
|
2394
|
+
if (this.pointerCancelHandler) {
|
|
2395
|
+
this.pointerCanvas.removeEventListener('pointercancel', this.pointerCancelHandler);
|
|
2396
|
+
this.pointerCanvas.removeEventListener('pointerleave', this.pointerCancelHandler);
|
|
2397
|
+
}
|
|
2181
2398
|
this.pointerMoveHandler = undefined;
|
|
2399
|
+
this.pointerUpHandler = undefined;
|
|
2400
|
+
this.pointerCancelHandler = undefined;
|
|
2182
2401
|
this.pointerCanvas = undefined;
|
|
2183
2402
|
}
|
|
2184
2403
|
|
|
@@ -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
|
}} />
|
|
@@ -60,6 +81,7 @@
|
|
|
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 {
|
|
@@ -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
|
/**
|