@rpgjs/client 5.0.0-alpha.33 → 5.0.0-alpha.36

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.
@@ -1,4 +1,4 @@
1
- import { RpgCommonMap } from '@rpgjs/common';
1
+ import { RpgCommonMap, WeatherState } from '@rpgjs/common';
2
2
  import { RpgClientPlayer } from './Player';
3
3
  import { RpgClientEvent } from './Event';
4
4
  import { RpgClientEngine } from '../RpgClientEngine';
@@ -7,8 +7,18 @@ export declare class RpgClientMap extends RpgCommonMap<any> {
7
7
  players: import('canvasengine').WritableObjectSignal<Record<string, RpgClientPlayer>>;
8
8
  events: import('canvasengine').WritableObjectSignal<Record<string, RpgClientEvent>>;
9
9
  currentPlayer: import('canvasengine').ComputedSignal<RpgClientPlayer>;
10
+ weatherState: import('canvasengine').WritableSignal<WeatherState | null>;
11
+ localWeatherOverride: import('canvasengine').WritableSignal<WeatherState | null>;
12
+ weather: import('canvasengine').ComputedSignal<WeatherState | null>;
13
+ private manualClientPhysicsTick;
14
+ private readonly isTestEnvironment;
10
15
  constructor();
16
+ configureClientPrediction(enabled: boolean): void;
11
17
  getCurrentPlayer(): RpgClientPlayer;
12
18
  reset(force?: boolean): void;
19
+ getWeather(): WeatherState | null;
20
+ setLocalWeather(next: WeatherState | null): void;
21
+ clearLocalWeather(): void;
22
+ stepClientPhysics(deltaMs: number): number;
13
23
  stepPredictionTick(): void;
14
24
  }
package/dist/Game/Map.js CHANGED
@@ -22,11 +22,24 @@ class RpgClientMap extends RpgCommonMap {
22
22
  this.players = signal({});
23
23
  this.events = signal({});
24
24
  this.currentPlayer = computed(() => this.players()[this.engine.playerIdSignal()]);
25
+ this.weatherState = signal(null);
26
+ this.localWeatherOverride = signal(null);
27
+ this.weather = computed(() => {
28
+ const local = this.localWeatherOverride();
29
+ const state = this.weatherState();
30
+ return local ?? state;
31
+ });
32
+ this.manualClientPhysicsTick = false;
25
33
  const isTest = typeof process !== "undefined" && process.env?.TEST === "true" || typeof window !== "undefined" && window.__RPGJS_TEST__ === true;
34
+ this.isTestEnvironment = isTest;
26
35
  if (isTest) {
27
36
  this.autoTickEnabled = false;
28
37
  }
29
38
  }
39
+ configureClientPrediction(enabled) {
40
+ this.manualClientPhysicsTick = enabled;
41
+ this.autoTickEnabled = enabled ? false : !this.isTestEnvironment;
42
+ }
30
43
  getCurrentPlayer() {
31
44
  return this.currentPlayer();
32
45
  }
@@ -37,8 +50,25 @@ class RpgClientMap extends RpgCommonMap {
37
50
  currentPlayerId && currentPlayer ? { [currentPlayerId]: currentPlayer } : {}
38
51
  );
39
52
  this.events.set({});
53
+ this.weatherState.set(null);
54
+ this.localWeatherOverride.set(null);
40
55
  this.clearPhysic();
41
56
  }
57
+ getWeather() {
58
+ return this.weather();
59
+ }
60
+ setLocalWeather(next) {
61
+ this.localWeatherOverride.set(next);
62
+ }
63
+ clearLocalWeather() {
64
+ this.localWeatherOverride.set(null);
65
+ }
66
+ stepClientPhysics(deltaMs) {
67
+ if (!this.manualClientPhysicsTick) {
68
+ return 0;
69
+ }
70
+ return this.nextTick(deltaMs);
71
+ }
42
72
  stepPredictionTick() {
43
73
  this.forceSingleTick();
44
74
  }
@@ -1 +1 @@
1
- {"version":3,"file":"Map.js","sources":["../../src/Game/Map.ts"],"sourcesContent":["import { RpgCommonMap } from \"@rpgjs/common\";\nimport { sync, users } from \"@signe/sync\";\nimport { RpgClientPlayer } from \"./Player\";\nimport { Signal, signal, computed, effect } from \"canvasengine\";\nimport { RpgClientEvent } from \"./Event\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { inject } from \"../core/inject\";\n\nexport class RpgClientMap extends RpgCommonMap<any> {\n engine: RpgClientEngine = inject(RpgClientEngine)\n @users(RpgClientPlayer) players = signal<Record<string, RpgClientPlayer>>({});\n @sync(RpgClientEvent) events = signal<Record<string, RpgClientEvent>>({});\n currentPlayer = computed(() => this.players()[this.engine.playerIdSignal()!])\n\n constructor() {\n super();\n // Détecter l'environnement de test\n const isTest = (typeof process !== 'undefined' && process.env?.TEST === 'true') \n || (typeof window !== 'undefined' && (window as any).__RPGJS_TEST__ === true);\n if (isTest) {\n this.autoTickEnabled = false;\n }\n }\n\n getCurrentPlayer() {\n return this.currentPlayer()\n }\n\n reset(force = false) {\n const currentPlayerId = this.engine.playerIdSignal();\n const currentPlayer = !force && currentPlayerId\n ? this.players()[currentPlayerId]\n : undefined;\n\n this.players.set(\n currentPlayerId && currentPlayer ? { [currentPlayerId]: currentPlayer } : {}\n );\n this.events.set({})\n this.clearPhysic()\n }\n\n stepPredictionTick(): void {\n this.forceSingleTick();\n }\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAQO,MAAM,qBAAqB,YAAA,CAAkB;AAAA,EAMlD,WAAA,GAAc;AACZ,IAAA,KAAA,EAAM;AANR,IAAA,IAAA,CAAA,MAAA,GAA0B,OAAO,eAAe,CAAA;AACxB,IAAA,IAAA,CAAA,OAAA,GAAU,MAAA,CAAwC,EAAE,CAAA;AACtD,IAAA,IAAA,CAAA,MAAA,GAAS,MAAA,CAAuC,EAAE,CAAA;AACxE,IAAA,IAAA,CAAA,aAAA,GAAgB,QAAA,CAAS,MAAM,IAAA,CAAK,OAAA,GAAU,IAAA,CAAK,MAAA,CAAO,cAAA,EAAiB,CAAC,CAAA;AAK1E,IAAA,MAAM,MAAA,GAAU,OAAO,OAAA,KAAY,WAAA,IAAe,OAAA,CAAQ,GAAA,EAAK,IAAA,KAAS,MAAA,IAClE,OAAO,MAAA,KAAW,WAAA,IAAgB,MAAA,CAAe,cAAA,KAAmB,IAAA;AAC1E,IAAA,IAAI,MAAA,EAAQ;AACV,MAAA,IAAA,CAAK,eAAA,GAAkB,KAAA;AAAA,IACzB;AAAA,EACF;AAAA,EAEA,gBAAA,GAAmB;AACjB,IAAA,OAAO,KAAK,aAAA,EAAc;AAAA,EAC5B;AAAA,EAEA,KAAA,CAAM,QAAQ,KAAA,EAAO;AACnB,IAAA,MAAM,eAAA,GAAkB,IAAA,CAAK,MAAA,CAAO,cAAA,EAAe;AACnD,IAAA,MAAM,aAAA,GAAgB,CAAC,KAAA,IAAS,eAAA,GAC5B,KAAK,OAAA,EAAQ,CAAE,eAAe,CAAA,GAC9B,MAAA;AAEJ,IAAA,IAAA,CAAK,OAAA,CAAQ,GAAA;AAAA,MACX,eAAA,IAAmB,gBAAgB,EAAE,CAAC,eAAe,GAAG,aAAA,KAAkB;AAAC,KAC7E;AACA,IAAA,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,EAAE,CAAA;AAClB,IAAA,IAAA,CAAK,WAAA,EAAY;AAAA,EACnB;AAAA,EAEA,kBAAA,GAA2B;AACzB,IAAA,IAAA,CAAK,eAAA,EAAgB;AAAA,EACvB;AACF;AAlC0B,eAAA,CAAA;AAAA,EAAvB,MAAM,eAAe;AAAA,CAAA,EAFX,YAAA,CAEa,SAAA,EAAA,SAAA,CAAA;AACF,eAAA,CAAA;AAAA,EAArB,KAAK,cAAc;AAAA,CAAA,EAHT,YAAA,CAGW,SAAA,EAAA,QAAA,CAAA;;;;"}
1
+ {"version":3,"file":"Map.js","sources":["../../src/Game/Map.ts"],"sourcesContent":["import { RpgCommonMap, type WeatherState } from \"@rpgjs/common\";\nimport { sync, users } from \"@signe/sync\";\nimport { RpgClientPlayer } from \"./Player\";\nimport { Signal, signal, computed, effect } from \"canvasengine\";\nimport { RpgClientEvent } from \"./Event\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { inject } from \"../core/inject\";\n\nexport class RpgClientMap extends RpgCommonMap<any> {\n engine: RpgClientEngine = inject(RpgClientEngine)\n @users(RpgClientPlayer) players = signal<Record<string, RpgClientPlayer>>({});\n @sync(RpgClientEvent) events = signal<Record<string, RpgClientEvent>>({});\n currentPlayer = computed(() => this.players()[this.engine.playerIdSignal()!])\n weatherState = signal<WeatherState | null>(null);\n localWeatherOverride = signal<WeatherState | null>(null);\n weather = computed<WeatherState | null>(() => {\n const local = this.localWeatherOverride() \n const state = this.weatherState()\n return local ?? state\n });\n private manualClientPhysicsTick = false;\n private readonly isTestEnvironment: boolean;\n\n constructor() {\n super();\n // Détecter l'environnement de test\n const isTest = (typeof process !== 'undefined' && process.env?.TEST === 'true')\n || (typeof window !== 'undefined' && (window as any).__RPGJS_TEST__ === true);\n this.isTestEnvironment = isTest;\n if (isTest) {\n this.autoTickEnabled = false;\n }\n }\n\n configureClientPrediction(enabled: boolean): void {\n this.manualClientPhysicsTick = enabled;\n this.autoTickEnabled = enabled ? false : !this.isTestEnvironment;\n }\n\n getCurrentPlayer() {\n return this.currentPlayer()\n }\n\n reset(force = false) {\n const currentPlayerId = this.engine.playerIdSignal();\n const currentPlayer = !force && currentPlayerId\n ? this.players()[currentPlayerId]\n : undefined;\n\n this.players.set(\n currentPlayerId && currentPlayer ? { [currentPlayerId]: currentPlayer } : {}\n );\n this.events.set({})\n this.weatherState.set(null);\n this.localWeatherOverride.set(null);\n this.clearPhysic()\n }\n\n getWeather(): WeatherState | null {\n return this.weather();\n }\n\n setLocalWeather(next: WeatherState | null): void {\n this.localWeatherOverride.set(next);\n }\n\n clearLocalWeather(): void {\n this.localWeatherOverride.set(null);\n }\n\n stepClientPhysics(deltaMs: number): number {\n if (!this.manualClientPhysicsTick) {\n return 0;\n }\n return this.nextTick(deltaMs);\n }\n\n stepPredictionTick(): void {\n this.forceSingleTick();\n }\n}\n"],"names":[],"mappings":"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"}
@@ -51,7 +51,7 @@ class RpgClientObject extends RpgCommonPlayer {
51
51
  this.engine.tick.pipe().subscribe(() => {
52
52
  const frame = this.frames.shift();
53
53
  if (frame) {
54
- if (!frame.x || !frame.y) return;
54
+ if (typeof frame.x !== "number" || typeof frame.y !== "number") return;
55
55
  this.engine.scene.setBodyPosition(
56
56
  this.id,
57
57
  frame.x,
@@ -1 +1 @@
1
- {"version":3,"file":"Object.js","sources":["../../src/Game/Object.ts"],"sourcesContent":["import { Hooks, ModulesToken, RpgCommonPlayer } from \"@rpgjs/common\";\nimport { trigger, signal, effect } from \"canvasengine\";\nimport { filter, from, map, Subscription, switchMap } from \"rxjs\";\nimport { inject } from \"../core/inject\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport TextComponent from \"../components/dynamics/text.ce\";\n\nconst DYNAMIC_COMPONENTS = {\n text: TextComponent,\n}\n\nexport abstract class RpgClientObject extends RpgCommonPlayer {\n abstract type: string;\n emitParticleTrigger = trigger();\n particleName = signal(\"\");\n animationCurrentIndex = signal(0);\n animationIsPlaying = signal(false);\n _param = signal({});\n frames: { x: number; y: number; ts: number }[] = [];\n graphicsSignals = signal<any[]>([]);\n _component = {} // temporary component memory\n flashTrigger = trigger();\n\n constructor() {\n super();\n this.hooks.callHooks(\"client-sprite-onInit\", this).subscribe();\n\n this._frames.observable.subscribe(({ items }) => {\n if (!this.id) return;\n //if (this.id == this.engine.playerIdSignal()!) return;\n this.frames = [...this.frames, ...items];\n });\n\n this.graphics.observable\n .pipe(\n map(({ items }) => items),\n filter(graphics => graphics.length > 0),\n switchMap(graphics => from(Promise.all(graphics.map(graphic => this.engine.getSpriteSheet(graphic)))))\n )\n .subscribe((sheets) => { \n this.graphicsSignals.set(sheets);\n });\n\n this.componentsTop.observable\n .pipe(\n filter(value => value !== null && value !== undefined),\n map((value) => typeof value === 'string' ? JSON.parse(value) : value),\n )\n .subscribe(({components}) => {\n for (const component of components) {\n for (const [key, value] of Object.entries(component)) {\n this._component = value as any; // temporary component memory\n console.log(value)\n const type = (value as any).type as keyof typeof DYNAMIC_COMPONENTS;\n if (DYNAMIC_COMPONENTS[type]) {\n this.engine.addSpriteComponentInFront(DYNAMIC_COMPONENTS[type]);\n }\n }\n }\n });\n\n this.engine.tick\n .pipe\n //throttleTime(10)\n ()\n .subscribe(() => {\n const frame = this.frames.shift();\n if (frame) {\n if (!frame.x || !frame.y) return;\n this.engine.scene.setBodyPosition(\n this.id,\n frame.x,\n frame.y,\n \"top-left\"\n );\n }\n });\n }\n\n get hooks() {\n return inject<Hooks>(ModulesToken);\n }\n\n get engine() {\n return inject(RpgClientEngine);\n }\n\n private animationSubscription?: Subscription;\n\n /**\n * Trigger a flash animation on this sprite\n * \n * This method triggers a flash effect using CanvasEngine's flash directive.\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 uses a trigger system that is connected to the flash directive in the\n * character component. This allows for flexible configuration and can be triggered\n * from both server events and client-side code.\n * \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 * // Simple flash with default settings (alpha flash)\n * player.flash();\n * \n * // Flash with red tint\n * player.flash({ type: 'tint', tint: 0xff0000 });\n * \n * // Flash with both alpha and tint\n * player.flash({ \n * type: 'both', \n * alpha: 0.5, \n * tint: 0xff0000,\n * duration: 200,\n * cycles: 2\n * });\n * \n * // Quick damage flash\n * player.flash({ \n * type: 'tint', \n * tint: 0xff0000, \n * duration: 150,\n * cycles: 1\n * });\n * ```\n */\n flash(options?: {\n type?: 'alpha' | 'tint' | 'both';\n duration?: number;\n cycles?: number;\n alpha?: number;\n tint?: number | string;\n }): void {\n const flashOptions = {\n type: options?.type || 'alpha',\n duration: options?.duration ?? 300,\n cycles: options?.cycles ?? 1,\n alpha: options?.alpha ?? 0.3,\n tint: options?.tint ?? 0xffffff,\n };\n \n // Convert color name to hex if needed\n let tintValue = flashOptions.tint;\n if (typeof tintValue === 'string') {\n // Common color name to hex mapping\n const colorMap: Record<string, number> = {\n 'white': 0xffffff,\n 'red': 0xff0000,\n 'green': 0x00ff00,\n 'blue': 0x0000ff,\n 'yellow': 0xffff00,\n 'cyan': 0x00ffff,\n 'magenta': 0xff00ff,\n 'black': 0x000000,\n };\n tintValue = colorMap[tintValue.toLowerCase()] ?? 0xffffff;\n }\n \n this.flashTrigger.start({\n ...flashOptions,\n tint: tintValue,\n });\n }\n\n /**\n * Reset animation state when animation changes externally\n *\n * This method should be called when the animation changes due to movement\n * or other external factors to ensure the animation system doesn't get stuck\n *\n * @example\n * ```ts\n * // Reset when player starts moving\n * player.resetAnimationState();\n * ```\n */\n resetAnimationState() {\n this.animationIsPlaying.set(false);\n this.animationCurrentIndex.set(0);\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n this.animationSubscription = undefined;\n }\n }\n\n /**\n * Set a custom animation for a specific number of times\n *\n * Plays a custom animation for the specified number of repetitions.\n * The animation system prevents overlapping animations and automatically\n * returns to the previous animation when complete.\n *\n * @param animationName - Name of the animation to play\n * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)\n *\n * @example\n * ```ts\n * // Play attack animation 3 times\n * player.setAnimation('attack', 3);\n *\n * // Play continuous spell animation\n * player.setAnimation('spell');\n * ```\n */\n setAnimation(animationName: string, nbTimes?: number): void;\n /**\n * Set a custom animation with temporary graphic change\n *\n * Plays a custom animation for the specified number of repetitions and temporarily\n * changes the player's graphic (sprite sheet) during the animation. The graphic\n * is automatically reset when the animation finishes.\n *\n * @param animationName - Name of the animation to play\n * @param graphic - The graphic(s) to temporarily use during the animation\n * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)\n *\n * @example\n * ```ts\n * // Play attack animation with temporary graphic change\n * player.setAnimation('attack', 'hero_attack', 3);\n * ```\n */\n setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number): void;\n setAnimation(animationName: string, graphicOrNbTimes?: string | string[] | number, nbTimes?: number): void {\n if (this.animationIsPlaying()) return;\n this.animationIsPlaying.set(true);\n const previousAnimationName = this.animationName();\n const previousGraphics = this.graphics();\n this.animationCurrentIndex.set(0);\n\n let graphic: string | string[] | undefined;\n let finalNbTimes: number = Infinity;\n\n // Handle overloads\n if (typeof graphicOrNbTimes === 'number') {\n // setAnimation(animationName, nbTimes)\n finalNbTimes = graphicOrNbTimes;\n } else if (graphicOrNbTimes !== undefined) {\n // setAnimation(animationName, graphic, nbTimes)\n graphic = graphicOrNbTimes;\n finalNbTimes = nbTimes ?? Infinity;\n } else {\n // setAnimation(animationName) - nbTimes remains Infinity\n finalNbTimes = Infinity;\n }\n\n // Temporarily change graphic if provided\n if (graphic !== undefined) {\n if (Array.isArray(graphic)) {\n this.graphics.set(graphic);\n } else {\n this.graphics.set([graphic]);\n }\n }\n\n // Clean up any existing subscription\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n }\n\n this.animationSubscription =\n this.animationCurrentIndex.observable.subscribe((index) => {\n if (index >= finalNbTimes) {\n this.animationCurrentIndex.set(0);\n this.animationName.set(previousAnimationName);\n // Reset graphic to previous value if it was changed\n if (graphic !== undefined) {\n this.graphics.set(previousGraphics);\n }\n this.animationIsPlaying.set(false);\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n this.animationSubscription = undefined;\n }\n }\n });\n this.animationName.set(animationName);\n }\n\n showComponentAnimation(id: string, params: any) {\n const engine = inject(RpgClientEngine);\n engine.getComponentAnimation(id).displayEffect(params, this);\n }\n \n isEvent(): boolean {\n return this.type === 'event';\n }\n\n isPlayer(): boolean {\n return this.type === 'player';\n }\n}\n"],"names":["TextComponent"],"mappings":";;;;;;;AAOA,MAAM,kBAAA,GAAqB;AAAA,EACzB,IAAA,EAAMA;AACR,CAAA;AAEO,MAAe,wBAAwB,eAAA,CAAgB;AAAA,EAY5D,WAAA,GAAc;AACZ,IAAA,KAAA,EAAM;AAXR,IAAA,IAAA,CAAA,mBAAA,GAAsB,OAAA,EAAQ;AAC9B,IAAA,IAAA,CAAA,YAAA,GAAe,OAAO,EAAE,CAAA;AACxB,IAAA,IAAA,CAAA,qBAAA,GAAwB,OAAO,CAAC,CAAA;AAChC,IAAA,IAAA,CAAA,kBAAA,GAAqB,OAAO,KAAK,CAAA;AACjC,IAAA,IAAA,CAAA,MAAA,GAAS,MAAA,CAAO,EAAE,CAAA;AAClB,IAAA,IAAA,CAAA,MAAA,GAAiD,EAAC;AAClD,IAAA,IAAA,CAAA,eAAA,GAAkB,MAAA,CAAc,EAAE,CAAA;AAClC,IAAA,IAAA,CAAA,UAAA,GAAa,EAAC;AACd;AAAA,IAAA,IAAA,CAAA,YAAA,GAAe,OAAA,EAAQ;AAIrB,IAAA,IAAA,CAAK,KAAA,CAAM,SAAA,CAAU,sBAAA,EAAwB,IAAI,EAAE,SAAA,EAAU;AAE7D,IAAA,IAAA,CAAK,QAAQ,UAAA,CAAW,SAAA,CAAU,CAAC,EAAE,OAAM,KAAM;AAC/C,MAAA,IAAI,CAAC,KAAK,EAAA,EAAI;AAEd,MAAA,IAAA,CAAK,SAAS,CAAC,GAAG,IAAA,CAAK,MAAA,EAAQ,GAAG,KAAK,CAAA;AAAA,IACzC,CAAC,CAAA;AAED,IAAA,IAAA,CAAK,SAAS,UAAA,CACb,IAAA;AAAA,MACC,GAAA,CAAI,CAAC,EAAE,KAAA,OAAY,KAAK,CAAA;AAAA,MACxB,MAAA,CAAO,CAAA,QAAA,KAAY,QAAA,CAAS,MAAA,GAAS,CAAC,CAAA;AAAA,MACtC,SAAA,CAAU,CAAA,QAAA,KAAY,IAAA,CAAK,OAAA,CAAQ,IAAI,QAAA,CAAS,GAAA,CAAI,CAAA,OAAA,KAAW,IAAA,CAAK,OAAO,cAAA,CAAe,OAAO,CAAC,CAAC,CAAC,CAAC;AAAA,KACvG,CACC,SAAA,CAAU,CAAC,MAAA,KAAW;AACrB,MAAA,IAAA,CAAK,eAAA,CAAgB,IAAI,MAAM,CAAA;AAAA,IACjC,CAAC,CAAA;AAED,IAAA,IAAA,CAAK,cAAc,UAAA,CAClB,IAAA;AAAA,MACC,MAAA,CAAO,CAAA,KAAA,KAAS,KAAA,KAAU,IAAA,IAAQ,UAAU,MAAS,CAAA;AAAA,MACrD,GAAA,CAAI,CAAC,KAAA,KAAU,OAAO,KAAA,KAAU,WAAW,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA,GAAI,KAAK;AAAA,KACtE,CACC,SAAA,CAAU,CAAC,EAAC,YAAU,KAAM;AAC3B,MAAA,KAAA,MAAW,aAAa,UAAA,EAAY;AAClC,QAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,SAAS,CAAA,EAAG;AACpD,UAAA,IAAA,CAAK,UAAA,GAAa,KAAA;AAClB,UAAA,OAAA,CAAQ,IAAI,KAAK,CAAA;AACjB,UAAA,MAAM,OAAQ,KAAA,CAAc,IAAA;AAC5B,UAAA,IAAI,kBAAA,CAAmB,IAAI,CAAA,EAAG;AAC5B,YAAA,IAAA,CAAK,MAAA,CAAO,yBAAA,CAA0B,kBAAA,CAAmB,IAAI,CAAC,CAAA;AAAA,UAChE;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAED,IAAA,IAAA,CAAK,MAAA,CAAO,IAAA,CACT,IAAA,EAEA,CACA,UAAU,MAAM;AACf,MAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,MAAA,CAAO,KAAA,EAAM;AAChC,MAAA,IAAI,KAAA,EAAO;AACT,QAAA,IAAI,CAAC,KAAA,CAAM,CAAA,IAAK,CAAC,MAAM,CAAA,EAAG;AAC1B,QAAA,IAAA,CAAK,OAAO,KAAA,CAAM,eAAA;AAAA,UAChB,IAAA,CAAK,EAAA;AAAA,UACL,KAAA,CAAM,CAAA;AAAA,UACN,KAAA,CAAM,CAAA;AAAA,UACN;AAAA,SACF;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AAAA,EACL;AAAA,EAEA,IAAI,KAAA,GAAQ;AACV,IAAA,OAAO,OAAc,YAAY,CAAA;AAAA,EACnC;AAAA,EAEA,IAAI,MAAA,GAAS;AACX,IAAA,OAAO,OAAO,eAAe,CAAA;AAAA,EAC/B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkDA,MAAM,OAAA,EAMG;AACP,IAAA,MAAM,YAAA,GAAe;AAAA,MACnB,IAAA,EAAM,SAAS,IAAA,IAAQ,OAAA;AAAA,MACvB,QAAA,EAAU,SAAS,QAAA,IAAY,GAAA;AAAA,MAC/B,MAAA,EAAQ,SAAS,MAAA,IAAU,CAAA;AAAA,MAC3B,KAAA,EAAO,SAAS,KAAA,IAAS,GAAA;AAAA,MACzB,IAAA,EAAM,SAAS,IAAA,IAAQ;AAAA,KACzB;AAGA,IAAA,IAAI,YAAY,YAAA,CAAa,IAAA;AAC7B,IAAA,IAAI,OAAO,cAAc,QAAA,EAAU;AAEjC,MAAA,MAAM,QAAA,GAAmC;AAAA,QACvC,OAAA,EAAS,QAAA;AAAA,QACT,KAAA,EAAO,QAAA;AAAA,QACP,OAAA,EAAS,KAAA;AAAA,QACT,MAAA,EAAQ,GAAA;AAAA,QACR,QAAA,EAAU,QAAA;AAAA,QACV,MAAA,EAAQ,KAAA;AAAA,QACR,SAAA,EAAW,QAAA;AAAA,QACX,OAAA,EAAS;AAAA,OACX;AACA,MAAA,SAAA,GAAY,QAAA,CAAS,SAAA,CAAU,WAAA,EAAa,CAAA,IAAK,QAAA;AAAA,IACnD;AAEA,IAAA,IAAA,CAAK,aAAa,KAAA,CAAM;AAAA,MACtB,GAAG,YAAA;AAAA,MACH,IAAA,EAAM;AAAA,KACP,CAAA;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,mBAAA,GAAsB;AACpB,IAAA,IAAA,CAAK,kBAAA,CAAmB,IAAI,KAAK,CAAA;AACjC,IAAA,IAAA,CAAK,qBAAA,CAAsB,IAAI,CAAC,CAAA;AAChC,IAAA,IAAI,KAAK,qBAAA,EAAuB;AAC9B,MAAA,IAAA,CAAK,sBAAsB,WAAA,EAAY;AACvC,MAAA,IAAA,CAAK,qBAAA,GAAwB,MAAA;AAAA,IAC/B;AAAA,EACF;AAAA,EAwCA,YAAA,CAAa,aAAA,EAAuB,gBAAA,EAA+C,OAAA,EAAwB;AACzG,IAAA,IAAI,IAAA,CAAK,oBAAmB,EAAG;AAC/B,IAAA,IAAA,CAAK,kBAAA,CAAmB,IAAI,IAAI,CAAA;AAChC,IAAA,MAAM,qBAAA,GAAwB,KAAK,aAAA,EAAc;AACjD,IAAA,MAAM,gBAAA,GAAmB,KAAK,QAAA,EAAS;AACvC,IAAA,IAAA,CAAK,qBAAA,CAAsB,IAAI,CAAC,CAAA;AAEhC,IAAA,IAAI,OAAA;AACJ,IAAA,IAAI,YAAA,GAAuB,QAAA;AAG3B,IAAA,IAAI,OAAO,qBAAqB,QAAA,EAAU;AAExC,MAAA,YAAA,GAAe,gBAAA;AAAA,IACjB,CAAA,MAAA,IAAW,qBAAqB,MAAA,EAAW;AAEzC,MAAA,OAAA,GAAU,gBAAA;AACV,MAAA,YAAA,GAAe,OAAA,IAAW,QAAA;AAAA,IAC5B,CAAA,MAAO;AAEL,MAAA,YAAA,GAAe,QAAA;AAAA,IACjB;AAGA,IAAA,IAAI,YAAY,MAAA,EAAW;AACzB,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,OAAO,CAAA,EAAG;AAC1B,QAAA,IAAA,CAAK,QAAA,CAAS,IAAI,OAAO,CAAA;AAAA,MAC3B,CAAA,MAAO;AACL,QAAA,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,CAAC,OAAO,CAAC,CAAA;AAAA,MAC7B;AAAA,IACF;AAGA,IAAA,IAAI,KAAK,qBAAA,EAAuB;AAC9B,MAAA,IAAA,CAAK,sBAAsB,WAAA,EAAY;AAAA,IACzC;AAEA,IAAA,IAAA,CAAK,wBACH,IAAA,CAAK,qBAAA,CAAsB,UAAA,CAAW,SAAA,CAAU,CAAC,KAAA,KAAU;AACzD,MAAA,IAAI,SAAS,YAAA,EAAc;AACzB,QAAA,IAAA,CAAK,qBAAA,CAAsB,IAAI,CAAC,CAAA;AAChC,QAAA,IAAA,CAAK,aAAA,CAAc,IAAI,qBAAqB,CAAA;AAE5C,QAAA,IAAI,YAAY,MAAA,EAAW;AACzB,UAAA,IAAA,CAAK,QAAA,CAAS,IAAI,gBAAgB,CAAA;AAAA,QACpC;AACA,QAAA,IAAA,CAAK,kBAAA,CAAmB,IAAI,KAAK,CAAA;AACjC,QAAA,IAAI,KAAK,qBAAA,EAAuB;AAC9B,UAAA,IAAA,CAAK,sBAAsB,WAAA,EAAY;AACvC,UAAA,IAAA,CAAK,qBAAA,GAAwB,MAAA;AAAA,QAC/B;AAAA,MACF;AAAA,IACF,CAAC,CAAA;AACH,IAAA,IAAA,CAAK,aAAA,CAAc,IAAI,aAAa,CAAA;AAAA,EACtC;AAAA,EAEA,sBAAA,CAAuB,IAAY,MAAA,EAAa;AAC9C,IAAA,MAAM,MAAA,GAAS,OAAO,eAAe,CAAA;AACrC,IAAA,MAAA,CAAO,qBAAA,CAAsB,EAAE,CAAA,CAAE,aAAA,CAAc,QAAQ,IAAI,CAAA;AAAA,EAC7D;AAAA,EAEA,OAAA,GAAmB;AACjB,IAAA,OAAO,KAAK,IAAA,KAAS,OAAA;AAAA,EACvB;AAAA,EAEA,QAAA,GAAoB;AAClB,IAAA,OAAO,KAAK,IAAA,KAAS,QAAA;AAAA,EACvB;AACF;;;;"}
1
+ {"version":3,"file":"Object.js","sources":["../../src/Game/Object.ts"],"sourcesContent":["import { Hooks, ModulesToken, RpgCommonPlayer } from \"@rpgjs/common\";\nimport { trigger, signal, effect } from \"canvasengine\";\nimport { filter, from, map, Subscription, switchMap } from \"rxjs\";\nimport { inject } from \"../core/inject\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport TextComponent from \"../components/dynamics/text.ce\";\n\nconst DYNAMIC_COMPONENTS = {\n text: TextComponent,\n}\n\nexport abstract class RpgClientObject extends RpgCommonPlayer {\n abstract type: string;\n emitParticleTrigger = trigger();\n particleName = signal(\"\");\n animationCurrentIndex = signal(0);\n animationIsPlaying = signal(false);\n _param = signal({});\n frames: { x: number; y: number; ts: number }[] = [];\n graphicsSignals = signal<any[]>([]);\n _component = {} // temporary component memory\n flashTrigger = trigger();\n\n constructor() {\n super();\n this.hooks.callHooks(\"client-sprite-onInit\", this).subscribe();\n\n this._frames.observable.subscribe(({ items }) => {\n if (!this.id) return;\n //if (this.id == this.engine.playerIdSignal()!) return;\n this.frames = [...this.frames, ...items];\n });\n\n this.graphics.observable\n .pipe(\n map(({ items }) => items),\n filter(graphics => graphics.length > 0),\n switchMap(graphics => from(Promise.all(graphics.map(graphic => this.engine.getSpriteSheet(graphic)))))\n )\n .subscribe((sheets) => { \n this.graphicsSignals.set(sheets);\n });\n\n this.componentsTop.observable\n .pipe(\n filter(value => value !== null && value !== undefined),\n map((value) => typeof value === 'string' ? JSON.parse(value) : value),\n )\n .subscribe(({components}) => {\n for (const component of components) {\n for (const [key, value] of Object.entries(component)) {\n this._component = value as any; // temporary component memory\n console.log(value)\n const type = (value as any).type as keyof typeof DYNAMIC_COMPONENTS;\n if (DYNAMIC_COMPONENTS[type]) {\n this.engine.addSpriteComponentInFront(DYNAMIC_COMPONENTS[type]);\n }\n }\n }\n });\n\n this.engine.tick\n .pipe\n //throttleTime(10)\n ()\n .subscribe(() => {\n const frame = this.frames.shift();\n if (frame) {\n if (typeof frame.x !== \"number\" || typeof frame.y !== \"number\") return;\n this.engine.scene.setBodyPosition(\n this.id,\n frame.x,\n frame.y,\n \"top-left\"\n );\n }\n });\n }\n\n get hooks() {\n return inject<Hooks>(ModulesToken);\n }\n\n get engine() {\n return inject(RpgClientEngine);\n }\n\n private animationSubscription?: Subscription;\n\n /**\n * Trigger a flash animation on this sprite\n * \n * This method triggers a flash effect using CanvasEngine's flash directive.\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 uses a trigger system that is connected to the flash directive in the\n * character component. This allows for flexible configuration and can be triggered\n * from both server events and client-side code.\n * \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 * // Simple flash with default settings (alpha flash)\n * player.flash();\n * \n * // Flash with red tint\n * player.flash({ type: 'tint', tint: 0xff0000 });\n * \n * // Flash with both alpha and tint\n * player.flash({ \n * type: 'both', \n * alpha: 0.5, \n * tint: 0xff0000,\n * duration: 200,\n * cycles: 2\n * });\n * \n * // Quick damage flash\n * player.flash({ \n * type: 'tint', \n * tint: 0xff0000, \n * duration: 150,\n * cycles: 1\n * });\n * ```\n */\n flash(options?: {\n type?: 'alpha' | 'tint' | 'both';\n duration?: number;\n cycles?: number;\n alpha?: number;\n tint?: number | string;\n }): void {\n const flashOptions = {\n type: options?.type || 'alpha',\n duration: options?.duration ?? 300,\n cycles: options?.cycles ?? 1,\n alpha: options?.alpha ?? 0.3,\n tint: options?.tint ?? 0xffffff,\n };\n \n // Convert color name to hex if needed\n let tintValue = flashOptions.tint;\n if (typeof tintValue === 'string') {\n // Common color name to hex mapping\n const colorMap: Record<string, number> = {\n 'white': 0xffffff,\n 'red': 0xff0000,\n 'green': 0x00ff00,\n 'blue': 0x0000ff,\n 'yellow': 0xffff00,\n 'cyan': 0x00ffff,\n 'magenta': 0xff00ff,\n 'black': 0x000000,\n };\n tintValue = colorMap[tintValue.toLowerCase()] ?? 0xffffff;\n }\n \n this.flashTrigger.start({\n ...flashOptions,\n tint: tintValue,\n });\n }\n\n /**\n * Reset animation state when animation changes externally\n *\n * This method should be called when the animation changes due to movement\n * or other external factors to ensure the animation system doesn't get stuck\n *\n * @example\n * ```ts\n * // Reset when player starts moving\n * player.resetAnimationState();\n * ```\n */\n resetAnimationState() {\n this.animationIsPlaying.set(false);\n this.animationCurrentIndex.set(0);\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n this.animationSubscription = undefined;\n }\n }\n\n /**\n * Set a custom animation for a specific number of times\n *\n * Plays a custom animation for the specified number of repetitions.\n * The animation system prevents overlapping animations and automatically\n * returns to the previous animation when complete.\n *\n * @param animationName - Name of the animation to play\n * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)\n *\n * @example\n * ```ts\n * // Play attack animation 3 times\n * player.setAnimation('attack', 3);\n *\n * // Play continuous spell animation\n * player.setAnimation('spell');\n * ```\n */\n setAnimation(animationName: string, nbTimes?: number): void;\n /**\n * Set a custom animation with temporary graphic change\n *\n * Plays a custom animation for the specified number of repetitions and temporarily\n * changes the player's graphic (sprite sheet) during the animation. The graphic\n * is automatically reset when the animation finishes.\n *\n * @param animationName - Name of the animation to play\n * @param graphic - The graphic(s) to temporarily use during the animation\n * @param nbTimes - Number of times to repeat the animation (default: Infinity for continuous)\n *\n * @example\n * ```ts\n * // Play attack animation with temporary graphic change\n * player.setAnimation('attack', 'hero_attack', 3);\n * ```\n */\n setAnimation(animationName: string, graphic?: string | string[], nbTimes?: number): void;\n setAnimation(animationName: string, graphicOrNbTimes?: string | string[] | number, nbTimes?: number): void {\n if (this.animationIsPlaying()) return;\n this.animationIsPlaying.set(true);\n const previousAnimationName = this.animationName();\n const previousGraphics = this.graphics();\n this.animationCurrentIndex.set(0);\n\n let graphic: string | string[] | undefined;\n let finalNbTimes: number = Infinity;\n\n // Handle overloads\n if (typeof graphicOrNbTimes === 'number') {\n // setAnimation(animationName, nbTimes)\n finalNbTimes = graphicOrNbTimes;\n } else if (graphicOrNbTimes !== undefined) {\n // setAnimation(animationName, graphic, nbTimes)\n graphic = graphicOrNbTimes;\n finalNbTimes = nbTimes ?? Infinity;\n } else {\n // setAnimation(animationName) - nbTimes remains Infinity\n finalNbTimes = Infinity;\n }\n\n // Temporarily change graphic if provided\n if (graphic !== undefined) {\n if (Array.isArray(graphic)) {\n this.graphics.set(graphic);\n } else {\n this.graphics.set([graphic]);\n }\n }\n\n // Clean up any existing subscription\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n }\n\n this.animationSubscription =\n this.animationCurrentIndex.observable.subscribe((index) => {\n if (index >= finalNbTimes) {\n this.animationCurrentIndex.set(0);\n this.animationName.set(previousAnimationName);\n // Reset graphic to previous value if it was changed\n if (graphic !== undefined) {\n this.graphics.set(previousGraphics);\n }\n this.animationIsPlaying.set(false);\n if (this.animationSubscription) {\n this.animationSubscription.unsubscribe();\n this.animationSubscription = undefined;\n }\n }\n });\n this.animationName.set(animationName);\n }\n\n showComponentAnimation(id: string, params: any) {\n const engine = inject(RpgClientEngine);\n engine.getComponentAnimation(id).displayEffect(params, this);\n }\n \n isEvent(): boolean {\n return this.type === 'event';\n }\n\n isPlayer(): boolean {\n return this.type === 'player';\n }\n}\n"],"names":["TextComponent"],"mappings":"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"}
package/dist/Gui/Gui.js CHANGED
@@ -2,6 +2,7 @@ import { inject } from '../node_modules/.pnpm/@signe_di@2.8.3/node_modules/@sign
2
2
  import { signal } from 'canvasengine';
3
3
  import { WebSocketToken } from '../services/AbstractSocket.js';
4
4
  import component from '../components/gui/dialogbox/index.ce.js';
5
+ import '@canvasengine/presets';
5
6
  import { combineLatest } from 'rxjs';
6
7
  import { PrebuiltGui } from '@rpgjs/common';
7
8
  import '../Sound.js';
@@ -11,7 +12,6 @@ import '../Game/Map.js';
11
12
  import 'pixi.js';
12
13
  import '../node_modules/.pnpm/@signe_room@2.8.3/node_modules/@signe/room/dist/index.js';
13
14
  import '../node_modules/.pnpm/@signe_sync@2.8.3/node_modules/@signe/sync/dist/client/index.js';
14
- import '@canvasengine/presets';
15
15
  import component$2 from '../components/gui/shop/shop.ce.js';
16
16
  import component$4 from '../components/gui/save-load.ce.js';
17
17
  import component$1 from '../components/gui/menu/main-menu.ce.js';
@@ -42,11 +42,17 @@ export declare class RpgClientEngine<T = any> {
42
42
  private prediction?;
43
43
  private readonly SERVER_CORRECTION_THRESHOLD;
44
44
  private inputFrameCounter;
45
+ private pendingPredictionFrames;
46
+ private lastClientPhysicsStepAt;
45
47
  private frameOffset;
46
48
  private rtt;
47
49
  private pingInterval;
48
50
  private readonly PING_INTERVAL_MS;
49
51
  private lastInputTime;
52
+ private readonly MOVE_PATH_RESEND_INTERVAL_MS;
53
+ private readonly MAX_MOVE_TRAJECTORY_POINTS;
54
+ private lastMovePathSentAt;
55
+ private lastMovePathSentFrame;
50
56
  private mapLoadCompleted$;
51
57
  private playerIdReceived$;
52
58
  private playersReceived$;
@@ -92,6 +98,8 @@ export declare class RpgClientEngine<T = any> {
92
98
  */
93
99
  setKeyboardControls(controlInstance: any): void;
94
100
  start(): Promise<void>;
101
+ private prepareSyncPayload;
102
+ private normalizeAckWithSyncState;
95
103
  private initListeners;
96
104
  /**
97
105
  * Start periodic ping/pong for client-server synchronization
@@ -526,6 +534,12 @@ export declare class RpgClientEngine<T = any> {
526
534
  get playerId(): string | null;
527
535
  get scene(): RpgClientMap;
528
536
  private getPhysicsTick;
537
+ private ensureCurrentPlayerBody;
538
+ private stepClientPhysicsTick;
539
+ private flushPendingPredictedStates;
540
+ private buildPendingMoveTrajectory;
541
+ private emitMovePacket;
542
+ private flushPendingMovePath;
529
543
  private getLocalPlayerState;
530
544
  private applyAuthoritativeState;
531
545
  private initializePredictionController;
@@ -624,6 +638,7 @@ export declare class RpgClientEngine<T = any> {
624
638
  tint?: number | string;
625
639
  }): void;
626
640
  private applyServerAck;
641
+ private reconcilePrediction;
627
642
  /**
628
643
  * Replay unacknowledged inputs from a given frame to resimulate client prediction
629
644
  * after applying server authority at a certain frame.
@@ -41,6 +41,8 @@ class RpgClientEngine {
41
41
  this.predictionEnabled = false;
42
42
  this.SERVER_CORRECTION_THRESHOLD = 30;
43
43
  this.inputFrameCounter = 0;
44
+ this.pendingPredictionFrames = [];
45
+ this.lastClientPhysicsStepAt = 0;
44
46
  this.frameOffset = 0;
45
47
  // Ping/Pong for RTT measurement
46
48
  this.rtt = 0;
@@ -49,6 +51,10 @@ class RpgClientEngine {
49
51
  this.PING_INTERVAL_MS = 5e3;
50
52
  // Send ping every 5 seconds
51
53
  this.lastInputTime = 0;
54
+ this.MOVE_PATH_RESEND_INTERVAL_MS = 120;
55
+ this.MAX_MOVE_TRAJECTORY_POINTS = 240;
56
+ this.lastMovePathSentAt = 0;
57
+ this.lastMovePathSentFrame = 0;
52
58
  // Track map loading state for onAfterLoading hook using RxJS
53
59
  this.mapLoadCompleted$ = new BehaviorSubject(false);
54
60
  this.playerIdReceived$ = new BehaviorSubject(false);
@@ -125,6 +131,8 @@ class RpgClientEngine {
125
131
  }
126
132
  async start() {
127
133
  this.sceneMap = new RpgClientMap();
134
+ this.sceneMap.configureClientPrediction(this.predictionEnabled);
135
+ this.sceneMap.loadPhysic();
128
136
  this.selector = document.body.querySelector("#rpg");
129
137
  const bootstrapOptions = this.globalConfig?.bootstrapCanvasOptions;
130
138
  const { app, canvasElement } = await bootstrapCanvas(
@@ -160,6 +168,9 @@ class RpgClientEngine {
160
168
  };
161
169
  window.addEventListener("resize", this.resizeHandler);
162
170
  const tickSubscription = this.tick.subscribe((tick) => {
171
+ this.stepClientPhysicsTick();
172
+ this.flushPendingPredictedStates();
173
+ this.flushPendingMovePath();
163
174
  this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
164
175
  if (tick % 60 === 0) {
165
176
  const now = Date.now();
@@ -173,8 +184,45 @@ class RpgClientEngine {
173
184
  saveClient.initialize(this.webSocket);
174
185
  this.initListeners();
175
186
  this.guiService._initialize();
187
+ this.startPingPong();
176
188
  });
177
189
  }
190
+ prepareSyncPayload(data) {
191
+ const payload = { ...data ?? {} };
192
+ delete payload.ack;
193
+ delete payload.timestamp;
194
+ const myId = this.playerIdSignal();
195
+ const players = payload.players;
196
+ const shouldMaskLocalPosition = this.predictionEnabled && !!this.prediction?.hasPendingInputs();
197
+ if (shouldMaskLocalPosition && myId && players && players[myId]) {
198
+ const localPatch = { ...players[myId] };
199
+ delete localPatch.x;
200
+ delete localPatch.y;
201
+ delete localPatch.direction;
202
+ delete localPatch._frames;
203
+ payload.players = {
204
+ ...players,
205
+ [myId]: localPatch
206
+ };
207
+ }
208
+ return payload;
209
+ }
210
+ normalizeAckWithSyncState(ack, syncData) {
211
+ const myId = this.playerIdSignal();
212
+ if (!myId) {
213
+ return ack;
214
+ }
215
+ const localPatch = syncData?.players?.[myId];
216
+ if (typeof localPatch?.x !== "number" || typeof localPatch?.y !== "number") {
217
+ return ack;
218
+ }
219
+ return {
220
+ ...ack,
221
+ x: localPatch.x,
222
+ y: localPatch.y,
223
+ direction: localPatch.direction ?? ack.direction
224
+ };
225
+ }
178
226
  initListeners() {
179
227
  this.webSocket.on("sync", (data) => {
180
228
  if (data.pId) {
@@ -183,22 +231,29 @@ class RpgClientEngine {
183
231
  }
184
232
  if (this.sceneResetQueued) {
185
233
  this.sceneMap.reset();
234
+ this.sceneMap.loadPhysic();
186
235
  this.sceneResetQueued = false;
187
236
  }
188
237
  this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
189
- load(this.sceneMap, data, true);
190
- for (const playerId in data.players ?? {}) {
191
- const player = data.players[playerId];
238
+ const ack = data?.ack;
239
+ const normalizedAck = ack && typeof ack.frame === "number" ? this.normalizeAckWithSyncState(ack, data) : void 0;
240
+ const payload = this.prepareSyncPayload(data);
241
+ load(this.sceneMap, payload, true);
242
+ if (normalizedAck) {
243
+ this.applyServerAck(normalizedAck);
244
+ }
245
+ for (const playerId in payload.players ?? {}) {
246
+ const player = payload.players[playerId];
192
247
  if (!player._param) continue;
193
248
  for (const param in player._param) {
194
249
  this.sceneMap.players()[playerId]._param()[param] = player._param[param];
195
250
  }
196
251
  }
197
- const players = data.players || this.sceneMap.players();
252
+ const players = payload.players || this.sceneMap.players();
198
253
  if (players && Object.keys(players).length > 0) {
199
254
  this.playersReceived$.next(true);
200
255
  }
201
- const events = data.events || this.sceneMap.events();
256
+ const events = payload.events || this.sceneMap.events();
202
257
  if (events !== void 0) {
203
258
  this.eventsReceived$.next(true);
204
259
  }
@@ -216,7 +271,8 @@ class RpgClientEngine {
216
271
  this.webSocket.on("changeMap", (data) => {
217
272
  this.sceneResetQueued = true;
218
273
  this.cameraFollowTargetId.set(null);
219
- this.loadScene(data.mapId);
274
+ const transferToken = typeof data?.transferToken === "string" ? data.transferToken : void 0;
275
+ this.loadScene(data.mapId, transferToken);
220
276
  });
221
277
  this.webSocket.on("showComponentAnimation", (data) => {
222
278
  const { params, object, position, id } = data;
@@ -269,8 +325,29 @@ class RpgClientEngine {
269
325
  direction
270
326
  });
271
327
  });
328
+ this.webSocket.on("weatherState", (data) => {
329
+ const raw = data && typeof data === "object" && "value" in data ? data.value : data;
330
+ if (raw === null) {
331
+ this.sceneMap.weatherState.set(null);
332
+ return;
333
+ }
334
+ const validEffects = ["rain", "snow", "fog", "cloud"];
335
+ if (!raw || !validEffects.includes(raw.effect)) {
336
+ return;
337
+ }
338
+ this.sceneMap.weatherState.set({
339
+ effect: raw.effect,
340
+ preset: raw.preset,
341
+ params: raw.params,
342
+ transitionMs: raw.transitionMs,
343
+ durationMs: raw.durationMs,
344
+ startedAt: raw.startedAt,
345
+ seed: raw.seed
346
+ });
347
+ });
272
348
  this.webSocket.on("open", () => {
273
349
  this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
350
+ this.startPingPong();
274
351
  });
275
352
  this.webSocket.on("close", () => {
276
353
  this.hooks.callHooks("client-engine-onDisconnected", this, this.socket).subscribe();
@@ -343,7 +420,7 @@ class RpgClientEngine {
343
420
  clientFrame
344
421
  });
345
422
  }
346
- async loadScene(mapId) {
423
+ async loadScene(mapId, transferToken) {
347
424
  await lastValueFrom(this.hooks.callHooks("client-sceneMap-onBeforeLoading", this.sceneMap));
348
425
  this.clearClientPredictionStates();
349
426
  this.mapLoadCompleted$.next(false);
@@ -354,7 +431,10 @@ class RpgClientEngine {
354
431
  this.onAfterLoadingSubscription.unsubscribe();
355
432
  }
356
433
  this.setupOnAfterLoadingObserver();
357
- this.webSocket.updateProperties({ room: mapId });
434
+ this.webSocket.updateProperties({
435
+ room: mapId,
436
+ query: transferToken ? { transferToken } : void 0
437
+ });
358
438
  await this.webSocket.reconnect(() => {
359
439
  const saveClient = inject(SaveClientService);
360
440
  saveClient.initialize(this.webSocket);
@@ -375,6 +455,7 @@ class RpgClientEngine {
375
455
  this.eventsReceived$.next(true);
376
456
  }
377
457
  this.mapLoadCompleted$.next(true);
458
+ this.sceneMap.configureClientPrediction(this.predictionEnabled);
378
459
  this.sceneMap.loadPhysic();
379
460
  }
380
461
  addSpriteSheet(spritesheetClass, id) {
@@ -899,19 +980,22 @@ class RpgClientEngine {
899
980
  frame = ++this.inputFrameCounter;
900
981
  tick = this.getPhysicsTick();
901
982
  }
983
+ this.inputFrameCounter = frame;
902
984
  this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
903
- this.webSocket.emit("move", {
904
- input,
905
- timestamp,
906
- frame,
907
- tick
908
- });
909
985
  const currentPlayer = this.sceneMap.getCurrentPlayer();
910
- if (currentPlayer) {
986
+ const bodyReady = this.ensureCurrentPlayerBody();
987
+ if (currentPlayer && bodyReady) {
988
+ currentPlayer.changeDirection(input);
911
989
  this.sceneMap.moveBody(currentPlayer, input);
990
+ if (this.predictionEnabled && this.prediction) {
991
+ this.pendingPredictionFrames.push(frame);
992
+ if (this.pendingPredictionFrames.length > 240) {
993
+ this.pendingPredictionFrames = this.pendingPredictionFrames.slice(-240);
994
+ }
995
+ }
912
996
  }
997
+ this.emitMovePacket(input, frame, tick, timestamp, true);
913
998
  this.lastInputTime = Date.now();
914
- this.playerIdSignal();
915
999
  }
916
1000
  processAction({ action }) {
917
1001
  if (this.stopProcessingInput) return;
@@ -933,6 +1017,110 @@ class RpgClientEngine {
933
1017
  getPhysicsTick() {
934
1018
  return this.sceneMap?.getTick?.() ?? 0;
935
1019
  }
1020
+ ensureCurrentPlayerBody() {
1021
+ const player = this.sceneMap?.getCurrentPlayer();
1022
+ const myId = this.playerIdSignal();
1023
+ if (!player || !myId) {
1024
+ return false;
1025
+ }
1026
+ if (!player.id) {
1027
+ player.id = myId;
1028
+ }
1029
+ if (this.sceneMap.getBody(myId)) {
1030
+ return true;
1031
+ }
1032
+ try {
1033
+ this.sceneMap.loadPhysic();
1034
+ } catch (error) {
1035
+ console.error("[RPGJS] Unable to initialize client physics before input:", error);
1036
+ return false;
1037
+ }
1038
+ return !!this.sceneMap.getBody(myId);
1039
+ }
1040
+ stepClientPhysicsTick() {
1041
+ if (!this.predictionEnabled || !this.sceneMap) {
1042
+ return;
1043
+ }
1044
+ const now = Date.now();
1045
+ if (this.lastClientPhysicsStepAt === 0) {
1046
+ this.lastClientPhysicsStepAt = now;
1047
+ }
1048
+ const deltaMs = Math.max(1, Math.min(100, now - this.lastClientPhysicsStepAt));
1049
+ this.lastClientPhysicsStepAt = now;
1050
+ this.sceneMap.stepClientPhysics(deltaMs);
1051
+ }
1052
+ flushPendingPredictedStates() {
1053
+ if (!this.predictionEnabled || !this.prediction || this.pendingPredictionFrames.length === 0) {
1054
+ return;
1055
+ }
1056
+ const state = this.getLocalPlayerState();
1057
+ while (this.pendingPredictionFrames.length > 0) {
1058
+ const frame = this.pendingPredictionFrames.shift();
1059
+ if (typeof frame === "number") {
1060
+ this.prediction.attachPredictedState(frame, state);
1061
+ }
1062
+ }
1063
+ }
1064
+ buildPendingMoveTrajectory() {
1065
+ if (!this.predictionEnabled || !this.prediction) {
1066
+ return [];
1067
+ }
1068
+ const pendingInputs = this.prediction.getPendingInputs();
1069
+ const trajectory = [];
1070
+ for (const entry of pendingInputs) {
1071
+ const state = entry.state;
1072
+ if (!state) continue;
1073
+ if (typeof state.x !== "number" || typeof state.y !== "number") continue;
1074
+ trajectory.push({
1075
+ frame: entry.frame,
1076
+ tick: entry.tick,
1077
+ timestamp: entry.timestamp,
1078
+ input: entry.direction,
1079
+ x: state.x,
1080
+ y: state.y,
1081
+ direction: state.direction ?? entry.direction
1082
+ });
1083
+ }
1084
+ if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
1085
+ return trajectory.slice(-this.MAX_MOVE_TRAJECTORY_POINTS);
1086
+ }
1087
+ return trajectory;
1088
+ }
1089
+ emitMovePacket(input, frame, tick, timestamp, force = false) {
1090
+ const trajectory = this.buildPendingMoveTrajectory();
1091
+ const latestTrajectoryFrame = trajectory.length > 0 ? trajectory[trajectory.length - 1].frame : frame;
1092
+ const shouldThrottle = !force && latestTrajectoryFrame <= this.lastMovePathSentFrame && timestamp - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS;
1093
+ if (shouldThrottle) {
1094
+ return;
1095
+ }
1096
+ this.webSocket.emit("move", {
1097
+ input,
1098
+ timestamp,
1099
+ frame,
1100
+ tick,
1101
+ trajectory
1102
+ });
1103
+ this.lastMovePathSentAt = timestamp;
1104
+ this.lastMovePathSentFrame = Math.max(this.lastMovePathSentFrame, latestTrajectoryFrame, frame);
1105
+ }
1106
+ flushPendingMovePath() {
1107
+ if (!this.predictionEnabled || !this.prediction) {
1108
+ return;
1109
+ }
1110
+ const pendingInputs = this.prediction.getPendingInputs();
1111
+ if (pendingInputs.length === 0) {
1112
+ return;
1113
+ }
1114
+ const latest = pendingInputs[pendingInputs.length - 1];
1115
+ if (!latest) {
1116
+ return;
1117
+ }
1118
+ const now = Date.now();
1119
+ if (now - this.lastMovePathSentAt < this.MOVE_PATH_RESEND_INTERVAL_MS) {
1120
+ return;
1121
+ }
1122
+ this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
1123
+ }
936
1124
  getLocalPlayerState() {
937
1125
  const currentPlayer = this.sceneMap?.getCurrentPlayer();
938
1126
  if (!currentPlayer) {
@@ -963,11 +1151,18 @@ class RpgClientEngine {
963
1151
  initializePredictionController() {
964
1152
  if (!this.predictionEnabled) {
965
1153
  this.prediction = void 0;
1154
+ this.sceneMap?.configureClientPrediction?.(false);
966
1155
  return;
967
1156
  }
1157
+ const configuredTtl = this.globalConfig?.prediction?.historyTtlMs;
1158
+ const historyTtlMs = typeof configuredTtl === "number" ? configuredTtl : 1e4;
1159
+ const configuredMaxEntries = this.globalConfig?.prediction?.maxHistoryEntries;
1160
+ const maxHistoryEntries = typeof configuredMaxEntries === "number" ? configuredMaxEntries : Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
1161
+ this.sceneMap?.configureClientPrediction?.(true);
968
1162
  this.prediction = new PredictionController({
969
1163
  correctionThreshold: this.globalConfig?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
970
- historyTtlMs: this.globalConfig?.prediction?.historyTtlMs ?? 2e3,
1164
+ historyTtlMs,
1165
+ maxHistoryEntries,
971
1166
  getPhysicsTick: () => this.getPhysicsTick(),
972
1167
  getCurrentState: () => this.getLocalPlayerState(),
973
1168
  setAuthoritativeState: (state) => this.applyAuthoritativeState(state)
@@ -1031,6 +1226,10 @@ class RpgClientEngine {
1031
1226
  this.initializePredictionController();
1032
1227
  this.frameOffset = 0;
1033
1228
  this.inputFrameCounter = 0;
1229
+ this.pendingPredictionFrames = [];
1230
+ this.lastClientPhysicsStepAt = 0;
1231
+ this.lastMovePathSentAt = 0;
1232
+ this.lastMovePathSentFrame = 0;
1034
1233
  }
1035
1234
  /**
1036
1235
  * Trigger a flash animation on a sprite
@@ -1089,11 +1288,14 @@ class RpgClientEngine {
1089
1288
  }
1090
1289
  applyServerAck(ack) {
1091
1290
  if (this.predictionEnabled && this.prediction) {
1092
- this.prediction.applyServerAck({
1291
+ const result = this.prediction.applyServerAck({
1093
1292
  frame: ack.frame,
1094
1293
  serverTick: ack.serverTick,
1095
1294
  state: typeof ack.x === "number" && typeof ack.y === "number" ? { x: ack.x, y: ack.y, direction: ack.direction } : void 0
1096
1295
  });
1296
+ if (result.state && result.needsReconciliation) {
1297
+ this.reconcilePrediction(result.state, result.pendingInputs);
1298
+ }
1097
1299
  return;
1098
1300
  }
1099
1301
  if (typeof ack.x !== "number" || typeof ack.y !== "number") {
@@ -1117,6 +1319,24 @@ class RpgClientEngine {
1117
1319
  player.changeDirection(ack.direction);
1118
1320
  }
1119
1321
  }
1322
+ reconcilePrediction(authoritativeState, pendingInputs) {
1323
+ const player = this.getCurrentPlayer();
1324
+ if (!player) {
1325
+ return;
1326
+ }
1327
+ this.sceneMap.stopMovement(player);
1328
+ this.applyAuthoritativeState(authoritativeState);
1329
+ if (!pendingInputs.length) {
1330
+ return;
1331
+ }
1332
+ const replayInputs = pendingInputs.slice(-600);
1333
+ for (const entry of replayInputs) {
1334
+ if (!entry?.direction) continue;
1335
+ this.sceneMap.moveBody(player, entry.direction);
1336
+ this.sceneMap.stepPredictionTick();
1337
+ this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
1338
+ }
1339
+ }
1120
1340
  /**
1121
1341
  * Replay unacknowledged inputs from a given frame to resimulate client prediction
1122
1342
  * after applying server authority at a certain frame.
@@ -1248,6 +1468,8 @@ class RpgClientEngine {
1248
1468
  this.inputFrameCounter = 0;
1249
1469
  this.frameOffset = 0;
1250
1470
  this.rtt = 0;
1471
+ this.lastMovePathSentAt = 0;
1472
+ this.lastMovePathSentFrame = 0;
1251
1473
  this.mapLoadCompleted$.next(false);
1252
1474
  this.playerIdReceived$.next(false);
1253
1475
  this.playersReceived$.next(false);