@rpgjs/client 5.0.0-beta.15 → 5.0.0-beta.16
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/RpgClientEngine.d.ts +10 -0
- package/dist/RpgClientEngine.js +69 -2
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/services/AbstractSocket.d.ts +2 -0
- package/dist/services/AbstractSocket.js.map +1 -1
- package/dist/services/mmorpg.d.ts +1 -0
- package/dist/services/mmorpg.js +1 -0
- package/dist/services/mmorpg.js.map +1 -1
- package/dist/services/standalone.d.ts +1 -0
- package/dist/services/standalone.js +1 -0
- package/dist/services/standalone.js.map +1 -1
- package/package.json +4 -4
- package/src/RpgClientEngine.ts +119 -3
- package/src/services/AbstractSocket.ts +3 -0
- package/src/services/mmorpg.ts +2 -0
- package/src/services/standalone.spec.ts +20 -0
- package/src/services/standalone.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @rpgjs/client
|
|
2
2
|
|
|
3
|
+
## 5.0.0-beta.16
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Release the next RPGJS beta with terrain rendering performance improvements and a unified server tick loop.
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @rpgjs/common@5.0.0-beta.15
|
|
10
|
+
- @rpgjs/server@5.0.0-beta.16
|
|
11
|
+
- @rpgjs/ui-css@5.0.0-beta.13
|
|
12
|
+
|
|
3
13
|
## 5.0.0-beta.15
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
|
@@ -66,6 +66,9 @@ export declare class RpgClientEngine<T = any> {
|
|
|
66
66
|
private predictionEnabled;
|
|
67
67
|
private prediction?;
|
|
68
68
|
private readonly SERVER_CORRECTION_THRESHOLD;
|
|
69
|
+
private localMovementAuthority;
|
|
70
|
+
private lastLocalMovementInputAt;
|
|
71
|
+
private readonly LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS;
|
|
69
72
|
private inputFrameCounter;
|
|
70
73
|
private pendingPredictionFrames;
|
|
71
74
|
private lastClientPhysicsStepAt;
|
|
@@ -103,6 +106,7 @@ export declare class RpgClientEngine<T = any> {
|
|
|
103
106
|
private i18nService;
|
|
104
107
|
private locale?;
|
|
105
108
|
constructor(context: any);
|
|
109
|
+
private resolveLocalMovementAuthority;
|
|
106
110
|
setLocale(locale: string): void;
|
|
107
111
|
getLocale(): string;
|
|
108
112
|
t(key: string, params?: I18nParams): string;
|
|
@@ -145,11 +149,17 @@ export declare class RpgClientEngine<T = any> {
|
|
|
145
149
|
*/
|
|
146
150
|
setKeyboardControls(controlInstance: any): void;
|
|
147
151
|
start(): Promise<void>;
|
|
152
|
+
private installCanvasResizeGuard;
|
|
153
|
+
private readCanvasResizeTargetSize;
|
|
154
|
+
private readCanvasRendererSize;
|
|
155
|
+
private cancelCanvasResizeFrame;
|
|
148
156
|
private resolveSceneMapComponent;
|
|
149
157
|
private setupPointerTracking;
|
|
150
158
|
updatePointerFromInteractionEvent(event: any): void;
|
|
151
159
|
private findViewportInstance;
|
|
152
160
|
private prepareSyncPayload;
|
|
161
|
+
private shouldPreserveLocalPlayerPosition;
|
|
162
|
+
private shouldKeepLocalPlayerMovement;
|
|
153
163
|
private normalizeAckWithSyncState;
|
|
154
164
|
private initListeners;
|
|
155
165
|
private beginMapTransfer;
|
package/dist/RpgClientEngine.js
CHANGED
|
@@ -111,6 +111,9 @@ var RpgClientEngine = class {
|
|
|
111
111
|
this.gamePause = signal(false);
|
|
112
112
|
this.predictionEnabled = false;
|
|
113
113
|
this.SERVER_CORRECTION_THRESHOLD = 30;
|
|
114
|
+
this.localMovementAuthority = false;
|
|
115
|
+
this.lastLocalMovementInputAt = 0;
|
|
116
|
+
this.LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS = 250;
|
|
114
117
|
this.inputFrameCounter = 0;
|
|
115
118
|
this.pendingPredictionFrames = [];
|
|
116
119
|
this.lastClientPhysicsStepAt = 0;
|
|
@@ -164,8 +167,16 @@ var RpgClientEngine = class {
|
|
|
164
167
|
this.registerSpriteComponent("rpg:shape", __ce_component$4);
|
|
165
168
|
this.registerSpriteComponent("rpg:image", __ce_component$5);
|
|
166
169
|
this.predictionEnabled = this.globalConfig?.prediction?.enabled !== false;
|
|
170
|
+
this.localMovementAuthority = this.resolveLocalMovementAuthority();
|
|
167
171
|
this.initializePredictionController();
|
|
168
172
|
}
|
|
173
|
+
resolveLocalMovementAuthority() {
|
|
174
|
+
const predictionConfig = this.globalConfig?.prediction;
|
|
175
|
+
const configured = this.globalConfig?.movementAuthority ?? predictionConfig?.movementAuthority ?? predictionConfig?.authority ?? predictionConfig?.mode;
|
|
176
|
+
if (configured === "server" || configured === "network" || configured === false) return false;
|
|
177
|
+
if (configured === "client" || configured === "local" || configured === true) return true;
|
|
178
|
+
return this.webSocket.mode === "standalone";
|
|
179
|
+
}
|
|
169
180
|
setLocale(locale) {
|
|
170
181
|
this.locale = locale;
|
|
171
182
|
}
|
|
@@ -240,6 +251,7 @@ var RpgClientEngine = class {
|
|
|
240
251
|
this.selector = document.body.querySelector("#rpg");
|
|
241
252
|
const bootstrapOptions = this.globalConfig?.bootstrapCanvasOptions;
|
|
242
253
|
const { app, canvasElement } = await bootstrapCanvas(this.selector, __ce_component, bootstrapOptions);
|
|
254
|
+
this.installCanvasResizeGuard(app);
|
|
243
255
|
this.canvasApp = app;
|
|
244
256
|
this.canvasElement = canvasElement;
|
|
245
257
|
this.renderer = app.renderer;
|
|
@@ -289,6 +301,45 @@ var RpgClientEngine = class {
|
|
|
289
301
|
this.tickSubscriptions.push(tickSubscription);
|
|
290
302
|
this.startPingPong();
|
|
291
303
|
}
|
|
304
|
+
installCanvasResizeGuard(app) {
|
|
305
|
+
if (!app || typeof app.resize !== "function") return;
|
|
306
|
+
const originalResize = app.resize.bind(app);
|
|
307
|
+
app.resize = () => {
|
|
308
|
+
const targetSize = this.readCanvasResizeTargetSize(app);
|
|
309
|
+
const rendererSize = this.readCanvasRendererSize(app);
|
|
310
|
+
if (targetSize && rendererSize && targetSize.width === rendererSize.width && targetSize.height === rendererSize.height) {
|
|
311
|
+
this.cancelCanvasResizeFrame(app);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
originalResize();
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
readCanvasResizeTargetSize(app) {
|
|
318
|
+
const resizeTarget = app?.resizeTo;
|
|
319
|
+
if (!resizeTarget || typeof window === "undefined") return null;
|
|
320
|
+
const rawWidth = resizeTarget === window ? window.innerWidth : resizeTarget.clientWidth;
|
|
321
|
+
const rawHeight = resizeTarget === window ? window.innerHeight : resizeTarget.clientHeight;
|
|
322
|
+
const width = Math.round(Number(rawWidth));
|
|
323
|
+
const height = Math.round(Number(rawHeight));
|
|
324
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) return null;
|
|
325
|
+
return {
|
|
326
|
+
width,
|
|
327
|
+
height
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
readCanvasRendererSize(app) {
|
|
331
|
+
const screen = app?.renderer?.screen;
|
|
332
|
+
const width = Math.round(Number(screen?.width));
|
|
333
|
+
const height = Math.round(Number(screen?.height));
|
|
334
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
335
|
+
return {
|
|
336
|
+
width,
|
|
337
|
+
height
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
cancelCanvasResizeFrame(app) {
|
|
341
|
+
if (typeof app?._cancelResize === "function") app._cancelResize();
|
|
342
|
+
}
|
|
292
343
|
resolveSceneMapComponent() {
|
|
293
344
|
const components = this.hooks.getHookFunctions("client-sceneMap-component");
|
|
294
345
|
const component = components[components.length - 1];
|
|
@@ -382,7 +433,8 @@ var RpgClientEngine = class {
|
|
|
382
433
|
delete payload.timestamp;
|
|
383
434
|
const myId = this.playerIdSignal();
|
|
384
435
|
const players = payload.players;
|
|
385
|
-
|
|
436
|
+
const localPatch = myId && players ? players[myId] : void 0;
|
|
437
|
+
if (this.shouldPreserveLocalPlayerPosition(localPatch) && myId && players && players[myId]) {
|
|
386
438
|
const localPatch = { ...players[myId] };
|
|
387
439
|
delete localPatch.x;
|
|
388
440
|
delete localPatch.y;
|
|
@@ -395,6 +447,18 @@ var RpgClientEngine = class {
|
|
|
395
447
|
}
|
|
396
448
|
return payload;
|
|
397
449
|
}
|
|
450
|
+
shouldPreserveLocalPlayerPosition(localPatch) {
|
|
451
|
+
if (!localPatch) return false;
|
|
452
|
+
if (this.predictionEnabled && !!this.prediction?.hasPendingInputs()) return true;
|
|
453
|
+
return this.shouldKeepLocalPlayerMovement();
|
|
454
|
+
}
|
|
455
|
+
shouldKeepLocalPlayerMovement() {
|
|
456
|
+
if (!this.localMovementAuthority || this.mapTransitionInProgress) return false;
|
|
457
|
+
const myId = this.playerIdSignal();
|
|
458
|
+
if (!(myId ? this.sceneMap?.players?.()?.[myId] : void 0)) return false;
|
|
459
|
+
if (this.prediction?.hasPendingInputs()) return true;
|
|
460
|
+
return Date.now() - this.lastLocalMovementInputAt <= this.LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS;
|
|
461
|
+
}
|
|
398
462
|
normalizeAckWithSyncState(ack, syncData) {
|
|
399
463
|
const myId = this.playerIdSignal();
|
|
400
464
|
if (!myId) return ack;
|
|
@@ -1321,6 +1385,7 @@ var RpgClientEngine = class {
|
|
|
1321
1385
|
if (timestamp < this.dashLockedUntil) return;
|
|
1322
1386
|
this.dashLockedUntil = timestamp + cooldown;
|
|
1323
1387
|
}
|
|
1388
|
+
this.lastLocalMovementInputAt = timestamp;
|
|
1324
1389
|
let frame;
|
|
1325
1390
|
let tick;
|
|
1326
1391
|
if (this.predictionEnabled && this.prediction) {
|
|
@@ -1693,11 +1758,12 @@ var RpgClientEngine = class {
|
|
|
1693
1758
|
}
|
|
1694
1759
|
applyServerAck(ack) {
|
|
1695
1760
|
this.updateServerTickEstimate(ack.serverTick);
|
|
1761
|
+
const keepLocalMovement = this.shouldKeepLocalPlayerMovement();
|
|
1696
1762
|
if (this.predictionEnabled && this.prediction) {
|
|
1697
1763
|
const result = this.prediction.applyServerAck({
|
|
1698
1764
|
frame: ack.frame,
|
|
1699
1765
|
serverTick: ack.serverTick,
|
|
1700
|
-
state: typeof ack.x === "number" && typeof ack.y === "number" ? {
|
|
1766
|
+
state: !keepLocalMovement && typeof ack.x === "number" && typeof ack.y === "number" ? {
|
|
1701
1767
|
x: ack.x,
|
|
1702
1768
|
y: ack.y,
|
|
1703
1769
|
direction: ack.direction
|
|
@@ -1707,6 +1773,7 @@ var RpgClientEngine = class {
|
|
|
1707
1773
|
return;
|
|
1708
1774
|
}
|
|
1709
1775
|
if (typeof ack.x !== "number" || typeof ack.y !== "number") return;
|
|
1776
|
+
if (keepLocalMovement) return;
|
|
1710
1777
|
const player = this.getCurrentPlayer();
|
|
1711
1778
|
const myId = this.playerIdSignal();
|
|
1712
1779
|
if (!player || !myId) return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RpgClientEngine.js","names":[],"sources":["../src/RpgClientEngine.ts"],"sourcesContent":["import Canvas from \"./components/scenes/canvas.ce\";\nimport BuiltinSceneMap from \"./components/scenes/draw-map.ce\";\nimport { inject } from './core/inject'\nimport { signal, bootstrapCanvas, Howl, trigger, type Trigger } from \"canvasengine\";\nimport { AbstractWebsocket, WebSocketToken } from \"./services/AbstractSocket\";\nimport { LoadMapService, LoadMapToken } from \"./services/loadMap\";\nimport { RpgSound } from \"./Sound\";\nimport { RpgResource } from \"./Resource\";\nimport { getOrCreateI18nService, Hooks, ModulesToken, Direction, normalizeLightingState, Vector2, type I18nParams, type I18nService } from \"@rpgjs/common\";\nimport type { EventComponentConfig } from \"./RpgClient\";\nimport type { RpgClientEvent } from \"./Game/Event\";\nimport { load } from \"@signe/sync\";\nimport { RpgClientMap } from \"./Game/Map\"\nimport { RpgGui } from \"./Gui/Gui\";\nimport { AnimationManager } from \"./Game/AnimationManager\";\nimport { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, switchMap, take } from \"rxjs\";\nimport { GlobalConfigToken } from \"./module\";\nimport * as PIXI from \"pixi.js\";\nimport { PrebuiltComponentAnimations } from \"./components/animations\";\nimport TextComponent from \"./components/dynamics/text.ce\";\nimport BarComponent from \"./components/dynamics/bar.ce\";\nimport ShapeComponent from \"./components/dynamics/shape.ce\";\nimport ImageComponent from \"./components/dynamics/image.ce\";\nimport {\n PredictionController,\n type PredictionHistoryEntry,\n type PredictionState,\n type RpgActionInput,\n type RpgActionName,\n type RpgDashInput,\n type RpgMovementInput,\n} from \"@rpgjs/common\";\nimport { NotificationManager } from \"./Gui/NotificationManager\";\nimport { SaveClientService } from \"./services/save\";\nimport { getCanMoveValue } from \"./utils/readPropValue\";\nimport { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSpawn } from \"./Game/ProjectileManager\";\nimport { ClientVisualRegistry, type ClientVisualHandler, type ClientVisualMap, type ClientVisualPacket } from \"./Game/ClientVisuals\";\nimport { normalizeActionInput } from \"./services/actionInput\";\nimport { createClientPointerContext, type ClientPointerContext } from \"./services/pointerContext\";\nimport { RpgClientInteractions } from \"./services/interactions\";\nimport { normalizeRoomMapId } from \"./utils/mapId\";\nimport { EventComponentResolverRegistry, type EventComponentResolver } from \"./Game/EventComponentResolver\";\nimport { RpgClientBuiltinI18n } from \"./i18n\";\n\ninterface MovementTrajectoryPoint {\n frame: number;\n tick: number;\n timestamp: number;\n input: RpgMovementInput;\n x: number;\n y: number;\n direction?: Direction;\n}\n\nconst DEFAULT_DASH_ADDITIONAL_SPEED = 8;\nconst DEFAULT_DASH_DURATION_MS = 180;\nconst DEFAULT_DASH_COOLDOWN_MS = 450;\n\nconst isDashInput = (input: RpgMovementInput): input is RpgDashInput =>\n typeof input === \"object\" && input !== null && input.type === \"dash\";\n\nconst isMoveInput = (\n input: RpgMovementInput\n): input is { type: \"move\"; direction: Direction } =>\n typeof input === \"object\" && input !== null && input.type === \"move\";\n\nconst resolveMoveDirection = (input: RpgMovementInput): Direction | undefined => {\n if (isMoveInput(input)) return input.direction;\n if (typeof input === \"string\" || typeof input === \"number\") {\n return input as Direction;\n }\n return undefined;\n};\n\nconst directionToVector = (direction: Direction | undefined) => {\n switch (direction) {\n case Direction.Left:\n return { x: -1, y: 0 };\n case Direction.Right:\n return { x: 1, y: 0 };\n case Direction.Up:\n return { x: 0, y: -1 };\n case Direction.Down:\n default:\n return { x: 0, y: 1 };\n }\n};\n\nconst vectorToDirection = (direction: { x: number; y: number }): Direction => {\n if (Math.abs(direction.x) > Math.abs(direction.y)) {\n return direction.x < 0 ? Direction.Left : Direction.Right;\n }\n return direction.y < 0 ? Direction.Up : Direction.Down;\n};\n\nconst normalizeDashInput = (\n input: Partial<RpgDashInput>,\n fallbackDirection: Direction | undefined\n): RpgDashInput | null => {\n const rawDirection = input.direction ?? directionToVector(fallbackDirection);\n const rawX = Number(rawDirection?.x ?? 0);\n const rawY = Number(rawDirection?.y ?? 0);\n const magnitude = Math.hypot(rawX, rawY);\n if (!Number.isFinite(magnitude) || magnitude <= 0) return null;\n\n const additionalSpeed =\n typeof input.additionalSpeed === \"number\" && Number.isFinite(input.additionalSpeed)\n ? Math.max(0, Math.min(input.additionalSpeed, 64))\n : DEFAULT_DASH_ADDITIONAL_SPEED;\n const duration =\n typeof input.duration === \"number\" && Number.isFinite(input.duration)\n ? Math.max(1, Math.min(input.duration, 1000))\n : DEFAULT_DASH_DURATION_MS;\n const cooldown =\n typeof input.cooldown === \"number\" && Number.isFinite(input.cooldown)\n ? Math.max(0, Math.min(input.cooldown, 5000))\n : DEFAULT_DASH_COOLDOWN_MS;\n\n return {\n type: \"dash\",\n direction: {\n x: rawX / magnitude,\n y: rawY / magnitude,\n },\n additionalSpeed,\n duration,\n cooldown,\n };\n};\n\ntype ConfigurableTrigger<T> = Omit<Trigger<T>, \"start\"> & {\n start(config?: T): Promise<void>;\n};\n\ntype MapShakeOptions = {\n intensity?: number;\n duration?: number;\n frequency?: number;\n direction?: string;\n};\n\nexport class RpgClientEngine<T = any> {\n private guiService: RpgGui;\n private webSocket: AbstractWebsocket;\n private loadMapService: LoadMapService;\n private hooks: Hooks;\n private sceneMap: RpgClientMap\n private selector: HTMLElement;\n public globalConfig: T;\n public sceneComponent: any;\n public sceneMapComponent: any = BuiltinSceneMap;\n stopProcessingInput = false;\n width = signal(\"100%\");\n height = signal(\"100%\");\n spritesheets: Map<string | number, any> = new Map();\n private spritesheetPromises: Map<string | number, Promise<any>> = new Map();\n sounds: Map<string, any> = new Map();\n componentAnimations: any[] = [];\n clientVisuals = new ClientVisualRegistry();\n projectiles: ProjectileManager;\n pointer: ClientPointerContext = createClientPointerContext();\n interactions: RpgClientInteractions = new RpgClientInteractions(this);\n private spritesheetResolver?: (id: string | number) => any | Promise<any>;\n private soundResolver?: (id: string) => any | Promise<any>;\n particleSettings: {\n emitters: any[]\n } = {\n emitters: []\n }\n renderer: PIXI.Renderer;\n tick: Observable<number>;\n private canvasApp?: any;\n private canvasElement?: any;\n playerIdSignal = signal<string | null>(null);\n spriteComponentsBehind = signal<any[]>([]);\n spriteComponentsInFront = signal<any[]>([]);\n spriteComponents: Map<string, any> = new Map();\n private eventComponentResolvers = new EventComponentResolverRegistry();\n /** ID of the sprite that the camera should follow. null means follow the current player */\n cameraFollowTargetId = signal<string | null>(null);\n /** Trigger for map shake animation */\n mapShakeTrigger: ConfigurableTrigger<MapShakeOptions> = trigger<MapShakeOptions>();\n\n controlsReady = signal(undefined); \n gamePause = signal(false);\n\n private predictionEnabled = false;\n private prediction?: PredictionController<RpgMovementInput, Direction>;\n private readonly SERVER_CORRECTION_THRESHOLD = 30;\n private inputFrameCounter = 0;\n private pendingPredictionFrames: number[] = [];\n private lastClientPhysicsStepAt = 0;\n private frameOffset = 0;\n private latestServerTick?: number;\n private latestServerTickAt = 0;\n private dashLockedUntil = 0;\n // Ping/Pong for RTT measurement\n private rtt: number = 0; // Round-trip time in ms\n private pingInterval: any = null;\n private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds\n private lastInputTime = 0;\n private readonly MOVE_PATH_RESEND_INTERVAL_MS = 120;\n private readonly MAX_MOVE_TRAJECTORY_POINTS = 240;\n private lastMovePathSentAt = 0;\n private lastMovePathSentFrame = 0;\n // Track map loading state for onAfterLoading hook using RxJS\n private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);\n private playerIdReceived$ = new BehaviorSubject<boolean>(false);\n private playersReceived$ = new BehaviorSubject<boolean>(false);\n private eventsReceived$ = new BehaviorSubject<boolean>(false);\n private onAfterLoadingSubscription?: any;\n private sceneResetQueued = false;\n private mapTransitionInProgress = false;\n private currentMapRoomId?: string;\n private socketListenersInitialized = false;\n private clientReadyForMapChanges = false;\n private pendingMapChanges: any[] = [];\n \n // Store subscriptions and event listeners for cleanup\n private tickSubscriptions: any[] = [];\n private resizeHandler?: () => void;\n private pointerMoveHandler?: (event: PointerEvent) => void;\n private pointerUpHandler?: (event: PointerEvent) => void;\n private pointerCancelHandler?: (event: PointerEvent) => void;\n private pointerCanvas?: HTMLCanvasElement;\n private pendingSyncPackets: any[] = [];\n private notificationManager: NotificationManager = new NotificationManager();\n private i18nService: I18nService;\n private locale?: string;\n\n constructor(public context) {\n this.webSocket = inject(WebSocketToken);\n this.guiService = inject(RpgGui);\n this.loadMapService = inject(LoadMapToken);\n this.hooks = inject<Hooks>(ModulesToken);\n this.i18nService = getOrCreateI18nService(context);\n this.i18nService.addMessages(RpgClientBuiltinI18n, \"rpgjs-client\", 0);\n this.projectiles = new ProjectileManager(\n this.hooks,\n (projectile) => this.predictProjectileImpact(projectile),\n );\n this.globalConfig = inject(GlobalConfigToken)\n\n if (!this.globalConfig) {\n this.globalConfig = {} as T\n }\n if (!(this.globalConfig as any).box) {\n (this.globalConfig as any).box = {\n styles: {\n backgroundColor: \"#1a1a2e\",\n backgroundOpacity: 0.9\n },\n sounds: {}\n }\n }\n\n this.addComponentAnimation({\n id: \"animation\",\n component: PrebuiltComponentAnimations.Animation\n })\n\n this.registerSpriteComponent(\"rpg:text\", TextComponent);\n this.registerSpriteComponent(\"rpg:hpBar\", BarComponent);\n this.registerSpriteComponent(\"rpg:spBar\", BarComponent);\n this.registerSpriteComponent(\"rpg:bar\", BarComponent);\n this.registerSpriteComponent(\"rpg:shape\", ShapeComponent);\n this.registerSpriteComponent(\"rpg:image\", ImageComponent);\n\n this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;\n this.initializePredictionController();\n }\n\n setLocale(locale: string) {\n this.locale = locale;\n }\n\n getLocale(): string {\n return this.locale || this.i18nService.defaultLocale;\n }\n\n t(key: string, params?: I18nParams): string {\n return this.i18nService.t(key, params, this.getLocale());\n }\n\n i18n() {\n return {\n locale: this.getLocale(),\n t: (key: string, params?: I18nParams) => this.t(key, params),\n };\n }\n\n /**\n * Assigns a CanvasEngine KeyboardControls instance to the dependency injection context\n * \n * This method registers a KeyboardControls instance from CanvasEngine into the DI container,\n * making it available for injection throughout the application. The particularity is that\n * this method is automatically called when a sprite is displayed on the map, allowing the\n * controls to be automatically associated with the active sprite.\n * \n * ## Design\n * \n * - The instance is stored in the DI context under the `KeyboardControls` token\n * - It's automatically assigned when a sprite component mounts (in `character.ce`)\n * - The controls instance comes from the CanvasEngine component's directives\n * - Once registered, it can be retrieved using `inject(KeyboardControls)` from anywhere\n * \n * @param controlInstance - The CanvasEngine KeyboardControls instance to register\n * \n * @example\n * ```ts\n * // The method is automatically called when a sprite is displayed:\n * // client.setKeyboardControls(element.directives.controls)\n * \n * // Later, retrieve and use the controls instance:\n * import { Input, inject, KeyboardControls } from '@rpgjs/client'\n * \n * const controls = inject(KeyboardControls)\n * const control = controls.getControl(Input.Enter)\n * \n * if (control) {\n * console.log(control.actionName) // 'action'\n * }\n * ```\n */\n setKeyboardControls(controlInstance: any) {\n const currentValues = this.context.values['inject:' + 'KeyboardControls']\n this.context.values['inject:' + 'KeyboardControls'] = {\n ...currentValues,\n values: new Map([['__default__', controlInstance]])\n }\n this.controlsReady.set(undefined);\n }\n\n async start() {\n this.sceneMap = new RpgClientMap()\n this.sceneMap.configureClientPrediction(this.predictionEnabled);\n this.sceneMap.loadPhysic();\n this.resolveSceneMapComponent();\n\n const saveClient = inject(SaveClientService);\n saveClient.initialize();\n this.initListeners();\n this.guiService._initialize();\n\n try {\n await this.webSocket.connection();\n }\n catch (error) {\n this.stopPingPong();\n await this.callConnectError(error);\n throw error;\n }\n\n this.selector = document.body.querySelector(\"#rpg\") as HTMLElement;\n\n const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;\n const { app, canvasElement } = await bootstrapCanvas(\n this.selector,\n Canvas,\n bootstrapOptions\n );\n this.canvasApp = app;\n this.canvasElement = canvasElement;\n this.renderer = app.renderer as unknown as PIXI.Renderer;\n this.setupPointerTracking();\n this.tick = canvasElement?.propObservables?.context['tick'].observable\n\n const inputCheckSubscription = this.tick.subscribe(() => {\n if (Date.now() - this.lastInputTime > 100) {\n const player = this.getCurrentPlayer();\n if (!player) return;\n (this.sceneMap as any).stopMovement(player);\n }\n });\n this.tickSubscriptions.push(inputCheckSubscription);\n\n\n this.hooks.callHooks(\"client-spritesheets-load\", this).subscribe();\n this.hooks.callHooks(\"client-spritesheetResolver-load\", this).subscribe();\n this.flushPendingSyncPackets();\n this.hooks.callHooks(\"client-sounds-load\", this).subscribe();\n this.hooks.callHooks(\"client-soundResolver-load\", this).subscribe();\n\n RpgSound.init(this);\n RpgResource.init(this);\n this.hooks.callHooks(\"client-gui-load\", this).subscribe();\n this.hooks.callHooks(\"client-particles-load\", this).subscribe();\n this.hooks.callHooks(\"client-componentAnimations-load\", this).subscribe();\n this.hooks.callHooks(\"client-clientVisuals-load\", this).subscribe();\n this.hooks.callHooks(\"client-projectiles-load\", this).subscribe();\n this.hooks.callHooks(\"client-interactions-load\", this).subscribe();\n this.hooks.callHooks(\"client-sprite-load\", this).subscribe();\n\n await lastValueFrom(this.hooks.callHooks(\"client-engine-onStart\", this));\n this.clientReadyForMapChanges = true;\n this.flushPendingMapChanges();\n\n // wondow is resize\n this.resizeHandler = () => {\n this.hooks.callHooks(\"client-engine-onWindowResize\", this).subscribe();\n };\n window.addEventListener('resize', this.resizeHandler);\n\n const tickSubscription = this.tick.subscribe((tick) => {\n this.stepClientPhysicsTick();\n this.projectiles.step();\n this.flushPendingPredictedStates();\n this.flushPendingMovePath();\n this.hooks.callHooks(\"client-engine-onStep\", this, tick).subscribe();\n\n // Clean up old prediction states and input history every 60 ticks (approximately every second at 60fps)\n if (tick % 60 === 0) {\n const now = Date.now();\n this.prediction?.cleanup(now);\n this.prediction?.tryApplyPendingSnapshot();\n }\n });\n this.tickSubscriptions.push(tickSubscription);\n\n this.startPingPong();\n }\n\n private resolveSceneMapComponent() {\n const components = this.hooks.getHookFunctions(\"client-sceneMap-component\");\n const component = components[components.length - 1];\n if (component) {\n this.sceneMapComponent = component;\n }\n }\n\n private setupPointerTracking() {\n const renderer = this.renderer as any;\n const canvas = renderer?.canvas ?? renderer?.view ?? (this.canvasApp as any)?.canvas;\n\n if (!canvas || typeof canvas.addEventListener !== \"function\") {\n return;\n }\n\n this.pointerCanvas = canvas;\n const updatePointer = (event: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n const screen = {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n };\n const viewport = this.findViewportInstance();\n let world = screen;\n\n if (viewport && typeof viewport.toWorld === \"function\") {\n const point = viewport.toWorld(screen.x, screen.y);\n world = { x: Number(point.x), y: Number(point.y) };\n } else if (viewport && typeof viewport.toLocal === \"function\") {\n const point = viewport.toLocal(screen);\n world = { x: Number(point.x), y: Number(point.y) };\n }\n\n this.pointer.update(screen, world);\n };\n\n this.pointerMoveHandler = (event: PointerEvent) => {\n updatePointer(event);\n this.interactions.handlePointerMove(event);\n };\n this.pointerUpHandler = (event: PointerEvent) => {\n updatePointer(event);\n this.interactions.handlePointerUp(event);\n };\n this.pointerCancelHandler = (event: PointerEvent) => {\n updatePointer(event);\n this.interactions.cancelDrag(event);\n };\n\n canvas.addEventListener(\"pointermove\", this.pointerMoveHandler);\n canvas.addEventListener(\"pointerdown\", this.pointerMoveHandler);\n canvas.addEventListener(\"pointerup\", this.pointerUpHandler);\n canvas.addEventListener(\"pointercancel\", this.pointerCancelHandler);\n canvas.addEventListener(\"pointerleave\", this.pointerCancelHandler);\n }\n\n updatePointerFromInteractionEvent(event: any): void {\n const global = event?.global ?? event?.data?.global;\n\n if (!global) {\n this.pointer.updateFromEvent(event);\n return;\n }\n\n const screen = {\n x: Number(global.x),\n y: Number(global.y),\n };\n if (!Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {\n this.pointer.updateFromEvent(event);\n return;\n }\n\n const viewport = this.findViewportInstance();\n if (viewport && typeof viewport.toWorld === \"function\") {\n const point = viewport.toWorld(screen.x, screen.y);\n this.pointer.update(screen, { x: Number(point.x), y: Number(point.y) });\n return;\n }\n\n this.pointer.update(screen);\n }\n\n private findViewportInstance(): any {\n const find = (node: any): any => {\n if (!node) return undefined;\n if (typeof node?.toWorld === \"function\" || node?.constructor?.name === \"Viewport\") {\n return node;\n }\n for (const child of node.children ?? []) {\n const viewport = find(child);\n if (viewport) return viewport;\n }\n return undefined;\n };\n\n return find((this.canvasApp as any)?.stage);\n }\n\n private prepareSyncPayload(data: any): any {\n const payload = { ...(data ?? {}) };\n delete payload.ack;\n delete payload.timestamp;\n\n const myId = this.playerIdSignal();\n const players = payload.players;\n const shouldMaskLocalPosition =\n this.predictionEnabled && !!this.prediction?.hasPendingInputs();\n if (shouldMaskLocalPosition && myId && players && players[myId]) {\n const localPatch = { ...players[myId] };\n delete localPatch.x;\n delete localPatch.y;\n delete localPatch.direction;\n delete localPatch._frames;\n payload.players = {\n ...players,\n [myId]: localPatch,\n };\n }\n\n return payload;\n }\n\n private normalizeAckWithSyncState(\n ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },\n syncData: any,\n ): { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction } {\n const myId = this.playerIdSignal();\n if (!myId) {\n return ack;\n }\n\n const localPatch = syncData?.players?.[myId];\n if (typeof localPatch?.x !== \"number\" || typeof localPatch?.y !== \"number\") {\n return ack;\n }\n\n return {\n ...ack,\n x: localPatch.x,\n y: localPatch.y,\n direction: localPatch.direction ?? ack.direction,\n };\n }\n\n private initListeners() {\n if (this.socketListenersInitialized) return;\n this.socketListenersInitialized = true;\n\n this.webSocket.on(\"sync\", (data) => {\n if (!this.tick) {\n this.pendingSyncPackets.push(data);\n return;\n }\n this.applySyncPacket(data);\n });\n\n // Handle pong responses for RTT measurement\n this.webSocket.on(\"pong\", (data: { serverTick: number; clientFrame: number; clientTime: number }) => {\n const now = Date.now();\n this.rtt = now - data.clientTime;\n\n // Calculate frame offset: how many ticks ahead the server is compared to our frame counter\n // This helps us estimate which server tick corresponds to each client input frame\n const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT\n const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;\n this.updateServerTickEstimate(estimatedServerTickNow, now);\n\n // Update frame offset (only if we have inputs to calibrate with)\n if (this.inputFrameCounter > 0) {\n this.frameOffset = estimatedServerTickNow - data.clientFrame;\n }\n\n console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);\n });\n\n this.webSocket.on(\"changeMap\", (data) => {\n if (!this.clientReadyForMapChanges) {\n this.pendingMapChanges.push(data);\n return;\n }\n this.handleChangeMap(data);\n });\n\n this.webSocket.on(\"showComponentAnimation\", (data) => {\n const { params, object, position, id } = data;\n if (!object && position === undefined) {\n throw new Error(\"Please provide an object or x and y coordinates\");\n }\n const player = object ? this.sceneMap.getObjectById(object) : undefined;\n this.getComponentAnimation(id).displayEffect(params, player || position)\n });\n\n this.webSocket.on(\"clientVisual\", (data) => {\n this.playClientVisual(data);\n });\n\n this.webSocket.on(\"projectile:spawnBatch\", (data) => {\n if (!this.shouldProcessProjectilePacket(data)) return;\n this.projectiles.spawnBatch(data?.projectiles ?? [], {\n mapId: data?.mapId,\n currentServerTick: this.estimateServerTick(),\n tickDurationMs: this.getPhysicsTickDurationMs(),\n });\n });\n\n this.webSocket.on(\"projectile:impactBatch\", (data) => {\n if (!this.shouldProcessProjectilePacket(data)) return;\n this.projectiles.impactBatch(data?.impacts ?? [], {\n mapId: data?.mapId,\n });\n });\n\n this.webSocket.on(\"projectile:destroyBatch\", (data) => {\n if (!this.shouldProcessProjectilePacket(data)) return;\n this.projectiles.destroyBatch(data?.projectiles ?? [], {\n mapId: data?.mapId,\n });\n });\n\n this.webSocket.on(\"projectile:clear\", (data) => {\n if (!this.shouldProcessProjectilePacket(data)) return;\n this.projectiles.clear();\n });\n\n this.webSocket.on(\"notification\", (data) => {\n this.notificationManager.add(data);\n });\n\n this.webSocket.on(\"setAnimation\", (data) => {\n const {\n animationName,\n nbTimes,\n object,\n graphic,\n restoreAnimationName,\n restoreGraphics,\n } = data;\n const player = object ? this.sceneMap.getObjectById(object) : undefined;\n if (!player) return;\n const restoreOptions = {\n restoreAnimationName,\n restoreGraphics,\n };\n if (graphic !== undefined) {\n player.setAnimation(animationName, graphic, nbTimes, restoreOptions);\n } else {\n player.setAnimation(animationName, nbTimes, restoreOptions);\n }\n })\n\n this.webSocket.on(\"playSound\", (data) => {\n const { soundId, volume, loop } = data;\n this.playSound(soundId, { volume, loop });\n });\n\n this.webSocket.on(\"stopSound\", (data) => {\n const { soundId } = data;\n this.stopSound(soundId);\n });\n\n this.webSocket.on(\"stopAllSounds\", () => {\n this.stopAllSounds();\n });\n\n this.webSocket.on(\"cameraFollow\", (data) => {\n const { targetId, smoothMove } = data;\n this.setCameraFollow(targetId, smoothMove);\n });\n\n this.webSocket.on(\"flash\", (data) => {\n const { object, type, duration, cycles, alpha, tint } = data;\n const sprite = object ? this.sceneMap.getObjectById(object) : undefined;\n if (sprite && typeof sprite.flash === 'function') {\n sprite.flash({ type, duration, cycles, alpha, tint });\n }\n });\n\n this.webSocket.on(\"shakeMap\", (data) => {\n const { intensity, duration, frequency, direction } = data || {};\n this.mapShakeTrigger.start({\n intensity,\n duration,\n frequency,\n direction\n });\n });\n\n this.webSocket.on(\"weatherState\", (data) => {\n const raw = (data && typeof data === \"object\" && \"value\" in data)\n ? (data as any).value\n : data;\n\n if (raw === null) {\n this.sceneMap.weatherState.set(null);\n return;\n }\n\n const validEffects = [\"rain\", \"snow\", \"fog\", \"cloud\"];\n if (!raw || !validEffects.includes((raw as any).effect)) {\n return;\n }\n\n this.sceneMap.weatherState.set({\n effect: (raw as any).effect,\n preset: (raw as any).preset,\n params: (raw as any).params,\n transitionMs: (raw as any).transitionMs,\n durationMs: (raw as any).durationMs,\n startedAt: (raw as any).startedAt,\n seed: (raw as any).seed,\n });\n });\n\n this.webSocket.on(\"lightingState\", (data) => {\n const raw = (data && typeof data === \"object\" && \"value\" in data)\n ? (data as any).value\n : data;\n\n this.sceneMap.lightingState.set(normalizeLightingState(raw));\n });\n\n this.webSocket.on('open', () => {\n this.hooks.callHooks(\"client-engine-onConnected\", this, this.socket).subscribe();\n // Start ping/pong for synchronization\n this.startPingPong();\n })\n\n this.webSocket.on('close', () => {\n this.hooks.callHooks(\"client-engine-onDisconnected\", this, this.socket).subscribe();\n // Stop ping/pong when disconnected\n this.stopPingPong();\n })\n\n this.webSocket.on('error', (error) => {\n void this.callConnectError(error);\n })\n }\n\n private beginMapTransfer(nextMapId?: string) {\n this.mapTransitionInProgress = true;\n this.currentMapRoomId = nextMapId;\n this.sceneResetQueued = false;\n this.clearClientPredictionStates();\n this.sceneMap.weatherState.set(null);\n this.sceneMap.lightingState.set(null);\n this.sceneMap.clearLightSpots();\n this.clearComponentAnimations();\n this.projectiles.setMapId(nextMapId);\n this.cameraFollowTargetId.set(null);\n this.sceneMap.reset();\n this.sceneMap.loadPhysic();\n }\n\n private clearComponentAnimations() {\n this.componentAnimations.forEach((componentAnimation) => {\n componentAnimation.instance?.clear?.();\n });\n }\n\n private shouldProcessProjectilePacket(data: any): boolean {\n if (this.mapTransitionInProgress) return false;\n const packetMapId = normalizeRoomMapId(\n typeof data?.mapId === \"string\" ? data.mapId : undefined,\n );\n const currentMapId = normalizeRoomMapId(this.currentMapRoomId);\n return !packetMapId || !currentMapId || packetMapId === currentMapId;\n }\n\n private async callConnectError(error: any) {\n await lastValueFrom(this.hooks.callHooks(\"client-engine-onConnectError\", this, error, this.socket));\n }\n\n private flushPendingSyncPackets() {\n const packets = this.pendingSyncPackets;\n this.pendingSyncPackets = [];\n packets.forEach((packet) => this.applySyncPacket(packet));\n }\n\n private flushPendingMapChanges() {\n const packets = this.pendingMapChanges;\n this.pendingMapChanges = [];\n packets.forEach((packet) => this.handleChangeMap(packet));\n }\n\n private handleChangeMap(data: any) {\n const nextMapId = typeof data?.mapId === \"string\" ? data.mapId : undefined;\n this.beginMapTransfer(nextMapId);\n const transferToken = typeof data?.transferToken === \"string\" ? data.transferToken : undefined;\n this.loadScene(data.mapId, transferToken);\n }\n\n private applySyncPacket(data: any) {\n if (data.pId) {\n this.playerIdSignal.set(data.pId);\n // Signal that player ID was received\n this.playerIdReceived$.next(true);\n }\n\n if (this.sceneResetQueued) {\n const weatherState = this.sceneMap.weatherState();\n const lightingState = this.sceneMap.lightingState();\n this.sceneMap.reset();\n this.sceneMap.weatherState.set(weatherState);\n this.sceneMap.lightingState.set(lightingState);\n this.sceneMap.loadPhysic();\n this.sceneResetQueued = false;\n }\n\n // Apply client-side prediction filtering and server reconciliation\n this.hooks.callHooks(\"client-sceneMap-onChanges\", this.sceneMap, { partial: data }).subscribe();\n\n const ack = data?.ack;\n const normalizedAck =\n ack && typeof ack.frame === \"number\"\n ? this.normalizeAckWithSyncState(ack, data)\n : undefined;\n const payload = this.prepareSyncPayload(data);\n load(this.sceneMap, payload, true);\n\n if (normalizedAck) {\n this.applyServerAck(normalizedAck);\n }\n\n for (const playerId in payload.players ?? {}) {\n const player = payload.players[playerId]\n if (!player._param) continue\n for (const param in player._param) {\n this.sceneMap.players()[playerId]._param()[param] = player._param[param]\n }\n }\n\n // Check if players and events are present in sync data\n const players = payload.players || this.sceneMap.players();\n if (players && Object.keys(players).length > 0) {\n this.playersReceived$.next(true);\n }\n\n const events = payload.events || this.sceneMap.events();\n if (events !== undefined) {\n this.eventsReceived$.next(true);\n }\n }\n\n /**\n * Start periodic ping/pong for client-server synchronization\n * \n * Sends ping requests to the server to measure round-trip time (RTT) and\n * calculate the frame offset between client and server ticks.\n * \n * ## Design\n * \n * - Sends ping every 5 seconds\n * - Measures RTT for latency compensation\n * - Calculates frame offset to map client frames to server ticks\n * - Used for accurate server reconciliation\n * \n * @example\n * ```ts\n * // Called automatically when connection opens\n * this.startPingPong();\n * ```\n */\n private startPingPong(): void {\n // Stop existing interval if any\n this.stopPingPong();\n\n // Send initial ping immediately\n this.sendPing();\n\n // Set up periodic pings\n this.pingInterval = setInterval(() => {\n this.sendPing();\n }, this.PING_INTERVAL_MS);\n }\n\n /**\n * Stop periodic ping/pong\n * \n * Stops the ping interval when disconnecting or changing maps.\n * \n * @example\n * ```ts\n * // Called automatically when connection closes\n * this.stopPingPong();\n * ```\n */\n private stopPingPong(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Send a ping request to the server\n * \n * Sends current client time and frame counter to the server,\n * which will respond with its server tick for synchronization.\n * \n * @example\n * ```ts\n * // Send a ping to measure RTT\n * this.sendPing();\n * ```\n */\n private sendPing(): void {\n const clientTime = Date.now();\n const clientFrame = this.getPhysicsTick();\n\n this.webSocket.emit('ping', {\n clientTime,\n clientFrame\n });\n }\n\n private async loadScene(mapId: string, transferToken?: string) {\n await lastValueFrom(this.hooks.callHooks(\"client-sceneMap-onBeforeLoading\", this.sceneMap));\n\n // Clear client prediction states when changing maps\n this.clearClientPredictionStates();\n\n // Reset all conditions for new map loading\n this.mapLoadCompleted$.next(false);\n this.playerIdReceived$.next(false);\n this.playersReceived$.next(false);\n this.eventsReceived$.next(false);\n\n // Unsubscribe previous subscription if exists\n if (this.onAfterLoadingSubscription) {\n this.onAfterLoadingSubscription.unsubscribe();\n }\n\n // Setup RxJS observable to wait for all conditions\n this.setupOnAfterLoadingObserver();\n\n this.webSocket.updateProperties({\n room: mapId,\n query: transferToken ? { transferToken } : undefined,\n })\n try {\n await this.webSocket.reconnect()\n }\n catch (error) {\n this.mapTransitionInProgress = false;\n this.stopPingPong();\n await this.callConnectError(error);\n throw error;\n }\n const res = await this.loadMapService.load(mapId)\n const loadedLighting = typeof res?.lighting !== \"undefined\"\n ? res.lighting\n : res?.data?.lighting;\n if (typeof loadedLighting !== \"undefined\") {\n this.sceneMap.lightingState.set(normalizeLightingState(loadedLighting));\n }\n this.sceneMap.data.set(res)\n \n // Check if playerId is already present\n if (this.playerIdSignal()) {\n this.playerIdReceived$.next(true);\n }\n \n // Check if players and events are already present in sceneMap\n const players = this.sceneMap.players();\n if (players && Object.keys(players).length > 0) {\n this.playersReceived$.next(true);\n }\n \n const events = this.sceneMap.events();\n if (events !== undefined) {\n this.eventsReceived$.next(true);\n }\n \n // Signal that map loading is completed (this should be last to ensure other checks are done)\n this.mapLoadCompleted$.next(true);\n this.currentMapRoomId = mapId;\n this.mapTransitionInProgress = false;\n this.sceneMap.configureClientPrediction(this.predictionEnabled);\n this.sceneMap.loadPhysic()\n }\n\n addSpriteSheet<T = any>(spritesheetClass: any, id?: string): any {\n this.spritesheets.set(id || spritesheetClass.id, spritesheetClass);\n return spritesheetClass as any;\n }\n\n /**\n * Set a resolver function for spritesheets\n * \n * The resolver is called when a spritesheet is requested but not found in the cache.\n * It can be synchronous (returns directly) or asynchronous (returns a Promise).\n * The resolved spritesheet is automatically cached for future use.\n * \n * @param resolver - Function that takes a spritesheet ID and returns a spritesheet or Promise of spritesheet\n * \n * @example\n * ```ts\n * // Synchronous resolver\n * engine.setSpritesheetResolver((id) => {\n * if (id === 'dynamic-sprite') {\n * return { id: 'dynamic-sprite', image: 'path/to/image.png', framesWidth: 32, framesHeight: 32 };\n * }\n * return undefined;\n * });\n * \n * // Asynchronous resolver (loading from API)\n * engine.setSpritesheetResolver(async (id) => {\n * const response = await fetch(`/api/spritesheets/${id}`);\n * const data = await response.json();\n * return data;\n * });\n * ```\n */\n setSpritesheetResolver(resolver: (id: string | number) => any | Promise<any>): void {\n this.spritesheetResolver = resolver;\n }\n\n /**\n * Get a spritesheet by ID, using resolver if not found in cache\n * \n * This method first checks if the spritesheet exists in the cache.\n * If not found and a resolver is set, it calls the resolver to create the spritesheet.\n * The resolved spritesheet is automatically cached for future use.\n * \n * @param id - The spritesheet ID or legacy tile ID to retrieve\n * @returns The spritesheet if found or created, or undefined if not found and no resolver\n * @returns Promise<any> if the resolver is asynchronous\n * \n * @example\n * ```ts\n * // Synchronous usage\n * const spritesheet = engine.getSpriteSheet('my-sprite');\n * \n * // Asynchronous usage (when resolver returns Promise)\n * const spritesheet = await engine.getSpriteSheet('dynamic-sprite');\n * ```\n */\n getSpriteSheet(id: string | number): any | Promise<any> {\n // Check cache first\n if (this.spritesheets.has(id)) {\n return this.spritesheets.get(id);\n }\n\n // If not in cache and resolver exists, use it\n if (this.spritesheetResolver) {\n if (this.spritesheetPromises.has(id)) {\n return this.spritesheetPromises.get(id);\n }\n\n const result = this.spritesheetResolver(id);\n\n // Check if result is a Promise\n if (result instanceof Promise) {\n const promise = result\n .then((spritesheet) => {\n if (spritesheet) {\n // Cache the resolved spritesheet\n this.spritesheets.set(id, spritesheet);\n }\n this.spritesheetPromises.delete(id);\n return spritesheet;\n })\n .catch((error) => {\n this.spritesheetPromises.delete(id);\n throw error;\n });\n this.spritesheetPromises.set(id, promise);\n return promise;\n } else {\n // Synchronous result\n if (result) {\n // Cache the resolved spritesheet\n this.spritesheets.set(id, result);\n }\n return result;\n }\n }\n\n // No resolver and not in cache\n return undefined;\n }\n\n /**\n * Add a sound to the engine\n * \n * Adds a sound to the engine's sound cache. The sound can be:\n * - A simple object with `id` and `src` properties\n * - A Howler instance\n * - An object with a `play()` method\n * \n * If the sound has a `src` property, a Howler instance will be created automatically.\n * \n * @param sound - The sound object or Howler instance\n * @param id - Optional sound ID (if not provided, uses sound.id)\n * @returns The added sound\n * \n * @example\n * ```ts\n * // Simple sound object\n * engine.addSound({ id: 'click', src: 'click.mp3' });\n * \n * // With explicit ID\n * engine.addSound({ src: 'music.mp3' }, 'background-music');\n * ```\n */\n addSound(sound: any, id?: string): any {\n const soundId = id || sound.id;\n \n if (!soundId) {\n console.warn('Sound added without an ID. It will not be retrievable.');\n return sound;\n }\n\n // If sound has a src property, create a Howler instance\n if (sound.src && typeof sound.src === 'string') {\n const howlOptions: any = {\n src: [sound.src],\n loop: sound.loop || false,\n volume: sound.volume !== undefined ? sound.volume : 1.0,\n };\n\n const howl = new (Howl as any).Howl(howlOptions);\n this.sounds.set(soundId, howl);\n return howl;\n }\n\n // If sound already has a play method (Howler instance or custom), use it directly\n if (sound && typeof sound.play === 'function') {\n this.sounds.set(soundId, sound);\n return sound;\n }\n\n // Otherwise, store as-is\n this.sounds.set(soundId, sound);\n return sound;\n }\n\n /**\n * Set a resolver function for sounds\n * \n * The resolver is called when a sound is requested but not found in the cache.\n * It can be synchronous (returns directly) or asynchronous (returns a Promise).\n * The resolved sound is automatically cached for future use.\n * \n * @param resolver - Function that takes a sound ID and returns a sound or Promise of sound\n * \n * @example\n * ```ts\n * // Synchronous resolver\n * engine.setSoundResolver((id) => {\n * if (id === 'dynamic-sound') {\n * return { id: 'dynamic-sound', src: 'path/to/sound.mp3' };\n * }\n * return undefined;\n * });\n * \n * // Asynchronous resolver (loading from API)\n * engine.setSoundResolver(async (id) => {\n * const response = await fetch(`/api/sounds/${id}`);\n * const data = await response.json();\n * return data;\n * });\n * ```\n */\n setSoundResolver(resolver: (id: string) => any | Promise<any>): void {\n this.soundResolver = resolver;\n }\n\n /**\n * Get a sound by ID, using resolver if not found in cache\n * \n * This method first checks if the sound exists in the cache.\n * If not found and a resolver is set, it calls the resolver to create the sound.\n * The resolved sound is automatically cached for future use.\n * \n * @param id - The sound ID to retrieve\n * @returns The sound if found or created, or undefined if not found and no resolver\n * @returns Promise<any> if the resolver is asynchronous\n * \n * @example\n * ```ts\n * // Synchronous usage\n * const sound = engine.getSound('my-sound');\n * \n * // Asynchronous usage (when resolver returns Promise)\n * const sound = await engine.getSound('dynamic-sound');\n * ```\n */\n getSound(id: string): any | Promise<any> {\n // Check cache first\n if (this.sounds.has(id)) {\n return this.sounds.get(id);\n }\n\n // If not in cache and resolver exists, use it\n if (this.soundResolver) {\n const result = this.soundResolver(id);\n\n // Check if result is a Promise\n if (result instanceof Promise) {\n return result.then((sound) => {\n if (sound) {\n // Cache the resolved sound\n this.sounds.set(id, sound);\n }\n return sound;\n });\n } else {\n // Synchronous result\n if (result) {\n // Cache the resolved sound\n this.sounds.set(id, result);\n }\n return result;\n }\n }\n\n // No resolver and not in cache\n return undefined;\n }\n\n /**\n * Play a sound by its ID\n * \n * This method retrieves a sound from the cache or resolver and plays it.\n * If the sound is not found, it will attempt to resolve it using the soundResolver.\n * Uses Howler.js for audio playback instead of native Audio elements.\n * \n * @param soundId - The sound ID to play\n * @param options - Optional sound configuration\n * @param options.volume - Volume level (0.0 to 1.0, overrides sound default)\n * @param options.loop - Whether the sound should loop (overrides sound default)\n * \n * @example\n * ```ts\n * // Play a sound synchronously\n * engine.playSound('item-pickup');\n * \n * // Play a sound with volume and loop\n * engine.playSound('background-music', { volume: 0.5, loop: true });\n * \n * // Play a sound asynchronously (when resolver returns Promise)\n * await engine.playSound('dynamic-sound', { volume: 0.8 });\n * ```\n */\n async playSound(soundId: string, options?: { volume?: number; loop?: boolean }): Promise<void> {\n const sound = await this.getSound(soundId);\n if (sound && sound.play) {\n // Sound is already a Howler instance or has a play method\n const howlSoundId = sound._sounds?.[0]?._id;\n \n // Apply volume if provided\n if (options?.volume !== undefined) {\n if (howlSoundId !== undefined) {\n sound.volume(Math.max(0, Math.min(1, options.volume)), howlSoundId);\n } else {\n sound.volume(Math.max(0, Math.min(1, options.volume)));\n }\n }\n \n // Apply loop if provided\n if (options?.loop !== undefined) {\n if (howlSoundId !== undefined) {\n sound.loop(options.loop, howlSoundId);\n } else {\n sound.loop(options.loop);\n }\n }\n \n if (howlSoundId !== undefined) {\n sound.play(howlSoundId);\n } else {\n sound.play();\n }\n } else if (sound && sound.src) {\n // If sound is just a source URL, create a Howler instance and cache it\n const howlOptions: any = {\n src: [sound.src],\n loop: options?.loop !== undefined ? options.loop : (sound.loop || false),\n volume: options?.volume !== undefined ? Math.max(0, Math.min(1, options.volume)) : (sound.volume !== undefined ? sound.volume : 1.0),\n };\n\n const howl = new (Howl as any).Howl(howlOptions);\n \n // Cache the Howler instance for future use\n this.sounds.set(soundId, howl);\n \n // Play the sound\n howl.play();\n } else {\n console.warn(`Sound with id \"${soundId}\" not found or cannot be played`);\n }\n }\n\n /**\n * Stop a sound that is currently playing\n * \n * This method stops a sound that was previously started with `playSound()`.\n * \n * @param soundId - The sound ID to stop\n * \n * @example\n * ```ts\n * // Start a looping sound\n * engine.playSound('background-music', { loop: true });\n * \n * // Later, stop it\n * engine.stopSound('background-music');\n * ```\n */\n stopSound(soundId: string): void {\n const sound = this.sounds.get(soundId);\n if (sound && sound.stop) {\n sound.stop();\n } else {\n console.warn(`Sound with id \"${soundId}\" not found or cannot be stopped`);\n }\n }\n\n /**\n * Stop all currently playing sounds\n * \n * This method stops all sounds that are currently playing.\n * Useful when changing maps to prevent sound overlap.\n * \n * @example\n * ```ts\n * // Stop all sounds\n * engine.stopAllSounds();\n * ```\n */\n stopAllSounds(): void {\n this.sounds.forEach((sound) => {\n if (sound && sound.stop) {\n sound.stop();\n }\n });\n }\n\n /**\n * Set the camera to follow a specific sprite\n * \n * This method changes which sprite the camera viewport should follow.\n * The camera will smoothly animate to the target sprite if smoothMove options are provided.\n * \n * ## Design\n * \n * The camera follow target is stored in a signal that is read by sprite components.\n * Each sprite checks if it should be followed by comparing its ID with the target ID.\n * When smoothMove options are provided, the viewport animation is handled by CanvasEngine's\n * viewport system.\n * \n * @param targetId - The ID of the sprite to follow. Set to null to follow the current player\n * @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease\n * @param smoothMove.time - Duration of the animation in milliseconds (optional)\n * @param smoothMove.ease - Easing function name from https://easings.net (optional)\n * \n * @example\n * ```ts\n * // Follow another player with default smooth animation\n * engine.setCameraFollow(otherPlayerId, true);\n * \n * // Follow an event with custom smooth animation\n * engine.setCameraFollow(eventId, {\n * time: 1000,\n * ease: \"easeInOutQuad\"\n * });\n * \n * // Follow without animation (instant)\n * engine.setCameraFollow(targetId, false);\n * \n * // Return to following current player\n * engine.setCameraFollow(null);\n * ```\n */\n setCameraFollow(\n targetId: string | null,\n smoothMove?: boolean | { time?: number; ease?: string }\n ): void {\n // Store smoothMove options for potential future use with viewport animation\n // For now, we just set the target ID and let CanvasEngine handle the viewport follow\n // The smoothMove options could be used to configure viewport animation if CanvasEngine supports it\n this.cameraFollowTargetId.set(targetId);\n \n // If smoothMove is an object, we could store it for viewport configuration\n // This would require integration with CanvasEngine's viewport animation system\n if (typeof smoothMove === \"object\" && smoothMove !== null) {\n // Future: Apply smoothMove.time and smoothMove.ease to viewport animation\n // For now, CanvasEngine handles viewport following automatically\n }\n }\n\n addParticle(particle: any) {\n this.particleSettings.emitters.push(particle)\n return particle;\n }\n\n /**\n * Add a component to render behind sprites\n * Components added with this method will be displayed with a lower z-index than the sprite\n * \n * Supports multiple formats:\n * 1. Direct component: `ShadowComponent`\n * 2. Configuration object: `{ component: LightHalo, props: {...} }`\n * 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`\n * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`\n * \n * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).\n * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.\n * \n * @param component - The component to add behind sprites, or a configuration object\n * @param component.component - The component function to render\n * @param component.props - Static props object or function that receives the sprite object and returns props\n * @param component.dependencies - Function that receives the sprite object and returns an array of Signals\n * @returns The added component or configuration\n * \n * @example\n * ```ts\n * // Add a shadow component behind all sprites\n * engine.addSpriteComponentBehind(ShadowComponent);\n * \n * // Add a component with static props\n * engine.addSpriteComponentBehind({ \n * component: LightHalo, \n * props: { radius: 30 } \n * });\n * \n * // Add a component with dynamic props and dependencies\n * engine.addSpriteComponentBehind({ \n * component: HealthBar, \n * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),\n * dependencies: (object) => [object.hp, object.param.maxHp]\n * });\n * ```\n */\n addSpriteComponentBehind(component: any) {\n this.spriteComponentsBehind.update((components: any[]) => [...components, component])\n return component\n }\n\n /**\n * Add a component to render in front of sprites\n * Components added with this method will be displayed with a higher z-index than the sprite\n * \n * Supports multiple formats:\n * 1. Direct component: `HealthBarComponent`\n * 2. Configuration object: `{ component: StatusIndicator, props: {...} }`\n * 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }`\n * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`\n * \n * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).\n * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.\n * \n * @param component - The component to add in front of sprites, or a configuration object\n * @param component.component - The component function to render\n * @param component.props - Static props object or function that receives the sprite object and returns props\n * @param component.dependencies - Function that receives the sprite object and returns an array of Signals\n * @returns The added component or configuration\n * \n * @example\n * ```ts\n * // Add a health bar component in front of all sprites\n * engine.addSpriteComponentInFront(HealthBarComponent);\n * \n * // Add a component with static props\n * engine.addSpriteComponentInFront({ \n * component: StatusIndicator, \n * props: { type: 'poison' } \n * });\n * \n * // Add a component with dynamic props and dependencies\n * engine.addSpriteComponentInFront({ \n * component: HealthBar, \n * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),\n * dependencies: (object) => [object.hp, object.param.maxHp]\n * });\n * ```\n */\n addSpriteComponentInFront(component: any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }) {\n this.spriteComponentsInFront.update((components: any[]) => [...components, component])\n return component\n }\n\n /**\n * Register a reusable sprite component that can be addressed by the server.\n *\n * Server-side component definitions only carry the component id and\n * serializable props. The client registry maps that id to the CanvasEngine\n * component that performs the actual rendering.\n *\n * @param id - Stable component id used by server component definitions\n * @param component - CanvasEngine component to render for this id\n * @returns The registered component\n *\n * @example\n * ```ts\n * engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);\n * ```\n */\n registerSpriteComponent(id: string, component: any) {\n this.spriteComponents.set(id, component);\n return component;\n }\n\n /**\n * Get a reusable sprite component by id.\n *\n * @param id - Component id registered on the client\n * @returns The CanvasEngine component, or undefined when missing\n */\n getSpriteComponent(id: string) {\n return this.spriteComponents.get(id);\n }\n\n /**\n * Register a custom event component resolver.\n *\n * The last resolver returning a component wins. This lets later modules\n * override earlier defaults without replacing the whole map scene.\n *\n * @param resolver - Function receiving the synced event object\n * @returns The registered resolver\n */\n addEventComponentResolver(resolver: EventComponentResolver) {\n return this.eventComponentResolvers.add(resolver);\n }\n\n /**\n * Resolve the custom CanvasEngine component for an event, if any.\n *\n * @param event - Synced client event object\n * @returns The component/config returned by the last matching resolver\n */\n resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null {\n return this.eventComponentResolvers.resolve(event);\n }\n\n registerProjectileComponent(type: string, component: any) {\n return this.projectiles.register(type, component);\n }\n\n getProjectileComponent(type: string) {\n return this.projectiles.get(type);\n }\n\n /**\n * Register a named client visual macro.\n *\n * Client visuals are small client-side functions that group existing visual\n * primitives such as flash, sound, component animations, sprite animation, or\n * map shake. The server sends only the visual name and a serializable payload.\n *\n * @param name - Stable visual name sent by the server\n * @param handler - Client-side visual handler\n * @returns The registered handler\n */\n registerClientVisual(name: string, handler: ClientVisualHandler) {\n return this.clientVisuals.register(name, handler);\n }\n\n /**\n * Register several named client visual macros.\n *\n * @param visuals - Map of visual names to client-side handlers\n */\n registerClientVisuals(visuals: ClientVisualMap) {\n this.clientVisuals.registerMany(visuals);\n }\n\n /**\n * Play a registered client visual locally.\n *\n * This is also used by the websocket listener when the server calls\n * `player.clientVisual()` or `map.clientVisual()`.\n *\n * @param packet - Visual name and serializable payload\n */\n playClientVisual(packet: ClientVisualPacket) {\n return this.clientVisuals.play(packet, this);\n }\n\n /**\n * Add a component animation to the engine\n * \n * Component animations are temporary visual effects that can be displayed\n * on sprites or objects, such as hit indicators, spell effects, or status animations.\n * \n * @param componentAnimation - The component animation configuration\n * @param componentAnimation.id - Unique identifier for the animation\n * @param componentAnimation.component - The component function to render\n * @returns The added component animation configuration\n * \n * @example\n * ```ts\n * // Add a hit animation component\n * engine.addComponentAnimation({\n * id: 'hit',\n * component: HitComponent\n * });\n * \n * // Add an explosion effect component\n * engine.addComponentAnimation({\n * id: 'explosion',\n * component: ExplosionComponent\n * });\n * ```\n */\n addComponentAnimation(componentAnimation: {\n component: any,\n id: string\n }) {\n const instance = new AnimationManager()\n this.componentAnimations.push({\n id: componentAnimation.id,\n component: componentAnimation.component,\n instance: instance,\n current: instance.current\n })\n return componentAnimation;\n }\n\n /**\n * Get a component animation by its ID\n * \n * Retrieves the EffectManager instance for a specific component animation,\n * which can be used to display the animation on sprites or objects.\n * \n * @param id - The unique identifier of the component animation\n * @returns The EffectManager instance for the animation\n * @throws Error if the component animation is not found\n * \n * @example\n * ```ts\n * // Get the hit animation and display it\n * const hitAnimation = engine.getComponentAnimation('hit');\n * hitAnimation.displayEffect({ text: \"Critical!\" }, player);\n * ```\n */\n getComponentAnimation(id: string): AnimationManager {\n const componentAnimation = this.componentAnimations.find((componentAnimation) => componentAnimation.id === id)\n if (!componentAnimation) {\n throw new Error(`Component animation with id ${id} not found`)\n }\n return componentAnimation.instance\n }\n\n /**\n * Start a transition\n * \n * Convenience method to display a transition by its ID using the GUI system.\n * \n * @param id - The unique identifier of the transition to start\n * @param props - Props to pass to the transition component\n * \n * @example\n * ```ts\n * // Start a fade transition\n * engine.startTransition('fade', { duration: 1000, color: 'black' });\n * \n * // Start with onFinish callback\n * engine.startTransition('fade', {\n * duration: 1000,\n * onFinish: () => console.log('Fade complete')\n * });\n *\n * // Wait until the transition component calls onFinish\n * await engine.startTransition('fade', { duration: 1000 });\n * ```\n */\n startTransition(id: string, props: any = {}): Promise<void> {\n if (!this.guiService.exists(id)) {\n throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);\n }\n return new Promise<void>((resolve) => {\n let finished = false;\n const finish = (data?: any) => {\n if (finished) return;\n finished = true;\n props?.onFinish?.(data);\n resolve();\n };\n\n this.guiService.display(id, {\n ...props,\n onFinish: finish,\n });\n });\n }\n\n async processInput({ input }: { input: RpgMovementInput }) {\n if (this.stopProcessingInput) return;\n\n const currentPlayer = this.sceneMap.getCurrentPlayer() as any;\n const canMove =\n !currentPlayer ||\n getCanMoveValue(currentPlayer);\n if (!canMove) {\n this.interruptCurrentPlayerMovement(currentPlayer);\n return;\n }\n\n const timestamp = Date.now();\n const movementInput = isDashInput(input)\n ? normalizeDashInput(input, currentPlayer?.direction?.())\n : input;\n if (!movementInput) return;\n if (isDashInput(movementInput)) {\n const cooldown = movementInput.cooldown ?? DEFAULT_DASH_COOLDOWN_MS;\n if (timestamp < this.dashLockedUntil) return;\n this.dashLockedUntil = timestamp + cooldown;\n }\n\n let frame: number;\n let tick: number;\n if (this.predictionEnabled && this.prediction) {\n const meta = this.prediction.recordInput(movementInput, timestamp);\n frame = meta.frame;\n tick = meta.tick;\n } else {\n frame = ++this.inputFrameCounter;\n tick = this.getPhysicsTick();\n }\n this.inputFrameCounter = frame;\n this.hooks.callHooks(\"client-engine-onInput\", this, { input: movementInput, playerId: this.playerId }).subscribe();\n\n const bodyReady = this.ensureCurrentPlayerBody();\n if (currentPlayer && bodyReady) {\n this.applyPredictedMovementInput(currentPlayer, movementInput);\n if (this.predictionEnabled && this.prediction) {\n this.pendingPredictionFrames.push(frame);\n if (this.pendingPredictionFrames.length > 240) {\n this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);\n }\n }\n }\n\n this.emitMovePacket(movementInput, frame, tick, timestamp, true);\n this.lastInputTime = isDashInput(movementInput)\n ? Date.now() + (movementInput.duration ?? DEFAULT_DASH_DURATION_MS)\n : Date.now();\n }\n\n async processDash(input: Partial<RpgDashInput> = {}) {\n const currentPlayer = this.sceneMap.getCurrentPlayer() as any;\n const fallbackDirection =\n typeof currentPlayer?.direction === \"function\"\n ? currentPlayer.direction()\n : currentPlayer?.direction;\n const dashInput = normalizeDashInput(input, fallbackDirection);\n if (!dashInput) return;\n await this.processInput({ input: dashInput });\n }\n\n processAction(action: RpgActionName, data?: any): void;\n processAction(action: RpgActionInput): void;\n processAction(action: RpgActionName | RpgActionInput, data?: any): void {\n if (this.stopProcessingInput) return;\n const currentPlayer = this.sceneMap.getCurrentPlayer() as any;\n const canMove =\n !currentPlayer ||\n getCanMoveValue(currentPlayer);\n if (!canMove) return;\n\n const payload = normalizeActionInput(action as any, data);\n\n this.hooks.callHooks(\"client-engine-onInput\", this, {\n input: payload.action,\n action: payload.action,\n data: payload.data,\n playerId: this.playerId,\n }).subscribe();\n this.webSocket.emit('action', payload)\n }\n\n get PIXI() {\n return PIXI\n }\n\n get socket() {\n return this.webSocket\n }\n\n get playerId() {\n return this.playerIdSignal()\n }\n\n get scene() {\n return this.sceneMap\n }\n\n getObjectById(id: string) {\n return this.sceneMap?.getObjectById(id);\n }\n\n private getPhysicsTick(): number {\n return (this.sceneMap as any)?.getTick?.() ?? 0;\n }\n\n private getPhysicsTickDurationMs(): number {\n const timeStep = (this.sceneMap as any)?.physic?.getWorld?.()?.getTimeStep?.();\n return typeof timeStep === \"number\" && Number.isFinite(timeStep) && timeStep > 0\n ? timeStep * 1000\n : 1000 / 60;\n }\n\n private updateServerTickEstimate(serverTick: number | undefined, now = Date.now()): void {\n if (typeof serverTick !== \"number\" || !Number.isFinite(serverTick)) {\n return;\n }\n this.latestServerTick = serverTick;\n this.latestServerTickAt = now;\n }\n\n private estimateServerTick(now = Date.now()): number | undefined {\n if (typeof this.latestServerTick !== \"number\" || this.latestServerTickAt <= 0) {\n return undefined;\n }\n const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());\n return this.latestServerTick + elapsedTicks;\n }\n\n private predictProjectileImpact(projectile: ClientProjectileSpawn): ClientProjectileImpact | null {\n if (projectile.predictImpact === false) {\n return null;\n }\n const sceneMap = this.sceneMap as any;\n if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) {\n return null;\n }\n const origin = projectile.origin;\n const direction = projectile.direction;\n if (\n !origin ||\n !direction ||\n !Number.isFinite(origin.x) ||\n !Number.isFinite(origin.y) ||\n !Number.isFinite(direction.x) ||\n !Number.isFinite(direction.y) ||\n (direction.x === 0 && direction.y === 0)\n ) {\n return null;\n }\n\n const hit = sceneMap.physic.raycast(\n new Vector2(origin.x, origin.y),\n new Vector2(direction.x, direction.y),\n projectile.range,\n projectile.collisionMask,\n (entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId,\n );\n if (!hit) {\n return null;\n }\n return {\n id: projectile.id,\n targetId: hit.entity.uuid,\n x: hit.point.x,\n y: hit.point.y,\n distance: hit.distance,\n };\n }\n\n private ensureCurrentPlayerBody(): boolean {\n const player = this.sceneMap?.getCurrentPlayer();\n const myId = this.playerIdSignal();\n if (!player || !myId) {\n return false;\n }\n if (!player.id) {\n player.id = myId;\n }\n if (this.sceneMap.getBody(myId)) {\n return true;\n }\n try {\n this.sceneMap.loadPhysic();\n } catch (error) {\n console.error(\"[RPGJS] Unable to initialize client physics before input:\", error);\n return false;\n }\n return !!this.sceneMap.getBody(myId);\n }\n\n private stepClientPhysicsTick(): void {\n if (!this.predictionEnabled || !this.sceneMap) {\n return;\n }\n const now = Date.now();\n if (this.lastClientPhysicsStepAt === 0) {\n this.lastClientPhysicsStepAt = now;\n }\n const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));\n this.lastClientPhysicsStepAt = now;\n this.sceneMap.stepClientPhysics(deltaMs);\n }\n\n private flushPendingPredictedStates(): void {\n if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) {\n return;\n }\n const state = this.getLocalPlayerState();\n while (this.pendingPredictionFrames.length > 0) {\n const frame = this.pendingPredictionFrames.shift();\n if (typeof frame === \"number\") {\n this.prediction.attachPredictedState(frame, state);\n }\n }\n }\n\n private buildPendingMoveTrajectory(): MovementTrajectoryPoint[] {\n if (!this.predictionEnabled || !this.prediction) {\n return [];\n }\n const pendingInputs = this.prediction.getPendingInputs();\n const trajectory: MovementTrajectoryPoint[] = [];\n for (const entry of pendingInputs) {\n const state = entry.state;\n if (!state) continue;\n if (typeof state.x !== \"number\" || typeof state.y !== \"number\") continue;\n trajectory.push({\n frame: entry.frame,\n tick: entry.tick,\n timestamp: entry.timestamp,\n input: entry.direction,\n x: state.x,\n y: state.y,\n direction: state.direction ?? resolveMoveDirection(entry.direction),\n });\n }\n if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {\n return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);\n }\n return trajectory;\n }\n\n private emitMovePacket(\n input: RpgMovementInput,\n frame: number,\n tick: number,\n timestamp: number,\n force = false,\n ): void {\n const trajectory = this.buildPendingMoveTrajectory();\n const latestTrajectoryFrame =\n trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;\n const shouldThrottle =\n !force &&\n latestTrajectoryFrame <= this.lastMovePathSentFrame &&\n timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS;\n if (shouldThrottle) {\n return;\n }\n\n this.webSocket.emit(\"move\", {\n input,\n timestamp,\n frame,\n tick,\n trajectory,\n });\n this.lastMovePathSentAt = timestamp;\n this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);\n }\n\n private flushPendingMovePath(): void {\n if (!this.predictionEnabled || !this.prediction) {\n return;\n }\n const player = this.sceneMap?.getCurrentPlayer?.() as any;\n if (\n player &&\n !getCanMoveValue(player)\n ) {\n this.interruptCurrentPlayerMovement(player);\n return;\n }\n const pendingInputs = this.prediction.getPendingInputs();\n if (pendingInputs.length === 0) {\n return;\n }\n const latest = pendingInputs[pendingInputs.length - 1];\n if (!latest) {\n return;\n }\n const now = Date.now();\n if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) {\n return;\n }\n this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);\n }\n\n private applyPredictedMovementInput(\n player: any,\n input: RpgMovementInput\n ): boolean {\n if (isDashInput(input)) {\n const direction = vectorToDirection(input.direction);\n player.changeDirection(direction);\n return Boolean((this.sceneMap as any).dashBody?.(player, input));\n }\n\n const direction = resolveMoveDirection(input);\n if (!direction) return false;\n player.changeDirection(direction);\n return Boolean((this.sceneMap as any).moveBody?.(player, direction));\n }\n\n private getLocalPlayerState(): PredictionState<Direction> {\n const currentPlayer = this.sceneMap?.getCurrentPlayer();\n if (!currentPlayer) {\n return { x: 0, y: 0, direction: Direction.Down };\n }\n const topLeft = this.sceneMap.getBodyPosition(currentPlayer.id, \"top-left\");\n const x = topLeft?.x ?? currentPlayer.x();\n const y = topLeft?.y ?? currentPlayer.y();\n const direction = currentPlayer.direction();\n return { x, y, direction };\n }\n\n private applyAuthoritativeState(state: PredictionState<Direction>): void {\n const player = this.sceneMap?.getCurrentPlayer();\n if (!player) return;\n const hitbox = typeof player.hitbox === \"function\" ? player.hitbox() : player.hitbox;\n const width = hitbox?.w ?? 0;\n const height = hitbox?.h ?? 0;\n const updated = this.sceneMap.updateHitbox(player.id, state.x, state.y, width, height);\n if (!updated) {\n this.sceneMap.setBodyPosition(player.id, state.x, state.y, \"top-left\");\n }\n player.x.set(Math.round(state.x));\n player.y.set(Math.round(state.y));\n if (state.direction) {\n player.changeDirection(state.direction);\n }\n }\n\n private initializePredictionController(): void {\n if (!this.predictionEnabled) {\n this.prediction = undefined;\n this.sceneMap?.configureClientPrediction?.(false);\n return;\n }\n const configuredTtl = (this.globalConfig as any)?.prediction?.historyTtlMs;\n const historyTtlMs = typeof configuredTtl === \"number\" ? configuredTtl : 10000;\n const configuredMaxEntries = (this.globalConfig as any)?.prediction?.maxHistoryEntries;\n const maxHistoryEntries =\n typeof configuredMaxEntries === \"number\"\n ? configuredMaxEntries\n : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);\n this.sceneMap?.configureClientPrediction?.(true);\n this.prediction = new PredictionController<RpgMovementInput, Direction>({\n correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,\n historyTtlMs,\n maxHistoryEntries,\n getPhysicsTick: () => this.getPhysicsTick(),\n getCurrentState: () => this.getLocalPlayerState(),\n setAuthoritativeState: (state) => this.applyAuthoritativeState(state),\n });\n }\n\n getCurrentPlayer() {\n return this.sceneMap.getCurrentPlayer()\n }\n\n emitSceneMapHook(hookName: string, ...args: any[]): void {\n this.hooks.callHooks(`client-sceneMap-${hookName}`, ...args).subscribe();\n }\n\n /**\n * Setup RxJS observer to wait for all conditions before calling onAfterLoading hook\n * \n * This method uses RxJS `combineLatest` to wait for all conditions to be met,\n * regardless of the order in which they arrive:\n * 1. The map loading is completed (loadMapService.load is finished)\n * 2. We received a player ID (pId)\n * 3. Players array has at least one element\n * 4. Events property is present in the sync data\n * \n * Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.\n * \n * ## Design\n * \n * Uses BehaviorSubjects to track each condition state, allowing events to arrive\n * in any order. The `combineLatest` operator waits until all observables emit `true`,\n * then `take(1)` ensures the hook is called only once, and `switchMap` handles\n * the hook execution.\n * \n * @example\n * ```ts\n * // Called automatically in loadScene to setup the observer\n * this.setupOnAfterLoadingObserver();\n * ```\n */\n private setupOnAfterLoadingObserver(): void {\n this.onAfterLoadingSubscription = combineLatest([\n this.mapLoadCompleted$.pipe(filter(completed => completed === true)),\n this.playerIdReceived$.pipe(filter(received => received === true)),\n this.playersReceived$.pipe(filter(received => received === true)),\n this.eventsReceived$.pipe(filter(received => received === true))\n ]).pipe(\n take(1), // Only execute once when all conditions are met\n switchMap(() => {\n // Call the hook and return the observable\n return this.hooks.callHooks(\"client-sceneMap-onAfterLoading\", this.sceneMap);\n })\n ).subscribe();\n }\n\n /**\n * Clear client prediction states for cleanup\n * \n * Removes old prediction states and input history to prevent memory leaks.\n * Should be called when changing maps or disconnecting.\n * \n * @example\n * ```ts\n * // Clear prediction states when changing maps\n * engine.clearClientPredictionStates();\n * ```\n */\n clearClientPredictionStates() {\n this.initializePredictionController();\n this.frameOffset = 0;\n this.inputFrameCounter = 0;\n this.pendingPredictionFrames = [];\n this.lastClientPhysicsStepAt = 0;\n this.lastMovePathSentAt = 0;\n this.lastMovePathSentFrame = 0;\n }\n\n /**\n * Stop local movement immediately and discard pending predicted movement.\n *\n * Use this before a blocking action such as an A-RPG attack, dialog, dash\n * startup, or any client-side state where already buffered movement inputs\n * must not be replayed after server reconciliation.\n *\n * @param player - Player object to stop. Defaults to the current player.\n * @returns `true` when a player was found and interrupted.\n *\n * @example\n * ```ts\n * engine.interruptCurrentPlayerMovement();\n * ```\n */\n interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {\n if (!player) {\n return false;\n }\n (this.sceneMap as any)?.stopMovement?.(player);\n this.prediction?.clearPendingInputs();\n this.pendingPredictionFrames = [];\n this.lastInputTime = 0;\n this.lastMovePathSentAt = Date.now();\n this.lastMovePathSentFrame = this.inputFrameCounter;\n return true;\n }\n\n /**\n * Trigger a flash animation on a sprite\n * \n * This method allows you to trigger a flash effect on any sprite from client-side code.\n * The flash can be configured with various options including type (alpha, tint, or both),\n * duration, cycles, and color.\n * \n * ## Design\n * \n * The flash is applied directly to the sprite object using its flash trigger.\n * This is useful for client-side visual feedback, UI interactions, or local effects\n * that don't need to be synchronized with the server.\n * \n * @param spriteId - The ID of the sprite to flash. If not provided, flashes the current player\n * @param options - Flash configuration options\n * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')\n * @param options.duration - Duration of the flash animation in milliseconds (default: 300)\n * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)\n * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)\n * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)\n * \n * @example\n * ```ts\n * // Flash the current player with default settings\n * engine.flash();\n * \n * // Flash a specific sprite with red tint\n * engine.flash('sprite-id', { type: 'tint', tint: 0xff0000 });\n * \n * // Flash with both alpha and tint for dramatic effect\n * engine.flash(undefined, { \n * type: 'both', \n * alpha: 0.5, \n * tint: 0xff0000,\n * duration: 200,\n * cycles: 2\n * });\n * \n * // Quick damage flash on current player\n * engine.flash(undefined, { \n * type: 'tint', \n * tint: 'red', \n * duration: 150,\n * cycles: 1\n * });\n * ```\n */\n flash(\n spriteId?: string,\n options?: {\n type?: 'alpha' | 'tint' | 'both';\n duration?: number;\n cycles?: number;\n alpha?: number;\n tint?: number | string;\n }\n ): void {\n const targetId = spriteId || this.playerId;\n if (!targetId) return;\n\n const sprite = this.sceneMap.getObjectById(targetId);\n if (sprite && typeof sprite.flash === 'function') {\n sprite.flash(options);\n }\n }\n\n private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {\n this.updateServerTickEstimate(ack.serverTick);\n if (this.predictionEnabled && this.prediction) {\n const result = this.prediction.applyServerAck({\n frame: ack.frame,\n serverTick: ack.serverTick,\n state:\n typeof ack.x === \"number\" && typeof ack.y === \"number\"\n ? { x: ack.x, y: ack.y, direction: ack.direction }\n : undefined,\n });\n if (result.state && result.needsReconciliation) {\n this.reconcilePrediction(result.state, result.pendingInputs);\n }\n return;\n }\n\n if (typeof ack.x !== \"number\" || typeof ack.y !== \"number\") {\n return;\n }\n const player = this.getCurrentPlayer() as any;\n const myId = this.playerIdSignal();\n if (!player || !myId) {\n return;\n }\n const hitbox = typeof player.hitbox === \"function\" ? player.hitbox() : player.hitbox;\n const width = hitbox?.w ?? 0;\n const height = hitbox?.h ?? 0;\n const updated = this.sceneMap.updateHitbox(myId, ack.x, ack.y, width, height);\n if (!updated) {\n this.sceneMap.setBodyPosition(myId, ack.x, ack.y, \"top-left\");\n }\n player.x.set(Math.round(ack.x));\n player.y.set(Math.round(ack.y));\n if (ack.direction) {\n player.changeDirection(ack.direction);\n }\n }\n\n private reconcilePrediction(\n authoritativeState: PredictionState<Direction>,\n pendingInputs: PredictionHistoryEntry<RpgMovementInput, Direction>[],\n ): void {\n const player = this.getCurrentPlayer() as any;\n if (!player) {\n return;\n }\n if (!getCanMoveValue(player)) {\n this.interruptCurrentPlayerMovement(player);\n return;\n }\n\n (this.sceneMap as any).stopMovement(player);\n this.applyAuthoritativeState(authoritativeState);\n\n if (!pendingInputs.length) {\n return;\n }\n\n // Keep replay bounded while still tolerating high-latency links.\n const replayInputs = pendingInputs.slice(-600);\n for (const entry of replayInputs) {\n if (!entry?.direction) continue;\n this.applyPredictedMovementInput(player, entry.direction);\n this.sceneMap.stepPredictionTick();\n this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());\n }\n }\n\n /**\n * Replay unacknowledged inputs from a given frame to resimulate client prediction\n * after applying server authority at a certain frame.\n * \n * @param startFrame - The last server-acknowledged frame\n * \n * @example\n * ```ts\n * // After applying a server correction at frame N\n * this.replayUnackedInputsFromFrame(N);\n * ```\n */\n private async replayUnackedInputsFromFrame(_startFrame: number): Promise<void> {\n // Prediction controller handles replay internally. Kept for backwards compatibility.\n }\n\n /**\n * Clear all client resources and reset state\n * \n * This method should be called to clean up all client-side resources when\n * shutting down or resetting the client engine. It:\n * - Destroys the PIXI renderer\n * - Stops all sounds\n * - Cleans up subscriptions and event listeners\n * - Resets scene map\n * - Stops ping/pong interval\n * - Clears prediction states\n * \n * ## Design\n * \n * This method is used primarily in testing environments to ensure clean\n * state between tests. In production, the client engine typically persists\n * for the lifetime of the application.\n * \n * @example\n * ```ts\n * // In test cleanup\n * afterEach(() => {\n * clientEngine.clear();\n * });\n * ```\n */\n clear(): void {\n try {\n // First, unsubscribe from all tick subscriptions to stop rendering attempts\n for (const subscription of this.tickSubscriptions) {\n if (subscription && typeof subscription.unsubscribe === 'function') {\n subscription.unsubscribe();\n }\n }\n this.tickSubscriptions = [];\n\n // Stop ping/pong interval\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n\n // Clean up onAfterLoading subscription\n if (this.onAfterLoadingSubscription && typeof this.onAfterLoadingSubscription.unsubscribe === 'function') {\n this.onAfterLoadingSubscription.unsubscribe();\n this.onAfterLoadingSubscription = undefined;\n }\n\n // Clean up canvasElement (CanvasEngine) BEFORE destroying PIXI app\n // This prevents CanvasEngine from trying to render after PIXI is destroyed\n // CanvasEngine manages its own render loop which could try to access PIXI after destruction\n if (this.canvasElement) {\n try {\n // Try to stop or cleanup canvasElement if it has cleanup methods\n if (typeof (this.canvasElement as any).destroy === 'function') {\n (this.canvasElement as any).destroy();\n }\n // Clear the reference\n this.canvasElement = undefined;\n } catch (error) {\n // Ignore errors during canvasElement cleanup\n }\n }\n\n // Reset scene map if it exists (this should stop any ongoing animations/renders)\n if (this.sceneMap && typeof (this.sceneMap as any).reset === 'function') {\n (this.sceneMap as any).reset(true);\n }\n\n // Stop all sounds\n this.stopAllSounds();\n\n // Remove resize event listener\n if (this.resizeHandler && typeof window !== 'undefined') {\n window.removeEventListener('resize', this.resizeHandler);\n this.resizeHandler = undefined;\n }\n\n if (this.pointerMoveHandler && this.pointerCanvas) {\n this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);\n this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);\n if (this.pointerUpHandler) {\n this.pointerCanvas.removeEventListener('pointerup', this.pointerUpHandler);\n }\n if (this.pointerCancelHandler) {\n this.pointerCanvas.removeEventListener('pointercancel', this.pointerCancelHandler);\n this.pointerCanvas.removeEventListener('pointerleave', this.pointerCancelHandler);\n }\n this.pointerMoveHandler = undefined;\n this.pointerUpHandler = undefined;\n this.pointerCancelHandler = undefined;\n this.pointerCanvas = undefined;\n }\n\n // Destroy PIXI app and renderer if they exist\n // Destroy the app first, which will destroy the renderer\n // Store renderer reference before destroying app (since app.destroy() will destroy the renderer)\n const rendererStillExists = this.renderer && typeof this.renderer.destroy === 'function';\n \n if (this.canvasApp && typeof this.canvasApp.destroy === 'function') {\n try {\n // Stop the ticker first to prevent any render calls during destruction\n if (this.canvasApp.ticker) {\n if (typeof this.canvasApp.ticker.stop === 'function') {\n this.canvasApp.ticker.stop();\n }\n // Also remove all listeners from ticker to prevent callbacks\n if (typeof this.canvasApp.ticker.removeAll === 'function') {\n this.canvasApp.ticker.removeAll();\n }\n }\n \n // Stop the renderer's ticker if it exists separately\n if (this.renderer && (this.renderer as any).ticker) {\n if (typeof (this.renderer as any).ticker.stop === 'function') {\n (this.renderer as any).ticker.stop();\n }\n if (typeof (this.renderer as any).ticker.removeAll === 'function') {\n (this.renderer as any).ticker.removeAll();\n }\n }\n \n // Remove the canvas from DOM before destroying to prevent render attempts\n if (this.canvasApp.canvas && this.canvasApp.canvas.parentNode) {\n this.canvasApp.canvas.parentNode.removeChild(this.canvasApp.canvas);\n }\n \n // Destroy with minimal options to avoid issues\n // Don't pass options that might trigger additional cleanup that could fail\n this.canvasApp.destroy(true);\n } catch (error) {\n // Ignore errors during destruction\n }\n this.canvasApp = undefined;\n // canvasApp.destroy() already destroyed the renderer, so just null it\n this.renderer = null as any;\n } else if (rendererStillExists) {\n // Fallback: destroy renderer directly only if app doesn't exist or wasn't destroyed\n try {\n // Stop the renderer's ticker if it has one\n if ((this.renderer as any).ticker) {\n if (typeof (this.renderer as any).ticker.stop === 'function') {\n (this.renderer as any).ticker.stop();\n }\n if (typeof (this.renderer as any).ticker.removeAll === 'function') {\n (this.renderer as any).ticker.removeAll();\n }\n }\n \n this.renderer.destroy(true);\n } catch (error) {\n // Ignore errors during destruction\n }\n this.renderer = null as any;\n }\n\n // Clean up prediction controller\n if (this.prediction) {\n // Prediction controller cleanup is handled internally when destroyed\n this.prediction = undefined;\n }\n\n // Reset signals\n this.playerIdSignal.set(null);\n this.cameraFollowTargetId.set(null);\n this.spriteComponentsBehind.set([]);\n this.spriteComponentsInFront.set([]);\n this.eventComponentResolvers.clear();\n \n // Clear maps and arrays\n this.spritesheets.clear();\n this.sounds.clear();\n this.componentAnimations = [];\n this.particleSettings.emitters = [];\n\n // Reset state\n this.stopProcessingInput = false;\n this.lastInputTime = 0;\n this.inputFrameCounter = 0;\n this.frameOffset = 0;\n this.rtt = 0;\n this.lastMovePathSentAt = 0;\n this.lastMovePathSentFrame = 0;\n\n // Reset behavior subjects\n this.mapLoadCompleted$.next(false);\n this.playerIdReceived$.next(false);\n this.playersReceived$.next(false);\n this.eventsReceived$.next(false);\n } catch (error) {\n console.warn('Error during client engine cleanup:', error);\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RpgClientEngine.js","names":[],"sources":["../src/RpgClientEngine.ts"],"sourcesContent":["import Canvas from \"./components/scenes/canvas.ce\";\nimport BuiltinSceneMap from \"./components/scenes/draw-map.ce\";\nimport { inject } from './core/inject'\nimport { signal, bootstrapCanvas, Howl, trigger, type Trigger } from \"canvasengine\";\nimport { AbstractWebsocket, WebSocketToken } from \"./services/AbstractSocket\";\nimport { LoadMapService, LoadMapToken } from \"./services/loadMap\";\nimport { RpgSound } from \"./Sound\";\nimport { RpgResource } from \"./Resource\";\nimport { getOrCreateI18nService, Hooks, ModulesToken, Direction, normalizeLightingState, Vector2, type I18nParams, type I18nService } from \"@rpgjs/common\";\nimport type { EventComponentConfig } from \"./RpgClient\";\nimport type { RpgClientEvent } from \"./Game/Event\";\nimport { load } from \"@signe/sync\";\nimport { RpgClientMap } from \"./Game/Map\"\nimport { RpgGui } from \"./Gui/Gui\";\nimport { AnimationManager } from \"./Game/AnimationManager\";\nimport { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, switchMap, take } from \"rxjs\";\nimport { GlobalConfigToken } from \"./module\";\nimport * as PIXI from \"pixi.js\";\nimport { PrebuiltComponentAnimations } from \"./components/animations\";\nimport TextComponent from \"./components/dynamics/text.ce\";\nimport BarComponent from \"./components/dynamics/bar.ce\";\nimport ShapeComponent from \"./components/dynamics/shape.ce\";\nimport ImageComponent from \"./components/dynamics/image.ce\";\nimport {\n PredictionController,\n type PredictionHistoryEntry,\n type PredictionState,\n type RpgActionInput,\n type RpgActionName,\n type RpgDashInput,\n type RpgMovementInput,\n} from \"@rpgjs/common\";\nimport { NotificationManager } from \"./Gui/NotificationManager\";\nimport { SaveClientService } from \"./services/save\";\nimport { getCanMoveValue } from \"./utils/readPropValue\";\nimport { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSpawn } from \"./Game/ProjectileManager\";\nimport { ClientVisualRegistry, type ClientVisualHandler, type ClientVisualMap, type ClientVisualPacket } from \"./Game/ClientVisuals\";\nimport { normalizeActionInput } from \"./services/actionInput\";\nimport { createClientPointerContext, type ClientPointerContext } from \"./services/pointerContext\";\nimport { RpgClientInteractions } from \"./services/interactions\";\nimport { normalizeRoomMapId } from \"./utils/mapId\";\nimport { EventComponentResolverRegistry, type EventComponentResolver } from \"./Game/EventComponentResolver\";\nimport { RpgClientBuiltinI18n } from \"./i18n\";\n\ninterface MovementTrajectoryPoint {\n frame: number;\n tick: number;\n timestamp: number;\n input: RpgMovementInput;\n x: number;\n y: number;\n direction?: Direction;\n}\n\ninterface CanvasResizeSize {\n width: number;\n height: number;\n}\n\nconst DEFAULT_DASH_ADDITIONAL_SPEED = 8;\nconst DEFAULT_DASH_DURATION_MS = 180;\nconst DEFAULT_DASH_COOLDOWN_MS = 450;\n\nconst isDashInput = (input: RpgMovementInput): input is RpgDashInput =>\n typeof input === \"object\" && input !== null && input.type === \"dash\";\n\nconst isMoveInput = (\n input: RpgMovementInput\n): input is { type: \"move\"; direction: Direction } =>\n typeof input === \"object\" && input !== null && input.type === \"move\";\n\nconst resolveMoveDirection = (input: RpgMovementInput): Direction | undefined => {\n if (isMoveInput(input)) return input.direction;\n if (typeof input === \"string\" || typeof input === \"number\") {\n return input as Direction;\n }\n return undefined;\n};\n\nconst directionToVector = (direction: Direction | undefined) => {\n switch (direction) {\n case Direction.Left:\n return { x: -1, y: 0 };\n case Direction.Right:\n return { x: 1, y: 0 };\n case Direction.Up:\n return { x: 0, y: -1 };\n case Direction.Down:\n default:\n return { x: 0, y: 1 };\n }\n};\n\nconst vectorToDirection = (direction: { x: number; y: number }): Direction => {\n if (Math.abs(direction.x) > Math.abs(direction.y)) {\n return direction.x < 0 ? Direction.Left : Direction.Right;\n }\n return direction.y < 0 ? Direction.Up : Direction.Down;\n};\n\nconst normalizeDashInput = (\n input: Partial<RpgDashInput>,\n fallbackDirection: Direction | undefined\n): RpgDashInput | null => {\n const rawDirection = input.direction ?? directionToVector(fallbackDirection);\n const rawX = Number(rawDirection?.x ?? 0);\n const rawY = Number(rawDirection?.y ?? 0);\n const magnitude = Math.hypot(rawX, rawY);\n if (!Number.isFinite(magnitude) || magnitude <= 0) return null;\n\n const additionalSpeed =\n typeof input.additionalSpeed === \"number\" && Number.isFinite(input.additionalSpeed)\n ? Math.max(0, Math.min(input.additionalSpeed, 64))\n : DEFAULT_DASH_ADDITIONAL_SPEED;\n const duration =\n typeof input.duration === \"number\" && Number.isFinite(input.duration)\n ? Math.max(1, Math.min(input.duration, 1000))\n : DEFAULT_DASH_DURATION_MS;\n const cooldown =\n typeof input.cooldown === \"number\" && Number.isFinite(input.cooldown)\n ? Math.max(0, Math.min(input.cooldown, 5000))\n : DEFAULT_DASH_COOLDOWN_MS;\n\n return {\n type: \"dash\",\n direction: {\n x: rawX / magnitude,\n y: rawY / magnitude,\n },\n additionalSpeed,\n duration,\n cooldown,\n };\n};\n\ntype ConfigurableTrigger<T> = Omit<Trigger<T>, \"start\"> & {\n start(config?: T): Promise<void>;\n};\n\ntype MapShakeOptions = {\n intensity?: number;\n duration?: number;\n frequency?: number;\n direction?: string;\n};\n\nexport class RpgClientEngine<T = any> {\n private guiService: RpgGui;\n private webSocket: AbstractWebsocket;\n private loadMapService: LoadMapService;\n private hooks: Hooks;\n private sceneMap: RpgClientMap\n private selector: HTMLElement;\n public globalConfig: T;\n public sceneComponent: any;\n public sceneMapComponent: any = BuiltinSceneMap;\n stopProcessingInput = false;\n width = signal(\"100%\");\n height = signal(\"100%\");\n spritesheets: Map<string | number, any> = new Map();\n private spritesheetPromises: Map<string | number, Promise<any>> = new Map();\n sounds: Map<string, any> = new Map();\n componentAnimations: any[] = [];\n clientVisuals = new ClientVisualRegistry();\n projectiles: ProjectileManager;\n pointer: ClientPointerContext = createClientPointerContext();\n interactions: RpgClientInteractions = new RpgClientInteractions(this);\n private spritesheetResolver?: (id: string | number) => any | Promise<any>;\n private soundResolver?: (id: string) => any | Promise<any>;\n particleSettings: {\n emitters: any[]\n } = {\n emitters: []\n }\n renderer: PIXI.Renderer;\n tick: Observable<number>;\n private canvasApp?: any;\n private canvasElement?: any;\n playerIdSignal = signal<string | null>(null);\n spriteComponentsBehind = signal<any[]>([]);\n spriteComponentsInFront = signal<any[]>([]);\n spriteComponents: Map<string, any> = new Map();\n private eventComponentResolvers = new EventComponentResolverRegistry();\n /** ID of the sprite that the camera should follow. null means follow the current player */\n cameraFollowTargetId = signal<string | null>(null);\n /** Trigger for map shake animation */\n mapShakeTrigger: ConfigurableTrigger<MapShakeOptions> = trigger<MapShakeOptions>();\n\n controlsReady = signal(undefined); \n gamePause = signal(false);\n\n private predictionEnabled = false;\n private prediction?: PredictionController<RpgMovementInput, Direction>;\n private readonly SERVER_CORRECTION_THRESHOLD = 30;\n private localMovementAuthority = false;\n private lastLocalMovementInputAt = 0;\n private readonly LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS = 250;\n private inputFrameCounter = 0;\n private pendingPredictionFrames: number[] = [];\n private lastClientPhysicsStepAt = 0;\n private frameOffset = 0;\n private latestServerTick?: number;\n private latestServerTickAt = 0;\n private dashLockedUntil = 0;\n // Ping/Pong for RTT measurement\n private rtt: number = 0; // Round-trip time in ms\n private pingInterval: any = null;\n private readonly PING_INTERVAL_MS = 5000; // Send ping every 5 seconds\n private lastInputTime = 0;\n private readonly MOVE_PATH_RESEND_INTERVAL_MS = 120;\n private readonly MAX_MOVE_TRAJECTORY_POINTS = 240;\n private lastMovePathSentAt = 0;\n private lastMovePathSentFrame = 0;\n // Track map loading state for onAfterLoading hook using RxJS\n private mapLoadCompleted$ = new BehaviorSubject<boolean>(false);\n private playerIdReceived$ = new BehaviorSubject<boolean>(false);\n private playersReceived$ = new BehaviorSubject<boolean>(false);\n private eventsReceived$ = new BehaviorSubject<boolean>(false);\n private onAfterLoadingSubscription?: any;\n private sceneResetQueued = false;\n private mapTransitionInProgress = false;\n private currentMapRoomId?: string;\n private socketListenersInitialized = false;\n private clientReadyForMapChanges = false;\n private pendingMapChanges: any[] = [];\n \n // Store subscriptions and event listeners for cleanup\n private tickSubscriptions: any[] = [];\n private resizeHandler?: () => void;\n private pointerMoveHandler?: (event: PointerEvent) => void;\n private pointerUpHandler?: (event: PointerEvent) => void;\n private pointerCancelHandler?: (event: PointerEvent) => void;\n private pointerCanvas?: HTMLCanvasElement;\n private pendingSyncPackets: any[] = [];\n private notificationManager: NotificationManager = new NotificationManager();\n private i18nService: I18nService;\n private locale?: string;\n\n constructor(public context) {\n this.webSocket = inject(WebSocketToken);\n this.guiService = inject(RpgGui);\n this.loadMapService = inject(LoadMapToken);\n this.hooks = inject<Hooks>(ModulesToken);\n this.i18nService = getOrCreateI18nService(context);\n this.i18nService.addMessages(RpgClientBuiltinI18n, \"rpgjs-client\", 0);\n this.projectiles = new ProjectileManager(\n this.hooks,\n (projectile) => this.predictProjectileImpact(projectile),\n );\n this.globalConfig = inject(GlobalConfigToken)\n\n if (!this.globalConfig) {\n this.globalConfig = {} as T\n }\n if (!(this.globalConfig as any).box) {\n (this.globalConfig as any).box = {\n styles: {\n backgroundColor: \"#1a1a2e\",\n backgroundOpacity: 0.9\n },\n sounds: {}\n }\n }\n\n this.addComponentAnimation({\n id: \"animation\",\n component: PrebuiltComponentAnimations.Animation\n })\n\n this.registerSpriteComponent(\"rpg:text\", TextComponent);\n this.registerSpriteComponent(\"rpg:hpBar\", BarComponent);\n this.registerSpriteComponent(\"rpg:spBar\", BarComponent);\n this.registerSpriteComponent(\"rpg:bar\", BarComponent);\n this.registerSpriteComponent(\"rpg:shape\", ShapeComponent);\n this.registerSpriteComponent(\"rpg:image\", ImageComponent);\n\n this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;\n this.localMovementAuthority = this.resolveLocalMovementAuthority();\n this.initializePredictionController();\n }\n\n private resolveLocalMovementAuthority(): boolean {\n const predictionConfig = (this.globalConfig as any)?.prediction;\n const configured =\n (this.globalConfig as any)?.movementAuthority ??\n predictionConfig?.movementAuthority ??\n predictionConfig?.authority ??\n predictionConfig?.mode;\n\n if (\n configured === \"server\" ||\n configured === \"network\" ||\n configured === false\n ) {\n return false;\n }\n if (\n configured === \"client\" ||\n configured === \"local\" ||\n configured === true\n ) {\n return true;\n }\n\n return this.webSocket.mode === \"standalone\";\n }\n\n setLocale(locale: string) {\n this.locale = locale;\n }\n\n getLocale(): string {\n return this.locale || this.i18nService.defaultLocale;\n }\n\n t(key: string, params?: I18nParams): string {\n return this.i18nService.t(key, params, this.getLocale());\n }\n\n i18n() {\n return {\n locale: this.getLocale(),\n t: (key: string, params?: I18nParams) => this.t(key, params),\n };\n }\n\n /**\n * Assigns a CanvasEngine KeyboardControls instance to the dependency injection context\n * \n * This method registers a KeyboardControls instance from CanvasEngine into the DI container,\n * making it available for injection throughout the application. The particularity is that\n * this method is automatically called when a sprite is displayed on the map, allowing the\n * controls to be automatically associated with the active sprite.\n * \n * ## Design\n * \n * - The instance is stored in the DI context under the `KeyboardControls` token\n * - It's automatically assigned when a sprite component mounts (in `character.ce`)\n * - The controls instance comes from the CanvasEngine component's directives\n * - Once registered, it can be retrieved using `inject(KeyboardControls)` from anywhere\n * \n * @param controlInstance - The CanvasEngine KeyboardControls instance to register\n * \n * @example\n * ```ts\n * // The method is automatically called when a sprite is displayed:\n * // client.setKeyboardControls(element.directives.controls)\n * \n * // Later, retrieve and use the controls instance:\n * import { Input, inject, KeyboardControls } from '@rpgjs/client'\n * \n * const controls = inject(KeyboardControls)\n * const control = controls.getControl(Input.Enter)\n * \n * if (control) {\n * console.log(control.actionName) // 'action'\n * }\n * ```\n */\n setKeyboardControls(controlInstance: any) {\n const currentValues = this.context.values['inject:' + 'KeyboardControls']\n this.context.values['inject:' + 'KeyboardControls'] = {\n ...currentValues,\n values: new Map([['__default__', controlInstance]])\n }\n this.controlsReady.set(undefined);\n }\n\n async start() {\n this.sceneMap = new RpgClientMap()\n this.sceneMap.configureClientPrediction(this.predictionEnabled);\n this.sceneMap.loadPhysic();\n this.resolveSceneMapComponent();\n\n const saveClient = inject(SaveClientService);\n saveClient.initialize();\n this.initListeners();\n this.guiService._initialize();\n\n try {\n await this.webSocket.connection();\n }\n catch (error) {\n this.stopPingPong();\n await this.callConnectError(error);\n throw error;\n }\n\n this.selector = document.body.querySelector(\"#rpg\") as HTMLElement;\n\n const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;\n const { app, canvasElement } = await bootstrapCanvas(\n this.selector,\n Canvas,\n bootstrapOptions\n );\n this.installCanvasResizeGuard(app);\n this.canvasApp = app;\n this.canvasElement = canvasElement;\n this.renderer = app.renderer as unknown as PIXI.Renderer;\n this.setupPointerTracking();\n this.tick = canvasElement?.propObservables?.context['tick'].observable\n\n const inputCheckSubscription = this.tick.subscribe(() => {\n if (Date.now() - this.lastInputTime > 100) {\n const player = this.getCurrentPlayer();\n if (!player) return;\n (this.sceneMap as any).stopMovement(player);\n }\n });\n this.tickSubscriptions.push(inputCheckSubscription);\n\n\n this.hooks.callHooks(\"client-spritesheets-load\", this).subscribe();\n this.hooks.callHooks(\"client-spritesheetResolver-load\", this).subscribe();\n this.flushPendingSyncPackets();\n this.hooks.callHooks(\"client-sounds-load\", this).subscribe();\n this.hooks.callHooks(\"client-soundResolver-load\", this).subscribe();\n\n RpgSound.init(this);\n RpgResource.init(this);\n this.hooks.callHooks(\"client-gui-load\", this).subscribe();\n this.hooks.callHooks(\"client-particles-load\", this).subscribe();\n this.hooks.callHooks(\"client-componentAnimations-load\", this).subscribe();\n this.hooks.callHooks(\"client-clientVisuals-load\", this).subscribe();\n this.hooks.callHooks(\"client-projectiles-load\", this).subscribe();\n this.hooks.callHooks(\"client-interactions-load\", this).subscribe();\n this.hooks.callHooks(\"client-sprite-load\", this).subscribe();\n\n await lastValueFrom(this.hooks.callHooks(\"client-engine-onStart\", this));\n this.clientReadyForMapChanges = true;\n this.flushPendingMapChanges();\n\n // wondow is resize\n this.resizeHandler = () => {\n this.hooks.callHooks(\"client-engine-onWindowResize\", this).subscribe();\n };\n window.addEventListener('resize', this.resizeHandler);\n\n const tickSubscription = this.tick.subscribe((tick) => {\n this.stepClientPhysicsTick();\n this.projectiles.step();\n this.flushPendingPredictedStates();\n this.flushPendingMovePath();\n this.hooks.callHooks(\"client-engine-onStep\", this, tick).subscribe();\n\n // Clean up old prediction states and input history every 60 ticks (approximately every second at 60fps)\n if (tick % 60 === 0) {\n const now = Date.now();\n this.prediction?.cleanup(now);\n this.prediction?.tryApplyPendingSnapshot();\n }\n });\n this.tickSubscriptions.push(tickSubscription);\n\n this.startPingPong();\n }\n\n private installCanvasResizeGuard(app: any) {\n if (!app || typeof app.resize !== \"function\") return;\n\n const originalResize = app.resize.bind(app);\n app.resize = () => {\n const targetSize = this.readCanvasResizeTargetSize(app);\n const rendererSize = this.readCanvasRendererSize(app);\n\n if (\n targetSize &&\n rendererSize &&\n targetSize.width === rendererSize.width &&\n targetSize.height === rendererSize.height\n ) {\n this.cancelCanvasResizeFrame(app);\n return;\n }\n\n originalResize();\n };\n }\n\n private readCanvasResizeTargetSize(app: any): CanvasResizeSize | null {\n const resizeTarget = app?.resizeTo;\n if (!resizeTarget || typeof window === \"undefined\") return null;\n\n const rawWidth = resizeTarget === window ? window.innerWidth : resizeTarget.clientWidth;\n const rawHeight = resizeTarget === window ? window.innerHeight : resizeTarget.clientHeight;\n const width = Math.round(Number(rawWidth));\n const height = Math.round(Number(rawHeight));\n\n if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) return null;\n return { width, height };\n }\n\n private readCanvasRendererSize(app: any): CanvasResizeSize | null {\n const screen = app?.renderer?.screen;\n const width = Math.round(Number(screen?.width));\n const height = Math.round(Number(screen?.height));\n\n if (!Number.isFinite(width) || !Number.isFinite(height)) return null;\n return { width, height };\n }\n\n private cancelCanvasResizeFrame(app: any) {\n if (typeof app?._cancelResize === \"function\") {\n app._cancelResize();\n }\n }\n\n private resolveSceneMapComponent() {\n const components = this.hooks.getHookFunctions(\"client-sceneMap-component\");\n const component = components[components.length - 1];\n if (component) {\n this.sceneMapComponent = component;\n }\n }\n\n private setupPointerTracking() {\n const renderer = this.renderer as any;\n const canvas = renderer?.canvas ?? renderer?.view ?? (this.canvasApp as any)?.canvas;\n\n if (!canvas || typeof canvas.addEventListener !== \"function\") {\n return;\n }\n\n this.pointerCanvas = canvas;\n const updatePointer = (event: PointerEvent) => {\n const rect = canvas.getBoundingClientRect();\n const screen = {\n x: event.clientX - rect.left,\n y: event.clientY - rect.top,\n };\n const viewport = this.findViewportInstance();\n let world = screen;\n\n if (viewport && typeof viewport.toWorld === \"function\") {\n const point = viewport.toWorld(screen.x, screen.y);\n world = { x: Number(point.x), y: Number(point.y) };\n } else if (viewport && typeof viewport.toLocal === \"function\") {\n const point = viewport.toLocal(screen);\n world = { x: Number(point.x), y: Number(point.y) };\n }\n\n this.pointer.update(screen, world);\n };\n\n this.pointerMoveHandler = (event: PointerEvent) => {\n updatePointer(event);\n this.interactions.handlePointerMove(event);\n };\n this.pointerUpHandler = (event: PointerEvent) => {\n updatePointer(event);\n this.interactions.handlePointerUp(event);\n };\n this.pointerCancelHandler = (event: PointerEvent) => {\n updatePointer(event);\n this.interactions.cancelDrag(event);\n };\n\n canvas.addEventListener(\"pointermove\", this.pointerMoveHandler);\n canvas.addEventListener(\"pointerdown\", this.pointerMoveHandler);\n canvas.addEventListener(\"pointerup\", this.pointerUpHandler);\n canvas.addEventListener(\"pointercancel\", this.pointerCancelHandler);\n canvas.addEventListener(\"pointerleave\", this.pointerCancelHandler);\n }\n\n updatePointerFromInteractionEvent(event: any): void {\n const global = event?.global ?? event?.data?.global;\n\n if (!global) {\n this.pointer.updateFromEvent(event);\n return;\n }\n\n const screen = {\n x: Number(global.x),\n y: Number(global.y),\n };\n if (!Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {\n this.pointer.updateFromEvent(event);\n return;\n }\n\n const viewport = this.findViewportInstance();\n if (viewport && typeof viewport.toWorld === \"function\") {\n const point = viewport.toWorld(screen.x, screen.y);\n this.pointer.update(screen, { x: Number(point.x), y: Number(point.y) });\n return;\n }\n\n this.pointer.update(screen);\n }\n\n private findViewportInstance(): any {\n const find = (node: any): any => {\n if (!node) return undefined;\n if (typeof node?.toWorld === \"function\" || node?.constructor?.name === \"Viewport\") {\n return node;\n }\n for (const child of node.children ?? []) {\n const viewport = find(child);\n if (viewport) return viewport;\n }\n return undefined;\n };\n\n return find((this.canvasApp as any)?.stage);\n }\n\n private prepareSyncPayload(data: any): any {\n const payload = { ...(data ?? {}) };\n delete payload.ack;\n delete payload.timestamp;\n\n const myId = this.playerIdSignal();\n const players = payload.players;\n const localPatch = myId && players ? players[myId] : undefined;\n const shouldMaskLocalPosition = this.shouldPreserveLocalPlayerPosition(localPatch);\n if (shouldMaskLocalPosition && myId && players && players[myId]) {\n const localPatch = { ...players[myId] };\n delete localPatch.x;\n delete localPatch.y;\n delete localPatch.direction;\n delete localPatch._frames;\n payload.players = {\n ...players,\n [myId]: localPatch,\n };\n }\n\n return payload;\n }\n\n private shouldPreserveLocalPlayerPosition(localPatch?: any): boolean {\n if (!localPatch) {\n return false;\n }\n if (this.predictionEnabled && !!this.prediction?.hasPendingInputs()) {\n return true;\n }\n return this.shouldKeepLocalPlayerMovement();\n }\n\n private shouldKeepLocalPlayerMovement(): boolean {\n if (!this.localMovementAuthority || this.mapTransitionInProgress) {\n return false;\n }\n const myId = this.playerIdSignal();\n const player = myId ? this.sceneMap?.players?.()?.[myId] : undefined;\n if (!player) {\n return false;\n }\n if (this.prediction?.hasPendingInputs()) {\n return true;\n }\n return Date.now() - this.lastLocalMovementInputAt <= this.LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS;\n }\n\n private normalizeAckWithSyncState(\n ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },\n syncData: any,\n ): { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction } {\n const myId = this.playerIdSignal();\n if (!myId) {\n return ack;\n }\n\n const localPatch = syncData?.players?.[myId];\n if (typeof localPatch?.x !== \"number\" || typeof localPatch?.y !== \"number\") {\n return ack;\n }\n\n return {\n ...ack,\n x: localPatch.x,\n y: localPatch.y,\n direction: localPatch.direction ?? ack.direction,\n };\n }\n\n private initListeners() {\n if (this.socketListenersInitialized) return;\n this.socketListenersInitialized = true;\n\n this.webSocket.on(\"sync\", (data) => {\n if (!this.tick) {\n this.pendingSyncPackets.push(data);\n return;\n }\n this.applySyncPacket(data);\n });\n\n // Handle pong responses for RTT measurement\n this.webSocket.on(\"pong\", (data: { serverTick: number; clientFrame: number; clientTime: number }) => {\n const now = Date.now();\n this.rtt = now - data.clientTime;\n\n // Calculate frame offset: how many ticks ahead the server is compared to our frame counter\n // This helps us estimate which server tick corresponds to each client input frame\n const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT\n const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;\n this.updateServerTickEstimate(estimatedServerTickNow, now);\n\n // Update frame offset (only if we have inputs to calibrate with)\n if (this.inputFrameCounter > 0) {\n this.frameOffset = estimatedServerTickNow - data.clientFrame;\n }\n\n console.debug(`[Ping/Pong] RTT: ${this.rtt}ms, ServerTick: ${data.serverTick}, FrameOffset: ${this.frameOffset}`);\n });\n\n this.webSocket.on(\"changeMap\", (data) => {\n if (!this.clientReadyForMapChanges) {\n this.pendingMapChanges.push(data);\n return;\n }\n this.handleChangeMap(data);\n });\n\n this.webSocket.on(\"showComponentAnimation\", (data) => {\n const { params, object, position, id } = data;\n if (!object && position === undefined) {\n throw new Error(\"Please provide an object or x and y coordinates\");\n }\n const player = object ? this.sceneMap.getObjectById(object) : undefined;\n this.getComponentAnimation(id).displayEffect(params, player || position)\n });\n\n this.webSocket.on(\"clientVisual\", (data) => {\n this.playClientVisual(data);\n });\n\n this.webSocket.on(\"projectile:spawnBatch\", (data) => {\n if (!this.shouldProcessProjectilePacket(data)) return;\n this.projectiles.spawnBatch(data?.projectiles ?? [], {\n mapId: data?.mapId,\n currentServerTick: this.estimateServerTick(),\n tickDurationMs: this.getPhysicsTickDurationMs(),\n });\n });\n\n this.webSocket.on(\"projectile:impactBatch\", (data) => {\n if (!this.shouldProcessProjectilePacket(data)) return;\n this.projectiles.impactBatch(data?.impacts ?? [], {\n mapId: data?.mapId,\n });\n });\n\n this.webSocket.on(\"projectile:destroyBatch\", (data) => {\n if (!this.shouldProcessProjectilePacket(data)) return;\n this.projectiles.destroyBatch(data?.projectiles ?? [], {\n mapId: data?.mapId,\n });\n });\n\n this.webSocket.on(\"projectile:clear\", (data) => {\n if (!this.shouldProcessProjectilePacket(data)) return;\n this.projectiles.clear();\n });\n\n this.webSocket.on(\"notification\", (data) => {\n this.notificationManager.add(data);\n });\n\n this.webSocket.on(\"setAnimation\", (data) => {\n const {\n animationName,\n nbTimes,\n object,\n graphic,\n restoreAnimationName,\n restoreGraphics,\n } = data;\n const player = object ? this.sceneMap.getObjectById(object) : undefined;\n if (!player) return;\n const restoreOptions = {\n restoreAnimationName,\n restoreGraphics,\n };\n if (graphic !== undefined) {\n player.setAnimation(animationName, graphic, nbTimes, restoreOptions);\n } else {\n player.setAnimation(animationName, nbTimes, restoreOptions);\n }\n })\n\n this.webSocket.on(\"playSound\", (data) => {\n const { soundId, volume, loop } = data;\n this.playSound(soundId, { volume, loop });\n });\n\n this.webSocket.on(\"stopSound\", (data) => {\n const { soundId } = data;\n this.stopSound(soundId);\n });\n\n this.webSocket.on(\"stopAllSounds\", () => {\n this.stopAllSounds();\n });\n\n this.webSocket.on(\"cameraFollow\", (data) => {\n const { targetId, smoothMove } = data;\n this.setCameraFollow(targetId, smoothMove);\n });\n\n this.webSocket.on(\"flash\", (data) => {\n const { object, type, duration, cycles, alpha, tint } = data;\n const sprite = object ? this.sceneMap.getObjectById(object) : undefined;\n if (sprite && typeof sprite.flash === 'function') {\n sprite.flash({ type, duration, cycles, alpha, tint });\n }\n });\n\n this.webSocket.on(\"shakeMap\", (data) => {\n const { intensity, duration, frequency, direction } = data || {};\n this.mapShakeTrigger.start({\n intensity,\n duration,\n frequency,\n direction\n });\n });\n\n this.webSocket.on(\"weatherState\", (data) => {\n const raw = (data && typeof data === \"object\" && \"value\" in data)\n ? (data as any).value\n : data;\n\n if (raw === null) {\n this.sceneMap.weatherState.set(null);\n return;\n }\n\n const validEffects = [\"rain\", \"snow\", \"fog\", \"cloud\"];\n if (!raw || !validEffects.includes((raw as any).effect)) {\n return;\n }\n\n this.sceneMap.weatherState.set({\n effect: (raw as any).effect,\n preset: (raw as any).preset,\n params: (raw as any).params,\n transitionMs: (raw as any).transitionMs,\n durationMs: (raw as any).durationMs,\n startedAt: (raw as any).startedAt,\n seed: (raw as any).seed,\n });\n });\n\n this.webSocket.on(\"lightingState\", (data) => {\n const raw = (data && typeof data === \"object\" && \"value\" in data)\n ? (data as any).value\n : data;\n\n this.sceneMap.lightingState.set(normalizeLightingState(raw));\n });\n\n this.webSocket.on('open', () => {\n this.hooks.callHooks(\"client-engine-onConnected\", this, this.socket).subscribe();\n // Start ping/pong for synchronization\n this.startPingPong();\n })\n\n this.webSocket.on('close', () => {\n this.hooks.callHooks(\"client-engine-onDisconnected\", this, this.socket).subscribe();\n // Stop ping/pong when disconnected\n this.stopPingPong();\n })\n\n this.webSocket.on('error', (error) => {\n void this.callConnectError(error);\n })\n }\n\n private beginMapTransfer(nextMapId?: string) {\n this.mapTransitionInProgress = true;\n this.currentMapRoomId = nextMapId;\n this.sceneResetQueued = false;\n this.clearClientPredictionStates();\n this.sceneMap.weatherState.set(null);\n this.sceneMap.lightingState.set(null);\n this.sceneMap.clearLightSpots();\n this.clearComponentAnimations();\n this.projectiles.setMapId(nextMapId);\n this.cameraFollowTargetId.set(null);\n this.sceneMap.reset();\n this.sceneMap.loadPhysic();\n }\n\n private clearComponentAnimations() {\n this.componentAnimations.forEach((componentAnimation) => {\n componentAnimation.instance?.clear?.();\n });\n }\n\n private shouldProcessProjectilePacket(data: any): boolean {\n if (this.mapTransitionInProgress) return false;\n const packetMapId = normalizeRoomMapId(\n typeof data?.mapId === \"string\" ? data.mapId : undefined,\n );\n const currentMapId = normalizeRoomMapId(this.currentMapRoomId);\n return !packetMapId || !currentMapId || packetMapId === currentMapId;\n }\n\n private async callConnectError(error: any) {\n await lastValueFrom(this.hooks.callHooks(\"client-engine-onConnectError\", this, error, this.socket));\n }\n\n private flushPendingSyncPackets() {\n const packets = this.pendingSyncPackets;\n this.pendingSyncPackets = [];\n packets.forEach((packet) => this.applySyncPacket(packet));\n }\n\n private flushPendingMapChanges() {\n const packets = this.pendingMapChanges;\n this.pendingMapChanges = [];\n packets.forEach((packet) => this.handleChangeMap(packet));\n }\n\n private handleChangeMap(data: any) {\n const nextMapId = typeof data?.mapId === \"string\" ? data.mapId : undefined;\n this.beginMapTransfer(nextMapId);\n const transferToken = typeof data?.transferToken === \"string\" ? data.transferToken : undefined;\n this.loadScene(data.mapId, transferToken);\n }\n\n private applySyncPacket(data: any) {\n if (data.pId) {\n this.playerIdSignal.set(data.pId);\n // Signal that player ID was received\n this.playerIdReceived$.next(true);\n }\n\n if (this.sceneResetQueued) {\n const weatherState = this.sceneMap.weatherState();\n const lightingState = this.sceneMap.lightingState();\n this.sceneMap.reset();\n this.sceneMap.weatherState.set(weatherState);\n this.sceneMap.lightingState.set(lightingState);\n this.sceneMap.loadPhysic();\n this.sceneResetQueued = false;\n }\n\n // Apply client-side prediction filtering and server reconciliation\n this.hooks.callHooks(\"client-sceneMap-onChanges\", this.sceneMap, { partial: data }).subscribe();\n\n const ack = data?.ack;\n const normalizedAck =\n ack && typeof ack.frame === \"number\"\n ? this.normalizeAckWithSyncState(ack, data)\n : undefined;\n const payload = this.prepareSyncPayload(data);\n load(this.sceneMap, payload, true);\n\n if (normalizedAck) {\n this.applyServerAck(normalizedAck);\n }\n\n for (const playerId in payload.players ?? {}) {\n const player = payload.players[playerId]\n if (!player._param) continue\n for (const param in player._param) {\n this.sceneMap.players()[playerId]._param()[param] = player._param[param]\n }\n }\n\n // Check if players and events are present in sync data\n const players = payload.players || this.sceneMap.players();\n if (players && Object.keys(players).length > 0) {\n this.playersReceived$.next(true);\n }\n\n const events = payload.events || this.sceneMap.events();\n if (events !== undefined) {\n this.eventsReceived$.next(true);\n }\n }\n\n /**\n * Start periodic ping/pong for client-server synchronization\n * \n * Sends ping requests to the server to measure round-trip time (RTT) and\n * calculate the frame offset between client and server ticks.\n * \n * ## Design\n * \n * - Sends ping every 5 seconds\n * - Measures RTT for latency compensation\n * - Calculates frame offset to map client frames to server ticks\n * - Used for accurate server reconciliation\n * \n * @example\n * ```ts\n * // Called automatically when connection opens\n * this.startPingPong();\n * ```\n */\n private startPingPong(): void {\n // Stop existing interval if any\n this.stopPingPong();\n\n // Send initial ping immediately\n this.sendPing();\n\n // Set up periodic pings\n this.pingInterval = setInterval(() => {\n this.sendPing();\n }, this.PING_INTERVAL_MS);\n }\n\n /**\n * Stop periodic ping/pong\n * \n * Stops the ping interval when disconnecting or changing maps.\n * \n * @example\n * ```ts\n * // Called automatically when connection closes\n * this.stopPingPong();\n * ```\n */\n private stopPingPong(): void {\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n }\n\n /**\n * Send a ping request to the server\n * \n * Sends current client time and frame counter to the server,\n * which will respond with its server tick for synchronization.\n * \n * @example\n * ```ts\n * // Send a ping to measure RTT\n * this.sendPing();\n * ```\n */\n private sendPing(): void {\n const clientTime = Date.now();\n const clientFrame = this.getPhysicsTick();\n\n this.webSocket.emit('ping', {\n clientTime,\n clientFrame\n });\n }\n\n private async loadScene(mapId: string, transferToken?: string) {\n await lastValueFrom(this.hooks.callHooks(\"client-sceneMap-onBeforeLoading\", this.sceneMap));\n\n // Clear client prediction states when changing maps\n this.clearClientPredictionStates();\n\n // Reset all conditions for new map loading\n this.mapLoadCompleted$.next(false);\n this.playerIdReceived$.next(false);\n this.playersReceived$.next(false);\n this.eventsReceived$.next(false);\n\n // Unsubscribe previous subscription if exists\n if (this.onAfterLoadingSubscription) {\n this.onAfterLoadingSubscription.unsubscribe();\n }\n\n // Setup RxJS observable to wait for all conditions\n this.setupOnAfterLoadingObserver();\n\n this.webSocket.updateProperties({\n room: mapId,\n query: transferToken ? { transferToken } : undefined,\n })\n try {\n await this.webSocket.reconnect()\n }\n catch (error) {\n this.mapTransitionInProgress = false;\n this.stopPingPong();\n await this.callConnectError(error);\n throw error;\n }\n const res = await this.loadMapService.load(mapId)\n const loadedLighting = typeof res?.lighting !== \"undefined\"\n ? res.lighting\n : res?.data?.lighting;\n if (typeof loadedLighting !== \"undefined\") {\n this.sceneMap.lightingState.set(normalizeLightingState(loadedLighting));\n }\n this.sceneMap.data.set(res)\n \n // Check if playerId is already present\n if (this.playerIdSignal()) {\n this.playerIdReceived$.next(true);\n }\n \n // Check if players and events are already present in sceneMap\n const players = this.sceneMap.players();\n if (players && Object.keys(players).length > 0) {\n this.playersReceived$.next(true);\n }\n \n const events = this.sceneMap.events();\n if (events !== undefined) {\n this.eventsReceived$.next(true);\n }\n \n // Signal that map loading is completed (this should be last to ensure other checks are done)\n this.mapLoadCompleted$.next(true);\n this.currentMapRoomId = mapId;\n this.mapTransitionInProgress = false;\n this.sceneMap.configureClientPrediction(this.predictionEnabled);\n this.sceneMap.loadPhysic()\n }\n\n addSpriteSheet<T = any>(spritesheetClass: any, id?: string): any {\n this.spritesheets.set(id || spritesheetClass.id, spritesheetClass);\n return spritesheetClass as any;\n }\n\n /**\n * Set a resolver function for spritesheets\n * \n * The resolver is called when a spritesheet is requested but not found in the cache.\n * It can be synchronous (returns directly) or asynchronous (returns a Promise).\n * The resolved spritesheet is automatically cached for future use.\n * \n * @param resolver - Function that takes a spritesheet ID and returns a spritesheet or Promise of spritesheet\n * \n * @example\n * ```ts\n * // Synchronous resolver\n * engine.setSpritesheetResolver((id) => {\n * if (id === 'dynamic-sprite') {\n * return { id: 'dynamic-sprite', image: 'path/to/image.png', framesWidth: 32, framesHeight: 32 };\n * }\n * return undefined;\n * });\n * \n * // Asynchronous resolver (loading from API)\n * engine.setSpritesheetResolver(async (id) => {\n * const response = await fetch(`/api/spritesheets/${id}`);\n * const data = await response.json();\n * return data;\n * });\n * ```\n */\n setSpritesheetResolver(resolver: (id: string | number) => any | Promise<any>): void {\n this.spritesheetResolver = resolver;\n }\n\n /**\n * Get a spritesheet by ID, using resolver if not found in cache\n * \n * This method first checks if the spritesheet exists in the cache.\n * If not found and a resolver is set, it calls the resolver to create the spritesheet.\n * The resolved spritesheet is automatically cached for future use.\n * \n * @param id - The spritesheet ID or legacy tile ID to retrieve\n * @returns The spritesheet if found or created, or undefined if not found and no resolver\n * @returns Promise<any> if the resolver is asynchronous\n * \n * @example\n * ```ts\n * // Synchronous usage\n * const spritesheet = engine.getSpriteSheet('my-sprite');\n * \n * // Asynchronous usage (when resolver returns Promise)\n * const spritesheet = await engine.getSpriteSheet('dynamic-sprite');\n * ```\n */\n getSpriteSheet(id: string | number): any | Promise<any> {\n // Check cache first\n if (this.spritesheets.has(id)) {\n return this.spritesheets.get(id);\n }\n\n // If not in cache and resolver exists, use it\n if (this.spritesheetResolver) {\n if (this.spritesheetPromises.has(id)) {\n return this.spritesheetPromises.get(id);\n }\n\n const result = this.spritesheetResolver(id);\n\n // Check if result is a Promise\n if (result instanceof Promise) {\n const promise = result\n .then((spritesheet) => {\n if (spritesheet) {\n // Cache the resolved spritesheet\n this.spritesheets.set(id, spritesheet);\n }\n this.spritesheetPromises.delete(id);\n return spritesheet;\n })\n .catch((error) => {\n this.spritesheetPromises.delete(id);\n throw error;\n });\n this.spritesheetPromises.set(id, promise);\n return promise;\n } else {\n // Synchronous result\n if (result) {\n // Cache the resolved spritesheet\n this.spritesheets.set(id, result);\n }\n return result;\n }\n }\n\n // No resolver and not in cache\n return undefined;\n }\n\n /**\n * Add a sound to the engine\n * \n * Adds a sound to the engine's sound cache. The sound can be:\n * - A simple object with `id` and `src` properties\n * - A Howler instance\n * - An object with a `play()` method\n * \n * If the sound has a `src` property, a Howler instance will be created automatically.\n * \n * @param sound - The sound object or Howler instance\n * @param id - Optional sound ID (if not provided, uses sound.id)\n * @returns The added sound\n * \n * @example\n * ```ts\n * // Simple sound object\n * engine.addSound({ id: 'click', src: 'click.mp3' });\n * \n * // With explicit ID\n * engine.addSound({ src: 'music.mp3' }, 'background-music');\n * ```\n */\n addSound(sound: any, id?: string): any {\n const soundId = id || sound.id;\n \n if (!soundId) {\n console.warn('Sound added without an ID. It will not be retrievable.');\n return sound;\n }\n\n // If sound has a src property, create a Howler instance\n if (sound.src && typeof sound.src === 'string') {\n const howlOptions: any = {\n src: [sound.src],\n loop: sound.loop || false,\n volume: sound.volume !== undefined ? sound.volume : 1.0,\n };\n\n const howl = new (Howl as any).Howl(howlOptions);\n this.sounds.set(soundId, howl);\n return howl;\n }\n\n // If sound already has a play method (Howler instance or custom), use it directly\n if (sound && typeof sound.play === 'function') {\n this.sounds.set(soundId, sound);\n return sound;\n }\n\n // Otherwise, store as-is\n this.sounds.set(soundId, sound);\n return sound;\n }\n\n /**\n * Set a resolver function for sounds\n * \n * The resolver is called when a sound is requested but not found in the cache.\n * It can be synchronous (returns directly) or asynchronous (returns a Promise).\n * The resolved sound is automatically cached for future use.\n * \n * @param resolver - Function that takes a sound ID and returns a sound or Promise of sound\n * \n * @example\n * ```ts\n * // Synchronous resolver\n * engine.setSoundResolver((id) => {\n * if (id === 'dynamic-sound') {\n * return { id: 'dynamic-sound', src: 'path/to/sound.mp3' };\n * }\n * return undefined;\n * });\n * \n * // Asynchronous resolver (loading from API)\n * engine.setSoundResolver(async (id) => {\n * const response = await fetch(`/api/sounds/${id}`);\n * const data = await response.json();\n * return data;\n * });\n * ```\n */\n setSoundResolver(resolver: (id: string) => any | Promise<any>): void {\n this.soundResolver = resolver;\n }\n\n /**\n * Get a sound by ID, using resolver if not found in cache\n * \n * This method first checks if the sound exists in the cache.\n * If not found and a resolver is set, it calls the resolver to create the sound.\n * The resolved sound is automatically cached for future use.\n * \n * @param id - The sound ID to retrieve\n * @returns The sound if found or created, or undefined if not found and no resolver\n * @returns Promise<any> if the resolver is asynchronous\n * \n * @example\n * ```ts\n * // Synchronous usage\n * const sound = engine.getSound('my-sound');\n * \n * // Asynchronous usage (when resolver returns Promise)\n * const sound = await engine.getSound('dynamic-sound');\n * ```\n */\n getSound(id: string): any | Promise<any> {\n // Check cache first\n if (this.sounds.has(id)) {\n return this.sounds.get(id);\n }\n\n // If not in cache and resolver exists, use it\n if (this.soundResolver) {\n const result = this.soundResolver(id);\n\n // Check if result is a Promise\n if (result instanceof Promise) {\n return result.then((sound) => {\n if (sound) {\n // Cache the resolved sound\n this.sounds.set(id, sound);\n }\n return sound;\n });\n } else {\n // Synchronous result\n if (result) {\n // Cache the resolved sound\n this.sounds.set(id, result);\n }\n return result;\n }\n }\n\n // No resolver and not in cache\n return undefined;\n }\n\n /**\n * Play a sound by its ID\n * \n * This method retrieves a sound from the cache or resolver and plays it.\n * If the sound is not found, it will attempt to resolve it using the soundResolver.\n * Uses Howler.js for audio playback instead of native Audio elements.\n * \n * @param soundId - The sound ID to play\n * @param options - Optional sound configuration\n * @param options.volume - Volume level (0.0 to 1.0, overrides sound default)\n * @param options.loop - Whether the sound should loop (overrides sound default)\n * \n * @example\n * ```ts\n * // Play a sound synchronously\n * engine.playSound('item-pickup');\n * \n * // Play a sound with volume and loop\n * engine.playSound('background-music', { volume: 0.5, loop: true });\n * \n * // Play a sound asynchronously (when resolver returns Promise)\n * await engine.playSound('dynamic-sound', { volume: 0.8 });\n * ```\n */\n async playSound(soundId: string, options?: { volume?: number; loop?: boolean }): Promise<void> {\n const sound = await this.getSound(soundId);\n if (sound && sound.play) {\n // Sound is already a Howler instance or has a play method\n const howlSoundId = sound._sounds?.[0]?._id;\n \n // Apply volume if provided\n if (options?.volume !== undefined) {\n if (howlSoundId !== undefined) {\n sound.volume(Math.max(0, Math.min(1, options.volume)), howlSoundId);\n } else {\n sound.volume(Math.max(0, Math.min(1, options.volume)));\n }\n }\n \n // Apply loop if provided\n if (options?.loop !== undefined) {\n if (howlSoundId !== undefined) {\n sound.loop(options.loop, howlSoundId);\n } else {\n sound.loop(options.loop);\n }\n }\n \n if (howlSoundId !== undefined) {\n sound.play(howlSoundId);\n } else {\n sound.play();\n }\n } else if (sound && sound.src) {\n // If sound is just a source URL, create a Howler instance and cache it\n const howlOptions: any = {\n src: [sound.src],\n loop: options?.loop !== undefined ? options.loop : (sound.loop || false),\n volume: options?.volume !== undefined ? Math.max(0, Math.min(1, options.volume)) : (sound.volume !== undefined ? sound.volume : 1.0),\n };\n\n const howl = new (Howl as any).Howl(howlOptions);\n \n // Cache the Howler instance for future use\n this.sounds.set(soundId, howl);\n \n // Play the sound\n howl.play();\n } else {\n console.warn(`Sound with id \"${soundId}\" not found or cannot be played`);\n }\n }\n\n /**\n * Stop a sound that is currently playing\n * \n * This method stops a sound that was previously started with `playSound()`.\n * \n * @param soundId - The sound ID to stop\n * \n * @example\n * ```ts\n * // Start a looping sound\n * engine.playSound('background-music', { loop: true });\n * \n * // Later, stop it\n * engine.stopSound('background-music');\n * ```\n */\n stopSound(soundId: string): void {\n const sound = this.sounds.get(soundId);\n if (sound && sound.stop) {\n sound.stop();\n } else {\n console.warn(`Sound with id \"${soundId}\" not found or cannot be stopped`);\n }\n }\n\n /**\n * Stop all currently playing sounds\n * \n * This method stops all sounds that are currently playing.\n * Useful when changing maps to prevent sound overlap.\n * \n * @example\n * ```ts\n * // Stop all sounds\n * engine.stopAllSounds();\n * ```\n */\n stopAllSounds(): void {\n this.sounds.forEach((sound) => {\n if (sound && sound.stop) {\n sound.stop();\n }\n });\n }\n\n /**\n * Set the camera to follow a specific sprite\n * \n * This method changes which sprite the camera viewport should follow.\n * The camera will smoothly animate to the target sprite if smoothMove options are provided.\n * \n * ## Design\n * \n * The camera follow target is stored in a signal that is read by sprite components.\n * Each sprite checks if it should be followed by comparing its ID with the target ID.\n * When smoothMove options are provided, the viewport animation is handled by CanvasEngine's\n * viewport system.\n * \n * @param targetId - The ID of the sprite to follow. Set to null to follow the current player\n * @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease\n * @param smoothMove.time - Duration of the animation in milliseconds (optional)\n * @param smoothMove.ease - Easing function name from https://easings.net (optional)\n * \n * @example\n * ```ts\n * // Follow another player with default smooth animation\n * engine.setCameraFollow(otherPlayerId, true);\n * \n * // Follow an event with custom smooth animation\n * engine.setCameraFollow(eventId, {\n * time: 1000,\n * ease: \"easeInOutQuad\"\n * });\n * \n * // Follow without animation (instant)\n * engine.setCameraFollow(targetId, false);\n * \n * // Return to following current player\n * engine.setCameraFollow(null);\n * ```\n */\n setCameraFollow(\n targetId: string | null,\n smoothMove?: boolean | { time?: number; ease?: string }\n ): void {\n // Store smoothMove options for potential future use with viewport animation\n // For now, we just set the target ID and let CanvasEngine handle the viewport follow\n // The smoothMove options could be used to configure viewport animation if CanvasEngine supports it\n this.cameraFollowTargetId.set(targetId);\n \n // If smoothMove is an object, we could store it for viewport configuration\n // This would require integration with CanvasEngine's viewport animation system\n if (typeof smoothMove === \"object\" && smoothMove !== null) {\n // Future: Apply smoothMove.time and smoothMove.ease to viewport animation\n // For now, CanvasEngine handles viewport following automatically\n }\n }\n\n addParticle(particle: any) {\n this.particleSettings.emitters.push(particle)\n return particle;\n }\n\n /**\n * Add a component to render behind sprites\n * Components added with this method will be displayed with a lower z-index than the sprite\n * \n * Supports multiple formats:\n * 1. Direct component: `ShadowComponent`\n * 2. Configuration object: `{ component: LightHalo, props: {...} }`\n * 3. With dynamic props: `{ component: LightHalo, props: (object) => {...} }`\n * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`\n * \n * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).\n * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.\n * \n * @param component - The component to add behind sprites, or a configuration object\n * @param component.component - The component function to render\n * @param component.props - Static props object or function that receives the sprite object and returns props\n * @param component.dependencies - Function that receives the sprite object and returns an array of Signals\n * @returns The added component or configuration\n * \n * @example\n * ```ts\n * // Add a shadow component behind all sprites\n * engine.addSpriteComponentBehind(ShadowComponent);\n * \n * // Add a component with static props\n * engine.addSpriteComponentBehind({ \n * component: LightHalo, \n * props: { radius: 30 } \n * });\n * \n * // Add a component with dynamic props and dependencies\n * engine.addSpriteComponentBehind({ \n * component: HealthBar, \n * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),\n * dependencies: (object) => [object.hp, object.param.maxHp]\n * });\n * ```\n */\n addSpriteComponentBehind(component: any) {\n this.spriteComponentsBehind.update((components: any[]) => [...components, component])\n return component\n }\n\n /**\n * Add a component to render in front of sprites\n * Components added with this method will be displayed with a higher z-index than the sprite\n * \n * Supports multiple formats:\n * 1. Direct component: `HealthBarComponent`\n * 2. Configuration object: `{ component: StatusIndicator, props: {...} }`\n * 3. With dynamic props: `{ component: HealthBar, props: (object) => {...} }`\n * 4. With dependencies: `{ component: HealthBar, dependencies: (object) => [object.hp, object.param.maxHp] }`\n * \n * Components with dependencies will only be displayed when all dependencies are resolved (!= undefined).\n * The object (sprite) is passed to the dependencies function to allow sprite-specific dependency resolution.\n * \n * @param component - The component to add in front of sprites, or a configuration object\n * @param component.component - The component function to render\n * @param component.props - Static props object or function that receives the sprite object and returns props\n * @param component.dependencies - Function that receives the sprite object and returns an array of Signals\n * @returns The added component or configuration\n * \n * @example\n * ```ts\n * // Add a health bar component in front of all sprites\n * engine.addSpriteComponentInFront(HealthBarComponent);\n * \n * // Add a component with static props\n * engine.addSpriteComponentInFront({ \n * component: StatusIndicator, \n * props: { type: 'poison' } \n * });\n * \n * // Add a component with dynamic props and dependencies\n * engine.addSpriteComponentInFront({ \n * component: HealthBar, \n * props: (object) => ({ hp: object.hp(), maxHp: object.param.maxHp() }),\n * dependencies: (object) => [object.hp, object.param.maxHp]\n * });\n * ```\n */\n addSpriteComponentInFront(component: any | { component: any, props: (object: any) => any, dependencies?: (object: any) => any[] }) {\n this.spriteComponentsInFront.update((components: any[]) => [...components, component])\n return component\n }\n\n /**\n * Register a reusable sprite component that can be addressed by the server.\n *\n * Server-side component definitions only carry the component id and\n * serializable props. The client registry maps that id to the CanvasEngine\n * component that performs the actual rendering.\n *\n * @param id - Stable component id used by server component definitions\n * @param component - CanvasEngine component to render for this id\n * @returns The registered component\n *\n * @example\n * ```ts\n * engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);\n * ```\n */\n registerSpriteComponent(id: string, component: any) {\n this.spriteComponents.set(id, component);\n return component;\n }\n\n /**\n * Get a reusable sprite component by id.\n *\n * @param id - Component id registered on the client\n * @returns The CanvasEngine component, or undefined when missing\n */\n getSpriteComponent(id: string) {\n return this.spriteComponents.get(id);\n }\n\n /**\n * Register a custom event component resolver.\n *\n * The last resolver returning a component wins. This lets later modules\n * override earlier defaults without replacing the whole map scene.\n *\n * @param resolver - Function receiving the synced event object\n * @returns The registered resolver\n */\n addEventComponentResolver(resolver: EventComponentResolver) {\n return this.eventComponentResolvers.add(resolver);\n }\n\n /**\n * Resolve the custom CanvasEngine component for an event, if any.\n *\n * @param event - Synced client event object\n * @returns The component/config returned by the last matching resolver\n */\n resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null {\n return this.eventComponentResolvers.resolve(event);\n }\n\n registerProjectileComponent(type: string, component: any) {\n return this.projectiles.register(type, component);\n }\n\n getProjectileComponent(type: string) {\n return this.projectiles.get(type);\n }\n\n /**\n * Register a named client visual macro.\n *\n * Client visuals are small client-side functions that group existing visual\n * primitives such as flash, sound, component animations, sprite animation, or\n * map shake. The server sends only the visual name and a serializable payload.\n *\n * @param name - Stable visual name sent by the server\n * @param handler - Client-side visual handler\n * @returns The registered handler\n */\n registerClientVisual(name: string, handler: ClientVisualHandler) {\n return this.clientVisuals.register(name, handler);\n }\n\n /**\n * Register several named client visual macros.\n *\n * @param visuals - Map of visual names to client-side handlers\n */\n registerClientVisuals(visuals: ClientVisualMap) {\n this.clientVisuals.registerMany(visuals);\n }\n\n /**\n * Play a registered client visual locally.\n *\n * This is also used by the websocket listener when the server calls\n * `player.clientVisual()` or `map.clientVisual()`.\n *\n * @param packet - Visual name and serializable payload\n */\n playClientVisual(packet: ClientVisualPacket) {\n return this.clientVisuals.play(packet, this);\n }\n\n /**\n * Add a component animation to the engine\n * \n * Component animations are temporary visual effects that can be displayed\n * on sprites or objects, such as hit indicators, spell effects, or status animations.\n * \n * @param componentAnimation - The component animation configuration\n * @param componentAnimation.id - Unique identifier for the animation\n * @param componentAnimation.component - The component function to render\n * @returns The added component animation configuration\n * \n * @example\n * ```ts\n * // Add a hit animation component\n * engine.addComponentAnimation({\n * id: 'hit',\n * component: HitComponent\n * });\n * \n * // Add an explosion effect component\n * engine.addComponentAnimation({\n * id: 'explosion',\n * component: ExplosionComponent\n * });\n * ```\n */\n addComponentAnimation(componentAnimation: {\n component: any,\n id: string\n }) {\n const instance = new AnimationManager()\n this.componentAnimations.push({\n id: componentAnimation.id,\n component: componentAnimation.component,\n instance: instance,\n current: instance.current\n })\n return componentAnimation;\n }\n\n /**\n * Get a component animation by its ID\n * \n * Retrieves the EffectManager instance for a specific component animation,\n * which can be used to display the animation on sprites or objects.\n * \n * @param id - The unique identifier of the component animation\n * @returns The EffectManager instance for the animation\n * @throws Error if the component animation is not found\n * \n * @example\n * ```ts\n * // Get the hit animation and display it\n * const hitAnimation = engine.getComponentAnimation('hit');\n * hitAnimation.displayEffect({ text: \"Critical!\" }, player);\n * ```\n */\n getComponentAnimation(id: string): AnimationManager {\n const componentAnimation = this.componentAnimations.find((componentAnimation) => componentAnimation.id === id)\n if (!componentAnimation) {\n throw new Error(`Component animation with id ${id} not found`)\n }\n return componentAnimation.instance\n }\n\n /**\n * Start a transition\n * \n * Convenience method to display a transition by its ID using the GUI system.\n * \n * @param id - The unique identifier of the transition to start\n * @param props - Props to pass to the transition component\n * \n * @example\n * ```ts\n * // Start a fade transition\n * engine.startTransition('fade', { duration: 1000, color: 'black' });\n * \n * // Start with onFinish callback\n * engine.startTransition('fade', {\n * duration: 1000,\n * onFinish: () => console.log('Fade complete')\n * });\n *\n * // Wait until the transition component calls onFinish\n * await engine.startTransition('fade', { duration: 1000 });\n * ```\n */\n startTransition(id: string, props: any = {}): Promise<void> {\n if (!this.guiService.exists(id)) {\n throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);\n }\n return new Promise<void>((resolve) => {\n let finished = false;\n const finish = (data?: any) => {\n if (finished) return;\n finished = true;\n props?.onFinish?.(data);\n resolve();\n };\n\n this.guiService.display(id, {\n ...props,\n onFinish: finish,\n });\n });\n }\n\n async processInput({ input }: { input: RpgMovementInput }) {\n if (this.stopProcessingInput) return;\n\n const currentPlayer = this.sceneMap.getCurrentPlayer() as any;\n const canMove =\n !currentPlayer ||\n getCanMoveValue(currentPlayer);\n if (!canMove) {\n this.interruptCurrentPlayerMovement(currentPlayer);\n return;\n }\n\n const timestamp = Date.now();\n const movementInput = isDashInput(input)\n ? normalizeDashInput(input, currentPlayer?.direction?.())\n : input;\n if (!movementInput) return;\n if (isDashInput(movementInput)) {\n const cooldown = movementInput.cooldown ?? DEFAULT_DASH_COOLDOWN_MS;\n if (timestamp < this.dashLockedUntil) return;\n this.dashLockedUntil = timestamp + cooldown;\n }\n this.lastLocalMovementInputAt = timestamp;\n\n let frame: number;\n let tick: number;\n if (this.predictionEnabled && this.prediction) {\n const meta = this.prediction.recordInput(movementInput, timestamp);\n frame = meta.frame;\n tick = meta.tick;\n } else {\n frame = ++this.inputFrameCounter;\n tick = this.getPhysicsTick();\n }\n this.inputFrameCounter = frame;\n this.hooks.callHooks(\"client-engine-onInput\", this, { input: movementInput, playerId: this.playerId }).subscribe();\n\n const bodyReady = this.ensureCurrentPlayerBody();\n if (currentPlayer && bodyReady) {\n this.applyPredictedMovementInput(currentPlayer, movementInput);\n if (this.predictionEnabled && this.prediction) {\n this.pendingPredictionFrames.push(frame);\n if (this.pendingPredictionFrames.length > 240) {\n this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);\n }\n }\n }\n\n this.emitMovePacket(movementInput, frame, tick, timestamp, true);\n this.lastInputTime = isDashInput(movementInput)\n ? Date.now() + (movementInput.duration ?? DEFAULT_DASH_DURATION_MS)\n : Date.now();\n }\n\n async processDash(input: Partial<RpgDashInput> = {}) {\n const currentPlayer = this.sceneMap.getCurrentPlayer() as any;\n const fallbackDirection =\n typeof currentPlayer?.direction === \"function\"\n ? currentPlayer.direction()\n : currentPlayer?.direction;\n const dashInput = normalizeDashInput(input, fallbackDirection);\n if (!dashInput) return;\n await this.processInput({ input: dashInput });\n }\n\n processAction(action: RpgActionName, data?: any): void;\n processAction(action: RpgActionInput): void;\n processAction(action: RpgActionName | RpgActionInput, data?: any): void {\n if (this.stopProcessingInput) return;\n const currentPlayer = this.sceneMap.getCurrentPlayer() as any;\n const canMove =\n !currentPlayer ||\n getCanMoveValue(currentPlayer);\n if (!canMove) return;\n\n const payload = normalizeActionInput(action as any, data);\n\n this.hooks.callHooks(\"client-engine-onInput\", this, {\n input: payload.action,\n action: payload.action,\n data: payload.data,\n playerId: this.playerId,\n }).subscribe();\n this.webSocket.emit('action', payload)\n }\n\n get PIXI() {\n return PIXI\n }\n\n get socket() {\n return this.webSocket\n }\n\n get playerId() {\n return this.playerIdSignal()\n }\n\n get scene() {\n return this.sceneMap\n }\n\n getObjectById(id: string) {\n return this.sceneMap?.getObjectById(id);\n }\n\n private getPhysicsTick(): number {\n return (this.sceneMap as any)?.getTick?.() ?? 0;\n }\n\n private getPhysicsTickDurationMs(): number {\n const timeStep = (this.sceneMap as any)?.physic?.getWorld?.()?.getTimeStep?.();\n return typeof timeStep === \"number\" && Number.isFinite(timeStep) && timeStep > 0\n ? timeStep * 1000\n : 1000 / 60;\n }\n\n private updateServerTickEstimate(serverTick: number | undefined, now = Date.now()): void {\n if (typeof serverTick !== \"number\" || !Number.isFinite(serverTick)) {\n return;\n }\n this.latestServerTick = serverTick;\n this.latestServerTickAt = now;\n }\n\n private estimateServerTick(now = Date.now()): number | undefined {\n if (typeof this.latestServerTick !== \"number\" || this.latestServerTickAt <= 0) {\n return undefined;\n }\n const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());\n return this.latestServerTick + elapsedTicks;\n }\n\n private predictProjectileImpact(projectile: ClientProjectileSpawn): ClientProjectileImpact | null {\n if (projectile.predictImpact === false) {\n return null;\n }\n const sceneMap = this.sceneMap as any;\n if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) {\n return null;\n }\n const origin = projectile.origin;\n const direction = projectile.direction;\n if (\n !origin ||\n !direction ||\n !Number.isFinite(origin.x) ||\n !Number.isFinite(origin.y) ||\n !Number.isFinite(direction.x) ||\n !Number.isFinite(direction.y) ||\n (direction.x === 0 && direction.y === 0)\n ) {\n return null;\n }\n\n const hit = sceneMap.physic.raycast(\n new Vector2(origin.x, origin.y),\n new Vector2(direction.x, direction.y),\n projectile.range,\n projectile.collisionMask,\n (entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId,\n );\n if (!hit) {\n return null;\n }\n return {\n id: projectile.id,\n targetId: hit.entity.uuid,\n x: hit.point.x,\n y: hit.point.y,\n distance: hit.distance,\n };\n }\n\n private ensureCurrentPlayerBody(): boolean {\n const player = this.sceneMap?.getCurrentPlayer();\n const myId = this.playerIdSignal();\n if (!player || !myId) {\n return false;\n }\n if (!player.id) {\n player.id = myId;\n }\n if (this.sceneMap.getBody(myId)) {\n return true;\n }\n try {\n this.sceneMap.loadPhysic();\n } catch (error) {\n console.error(\"[RPGJS] Unable to initialize client physics before input:\", error);\n return false;\n }\n return !!this.sceneMap.getBody(myId);\n }\n\n private stepClientPhysicsTick(): void {\n if (!this.predictionEnabled || !this.sceneMap) {\n return;\n }\n const now = Date.now();\n if (this.lastClientPhysicsStepAt === 0) {\n this.lastClientPhysicsStepAt = now;\n }\n const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));\n this.lastClientPhysicsStepAt = now;\n this.sceneMap.stepClientPhysics(deltaMs);\n }\n\n private flushPendingPredictedStates(): void {\n if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) {\n return;\n }\n const state = this.getLocalPlayerState();\n while (this.pendingPredictionFrames.length > 0) {\n const frame = this.pendingPredictionFrames.shift();\n if (typeof frame === \"number\") {\n this.prediction.attachPredictedState(frame, state);\n }\n }\n }\n\n private buildPendingMoveTrajectory(): MovementTrajectoryPoint[] {\n if (!this.predictionEnabled || !this.prediction) {\n return [];\n }\n const pendingInputs = this.prediction.getPendingInputs();\n const trajectory: MovementTrajectoryPoint[] = [];\n for (const entry of pendingInputs) {\n const state = entry.state;\n if (!state) continue;\n if (typeof state.x !== \"number\" || typeof state.y !== \"number\") continue;\n trajectory.push({\n frame: entry.frame,\n tick: entry.tick,\n timestamp: entry.timestamp,\n input: entry.direction,\n x: state.x,\n y: state.y,\n direction: state.direction ?? resolveMoveDirection(entry.direction),\n });\n }\n if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {\n return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);\n }\n return trajectory;\n }\n\n private emitMovePacket(\n input: RpgMovementInput,\n frame: number,\n tick: number,\n timestamp: number,\n force = false,\n ): void {\n const trajectory = this.buildPendingMoveTrajectory();\n const latestTrajectoryFrame =\n trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;\n const shouldThrottle =\n !force &&\n latestTrajectoryFrame <= this.lastMovePathSentFrame &&\n timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS;\n if (shouldThrottle) {\n return;\n }\n\n this.webSocket.emit(\"move\", {\n input,\n timestamp,\n frame,\n tick,\n trajectory,\n });\n this.lastMovePathSentAt = timestamp;\n this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);\n }\n\n private flushPendingMovePath(): void {\n if (!this.predictionEnabled || !this.prediction) {\n return;\n }\n const player = this.sceneMap?.getCurrentPlayer?.() as any;\n if (\n player &&\n !getCanMoveValue(player)\n ) {\n this.interruptCurrentPlayerMovement(player);\n return;\n }\n const pendingInputs = this.prediction.getPendingInputs();\n if (pendingInputs.length === 0) {\n return;\n }\n const latest = pendingInputs[pendingInputs.length - 1];\n if (!latest) {\n return;\n }\n const now = Date.now();\n if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) {\n return;\n }\n this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);\n }\n\n private applyPredictedMovementInput(\n player: any,\n input: RpgMovementInput\n ): boolean {\n if (isDashInput(input)) {\n const direction = vectorToDirection(input.direction);\n player.changeDirection(direction);\n return Boolean((this.sceneMap as any).dashBody?.(player, input));\n }\n\n const direction = resolveMoveDirection(input);\n if (!direction) return false;\n player.changeDirection(direction);\n return Boolean((this.sceneMap as any).moveBody?.(player, direction));\n }\n\n private getLocalPlayerState(): PredictionState<Direction> {\n const currentPlayer = this.sceneMap?.getCurrentPlayer();\n if (!currentPlayer) {\n return { x: 0, y: 0, direction: Direction.Down };\n }\n const topLeft = this.sceneMap.getBodyPosition(currentPlayer.id, \"top-left\");\n const x = topLeft?.x ?? currentPlayer.x();\n const y = topLeft?.y ?? currentPlayer.y();\n const direction = currentPlayer.direction();\n return { x, y, direction };\n }\n\n private applyAuthoritativeState(state: PredictionState<Direction>): void {\n const player = this.sceneMap?.getCurrentPlayer();\n if (!player) return;\n const hitbox = typeof player.hitbox === \"function\" ? player.hitbox() : player.hitbox;\n const width = hitbox?.w ?? 0;\n const height = hitbox?.h ?? 0;\n const updated = this.sceneMap.updateHitbox(player.id, state.x, state.y, width, height);\n if (!updated) {\n this.sceneMap.setBodyPosition(player.id, state.x, state.y, \"top-left\");\n }\n player.x.set(Math.round(state.x));\n player.y.set(Math.round(state.y));\n if (state.direction) {\n player.changeDirection(state.direction);\n }\n }\n\n private initializePredictionController(): void {\n if (!this.predictionEnabled) {\n this.prediction = undefined;\n this.sceneMap?.configureClientPrediction?.(false);\n return;\n }\n const configuredTtl = (this.globalConfig as any)?.prediction?.historyTtlMs;\n const historyTtlMs = typeof configuredTtl === \"number\" ? configuredTtl : 10000;\n const configuredMaxEntries = (this.globalConfig as any)?.prediction?.maxHistoryEntries;\n const maxHistoryEntries =\n typeof configuredMaxEntries === \"number\"\n ? configuredMaxEntries\n : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);\n this.sceneMap?.configureClientPrediction?.(true);\n this.prediction = new PredictionController<RpgMovementInput, Direction>({\n correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,\n historyTtlMs,\n maxHistoryEntries,\n getPhysicsTick: () => this.getPhysicsTick(),\n getCurrentState: () => this.getLocalPlayerState(),\n setAuthoritativeState: (state) => this.applyAuthoritativeState(state),\n });\n }\n\n getCurrentPlayer() {\n return this.sceneMap.getCurrentPlayer()\n }\n\n emitSceneMapHook(hookName: string, ...args: any[]): void {\n this.hooks.callHooks(`client-sceneMap-${hookName}`, ...args).subscribe();\n }\n\n /**\n * Setup RxJS observer to wait for all conditions before calling onAfterLoading hook\n * \n * This method uses RxJS `combineLatest` to wait for all conditions to be met,\n * regardless of the order in which they arrive:\n * 1. The map loading is completed (loadMapService.load is finished)\n * 2. We received a player ID (pId)\n * 3. Players array has at least one element\n * 4. Events property is present in the sync data\n * \n * Once all conditions are met, it uses `switchMap` to call the onAfterLoading hook once.\n * \n * ## Design\n * \n * Uses BehaviorSubjects to track each condition state, allowing events to arrive\n * in any order. The `combineLatest` operator waits until all observables emit `true`,\n * then `take(1)` ensures the hook is called only once, and `switchMap` handles\n * the hook execution.\n * \n * @example\n * ```ts\n * // Called automatically in loadScene to setup the observer\n * this.setupOnAfterLoadingObserver();\n * ```\n */\n private setupOnAfterLoadingObserver(): void {\n this.onAfterLoadingSubscription = combineLatest([\n this.mapLoadCompleted$.pipe(filter(completed => completed === true)),\n this.playerIdReceived$.pipe(filter(received => received === true)),\n this.playersReceived$.pipe(filter(received => received === true)),\n this.eventsReceived$.pipe(filter(received => received === true))\n ]).pipe(\n take(1), // Only execute once when all conditions are met\n switchMap(() => {\n // Call the hook and return the observable\n return this.hooks.callHooks(\"client-sceneMap-onAfterLoading\", this.sceneMap);\n })\n ).subscribe();\n }\n\n /**\n * Clear client prediction states for cleanup\n * \n * Removes old prediction states and input history to prevent memory leaks.\n * Should be called when changing maps or disconnecting.\n * \n * @example\n * ```ts\n * // Clear prediction states when changing maps\n * engine.clearClientPredictionStates();\n * ```\n */\n clearClientPredictionStates() {\n this.initializePredictionController();\n this.frameOffset = 0;\n this.inputFrameCounter = 0;\n this.pendingPredictionFrames = [];\n this.lastClientPhysicsStepAt = 0;\n this.lastMovePathSentAt = 0;\n this.lastMovePathSentFrame = 0;\n }\n\n /**\n * Stop local movement immediately and discard pending predicted movement.\n *\n * Use this before a blocking action such as an A-RPG attack, dialog, dash\n * startup, or any client-side state where already buffered movement inputs\n * must not be replayed after server reconciliation.\n *\n * @param player - Player object to stop. Defaults to the current player.\n * @returns `true` when a player was found and interrupted.\n *\n * @example\n * ```ts\n * engine.interruptCurrentPlayerMovement();\n * ```\n */\n interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {\n if (!player) {\n return false;\n }\n (this.sceneMap as any)?.stopMovement?.(player);\n this.prediction?.clearPendingInputs();\n this.pendingPredictionFrames = [];\n this.lastInputTime = 0;\n this.lastMovePathSentAt = Date.now();\n this.lastMovePathSentFrame = this.inputFrameCounter;\n return true;\n }\n\n /**\n * Trigger a flash animation on a sprite\n * \n * This method allows you to trigger a flash effect on any sprite from client-side code.\n * The flash can be configured with various options including type (alpha, tint, or both),\n * duration, cycles, and color.\n * \n * ## Design\n * \n * The flash is applied directly to the sprite object using its flash trigger.\n * This is useful for client-side visual feedback, UI interactions, or local effects\n * that don't need to be synchronized with the server.\n * \n * @param spriteId - The ID of the sprite to flash. If not provided, flashes the current player\n * @param options - Flash configuration options\n * @param options.type - Type of flash effect: 'alpha' (opacity), 'tint' (color), or 'both' (default: 'alpha')\n * @param options.duration - Duration of the flash animation in milliseconds (default: 300)\n * @param options.cycles - Number of flash cycles (flash on/off) (default: 1)\n * @param options.alpha - Alpha value when flashing, from 0 to 1 (default: 0.3)\n * @param options.tint - Tint color when flashing as hex value or color name (default: 0xffffff - white)\n * \n * @example\n * ```ts\n * // Flash the current player with default settings\n * engine.flash();\n * \n * // Flash a specific sprite with red tint\n * engine.flash('sprite-id', { type: 'tint', tint: 0xff0000 });\n * \n * // Flash with both alpha and tint for dramatic effect\n * engine.flash(undefined, { \n * type: 'both', \n * alpha: 0.5, \n * tint: 0xff0000,\n * duration: 200,\n * cycles: 2\n * });\n * \n * // Quick damage flash on current player\n * engine.flash(undefined, { \n * type: 'tint', \n * tint: 'red', \n * duration: 150,\n * cycles: 1\n * });\n * ```\n */\n flash(\n spriteId?: string,\n options?: {\n type?: 'alpha' | 'tint' | 'both';\n duration?: number;\n cycles?: number;\n alpha?: number;\n tint?: number | string;\n }\n ): void {\n const targetId = spriteId || this.playerId;\n if (!targetId) return;\n\n const sprite = this.sceneMap.getObjectById(targetId);\n if (sprite && typeof sprite.flash === 'function') {\n sprite.flash(options);\n }\n }\n\n private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {\n this.updateServerTickEstimate(ack.serverTick);\n const keepLocalMovement = this.shouldKeepLocalPlayerMovement();\n if (this.predictionEnabled && this.prediction) {\n const result = this.prediction.applyServerAck({\n frame: ack.frame,\n serverTick: ack.serverTick,\n state:\n !keepLocalMovement && typeof ack.x === \"number\" && typeof ack.y === \"number\"\n ? { x: ack.x, y: ack.y, direction: ack.direction }\n : undefined,\n });\n if (result.state && result.needsReconciliation) {\n this.reconcilePrediction(result.state, result.pendingInputs);\n }\n return;\n }\n\n if (typeof ack.x !== \"number\" || typeof ack.y !== \"number\") {\n return;\n }\n if (keepLocalMovement) {\n return;\n }\n const player = this.getCurrentPlayer() as any;\n const myId = this.playerIdSignal();\n if (!player || !myId) {\n return;\n }\n const hitbox = typeof player.hitbox === \"function\" ? player.hitbox() : player.hitbox;\n const width = hitbox?.w ?? 0;\n const height = hitbox?.h ?? 0;\n const updated = this.sceneMap.updateHitbox(myId, ack.x, ack.y, width, height);\n if (!updated) {\n this.sceneMap.setBodyPosition(myId, ack.x, ack.y, \"top-left\");\n }\n player.x.set(Math.round(ack.x));\n player.y.set(Math.round(ack.y));\n if (ack.direction) {\n player.changeDirection(ack.direction);\n }\n }\n\n private reconcilePrediction(\n authoritativeState: PredictionState<Direction>,\n pendingInputs: PredictionHistoryEntry<RpgMovementInput, Direction>[],\n ): void {\n const player = this.getCurrentPlayer() as any;\n if (!player) {\n return;\n }\n if (!getCanMoveValue(player)) {\n this.interruptCurrentPlayerMovement(player);\n return;\n }\n\n (this.sceneMap as any).stopMovement(player);\n this.applyAuthoritativeState(authoritativeState);\n\n if (!pendingInputs.length) {\n return;\n }\n\n // Keep replay bounded while still tolerating high-latency links.\n const replayInputs = pendingInputs.slice(-600);\n for (const entry of replayInputs) {\n if (!entry?.direction) continue;\n this.applyPredictedMovementInput(player, entry.direction);\n this.sceneMap.stepPredictionTick();\n this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());\n }\n }\n\n /**\n * Replay unacknowledged inputs from a given frame to resimulate client prediction\n * after applying server authority at a certain frame.\n * \n * @param startFrame - The last server-acknowledged frame\n * \n * @example\n * ```ts\n * // After applying a server correction at frame N\n * this.replayUnackedInputsFromFrame(N);\n * ```\n */\n private async replayUnackedInputsFromFrame(_startFrame: number): Promise<void> {\n // Prediction controller handles replay internally. Kept for backwards compatibility.\n }\n\n /**\n * Clear all client resources and reset state\n * \n * This method should be called to clean up all client-side resources when\n * shutting down or resetting the client engine. It:\n * - Destroys the PIXI renderer\n * - Stops all sounds\n * - Cleans up subscriptions and event listeners\n * - Resets scene map\n * - Stops ping/pong interval\n * - Clears prediction states\n * \n * ## Design\n * \n * This method is used primarily in testing environments to ensure clean\n * state between tests. In production, the client engine typically persists\n * for the lifetime of the application.\n * \n * @example\n * ```ts\n * // In test cleanup\n * afterEach(() => {\n * clientEngine.clear();\n * });\n * ```\n */\n clear(): void {\n try {\n // First, unsubscribe from all tick subscriptions to stop rendering attempts\n for (const subscription of this.tickSubscriptions) {\n if (subscription && typeof subscription.unsubscribe === 'function') {\n subscription.unsubscribe();\n }\n }\n this.tickSubscriptions = [];\n\n // Stop ping/pong interval\n if (this.pingInterval) {\n clearInterval(this.pingInterval);\n this.pingInterval = null;\n }\n\n // Clean up onAfterLoading subscription\n if (this.onAfterLoadingSubscription && typeof this.onAfterLoadingSubscription.unsubscribe === 'function') {\n this.onAfterLoadingSubscription.unsubscribe();\n this.onAfterLoadingSubscription = undefined;\n }\n\n // Clean up canvasElement (CanvasEngine) BEFORE destroying PIXI app\n // This prevents CanvasEngine from trying to render after PIXI is destroyed\n // CanvasEngine manages its own render loop which could try to access PIXI after destruction\n if (this.canvasElement) {\n try {\n // Try to stop or cleanup canvasElement if it has cleanup methods\n if (typeof (this.canvasElement as any).destroy === 'function') {\n (this.canvasElement as any).destroy();\n }\n // Clear the reference\n this.canvasElement = undefined;\n } catch (error) {\n // Ignore errors during canvasElement cleanup\n }\n }\n\n // Reset scene map if it exists (this should stop any ongoing animations/renders)\n if (this.sceneMap && typeof (this.sceneMap as any).reset === 'function') {\n (this.sceneMap as any).reset(true);\n }\n\n // Stop all sounds\n this.stopAllSounds();\n\n // Remove resize event listener\n if (this.resizeHandler && typeof window !== 'undefined') {\n window.removeEventListener('resize', this.resizeHandler);\n this.resizeHandler = undefined;\n }\n\n if (this.pointerMoveHandler && this.pointerCanvas) {\n this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);\n this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);\n if (this.pointerUpHandler) {\n this.pointerCanvas.removeEventListener('pointerup', this.pointerUpHandler);\n }\n if (this.pointerCancelHandler) {\n this.pointerCanvas.removeEventListener('pointercancel', this.pointerCancelHandler);\n this.pointerCanvas.removeEventListener('pointerleave', this.pointerCancelHandler);\n }\n this.pointerMoveHandler = undefined;\n this.pointerUpHandler = undefined;\n this.pointerCancelHandler = undefined;\n this.pointerCanvas = undefined;\n }\n\n // Destroy PIXI app and renderer if they exist\n // Destroy the app first, which will destroy the renderer\n // Store renderer reference before destroying app (since app.destroy() will destroy the renderer)\n const rendererStillExists = this.renderer && typeof this.renderer.destroy === 'function';\n \n if (this.canvasApp && typeof this.canvasApp.destroy === 'function') {\n try {\n // Stop the ticker first to prevent any render calls during destruction\n if (this.canvasApp.ticker) {\n if (typeof this.canvasApp.ticker.stop === 'function') {\n this.canvasApp.ticker.stop();\n }\n // Also remove all listeners from ticker to prevent callbacks\n if (typeof this.canvasApp.ticker.removeAll === 'function') {\n this.canvasApp.ticker.removeAll();\n }\n }\n \n // Stop the renderer's ticker if it exists separately\n if (this.renderer && (this.renderer as any).ticker) {\n if (typeof (this.renderer as any).ticker.stop === 'function') {\n (this.renderer as any).ticker.stop();\n }\n if (typeof (this.renderer as any).ticker.removeAll === 'function') {\n (this.renderer as any).ticker.removeAll();\n }\n }\n \n // Remove the canvas from DOM before destroying to prevent render attempts\n if (this.canvasApp.canvas && this.canvasApp.canvas.parentNode) {\n this.canvasApp.canvas.parentNode.removeChild(this.canvasApp.canvas);\n }\n \n // Destroy with minimal options to avoid issues\n // Don't pass options that might trigger additional cleanup that could fail\n this.canvasApp.destroy(true);\n } catch (error) {\n // Ignore errors during destruction\n }\n this.canvasApp = undefined;\n // canvasApp.destroy() already destroyed the renderer, so just null it\n this.renderer = null as any;\n } else if (rendererStillExists) {\n // Fallback: destroy renderer directly only if app doesn't exist or wasn't destroyed\n try {\n // Stop the renderer's ticker if it has one\n if ((this.renderer as any).ticker) {\n if (typeof (this.renderer as any).ticker.stop === 'function') {\n (this.renderer as any).ticker.stop();\n }\n if (typeof (this.renderer as any).ticker.removeAll === 'function') {\n (this.renderer as any).ticker.removeAll();\n }\n }\n \n this.renderer.destroy(true);\n } catch (error) {\n // Ignore errors during destruction\n }\n this.renderer = null as any;\n }\n\n // Clean up prediction controller\n if (this.prediction) {\n // Prediction controller cleanup is handled internally when destroyed\n this.prediction = undefined;\n }\n\n // Reset signals\n this.playerIdSignal.set(null);\n this.cameraFollowTargetId.set(null);\n this.spriteComponentsBehind.set([]);\n this.spriteComponentsInFront.set([]);\n this.eventComponentResolvers.clear();\n \n // Clear maps and arrays\n this.spritesheets.clear();\n this.sounds.clear();\n this.componentAnimations = [];\n this.particleSettings.emitters = [];\n\n // Reset state\n this.stopProcessingInput = false;\n this.lastInputTime = 0;\n this.inputFrameCounter = 0;\n this.frameOffset = 0;\n this.rtt = 0;\n this.lastMovePathSentAt = 0;\n this.lastMovePathSentFrame = 0;\n\n // Reset behavior subjects\n this.mapLoadCompleted$.next(false);\n this.playerIdReceived$.next(false);\n this.playersReceived$.next(false);\n this.eventsReceived$.next(false);\n } catch (error) {\n console.warn('Error during client engine cleanup:', error);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2DA,IAAM,gCAAgC;AACtC,IAAM,2BAA2B;AACjC,IAAM,2BAA2B;AAEjC,IAAM,eAAe,UACnB,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,SAAS;AAEhE,IAAM,eACJ,UAEA,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,SAAS;AAEhE,IAAM,wBAAwB,UAAmD;CAC/E,IAAI,YAAY,KAAK,GAAG,OAAO,MAAM;CACrC,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAChD,OAAO;AAGX;AAEA,IAAM,qBAAqB,cAAqC;CAC9D,QAAQ,WAAR;EACE,KAAK,UAAU,MACb,OAAO;GAAE,GAAG;GAAI,GAAG;EAAE;EACvB,KAAK,UAAU,OACb,OAAO;GAAE,GAAG;GAAG,GAAG;EAAE;EACtB,KAAK,UAAU,IACb,OAAO;GAAE,GAAG;GAAG,GAAG;EAAG;EACvB,KAAK,UAAU;EACf,SACE,OAAO;GAAE,GAAG;GAAG,GAAG;EAAE;CACxB;AACF;AAEA,IAAM,qBAAqB,cAAmD;CAC5E,IAAI,KAAK,IAAI,UAAU,CAAC,IAAI,KAAK,IAAI,UAAU,CAAC,GAC9C,OAAO,UAAU,IAAI,IAAI,UAAU,OAAO,UAAU;CAEtD,OAAO,UAAU,IAAI,IAAI,UAAU,KAAK,UAAU;AACpD;AAEA,IAAM,sBACJ,OACA,sBACwB;CACxB,MAAM,eAAe,MAAM,aAAa,kBAAkB,iBAAiB;CAC3E,MAAM,OAAO,OAAO,cAAc,KAAK,CAAC;CACxC,MAAM,OAAO,OAAO,cAAc,KAAK,CAAC;CACxC,MAAM,YAAY,KAAK,MAAM,MAAM,IAAI;CACvC,IAAI,CAAC,OAAO,SAAS,SAAS,KAAK,aAAa,GAAG,OAAO;CAE1D,MAAM,kBACJ,OAAO,MAAM,oBAAoB,YAAY,OAAO,SAAS,MAAM,eAAe,IAC9E,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,iBAAiB,EAAE,CAAC,IAC/C;CACN,MAAM,WACJ,OAAO,MAAM,aAAa,YAAY,OAAO,SAAS,MAAM,QAAQ,IAChE,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,UAAU,GAAI,CAAC,IAC1C;CACN,MAAM,WACJ,OAAO,MAAM,aAAa,YAAY,OAAO,SAAS,MAAM,QAAQ,IAChE,KAAK,IAAI,GAAG,KAAK,IAAI,MAAM,UAAU,GAAI,CAAC,IAC1C;CAEN,OAAO;EACL,MAAM;EACN,WAAW;GACT,GAAG,OAAO;GACV,GAAG,OAAO;EACZ;EACA;EACA;EACA;CACF;AACF;AAaA,IAAa,kBAAb,MAAsC;CA4FpC,YAAY,SAAgB;EAAT,KAAA,UAAA;2BAnFa;6BACV;eACd,OAAO,MAAM;gBACZ,OAAO,MAAM;sCACoB,IAAI,IAAI;6CACgB,IAAI,IAAI;gCAC/C,IAAI,IAAI;6BACN,CAAC;uBACd,IAAI,qBAAqB;iBAET,2BAA2B;sBACrB,IAAI,sBAAsB,IAAI;0BAKhE,EACA,UAAU,CAAC,EACb;wBAKe,OAAsB,IAAI;gCAClB,OAAc,CAAC,CAAC;iCACf,OAAc,CAAC,CAAC;0CACL,IAAI,IAAI;iCACX,IAAI,+BAA+B;8BAE9C,OAAsB,IAAI;yBAEO,QAAyB;uBAEjE,OAAO,KAAA,CAAS;mBACpB,OAAO,KAAK;2BAEI;qCAEmB;gCACd;kCACE;+CACsB;2BAC7B;iCACgB,CAAC;iCACX;qBACZ;4BAEO;yBACH;aAEJ;sBACM;0BACQ;uBACZ;sCACwB;oCACF;4BACjB;+BACG;2BAEJ,IAAI,gBAAyB,KAAK;2BAClC,IAAI,gBAAyB,KAAK;0BACnC,IAAI,gBAAyB,KAAK;yBACnC,IAAI,gBAAyB,KAAK;0BAEjC;iCACO;oCAEG;kCACF;2BACA,CAAC;2BAGD,CAAC;4BAMA,CAAC;6BACc,IAAI,oBAAoB;EAKzE,KAAK,YAAY,OAAO,cAAc;EACtC,KAAK,aAAa,OAAO,MAAM;EAC/B,KAAK,iBAAiB,OAAO,YAAY;EACzC,KAAK,QAAQ,OAAc,YAAY;EACvC,KAAK,cAAc,uBAAuB,OAAO;EACjD,KAAK,YAAY,YAAY,sBAAsB,gBAAgB,CAAC;EACpE,KAAK,cAAc,IAAI,kBACrB,KAAK,QACJ,eAAe,KAAK,wBAAwB,UAAU,CACzD;EACA,KAAK,eAAe,OAAO,iBAAiB;EAE5C,IAAI,CAAC,KAAK,cACR,KAAK,eAAe,CAAC;EAEvB,IAAI,CAAE,KAAK,aAAqB,KAC9B,KAAM,aAAqB,MAAM;GAC/B,QAAQ;IACN,iBAAiB;IACjB,mBAAmB;GACrB;GACA,QAAQ,CAAC;EACX;EAGF,KAAK,sBAAsB;GACzB,IAAI;GACJ,WAAW,4BAA4B;EACzC,CAAC;EAED,KAAK,wBAAwB,YAAY,gBAAa;EACtD,KAAK,wBAAwB,aAAa,gBAAY;EACtD,KAAK,wBAAwB,aAAa,gBAAY;EACtD,KAAK,wBAAwB,WAAW,gBAAY;EACpD,KAAK,wBAAwB,aAAa,gBAAc;EACxD,KAAK,wBAAwB,aAAa,gBAAc;EAExD,KAAK,oBAAqB,KAAK,cAAsB,YAAY,YAAY;EAC7E,KAAK,yBAAyB,KAAK,8BAA8B;EACjE,KAAK,+BAA+B;CACtC;CAEA,gCAAiD;EAC/C,MAAM,mBAAoB,KAAK,cAAsB;EACrD,MAAM,aACH,KAAK,cAAsB,qBAC5B,kBAAkB,qBAClB,kBAAkB,aAClB,kBAAkB;EAEpB,IACE,eAAe,YACf,eAAe,aACf,eAAe,OAEf,OAAO;EAET,IACE,eAAe,YACf,eAAe,WACf,eAAe,MAEf,OAAO;EAGT,OAAO,KAAK,UAAU,SAAS;CACjC;CAEA,UAAU,QAAgB;EACxB,KAAK,SAAS;CAChB;CAEA,YAAoB;EAClB,OAAO,KAAK,UAAU,KAAK,YAAY;CACzC;CAEA,EAAE,KAAa,QAA6B;EAC1C,OAAO,KAAK,YAAY,EAAE,KAAK,QAAQ,KAAK,UAAU,CAAC;CACzD;CAEA,OAAO;EACL,OAAO;GACL,QAAQ,KAAK,UAAU;GACvB,IAAI,KAAa,WAAwB,KAAK,EAAE,KAAK,MAAM;EAC7D;CACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCA,oBAAoB,iBAAsB;EACxC,MAAM,gBAAgB,KAAK,QAAQ,OAAO;EAC1C,KAAK,QAAQ,OAAO,6BAAkC;GACpD,GAAG;GACH,QAAQ,IAAI,IAAI,CAAC,CAAC,eAAe,eAAe,CAAC,CAAC;EACpD;EACA,KAAK,cAAc,IAAI,KAAA,CAAS;CAClC;CAEA,MAAM,QAAQ;EACZ,KAAK,WAAW,IAAI,aAAa;EACjC,KAAK,SAAS,0BAA0B,KAAK,iBAAiB;EAC9D,KAAK,SAAS,WAAW;EACzB,KAAK,yBAAyB;EAG9B,OAD0B,iBAC1B,EAAW,WAAW;EACtB,KAAK,cAAc;EACnB,KAAK,WAAW,YAAY;EAE5B,IAAI;GACF,MAAM,KAAK,UAAU,WAAW;EAClC,SACO,OAAO;GACZ,KAAK,aAAa;GAClB,MAAM,KAAK,iBAAiB,KAAK;GACjC,MAAM;EACR;EAEA,KAAK,WAAW,SAAS,KAAK,cAAc,MAAM;EAElD,MAAM,mBAAoB,KAAK,cAAsB;EACrD,MAAM,EAAE,KAAK,kBAAkB,MAAM,gBACnC,KAAK,UACL,gBACA,gBACF;EACA,KAAK,yBAAyB,GAAG;EACjC,KAAK,YAAY;EACjB,KAAK,gBAAgB;EACrB,KAAK,WAAW,IAAI;EACpB,KAAK,qBAAqB;EAC1B,KAAK,OAAO,eAAe,iBAAiB,QAAQ,QAAQ;EAE5D,MAAM,yBAAyB,KAAK,KAAK,gBAAgB;GACvD,IAAI,KAAK,IAAI,IAAI,KAAK,gBAAgB,KAAK;IACzC,MAAM,SAAS,KAAK,iBAAiB;IACrC,IAAI,CAAC,QAAQ;IACb,KAAM,SAAiB,aAAa,MAAM;GAC5C;EACF,CAAC;EACD,KAAK,kBAAkB,KAAK,sBAAsB;EAGlD,KAAK,MAAM,UAAU,4BAA4B,IAAI,EAAE,UAAU;EACjE,KAAK,MAAM,UAAU,mCAAmC,IAAI,EAAE,UAAU;EACxE,KAAK,wBAAwB;EAC7B,KAAK,MAAM,UAAU,sBAAsB,IAAI,EAAE,UAAU;EAC3D,KAAK,MAAM,UAAU,6BAA6B,IAAI,EAAE,UAAU;EAElE,SAAS,KAAK,IAAI;EAClB,YAAY,KAAK,IAAI;EACrB,KAAK,MAAM,UAAU,mBAAmB,IAAI,EAAE,UAAU;EACxD,KAAK,MAAM,UAAU,yBAAyB,IAAI,EAAE,UAAU;EAC9D,KAAK,MAAM,UAAU,mCAAmC,IAAI,EAAE,UAAU;EACxE,KAAK,MAAM,UAAU,6BAA6B,IAAI,EAAE,UAAU;EAClE,KAAK,MAAM,UAAU,2BAA2B,IAAI,EAAE,UAAU;EAChE,KAAK,MAAM,UAAU,4BAA4B,IAAI,EAAE,UAAU;EACjE,KAAK,MAAM,UAAU,sBAAsB,IAAI,EAAE,UAAU;EAE3D,MAAM,cAAc,KAAK,MAAM,UAAU,yBAAyB,IAAI,CAAC;EACvE,KAAK,2BAA2B;EAChC,KAAK,uBAAuB;EAG5B,KAAK,sBAAsB;GACzB,KAAK,MAAM,UAAU,gCAAgC,IAAI,EAAE,UAAU;EACvE;EACA,OAAO,iBAAiB,UAAU,KAAK,aAAa;EAEpD,MAAM,mBAAmB,KAAK,KAAK,WAAW,SAAS;GACrD,KAAK,sBAAsB;GAC3B,KAAK,YAAY,KAAK;GACtB,KAAK,4BAA4B;GACjC,KAAK,qBAAqB;GAC1B,KAAK,MAAM,UAAU,wBAAwB,MAAM,IAAI,EAAE,UAAU;GAGnE,IAAI,OAAO,OAAO,GAAG;IACnB,MAAM,MAAM,KAAK,IAAI;IACrB,KAAK,YAAY,QAAQ,GAAG;IAC5B,KAAK,YAAY,wBAAwB;GAC3C;EACF,CAAC;EACD,KAAK,kBAAkB,KAAK,gBAAgB;EAE5C,KAAK,cAAc;CACrB;CAEA,yBAAiC,KAAU;EACzC,IAAI,CAAC,OAAO,OAAO,IAAI,WAAW,YAAY;EAE9C,MAAM,iBAAiB,IAAI,OAAO,KAAK,GAAG;EAC1C,IAAI,eAAe;GACjB,MAAM,aAAa,KAAK,2BAA2B,GAAG;GACtD,MAAM,eAAe,KAAK,uBAAuB,GAAG;GAEpD,IACE,cACA,gBACA,WAAW,UAAU,aAAa,SAClC,WAAW,WAAW,aAAa,QACnC;IACA,KAAK,wBAAwB,GAAG;IAChC;GACF;GAEA,eAAe;EACjB;CACF;CAEA,2BAAmC,KAAmC;EACpE,MAAM,eAAe,KAAK;EAC1B,IAAI,CAAC,gBAAgB,OAAO,WAAW,aAAa,OAAO;EAE3D,MAAM,WAAW,iBAAiB,SAAS,OAAO,aAAa,aAAa;EAC5E,MAAM,YAAY,iBAAiB,SAAS,OAAO,cAAc,aAAa;EAC9E,MAAM,QAAQ,KAAK,MAAM,OAAO,QAAQ,CAAC;EACzC,MAAM,SAAS,KAAK,MAAM,OAAO,SAAS,CAAC;EAE3C,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,MAAM,KAAK,QAAQ,KAAK,SAAS,GAAG,OAAO;EAC3F,OAAO;GAAE;GAAO;EAAO;CACzB;CAEA,uBAA+B,KAAmC;EAChE,MAAM,SAAS,KAAK,UAAU;EAC9B,MAAM,QAAQ,KAAK,MAAM,OAAO,QAAQ,KAAK,CAAC;EAC9C,MAAM,SAAS,KAAK,MAAM,OAAO,QAAQ,MAAM,CAAC;EAEhD,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,CAAC,OAAO,SAAS,MAAM,GAAG,OAAO;EAChE,OAAO;GAAE;GAAO;EAAO;CACzB;CAEA,wBAAgC,KAAU;EACxC,IAAI,OAAO,KAAK,kBAAkB,YAChC,IAAI,cAAc;CAEtB;CAEA,2BAAmC;EACjC,MAAM,aAAa,KAAK,MAAM,iBAAiB,2BAA2B;EAC1E,MAAM,YAAY,WAAW,WAAW,SAAS;EACjD,IAAI,WACF,KAAK,oBAAoB;CAE7B;CAEA,uBAA+B;EAC7B,MAAM,WAAW,KAAK;EACtB,MAAM,SAAS,UAAU,UAAU,UAAU,QAAS,KAAK,WAAmB;EAE9E,IAAI,CAAC,UAAU,OAAO,OAAO,qBAAqB,YAChD;EAGF,KAAK,gBAAgB;EACrB,MAAM,iBAAiB,UAAwB;GAC7C,MAAM,OAAO,OAAO,sBAAsB;GAC1C,MAAM,SAAS;IACb,GAAG,MAAM,UAAU,KAAK;IACxB,GAAG,MAAM,UAAU,KAAK;GAC1B;GACA,MAAM,WAAW,KAAK,qBAAqB;GAC3C,IAAI,QAAQ;GAEZ,IAAI,YAAY,OAAO,SAAS,YAAY,YAAY;IACtD,MAAM,QAAQ,SAAS,QAAQ,OAAO,GAAG,OAAO,CAAC;IACjD,QAAQ;KAAE,GAAG,OAAO,MAAM,CAAC;KAAG,GAAG,OAAO,MAAM,CAAC;IAAE;GACnD,OAAO,IAAI,YAAY,OAAO,SAAS,YAAY,YAAY;IAC7D,MAAM,QAAQ,SAAS,QAAQ,MAAM;IACrC,QAAQ;KAAE,GAAG,OAAO,MAAM,CAAC;KAAG,GAAG,OAAO,MAAM,CAAC;IAAE;GACnD;GAEA,KAAK,QAAQ,OAAO,QAAQ,KAAK;EACnC;EAEA,KAAK,sBAAsB,UAAwB;GACjD,cAAc,KAAK;GACnB,KAAK,aAAa,kBAAkB,KAAK;EAC3C;EACA,KAAK,oBAAoB,UAAwB;GAC/C,cAAc,KAAK;GACnB,KAAK,aAAa,gBAAgB,KAAK;EACzC;EACA,KAAK,wBAAwB,UAAwB;GACnD,cAAc,KAAK;GACnB,KAAK,aAAa,WAAW,KAAK;EACpC;EAEA,OAAO,iBAAiB,eAAe,KAAK,kBAAkB;EAC9D,OAAO,iBAAiB,eAAe,KAAK,kBAAkB;EAC9D,OAAO,iBAAiB,aAAa,KAAK,gBAAgB;EAC1D,OAAO,iBAAiB,iBAAiB,KAAK,oBAAoB;EAClE,OAAO,iBAAiB,gBAAgB,KAAK,oBAAoB;CACnE;CAEA,kCAAkC,OAAkB;EAClD,MAAM,SAAS,OAAO,UAAU,OAAO,MAAM;EAE7C,IAAI,CAAC,QAAQ;GACX,KAAK,QAAQ,gBAAgB,KAAK;GAClC;EACF;EAEA,MAAM,SAAS;GACb,GAAG,OAAO,OAAO,CAAC;GAClB,GAAG,OAAO,OAAO,CAAC;EACpB;EACA,IAAI,CAAC,OAAO,SAAS,OAAO,CAAC,KAAK,CAAC,OAAO,SAAS,OAAO,CAAC,GAAG;GAC5D,KAAK,QAAQ,gBAAgB,KAAK;GAClC;EACF;EAEA,MAAM,WAAW,KAAK,qBAAqB;EAC3C,IAAI,YAAY,OAAO,SAAS,YAAY,YAAY;GACtD,MAAM,QAAQ,SAAS,QAAQ,OAAO,GAAG,OAAO,CAAC;GACjD,KAAK,QAAQ,OAAO,QAAQ;IAAE,GAAG,OAAO,MAAM,CAAC;IAAG,GAAG,OAAO,MAAM,CAAC;GAAE,CAAC;GACtE;EACF;EAEA,KAAK,QAAQ,OAAO,MAAM;CAC5B;CAEA,uBAAoC;EAClC,MAAM,QAAQ,SAAmB;GAC/B,IAAI,CAAC,MAAM,OAAO,KAAA;GAClB,IAAI,OAAO,MAAM,YAAY,cAAc,MAAM,aAAa,SAAS,YACrE,OAAO;GAET,KAAK,MAAM,SAAS,KAAK,YAAY,CAAC,GAAG;IACvC,MAAM,WAAW,KAAK,KAAK;IAC3B,IAAI,UAAU,OAAO;GACvB;EAEF;EAEA,OAAO,KAAM,KAAK,WAAmB,KAAK;CAC5C;CAEA,mBAA2B,MAAgB;EACzC,MAAM,UAAU,EAAE,GAAI,QAAQ,CAAC,EAAG;EAClC,OAAO,QAAQ;EACf,OAAO,QAAQ;EAEf,MAAM,OAAO,KAAK,eAAe;EACjC,MAAM,UAAU,QAAQ;EACxB,MAAM,aAAa,QAAQ,UAAU,QAAQ,QAAQ,KAAA;EAErD,IADgC,KAAK,kCAAkC,UACnE,KAA2B,QAAQ,WAAW,QAAQ,OAAO;GAC/D,MAAM,aAAa,EAAE,GAAG,QAAQ,MAAM;GACtC,OAAO,WAAW;GAClB,OAAO,WAAW;GAClB,OAAO,WAAW;GAClB,OAAO,WAAW;GAClB,QAAQ,UAAU;IAChB,GAAG;KACF,OAAO;GACV;EACF;EAEA,OAAO;CACT;CAEA,kCAA0C,YAA2B;EACnE,IAAI,CAAC,YACH,OAAO;EAET,IAAI,KAAK,qBAAqB,CAAC,CAAC,KAAK,YAAY,iBAAiB,GAChE,OAAO;EAET,OAAO,KAAK,8BAA8B;CAC5C;CAEA,gCAAiD;EAC/C,IAAI,CAAC,KAAK,0BAA0B,KAAK,yBACvC,OAAO;EAET,MAAM,OAAO,KAAK,eAAe;EAEjC,IAAI,EADW,OAAO,KAAK,UAAU,UAAU,IAAI,QAAQ,KAAA,IAEzD,OAAO;EAET,IAAI,KAAK,YAAY,iBAAiB,GACpC,OAAO;EAET,OAAO,KAAK,IAAI,IAAI,KAAK,4BAA4B,KAAK;CAC5D;CAEA,0BACE,KACA,UACuF;EACvF,MAAM,OAAO,KAAK,eAAe;EACjC,IAAI,CAAC,MACH,OAAO;EAGT,MAAM,aAAa,UAAU,UAAU;EACvC,IAAI,OAAO,YAAY,MAAM,YAAY,OAAO,YAAY,MAAM,UAChE,OAAO;EAGT,OAAO;GACL,GAAG;GACH,GAAG,WAAW;GACd,GAAG,WAAW;GACd,WAAW,WAAW,aAAa,IAAI;EACzC;CACF;CAEA,gBAAwB;EACtB,IAAI,KAAK,4BAA4B;EACrC,KAAK,6BAA6B;EAElC,KAAK,UAAU,GAAG,SAAS,SAAS;GAClC,IAAI,CAAC,KAAK,MAAM;IACd,KAAK,mBAAmB,KAAK,IAAI;IACjC;GACF;GACA,KAAK,gBAAgB,IAAI;EAC3B,CAAC;EAGD,KAAK,UAAU,GAAG,SAAS,SAA0E;GACnG,MAAM,MAAM,KAAK,IAAI;GACrB,KAAK,MAAM,MAAM,KAAK;GAItB,MAAM,yBAAyB,KAAK,MAAM,KAAK,MAAM,KAAK,MAAO,GAAG;GACpE,MAAM,yBAAyB,KAAK,aAAa;GACjD,KAAK,yBAAyB,wBAAwB,GAAG;GAGzD,IAAI,KAAK,oBAAoB,GAC3B,KAAK,cAAc,yBAAyB,KAAK;GAGnD,QAAQ,MAAM,oBAAoB,KAAK,IAAI,kBAAkB,KAAK,WAAW,iBAAiB,KAAK,aAAa;EAClH,CAAC;EAED,KAAK,UAAU,GAAG,cAAc,SAAS;GACvC,IAAI,CAAC,KAAK,0BAA0B;IAClC,KAAK,kBAAkB,KAAK,IAAI;IAChC;GACF;GACA,KAAK,gBAAgB,IAAI;EAC3B,CAAC;EAED,KAAK,UAAU,GAAG,2BAA2B,SAAS;GACpD,MAAM,EAAE,QAAQ,QAAQ,UAAU,OAAO;GACzC,IAAI,CAAC,UAAU,aAAa,KAAA,GAC1B,MAAM,IAAI,MAAM,iDAAiD;GAEnE,MAAM,SAAS,SAAS,KAAK,SAAS,cAAc,MAAM,IAAI,KAAA;GAC9D,KAAK,sBAAsB,EAAE,EAAE,cAAc,QAAQ,UAAU,QAAQ;EACzE,CAAC;EAED,KAAK,UAAU,GAAG,iBAAiB,SAAS;GAC1C,KAAK,iBAAiB,IAAI;EAC5B,CAAC;EAED,KAAK,UAAU,GAAG,0BAA0B,SAAS;GACnD,IAAI,CAAC,KAAK,8BAA8B,IAAI,GAAG;GAC/C,KAAK,YAAY,WAAW,MAAM,eAAe,CAAC,GAAG;IACnD,OAAO,MAAM;IACb,mBAAmB,KAAK,mBAAmB;IAC3C,gBAAgB,KAAK,yBAAyB;GAChD,CAAC;EACH,CAAC;EAED,KAAK,UAAU,GAAG,2BAA2B,SAAS;GACpD,IAAI,CAAC,KAAK,8BAA8B,IAAI,GAAG;GAC/C,KAAK,YAAY,YAAY,MAAM,WAAW,CAAC,GAAG,EAChD,OAAO,MAAM,MACf,CAAC;EACH,CAAC;EAED,KAAK,UAAU,GAAG,4BAA4B,SAAS;GACrD,IAAI,CAAC,KAAK,8BAA8B,IAAI,GAAG;GAC/C,KAAK,YAAY,aAAa,MAAM,eAAe,CAAC,GAAG,EACrD,OAAO,MAAM,MACf,CAAC;EACH,CAAC;EAED,KAAK,UAAU,GAAG,qBAAqB,SAAS;GAC9C,IAAI,CAAC,KAAK,8BAA8B,IAAI,GAAG;GAC/C,KAAK,YAAY,MAAM;EACzB,CAAC;EAED,KAAK,UAAU,GAAG,iBAAiB,SAAS;GAC1C,KAAK,oBAAoB,IAAI,IAAI;EACnC,CAAC;EAED,KAAK,UAAU,GAAG,iBAAiB,SAAS;GAC1C,MAAM,EACJ,eACA,SACA,QACA,SACA,sBACA,oBACE;GACJ,MAAM,SAAS,SAAS,KAAK,SAAS,cAAc,MAAM,IAAI,KAAA;GAC9D,IAAI,CAAC,QAAQ;GACb,MAAM,iBAAiB;IACrB;IACA;GACF;GACA,IAAI,YAAY,KAAA,GACd,OAAO,aAAa,eAAe,SAAS,SAAS,cAAc;QAEnE,OAAO,aAAa,eAAe,SAAS,cAAc;EAE9D,CAAC;EAED,KAAK,UAAU,GAAG,cAAc,SAAS;GACvC,MAAM,EAAE,SAAS,QAAQ,SAAS;GAClC,KAAK,UAAU,SAAS;IAAE;IAAQ;GAAK,CAAC;EAC1C,CAAC;EAED,KAAK,UAAU,GAAG,cAAc,SAAS;GACvC,MAAM,EAAE,YAAY;GACpB,KAAK,UAAU,OAAO;EACxB,CAAC;EAED,KAAK,UAAU,GAAG,uBAAuB;GACvC,KAAK,cAAc;EACrB,CAAC;EAED,KAAK,UAAU,GAAG,iBAAiB,SAAS;GAC1C,MAAM,EAAE,UAAU,eAAe;GACjC,KAAK,gBAAgB,UAAU,UAAU;EAC3C,CAAC;EAED,KAAK,UAAU,GAAG,UAAU,SAAS;GACnC,MAAM,EAAE,QAAQ,MAAM,UAAU,QAAQ,OAAO,SAAS;GACxD,MAAM,SAAS,SAAS,KAAK,SAAS,cAAc,MAAM,IAAI,KAAA;GAC9D,IAAI,UAAU,OAAO,OAAO,UAAU,YACpC,OAAO,MAAM;IAAE;IAAM;IAAU;IAAQ;IAAO;GAAK,CAAC;EAExD,CAAC;EAED,KAAK,UAAU,GAAG,aAAa,SAAS;GACtC,MAAM,EAAE,WAAW,UAAU,WAAW,cAAc,QAAQ,CAAC;GAC/D,KAAK,gBAAgB,MAAM;IACzB;IACA;IACA;IACA;GACF,CAAC;EACH,CAAC;EAED,KAAK,UAAU,GAAG,iBAAiB,SAAS;GAC1C,MAAM,MAAO,QAAQ,OAAO,SAAS,YAAY,WAAW,OACvD,KAAa,QACd;GAEJ,IAAI,QAAQ,MAAM;IAChB,KAAK,SAAS,aAAa,IAAI,IAAI;IACnC;GACF;GAGA,IAAI,CAAC,OAAO,CAAC;IADS;IAAQ;IAAQ;IAAO;GAChC,EAAa,SAAU,IAAY,MAAM,GACpD;GAGF,KAAK,SAAS,aAAa,IAAI;IAC7B,QAAS,IAAY;IACrB,QAAS,IAAY;IACrB,QAAS,IAAY;IACrB,cAAe,IAAY;IAC3B,YAAa,IAAY;IACzB,WAAY,IAAY;IACxB,MAAO,IAAY;GACrB,CAAC;EACH,CAAC;EAED,KAAK,UAAU,GAAG,kBAAkB,SAAS;GAC3C,MAAM,MAAO,QAAQ,OAAO,SAAS,YAAY,WAAW,OACvD,KAAa,QACd;GAEJ,KAAK,SAAS,cAAc,IAAI,uBAAuB,GAAG,CAAC;EAC7D,CAAC;EAED,KAAK,UAAU,GAAG,cAAc;GAC9B,KAAK,MAAM,UAAU,6BAA6B,MAAM,KAAK,MAAM,EAAE,UAAU;GAE/E,KAAK,cAAc;EACrB,CAAC;EAED,KAAK,UAAU,GAAG,eAAe;GAC/B,KAAK,MAAM,UAAU,gCAAgC,MAAM,KAAK,MAAM,EAAE,UAAU;GAElF,KAAK,aAAa;EACpB,CAAC;EAED,KAAK,UAAU,GAAG,UAAU,UAAU;GACpC,KAAU,iBAAiB,KAAK;EAClC,CAAC;CACH;CAEA,iBAAyB,WAAoB;EAC3C,KAAK,0BAA0B;EAC/B,KAAK,mBAAmB;EACxB,KAAK,mBAAmB;EACxB,KAAK,4BAA4B;EACjC,KAAK,SAAS,aAAa,IAAI,IAAI;EACnC,KAAK,SAAS,cAAc,IAAI,IAAI;EACpC,KAAK,SAAS,gBAAgB;EAC9B,KAAK,yBAAyB;EAC9B,KAAK,YAAY,SAAS,SAAS;EACnC,KAAK,qBAAqB,IAAI,IAAI;EAClC,KAAK,SAAS,MAAM;EACpB,KAAK,SAAS,WAAW;CAC3B;CAEA,2BAAmC;EACjC,KAAK,oBAAoB,SAAS,uBAAuB;GACvD,mBAAmB,UAAU,QAAQ;EACvC,CAAC;CACH;CAEA,8BAAsC,MAAoB;EACxD,IAAI,KAAK,yBAAyB,OAAO;EACzC,MAAM,cAAc,mBAClB,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ,KAAA,CACjD;EACA,MAAM,eAAe,mBAAmB,KAAK,gBAAgB;EAC7D,OAAO,CAAC,eAAe,CAAC,gBAAgB,gBAAgB;CAC1D;CAEA,MAAc,iBAAiB,OAAY;EACzC,MAAM,cAAc,KAAK,MAAM,UAAU,gCAAgC,MAAM,OAAO,KAAK,MAAM,CAAC;CACpG;CAEA,0BAAkC;EAChC,MAAM,UAAU,KAAK;EACrB,KAAK,qBAAqB,CAAC;EAC3B,QAAQ,SAAS,WAAW,KAAK,gBAAgB,MAAM,CAAC;CAC1D;CAEA,yBAAiC;EAC/B,MAAM,UAAU,KAAK;EACrB,KAAK,oBAAoB,CAAC;EAC1B,QAAQ,SAAS,WAAW,KAAK,gBAAgB,MAAM,CAAC;CAC1D;CAEA,gBAAwB,MAAW;EACjC,MAAM,YAAY,OAAO,MAAM,UAAU,WAAW,KAAK,QAAQ,KAAA;EACjE,KAAK,iBAAiB,SAAS;EAC/B,MAAM,gBAAgB,OAAO,MAAM,kBAAkB,WAAW,KAAK,gBAAgB,KAAA;EACrF,KAAK,UAAU,KAAK,OAAO,aAAa;CAC1C;CAEA,gBAAwB,MAAW;EACjC,IAAI,KAAK,KAAK;GACZ,KAAK,eAAe,IAAI,KAAK,GAAG;GAEhC,KAAK,kBAAkB,KAAK,IAAI;EAClC;EAEA,IAAI,KAAK,kBAAkB;GACzB,MAAM,eAAe,KAAK,SAAS,aAAa;GAChD,MAAM,gBAAgB,KAAK,SAAS,cAAc;GAClD,KAAK,SAAS,MAAM;GACpB,KAAK,SAAS,aAAa,IAAI,YAAY;GAC3C,KAAK,SAAS,cAAc,IAAI,aAAa;GAC7C,KAAK,SAAS,WAAW;GACzB,KAAK,mBAAmB;EAC1B;EAGA,KAAK,MAAM,UAAU,6BAA6B,KAAK,UAAU,EAAE,SAAS,KAAK,CAAC,EAAE,UAAU;EAE9F,MAAM,MAAM,MAAM;EAClB,MAAM,gBACJ,OAAO,OAAO,IAAI,UAAU,WACxB,KAAK,0BAA0B,KAAK,IAAI,IACxC,KAAA;EACN,MAAM,UAAU,KAAK,mBAAmB,IAAI;EAC5C,KAAK,KAAK,UAAU,SAAS,IAAI;EAEjC,IAAI,eACF,KAAK,eAAe,aAAa;EAGnC,KAAK,MAAM,YAAY,QAAQ,WAAW,CAAC,GAAG;GAC5C,MAAM,SAAS,QAAQ,QAAQ;GAC/B,IAAI,CAAC,OAAO,QAAQ;GACpB,KAAK,MAAM,SAAS,OAAO,QAC1B,KAAK,SAAS,QAAQ,EAAE,UAAU,OAAO,EAAE,SAAS,OAAO,OAAO;EAErE;EAGA,MAAM,UAAU,QAAQ,WAAW,KAAK,SAAS,QAAQ;EACzD,IAAI,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,GAC3C,KAAK,iBAAiB,KAAK,IAAI;EAIjC,KADe,QAAQ,UAAU,KAAK,SAAS,OAAO,OACvC,KAAA,GACb,KAAK,gBAAgB,KAAK,IAAI;CAElC;;;;;;;;;;;;;;;;;;;;CAqBA,gBAA8B;EAE5B,KAAK,aAAa;EAGlB,KAAK,SAAS;EAGd,KAAK,eAAe,kBAAkB;GACpC,KAAK,SAAS;EAChB,GAAG,KAAK,gBAAgB;CAC1B;;;;;;;;;;;;CAaA,eAA6B;EAC3B,IAAI,KAAK,cAAc;GACrB,cAAc,KAAK,YAAY;GAC/B,KAAK,eAAe;EACtB;CACF;;;;;;;;;;;;;CAcA,WAAyB;EACvB,MAAM,aAAa,KAAK,IAAI;EAC5B,MAAM,cAAc,KAAK,eAAe;EAExC,KAAK,UAAU,KAAK,QAAQ;GAC1B;GACA;EACF,CAAC;CACH;CAEA,MAAc,UAAU,OAAe,eAAwB;EAC7D,MAAM,cAAc,KAAK,MAAM,UAAU,mCAAmC,KAAK,QAAQ,CAAC;EAG1F,KAAK,4BAA4B;EAGjC,KAAK,kBAAkB,KAAK,KAAK;EACjC,KAAK,kBAAkB,KAAK,KAAK;EACjC,KAAK,iBAAiB,KAAK,KAAK;EAChC,KAAK,gBAAgB,KAAK,KAAK;EAG/B,IAAI,KAAK,4BACP,KAAK,2BAA2B,YAAY;EAI9C,KAAK,4BAA4B;EAEjC,KAAK,UAAU,iBAAiB;GAC9B,MAAM;GACN,OAAO,gBAAgB,EAAE,cAAc,IAAI,KAAA;EAC7C,CAAC;EACD,IAAI;GACF,MAAM,KAAK,UAAU,UAAU;EACjC,SACO,OAAO;GACZ,KAAK,0BAA0B;GAC/B,KAAK,aAAa;GAClB,MAAM,KAAK,iBAAiB,KAAK;GACjC,MAAM;EACR;EACA,MAAM,MAAM,MAAM,KAAK,eAAe,KAAK,KAAK;EAChD,MAAM,iBAAiB,OAAO,KAAK,aAAa,cAC5C,IAAI,WACJ,KAAK,MAAM;EACf,IAAI,OAAO,mBAAmB,aAC5B,KAAK,SAAS,cAAc,IAAI,uBAAuB,cAAc,CAAC;EAExE,KAAK,SAAS,KAAK,IAAI,GAAG;EAG1B,IAAI,KAAK,eAAe,GACtB,KAAK,kBAAkB,KAAK,IAAI;EAIlC,MAAM,UAAU,KAAK,SAAS,QAAQ;EACtC,IAAI,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,GAC3C,KAAK,iBAAiB,KAAK,IAAI;EAIjC,IADe,KAAK,SAAS,OACzB,MAAW,KAAA,GACb,KAAK,gBAAgB,KAAK,IAAI;EAIhC,KAAK,kBAAkB,KAAK,IAAI;EAChC,KAAK,mBAAmB;EACxB,KAAK,0BAA0B;EAC/B,KAAK,SAAS,0BAA0B,KAAK,iBAAiB;EAC9D,KAAK,SAAS,WAAW;CAC3B;CAEA,eAAwB,kBAAuB,IAAkB;EAC/D,KAAK,aAAa,IAAI,MAAM,iBAAiB,IAAI,gBAAgB;EACjE,OAAO;CACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BA,uBAAuB,UAA6D;EAClF,KAAK,sBAAsB;CAC7B;;;;;;;;;;;;;;;;;;;;;CAsBA,eAAe,IAAyC;EAEtD,IAAI,KAAK,aAAa,IAAI,EAAE,GAC1B,OAAO,KAAK,aAAa,IAAI,EAAE;EAIjC,IAAI,KAAK,qBAAqB;GAC5B,IAAI,KAAK,oBAAoB,IAAI,EAAE,GACjC,OAAO,KAAK,oBAAoB,IAAI,EAAE;GAGxC,MAAM,SAAS,KAAK,oBAAoB,EAAE;GAG1C,IAAI,kBAAkB,SAAS;IAC7B,MAAM,UAAU,OACb,MAAM,gBAAgB;KACrB,IAAI,aAEF,KAAK,aAAa,IAAI,IAAI,WAAW;KAEvC,KAAK,oBAAoB,OAAO,EAAE;KAClC,OAAO;IACT,CAAC,EACA,OAAO,UAAU;KAChB,KAAK,oBAAoB,OAAO,EAAE;KAClC,MAAM;IACR,CAAC;IACH,KAAK,oBAAoB,IAAI,IAAI,OAAO;IACxC,OAAO;GACT,OAAO;IAEL,IAAI,QAEF,KAAK,aAAa,IAAI,IAAI,MAAM;IAElC,OAAO;GACT;EACF;CAIF;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,SAAS,OAAY,IAAkB;EACrC,MAAM,UAAU,MAAM,MAAM;EAE5B,IAAI,CAAC,SAAS;GACZ,QAAQ,KAAK,wDAAwD;GACrE,OAAO;EACT;EAGA,IAAI,MAAM,OAAO,OAAO,MAAM,QAAQ,UAAU;GAC9C,MAAM,cAAmB;IACvB,KAAK,CAAC,MAAM,GAAG;IACf,MAAM,MAAM,QAAQ;IACpB,QAAQ,MAAM,WAAW,KAAA,IAAY,MAAM,SAAS;GACtD;GAEA,MAAM,OAAO,IAAK,KAAa,KAAK,WAAW;GAC/C,KAAK,OAAO,IAAI,SAAS,IAAI;GAC7B,OAAO;EACT;EAGA,IAAI,SAAS,OAAO,MAAM,SAAS,YAAY;GAC7C,KAAK,OAAO,IAAI,SAAS,KAAK;GAC9B,OAAO;EACT;EAGA,KAAK,OAAO,IAAI,SAAS,KAAK;EAC9B,OAAO;CACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BA,iBAAiB,UAAoD;EACnE,KAAK,gBAAgB;CACvB;;;;;;;;;;;;;;;;;;;;;CAsBA,SAAS,IAAgC;EAEvC,IAAI,KAAK,OAAO,IAAI,EAAE,GACpB,OAAO,KAAK,OAAO,IAAI,EAAE;EAI3B,IAAI,KAAK,eAAe;GACtB,MAAM,SAAS,KAAK,cAAc,EAAE;GAGpC,IAAI,kBAAkB,SACpB,OAAO,OAAO,MAAM,UAAU;IAC5B,IAAI,OAEF,KAAK,OAAO,IAAI,IAAI,KAAK;IAE3B,OAAO;GACT,CAAC;QACI;IAEL,IAAI,QAEF,KAAK,OAAO,IAAI,IAAI,MAAM;IAE5B,OAAO;GACT;EACF;CAIF;;;;;;;;;;;;;;;;;;;;;;;;;CA0BA,MAAM,UAAU,SAAiB,SAA8D;EAC7F,MAAM,QAAQ,MAAM,KAAK,SAAS,OAAO;EACzC,IAAI,SAAS,MAAM,MAAM;GAEvB,MAAM,cAAc,MAAM,UAAU,IAAI;GAGxC,IAAI,SAAS,WAAW,KAAA,GACtB,IAAI,gBAAgB,KAAA,GAClB,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,MAAM,CAAC,GAAG,WAAW;QAElE,MAAM,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,MAAM,CAAC,CAAC;GAKzD,IAAI,SAAS,SAAS,KAAA,GACpB,IAAI,gBAAgB,KAAA,GAClB,MAAM,KAAK,QAAQ,MAAM,WAAW;QAEpC,MAAM,KAAK,QAAQ,IAAI;GAI3B,IAAI,gBAAgB,KAAA,GAClB,MAAM,KAAK,WAAW;QAEtB,MAAM,KAAK;EAEf,OAAO,IAAI,SAAS,MAAM,KAAK;GAE7B,MAAM,cAAmB;IACvB,KAAK,CAAC,MAAM,GAAG;IACf,MAAM,SAAS,SAAS,KAAA,IAAY,QAAQ,OAAQ,MAAM,QAAQ;IAClE,QAAQ,SAAS,WAAW,KAAA,IAAY,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,MAAM,CAAC,IAAK,MAAM,WAAW,KAAA,IAAY,MAAM,SAAS;GAClI;GAEA,MAAM,OAAO,IAAK,KAAa,KAAK,WAAW;GAG/C,KAAK,OAAO,IAAI,SAAS,IAAI;GAG7B,KAAK,KAAK;EACZ,OACE,QAAQ,KAAK,kBAAkB,QAAQ,gCAAgC;CAE3E;;;;;;;;;;;;;;;;;CAkBA,UAAU,SAAuB;EAC/B,MAAM,QAAQ,KAAK,OAAO,IAAI,OAAO;EACrC,IAAI,SAAS,MAAM,MACjB,MAAM,KAAK;OAEX,QAAQ,KAAK,kBAAkB,QAAQ,iCAAiC;CAE5E;;;;;;;;;;;;;CAcA,gBAAsB;EACpB,KAAK,OAAO,SAAS,UAAU;GAC7B,IAAI,SAAS,MAAM,MACjB,MAAM,KAAK;EAEf,CAAC;CACH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAsCA,gBACE,UACA,YACM;EAIN,KAAK,qBAAqB,IAAI,QAAQ;EAItC,IAAI,OAAO,eAAe,YAAY,eAAe,MAAM,CAG3D;CACF;CAEA,YAAY,UAAe;EACzB,KAAK,iBAAiB,SAAS,KAAK,QAAQ;EAC5C,OAAO;CACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCA,yBAAyB,WAAgB;EACvC,KAAK,uBAAuB,QAAQ,eAAsB,CAAC,GAAG,YAAY,SAAS,CAAC;EACpF,OAAO;CACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCA,0BAA0B,WAAyG;EACjI,KAAK,wBAAwB,QAAQ,eAAsB,CAAC,GAAG,YAAY,SAAS,CAAC;EACrF,OAAO;CACT;;;;;;;;;;;;;;;;;CAkBA,wBAAwB,IAAY,WAAgB;EAClD,KAAK,iBAAiB,IAAI,IAAI,SAAS;EACvC,OAAO;CACT;;;;;;;CAQA,mBAAmB,IAAY;EAC7B,OAAO,KAAK,iBAAiB,IAAI,EAAE;CACrC;;;;;;;;;;CAWA,0BAA0B,UAAkC;EAC1D,OAAO,KAAK,wBAAwB,IAAI,QAAQ;CAClD;;;;;;;CAQA,sBAAsB,OAAoD;EACxE,OAAO,KAAK,wBAAwB,QAAQ,KAAK;CACnD;CAEA,4BAA4B,MAAc,WAAgB;EACxD,OAAO,KAAK,YAAY,SAAS,MAAM,SAAS;CAClD;CAEA,uBAAuB,MAAc;EACnC,OAAO,KAAK,YAAY,IAAI,IAAI;CAClC;;;;;;;;;;;;CAaA,qBAAqB,MAAc,SAA8B;EAC/D,OAAO,KAAK,cAAc,SAAS,MAAM,OAAO;CAClD;;;;;;CAOA,sBAAsB,SAA0B;EAC9C,KAAK,cAAc,aAAa,OAAO;CACzC;;;;;;;;;CAUA,iBAAiB,QAA4B;EAC3C,OAAO,KAAK,cAAc,KAAK,QAAQ,IAAI;CAC7C;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BA,sBAAsB,oBAGnB;EACD,MAAM,WAAW,IAAI,iBAAiB;EACtC,KAAK,oBAAoB,KAAK;GAC5B,IAAI,mBAAmB;GACvB,WAAW,mBAAmB;GACpB;GACV,SAAS,SAAS;EACpB,CAAC;EACD,OAAO;CACT;;;;;;;;;;;;;;;;;;CAmBA,sBAAsB,IAA8B;EAClD,MAAM,qBAAqB,KAAK,oBAAoB,MAAM,uBAAuB,mBAAmB,OAAO,EAAE;EAC7G,IAAI,CAAC,oBACH,MAAM,IAAI,MAAM,+BAA+B,GAAG,WAAW;EAE/D,OAAO,mBAAmB;CAC5B;;;;;;;;;;;;;;;;;;;;;;;;CAyBA,gBAAgB,IAAY,QAAa,CAAC,GAAkB;EAC1D,IAAI,CAAC,KAAK,WAAW,OAAO,EAAE,GAC5B,MAAM,IAAI,MAAM,sBAAsB,GAAG,uGAAuG;EAElJ,OAAO,IAAI,SAAe,YAAY;GACpC,IAAI,WAAW;GACf,MAAM,UAAU,SAAe;IAC7B,IAAI,UAAU;IACd,WAAW;IACX,OAAO,WAAW,IAAI;IACtB,QAAQ;GACV;GAEA,KAAK,WAAW,QAAQ,IAAI;IAC1B,GAAG;IACH,UAAU;GACZ,CAAC;EACH,CAAC;CACH;CAEA,MAAM,aAAa,EAAE,SAAsC;EACzD,IAAI,KAAK,qBAAqB;EAE9B,MAAM,gBAAgB,KAAK,SAAS,iBAAiB;EAIrD,IAAI,EAFF,CAAC,iBACD,gBAAgB,aAAa,IACjB;GACZ,KAAK,+BAA+B,aAAa;GACjD;EACF;EAEA,MAAM,YAAY,KAAK,IAAI;EAC3B,MAAM,gBAAgB,YAAY,KAAK,IACnC,mBAAmB,OAAO,eAAe,YAAY,CAAC,IACtD;EACJ,IAAI,CAAC,eAAe;EACpB,IAAI,YAAY,aAAa,GAAG;GAC9B,MAAM,WAAW,cAAc,YAAY;GAC3C,IAAI,YAAY,KAAK,iBAAiB;GACtC,KAAK,kBAAkB,YAAY;EACrC;EACA,KAAK,2BAA2B;EAEhC,IAAI;EACJ,IAAI;EACJ,IAAI,KAAK,qBAAqB,KAAK,YAAY;GAC7C,MAAM,OAAO,KAAK,WAAW,YAAY,eAAe,SAAS;GACjE,QAAQ,KAAK;GACb,OAAO,KAAK;EACd,OAAO;GACL,QAAQ,EAAE,KAAK;GACf,OAAO,KAAK,eAAe;EAC7B;EACA,KAAK,oBAAoB;EACzB,KAAK,MAAM,UAAU,yBAAyB,MAAM;GAAE,OAAO;GAAe,UAAU,KAAK;EAAS,CAAC,EAAE,UAAU;EAEjH,MAAM,YAAY,KAAK,wBAAwB;EAC/C,IAAI,iBAAiB,WAAW;GAC9B,KAAK,4BAA4B,eAAe,aAAa;GAC7D,IAAI,KAAK,qBAAqB,KAAK,YAAY;IAC7C,KAAK,wBAAwB,KAAK,KAAK;IACvC,IAAI,KAAK,wBAAwB,SAAS,KACxC,KAAK,0BAA0B,KAAK,wBAAwB,MAAM,IAAI;GAE1E;EACF;EAEA,KAAK,eAAe,eAAe,OAAO,MAAM,WAAW,IAAI;EAC/D,KAAK,gBAAgB,YAAY,aAAa,IAC1C,KAAK,IAAI,KAAK,cAAc,YAAY,4BACxC,KAAK,IAAI;CACf;CAEA,MAAM,YAAY,QAA+B,CAAC,GAAG;EACnD,MAAM,gBAAgB,KAAK,SAAS,iBAAiB;EAKrD,MAAM,YAAY,mBAAmB,OAHnC,OAAO,eAAe,cAAc,aAChC,cAAc,UAAU,IACxB,eAAe,SACwC;EAC7D,IAAI,CAAC,WAAW;EAChB,MAAM,KAAK,aAAa,EAAE,OAAO,UAAU,CAAC;CAC9C;CAIA,cAAc,QAAwC,MAAkB;EACtE,IAAI,KAAK,qBAAqB;EAC9B,MAAM,gBAAgB,KAAK,SAAS,iBAAiB;EAIrD,IAAI,EAFF,CAAC,iBACD,gBAAgB,aAAa,IACjB;EAEd,MAAM,UAAU,qBAAqB,QAAe,IAAI;EAExD,KAAK,MAAM,UAAU,yBAAyB,MAAM;GAClD,OAAO,QAAQ;GACf,QAAQ,QAAQ;GAChB,MAAM,QAAQ;GACd,UAAU,KAAK;EACjB,CAAC,EAAE,UAAU;EACb,KAAK,UAAU,KAAK,UAAU,OAAO;CACvC;CAEA,IAAI,OAAO;EACT,OAAO;CACT;CAEA,IAAI,SAAS;EACX,OAAO,KAAK;CACd;CAEA,IAAI,WAAW;EACb,OAAO,KAAK,eAAe;CAC7B;CAEA,IAAI,QAAQ;EACV,OAAO,KAAK;CACd;CAEA,cAAc,IAAY;EACxB,OAAO,KAAK,UAAU,cAAc,EAAE;CACxC;CAEA,iBAAiC;EAC/B,OAAQ,KAAK,UAAkB,UAAU,KAAK;CAChD;CAEA,2BAA2C;EACzC,MAAM,WAAY,KAAK,UAAkB,QAAQ,WAAW,GAAG,cAAc;EAC7E,OAAO,OAAO,aAAa,YAAY,OAAO,SAAS,QAAQ,KAAK,WAAW,IAC3E,WAAW,MACX,MAAO;CACb;CAEA,yBAAiC,YAAgC,MAAM,KAAK,IAAI,GAAS;EACvF,IAAI,OAAO,eAAe,YAAY,CAAC,OAAO,SAAS,UAAU,GAC/D;EAEF,KAAK,mBAAmB;EACxB,KAAK,qBAAqB;CAC5B;CAEA,mBAA2B,MAAM,KAAK,IAAI,GAAuB;EAC/D,IAAI,OAAO,KAAK,qBAAqB,YAAY,KAAK,sBAAsB,GAC1E;EAEF,MAAM,eAAe,KAAK,IAAI,IAAI,MAAM,KAAK,sBAAsB,KAAK,yBAAyB,CAAC;EAClG,OAAO,KAAK,mBAAmB;CACjC;CAEA,wBAAgC,YAAkE;EAChG,IAAI,WAAW,kBAAkB,OAC/B,OAAO;EAET,MAAM,WAAW,KAAK;EACtB,IAAI,CAAC,UAAU,UAAU,CAAC,OAAO,SAAS,WAAW,KAAK,KAAK,WAAW,SAAS,GACjF,OAAO;EAET,MAAM,SAAS,WAAW;EAC1B,MAAM,YAAY,WAAW;EAC7B,IACE,CAAC,UACD,CAAC,aACD,CAAC,OAAO,SAAS,OAAO,CAAC,KACzB,CAAC,OAAO,SAAS,OAAO,CAAC,KACzB,CAAC,OAAO,SAAS,UAAU,CAAC,KAC5B,CAAC,OAAO,SAAS,UAAU,CAAC,KAC3B,UAAU,MAAM,KAAK,UAAU,MAAM,GAEtC,OAAO;EAGT,MAAM,MAAM,SAAS,OAAO,QAC1B,IAAI,QAAQ,OAAO,GAAG,OAAO,CAAC,GAC9B,IAAI,QAAQ,UAAU,GAAG,UAAU,CAAC,GACpC,WAAW,OACX,WAAW,gBACV,WAAW,WAAW,gBAAgB,SAAS,CAAC,WAAW,WAAW,OAAO,SAAS,WAAW,OACpG;EACA,IAAI,CAAC,KACH,OAAO;EAET,OAAO;GACL,IAAI,WAAW;GACf,UAAU,IAAI,OAAO;GACrB,GAAG,IAAI,MAAM;GACb,GAAG,IAAI,MAAM;GACb,UAAU,IAAI;EAChB;CACF;CAEA,0BAA2C;EACzC,MAAM,SAAS,KAAK,UAAU,iBAAiB;EAC/C,MAAM,OAAO,KAAK,eAAe;EACjC,IAAI,CAAC,UAAU,CAAC,MACd,OAAO;EAET,IAAI,CAAC,OAAO,IACV,OAAO,KAAK;EAEd,IAAI,KAAK,SAAS,QAAQ,IAAI,GAC5B,OAAO;EAET,IAAI;GACF,KAAK,SAAS,WAAW;EAC3B,SAAS,OAAO;GACd,QAAQ,MAAM,6DAA6D,KAAK;GAChF,OAAO;EACT;EACA,OAAO,CAAC,CAAC,KAAK,SAAS,QAAQ,IAAI;CACrC;CAEA,wBAAsC;EACpC,IAAI,CAAC,KAAK,qBAAqB,CAAC,KAAK,UACnC;EAEF,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,KAAK,4BAA4B,GACnC,KAAK,0BAA0B;EAEjC,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,KAAK,uBAAuB,CAAC;EAC7E,KAAK,0BAA0B;EAC/B,KAAK,SAAS,kBAAkB,OAAO;CACzC;CAEA,8BAA4C;EAC1C,IAAI,CAAC,KAAK,qBAAqB,CAAC,KAAK,cAAc,KAAK,wBAAwB,WAAW,GACzF;EAEF,MAAM,QAAQ,KAAK,oBAAoB;EACvC,OAAO,KAAK,wBAAwB,SAAS,GAAG;GAC9C,MAAM,QAAQ,KAAK,wBAAwB,MAAM;GACjD,IAAI,OAAO,UAAU,UACnB,KAAK,WAAW,qBAAqB,OAAO,KAAK;EAErD;CACF;CAEA,6BAAgE;EAC9D,IAAI,CAAC,KAAK,qBAAqB,CAAC,KAAK,YACnC,OAAO,CAAC;EAEV,MAAM,gBAAgB,KAAK,WAAW,iBAAiB;EACvD,MAAM,aAAwC,CAAC;EAC/C,KAAK,MAAM,SAAS,eAAe;GACjC,MAAM,QAAQ,MAAM;GACpB,IAAI,CAAC,OAAO;GACZ,IAAI,OAAO,MAAM,MAAM,YAAY,OAAO,MAAM,MAAM,UAAU;GAChE,WAAW,KAAK;IACd,OAAO,MAAM;IACb,MAAM,MAAM;IACZ,WAAW,MAAM;IACjB,OAAO,MAAM;IACb,GAAG,MAAM;IACT,GAAG,MAAM;IACT,WAAW,MAAM,aAAa,qBAAqB,MAAM,SAAS;GACpE,CAAC;EACH;EACA,IAAI,WAAW,SAAS,KAAK,4BAC3B,OAAO,WAAW,MAAM,CAAC,KAAK,0BAA0B;EAE1D,OAAO;CACT;CAEA,eACE,OACA,OACA,MACA,WACA,QAAQ,OACF;EACN,MAAM,aAAa,KAAK,2BAA2B;EACnD,MAAM,wBACJ,WAAW,SAAS,IAAI,WAAW,WAAW,SAAS,GAAG,QAAQ;EAKpE,IAHE,CAAC,SACD,yBAAyB,KAAK,yBAC9B,YAAY,KAAK,qBAAqB,KAAK,8BAE3C;EAGF,KAAK,UAAU,KAAK,QAAQ;GAC1B;GACA;GACA;GACA;GACA;EACF,CAAC;EACD,KAAK,qBAAqB;EAC1B,KAAK,wBAAwB,KAAK,IAAI,KAAK,uBAAuB,uBAAuB,KAAK;CAChG;CAEA,uBAAqC;EACnC,IAAI,CAAC,KAAK,qBAAqB,CAAC,KAAK,YACnC;EAEF,MAAM,SAAS,KAAK,UAAU,mBAAmB;EACjD,IACE,UACA,CAAC,gBAAgB,MAAM,GACvB;GACA,KAAK,+BAA+B,MAAM;GAC1C;EACF;EACA,MAAM,gBAAgB,KAAK,WAAW,iBAAiB;EACvD,IAAI,cAAc,WAAW,GAC3B;EAEF,MAAM,SAAS,cAAc,cAAc,SAAS;EACpD,IAAI,CAAC,QACH;EAEF,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,MAAM,KAAK,qBAAqB,KAAK,8BACvC;EAEF,KAAK,eAAe,OAAO,WAAW,OAAO,OAAO,OAAO,MAAM,KAAK,KAAK;CAC7E;CAEA,4BACE,QACA,OACS;EACT,IAAI,YAAY,KAAK,GAAG;GACtB,MAAM,YAAY,kBAAkB,MAAM,SAAS;GACnD,OAAO,gBAAgB,SAAS;GAChC,OAAO,QAAS,KAAK,SAAiB,WAAW,QAAQ,KAAK,CAAC;EACjE;EAEA,MAAM,YAAY,qBAAqB,KAAK;EAC5C,IAAI,CAAC,WAAW,OAAO;EACvB,OAAO,gBAAgB,SAAS;EAChC,OAAO,QAAS,KAAK,SAAiB,WAAW,QAAQ,SAAS,CAAC;CACrE;CAEA,sBAA0D;EACxD,MAAM,gBAAgB,KAAK,UAAU,iBAAiB;EACtD,IAAI,CAAC,eACH,OAAO;GAAE,GAAG;GAAG,GAAG;GAAG,WAAW,UAAU;EAAK;EAEjD,MAAM,UAAU,KAAK,SAAS,gBAAgB,cAAc,IAAI,UAAU;EAI1E,OAAO;GAAE,GAHC,SAAS,KAAK,cAAc,EAAE;GAG5B,GAFF,SAAS,KAAK,cAAc,EAAE;GAEzB,WADG,cAAc,UACjB;EAAU;CAC3B;CAEA,wBAAgC,OAAyC;EACvE,MAAM,SAAS,KAAK,UAAU,iBAAiB;EAC/C,IAAI,CAAC,QAAQ;EACb,MAAM,SAAS,OAAO,OAAO,WAAW,aAAa,OAAO,OAAO,IAAI,OAAO;EAC9E,MAAM,QAAQ,QAAQ,KAAK;EAC3B,MAAM,SAAS,QAAQ,KAAK;EAE5B,IAAI,CADY,KAAK,SAAS,aAAa,OAAO,IAAI,MAAM,GAAG,MAAM,GAAG,OAAO,MAC1E,GACH,KAAK,SAAS,gBAAgB,OAAO,IAAI,MAAM,GAAG,MAAM,GAAG,UAAU;EAEvE,OAAO,EAAE,IAAI,KAAK,MAAM,MAAM,CAAC,CAAC;EAChC,OAAO,EAAE,IAAI,KAAK,MAAM,MAAM,CAAC,CAAC;EAChC,IAAI,MAAM,WACR,OAAO,gBAAgB,MAAM,SAAS;CAE1C;CAEA,iCAA+C;EAC7C,IAAI,CAAC,KAAK,mBAAmB;GAC3B,KAAK,aAAa,KAAA;GAClB,KAAK,UAAU,4BAA4B,KAAK;GAChD;EACF;EACA,MAAM,gBAAiB,KAAK,cAAsB,YAAY;EAC9D,MAAM,eAAe,OAAO,kBAAkB,WAAW,gBAAgB;EACzE,MAAM,uBAAwB,KAAK,cAAsB,YAAY;EACrE,MAAM,oBACJ,OAAO,yBAAyB,WAC5B,uBACA,KAAK,IAAI,KAAK,KAAK,KAAK,eAAe,EAAE,IAAI,GAAG;EACtD,KAAK,UAAU,4BAA4B,IAAI;EAC/C,KAAK,aAAa,IAAI,qBAAkD;GACtE,qBAAsB,KAAK,cAAsB,YAAY,uBAAuB,KAAK;GACzF;GACA;GACA,sBAAsB,KAAK,eAAe;GAC1C,uBAAuB,KAAK,oBAAoB;GAChD,wBAAwB,UAAU,KAAK,wBAAwB,KAAK;EACtE,CAAC;CACH;CAEA,mBAAmB;EACjB,OAAO,KAAK,SAAS,iBAAiB;CACxC;CAEA,iBAAiB,UAAkB,GAAG,MAAmB;EACvD,KAAK,MAAM,UAAU,mBAAmB,YAAY,GAAG,IAAI,EAAE,UAAU;CACzE;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BA,8BAA4C;EAC1C,KAAK,6BAA6B,cAAc;GAC9C,KAAK,kBAAkB,KAAK,QAAO,cAAa,cAAc,IAAI,CAAC;GACnE,KAAK,kBAAkB,KAAK,QAAO,aAAY,aAAa,IAAI,CAAC;GACjE,KAAK,iBAAiB,KAAK,QAAO,aAAY,aAAa,IAAI,CAAC;GAChE,KAAK,gBAAgB,KAAK,QAAO,aAAY,aAAa,IAAI,CAAC;EACjE,CAAC,EAAE,KACD,KAAK,CAAC,GACN,gBAAgB;GAEd,OAAO,KAAK,MAAM,UAAU,kCAAkC,KAAK,QAAQ;EAC7E,CAAC,CACH,EAAE,UAAU;CACd;;;;;;;;;;;;;CAcA,8BAA8B;EAC5B,KAAK,+BAA+B;EACpC,KAAK,cAAc;EACnB,KAAK,oBAAoB;EACzB,KAAK,0BAA0B,CAAC;EAChC,KAAK,0BAA0B;EAC/B,KAAK,qBAAqB;EAC1B,KAAK,wBAAwB;CAC/B;;;;;;;;;;;;;;;;CAiBA,+BAA+B,SAAc,KAAK,UAAU,mBAAmB,GAAY;EACzF,IAAI,CAAC,QACH,OAAO;EAET,KAAM,UAAkB,eAAe,MAAM;EAC7C,KAAK,YAAY,mBAAmB;EACpC,KAAK,0BAA0B,CAAC;EAChC,KAAK,gBAAgB;EACrB,KAAK,qBAAqB,KAAK,IAAI;EACnC,KAAK,wBAAwB,KAAK;EAClC,OAAO;CACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiDA,MACE,UACA,SAOM;EACN,MAAM,WAAW,YAAY,KAAK;EAClC,IAAI,CAAC,UAAU;EAEf,MAAM,SAAS,KAAK,SAAS,cAAc,QAAQ;EACnD,IAAI,UAAU,OAAO,OAAO,UAAU,YACpC,OAAO,MAAM,OAAO;CAExB;CAEA,eAAuB,KAA4F;EACjH,KAAK,yBAAyB,IAAI,UAAU;EAC5C,MAAM,oBAAoB,KAAK,8BAA8B;EAC7D,IAAI,KAAK,qBAAqB,KAAK,YAAY;GAC7C,MAAM,SAAS,KAAK,WAAW,eAAe;IAC5C,OAAO,IAAI;IACX,YAAY,IAAI;IAChB,OACE,CAAC,qBAAqB,OAAO,IAAI,MAAM,YAAY,OAAO,IAAI,MAAM,WAChE;KAAE,GAAG,IAAI;KAAG,GAAG,IAAI;KAAG,WAAW,IAAI;IAAU,IAC/C,KAAA;GACR,CAAC;GACD,IAAI,OAAO,SAAS,OAAO,qBACzB,KAAK,oBAAoB,OAAO,OAAO,OAAO,aAAa;GAE7D;EACF;EAEA,IAAI,OAAO,IAAI,MAAM,YAAY,OAAO,IAAI,MAAM,UAChD;EAEF,IAAI,mBACF;EAEF,MAAM,SAAS,KAAK,iBAAiB;EACrC,MAAM,OAAO,KAAK,eAAe;EACjC,IAAI,CAAC,UAAU,CAAC,MACd;EAEF,MAAM,SAAS,OAAO,OAAO,WAAW,aAAa,OAAO,OAAO,IAAI,OAAO;EAC9E,MAAM,QAAQ,QAAQ,KAAK;EAC3B,MAAM,SAAS,QAAQ,KAAK;EAE5B,IAAI,CADY,KAAK,SAAS,aAAa,MAAM,IAAI,GAAG,IAAI,GAAG,OAAO,MACjE,GACH,KAAK,SAAS,gBAAgB,MAAM,IAAI,GAAG,IAAI,GAAG,UAAU;EAE9D,OAAO,EAAE,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC;EAC9B,OAAO,EAAE,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC;EAC9B,IAAI,IAAI,WACN,OAAO,gBAAgB,IAAI,SAAS;CAExC;CAEA,oBACE,oBACA,eACM;EACN,MAAM,SAAS,KAAK,iBAAiB;EACrC,IAAI,CAAC,QACH;EAEF,IAAI,CAAC,gBAAgB,MAAM,GAAG;GAC5B,KAAK,+BAA+B,MAAM;GAC1C;EACF;EAEA,KAAM,SAAiB,aAAa,MAAM;EAC1C,KAAK,wBAAwB,kBAAkB;EAE/C,IAAI,CAAC,cAAc,QACjB;EAIF,MAAM,eAAe,cAAc,MAAM,IAAI;EAC7C,KAAK,MAAM,SAAS,cAAc;GAChC,IAAI,CAAC,OAAO,WAAW;GACvB,KAAK,4BAA4B,QAAQ,MAAM,SAAS;GACxD,KAAK,SAAS,mBAAmB;GACjC,KAAK,YAAY,qBAAqB,MAAM,OAAO,KAAK,oBAAoB,CAAC;EAC/E;CACF;;;;;;;;;;;;;CAcA,MAAc,6BAA6B,aAAoC,CAE/E;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BA,QAAc;EACZ,IAAI;GAEF,KAAK,MAAM,gBAAgB,KAAK,mBAC9B,IAAI,gBAAgB,OAAO,aAAa,gBAAgB,YACtD,aAAa,YAAY;GAG7B,KAAK,oBAAoB,CAAC;GAG1B,IAAI,KAAK,cAAc;IACrB,cAAc,KAAK,YAAY;IAC/B,KAAK,eAAe;GACtB;GAGA,IAAI,KAAK,8BAA8B,OAAO,KAAK,2BAA2B,gBAAgB,YAAY;IACxG,KAAK,2BAA2B,YAAY;IAC5C,KAAK,6BAA6B,KAAA;GACpC;GAKA,IAAI,KAAK,eACP,IAAI;IAEF,IAAI,OAAQ,KAAK,cAAsB,YAAY,YACjD,KAAM,cAAsB,QAAQ;IAGtC,KAAK,gBAAgB,KAAA;GACvB,SAAS,OAAO,CAEhB;GAIF,IAAI,KAAK,YAAY,OAAQ,KAAK,SAAiB,UAAU,YAC3D,KAAM,SAAiB,MAAM,IAAI;GAInC,KAAK,cAAc;GAGnB,IAAI,KAAK,iBAAiB,OAAO,WAAW,aAAa;IACvD,OAAO,oBAAoB,UAAU,KAAK,aAAa;IACvD,KAAK,gBAAgB,KAAA;GACvB;GAEA,IAAI,KAAK,sBAAsB,KAAK,eAAe;IACjD,KAAK,cAAc,oBAAoB,eAAe,KAAK,kBAAkB;IAC7E,KAAK,cAAc,oBAAoB,eAAe,KAAK,kBAAkB;IAC7E,IAAI,KAAK,kBACP,KAAK,cAAc,oBAAoB,aAAa,KAAK,gBAAgB;IAE3E,IAAI,KAAK,sBAAsB;KAC7B,KAAK,cAAc,oBAAoB,iBAAiB,KAAK,oBAAoB;KACjF,KAAK,cAAc,oBAAoB,gBAAgB,KAAK,oBAAoB;IAClF;IACA,KAAK,qBAAqB,KAAA;IAC1B,KAAK,mBAAmB,KAAA;IACxB,KAAK,uBAAuB,KAAA;IAC5B,KAAK,gBAAgB,KAAA;GACvB;GAKA,MAAM,sBAAsB,KAAK,YAAY,OAAO,KAAK,SAAS,YAAY;GAE9E,IAAI,KAAK,aAAa,OAAO,KAAK,UAAU,YAAY,YAAY;IAClE,IAAI;KAEF,IAAI,KAAK,UAAU,QAAQ;MACzB,IAAI,OAAO,KAAK,UAAU,OAAO,SAAS,YACxC,KAAK,UAAU,OAAO,KAAK;MAG7B,IAAI,OAAO,KAAK,UAAU,OAAO,cAAc,YAC7C,KAAK,UAAU,OAAO,UAAU;KAEpC;KAGA,IAAI,KAAK,YAAa,KAAK,SAAiB,QAAQ;MAClD,IAAI,OAAQ,KAAK,SAAiB,OAAO,SAAS,YAChD,KAAM,SAAiB,OAAO,KAAK;MAErC,IAAI,OAAQ,KAAK,SAAiB,OAAO,cAAc,YACrD,KAAM,SAAiB,OAAO,UAAU;KAE5C;KAGA,IAAI,KAAK,UAAU,UAAU,KAAK,UAAU,OAAO,YACjD,KAAK,UAAU,OAAO,WAAW,YAAY,KAAK,UAAU,MAAM;KAKpE,KAAK,UAAU,QAAQ,IAAI;IAC7B,SAAS,OAAO,CAEhB;IACA,KAAK,YAAY,KAAA;IAEjB,KAAK,WAAW;GAClB,OAAO,IAAI,qBAAqB;IAE9B,IAAI;KAEF,IAAK,KAAK,SAAiB,QAAQ;MACjC,IAAI,OAAQ,KAAK,SAAiB,OAAO,SAAS,YAChD,KAAM,SAAiB,OAAO,KAAK;MAErC,IAAI,OAAQ,KAAK,SAAiB,OAAO,cAAc,YACrD,KAAM,SAAiB,OAAO,UAAU;KAE5C;KAEA,KAAK,SAAS,QAAQ,IAAI;IAC5B,SAAS,OAAO,CAEhB;IACA,KAAK,WAAW;GAClB;GAGA,IAAI,KAAK,YAEP,KAAK,aAAa,KAAA;GAIpB,KAAK,eAAe,IAAI,IAAI;GAC5B,KAAK,qBAAqB,IAAI,IAAI;GAClC,KAAK,uBAAuB,IAAI,CAAC,CAAC;GAClC,KAAK,wBAAwB,IAAI,CAAC,CAAC;GACnC,KAAK,wBAAwB,MAAM;GAGnC,KAAK,aAAa,MAAM;GACxB,KAAK,OAAO,MAAM;GAClB,KAAK,sBAAsB,CAAC;GAC5B,KAAK,iBAAiB,WAAW,CAAC;GAGlC,KAAK,sBAAsB;GAC3B,KAAK,gBAAgB;GACrB,KAAK,oBAAoB;GACzB,KAAK,cAAc;GACnB,KAAK,MAAM;GACX,KAAK,qBAAqB;GAC1B,KAAK,wBAAwB;GAG7B,KAAK,kBAAkB,KAAK,KAAK;GACjC,KAAK,kBAAkB,KAAK,KAAK;GACjC,KAAK,iBAAiB,KAAK,KAAK;GAChC,KAAK,gBAAgB,KAAK,KAAK;EACjC,SAAS,OAAO;GACd,QAAQ,KAAK,uCAAuC,KAAK;EAC3D;CACF;AACF"}
|
|
@@ -7,8 +7,10 @@ export type SocketUpdateProperties = {
|
|
|
7
7
|
host?: string;
|
|
8
8
|
query?: SocketQuery;
|
|
9
9
|
};
|
|
10
|
+
export type WebSocketMode = "standalone" | "mmorpg";
|
|
10
11
|
export declare abstract class AbstractWebsocket {
|
|
11
12
|
protected context: Context;
|
|
13
|
+
readonly mode?: WebSocketMode;
|
|
12
14
|
constructor(context: Context);
|
|
13
15
|
abstract connection(listeners?: (data: any) => void): Promise<void>;
|
|
14
16
|
abstract emit(event: string, data: any): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AbstractSocket.js","names":[],"sources":["../../src/services/AbstractSocket.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\n\nexport const WebSocketToken = \"websocket\";\n\nexport type SocketQueryValue = string | null | undefined;\nexport type SocketQuery = Record<string, SocketQueryValue>;\nexport type SocketUpdateProperties = {\n room: string;\n host?: string;\n query?: SocketQuery;\n};\n\nexport abstract class AbstractWebsocket {\n constructor(protected context: Context) {}\n\n abstract connection(listeners?: (data: any) => void): Promise<void>;\n abstract emit(event: string, data: any): void;\n abstract on(event: string, callback: (data: any) => void): void;\n abstract off(event: string, callback: (data: any) => void): void;\n abstract updateProperties(params: SocketUpdateProperties): void;\n abstract reconnect(listeners?: (data: any) => void): Promise<void>;\n}\n"],"mappings":";AAEA,IAAa,iBAAiB;
|
|
1
|
+
{"version":3,"file":"AbstractSocket.js","names":[],"sources":["../../src/services/AbstractSocket.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\n\nexport const WebSocketToken = \"websocket\";\n\nexport type SocketQueryValue = string | null | undefined;\nexport type SocketQuery = Record<string, SocketQueryValue>;\nexport type SocketUpdateProperties = {\n room: string;\n host?: string;\n query?: SocketQuery;\n};\nexport type WebSocketMode = \"standalone\" | \"mmorpg\";\n\nexport abstract class AbstractWebsocket {\n readonly mode?: WebSocketMode;\n\n constructor(protected context: Context) {}\n\n abstract connection(listeners?: (data: any) => void): Promise<void>;\n abstract emit(event: string, data: any): void;\n abstract on(event: string, callback: (data: any) => void): void;\n abstract off(event: string, callback: (data: any) => void): void;\n abstract updateProperties(params: SocketUpdateProperties): void;\n abstract reconnect(listeners?: (data: any) => void): Promise<void>;\n}\n"],"mappings":";AAEA,IAAa,iBAAiB;AAW9B,IAAsB,oBAAtB,MAAwC;CAGtC,YAAY,SAA4B;EAAlB,KAAA,UAAA;CAAmB;AAQ3C"}
|
package/dist/services/mmorpg.js
CHANGED
|
@@ -12,6 +12,7 @@ var BridgeWebsocket = class extends AbstractWebsocket {
|
|
|
12
12
|
super(context);
|
|
13
13
|
this.context = context;
|
|
14
14
|
this.options = options;
|
|
15
|
+
this.mode = "mmorpg";
|
|
15
16
|
this.pendingOn = [];
|
|
16
17
|
this.acceptedOpenListeners = /* @__PURE__ */ new Set();
|
|
17
18
|
this.targetRoom = "lobby-1";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mmorpg.js","names":[],"sources":["../../src/services/mmorpg.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\nimport { connectionRoom } from \"@signe/sync/client\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { isNativeSocketEvent, waitForRpgjsConnected } from \"./mmorpg-connection\";\n\nexport interface MmorpgOptions {\n host?: string;\n connectionId?: string;\n connectionIdScope?: \"local\" | \"session\" | \"ephemeral\";\n query?: SocketQuery | (() => SocketQuery | undefined);\n socketOptions?: Record<string, any>;\n}\n\nexport class BridgeWebsocket extends AbstractWebsocket {\n private socket: any;\n private privateId: string;\n private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];\n private acceptedOpenListeners = new Set<(data: any) => void>();\n private targetRoom = \"lobby-1\";\n\n constructor(protected context: Context, private options: MmorpgOptions = {}) {\n super(context);\n this.privateId = this.resolveConnectionId();\n }\n\n private resolveConnectionId(): string {\n if (this.options.connectionId) {\n return this.options.connectionId;\n }\n\n const scope = this.options.connectionIdScope ?? \"local\";\n const key = \"rpgjs-user-id\";\n\n if (scope === \"ephemeral\") {\n return crypto.randomUUID();\n }\n\n const storage =\n scope === \"session\"\n ? window.sessionStorage\n : window.localStorage;\n\n const existing = storage.getItem(key);\n if (existing) {\n return existing;\n }\n\n const id = crypto.randomUUID();\n storage.setItem(key, id);\n return id;\n }\n\n private resolveQuery(): SocketQuery {\n const query = typeof this.options.query === \"function\"\n ? this.options.query()\n : this.options.query;\n\n return query ?? {};\n }\n\n async connection(listeners?: (data: any) => void) {\n // tmp\n class Room {\n \n }\n const instance = new Room()\n const host = this.options.host || window.location.host;\n this.socket = await connectionRoom({\n maxRetries: 0,\n ...this.options.socketOptions,\n host,\n room: this.targetRoom,\n id: this.privateId,\n query: {\n ...this.resolveQuery(),\n id: this.privateId,\n },\n }, instance)\n\n const pendingOn = this.pendingOn;\n this.pendingOn = [];\n pendingOn\n .filter(({ event }) => !this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n await waitForRpgjsConnected(this.socket.conn);\n pendingOn\n .filter(({ event }) => this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n this.emitAcceptedOpen();\n listeners?.(this.socket)\n }\n\n on(key: string, callback: (data: any) => void) {\n if (!this.socket) {\n this.pendingOn.push({ event: key, callback });\n return;\n }\n this.attachEvent(key, callback);\n }\n\n off(event: string, callback: (data: any) => void) {\n if (!this.socket) return;\n if (event === \"open\") {\n this.acceptedOpenListeners.delete(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.removeEventListener(event, callback);\n return;\n }\n this.socket.off(event, callback);\n }\n\n emit(event: string, data: any) {\n this.socket.emit(event, data);\n }\n\n private attachEvent(event: string, callback: (data: any) => void) {\n if (event === \"open\") {\n this.acceptedOpenListeners.add(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.addEventListener(event, callback);\n return;\n }\n this.socket.on(event, callback);\n }\n\n private emitAcceptedOpen() {\n const event = new Event(\"open\");\n this.acceptedOpenListeners.forEach((callback) => callback(event));\n }\n\n updateProperties({ room, host, query }: SocketUpdateProperties) {\n if (!this.socket?.conn) return;\n this.targetRoom = room;\n this.socket.conn.updateProperties({\n room,\n id: this.privateId,\n host: host || this.options.host || window.location.host,\n query: {\n ...this.resolveQuery(),\n ...query,\n id: this.privateId,\n },\n })\n }\n\n private isNativeSocketEvent(event: string) {\n return isNativeSocketEvent(event);\n }\n\n async reconnect(_listeners?: (data: any) => void): Promise<void> {\n if (!this.socket?.conn) return;\n const conn = this.socket.conn;\n const connected = waitForRpgjsConnected(conn, 10000, { ignoreCleanClose: true });\n conn.reconnect();\n await connected;\n this.emitAcceptedOpen();\n }\n\n getCurrentRoom(): string {\n return this.targetRoom || this.socket?.conn?.room || \"lobby-1\";\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n constructor(protected context: Context, private _options: MmorpgOptions) {\n super(context);\n }\n\n async update(_map: any) {\n // In MMORPG mode, clients are untrusted and must not push map definitions.\n // Map bootstrap/update is handled server-side by @rpgjs/vite.\n return;\n }\n}\n\nexport function provideMmorpg(options: MmorpgOptions) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, options),\n },\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => new UpdateMapStandaloneService(context, options),\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":";;;;;;;;;AAkBA,IAAa,kBAAb,cAAqC,kBAAkB;
|
|
1
|
+
{"version":3,"file":"mmorpg.js","names":[],"sources":["../../src/services/mmorpg.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\nimport { connectionRoom } from \"@signe/sync/client\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { isNativeSocketEvent, waitForRpgjsConnected } from \"./mmorpg-connection\";\n\nexport interface MmorpgOptions {\n host?: string;\n connectionId?: string;\n connectionIdScope?: \"local\" | \"session\" | \"ephemeral\";\n query?: SocketQuery | (() => SocketQuery | undefined);\n socketOptions?: Record<string, any>;\n}\n\nexport class BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"mmorpg\" as const;\n\n private socket: any;\n private privateId: string;\n private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];\n private acceptedOpenListeners = new Set<(data: any) => void>();\n private targetRoom = \"lobby-1\";\n\n constructor(protected context: Context, private options: MmorpgOptions = {}) {\n super(context);\n this.privateId = this.resolveConnectionId();\n }\n\n private resolveConnectionId(): string {\n if (this.options.connectionId) {\n return this.options.connectionId;\n }\n\n const scope = this.options.connectionIdScope ?? \"local\";\n const key = \"rpgjs-user-id\";\n\n if (scope === \"ephemeral\") {\n return crypto.randomUUID();\n }\n\n const storage =\n scope === \"session\"\n ? window.sessionStorage\n : window.localStorage;\n\n const existing = storage.getItem(key);\n if (existing) {\n return existing;\n }\n\n const id = crypto.randomUUID();\n storage.setItem(key, id);\n return id;\n }\n\n private resolveQuery(): SocketQuery {\n const query = typeof this.options.query === \"function\"\n ? this.options.query()\n : this.options.query;\n\n return query ?? {};\n }\n\n async connection(listeners?: (data: any) => void) {\n // tmp\n class Room {\n \n }\n const instance = new Room()\n const host = this.options.host || window.location.host;\n this.socket = await connectionRoom({\n maxRetries: 0,\n ...this.options.socketOptions,\n host,\n room: this.targetRoom,\n id: this.privateId,\n query: {\n ...this.resolveQuery(),\n id: this.privateId,\n },\n }, instance)\n\n const pendingOn = this.pendingOn;\n this.pendingOn = [];\n pendingOn\n .filter(({ event }) => !this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n await waitForRpgjsConnected(this.socket.conn);\n pendingOn\n .filter(({ event }) => this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n this.emitAcceptedOpen();\n listeners?.(this.socket)\n }\n\n on(key: string, callback: (data: any) => void) {\n if (!this.socket) {\n this.pendingOn.push({ event: key, callback });\n return;\n }\n this.attachEvent(key, callback);\n }\n\n off(event: string, callback: (data: any) => void) {\n if (!this.socket) return;\n if (event === \"open\") {\n this.acceptedOpenListeners.delete(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.removeEventListener(event, callback);\n return;\n }\n this.socket.off(event, callback);\n }\n\n emit(event: string, data: any) {\n this.socket.emit(event, data);\n }\n\n private attachEvent(event: string, callback: (data: any) => void) {\n if (event === \"open\") {\n this.acceptedOpenListeners.add(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.addEventListener(event, callback);\n return;\n }\n this.socket.on(event, callback);\n }\n\n private emitAcceptedOpen() {\n const event = new Event(\"open\");\n this.acceptedOpenListeners.forEach((callback) => callback(event));\n }\n\n updateProperties({ room, host, query }: SocketUpdateProperties) {\n if (!this.socket?.conn) return;\n this.targetRoom = room;\n this.socket.conn.updateProperties({\n room,\n id: this.privateId,\n host: host || this.options.host || window.location.host,\n query: {\n ...this.resolveQuery(),\n ...query,\n id: this.privateId,\n },\n })\n }\n\n private isNativeSocketEvent(event: string) {\n return isNativeSocketEvent(event);\n }\n\n async reconnect(_listeners?: (data: any) => void): Promise<void> {\n if (!this.socket?.conn) return;\n const conn = this.socket.conn;\n const connected = waitForRpgjsConnected(conn, 10000, { ignoreCleanClose: true });\n conn.reconnect();\n await connected;\n this.emitAcceptedOpen();\n }\n\n getCurrentRoom(): string {\n return this.targetRoom || this.socket?.conn?.room || \"lobby-1\";\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n constructor(protected context: Context, private _options: MmorpgOptions) {\n super(context);\n }\n\n async update(_map: any) {\n // In MMORPG mode, clients are untrusted and must not push map definitions.\n // Map bootstrap/update is handled server-side by @rpgjs/vite.\n return;\n }\n}\n\nexport function provideMmorpg(options: MmorpgOptions) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, options),\n },\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => new UpdateMapStandaloneService(context, options),\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standalone.js","names":[],"sources":["../../src/services/standalone.ts"],"sourcesContent":["import { AbstractWebsocket, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { ClientIo, ServerIo } from \"@signe/room\";\nimport { Context } from \"@signe/di\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { LoadMapToken } from \"./loadMap\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { normalizeStandaloneMessage } from \"./standalone-message\";\n\ntype ServerIo = any;\ntype ClientIo = any;\n\ninterface StandaloneOptions {\n env?: Record<string, any>;\n}\n\nclass BridgeWebsocket extends AbstractWebsocket {\n private room: ServerIo;\n private socket: ClientIo;\n private socketRoom?: ServerIo;\n private listeners: Array<{\n event: string;\n callback: (data: any) => void;\n handler: (event: any) => void;\n }> = [];\n private rooms = {\n partyFn: async (roomId: string) => {\n this.room = new ServerIo(roomId, this.rooms);\n const server = new this.server(this.room)\n await server.onStart();\n await server.subRoom.onStart()\n this.context.set('server', server)\n return server\n },\n env: {}\n }\n private serverInstance: any;\n\n constructor(protected context: Context, private server: any, options: StandaloneOptions = {}) {\n super(context);\n // fake room\n this.rooms.env = options.env || {};\n this.room = new ServerIo(\"lobby-1\", this.rooms);\n }\n\n async connection(listeners?: (data: any) => void) {\n this.serverInstance = new this.server(this.room);\n await this.serverInstance.onStart();\n await this.serverInstance.subRoom.onStart()\n this.context.set('server', this.serverInstance)\n return this._connection(listeners)\n }\n\n private async _connection(listeners?: (data: any) => void) {\n this.detachCurrentSocket();\n this.serverInstance = this.context.get('server')\n this.socket = new ClientIo(this.serverInstance, 'player-client-id');\n const url = new URL('http://localhost')\n const request = new Request(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json'\n }\n })\n listeners?.(this.socket)\n this.room.clients.set(this.socket.id, this.socket);\n this.socketRoom = this.room;\n this.listeners.forEach(({ handler }) => {\n this.socket.addEventListener(\"message\", handler);\n });\n await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);\n return this.socket\n }\n\n on(key: string, callback: (data: any) => void) {\n if (\n this.listeners.some(\n (listener) => listener.event === key && listener.callback === callback\n )\n ) {\n return;\n }\n const handler = (event) => {\n const object = normalizeStandaloneMessage(event);\n if (object.type === key) {\n callback(object.value);\n }\n };\n this.listeners.push({ event: key, callback, handler });\n this.socket?.addEventListener(\"message\", handler);\n }\n\n off(event: string, callback: (data: any) => void) {\n const remaining: typeof this.listeners = [];\n for (const listener of this.listeners) {\n if (listener.event === event && listener.callback === callback) {\n this.socket?.removeEventListener(\"message\", listener.handler);\n continue;\n }\n remaining.push(listener);\n }\n this.listeners = remaining;\n }\n\n emit(event: string, data: any) {\n this.socket.send({\n action: event,\n value: data,\n });\n }\n\n /**\n * Update underlying connection properties before a reconnect\n *\n * Design\n * - Dynamically register a factory for the requested room to ensure a fresh server instance\n * - Swap the internal ServerIo to target the new room\n *\n * @param params - Properties to update\n * @param params.room - The target room id (e.g. `map-simplemap2`)\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-simplemap2' })\n * await websocket.reconnect()\n * ```\n */\n updateProperties(_params: SocketUpdateProperties) {\n // empty\n }\n\n /**\n * Reconnect the client to the current Party room\n *\n * Design\n * - Must be called after `updateProperties()` when switching rooms\n * - Rebuilds the client <-> server bridge and re-triggers connection listeners\n *\n * @param listeners - Optional callback to re-bind event handlers on the new socket\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-dungeon' })\n * await websocket.reconnect((socket) => {\n * // re-bind events here\n * })\n * ```\n */\n async reconnect(listeners?: (data: any) => void): Promise<void> {\n await this._connection((socket) => {\n listeners?.(socket)\n })\n }\n\n private detachCurrentSocket() {\n if (!this.socket) return;\n this.listeners.forEach(({ handler }) => {\n this.socket.removeEventListener(\"message\", handler);\n });\n this.socketRoom?.clients?.delete?.(this.socket.id);\n this.socket = undefined as any;\n this.socketRoom = undefined;\n }\n\n getServer() {\n return this.serverInstance\n }\n\n getSocket() {\n return this.socket\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n private server: any;\n\n /**\n * Update the current room map data on the server side\n *\n * Design\n * - Uses the in-memory server instance stored in context (standalone mode)\n * - Builds a local HTTP-like request to the current Party room endpoint\n *\n * @param map - The map payload to apply on the server\n *\n * @example\n * ```ts\n * await updateMapService.update({ width: 1024, height: 768, events: [] })\n * ```\n */\n async update(map: any) {\n this.server = this.context.get('server')\n const roomId = this.server?.room?.id ?? 'lobby-1'\n const req = {\n url: `http://localhost/parties/main/${roomId}/map/update`,\n method: 'POST',\n headers: new Headers({}),\n json: async () => {\n return map;\n }\n };\n await this.server.onRequest(req)\n }\n}\n\nexport function provideRpg(server: any, options: StandaloneOptions = {}) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, server, options),\n },\n {\n provide: UpdateMapToken,\n useClass: UpdateMapStandaloneService,\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":";;;;;;;;;AAkBA,IAAM,kBAAN,cAA8B,kBAAkB;
|
|
1
|
+
{"version":3,"file":"standalone.js","names":[],"sources":["../../src/services/standalone.ts"],"sourcesContent":["import { AbstractWebsocket, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { ClientIo, ServerIo } from \"@signe/room\";\nimport { Context } from \"@signe/di\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { LoadMapToken } from \"./loadMap\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { normalizeStandaloneMessage } from \"./standalone-message\";\n\ntype ServerIo = any;\ntype ClientIo = any;\n\ninterface StandaloneOptions {\n env?: Record<string, any>;\n}\n\nclass BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"standalone\" as const;\n\n private room: ServerIo;\n private socket: ClientIo;\n private socketRoom?: ServerIo;\n private listeners: Array<{\n event: string;\n callback: (data: any) => void;\n handler: (event: any) => void;\n }> = [];\n private rooms = {\n partyFn: async (roomId: string) => {\n this.room = new ServerIo(roomId, this.rooms);\n const server = new this.server(this.room)\n await server.onStart();\n await server.subRoom.onStart()\n this.context.set('server', server)\n return server\n },\n env: {}\n }\n private serverInstance: any;\n\n constructor(protected context: Context, private server: any, options: StandaloneOptions = {}) {\n super(context);\n // fake room\n this.rooms.env = options.env || {};\n this.room = new ServerIo(\"lobby-1\", this.rooms);\n }\n\n async connection(listeners?: (data: any) => void) {\n this.serverInstance = new this.server(this.room);\n await this.serverInstance.onStart();\n await this.serverInstance.subRoom.onStart()\n this.context.set('server', this.serverInstance)\n return this._connection(listeners)\n }\n\n private async _connection(listeners?: (data: any) => void) {\n this.detachCurrentSocket();\n this.serverInstance = this.context.get('server')\n this.socket = new ClientIo(this.serverInstance, 'player-client-id');\n const url = new URL('http://localhost')\n const request = new Request(url.toString(), {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json'\n }\n })\n listeners?.(this.socket)\n this.room.clients.set(this.socket.id, this.socket);\n this.socketRoom = this.room;\n this.listeners.forEach(({ handler }) => {\n this.socket.addEventListener(\"message\", handler);\n });\n await this.serverInstance.onConnect(this.socket.conn as any, { request } as any);\n return this.socket\n }\n\n on(key: string, callback: (data: any) => void) {\n if (\n this.listeners.some(\n (listener) => listener.event === key && listener.callback === callback\n )\n ) {\n return;\n }\n const handler = (event) => {\n const object = normalizeStandaloneMessage(event);\n if (object.type === key) {\n callback(object.value);\n }\n };\n this.listeners.push({ event: key, callback, handler });\n this.socket?.addEventListener(\"message\", handler);\n }\n\n off(event: string, callback: (data: any) => void) {\n const remaining: typeof this.listeners = [];\n for (const listener of this.listeners) {\n if (listener.event === event && listener.callback === callback) {\n this.socket?.removeEventListener(\"message\", listener.handler);\n continue;\n }\n remaining.push(listener);\n }\n this.listeners = remaining;\n }\n\n emit(event: string, data: any) {\n this.socket.send({\n action: event,\n value: data,\n });\n }\n\n /**\n * Update underlying connection properties before a reconnect\n *\n * Design\n * - Dynamically register a factory for the requested room to ensure a fresh server instance\n * - Swap the internal ServerIo to target the new room\n *\n * @param params - Properties to update\n * @param params.room - The target room id (e.g. `map-simplemap2`)\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-simplemap2' })\n * await websocket.reconnect()\n * ```\n */\n updateProperties(_params: SocketUpdateProperties) {\n // empty\n }\n\n /**\n * Reconnect the client to the current Party room\n *\n * Design\n * - Must be called after `updateProperties()` when switching rooms\n * - Rebuilds the client <-> server bridge and re-triggers connection listeners\n *\n * @param listeners - Optional callback to re-bind event handlers on the new socket\n *\n * @example\n * ```ts\n * websocket.updateProperties({ room: 'map-dungeon' })\n * await websocket.reconnect((socket) => {\n * // re-bind events here\n * })\n * ```\n */\n async reconnect(listeners?: (data: any) => void): Promise<void> {\n await this._connection((socket) => {\n listeners?.(socket)\n })\n }\n\n private detachCurrentSocket() {\n if (!this.socket) return;\n this.listeners.forEach(({ handler }) => {\n this.socket.removeEventListener(\"message\", handler);\n });\n this.socketRoom?.clients?.delete?.(this.socket.id);\n this.socket = undefined as any;\n this.socketRoom = undefined;\n }\n\n getServer() {\n return this.serverInstance\n }\n\n getSocket() {\n return this.socket\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n private server: any;\n\n /**\n * Update the current room map data on the server side\n *\n * Design\n * - Uses the in-memory server instance stored in context (standalone mode)\n * - Builds a local HTTP-like request to the current Party room endpoint\n *\n * @param map - The map payload to apply on the server\n *\n * @example\n * ```ts\n * await updateMapService.update({ width: 1024, height: 768, events: [] })\n * ```\n */\n async update(map: any) {\n this.server = this.context.get('server')\n const roomId = this.server?.room?.id ?? 'lobby-1'\n const req = {\n url: `http://localhost/parties/main/${roomId}/map/update`,\n method: 'POST',\n headers: new Headers({}),\n json: async () => {\n return map;\n }\n };\n await this.server.onRequest(req)\n }\n}\n\nexport function provideRpg(server: any, options: StandaloneOptions = {}) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, server, options),\n },\n {\n provide: UpdateMapToken,\n useClass: UpdateMapStandaloneService,\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rpgjs/client",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.16",
|
|
4
4
|
"description": "RPGJS is a framework for creating RPG/MMORPG games",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
"pixi.js": "^8.9.2"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@rpgjs/common": "5.0.0-beta.
|
|
26
|
-
"@rpgjs/server": "5.0.0-beta.
|
|
27
|
-
"@rpgjs/ui-css": "5.0.0-beta.
|
|
25
|
+
"@rpgjs/common": "5.0.0-beta.15",
|
|
26
|
+
"@rpgjs/server": "5.0.0-beta.16",
|
|
27
|
+
"@rpgjs/ui-css": "5.0.0-beta.13",
|
|
28
28
|
"@signe/di": "3.0.1",
|
|
29
29
|
"@signe/room": "3.0.1",
|
|
30
30
|
"@signe/sync": "3.0.1",
|
package/src/RpgClientEngine.ts
CHANGED
|
@@ -52,6 +52,11 @@ interface MovementTrajectoryPoint {
|
|
|
52
52
|
direction?: Direction;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
interface CanvasResizeSize {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
55
60
|
const DEFAULT_DASH_ADDITIONAL_SPEED = 8;
|
|
56
61
|
const DEFAULT_DASH_DURATION_MS = 180;
|
|
57
62
|
const DEFAULT_DASH_COOLDOWN_MS = 450;
|
|
@@ -187,6 +192,9 @@ export class RpgClientEngine<T = any> {
|
|
|
187
192
|
private predictionEnabled = false;
|
|
188
193
|
private prediction?: PredictionController<RpgMovementInput, Direction>;
|
|
189
194
|
private readonly SERVER_CORRECTION_THRESHOLD = 30;
|
|
195
|
+
private localMovementAuthority = false;
|
|
196
|
+
private lastLocalMovementInputAt = 0;
|
|
197
|
+
private readonly LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS = 250;
|
|
190
198
|
private inputFrameCounter = 0;
|
|
191
199
|
private pendingPredictionFrames: number[] = [];
|
|
192
200
|
private lastClientPhysicsStepAt = 0;
|
|
@@ -267,9 +275,36 @@ export class RpgClientEngine<T = any> {
|
|
|
267
275
|
this.registerSpriteComponent("rpg:image", ImageComponent);
|
|
268
276
|
|
|
269
277
|
this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;
|
|
278
|
+
this.localMovementAuthority = this.resolveLocalMovementAuthority();
|
|
270
279
|
this.initializePredictionController();
|
|
271
280
|
}
|
|
272
281
|
|
|
282
|
+
private resolveLocalMovementAuthority(): boolean {
|
|
283
|
+
const predictionConfig = (this.globalConfig as any)?.prediction;
|
|
284
|
+
const configured =
|
|
285
|
+
(this.globalConfig as any)?.movementAuthority ??
|
|
286
|
+
predictionConfig?.movementAuthority ??
|
|
287
|
+
predictionConfig?.authority ??
|
|
288
|
+
predictionConfig?.mode;
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
configured === "server" ||
|
|
292
|
+
configured === "network" ||
|
|
293
|
+
configured === false
|
|
294
|
+
) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
configured === "client" ||
|
|
299
|
+
configured === "local" ||
|
|
300
|
+
configured === true
|
|
301
|
+
) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return this.webSocket.mode === "standalone";
|
|
306
|
+
}
|
|
307
|
+
|
|
273
308
|
setLocale(locale: string) {
|
|
274
309
|
this.locale = locale;
|
|
275
310
|
}
|
|
@@ -359,6 +394,7 @@ export class RpgClientEngine<T = any> {
|
|
|
359
394
|
Canvas,
|
|
360
395
|
bootstrapOptions
|
|
361
396
|
);
|
|
397
|
+
this.installCanvasResizeGuard(app);
|
|
362
398
|
this.canvasApp = app;
|
|
363
399
|
this.canvasElement = canvasElement;
|
|
364
400
|
this.renderer = app.renderer as unknown as PIXI.Renderer;
|
|
@@ -420,6 +456,56 @@ export class RpgClientEngine<T = any> {
|
|
|
420
456
|
this.startPingPong();
|
|
421
457
|
}
|
|
422
458
|
|
|
459
|
+
private installCanvasResizeGuard(app: any) {
|
|
460
|
+
if (!app || typeof app.resize !== "function") return;
|
|
461
|
+
|
|
462
|
+
const originalResize = app.resize.bind(app);
|
|
463
|
+
app.resize = () => {
|
|
464
|
+
const targetSize = this.readCanvasResizeTargetSize(app);
|
|
465
|
+
const rendererSize = this.readCanvasRendererSize(app);
|
|
466
|
+
|
|
467
|
+
if (
|
|
468
|
+
targetSize &&
|
|
469
|
+
rendererSize &&
|
|
470
|
+
targetSize.width === rendererSize.width &&
|
|
471
|
+
targetSize.height === rendererSize.height
|
|
472
|
+
) {
|
|
473
|
+
this.cancelCanvasResizeFrame(app);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
originalResize();
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private readCanvasResizeTargetSize(app: any): CanvasResizeSize | null {
|
|
482
|
+
const resizeTarget = app?.resizeTo;
|
|
483
|
+
if (!resizeTarget || typeof window === "undefined") return null;
|
|
484
|
+
|
|
485
|
+
const rawWidth = resizeTarget === window ? window.innerWidth : resizeTarget.clientWidth;
|
|
486
|
+
const rawHeight = resizeTarget === window ? window.innerHeight : resizeTarget.clientHeight;
|
|
487
|
+
const width = Math.round(Number(rawWidth));
|
|
488
|
+
const height = Math.round(Number(rawHeight));
|
|
489
|
+
|
|
490
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) return null;
|
|
491
|
+
return { width, height };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private readCanvasRendererSize(app: any): CanvasResizeSize | null {
|
|
495
|
+
const screen = app?.renderer?.screen;
|
|
496
|
+
const width = Math.round(Number(screen?.width));
|
|
497
|
+
const height = Math.round(Number(screen?.height));
|
|
498
|
+
|
|
499
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
500
|
+
return { width, height };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private cancelCanvasResizeFrame(app: any) {
|
|
504
|
+
if (typeof app?._cancelResize === "function") {
|
|
505
|
+
app._cancelResize();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
423
509
|
private resolveSceneMapComponent() {
|
|
424
510
|
const components = this.hooks.getHookFunctions("client-sceneMap-component");
|
|
425
511
|
const component = components[components.length - 1];
|
|
@@ -527,8 +613,8 @@ export class RpgClientEngine<T = any> {
|
|
|
527
613
|
|
|
528
614
|
const myId = this.playerIdSignal();
|
|
529
615
|
const players = payload.players;
|
|
530
|
-
const
|
|
531
|
-
|
|
616
|
+
const localPatch = myId && players ? players[myId] : undefined;
|
|
617
|
+
const shouldMaskLocalPosition = this.shouldPreserveLocalPlayerPosition(localPatch);
|
|
532
618
|
if (shouldMaskLocalPosition && myId && players && players[myId]) {
|
|
533
619
|
const localPatch = { ...players[myId] };
|
|
534
620
|
delete localPatch.x;
|
|
@@ -544,6 +630,31 @@ export class RpgClientEngine<T = any> {
|
|
|
544
630
|
return payload;
|
|
545
631
|
}
|
|
546
632
|
|
|
633
|
+
private shouldPreserveLocalPlayerPosition(localPatch?: any): boolean {
|
|
634
|
+
if (!localPatch) {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
if (this.predictionEnabled && !!this.prediction?.hasPendingInputs()) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
return this.shouldKeepLocalPlayerMovement();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private shouldKeepLocalPlayerMovement(): boolean {
|
|
644
|
+
if (!this.localMovementAuthority || this.mapTransitionInProgress) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
const myId = this.playerIdSignal();
|
|
648
|
+
const player = myId ? this.sceneMap?.players?.()?.[myId] : undefined;
|
|
649
|
+
if (!player) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
if (this.prediction?.hasPendingInputs()) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
return Date.now() - this.lastLocalMovementInputAt <= this.LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS;
|
|
656
|
+
}
|
|
657
|
+
|
|
547
658
|
private normalizeAckWithSyncState(
|
|
548
659
|
ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },
|
|
549
660
|
syncData: any,
|
|
@@ -1733,6 +1844,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1733
1844
|
if (timestamp < this.dashLockedUntil) return;
|
|
1734
1845
|
this.dashLockedUntil = timestamp + cooldown;
|
|
1735
1846
|
}
|
|
1847
|
+
this.lastLocalMovementInputAt = timestamp;
|
|
1736
1848
|
|
|
1737
1849
|
let frame: number;
|
|
1738
1850
|
let tick: number;
|
|
@@ -2248,12 +2360,13 @@ export class RpgClientEngine<T = any> {
|
|
|
2248
2360
|
|
|
2249
2361
|
private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
|
|
2250
2362
|
this.updateServerTickEstimate(ack.serverTick);
|
|
2363
|
+
const keepLocalMovement = this.shouldKeepLocalPlayerMovement();
|
|
2251
2364
|
if (this.predictionEnabled && this.prediction) {
|
|
2252
2365
|
const result = this.prediction.applyServerAck({
|
|
2253
2366
|
frame: ack.frame,
|
|
2254
2367
|
serverTick: ack.serverTick,
|
|
2255
2368
|
state:
|
|
2256
|
-
typeof ack.x === "number" && typeof ack.y === "number"
|
|
2369
|
+
!keepLocalMovement && typeof ack.x === "number" && typeof ack.y === "number"
|
|
2257
2370
|
? { x: ack.x, y: ack.y, direction: ack.direction }
|
|
2258
2371
|
: undefined,
|
|
2259
2372
|
});
|
|
@@ -2266,6 +2379,9 @@ export class RpgClientEngine<T = any> {
|
|
|
2266
2379
|
if (typeof ack.x !== "number" || typeof ack.y !== "number") {
|
|
2267
2380
|
return;
|
|
2268
2381
|
}
|
|
2382
|
+
if (keepLocalMovement) {
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2269
2385
|
const player = this.getCurrentPlayer() as any;
|
|
2270
2386
|
const myId = this.playerIdSignal();
|
|
2271
2387
|
if (!player || !myId) {
|
|
@@ -9,8 +9,11 @@ export type SocketUpdateProperties = {
|
|
|
9
9
|
host?: string;
|
|
10
10
|
query?: SocketQuery;
|
|
11
11
|
};
|
|
12
|
+
export type WebSocketMode = "standalone" | "mmorpg";
|
|
12
13
|
|
|
13
14
|
export abstract class AbstractWebsocket {
|
|
15
|
+
readonly mode?: WebSocketMode;
|
|
16
|
+
|
|
14
17
|
constructor(protected context: Context) {}
|
|
15
18
|
|
|
16
19
|
abstract connection(listeners?: (data: any) => void): Promise<void>;
|
package/src/services/mmorpg.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface MmorpgOptions {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export class BridgeWebsocket extends AbstractWebsocket {
|
|
20
|
+
readonly mode = "mmorpg" as const;
|
|
21
|
+
|
|
20
22
|
private socket: any;
|
|
21
23
|
private privateId: string;
|
|
22
24
|
private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
1
3
|
import { describe, expect, test, vi } from "vitest";
|
|
4
|
+
import { Context } from "@signe/di";
|
|
5
|
+
import { WebSocketToken } from "./AbstractSocket";
|
|
6
|
+
import { provideMmorpg } from "./mmorpg";
|
|
7
|
+
import { provideRpg } from "./standalone";
|
|
2
8
|
import { normalizeStandaloneMessage } from "./standalone-message";
|
|
3
9
|
|
|
4
10
|
describe("standalone websocket bridge", () => {
|
|
@@ -31,4 +37,18 @@ describe("standalone websocket bridge", () => {
|
|
|
31
37
|
value: { projectiles: [] },
|
|
32
38
|
});
|
|
33
39
|
});
|
|
40
|
+
|
|
41
|
+
test("marks standalone and MMORPG websocket providers with their runtime mode", () => {
|
|
42
|
+
class Server {}
|
|
43
|
+
const context = new Context();
|
|
44
|
+
const standaloneProvider = provideRpg(Server).find(
|
|
45
|
+
(provider: any) => provider.provide === WebSocketToken,
|
|
46
|
+
) as any;
|
|
47
|
+
const mmorpgProvider = provideMmorpg({ connectionId: "test-client" }).find(
|
|
48
|
+
(provider: any) => provider.provide === WebSocketToken,
|
|
49
|
+
) as any;
|
|
50
|
+
|
|
51
|
+
expect(standaloneProvider.useFactory(context).mode).toBe("standalone");
|
|
52
|
+
expect(mmorpgProvider.useFactory(context).mode).toBe("mmorpg");
|
|
53
|
+
});
|
|
34
54
|
});
|