@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.
- package/dist/Game/Map.d.ts +11 -1
- package/dist/Game/Map.js +30 -0
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.js +1 -1
- package/dist/Game/Object.js.map +1 -1
- package/dist/Gui/Gui.js +1 -1
- package/dist/RpgClientEngine.d.ts +15 -0
- package/dist/RpgClientEngine.js +240 -18
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/components/scenes/canvas.ce.js +1 -1
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +34 -2
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/services/AbstractSocket.d.ts +9 -5
- package/dist/services/AbstractSocket.js.map +1 -1
- package/dist/services/mmorpg.d.ts +12 -8
- package/dist/services/mmorpg.js +67 -12
- package/dist/services/mmorpg.js.map +1 -1
- package/dist/services/standalone.d.ts +2 -4
- package/dist/services/standalone.js +1 -1
- package/dist/services/standalone.js.map +1 -1
- package/package.json +7 -7
- package/src/Game/Map.ts +38 -2
- package/src/Game/Object.ts +1 -1
- package/src/RpgClientEngine.ts +300 -20
- package/src/components/scenes/canvas.ce +1 -1
- package/src/components/scenes/draw-map.ce +37 -1
- package/src/services/AbstractSocket.ts +10 -2
- package/src/services/mmorpg.ts +84 -14
- package/src/services/standalone.ts +3 -3
package/dist/Game/Map.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/Game/Map.js.map
CHANGED
|
@@ -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')
|
|
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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"}
|
package/dist/Game/Object.js
CHANGED
|
@@ -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 (
|
|
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,
|
package/dist/Game/Object.js.map
CHANGED
|
@@ -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":"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"}
|
|
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.
|
package/dist/RpgClientEngine.js
CHANGED
|
@@ -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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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
|
|
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);
|