@rpgjs/client 5.0.0-beta.2 → 5.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +123 -0
- package/dist/Game/AnimationManager.d.ts +2 -1
- package/dist/Game/AnimationManager.js +21 -9
- package/dist/Game/AnimationManager.js.map +1 -1
- package/dist/Game/AnimationManager.spec.d.ts +1 -0
- package/dist/Game/ClientVisuals.d.ts +61 -0
- package/dist/Game/ClientVisuals.js +96 -0
- package/dist/Game/ClientVisuals.js.map +1 -0
- package/dist/Game/ClientVisuals.spec.d.ts +1 -0
- package/dist/Game/Event.js.map +1 -1
- package/dist/Game/EventComponentResolver.d.ts +16 -0
- package/dist/Game/EventComponentResolver.js +52 -0
- package/dist/Game/EventComponentResolver.js.map +1 -0
- package/dist/Game/EventComponentResolver.spec.d.ts +1 -0
- package/dist/Game/Map.d.ts +9 -1
- package/dist/Game/Map.js +83 -5
- package/dist/Game/Map.js.map +1 -1
- package/dist/Game/Object.d.ts +49 -15
- package/dist/Game/Object.js +101 -42
- package/dist/Game/Object.js.map +1 -1
- package/dist/Game/Object.spec.d.ts +1 -0
- package/dist/Game/Player.js.map +1 -1
- package/dist/Game/ProjectileManager.d.ts +98 -0
- package/dist/Game/ProjectileManager.js +196 -0
- package/dist/Game/ProjectileManager.js.map +1 -0
- package/dist/Game/ProjectileManager.spec.d.ts +1 -0
- package/dist/Gui/Gui.d.ts +20 -6
- package/dist/Gui/Gui.js +96 -54
- package/dist/Gui/Gui.js.map +1 -1
- package/dist/Gui/Gui.spec.d.ts +1 -0
- package/dist/Gui/NotificationManager.js.map +1 -1
- package/dist/Resource.js +1 -1
- package/dist/Resource.js.map +1 -1
- package/dist/RpgClient.d.ts +194 -15
- package/dist/RpgClientEngine.d.ts +176 -11
- package/dist/RpgClientEngine.js +669 -68
- package/dist/RpgClientEngine.js.map +1 -1
- package/dist/Sound.js.map +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.122.0/helpers → _@oxc-project_runtime@0.133.0/helpers/esm}/decorate.js +1 -1
- package/dist/_virtual/{_@oxc-project_runtime@0.122.0/helpers → _@oxc-project_runtime@0.133.0/helpers/esm}/decorateMetadata.js +1 -1
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/components/animations/animation.ce.js +6 -5
- package/dist/components/animations/animation.ce.js.map +1 -1
- package/dist/components/animations/fx.ce.js +60 -0
- package/dist/components/animations/fx.ce.js.map +1 -0
- package/dist/components/animations/hit.ce.js +24 -28
- package/dist/components/animations/hit.ce.js.map +1 -1
- package/dist/components/animations/index.d.ts +1 -0
- package/dist/components/animations/index.js +6 -4
- package/dist/components/animations/index.js.map +1 -1
- package/dist/components/character.ce.js +606 -248
- package/dist/components/character.ce.js.map +1 -1
- package/dist/components/dynamics/bar.ce.js +99 -0
- package/dist/components/dynamics/bar.ce.js.map +1 -0
- package/dist/components/dynamics/image.ce.js +26 -0
- package/dist/components/dynamics/image.ce.js.map +1 -0
- package/dist/components/dynamics/parse-value.d.ts +3 -0
- package/dist/components/dynamics/parse-value.js +54 -35
- package/dist/components/dynamics/parse-value.js.map +1 -1
- package/dist/components/dynamics/parse-value.spec.d.ts +1 -0
- package/dist/components/dynamics/shape-utils.d.ts +16 -0
- package/dist/components/dynamics/shape-utils.js +73 -0
- package/dist/components/dynamics/shape-utils.js.map +1 -0
- package/dist/components/dynamics/shape-utils.spec.d.ts +1 -0
- package/dist/components/dynamics/shape.ce.js +86 -0
- package/dist/components/dynamics/shape.ce.js.map +1 -0
- package/dist/components/dynamics/text.ce.js +36 -56
- package/dist/components/dynamics/text.ce.js.map +1 -1
- package/dist/components/gui/box.ce.js +9 -9
- package/dist/components/gui/box.ce.js.map +1 -1
- package/dist/components/gui/dialogbox/index.ce.js +75 -65
- package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
- package/dist/components/gui/gameover.ce.js +47 -68
- package/dist/components/gui/gameover.ce.js.map +1 -1
- package/dist/components/gui/hud/hud.ce.js +23 -31
- package/dist/components/gui/hud/hud.ce.js.map +1 -1
- package/dist/components/gui/menu/equip-menu.ce.js +121 -172
- package/dist/components/gui/menu/equip-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/exit-menu.ce.js +15 -10
- package/dist/components/gui/menu/exit-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/items-menu.ce.js +62 -77
- package/dist/components/gui/menu/items-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/main-menu.ce.js +89 -104
- package/dist/components/gui/menu/main-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/options-menu.ce.js +12 -8
- package/dist/components/gui/menu/options-menu.ce.js.map +1 -1
- package/dist/components/gui/menu/skills-menu.ce.js +17 -19
- package/dist/components/gui/menu/skills-menu.ce.js.map +1 -1
- package/dist/components/gui/mobile/index.js +2 -2
- package/dist/components/gui/mobile/index.js.map +1 -1
- package/dist/components/gui/mobile/mobile.ce.js +7 -5
- package/dist/components/gui/mobile/mobile.ce.js.map +1 -1
- package/dist/components/gui/notification/notification.ce.js +28 -26
- package/dist/components/gui/notification/notification.ce.js.map +1 -1
- package/dist/components/gui/save-load.ce.js +81 -256
- package/dist/components/gui/save-load.ce.js.map +1 -1
- package/dist/components/gui/shop/shop.ce.js +107 -142
- package/dist/components/gui/shop/shop.ce.js.map +1 -1
- package/dist/components/gui/title-screen.ce.js +50 -73
- package/dist/components/gui/title-screen.ce.js.map +1 -1
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +1 -0
- package/dist/components/interaction-components.ce.js +22 -0
- package/dist/components/interaction-components.ce.js.map +1 -0
- package/dist/components/player-components-utils.d.ts +67 -0
- package/dist/components/player-components-utils.js +162 -0
- package/dist/components/player-components-utils.js.map +1 -0
- package/dist/components/player-components-utils.spec.d.ts +1 -0
- package/dist/components/player-components.ce.js +191 -0
- package/dist/components/player-components.ce.js.map +1 -0
- package/dist/components/prebuilt/hp-bar.ce.js +45 -45
- package/dist/components/prebuilt/hp-bar.ce.js.map +1 -1
- package/dist/components/prebuilt/light-halo.ce.js +39 -60
- package/dist/components/prebuilt/light-halo.ce.js.map +1 -1
- package/dist/components/scenes/canvas.ce.js +205 -27
- package/dist/components/scenes/canvas.ce.js.map +1 -1
- package/dist/components/scenes/draw-map.ce.js +39 -39
- package/dist/components/scenes/draw-map.ce.js.map +1 -1
- package/dist/components/scenes/event-layer.ce.js +51 -10
- package/dist/components/scenes/event-layer.ce.js.map +1 -1
- package/dist/core/inject.js +1 -1
- package/dist/core/inject.js.map +1 -1
- package/dist/core/setup.js +1 -1
- package/dist/core/setup.js.map +1 -1
- package/dist/decorators/spritesheet.d.ts +1 -0
- package/dist/decorators/spritesheet.js +11 -0
- package/dist/decorators/spritesheet.js.map +1 -0
- package/dist/i18n.d.ts +55 -0
- package/dist/i18n.js +60 -0
- package/dist/i18n.js.map +1 -0
- package/dist/i18n.spec.d.ts +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +29 -21
- package/dist/module.js +45 -4
- package/dist/module.js.map +1 -1
- package/dist/node_modules/.pnpm/{@signe_di@2.9.0 → @signe_di@3.1.0}/node_modules/@signe/di/dist/index.js +7 -117
- package/dist/node_modules/.pnpm/@signe_di@3.1.0/node_modules/@signe/di/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@signe_reactive@3.1.0/node_modules/@signe/reactive/dist/index.js +239 -0
- package/dist/node_modules/.pnpm/@signe_reactive@3.1.0/node_modules/@signe/reactive/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/chunk-EUXUH3YW.js +13 -0
- package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/chunk-EUXUH3YW.js.map +1 -0
- package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/index.js +698 -0
- package/dist/node_modules/.pnpm/@signe_room@3.1.0_react@19.2.7/node_modules/@signe/room/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/@signe_sync@3.1.0_react@19.2.7/node_modules/@signe/sync/dist/client/index.js +43 -0
- package/dist/node_modules/.pnpm/@signe_sync@3.1.0_react@19.2.7/node_modules/@signe/sync/dist/client/index.js.map +1 -0
- package/dist/node_modules/.pnpm/{@signe_sync@2.9.0 → @signe_sync@3.1.0_react@19.2.7}/node_modules/@signe/sync/dist/index.js +57 -141
- package/dist/node_modules/.pnpm/@signe_sync@3.1.0_react@19.2.7/node_modules/@signe/sync/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/{partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js → partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/index.js} +30 -17
- package/dist/node_modules/.pnpm/partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/index.js.map +1 -0
- package/dist/node_modules/.pnpm/{partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js → partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/ws.js} +65 -34
- package/dist/node_modules/.pnpm/partysocket@1.2.0_react@19.2.7/node_modules/partysocket/dist/ws.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.js +13 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/index.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js +106 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/ZodError.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js +13 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/errors.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js +121 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js +10 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js +110 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js +99 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/helpers/util.js.map +1 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js +82 -0
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/locales/en.js.map +1 -0
- package/dist/node_modules/.pnpm/{zod@3.24.2/node_modules/zod/lib/index.js → zod@3.25.76/node_modules/zod/v3/types.js} +74 -611
- package/dist/node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/types.js.map +1 -0
- package/dist/presets/animation.js.map +1 -1
- package/dist/presets/faceset.js.map +1 -1
- package/dist/presets/icon.js.map +1 -1
- package/dist/presets/index.js.map +1 -1
- package/dist/presets/lpc.js.map +1 -1
- package/dist/presets/rmspritesheet.js.map +1 -1
- package/dist/services/AbstractSocket.d.ts +2 -0
- package/dist/services/AbstractSocket.js.map +1 -1
- package/dist/services/actionInput.d.ts +14 -0
- package/dist/services/actionInput.js +59 -0
- package/dist/services/actionInput.js.map +1 -0
- package/dist/services/actionInput.spec.d.ts +1 -0
- package/dist/services/interactions.d.ts +159 -0
- package/dist/services/interactions.js +460 -0
- package/dist/services/interactions.js.map +1 -0
- package/dist/services/interactions.spec.d.ts +1 -0
- package/dist/services/keyboardControls.d.ts +1 -0
- package/dist/services/keyboardControls.js +1 -0
- package/dist/services/keyboardControls.js.map +1 -1
- package/dist/services/loadMap.d.ts +9 -0
- package/dist/services/loadMap.js +1 -1
- package/dist/services/loadMap.js.map +1 -1
- package/dist/services/mmorpg-connection.d.ts +5 -0
- package/dist/services/mmorpg-connection.js +50 -0
- package/dist/services/mmorpg-connection.js.map +1 -0
- package/dist/services/mmorpg-connection.spec.d.ts +1 -0
- package/dist/services/mmorpg.d.ts +11 -4
- package/dist/services/mmorpg.js +57 -33
- package/dist/services/mmorpg.js.map +1 -1
- package/dist/services/pointerContext.d.ts +11 -0
- package/dist/services/pointerContext.js +48 -0
- package/dist/services/pointerContext.js.map +1 -0
- package/dist/services/pointerContext.spec.d.ts +1 -0
- package/dist/services/save.js.map +1 -1
- package/dist/services/save.spec.d.ts +1 -0
- package/dist/services/standalone-message.d.ts +1 -0
- package/dist/services/standalone-message.js +9 -0
- package/dist/services/standalone-message.js.map +1 -0
- package/dist/services/standalone.d.ts +4 -1
- package/dist/services/standalone.js +36 -16
- package/dist/services/standalone.js.map +1 -1
- package/dist/services/standalone.spec.d.ts +1 -0
- package/dist/utils/getEntityProp.js +4 -3
- package/dist/utils/getEntityProp.js.map +1 -1
- package/dist/utils/getEntityProp.spec.d.ts +1 -0
- package/dist/utils/mapId.d.ts +1 -0
- package/dist/utils/mapId.js +6 -0
- package/dist/utils/mapId.js.map +1 -0
- package/dist/utils/readPropValue.d.ts +2 -0
- package/dist/utils/readPropValue.js +13 -0
- package/dist/utils/readPropValue.js.map +1 -0
- package/package.json +13 -13
- package/src/Game/AnimationManager.spec.ts +30 -0
- package/src/Game/AnimationManager.ts +26 -10
- package/src/Game/ClientVisuals.spec.ts +56 -0
- package/src/Game/ClientVisuals.ts +184 -0
- package/src/Game/EventComponentResolver.spec.ts +84 -0
- package/src/Game/EventComponentResolver.ts +74 -0
- package/src/Game/Map.ts +134 -2
- package/src/Game/Object.spec.ts +59 -0
- package/src/Game/Object.ts +181 -77
- package/src/Game/ProjectileManager.spec.ts +449 -0
- package/src/Game/ProjectileManager.ts +346 -0
- package/src/Gui/Gui.spec.ts +340 -0
- package/src/Gui/Gui.ts +129 -57
- package/src/Resource.ts +1 -2
- package/src/RpgClient.ts +220 -17
- package/src/RpgClientEngine.ts +959 -116
- package/src/components/animations/fx.ce +101 -0
- package/src/components/animations/index.ts +4 -2
- package/src/components/character.ce +681 -45
- package/src/components/dynamics/bar.ce +88 -0
- package/src/components/dynamics/image.ce +21 -0
- package/src/components/dynamics/parse-value.spec.ts +83 -0
- package/src/components/dynamics/parse-value.ts +111 -37
- package/src/components/dynamics/shape-utils.spec.ts +46 -0
- package/src/components/dynamics/shape-utils.ts +61 -0
- package/src/components/dynamics/shape.ce +90 -0
- package/src/components/dynamics/text.ce +35 -149
- package/src/components/gui/dialogbox/index.ce +52 -21
- package/src/components/gui/gameover.ce +6 -4
- package/src/components/gui/menu/equip-menu.ce +11 -9
- package/src/components/gui/menu/exit-menu.ce +6 -4
- package/src/components/gui/menu/items-menu.ce +11 -9
- package/src/components/gui/menu/main-menu.ce +14 -12
- package/src/components/gui/menu/options-menu.ce +4 -3
- package/src/components/gui/menu/skills-menu.ce +2 -1
- package/src/components/gui/notification/notification.ce +7 -1
- package/src/components/gui/save-load.ce +13 -11
- package/src/components/gui/shop/shop.ce +20 -18
- package/src/components/gui/title-screen.ce +6 -4
- package/src/components/index.ts +2 -1
- package/src/components/interaction-components.ce +23 -0
- package/src/components/player-components-utils.spec.ts +109 -0
- package/src/components/player-components-utils.ts +205 -0
- package/src/components/player-components.ce +222 -0
- package/src/components/prebuilt/hp-bar.ce +4 -3
- package/src/components/prebuilt/light-halo.ce +2 -2
- package/src/components/scenes/canvas.ce +219 -15
- package/src/components/scenes/draw-map.ce +34 -22
- package/src/components/scenes/event-layer.ce +55 -4
- package/src/core/setup.ts +2 -2
- package/src/decorators/spritesheet.ts +8 -0
- package/src/i18n.spec.ts +39 -0
- package/src/i18n.ts +59 -0
- package/src/index.ts +7 -0
- package/src/module.ts +61 -11
- package/src/services/AbstractSocket.ts +3 -0
- package/src/services/actionInput.spec.ts +155 -0
- package/src/services/actionInput.ts +120 -0
- package/src/services/interactions.spec.ts +175 -0
- package/src/services/interactions.ts +722 -0
- package/src/services/keyboardControls.ts +2 -1
- package/src/services/loadMap.ts +5 -1
- package/src/services/mmorpg-connection.spec.ts +99 -0
- package/src/services/mmorpg-connection.ts +69 -0
- package/src/services/mmorpg.ts +70 -36
- package/src/services/pointerContext.spec.ts +36 -0
- package/src/services/pointerContext.ts +84 -0
- package/src/services/save.spec.ts +127 -0
- package/src/services/standalone-message.ts +7 -0
- package/src/services/standalone.spec.ts +54 -0
- package/src/services/standalone.ts +44 -12
- package/src/utils/getEntityProp.spec.ts +96 -0
- package/src/utils/getEntityProp.ts +4 -3
- package/src/utils/mapId.ts +2 -0
- package/src/utils/readPropValue.ts +16 -0
- package/dist/node_modules/.pnpm/@signe_di@2.9.0/node_modules/@signe/di/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js +0 -457
- package/dist/node_modules/.pnpm/@signe_reactive@2.8.3/node_modules/@signe/reactive/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js +0 -463
- package/dist/node_modules/.pnpm/@signe_reactive@2.9.0/node_modules/@signe/reactive/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js +0 -2191
- package/dist/node_modules/.pnpm/@signe_room@2.9.0/node_modules/@signe/room/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js +0 -10
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/chunk-7QVYU63E.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js +0 -91
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/client/index.js.map +0 -1
- package/dist/node_modules/.pnpm/@signe_sync@2.9.0/node_modules/@signe/sync/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js +0 -14
- package/dist/node_modules/.pnpm/dset@3.1.4/node_modules/dset/dist/index.js.map +0 -1
- package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-HAC622V3.js.map +0 -1
- package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/chunk-S74YV6PU.js.map +0 -1
- package/dist/node_modules/.pnpm/partysocket@1.1.3/node_modules/partysocket/dist/index.js +0 -2
- package/dist/node_modules/.pnpm/zod@3.24.2/node_modules/zod/lib/index.js.map +0 -1
package/src/RpgClientEngine.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import Canvas from "./components/scenes/canvas.ce";
|
|
2
|
+
import BuiltinSceneMap from "./components/scenes/draw-map.ce";
|
|
2
3
|
import { inject } from './core/inject'
|
|
3
|
-
import { signal, bootstrapCanvas, Howl, trigger } from "canvasengine";
|
|
4
|
+
import { signal, bootstrapCanvas, Howl, trigger, type Trigger } from "canvasengine";
|
|
4
5
|
import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket";
|
|
5
6
|
import { LoadMapService, LoadMapToken } from "./services/loadMap";
|
|
6
7
|
import { RpgSound } from "./Sound";
|
|
7
8
|
import { RpgResource } from "./Resource";
|
|
8
|
-
import { Hooks, ModulesToken, Direction } from "@rpgjs/common";
|
|
9
|
+
import { getOrCreateI18nService, Hooks, ModulesToken, Direction, normalizeLightingState, Vector2, type I18nParams, type I18nService } from "@rpgjs/common";
|
|
10
|
+
import type { EventComponentConfig } from "./RpgClient";
|
|
11
|
+
import type { RpgClientEvent } from "./Game/Event";
|
|
9
12
|
import { load } from "@signe/sync";
|
|
10
13
|
import { RpgClientMap } from "./Game/Map"
|
|
11
14
|
import { RpgGui } from "./Gui/Gui";
|
|
@@ -14,24 +17,133 @@ import { lastValueFrom, Observable, combineLatest, BehaviorSubject, filter, swit
|
|
|
14
17
|
import { GlobalConfigToken } from "./module";
|
|
15
18
|
import * as PIXI from "pixi.js";
|
|
16
19
|
import { PrebuiltComponentAnimations } from "./components/animations";
|
|
20
|
+
import TextComponent from "./components/dynamics/text.ce";
|
|
21
|
+
import BarComponent from "./components/dynamics/bar.ce";
|
|
22
|
+
import ShapeComponent from "./components/dynamics/shape.ce";
|
|
23
|
+
import ImageComponent from "./components/dynamics/image.ce";
|
|
17
24
|
import {
|
|
18
25
|
PredictionController,
|
|
19
26
|
type PredictionHistoryEntry,
|
|
20
27
|
type PredictionState,
|
|
28
|
+
type RpgActionInput,
|
|
29
|
+
type RpgActionName,
|
|
30
|
+
type RpgDashInput,
|
|
31
|
+
type RpgMovementInput,
|
|
21
32
|
} from "@rpgjs/common";
|
|
22
33
|
import { NotificationManager } from "./Gui/NotificationManager";
|
|
23
34
|
import { SaveClientService } from "./services/save";
|
|
35
|
+
import { getCanMoveValue } from "./utils/readPropValue";
|
|
36
|
+
import { ProjectileManager, type ClientProjectileImpact, type ClientProjectileSpawn } from "./Game/ProjectileManager";
|
|
37
|
+
import { ClientVisualRegistry, type ClientVisualHandler, type ClientVisualMap, type ClientVisualPacket } from "./Game/ClientVisuals";
|
|
38
|
+
import { normalizeActionInput } from "./services/actionInput";
|
|
39
|
+
import { createClientPointerContext, type ClientPointerContext } from "./services/pointerContext";
|
|
40
|
+
import { RpgClientInteractions } from "./services/interactions";
|
|
41
|
+
import { normalizeRoomMapId } from "./utils/mapId";
|
|
42
|
+
import { EventComponentResolverRegistry, type EventComponentResolver } from "./Game/EventComponentResolver";
|
|
43
|
+
import { RpgClientBuiltinI18n } from "./i18n";
|
|
24
44
|
|
|
25
45
|
interface MovementTrajectoryPoint {
|
|
26
46
|
frame: number;
|
|
27
47
|
tick: number;
|
|
28
48
|
timestamp: number;
|
|
29
|
-
input:
|
|
49
|
+
input: RpgMovementInput;
|
|
30
50
|
x: number;
|
|
31
51
|
y: number;
|
|
32
52
|
direction?: Direction;
|
|
33
53
|
}
|
|
34
54
|
|
|
55
|
+
interface CanvasResizeSize {
|
|
56
|
+
width: number;
|
|
57
|
+
height: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const DEFAULT_DASH_ADDITIONAL_SPEED = 8;
|
|
61
|
+
const DEFAULT_DASH_DURATION_MS = 180;
|
|
62
|
+
const DEFAULT_DASH_COOLDOWN_MS = 450;
|
|
63
|
+
|
|
64
|
+
const isDashInput = (input: RpgMovementInput): input is RpgDashInput =>
|
|
65
|
+
typeof input === "object" && input !== null && input.type === "dash";
|
|
66
|
+
|
|
67
|
+
const isMoveInput = (
|
|
68
|
+
input: RpgMovementInput
|
|
69
|
+
): input is { type: "move"; direction: Direction } =>
|
|
70
|
+
typeof input === "object" && input !== null && input.type === "move";
|
|
71
|
+
|
|
72
|
+
const resolveMoveDirection = (input: RpgMovementInput): Direction | undefined => {
|
|
73
|
+
if (isMoveInput(input)) return input.direction;
|
|
74
|
+
if (typeof input === "string" || typeof input === "number") {
|
|
75
|
+
return input as Direction;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const directionToVector = (direction: Direction | undefined) => {
|
|
81
|
+
switch (direction) {
|
|
82
|
+
case Direction.Left:
|
|
83
|
+
return { x: -1, y: 0 };
|
|
84
|
+
case Direction.Right:
|
|
85
|
+
return { x: 1, y: 0 };
|
|
86
|
+
case Direction.Up:
|
|
87
|
+
return { x: 0, y: -1 };
|
|
88
|
+
case Direction.Down:
|
|
89
|
+
default:
|
|
90
|
+
return { x: 0, y: 1 };
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const vectorToDirection = (direction: { x: number; y: number }): Direction => {
|
|
95
|
+
if (Math.abs(direction.x) > Math.abs(direction.y)) {
|
|
96
|
+
return direction.x < 0 ? Direction.Left : Direction.Right;
|
|
97
|
+
}
|
|
98
|
+
return direction.y < 0 ? Direction.Up : Direction.Down;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const normalizeDashInput = (
|
|
102
|
+
input: Partial<RpgDashInput>,
|
|
103
|
+
fallbackDirection: Direction | undefined
|
|
104
|
+
): RpgDashInput | null => {
|
|
105
|
+
const rawDirection = input.direction ?? directionToVector(fallbackDirection);
|
|
106
|
+
const rawX = Number(rawDirection?.x ?? 0);
|
|
107
|
+
const rawY = Number(rawDirection?.y ?? 0);
|
|
108
|
+
const magnitude = Math.hypot(rawX, rawY);
|
|
109
|
+
if (!Number.isFinite(magnitude) || magnitude <= 0) return null;
|
|
110
|
+
|
|
111
|
+
const additionalSpeed =
|
|
112
|
+
typeof input.additionalSpeed === "number" && Number.isFinite(input.additionalSpeed)
|
|
113
|
+
? Math.max(0, Math.min(input.additionalSpeed, 64))
|
|
114
|
+
: DEFAULT_DASH_ADDITIONAL_SPEED;
|
|
115
|
+
const duration =
|
|
116
|
+
typeof input.duration === "number" && Number.isFinite(input.duration)
|
|
117
|
+
? Math.max(1, Math.min(input.duration, 1000))
|
|
118
|
+
: DEFAULT_DASH_DURATION_MS;
|
|
119
|
+
const cooldown =
|
|
120
|
+
typeof input.cooldown === "number" && Number.isFinite(input.cooldown)
|
|
121
|
+
? Math.max(0, Math.min(input.cooldown, 5000))
|
|
122
|
+
: DEFAULT_DASH_COOLDOWN_MS;
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
type: "dash",
|
|
126
|
+
direction: {
|
|
127
|
+
x: rawX / magnitude,
|
|
128
|
+
y: rawY / magnitude,
|
|
129
|
+
},
|
|
130
|
+
additionalSpeed,
|
|
131
|
+
duration,
|
|
132
|
+
cooldown,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
|
|
137
|
+
start(config?: T): Promise<void>;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
type MapShakeOptions = {
|
|
141
|
+
intensity?: number;
|
|
142
|
+
duration?: number;
|
|
143
|
+
frequency?: number;
|
|
144
|
+
direction?: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
35
147
|
export class RpgClientEngine<T = any> {
|
|
36
148
|
private guiService: RpgGui;
|
|
37
149
|
private webSocket: AbstractWebsocket;
|
|
@@ -41,13 +153,19 @@ export class RpgClientEngine<T = any> {
|
|
|
41
153
|
private selector: HTMLElement;
|
|
42
154
|
public globalConfig: T;
|
|
43
155
|
public sceneComponent: any;
|
|
156
|
+
public sceneMapComponent: any = BuiltinSceneMap;
|
|
44
157
|
stopProcessingInput = false;
|
|
45
158
|
width = signal("100%");
|
|
46
159
|
height = signal("100%");
|
|
47
|
-
spritesheets: Map<string, any> = new Map();
|
|
160
|
+
spritesheets: Map<string | number, any> = new Map();
|
|
161
|
+
private spritesheetPromises: Map<string | number, Promise<any>> = new Map();
|
|
48
162
|
sounds: Map<string, any> = new Map();
|
|
49
163
|
componentAnimations: any[] = [];
|
|
50
|
-
|
|
164
|
+
clientVisuals = new ClientVisualRegistry();
|
|
165
|
+
projectiles: ProjectileManager;
|
|
166
|
+
pointer: ClientPointerContext = createClientPointerContext();
|
|
167
|
+
interactions: RpgClientInteractions = new RpgClientInteractions(this);
|
|
168
|
+
private spritesheetResolver?: (id: string | number) => any | Promise<any>;
|
|
51
169
|
private soundResolver?: (id: string) => any | Promise<any>;
|
|
52
170
|
particleSettings: {
|
|
53
171
|
emitters: any[]
|
|
@@ -61,21 +179,29 @@ export class RpgClientEngine<T = any> {
|
|
|
61
179
|
playerIdSignal = signal<string | null>(null);
|
|
62
180
|
spriteComponentsBehind = signal<any[]>([]);
|
|
63
181
|
spriteComponentsInFront = signal<any[]>([]);
|
|
182
|
+
spriteComponents: Map<string, any> = new Map();
|
|
183
|
+
private eventComponentResolvers = new EventComponentResolverRegistry();
|
|
64
184
|
/** ID of the sprite that the camera should follow. null means follow the current player */
|
|
65
185
|
cameraFollowTargetId = signal<string | null>(null);
|
|
66
186
|
/** Trigger for map shake animation */
|
|
67
|
-
mapShakeTrigger = trigger();
|
|
187
|
+
mapShakeTrigger: ConfigurableTrigger<MapShakeOptions> = trigger<MapShakeOptions>();
|
|
68
188
|
|
|
69
189
|
controlsReady = signal(undefined);
|
|
70
190
|
gamePause = signal(false);
|
|
71
191
|
|
|
72
192
|
private predictionEnabled = false;
|
|
73
|
-
private prediction?: PredictionController<Direction>;
|
|
193
|
+
private prediction?: PredictionController<RpgMovementInput, Direction>;
|
|
74
194
|
private readonly SERVER_CORRECTION_THRESHOLD = 30;
|
|
195
|
+
private localMovementAuthority = false;
|
|
196
|
+
private lastLocalMovementInputAt = 0;
|
|
197
|
+
private readonly LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS = 250;
|
|
75
198
|
private inputFrameCounter = 0;
|
|
76
199
|
private pendingPredictionFrames: number[] = [];
|
|
77
200
|
private lastClientPhysicsStepAt = 0;
|
|
78
201
|
private frameOffset = 0;
|
|
202
|
+
private latestServerTick?: number;
|
|
203
|
+
private latestServerTickAt = 0;
|
|
204
|
+
private dashLockedUntil = 0;
|
|
79
205
|
// Ping/Pong for RTT measurement
|
|
80
206
|
private rtt: number = 0; // Round-trip time in ms
|
|
81
207
|
private pingInterval: any = null;
|
|
@@ -92,17 +218,35 @@ export class RpgClientEngine<T = any> {
|
|
|
92
218
|
private eventsReceived$ = new BehaviorSubject<boolean>(false);
|
|
93
219
|
private onAfterLoadingSubscription?: any;
|
|
94
220
|
private sceneResetQueued = false;
|
|
221
|
+
private mapTransitionInProgress = false;
|
|
222
|
+
private currentMapRoomId?: string;
|
|
223
|
+
private socketListenersInitialized = false;
|
|
224
|
+
private clientReadyForMapChanges = false;
|
|
225
|
+
private pendingMapChanges: any[] = [];
|
|
95
226
|
|
|
96
227
|
// Store subscriptions and event listeners for cleanup
|
|
97
228
|
private tickSubscriptions: any[] = [];
|
|
98
229
|
private resizeHandler?: () => void;
|
|
230
|
+
private pointerMoveHandler?: (event: PointerEvent) => void;
|
|
231
|
+
private pointerUpHandler?: (event: PointerEvent) => void;
|
|
232
|
+
private pointerCancelHandler?: (event: PointerEvent) => void;
|
|
233
|
+
private pointerCanvas?: HTMLCanvasElement;
|
|
234
|
+
private pendingSyncPackets: any[] = [];
|
|
99
235
|
private notificationManager: NotificationManager = new NotificationManager();
|
|
236
|
+
private i18nService: I18nService;
|
|
237
|
+
private locale?: string;
|
|
100
238
|
|
|
101
239
|
constructor(public context) {
|
|
102
240
|
this.webSocket = inject(WebSocketToken);
|
|
103
241
|
this.guiService = inject(RpgGui);
|
|
104
242
|
this.loadMapService = inject(LoadMapToken);
|
|
105
243
|
this.hooks = inject<Hooks>(ModulesToken);
|
|
244
|
+
this.i18nService = getOrCreateI18nService(context);
|
|
245
|
+
this.i18nService.addMessages(RpgClientBuiltinI18n, "rpgjs-client", 0);
|
|
246
|
+
this.projectiles = new ProjectileManager(
|
|
247
|
+
this.hooks,
|
|
248
|
+
(projectile) => this.predictProjectileImpact(projectile),
|
|
249
|
+
);
|
|
106
250
|
this.globalConfig = inject(GlobalConfigToken)
|
|
107
251
|
|
|
108
252
|
if (!this.globalConfig) {
|
|
@@ -123,10 +267,63 @@ export class RpgClientEngine<T = any> {
|
|
|
123
267
|
component: PrebuiltComponentAnimations.Animation
|
|
124
268
|
})
|
|
125
269
|
|
|
270
|
+
this.registerSpriteComponent("rpg:text", TextComponent);
|
|
271
|
+
this.registerSpriteComponent("rpg:hpBar", BarComponent);
|
|
272
|
+
this.registerSpriteComponent("rpg:spBar", BarComponent);
|
|
273
|
+
this.registerSpriteComponent("rpg:bar", BarComponent);
|
|
274
|
+
this.registerSpriteComponent("rpg:shape", ShapeComponent);
|
|
275
|
+
this.registerSpriteComponent("rpg:image", ImageComponent);
|
|
276
|
+
|
|
126
277
|
this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;
|
|
278
|
+
this.localMovementAuthority = this.resolveLocalMovementAuthority();
|
|
127
279
|
this.initializePredictionController();
|
|
128
280
|
}
|
|
129
281
|
|
|
282
|
+
private resolveLocalMovementAuthority(): boolean {
|
|
283
|
+
const predictionConfig = (this.globalConfig as any)?.prediction;
|
|
284
|
+
const configured =
|
|
285
|
+
(this.globalConfig as any)?.movementAuthority ??
|
|
286
|
+
predictionConfig?.movementAuthority ??
|
|
287
|
+
predictionConfig?.authority ??
|
|
288
|
+
predictionConfig?.mode;
|
|
289
|
+
|
|
290
|
+
if (
|
|
291
|
+
configured === "server" ||
|
|
292
|
+
configured === "network" ||
|
|
293
|
+
configured === false
|
|
294
|
+
) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (
|
|
298
|
+
configured === "client" ||
|
|
299
|
+
configured === "local" ||
|
|
300
|
+
configured === true
|
|
301
|
+
) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return this.webSocket.mode === "standalone";
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
setLocale(locale: string) {
|
|
309
|
+
this.locale = locale;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
getLocale(): string {
|
|
313
|
+
return this.locale || this.i18nService.defaultLocale;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
t(key: string, params?: I18nParams): string {
|
|
317
|
+
return this.i18nService.t(key, params, this.getLocale());
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
i18n() {
|
|
321
|
+
return {
|
|
322
|
+
locale: this.getLocale(),
|
|
323
|
+
t: (key: string, params?: I18nParams) => this.t(key, params),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
130
327
|
/**
|
|
131
328
|
* Assigns a CanvasEngine KeyboardControls instance to the dependency injection context
|
|
132
329
|
*
|
|
@@ -173,6 +370,22 @@ export class RpgClientEngine<T = any> {
|
|
|
173
370
|
this.sceneMap = new RpgClientMap()
|
|
174
371
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
175
372
|
this.sceneMap.loadPhysic();
|
|
373
|
+
this.resolveSceneMapComponent();
|
|
374
|
+
|
|
375
|
+
const saveClient = inject(SaveClientService);
|
|
376
|
+
saveClient.initialize();
|
|
377
|
+
this.initListeners();
|
|
378
|
+
this.guiService._initialize();
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
await this.webSocket.connection();
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
this.stopPingPong();
|
|
385
|
+
await this.callConnectError(error);
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
|
|
176
389
|
this.selector = document.body.querySelector("#rpg") as HTMLElement;
|
|
177
390
|
|
|
178
391
|
const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;
|
|
@@ -181,9 +394,11 @@ export class RpgClientEngine<T = any> {
|
|
|
181
394
|
Canvas,
|
|
182
395
|
bootstrapOptions
|
|
183
396
|
);
|
|
397
|
+
this.installCanvasResizeGuard(app);
|
|
184
398
|
this.canvasApp = app;
|
|
185
399
|
this.canvasElement = canvasElement;
|
|
186
|
-
this.renderer = app.renderer as PIXI.Renderer;
|
|
400
|
+
this.renderer = app.renderer as unknown as PIXI.Renderer;
|
|
401
|
+
this.setupPointerTracking();
|
|
187
402
|
this.tick = canvasElement?.propObservables?.context['tick'].observable
|
|
188
403
|
|
|
189
404
|
const inputCheckSubscription = this.tick.subscribe(() => {
|
|
@@ -198,17 +413,23 @@ export class RpgClientEngine<T = any> {
|
|
|
198
413
|
|
|
199
414
|
this.hooks.callHooks("client-spritesheets-load", this).subscribe();
|
|
200
415
|
this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
|
|
416
|
+
this.flushPendingSyncPackets();
|
|
201
417
|
this.hooks.callHooks("client-sounds-load", this).subscribe();
|
|
202
418
|
this.hooks.callHooks("client-soundResolver-load", this).subscribe();
|
|
203
|
-
|
|
419
|
+
|
|
204
420
|
RpgSound.init(this);
|
|
205
421
|
RpgResource.init(this);
|
|
206
422
|
this.hooks.callHooks("client-gui-load", this).subscribe();
|
|
207
423
|
this.hooks.callHooks("client-particles-load", this).subscribe();
|
|
208
424
|
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
425
|
+
this.hooks.callHooks("client-clientVisuals-load", this).subscribe();
|
|
426
|
+
this.hooks.callHooks("client-projectiles-load", this).subscribe();
|
|
427
|
+
this.hooks.callHooks("client-interactions-load", this).subscribe();
|
|
209
428
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
210
429
|
|
|
211
430
|
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
431
|
+
this.clientReadyForMapChanges = true;
|
|
432
|
+
this.flushPendingMapChanges();
|
|
212
433
|
|
|
213
434
|
// wondow is resize
|
|
214
435
|
this.resizeHandler = () => {
|
|
@@ -218,6 +439,7 @@ export class RpgClientEngine<T = any> {
|
|
|
218
439
|
|
|
219
440
|
const tickSubscription = this.tick.subscribe((tick) => {
|
|
220
441
|
this.stepClientPhysicsTick();
|
|
442
|
+
this.projectiles.step();
|
|
221
443
|
this.flushPendingPredictedStates();
|
|
222
444
|
this.flushPendingMovePath();
|
|
223
445
|
this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
|
|
@@ -231,13 +453,157 @@ export class RpgClientEngine<T = any> {
|
|
|
231
453
|
});
|
|
232
454
|
this.tickSubscriptions.push(tickSubscription);
|
|
233
455
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
456
|
+
this.startPingPong();
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private installCanvasResizeGuard(app: any) {
|
|
460
|
+
if (!app || typeof app.resize !== "function") return;
|
|
461
|
+
|
|
462
|
+
const originalResize = app.resize.bind(app);
|
|
463
|
+
app.resize = () => {
|
|
464
|
+
const targetSize = this.readCanvasResizeTargetSize(app);
|
|
465
|
+
const rendererSize = this.readCanvasRendererSize(app);
|
|
466
|
+
|
|
467
|
+
if (
|
|
468
|
+
targetSize &&
|
|
469
|
+
rendererSize &&
|
|
470
|
+
targetSize.width === rendererSize.width &&
|
|
471
|
+
targetSize.height === rendererSize.height
|
|
472
|
+
) {
|
|
473
|
+
this.cancelCanvasResizeFrame(app);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
originalResize();
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
private readCanvasResizeTargetSize(app: any): CanvasResizeSize | null {
|
|
482
|
+
const resizeTarget = app?.resizeTo;
|
|
483
|
+
if (!resizeTarget || typeof window === "undefined") return null;
|
|
484
|
+
|
|
485
|
+
const rawWidth = resizeTarget === window ? window.innerWidth : resizeTarget.clientWidth;
|
|
486
|
+
const rawHeight = resizeTarget === window ? window.innerHeight : resizeTarget.clientHeight;
|
|
487
|
+
const width = Math.round(Number(rawWidth));
|
|
488
|
+
const height = Math.round(Number(rawHeight));
|
|
489
|
+
|
|
490
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) return null;
|
|
491
|
+
return { width, height };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private readCanvasRendererSize(app: any): CanvasResizeSize | null {
|
|
495
|
+
const screen = app?.renderer?.screen;
|
|
496
|
+
const width = Math.round(Number(screen?.width));
|
|
497
|
+
const height = Math.round(Number(screen?.height));
|
|
498
|
+
|
|
499
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
500
|
+
return { width, height };
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private cancelCanvasResizeFrame(app: any) {
|
|
504
|
+
if (typeof app?._cancelResize === "function") {
|
|
505
|
+
app._cancelResize();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private resolveSceneMapComponent() {
|
|
510
|
+
const components = this.hooks.getHookFunctions("client-sceneMap-component");
|
|
511
|
+
const component = components[components.length - 1];
|
|
512
|
+
if (component) {
|
|
513
|
+
this.sceneMapComponent = component;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private setupPointerTracking() {
|
|
518
|
+
const renderer = this.renderer as any;
|
|
519
|
+
const canvas = renderer?.canvas ?? renderer?.view ?? (this.canvasApp as any)?.canvas;
|
|
520
|
+
|
|
521
|
+
if (!canvas || typeof canvas.addEventListener !== "function") {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this.pointerCanvas = canvas;
|
|
526
|
+
const updatePointer = (event: PointerEvent) => {
|
|
527
|
+
const rect = canvas.getBoundingClientRect();
|
|
528
|
+
const screen = {
|
|
529
|
+
x: event.clientX - rect.left,
|
|
530
|
+
y: event.clientY - rect.top,
|
|
531
|
+
};
|
|
532
|
+
const viewport = this.findViewportInstance();
|
|
533
|
+
let world = screen;
|
|
534
|
+
|
|
535
|
+
if (viewport && typeof viewport.toWorld === "function") {
|
|
536
|
+
const point = viewport.toWorld(screen.x, screen.y);
|
|
537
|
+
world = { x: Number(point.x), y: Number(point.y) };
|
|
538
|
+
} else if (viewport && typeof viewport.toLocal === "function") {
|
|
539
|
+
const point = viewport.toLocal(screen);
|
|
540
|
+
world = { x: Number(point.x), y: Number(point.y) };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
this.pointer.update(screen, world);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
this.pointerMoveHandler = (event: PointerEvent) => {
|
|
547
|
+
updatePointer(event);
|
|
548
|
+
this.interactions.handlePointerMove(event);
|
|
549
|
+
};
|
|
550
|
+
this.pointerUpHandler = (event: PointerEvent) => {
|
|
551
|
+
updatePointer(event);
|
|
552
|
+
this.interactions.handlePointerUp(event);
|
|
553
|
+
};
|
|
554
|
+
this.pointerCancelHandler = (event: PointerEvent) => {
|
|
555
|
+
updatePointer(event);
|
|
556
|
+
this.interactions.cancelDrag(event);
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
canvas.addEventListener("pointermove", this.pointerMoveHandler);
|
|
560
|
+
canvas.addEventListener("pointerdown", this.pointerMoveHandler);
|
|
561
|
+
canvas.addEventListener("pointerup", this.pointerUpHandler);
|
|
562
|
+
canvas.addEventListener("pointercancel", this.pointerCancelHandler);
|
|
563
|
+
canvas.addEventListener("pointerleave", this.pointerCancelHandler);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
updatePointerFromInteractionEvent(event: any): void {
|
|
567
|
+
const global = event?.global ?? event?.data?.global;
|
|
568
|
+
|
|
569
|
+
if (!global) {
|
|
570
|
+
this.pointer.updateFromEvent(event);
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const screen = {
|
|
575
|
+
x: Number(global.x),
|
|
576
|
+
y: Number(global.y),
|
|
577
|
+
};
|
|
578
|
+
if (!Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {
|
|
579
|
+
this.pointer.updateFromEvent(event);
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const viewport = this.findViewportInstance();
|
|
584
|
+
if (viewport && typeof viewport.toWorld === "function") {
|
|
585
|
+
const point = viewport.toWorld(screen.x, screen.y);
|
|
586
|
+
this.pointer.update(screen, { x: Number(point.x), y: Number(point.y) });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
this.pointer.update(screen);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
private findViewportInstance(): any {
|
|
594
|
+
const find = (node: any): any => {
|
|
595
|
+
if (!node) return undefined;
|
|
596
|
+
if (typeof node?.toWorld === "function" || node?.constructor?.name === "Viewport") {
|
|
597
|
+
return node;
|
|
598
|
+
}
|
|
599
|
+
for (const child of node.children ?? []) {
|
|
600
|
+
const viewport = find(child);
|
|
601
|
+
if (viewport) return viewport;
|
|
602
|
+
}
|
|
603
|
+
return undefined;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
return find((this.canvasApp as any)?.stage);
|
|
241
607
|
}
|
|
242
608
|
|
|
243
609
|
private prepareSyncPayload(data: any): any {
|
|
@@ -247,8 +613,8 @@ export class RpgClientEngine<T = any> {
|
|
|
247
613
|
|
|
248
614
|
const myId = this.playerIdSignal();
|
|
249
615
|
const players = payload.players;
|
|
250
|
-
const
|
|
251
|
-
|
|
616
|
+
const localPatch = myId && players ? players[myId] : undefined;
|
|
617
|
+
const shouldMaskLocalPosition = this.shouldPreserveLocalPlayerPosition(localPatch);
|
|
252
618
|
if (shouldMaskLocalPosition && myId && players && players[myId]) {
|
|
253
619
|
const localPatch = { ...players[myId] };
|
|
254
620
|
delete localPatch.x;
|
|
@@ -264,6 +630,31 @@ export class RpgClientEngine<T = any> {
|
|
|
264
630
|
return payload;
|
|
265
631
|
}
|
|
266
632
|
|
|
633
|
+
private shouldPreserveLocalPlayerPosition(localPatch?: any): boolean {
|
|
634
|
+
if (!localPatch) {
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
if (this.predictionEnabled && !!this.prediction?.hasPendingInputs()) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
return this.shouldKeepLocalPlayerMovement();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
private shouldKeepLocalPlayerMovement(): boolean {
|
|
644
|
+
if (!this.localMovementAuthority || this.mapTransitionInProgress) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
const myId = this.playerIdSignal();
|
|
648
|
+
const player = myId ? this.sceneMap?.players?.()?.[myId] : undefined;
|
|
649
|
+
if (!player) {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
if (this.prediction?.hasPendingInputs()) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
return Date.now() - this.lastLocalMovementInputAt <= this.LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS;
|
|
656
|
+
}
|
|
657
|
+
|
|
267
658
|
private normalizeAckWithSyncState(
|
|
268
659
|
ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },
|
|
269
660
|
syncData: any,
|
|
@@ -287,52 +678,15 @@ export class RpgClientEngine<T = any> {
|
|
|
287
678
|
}
|
|
288
679
|
|
|
289
680
|
private initListeners() {
|
|
290
|
-
this.
|
|
291
|
-
|
|
292
|
-
this.playerIdSignal.set(data.pId);
|
|
293
|
-
// Signal that player ID was received
|
|
294
|
-
this.playerIdReceived$.next(true);
|
|
295
|
-
}
|
|
681
|
+
if (this.socketListenersInitialized) return;
|
|
682
|
+
this.socketListenersInitialized = true;
|
|
296
683
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
this.
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Apply client-side prediction filtering and server reconciliation
|
|
304
|
-
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
305
|
-
|
|
306
|
-
const ack = data?.ack;
|
|
307
|
-
const normalizedAck =
|
|
308
|
-
ack && typeof ack.frame === "number"
|
|
309
|
-
? this.normalizeAckWithSyncState(ack, data)
|
|
310
|
-
: undefined;
|
|
311
|
-
const payload = this.prepareSyncPayload(data);
|
|
312
|
-
load(this.sceneMap, payload, true);
|
|
313
|
-
|
|
314
|
-
if (normalizedAck) {
|
|
315
|
-
this.applyServerAck(normalizedAck);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
for (const playerId in payload.players ?? {}) {
|
|
319
|
-
const player = payload.players[playerId]
|
|
320
|
-
if (!player._param) continue
|
|
321
|
-
for (const param in player._param) {
|
|
322
|
-
this.sceneMap.players()[playerId]._param()[param] = player._param[param]
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Check if players and events are present in sync data
|
|
327
|
-
const players = payload.players || this.sceneMap.players();
|
|
328
|
-
if (players && Object.keys(players).length > 0) {
|
|
329
|
-
this.playersReceived$.next(true);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const events = payload.events || this.sceneMap.events();
|
|
333
|
-
if (events !== undefined) {
|
|
334
|
-
this.eventsReceived$.next(true);
|
|
684
|
+
this.webSocket.on("sync", (data) => {
|
|
685
|
+
if (!this.tick) {
|
|
686
|
+
this.pendingSyncPackets.push(data);
|
|
687
|
+
return;
|
|
335
688
|
}
|
|
689
|
+
this.applySyncPacket(data);
|
|
336
690
|
});
|
|
337
691
|
|
|
338
692
|
// Handle pong responses for RTT measurement
|
|
@@ -344,6 +698,7 @@ export class RpgClientEngine<T = any> {
|
|
|
344
698
|
// This helps us estimate which server tick corresponds to each client input frame
|
|
345
699
|
const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT
|
|
346
700
|
const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
|
|
701
|
+
this.updateServerTickEstimate(estimatedServerTickNow, now);
|
|
347
702
|
|
|
348
703
|
// Update frame offset (only if we have inputs to calibrate with)
|
|
349
704
|
if (this.inputFrameCounter > 0) {
|
|
@@ -354,11 +709,11 @@ export class RpgClientEngine<T = any> {
|
|
|
354
709
|
});
|
|
355
710
|
|
|
356
711
|
this.webSocket.on("changeMap", (data) => {
|
|
357
|
-
this.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.
|
|
712
|
+
if (!this.clientReadyForMapChanges) {
|
|
713
|
+
this.pendingMapChanges.push(data);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
this.handleChangeMap(data);
|
|
362
717
|
});
|
|
363
718
|
|
|
364
719
|
this.webSocket.on("showComponentAnimation", (data) => {
|
|
@@ -370,19 +725,63 @@ export class RpgClientEngine<T = any> {
|
|
|
370
725
|
this.getComponentAnimation(id).displayEffect(params, player || position)
|
|
371
726
|
});
|
|
372
727
|
|
|
728
|
+
this.webSocket.on("clientVisual", (data) => {
|
|
729
|
+
this.playClientVisual(data);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
this.webSocket.on("projectile:spawnBatch", (data) => {
|
|
733
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
734
|
+
this.projectiles.spawnBatch(data?.projectiles ?? [], {
|
|
735
|
+
mapId: data?.mapId,
|
|
736
|
+
currentServerTick: this.estimateServerTick(),
|
|
737
|
+
tickDurationMs: this.getPhysicsTickDurationMs(),
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
this.webSocket.on("projectile:impactBatch", (data) => {
|
|
742
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
743
|
+
this.projectiles.impactBatch(data?.impacts ?? [], {
|
|
744
|
+
mapId: data?.mapId,
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
this.webSocket.on("projectile:destroyBatch", (data) => {
|
|
749
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
750
|
+
this.projectiles.destroyBatch(data?.projectiles ?? [], {
|
|
751
|
+
mapId: data?.mapId,
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
this.webSocket.on("projectile:clear", (data) => {
|
|
756
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
757
|
+
this.projectiles.clear();
|
|
758
|
+
});
|
|
759
|
+
|
|
373
760
|
this.webSocket.on("notification", (data) => {
|
|
374
761
|
this.notificationManager.add(data);
|
|
375
762
|
});
|
|
376
763
|
|
|
377
|
-
|
|
378
|
-
const {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
764
|
+
this.webSocket.on("setAnimation", (data) => {
|
|
765
|
+
const {
|
|
766
|
+
animationName,
|
|
767
|
+
nbTimes,
|
|
768
|
+
object,
|
|
769
|
+
graphic,
|
|
770
|
+
restoreAnimationName,
|
|
771
|
+
restoreGraphics,
|
|
772
|
+
} = data;
|
|
773
|
+
const player = object ? this.sceneMap.getObjectById(object) : undefined;
|
|
774
|
+
if (!player) return;
|
|
775
|
+
const restoreOptions = {
|
|
776
|
+
restoreAnimationName,
|
|
777
|
+
restoreGraphics,
|
|
778
|
+
};
|
|
779
|
+
if (graphic !== undefined) {
|
|
780
|
+
player.setAnimation(animationName, graphic, nbTimes, restoreOptions);
|
|
781
|
+
} else {
|
|
782
|
+
player.setAnimation(animationName, nbTimes, restoreOptions);
|
|
783
|
+
}
|
|
784
|
+
})
|
|
386
785
|
|
|
387
786
|
this.webSocket.on("playSound", (data) => {
|
|
388
787
|
const { soundId, volume, loop } = data;
|
|
@@ -413,7 +812,7 @@ export class RpgClientEngine<T = any> {
|
|
|
413
812
|
|
|
414
813
|
this.webSocket.on("shakeMap", (data) => {
|
|
415
814
|
const { intensity, duration, frequency, direction } = data || {};
|
|
416
|
-
|
|
815
|
+
this.mapShakeTrigger.start({
|
|
417
816
|
intensity,
|
|
418
817
|
duration,
|
|
419
818
|
frequency,
|
|
@@ -447,6 +846,14 @@ export class RpgClientEngine<T = any> {
|
|
|
447
846
|
});
|
|
448
847
|
});
|
|
449
848
|
|
|
849
|
+
this.webSocket.on("lightingState", (data) => {
|
|
850
|
+
const raw = (data && typeof data === "object" && "value" in data)
|
|
851
|
+
? (data as any).value
|
|
852
|
+
: data;
|
|
853
|
+
|
|
854
|
+
this.sceneMap.lightingState.set(normalizeLightingState(raw));
|
|
855
|
+
});
|
|
856
|
+
|
|
450
857
|
this.webSocket.on('open', () => {
|
|
451
858
|
this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
|
|
452
859
|
// Start ping/pong for synchronization
|
|
@@ -460,10 +867,115 @@ export class RpgClientEngine<T = any> {
|
|
|
460
867
|
})
|
|
461
868
|
|
|
462
869
|
this.webSocket.on('error', (error) => {
|
|
463
|
-
this.
|
|
870
|
+
void this.callConnectError(error);
|
|
464
871
|
})
|
|
465
872
|
}
|
|
466
873
|
|
|
874
|
+
private beginMapTransfer(nextMapId?: string) {
|
|
875
|
+
this.mapTransitionInProgress = true;
|
|
876
|
+
this.currentMapRoomId = nextMapId;
|
|
877
|
+
this.sceneResetQueued = false;
|
|
878
|
+
this.clearClientPredictionStates();
|
|
879
|
+
this.sceneMap.weatherState.set(null);
|
|
880
|
+
this.sceneMap.lightingState.set(null);
|
|
881
|
+
this.sceneMap.clearLightSpots();
|
|
882
|
+
this.clearComponentAnimations();
|
|
883
|
+
this.projectiles.setMapId(nextMapId);
|
|
884
|
+
this.cameraFollowTargetId.set(null);
|
|
885
|
+
this.sceneMap.reset();
|
|
886
|
+
this.sceneMap.loadPhysic();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private clearComponentAnimations() {
|
|
890
|
+
this.componentAnimations.forEach((componentAnimation) => {
|
|
891
|
+
componentAnimation.instance?.clear?.();
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
private shouldProcessProjectilePacket(data: any): boolean {
|
|
896
|
+
if (this.mapTransitionInProgress) return false;
|
|
897
|
+
const packetMapId = normalizeRoomMapId(
|
|
898
|
+
typeof data?.mapId === "string" ? data.mapId : undefined,
|
|
899
|
+
);
|
|
900
|
+
const currentMapId = normalizeRoomMapId(this.currentMapRoomId);
|
|
901
|
+
return !packetMapId || !currentMapId || packetMapId === currentMapId;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private async callConnectError(error: any) {
|
|
905
|
+
await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private flushPendingSyncPackets() {
|
|
909
|
+
const packets = this.pendingSyncPackets;
|
|
910
|
+
this.pendingSyncPackets = [];
|
|
911
|
+
packets.forEach((packet) => this.applySyncPacket(packet));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private flushPendingMapChanges() {
|
|
915
|
+
const packets = this.pendingMapChanges;
|
|
916
|
+
this.pendingMapChanges = [];
|
|
917
|
+
packets.forEach((packet) => this.handleChangeMap(packet));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private handleChangeMap(data: any) {
|
|
921
|
+
const nextMapId = typeof data?.mapId === "string" ? data.mapId : undefined;
|
|
922
|
+
this.beginMapTransfer(nextMapId);
|
|
923
|
+
const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
|
|
924
|
+
this.loadScene(data.mapId, transferToken);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
private applySyncPacket(data: any) {
|
|
928
|
+
if (data.pId) {
|
|
929
|
+
this.playerIdSignal.set(data.pId);
|
|
930
|
+
// Signal that player ID was received
|
|
931
|
+
this.playerIdReceived$.next(true);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
if (this.sceneResetQueued) {
|
|
935
|
+
const weatherState = this.sceneMap.weatherState();
|
|
936
|
+
const lightingState = this.sceneMap.lightingState();
|
|
937
|
+
this.sceneMap.reset();
|
|
938
|
+
this.sceneMap.weatherState.set(weatherState);
|
|
939
|
+
this.sceneMap.lightingState.set(lightingState);
|
|
940
|
+
this.sceneMap.loadPhysic();
|
|
941
|
+
this.sceneResetQueued = false;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// Apply client-side prediction filtering and server reconciliation
|
|
945
|
+
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
946
|
+
|
|
947
|
+
const ack = data?.ack;
|
|
948
|
+
const normalizedAck =
|
|
949
|
+
ack && typeof ack.frame === "number"
|
|
950
|
+
? this.normalizeAckWithSyncState(ack, data)
|
|
951
|
+
: undefined;
|
|
952
|
+
const payload = this.prepareSyncPayload(data);
|
|
953
|
+
load(this.sceneMap, payload, true);
|
|
954
|
+
|
|
955
|
+
if (normalizedAck) {
|
|
956
|
+
this.applyServerAck(normalizedAck);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
for (const playerId in payload.players ?? {}) {
|
|
960
|
+
const player = payload.players[playerId]
|
|
961
|
+
if (!player._param) continue
|
|
962
|
+
for (const param in player._param) {
|
|
963
|
+
this.sceneMap.players()[playerId]._param()[param] = player._param[param]
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Check if players and events are present in sync data
|
|
968
|
+
const players = payload.players || this.sceneMap.players();
|
|
969
|
+
if (players && Object.keys(players).length > 0) {
|
|
970
|
+
this.playersReceived$.next(true);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
const events = payload.events || this.sceneMap.events();
|
|
974
|
+
if (events !== undefined) {
|
|
975
|
+
this.eventsReceived$.next(true);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
467
979
|
/**
|
|
468
980
|
* Start periodic ping/pong for client-server synchronization
|
|
469
981
|
*
|
|
@@ -560,13 +1072,22 @@ export class RpgClientEngine<T = any> {
|
|
|
560
1072
|
room: mapId,
|
|
561
1073
|
query: transferToken ? { transferToken } : undefined,
|
|
562
1074
|
})
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.
|
|
568
|
-
|
|
1075
|
+
try {
|
|
1076
|
+
await this.webSocket.reconnect()
|
|
1077
|
+
}
|
|
1078
|
+
catch (error) {
|
|
1079
|
+
this.mapTransitionInProgress = false;
|
|
1080
|
+
this.stopPingPong();
|
|
1081
|
+
await this.callConnectError(error);
|
|
1082
|
+
throw error;
|
|
1083
|
+
}
|
|
569
1084
|
const res = await this.loadMapService.load(mapId)
|
|
1085
|
+
const loadedLighting = typeof res?.lighting !== "undefined"
|
|
1086
|
+
? res.lighting
|
|
1087
|
+
: res?.data?.lighting;
|
|
1088
|
+
if (typeof loadedLighting !== "undefined") {
|
|
1089
|
+
this.sceneMap.lightingState.set(normalizeLightingState(loadedLighting));
|
|
1090
|
+
}
|
|
570
1091
|
this.sceneMap.data.set(res)
|
|
571
1092
|
|
|
572
1093
|
// Check if playerId is already present
|
|
@@ -587,6 +1108,8 @@ export class RpgClientEngine<T = any> {
|
|
|
587
1108
|
|
|
588
1109
|
// Signal that map loading is completed (this should be last to ensure other checks are done)
|
|
589
1110
|
this.mapLoadCompleted$.next(true);
|
|
1111
|
+
this.currentMapRoomId = mapId;
|
|
1112
|
+
this.mapTransitionInProgress = false;
|
|
590
1113
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
591
1114
|
this.sceneMap.loadPhysic()
|
|
592
1115
|
}
|
|
@@ -623,7 +1146,7 @@ export class RpgClientEngine<T = any> {
|
|
|
623
1146
|
* });
|
|
624
1147
|
* ```
|
|
625
1148
|
*/
|
|
626
|
-
setSpritesheetResolver(resolver: (id: string) => any | Promise<any>): void {
|
|
1149
|
+
setSpritesheetResolver(resolver: (id: string | number) => any | Promise<any>): void {
|
|
627
1150
|
this.spritesheetResolver = resolver;
|
|
628
1151
|
}
|
|
629
1152
|
|
|
@@ -634,7 +1157,7 @@ export class RpgClientEngine<T = any> {
|
|
|
634
1157
|
* If not found and a resolver is set, it calls the resolver to create the spritesheet.
|
|
635
1158
|
* The resolved spritesheet is automatically cached for future use.
|
|
636
1159
|
*
|
|
637
|
-
* @param id - The spritesheet ID to retrieve
|
|
1160
|
+
* @param id - The spritesheet ID or legacy tile ID to retrieve
|
|
638
1161
|
* @returns The spritesheet if found or created, or undefined if not found and no resolver
|
|
639
1162
|
* @returns Promise<any> if the resolver is asynchronous
|
|
640
1163
|
*
|
|
@@ -647,7 +1170,7 @@ export class RpgClientEngine<T = any> {
|
|
|
647
1170
|
* const spritesheet = await engine.getSpriteSheet('dynamic-sprite');
|
|
648
1171
|
* ```
|
|
649
1172
|
*/
|
|
650
|
-
getSpriteSheet(id: string): any | Promise<any> {
|
|
1173
|
+
getSpriteSheet(id: string | number): any | Promise<any> {
|
|
651
1174
|
// Check cache first
|
|
652
1175
|
if (this.spritesheets.has(id)) {
|
|
653
1176
|
return this.spritesheets.get(id);
|
|
@@ -655,17 +1178,29 @@ export class RpgClientEngine<T = any> {
|
|
|
655
1178
|
|
|
656
1179
|
// If not in cache and resolver exists, use it
|
|
657
1180
|
if (this.spritesheetResolver) {
|
|
1181
|
+
if (this.spritesheetPromises.has(id)) {
|
|
1182
|
+
return this.spritesheetPromises.get(id);
|
|
1183
|
+
}
|
|
1184
|
+
|
|
658
1185
|
const result = this.spritesheetResolver(id);
|
|
659
1186
|
|
|
660
1187
|
// Check if result is a Promise
|
|
661
1188
|
if (result instanceof Promise) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1189
|
+
const promise = result
|
|
1190
|
+
.then((spritesheet) => {
|
|
1191
|
+
if (spritesheet) {
|
|
1192
|
+
// Cache the resolved spritesheet
|
|
1193
|
+
this.spritesheets.set(id, spritesheet);
|
|
1194
|
+
}
|
|
1195
|
+
this.spritesheetPromises.delete(id);
|
|
1196
|
+
return spritesheet;
|
|
1197
|
+
})
|
|
1198
|
+
.catch((error) => {
|
|
1199
|
+
this.spritesheetPromises.delete(id);
|
|
1200
|
+
throw error;
|
|
1201
|
+
});
|
|
1202
|
+
this.spritesheetPromises.set(id, promise);
|
|
1203
|
+
return promise;
|
|
669
1204
|
} else {
|
|
670
1205
|
// Synchronous result
|
|
671
1206
|
if (result) {
|
|
@@ -1081,6 +1616,104 @@ export class RpgClientEngine<T = any> {
|
|
|
1081
1616
|
return component
|
|
1082
1617
|
}
|
|
1083
1618
|
|
|
1619
|
+
/**
|
|
1620
|
+
* Register a reusable sprite component that can be addressed by the server.
|
|
1621
|
+
*
|
|
1622
|
+
* Server-side component definitions only carry the component id and
|
|
1623
|
+
* serializable props. The client registry maps that id to the CanvasEngine
|
|
1624
|
+
* component that performs the actual rendering.
|
|
1625
|
+
*
|
|
1626
|
+
* @param id - Stable component id used by server component definitions
|
|
1627
|
+
* @param component - CanvasEngine component to render for this id
|
|
1628
|
+
* @returns The registered component
|
|
1629
|
+
*
|
|
1630
|
+
* @example
|
|
1631
|
+
* ```ts
|
|
1632
|
+
* engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);
|
|
1633
|
+
* ```
|
|
1634
|
+
*/
|
|
1635
|
+
registerSpriteComponent(id: string, component: any) {
|
|
1636
|
+
this.spriteComponents.set(id, component);
|
|
1637
|
+
return component;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* Get a reusable sprite component by id.
|
|
1642
|
+
*
|
|
1643
|
+
* @param id - Component id registered on the client
|
|
1644
|
+
* @returns The CanvasEngine component, or undefined when missing
|
|
1645
|
+
*/
|
|
1646
|
+
getSpriteComponent(id: string) {
|
|
1647
|
+
return this.spriteComponents.get(id);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* Register a custom event component resolver.
|
|
1652
|
+
*
|
|
1653
|
+
* The last resolver returning a component wins. This lets later modules
|
|
1654
|
+
* override earlier defaults without replacing the whole map scene.
|
|
1655
|
+
*
|
|
1656
|
+
* @param resolver - Function receiving the synced event object
|
|
1657
|
+
* @returns The registered resolver
|
|
1658
|
+
*/
|
|
1659
|
+
addEventComponentResolver(resolver: EventComponentResolver) {
|
|
1660
|
+
return this.eventComponentResolvers.add(resolver);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
/**
|
|
1664
|
+
* Resolve the custom CanvasEngine component for an event, if any.
|
|
1665
|
+
*
|
|
1666
|
+
* @param event - Synced client event object
|
|
1667
|
+
* @returns The component/config returned by the last matching resolver
|
|
1668
|
+
*/
|
|
1669
|
+
resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null {
|
|
1670
|
+
return this.eventComponentResolvers.resolve(event);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
registerProjectileComponent(type: string, component: any) {
|
|
1674
|
+
return this.projectiles.register(type, component);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
getProjectileComponent(type: string) {
|
|
1678
|
+
return this.projectiles.get(type);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
/**
|
|
1682
|
+
* Register a named client visual macro.
|
|
1683
|
+
*
|
|
1684
|
+
* Client visuals are small client-side functions that group existing visual
|
|
1685
|
+
* primitives such as flash, sound, component animations, sprite animation, or
|
|
1686
|
+
* map shake. The server sends only the visual name and a serializable payload.
|
|
1687
|
+
*
|
|
1688
|
+
* @param name - Stable visual name sent by the server
|
|
1689
|
+
* @param handler - Client-side visual handler
|
|
1690
|
+
* @returns The registered handler
|
|
1691
|
+
*/
|
|
1692
|
+
registerClientVisual(name: string, handler: ClientVisualHandler) {
|
|
1693
|
+
return this.clientVisuals.register(name, handler);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Register several named client visual macros.
|
|
1698
|
+
*
|
|
1699
|
+
* @param visuals - Map of visual names to client-side handlers
|
|
1700
|
+
*/
|
|
1701
|
+
registerClientVisuals(visuals: ClientVisualMap) {
|
|
1702
|
+
this.clientVisuals.registerMany(visuals);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/**
|
|
1706
|
+
* Play a registered client visual locally.
|
|
1707
|
+
*
|
|
1708
|
+
* This is also used by the websocket listener when the server calls
|
|
1709
|
+
* `player.clientVisual()` or `map.clientVisual()`.
|
|
1710
|
+
*
|
|
1711
|
+
* @param packet - Visual name and serializable payload
|
|
1712
|
+
*/
|
|
1713
|
+
playClientVisual(packet: ClientVisualPacket) {
|
|
1714
|
+
return this.clientVisuals.play(packet, this);
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1084
1717
|
/**
|
|
1085
1718
|
* Add a component animation to the engine
|
|
1086
1719
|
*
|
|
@@ -1164,21 +1797,59 @@ export class RpgClientEngine<T = any> {
|
|
|
1164
1797
|
* duration: 1000,
|
|
1165
1798
|
* onFinish: () => console.log('Fade complete')
|
|
1166
1799
|
* });
|
|
1800
|
+
*
|
|
1801
|
+
* // Wait until the transition component calls onFinish
|
|
1802
|
+
* await engine.startTransition('fade', { duration: 1000 });
|
|
1167
1803
|
* ```
|
|
1168
1804
|
*/
|
|
1169
|
-
startTransition(id: string, props: any = {}) {
|
|
1805
|
+
startTransition(id: string, props: any = {}): Promise<void> {
|
|
1170
1806
|
if (!this.guiService.exists(id)) {
|
|
1171
1807
|
throw new Error(`Transition with id ${id} not found. Make sure to add it using engine.addTransition() or in your module's transitions property.`);
|
|
1172
1808
|
}
|
|
1173
|
-
|
|
1809
|
+
return new Promise<void>((resolve) => {
|
|
1810
|
+
let finished = false;
|
|
1811
|
+
const finish = (data?: any) => {
|
|
1812
|
+
if (finished) return;
|
|
1813
|
+
finished = true;
|
|
1814
|
+
props?.onFinish?.(data);
|
|
1815
|
+
resolve();
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
this.guiService.display(id, {
|
|
1819
|
+
...props,
|
|
1820
|
+
onFinish: finish,
|
|
1821
|
+
});
|
|
1822
|
+
});
|
|
1174
1823
|
}
|
|
1175
1824
|
|
|
1176
|
-
async processInput({ input }: { input:
|
|
1825
|
+
async processInput({ input }: { input: RpgMovementInput }) {
|
|
1826
|
+
if (this.stopProcessingInput) return;
|
|
1827
|
+
|
|
1828
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1829
|
+
const canMove =
|
|
1830
|
+
!currentPlayer ||
|
|
1831
|
+
getCanMoveValue(currentPlayer);
|
|
1832
|
+
if (!canMove) {
|
|
1833
|
+
this.interruptCurrentPlayerMovement(currentPlayer);
|
|
1834
|
+
return;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1177
1837
|
const timestamp = Date.now();
|
|
1838
|
+
const movementInput = isDashInput(input)
|
|
1839
|
+
? normalizeDashInput(input, currentPlayer?.direction?.())
|
|
1840
|
+
: input;
|
|
1841
|
+
if (!movementInput) return;
|
|
1842
|
+
if (isDashInput(movementInput)) {
|
|
1843
|
+
const cooldown = movementInput.cooldown ?? DEFAULT_DASH_COOLDOWN_MS;
|
|
1844
|
+
if (timestamp < this.dashLockedUntil) return;
|
|
1845
|
+
this.dashLockedUntil = timestamp + cooldown;
|
|
1846
|
+
}
|
|
1847
|
+
this.lastLocalMovementInputAt = timestamp;
|
|
1848
|
+
|
|
1178
1849
|
let frame: number;
|
|
1179
1850
|
let tick: number;
|
|
1180
1851
|
if (this.predictionEnabled && this.prediction) {
|
|
1181
|
-
const meta = this.prediction.recordInput(
|
|
1852
|
+
const meta = this.prediction.recordInput(movementInput, timestamp);
|
|
1182
1853
|
frame = meta.frame;
|
|
1183
1854
|
tick = meta.tick;
|
|
1184
1855
|
} else {
|
|
@@ -1186,13 +1857,11 @@ export class RpgClientEngine<T = any> {
|
|
|
1186
1857
|
tick = this.getPhysicsTick();
|
|
1187
1858
|
}
|
|
1188
1859
|
this.inputFrameCounter = frame;
|
|
1189
|
-
this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
|
|
1860
|
+
this.hooks.callHooks("client-engine-onInput", this, { input: movementInput, playerId: this.playerId }).subscribe();
|
|
1190
1861
|
|
|
1191
|
-
const currentPlayer = this.sceneMap.getCurrentPlayer();
|
|
1192
1862
|
const bodyReady = this.ensureCurrentPlayerBody();
|
|
1193
1863
|
if (currentPlayer && bodyReady) {
|
|
1194
|
-
|
|
1195
|
-
(this.sceneMap as any).moveBody(currentPlayer, input);
|
|
1864
|
+
this.applyPredictedMovementInput(currentPlayer, movementInput);
|
|
1196
1865
|
if (this.predictionEnabled && this.prediction) {
|
|
1197
1866
|
this.pendingPredictionFrames.push(frame);
|
|
1198
1867
|
if (this.pendingPredictionFrames.length > 240) {
|
|
@@ -1201,14 +1870,42 @@ export class RpgClientEngine<T = any> {
|
|
|
1201
1870
|
}
|
|
1202
1871
|
}
|
|
1203
1872
|
|
|
1204
|
-
this.emitMovePacket(
|
|
1205
|
-
this.lastInputTime =
|
|
1873
|
+
this.emitMovePacket(movementInput, frame, tick, timestamp, true);
|
|
1874
|
+
this.lastInputTime = isDashInput(movementInput)
|
|
1875
|
+
? Date.now() + (movementInput.duration ?? DEFAULT_DASH_DURATION_MS)
|
|
1876
|
+
: Date.now();
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
async processDash(input: Partial<RpgDashInput> = {}) {
|
|
1880
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1881
|
+
const fallbackDirection =
|
|
1882
|
+
typeof currentPlayer?.direction === "function"
|
|
1883
|
+
? currentPlayer.direction()
|
|
1884
|
+
: currentPlayer?.direction;
|
|
1885
|
+
const dashInput = normalizeDashInput(input, fallbackDirection);
|
|
1886
|
+
if (!dashInput) return;
|
|
1887
|
+
await this.processInput({ input: dashInput });
|
|
1206
1888
|
}
|
|
1207
1889
|
|
|
1208
|
-
processAction(
|
|
1890
|
+
processAction(action: RpgActionName, data?: any): void;
|
|
1891
|
+
processAction(action: RpgActionInput): void;
|
|
1892
|
+
processAction(action: RpgActionName | RpgActionInput, data?: any): void {
|
|
1209
1893
|
if (this.stopProcessingInput) return;
|
|
1210
|
-
|
|
1211
|
-
|
|
1894
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1895
|
+
const canMove =
|
|
1896
|
+
!currentPlayer ||
|
|
1897
|
+
getCanMoveValue(currentPlayer);
|
|
1898
|
+
if (!canMove) return;
|
|
1899
|
+
|
|
1900
|
+
const payload = normalizeActionInput(action as any, data);
|
|
1901
|
+
|
|
1902
|
+
this.hooks.callHooks("client-engine-onInput", this, {
|
|
1903
|
+
input: payload.action,
|
|
1904
|
+
action: payload.action,
|
|
1905
|
+
data: payload.data,
|
|
1906
|
+
playerId: this.playerId,
|
|
1907
|
+
}).subscribe();
|
|
1908
|
+
this.webSocket.emit('action', payload)
|
|
1212
1909
|
}
|
|
1213
1910
|
|
|
1214
1911
|
get PIXI() {
|
|
@@ -1227,8 +1924,76 @@ export class RpgClientEngine<T = any> {
|
|
|
1227
1924
|
return this.sceneMap
|
|
1228
1925
|
}
|
|
1229
1926
|
|
|
1927
|
+
getObjectById(id: string) {
|
|
1928
|
+
return this.sceneMap?.getObjectById(id);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1230
1931
|
private getPhysicsTick(): number {
|
|
1231
|
-
return this.sceneMap?.getTick?.() ?? 0;
|
|
1932
|
+
return (this.sceneMap as any)?.getTick?.() ?? 0;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
private getPhysicsTickDurationMs(): number {
|
|
1936
|
+
const timeStep = (this.sceneMap as any)?.physic?.getWorld?.()?.getTimeStep?.();
|
|
1937
|
+
return typeof timeStep === "number" && Number.isFinite(timeStep) && timeStep > 0
|
|
1938
|
+
? timeStep * 1000
|
|
1939
|
+
: 1000 / 60;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
private updateServerTickEstimate(serverTick: number | undefined, now = Date.now()): void {
|
|
1943
|
+
if (typeof serverTick !== "number" || !Number.isFinite(serverTick)) {
|
|
1944
|
+
return;
|
|
1945
|
+
}
|
|
1946
|
+
this.latestServerTick = serverTick;
|
|
1947
|
+
this.latestServerTickAt = now;
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
private estimateServerTick(now = Date.now()): number | undefined {
|
|
1951
|
+
if (typeof this.latestServerTick !== "number" || this.latestServerTickAt <= 0) {
|
|
1952
|
+
return undefined;
|
|
1953
|
+
}
|
|
1954
|
+
const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());
|
|
1955
|
+
return this.latestServerTick + elapsedTicks;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
private predictProjectileImpact(projectile: ClientProjectileSpawn): ClientProjectileImpact | null {
|
|
1959
|
+
if (projectile.predictImpact === false) {
|
|
1960
|
+
return null;
|
|
1961
|
+
}
|
|
1962
|
+
const sceneMap = this.sceneMap as any;
|
|
1963
|
+
if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) {
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
const origin = projectile.origin;
|
|
1967
|
+
const direction = projectile.direction;
|
|
1968
|
+
if (
|
|
1969
|
+
!origin ||
|
|
1970
|
+
!direction ||
|
|
1971
|
+
!Number.isFinite(origin.x) ||
|
|
1972
|
+
!Number.isFinite(origin.y) ||
|
|
1973
|
+
!Number.isFinite(direction.x) ||
|
|
1974
|
+
!Number.isFinite(direction.y) ||
|
|
1975
|
+
(direction.x === 0 && direction.y === 0)
|
|
1976
|
+
) {
|
|
1977
|
+
return null;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
const hit = sceneMap.physic.raycast(
|
|
1981
|
+
new Vector2(origin.x, origin.y),
|
|
1982
|
+
new Vector2(direction.x, direction.y),
|
|
1983
|
+
projectile.range,
|
|
1984
|
+
projectile.collisionMask,
|
|
1985
|
+
(entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId,
|
|
1986
|
+
);
|
|
1987
|
+
if (!hit) {
|
|
1988
|
+
return null;
|
|
1989
|
+
}
|
|
1990
|
+
return {
|
|
1991
|
+
id: projectile.id,
|
|
1992
|
+
targetId: hit.entity.uuid,
|
|
1993
|
+
x: hit.point.x,
|
|
1994
|
+
y: hit.point.y,
|
|
1995
|
+
distance: hit.distance,
|
|
1996
|
+
};
|
|
1232
1997
|
}
|
|
1233
1998
|
|
|
1234
1999
|
private ensureCurrentPlayerBody(): boolean {
|
|
@@ -1295,7 +2060,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1295
2060
|
input: entry.direction,
|
|
1296
2061
|
x: state.x,
|
|
1297
2062
|
y: state.y,
|
|
1298
|
-
direction: state.direction ?? entry.direction,
|
|
2063
|
+
direction: state.direction ?? resolveMoveDirection(entry.direction),
|
|
1299
2064
|
});
|
|
1300
2065
|
}
|
|
1301
2066
|
if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
|
|
@@ -1305,7 +2070,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1305
2070
|
}
|
|
1306
2071
|
|
|
1307
2072
|
private emitMovePacket(
|
|
1308
|
-
input:
|
|
2073
|
+
input: RpgMovementInput,
|
|
1309
2074
|
frame: number,
|
|
1310
2075
|
tick: number,
|
|
1311
2076
|
timestamp: number,
|
|
@@ -1337,6 +2102,14 @@ export class RpgClientEngine<T = any> {
|
|
|
1337
2102
|
if (!this.predictionEnabled || !this.prediction) {
|
|
1338
2103
|
return;
|
|
1339
2104
|
}
|
|
2105
|
+
const player = this.sceneMap?.getCurrentPlayer?.() as any;
|
|
2106
|
+
if (
|
|
2107
|
+
player &&
|
|
2108
|
+
!getCanMoveValue(player)
|
|
2109
|
+
) {
|
|
2110
|
+
this.interruptCurrentPlayerMovement(player);
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
1340
2113
|
const pendingInputs = this.prediction.getPendingInputs();
|
|
1341
2114
|
if (pendingInputs.length === 0) {
|
|
1342
2115
|
return;
|
|
@@ -1352,6 +2125,22 @@ export class RpgClientEngine<T = any> {
|
|
|
1352
2125
|
this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
|
|
1353
2126
|
}
|
|
1354
2127
|
|
|
2128
|
+
private applyPredictedMovementInput(
|
|
2129
|
+
player: any,
|
|
2130
|
+
input: RpgMovementInput
|
|
2131
|
+
): boolean {
|
|
2132
|
+
if (isDashInput(input)) {
|
|
2133
|
+
const direction = vectorToDirection(input.direction);
|
|
2134
|
+
player.changeDirection(direction);
|
|
2135
|
+
return Boolean((this.sceneMap as any).dashBody?.(player, input));
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
const direction = resolveMoveDirection(input);
|
|
2139
|
+
if (!direction) return false;
|
|
2140
|
+
player.changeDirection(direction);
|
|
2141
|
+
return Boolean((this.sceneMap as any).moveBody?.(player, direction));
|
|
2142
|
+
}
|
|
2143
|
+
|
|
1355
2144
|
private getLocalPlayerState(): PredictionState<Direction> {
|
|
1356
2145
|
const currentPlayer = this.sceneMap?.getCurrentPlayer();
|
|
1357
2146
|
if (!currentPlayer) {
|
|
@@ -1395,7 +2184,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1395
2184
|
? configuredMaxEntries
|
|
1396
2185
|
: Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
|
|
1397
2186
|
this.sceneMap?.configureClientPrediction?.(true);
|
|
1398
|
-
this.prediction = new PredictionController<Direction>({
|
|
2187
|
+
this.prediction = new PredictionController<RpgMovementInput, Direction>({
|
|
1399
2188
|
correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
|
|
1400
2189
|
historyTtlMs,
|
|
1401
2190
|
maxHistoryEntries,
|
|
@@ -1475,6 +2264,34 @@ export class RpgClientEngine<T = any> {
|
|
|
1475
2264
|
this.lastMovePathSentFrame = 0;
|
|
1476
2265
|
}
|
|
1477
2266
|
|
|
2267
|
+
/**
|
|
2268
|
+
* Stop local movement immediately and discard pending predicted movement.
|
|
2269
|
+
*
|
|
2270
|
+
* Use this before a blocking action such as an A-RPG attack, dialog, dash
|
|
2271
|
+
* startup, or any client-side state where already buffered movement inputs
|
|
2272
|
+
* must not be replayed after server reconciliation.
|
|
2273
|
+
*
|
|
2274
|
+
* @param player - Player object to stop. Defaults to the current player.
|
|
2275
|
+
* @returns `true` when a player was found and interrupted.
|
|
2276
|
+
*
|
|
2277
|
+
* @example
|
|
2278
|
+
* ```ts
|
|
2279
|
+
* engine.interruptCurrentPlayerMovement();
|
|
2280
|
+
* ```
|
|
2281
|
+
*/
|
|
2282
|
+
interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {
|
|
2283
|
+
if (!player) {
|
|
2284
|
+
return false;
|
|
2285
|
+
}
|
|
2286
|
+
(this.sceneMap as any)?.stopMovement?.(player);
|
|
2287
|
+
this.prediction?.clearPendingInputs();
|
|
2288
|
+
this.pendingPredictionFrames = [];
|
|
2289
|
+
this.lastInputTime = 0;
|
|
2290
|
+
this.lastMovePathSentAt = Date.now();
|
|
2291
|
+
this.lastMovePathSentFrame = this.inputFrameCounter;
|
|
2292
|
+
return true;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
1478
2295
|
/**
|
|
1479
2296
|
* Trigger a flash animation on a sprite
|
|
1480
2297
|
*
|
|
@@ -1542,12 +2359,14 @@ export class RpgClientEngine<T = any> {
|
|
|
1542
2359
|
}
|
|
1543
2360
|
|
|
1544
2361
|
private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
|
|
2362
|
+
this.updateServerTickEstimate(ack.serverTick);
|
|
2363
|
+
const keepLocalMovement = this.shouldKeepLocalPlayerMovement();
|
|
1545
2364
|
if (this.predictionEnabled && this.prediction) {
|
|
1546
2365
|
const result = this.prediction.applyServerAck({
|
|
1547
2366
|
frame: ack.frame,
|
|
1548
2367
|
serverTick: ack.serverTick,
|
|
1549
2368
|
state:
|
|
1550
|
-
typeof ack.x === "number" && typeof ack.y === "number"
|
|
2369
|
+
!keepLocalMovement && typeof ack.x === "number" && typeof ack.y === "number"
|
|
1551
2370
|
? { x: ack.x, y: ack.y, direction: ack.direction }
|
|
1552
2371
|
: undefined,
|
|
1553
2372
|
});
|
|
@@ -1560,7 +2379,10 @@ export class RpgClientEngine<T = any> {
|
|
|
1560
2379
|
if (typeof ack.x !== "number" || typeof ack.y !== "number") {
|
|
1561
2380
|
return;
|
|
1562
2381
|
}
|
|
1563
|
-
|
|
2382
|
+
if (keepLocalMovement) {
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
const player = this.getCurrentPlayer() as any;
|
|
1564
2386
|
const myId = this.playerIdSignal();
|
|
1565
2387
|
if (!player || !myId) {
|
|
1566
2388
|
return;
|
|
@@ -1581,12 +2403,16 @@ export class RpgClientEngine<T = any> {
|
|
|
1581
2403
|
|
|
1582
2404
|
private reconcilePrediction(
|
|
1583
2405
|
authoritativeState: PredictionState<Direction>,
|
|
1584
|
-
pendingInputs: PredictionHistoryEntry<Direction>[],
|
|
2406
|
+
pendingInputs: PredictionHistoryEntry<RpgMovementInput, Direction>[],
|
|
1585
2407
|
): void {
|
|
1586
|
-
const player = this.getCurrentPlayer();
|
|
2408
|
+
const player = this.getCurrentPlayer() as any;
|
|
1587
2409
|
if (!player) {
|
|
1588
2410
|
return;
|
|
1589
2411
|
}
|
|
2412
|
+
if (!getCanMoveValue(player)) {
|
|
2413
|
+
this.interruptCurrentPlayerMovement(player);
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
1590
2416
|
|
|
1591
2417
|
(this.sceneMap as any).stopMovement(player);
|
|
1592
2418
|
this.applyAuthoritativeState(authoritativeState);
|
|
@@ -1599,7 +2425,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1599
2425
|
const replayInputs = pendingInputs.slice(-600);
|
|
1600
2426
|
for (const entry of replayInputs) {
|
|
1601
2427
|
if (!entry?.direction) continue;
|
|
1602
|
-
|
|
2428
|
+
this.applyPredictedMovementInput(player, entry.direction);
|
|
1603
2429
|
this.sceneMap.stepPredictionTick();
|
|
1604
2430
|
this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
|
|
1605
2431
|
}
|
|
@@ -1699,6 +2525,22 @@ export class RpgClientEngine<T = any> {
|
|
|
1699
2525
|
this.resizeHandler = undefined;
|
|
1700
2526
|
}
|
|
1701
2527
|
|
|
2528
|
+
if (this.pointerMoveHandler && this.pointerCanvas) {
|
|
2529
|
+
this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);
|
|
2530
|
+
this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);
|
|
2531
|
+
if (this.pointerUpHandler) {
|
|
2532
|
+
this.pointerCanvas.removeEventListener('pointerup', this.pointerUpHandler);
|
|
2533
|
+
}
|
|
2534
|
+
if (this.pointerCancelHandler) {
|
|
2535
|
+
this.pointerCanvas.removeEventListener('pointercancel', this.pointerCancelHandler);
|
|
2536
|
+
this.pointerCanvas.removeEventListener('pointerleave', this.pointerCancelHandler);
|
|
2537
|
+
}
|
|
2538
|
+
this.pointerMoveHandler = undefined;
|
|
2539
|
+
this.pointerUpHandler = undefined;
|
|
2540
|
+
this.pointerCancelHandler = undefined;
|
|
2541
|
+
this.pointerCanvas = undefined;
|
|
2542
|
+
}
|
|
2543
|
+
|
|
1702
2544
|
// Destroy PIXI app and renderer if they exist
|
|
1703
2545
|
// Destroy the app first, which will destroy the renderer
|
|
1704
2546
|
// Store renderer reference before destroying app (since app.destroy() will destroy the renderer)
|
|
@@ -1772,6 +2614,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1772
2614
|
this.cameraFollowTargetId.set(null);
|
|
1773
2615
|
this.spriteComponentsBehind.set([]);
|
|
1774
2616
|
this.spriteComponentsInFront.set([]);
|
|
2617
|
+
this.eventComponentResolvers.clear();
|
|
1775
2618
|
|
|
1776
2619
|
// Clear maps and arrays
|
|
1777
2620
|
this.spritesheets.clear();
|