@rpgjs/client 5.0.0-beta.2 → 5.0.0-beta.22
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 +130 -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 +132 -57
- 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 +191 -19
- package/dist/RpgClientEngine.js +693 -74
- 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 +748 -249
- 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.d.ts +51 -2
- package/dist/components/gui/mobile/index.js +14 -6
- package/dist/components/gui/mobile/index.js.map +1 -1
- package/dist/components/gui/mobile/index.spec.d.ts +1 -0
- package/dist/components/gui/mobile/mobile.ce.js +309 -59
- 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 +8 -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/cameraFollow.d.ts +51 -0
- package/dist/services/cameraFollow.js +134 -0
- package/dist/services/cameraFollow.js.map +1 -0
- package/dist/services/cameraFollow.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 +359 -0
- package/src/Gui/Gui.ts +178 -67
- package/src/Resource.ts +1 -2
- package/src/RpgClient.ts +220 -17
- package/src/RpgClientEngine.ts +995 -133
- package/src/components/animations/fx.ce +101 -0
- package/src/components/animations/index.ts +4 -2
- package/src/components/character.ce +839 -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/mobile/index.spec.ts +94 -0
- package/src/components/gui/mobile/index.ts +74 -6
- package/src/components/gui/mobile/mobile.ce +347 -65
- 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 +19 -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/cameraFollow.spec.ts +220 -0
- package/src/services/cameraFollow.ts +222 -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,139 @@ 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";
|
|
44
|
+
import type { CameraFollowSmoothMove } from "./services/cameraFollow";
|
|
45
|
+
export type {
|
|
46
|
+
CameraFollowEase,
|
|
47
|
+
CameraFollowSmoothMove,
|
|
48
|
+
CameraFollowSmoothMoveOptions,
|
|
49
|
+
} from "./services/cameraFollow";
|
|
24
50
|
|
|
25
51
|
interface MovementTrajectoryPoint {
|
|
26
52
|
frame: number;
|
|
27
53
|
tick: number;
|
|
28
54
|
timestamp: number;
|
|
29
|
-
input:
|
|
55
|
+
input: RpgMovementInput;
|
|
30
56
|
x: number;
|
|
31
57
|
y: number;
|
|
32
58
|
direction?: Direction;
|
|
33
59
|
}
|
|
34
60
|
|
|
61
|
+
interface CanvasResizeSize {
|
|
62
|
+
width: number;
|
|
63
|
+
height: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const DEFAULT_DASH_ADDITIONAL_SPEED = 8;
|
|
67
|
+
const DEFAULT_DASH_DURATION_MS = 180;
|
|
68
|
+
const DEFAULT_DASH_COOLDOWN_MS = 450;
|
|
69
|
+
|
|
70
|
+
const isDashInput = (input: RpgMovementInput): input is RpgDashInput =>
|
|
71
|
+
typeof input === "object" && input !== null && input.type === "dash";
|
|
72
|
+
|
|
73
|
+
const isMoveInput = (
|
|
74
|
+
input: RpgMovementInput
|
|
75
|
+
): input is { type: "move"; direction: Direction } =>
|
|
76
|
+
typeof input === "object" && input !== null && input.type === "move";
|
|
77
|
+
|
|
78
|
+
const resolveMoveDirection = (input: RpgMovementInput): Direction | undefined => {
|
|
79
|
+
if (isMoveInput(input)) return input.direction;
|
|
80
|
+
if (typeof input === "string" || typeof input === "number") {
|
|
81
|
+
return input as Direction;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const directionToVector = (direction: Direction | undefined) => {
|
|
87
|
+
switch (direction) {
|
|
88
|
+
case Direction.Left:
|
|
89
|
+
return { x: -1, y: 0 };
|
|
90
|
+
case Direction.Right:
|
|
91
|
+
return { x: 1, y: 0 };
|
|
92
|
+
case Direction.Up:
|
|
93
|
+
return { x: 0, y: -1 };
|
|
94
|
+
case Direction.Down:
|
|
95
|
+
default:
|
|
96
|
+
return { x: 0, y: 1 };
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const vectorToDirection = (direction: { x: number; y: number }): Direction => {
|
|
101
|
+
if (Math.abs(direction.x) > Math.abs(direction.y)) {
|
|
102
|
+
return direction.x < 0 ? Direction.Left : Direction.Right;
|
|
103
|
+
}
|
|
104
|
+
return direction.y < 0 ? Direction.Up : Direction.Down;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const normalizeDashInput = (
|
|
108
|
+
input: Partial<RpgDashInput>,
|
|
109
|
+
fallbackDirection: Direction | undefined
|
|
110
|
+
): RpgDashInput | null => {
|
|
111
|
+
const rawDirection = input.direction ?? directionToVector(fallbackDirection);
|
|
112
|
+
const rawX = Number(rawDirection?.x ?? 0);
|
|
113
|
+
const rawY = Number(rawDirection?.y ?? 0);
|
|
114
|
+
const magnitude = Math.hypot(rawX, rawY);
|
|
115
|
+
if (!Number.isFinite(magnitude) || magnitude <= 0) return null;
|
|
116
|
+
|
|
117
|
+
const additionalSpeed =
|
|
118
|
+
typeof input.additionalSpeed === "number" && Number.isFinite(input.additionalSpeed)
|
|
119
|
+
? Math.max(0, Math.min(input.additionalSpeed, 64))
|
|
120
|
+
: DEFAULT_DASH_ADDITIONAL_SPEED;
|
|
121
|
+
const duration =
|
|
122
|
+
typeof input.duration === "number" && Number.isFinite(input.duration)
|
|
123
|
+
? Math.max(1, Math.min(input.duration, 1000))
|
|
124
|
+
: DEFAULT_DASH_DURATION_MS;
|
|
125
|
+
const cooldown =
|
|
126
|
+
typeof input.cooldown === "number" && Number.isFinite(input.cooldown)
|
|
127
|
+
? Math.max(0, Math.min(input.cooldown, 5000))
|
|
128
|
+
: DEFAULT_DASH_COOLDOWN_MS;
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
type: "dash",
|
|
132
|
+
direction: {
|
|
133
|
+
x: rawX / magnitude,
|
|
134
|
+
y: rawY / magnitude,
|
|
135
|
+
},
|
|
136
|
+
additionalSpeed,
|
|
137
|
+
duration,
|
|
138
|
+
cooldown,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
type ConfigurableTrigger<T> = Omit<Trigger<T>, "start"> & {
|
|
143
|
+
start(config?: T): Promise<void>;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
type MapShakeOptions = {
|
|
147
|
+
intensity?: number;
|
|
148
|
+
duration?: number;
|
|
149
|
+
frequency?: number;
|
|
150
|
+
direction?: string;
|
|
151
|
+
};
|
|
152
|
+
|
|
35
153
|
export class RpgClientEngine<T = any> {
|
|
36
154
|
private guiService: RpgGui;
|
|
37
155
|
private webSocket: AbstractWebsocket;
|
|
@@ -41,13 +159,19 @@ export class RpgClientEngine<T = any> {
|
|
|
41
159
|
private selector: HTMLElement;
|
|
42
160
|
public globalConfig: T;
|
|
43
161
|
public sceneComponent: any;
|
|
162
|
+
public sceneMapComponent: any = BuiltinSceneMap;
|
|
44
163
|
stopProcessingInput = false;
|
|
45
164
|
width = signal("100%");
|
|
46
165
|
height = signal("100%");
|
|
47
|
-
spritesheets: Map<string, any> = new Map();
|
|
166
|
+
spritesheets: Map<string | number, any> = new Map();
|
|
167
|
+
private spritesheetPromises: Map<string | number, Promise<any>> = new Map();
|
|
48
168
|
sounds: Map<string, any> = new Map();
|
|
49
169
|
componentAnimations: any[] = [];
|
|
50
|
-
|
|
170
|
+
clientVisuals = new ClientVisualRegistry();
|
|
171
|
+
projectiles: ProjectileManager;
|
|
172
|
+
pointer: ClientPointerContext = createClientPointerContext();
|
|
173
|
+
interactions: RpgClientInteractions = new RpgClientInteractions(this);
|
|
174
|
+
private spritesheetResolver?: (id: string | number) => any | Promise<any>;
|
|
51
175
|
private soundResolver?: (id: string) => any | Promise<any>;
|
|
52
176
|
particleSettings: {
|
|
53
177
|
emitters: any[]
|
|
@@ -61,21 +185,33 @@ export class RpgClientEngine<T = any> {
|
|
|
61
185
|
playerIdSignal = signal<string | null>(null);
|
|
62
186
|
spriteComponentsBehind = signal<any[]>([]);
|
|
63
187
|
spriteComponentsInFront = signal<any[]>([]);
|
|
188
|
+
spriteComponents: Map<string, any> = new Map();
|
|
189
|
+
private eventComponentResolvers = new EventComponentResolverRegistry();
|
|
64
190
|
/** ID of the sprite that the camera should follow. null means follow the current player */
|
|
65
191
|
cameraFollowTargetId = signal<string | null>(null);
|
|
192
|
+
/** Camera follow transition options used by character components when the target changes */
|
|
193
|
+
cameraFollowSmoothMove: CameraFollowSmoothMove = false;
|
|
194
|
+
/** Incremented for each camera follow command so repeated commands on the same target are applied */
|
|
195
|
+
cameraFollowRevision = signal(0);
|
|
66
196
|
/** Trigger for map shake animation */
|
|
67
|
-
mapShakeTrigger = trigger();
|
|
197
|
+
mapShakeTrigger: ConfigurableTrigger<MapShakeOptions> = trigger<MapShakeOptions>();
|
|
68
198
|
|
|
69
|
-
controlsReady = signal(undefined);
|
|
199
|
+
controlsReady = signal<boolean | undefined>(undefined);
|
|
70
200
|
gamePause = signal(false);
|
|
71
201
|
|
|
72
202
|
private predictionEnabled = false;
|
|
73
|
-
private prediction?: PredictionController<Direction>;
|
|
203
|
+
private prediction?: PredictionController<RpgMovementInput, Direction>;
|
|
74
204
|
private readonly SERVER_CORRECTION_THRESHOLD = 30;
|
|
205
|
+
private localMovementAuthority = false;
|
|
206
|
+
private lastLocalMovementInputAt = 0;
|
|
207
|
+
private readonly LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS = 250;
|
|
75
208
|
private inputFrameCounter = 0;
|
|
76
209
|
private pendingPredictionFrames: number[] = [];
|
|
77
210
|
private lastClientPhysicsStepAt = 0;
|
|
78
211
|
private frameOffset = 0;
|
|
212
|
+
private latestServerTick?: number;
|
|
213
|
+
private latestServerTickAt = 0;
|
|
214
|
+
private dashLockedUntil = 0;
|
|
79
215
|
// Ping/Pong for RTT measurement
|
|
80
216
|
private rtt: number = 0; // Round-trip time in ms
|
|
81
217
|
private pingInterval: any = null;
|
|
@@ -92,17 +228,35 @@ export class RpgClientEngine<T = any> {
|
|
|
92
228
|
private eventsReceived$ = new BehaviorSubject<boolean>(false);
|
|
93
229
|
private onAfterLoadingSubscription?: any;
|
|
94
230
|
private sceneResetQueued = false;
|
|
231
|
+
private mapTransitionInProgress = false;
|
|
232
|
+
private currentMapRoomId?: string;
|
|
233
|
+
private socketListenersInitialized = false;
|
|
234
|
+
private clientReadyForMapChanges = false;
|
|
235
|
+
private pendingMapChanges: any[] = [];
|
|
95
236
|
|
|
96
237
|
// Store subscriptions and event listeners for cleanup
|
|
97
238
|
private tickSubscriptions: any[] = [];
|
|
98
239
|
private resizeHandler?: () => void;
|
|
240
|
+
private pointerMoveHandler?: (event: PointerEvent) => void;
|
|
241
|
+
private pointerUpHandler?: (event: PointerEvent) => void;
|
|
242
|
+
private pointerCancelHandler?: (event: PointerEvent) => void;
|
|
243
|
+
private pointerCanvas?: HTMLCanvasElement;
|
|
244
|
+
private pendingSyncPackets: any[] = [];
|
|
99
245
|
private notificationManager: NotificationManager = new NotificationManager();
|
|
246
|
+
private i18nService: I18nService;
|
|
247
|
+
private locale?: string;
|
|
100
248
|
|
|
101
249
|
constructor(public context) {
|
|
102
250
|
this.webSocket = inject(WebSocketToken);
|
|
103
251
|
this.guiService = inject(RpgGui);
|
|
104
252
|
this.loadMapService = inject(LoadMapToken);
|
|
105
253
|
this.hooks = inject<Hooks>(ModulesToken);
|
|
254
|
+
this.i18nService = getOrCreateI18nService(context);
|
|
255
|
+
this.i18nService.addMessages(RpgClientBuiltinI18n, "rpgjs-client", 0);
|
|
256
|
+
this.projectiles = new ProjectileManager(
|
|
257
|
+
this.hooks,
|
|
258
|
+
(projectile) => this.predictProjectileImpact(projectile),
|
|
259
|
+
);
|
|
106
260
|
this.globalConfig = inject(GlobalConfigToken)
|
|
107
261
|
|
|
108
262
|
if (!this.globalConfig) {
|
|
@@ -123,10 +277,63 @@ export class RpgClientEngine<T = any> {
|
|
|
123
277
|
component: PrebuiltComponentAnimations.Animation
|
|
124
278
|
})
|
|
125
279
|
|
|
280
|
+
this.registerSpriteComponent("rpg:text", TextComponent);
|
|
281
|
+
this.registerSpriteComponent("rpg:hpBar", BarComponent);
|
|
282
|
+
this.registerSpriteComponent("rpg:spBar", BarComponent);
|
|
283
|
+
this.registerSpriteComponent("rpg:bar", BarComponent);
|
|
284
|
+
this.registerSpriteComponent("rpg:shape", ShapeComponent);
|
|
285
|
+
this.registerSpriteComponent("rpg:image", ImageComponent);
|
|
286
|
+
|
|
126
287
|
this.predictionEnabled = (this.globalConfig as any)?.prediction?.enabled !== false;
|
|
288
|
+
this.localMovementAuthority = this.resolveLocalMovementAuthority();
|
|
127
289
|
this.initializePredictionController();
|
|
128
290
|
}
|
|
129
291
|
|
|
292
|
+
private resolveLocalMovementAuthority(): boolean {
|
|
293
|
+
const predictionConfig = (this.globalConfig as any)?.prediction;
|
|
294
|
+
const configured =
|
|
295
|
+
(this.globalConfig as any)?.movementAuthority ??
|
|
296
|
+
predictionConfig?.movementAuthority ??
|
|
297
|
+
predictionConfig?.authority ??
|
|
298
|
+
predictionConfig?.mode;
|
|
299
|
+
|
|
300
|
+
if (
|
|
301
|
+
configured === "server" ||
|
|
302
|
+
configured === "network" ||
|
|
303
|
+
configured === false
|
|
304
|
+
) {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (
|
|
308
|
+
configured === "client" ||
|
|
309
|
+
configured === "local" ||
|
|
310
|
+
configured === true
|
|
311
|
+
) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return this.webSocket.mode === "standalone";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
setLocale(locale: string) {
|
|
319
|
+
this.locale = locale;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
getLocale(): string {
|
|
323
|
+
return this.locale || this.i18nService.defaultLocale;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
t(key: string, params?: I18nParams): string {
|
|
327
|
+
return this.i18nService.t(key, params, this.getLocale());
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
i18n() {
|
|
331
|
+
return {
|
|
332
|
+
locale: this.getLocale(),
|
|
333
|
+
t: (key: string, params?: I18nParams) => this.t(key, params),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
130
337
|
/**
|
|
131
338
|
* Assigns a CanvasEngine KeyboardControls instance to the dependency injection context
|
|
132
339
|
*
|
|
@@ -166,13 +373,29 @@ export class RpgClientEngine<T = any> {
|
|
|
166
373
|
...currentValues,
|
|
167
374
|
values: new Map([['__default__', controlInstance]])
|
|
168
375
|
}
|
|
169
|
-
this.controlsReady.set(
|
|
376
|
+
this.controlsReady.set(true);
|
|
170
377
|
}
|
|
171
378
|
|
|
172
379
|
async start() {
|
|
173
380
|
this.sceneMap = new RpgClientMap()
|
|
174
381
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
175
382
|
this.sceneMap.loadPhysic();
|
|
383
|
+
this.resolveSceneMapComponent();
|
|
384
|
+
|
|
385
|
+
const saveClient = inject(SaveClientService);
|
|
386
|
+
saveClient.initialize();
|
|
387
|
+
this.initListeners();
|
|
388
|
+
this.guiService._initialize();
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
await this.webSocket.connection();
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
this.stopPingPong();
|
|
395
|
+
await this.callConnectError(error);
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
|
|
176
399
|
this.selector = document.body.querySelector("#rpg") as HTMLElement;
|
|
177
400
|
|
|
178
401
|
const bootstrapOptions = (this.globalConfig as any)?.bootstrapCanvasOptions;
|
|
@@ -181,9 +404,11 @@ export class RpgClientEngine<T = any> {
|
|
|
181
404
|
Canvas,
|
|
182
405
|
bootstrapOptions
|
|
183
406
|
);
|
|
407
|
+
this.installCanvasResizeGuard(app);
|
|
184
408
|
this.canvasApp = app;
|
|
185
409
|
this.canvasElement = canvasElement;
|
|
186
|
-
this.renderer = app.renderer as PIXI.Renderer;
|
|
410
|
+
this.renderer = app.renderer as unknown as PIXI.Renderer;
|
|
411
|
+
this.setupPointerTracking();
|
|
187
412
|
this.tick = canvasElement?.propObservables?.context['tick'].observable
|
|
188
413
|
|
|
189
414
|
const inputCheckSubscription = this.tick.subscribe(() => {
|
|
@@ -198,17 +423,23 @@ export class RpgClientEngine<T = any> {
|
|
|
198
423
|
|
|
199
424
|
this.hooks.callHooks("client-spritesheets-load", this).subscribe();
|
|
200
425
|
this.hooks.callHooks("client-spritesheetResolver-load", this).subscribe();
|
|
426
|
+
this.flushPendingSyncPackets();
|
|
201
427
|
this.hooks.callHooks("client-sounds-load", this).subscribe();
|
|
202
428
|
this.hooks.callHooks("client-soundResolver-load", this).subscribe();
|
|
203
|
-
|
|
429
|
+
|
|
204
430
|
RpgSound.init(this);
|
|
205
431
|
RpgResource.init(this);
|
|
206
432
|
this.hooks.callHooks("client-gui-load", this).subscribe();
|
|
207
433
|
this.hooks.callHooks("client-particles-load", this).subscribe();
|
|
208
434
|
this.hooks.callHooks("client-componentAnimations-load", this).subscribe();
|
|
435
|
+
this.hooks.callHooks("client-clientVisuals-load", this).subscribe();
|
|
436
|
+
this.hooks.callHooks("client-projectiles-load", this).subscribe();
|
|
437
|
+
this.hooks.callHooks("client-interactions-load", this).subscribe();
|
|
209
438
|
this.hooks.callHooks("client-sprite-load", this).subscribe();
|
|
210
439
|
|
|
211
440
|
await lastValueFrom(this.hooks.callHooks("client-engine-onStart", this));
|
|
441
|
+
this.clientReadyForMapChanges = true;
|
|
442
|
+
this.flushPendingMapChanges();
|
|
212
443
|
|
|
213
444
|
// wondow is resize
|
|
214
445
|
this.resizeHandler = () => {
|
|
@@ -218,6 +449,7 @@ export class RpgClientEngine<T = any> {
|
|
|
218
449
|
|
|
219
450
|
const tickSubscription = this.tick.subscribe((tick) => {
|
|
220
451
|
this.stepClientPhysicsTick();
|
|
452
|
+
this.projectiles.step();
|
|
221
453
|
this.flushPendingPredictedStates();
|
|
222
454
|
this.flushPendingMovePath();
|
|
223
455
|
this.hooks.callHooks("client-engine-onStep", this, tick).subscribe();
|
|
@@ -231,13 +463,163 @@ export class RpgClientEngine<T = any> {
|
|
|
231
463
|
});
|
|
232
464
|
this.tickSubscriptions.push(tickSubscription);
|
|
233
465
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
466
|
+
this.startPingPong();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private installCanvasResizeGuard(app: any) {
|
|
470
|
+
if (!app || typeof app.resize !== "function") return;
|
|
471
|
+
|
|
472
|
+
const originalResize = app.resize.bind(app);
|
|
473
|
+
app.resize = () => {
|
|
474
|
+
const targetSize = this.readCanvasResizeTargetSize(app);
|
|
475
|
+
const rendererSize = this.readCanvasRendererSize(app);
|
|
476
|
+
|
|
477
|
+
if (
|
|
478
|
+
targetSize &&
|
|
479
|
+
rendererSize &&
|
|
480
|
+
targetSize.width === rendererSize.width &&
|
|
481
|
+
targetSize.height === rendererSize.height
|
|
482
|
+
) {
|
|
483
|
+
this.cancelCanvasResizeFrame(app);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
originalResize();
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private readCanvasResizeTargetSize(app: any): CanvasResizeSize | null {
|
|
492
|
+
const resizeTarget = app?.resizeTo;
|
|
493
|
+
if (!resizeTarget || typeof window === "undefined") return null;
|
|
494
|
+
|
|
495
|
+
const rawWidth = resizeTarget === window ? window.innerWidth : resizeTarget.clientWidth;
|
|
496
|
+
const rawHeight = resizeTarget === window ? window.innerHeight : resizeTarget.clientHeight;
|
|
497
|
+
const width = Math.round(Number(rawWidth));
|
|
498
|
+
const height = Math.round(Number(rawHeight));
|
|
499
|
+
|
|
500
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 0 || height < 0) return null;
|
|
501
|
+
return { width, height };
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private readCanvasRendererSize(app: any): CanvasResizeSize | null {
|
|
505
|
+
const screen = app?.renderer?.screen;
|
|
506
|
+
const width = Math.round(Number(screen?.width));
|
|
507
|
+
const height = Math.round(Number(screen?.height));
|
|
508
|
+
|
|
509
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
|
|
510
|
+
return { width, height };
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private cancelCanvasResizeFrame(app: any) {
|
|
514
|
+
if (typeof app?._cancelResize === "function") {
|
|
515
|
+
app._cancelResize();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
private resolveSceneMapComponent() {
|
|
520
|
+
const components = this.hooks.getHookFunctions("client-sceneMap-component");
|
|
521
|
+
const component = components[components.length - 1];
|
|
522
|
+
if (component) {
|
|
523
|
+
this.sceneMapComponent = component;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
private setupPointerTracking() {
|
|
528
|
+
const renderer = this.renderer as any;
|
|
529
|
+
const canvas = renderer?.canvas ?? renderer?.view ?? (this.canvasApp as any)?.canvas;
|
|
530
|
+
|
|
531
|
+
if (!canvas || typeof canvas.addEventListener !== "function") {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
this.pointerCanvas = canvas;
|
|
536
|
+
const updatePointer = (event: PointerEvent) => {
|
|
537
|
+
const rect = canvas.getBoundingClientRect();
|
|
538
|
+
const screen = {
|
|
539
|
+
x: event.clientX - rect.left,
|
|
540
|
+
y: event.clientY - rect.top,
|
|
541
|
+
};
|
|
542
|
+
const viewport = this.findViewportInstance();
|
|
543
|
+
let world = screen;
|
|
544
|
+
|
|
545
|
+
if (viewport && typeof viewport.toWorld === "function") {
|
|
546
|
+
const point = viewport.toWorld(screen.x, screen.y);
|
|
547
|
+
world = { x: Number(point.x), y: Number(point.y) };
|
|
548
|
+
} else if (viewport && typeof viewport.toLocal === "function") {
|
|
549
|
+
const point = viewport.toLocal(screen);
|
|
550
|
+
world = { x: Number(point.x), y: Number(point.y) };
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this.pointer.update(screen, world);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
this.pointerMoveHandler = (event: PointerEvent) => {
|
|
557
|
+
updatePointer(event);
|
|
558
|
+
this.interactions.handlePointerMove(event);
|
|
559
|
+
};
|
|
560
|
+
this.pointerUpHandler = (event: PointerEvent) => {
|
|
561
|
+
updatePointer(event);
|
|
562
|
+
this.interactions.handlePointerUp(event);
|
|
563
|
+
};
|
|
564
|
+
this.pointerCancelHandler = (event: PointerEvent) => {
|
|
565
|
+
updatePointer(event);
|
|
566
|
+
this.interactions.cancelDrag(event);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
canvas.addEventListener("pointermove", this.pointerMoveHandler);
|
|
570
|
+
canvas.addEventListener("pointerdown", this.pointerMoveHandler);
|
|
571
|
+
canvas.addEventListener("pointerup", this.pointerUpHandler);
|
|
572
|
+
canvas.addEventListener("pointercancel", this.pointerCancelHandler);
|
|
573
|
+
canvas.addEventListener("pointerleave", this.pointerCancelHandler);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
updatePointerFromInteractionEvent(event: any): void {
|
|
577
|
+
const global = event?.global ?? event?.data?.global;
|
|
578
|
+
|
|
579
|
+
if (!global) {
|
|
580
|
+
this.pointer.updateFromEvent(event);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const screen = {
|
|
585
|
+
x: Number(global.x),
|
|
586
|
+
y: Number(global.y),
|
|
587
|
+
};
|
|
588
|
+
if (!Number.isFinite(screen.x) || !Number.isFinite(screen.y)) {
|
|
589
|
+
this.pointer.updateFromEvent(event);
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const viewport = this.findViewportInstance();
|
|
594
|
+
if (viewport && typeof viewport.toWorld === "function") {
|
|
595
|
+
const point = viewport.toWorld(screen.x, screen.y);
|
|
596
|
+
this.pointer.update(screen, { x: Number(point.x), y: Number(point.y) });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
this.pointer.update(screen);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private findViewportInstance(): any {
|
|
604
|
+
const find = (node: any): any => {
|
|
605
|
+
if (!node) return undefined;
|
|
606
|
+
if (typeof node?.toWorld === "function" || node?.constructor?.name === "Viewport") {
|
|
607
|
+
return node;
|
|
608
|
+
}
|
|
609
|
+
for (const child of node.children ?? []) {
|
|
610
|
+
const viewport = find(child);
|
|
611
|
+
if (viewport) return viewport;
|
|
612
|
+
}
|
|
613
|
+
return undefined;
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
return find((this.canvasApp as any)?.stage);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private clearCameraFollowViewportPlugins(): void {
|
|
620
|
+
const viewport = this.findViewportInstance();
|
|
621
|
+
viewport?.plugins?.remove?.("animate");
|
|
622
|
+
viewport?.plugins?.remove?.("follow");
|
|
241
623
|
}
|
|
242
624
|
|
|
243
625
|
private prepareSyncPayload(data: any): any {
|
|
@@ -247,8 +629,8 @@ export class RpgClientEngine<T = any> {
|
|
|
247
629
|
|
|
248
630
|
const myId = this.playerIdSignal();
|
|
249
631
|
const players = payload.players;
|
|
250
|
-
const
|
|
251
|
-
|
|
632
|
+
const localPatch = myId && players ? players[myId] : undefined;
|
|
633
|
+
const shouldMaskLocalPosition = this.shouldPreserveLocalPlayerPosition(localPatch);
|
|
252
634
|
if (shouldMaskLocalPosition && myId && players && players[myId]) {
|
|
253
635
|
const localPatch = { ...players[myId] };
|
|
254
636
|
delete localPatch.x;
|
|
@@ -264,6 +646,31 @@ export class RpgClientEngine<T = any> {
|
|
|
264
646
|
return payload;
|
|
265
647
|
}
|
|
266
648
|
|
|
649
|
+
private shouldPreserveLocalPlayerPosition(localPatch?: any): boolean {
|
|
650
|
+
if (!localPatch) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
if (this.predictionEnabled && !!this.prediction?.hasPendingInputs()) {
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
return this.shouldKeepLocalPlayerMovement();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
private shouldKeepLocalPlayerMovement(): boolean {
|
|
660
|
+
if (!this.localMovementAuthority || this.mapTransitionInProgress) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
const myId = this.playerIdSignal();
|
|
664
|
+
const player = myId ? this.sceneMap?.players?.()?.[myId] : undefined;
|
|
665
|
+
if (!player) {
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (this.prediction?.hasPendingInputs()) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
return Date.now() - this.lastLocalMovementInputAt <= this.LOCAL_MOVEMENT_AUTHORITY_ACK_GRACE_MS;
|
|
672
|
+
}
|
|
673
|
+
|
|
267
674
|
private normalizeAckWithSyncState(
|
|
268
675
|
ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction },
|
|
269
676
|
syncData: any,
|
|
@@ -287,52 +694,15 @@ export class RpgClientEngine<T = any> {
|
|
|
287
694
|
}
|
|
288
695
|
|
|
289
696
|
private initListeners() {
|
|
290
|
-
this.
|
|
291
|
-
|
|
292
|
-
this.playerIdSignal.set(data.pId);
|
|
293
|
-
// Signal that player ID was received
|
|
294
|
-
this.playerIdReceived$.next(true);
|
|
295
|
-
}
|
|
697
|
+
if (this.socketListenersInitialized) return;
|
|
698
|
+
this.socketListenersInitialized = true;
|
|
296
699
|
|
|
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);
|
|
700
|
+
this.webSocket.on("sync", (data) => {
|
|
701
|
+
if (!this.tick) {
|
|
702
|
+
this.pendingSyncPackets.push(data);
|
|
703
|
+
return;
|
|
335
704
|
}
|
|
705
|
+
this.applySyncPacket(data);
|
|
336
706
|
});
|
|
337
707
|
|
|
338
708
|
// Handle pong responses for RTT measurement
|
|
@@ -344,6 +714,7 @@ export class RpgClientEngine<T = any> {
|
|
|
344
714
|
// This helps us estimate which server tick corresponds to each client input frame
|
|
345
715
|
const estimatedTicksInFlight = Math.floor(this.rtt / 2 / (1000 / 60)); // Estimate ticks during half RTT
|
|
346
716
|
const estimatedServerTickNow = data.serverTick + estimatedTicksInFlight;
|
|
717
|
+
this.updateServerTickEstimate(estimatedServerTickNow, now);
|
|
347
718
|
|
|
348
719
|
// Update frame offset (only if we have inputs to calibrate with)
|
|
349
720
|
if (this.inputFrameCounter > 0) {
|
|
@@ -354,11 +725,11 @@ export class RpgClientEngine<T = any> {
|
|
|
354
725
|
});
|
|
355
726
|
|
|
356
727
|
this.webSocket.on("changeMap", (data) => {
|
|
357
|
-
this.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
this.
|
|
728
|
+
if (!this.clientReadyForMapChanges) {
|
|
729
|
+
this.pendingMapChanges.push(data);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
this.handleChangeMap(data);
|
|
362
733
|
});
|
|
363
734
|
|
|
364
735
|
this.webSocket.on("showComponentAnimation", (data) => {
|
|
@@ -370,19 +741,63 @@ export class RpgClientEngine<T = any> {
|
|
|
370
741
|
this.getComponentAnimation(id).displayEffect(params, player || position)
|
|
371
742
|
});
|
|
372
743
|
|
|
744
|
+
this.webSocket.on("clientVisual", (data) => {
|
|
745
|
+
this.playClientVisual(data);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
this.webSocket.on("projectile:spawnBatch", (data) => {
|
|
749
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
750
|
+
this.projectiles.spawnBatch(data?.projectiles ?? [], {
|
|
751
|
+
mapId: data?.mapId,
|
|
752
|
+
currentServerTick: this.estimateServerTick(),
|
|
753
|
+
tickDurationMs: this.getPhysicsTickDurationMs(),
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
this.webSocket.on("projectile:impactBatch", (data) => {
|
|
758
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
759
|
+
this.projectiles.impactBatch(data?.impacts ?? [], {
|
|
760
|
+
mapId: data?.mapId,
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
this.webSocket.on("projectile:destroyBatch", (data) => {
|
|
765
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
766
|
+
this.projectiles.destroyBatch(data?.projectiles ?? [], {
|
|
767
|
+
mapId: data?.mapId,
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
this.webSocket.on("projectile:clear", (data) => {
|
|
772
|
+
if (!this.shouldProcessProjectilePacket(data)) return;
|
|
773
|
+
this.projectiles.clear();
|
|
774
|
+
});
|
|
775
|
+
|
|
373
776
|
this.webSocket.on("notification", (data) => {
|
|
374
777
|
this.notificationManager.add(data);
|
|
375
778
|
});
|
|
376
779
|
|
|
377
|
-
|
|
378
|
-
const {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
780
|
+
this.webSocket.on("setAnimation", (data) => {
|
|
781
|
+
const {
|
|
782
|
+
animationName,
|
|
783
|
+
nbTimes,
|
|
784
|
+
object,
|
|
785
|
+
graphic,
|
|
786
|
+
restoreAnimationName,
|
|
787
|
+
restoreGraphics,
|
|
788
|
+
} = data;
|
|
789
|
+
const player = object ? this.sceneMap.getObjectById(object) : undefined;
|
|
790
|
+
if (!player) return;
|
|
791
|
+
const restoreOptions = {
|
|
792
|
+
restoreAnimationName,
|
|
793
|
+
restoreGraphics,
|
|
794
|
+
};
|
|
795
|
+
if (graphic !== undefined) {
|
|
796
|
+
player.setAnimation(animationName, graphic, nbTimes, restoreOptions);
|
|
797
|
+
} else {
|
|
798
|
+
player.setAnimation(animationName, nbTimes, restoreOptions);
|
|
799
|
+
}
|
|
800
|
+
})
|
|
386
801
|
|
|
387
802
|
this.webSocket.on("playSound", (data) => {
|
|
388
803
|
const { soundId, volume, loop } = data;
|
|
@@ -413,7 +828,7 @@ export class RpgClientEngine<T = any> {
|
|
|
413
828
|
|
|
414
829
|
this.webSocket.on("shakeMap", (data) => {
|
|
415
830
|
const { intensity, duration, frequency, direction } = data || {};
|
|
416
|
-
|
|
831
|
+
this.mapShakeTrigger.start({
|
|
417
832
|
intensity,
|
|
418
833
|
duration,
|
|
419
834
|
frequency,
|
|
@@ -447,6 +862,14 @@ export class RpgClientEngine<T = any> {
|
|
|
447
862
|
});
|
|
448
863
|
});
|
|
449
864
|
|
|
865
|
+
this.webSocket.on("lightingState", (data) => {
|
|
866
|
+
const raw = (data && typeof data === "object" && "value" in data)
|
|
867
|
+
? (data as any).value
|
|
868
|
+
: data;
|
|
869
|
+
|
|
870
|
+
this.sceneMap.lightingState.set(normalizeLightingState(raw));
|
|
871
|
+
});
|
|
872
|
+
|
|
450
873
|
this.webSocket.on('open', () => {
|
|
451
874
|
this.hooks.callHooks("client-engine-onConnected", this, this.socket).subscribe();
|
|
452
875
|
// Start ping/pong for synchronization
|
|
@@ -460,10 +883,115 @@ export class RpgClientEngine<T = any> {
|
|
|
460
883
|
})
|
|
461
884
|
|
|
462
885
|
this.webSocket.on('error', (error) => {
|
|
463
|
-
this.
|
|
886
|
+
void this.callConnectError(error);
|
|
464
887
|
})
|
|
465
888
|
}
|
|
466
889
|
|
|
890
|
+
private beginMapTransfer(nextMapId?: string) {
|
|
891
|
+
this.mapTransitionInProgress = true;
|
|
892
|
+
this.currentMapRoomId = nextMapId;
|
|
893
|
+
this.sceneResetQueued = false;
|
|
894
|
+
this.clearClientPredictionStates();
|
|
895
|
+
this.sceneMap.weatherState.set(null);
|
|
896
|
+
this.sceneMap.lightingState.set(null);
|
|
897
|
+
this.sceneMap.clearLightSpots();
|
|
898
|
+
this.clearComponentAnimations();
|
|
899
|
+
this.projectiles.setMapId(nextMapId);
|
|
900
|
+
this.resetCameraFollow(false);
|
|
901
|
+
this.sceneMap.reset();
|
|
902
|
+
this.sceneMap.loadPhysic();
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
private clearComponentAnimations() {
|
|
906
|
+
this.componentAnimations.forEach((componentAnimation) => {
|
|
907
|
+
componentAnimation.instance?.clear?.();
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
private shouldProcessProjectilePacket(data: any): boolean {
|
|
912
|
+
if (this.mapTransitionInProgress) return false;
|
|
913
|
+
const packetMapId = normalizeRoomMapId(
|
|
914
|
+
typeof data?.mapId === "string" ? data.mapId : undefined,
|
|
915
|
+
);
|
|
916
|
+
const currentMapId = normalizeRoomMapId(this.currentMapRoomId);
|
|
917
|
+
return !packetMapId || !currentMapId || packetMapId === currentMapId;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
private async callConnectError(error: any) {
|
|
921
|
+
await lastValueFrom(this.hooks.callHooks("client-engine-onConnectError", this, error, this.socket));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
private flushPendingSyncPackets() {
|
|
925
|
+
const packets = this.pendingSyncPackets;
|
|
926
|
+
this.pendingSyncPackets = [];
|
|
927
|
+
packets.forEach((packet) => this.applySyncPacket(packet));
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
private flushPendingMapChanges() {
|
|
931
|
+
const packets = this.pendingMapChanges;
|
|
932
|
+
this.pendingMapChanges = [];
|
|
933
|
+
packets.forEach((packet) => this.handleChangeMap(packet));
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
private handleChangeMap(data: any) {
|
|
937
|
+
const nextMapId = typeof data?.mapId === "string" ? data.mapId : undefined;
|
|
938
|
+
this.beginMapTransfer(nextMapId);
|
|
939
|
+
const transferToken = typeof data?.transferToken === "string" ? data.transferToken : undefined;
|
|
940
|
+
this.loadScene(data.mapId, transferToken);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
private applySyncPacket(data: any) {
|
|
944
|
+
if (data.pId) {
|
|
945
|
+
this.playerIdSignal.set(data.pId);
|
|
946
|
+
// Signal that player ID was received
|
|
947
|
+
this.playerIdReceived$.next(true);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (this.sceneResetQueued) {
|
|
951
|
+
const weatherState = this.sceneMap.weatherState();
|
|
952
|
+
const lightingState = this.sceneMap.lightingState();
|
|
953
|
+
this.sceneMap.reset();
|
|
954
|
+
this.sceneMap.weatherState.set(weatherState);
|
|
955
|
+
this.sceneMap.lightingState.set(lightingState);
|
|
956
|
+
this.sceneMap.loadPhysic();
|
|
957
|
+
this.sceneResetQueued = false;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Apply client-side prediction filtering and server reconciliation
|
|
961
|
+
this.hooks.callHooks("client-sceneMap-onChanges", this.sceneMap, { partial: data }).subscribe();
|
|
962
|
+
|
|
963
|
+
const ack = data?.ack;
|
|
964
|
+
const normalizedAck =
|
|
965
|
+
ack && typeof ack.frame === "number"
|
|
966
|
+
? this.normalizeAckWithSyncState(ack, data)
|
|
967
|
+
: undefined;
|
|
968
|
+
const payload = this.prepareSyncPayload(data);
|
|
969
|
+
load(this.sceneMap, payload, true);
|
|
970
|
+
|
|
971
|
+
if (normalizedAck) {
|
|
972
|
+
this.applyServerAck(normalizedAck);
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
for (const playerId in payload.players ?? {}) {
|
|
976
|
+
const player = payload.players[playerId]
|
|
977
|
+
if (!player._param) continue
|
|
978
|
+
for (const param in player._param) {
|
|
979
|
+
this.sceneMap.players()[playerId]._param()[param] = player._param[param]
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Check if players and events are present in sync data
|
|
984
|
+
const players = payload.players || this.sceneMap.players();
|
|
985
|
+
if (players && Object.keys(players).length > 0) {
|
|
986
|
+
this.playersReceived$.next(true);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const events = payload.events || this.sceneMap.events();
|
|
990
|
+
if (events !== undefined) {
|
|
991
|
+
this.eventsReceived$.next(true);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
467
995
|
/**
|
|
468
996
|
* Start periodic ping/pong for client-server synchronization
|
|
469
997
|
*
|
|
@@ -560,13 +1088,22 @@ export class RpgClientEngine<T = any> {
|
|
|
560
1088
|
room: mapId,
|
|
561
1089
|
query: transferToken ? { transferToken } : undefined,
|
|
562
1090
|
})
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
this.
|
|
568
|
-
|
|
1091
|
+
try {
|
|
1092
|
+
await this.webSocket.reconnect()
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
this.mapTransitionInProgress = false;
|
|
1096
|
+
this.stopPingPong();
|
|
1097
|
+
await this.callConnectError(error);
|
|
1098
|
+
throw error;
|
|
1099
|
+
}
|
|
569
1100
|
const res = await this.loadMapService.load(mapId)
|
|
1101
|
+
const loadedLighting = typeof res?.lighting !== "undefined"
|
|
1102
|
+
? res.lighting
|
|
1103
|
+
: res?.data?.lighting;
|
|
1104
|
+
if (typeof loadedLighting !== "undefined") {
|
|
1105
|
+
this.sceneMap.lightingState.set(normalizeLightingState(loadedLighting));
|
|
1106
|
+
}
|
|
570
1107
|
this.sceneMap.data.set(res)
|
|
571
1108
|
|
|
572
1109
|
// Check if playerId is already present
|
|
@@ -587,6 +1124,8 @@ export class RpgClientEngine<T = any> {
|
|
|
587
1124
|
|
|
588
1125
|
// Signal that map loading is completed (this should be last to ensure other checks are done)
|
|
589
1126
|
this.mapLoadCompleted$.next(true);
|
|
1127
|
+
this.currentMapRoomId = mapId;
|
|
1128
|
+
this.mapTransitionInProgress = false;
|
|
590
1129
|
this.sceneMap.configureClientPrediction(this.predictionEnabled);
|
|
591
1130
|
this.sceneMap.loadPhysic()
|
|
592
1131
|
}
|
|
@@ -623,7 +1162,7 @@ export class RpgClientEngine<T = any> {
|
|
|
623
1162
|
* });
|
|
624
1163
|
* ```
|
|
625
1164
|
*/
|
|
626
|
-
setSpritesheetResolver(resolver: (id: string) => any | Promise<any>): void {
|
|
1165
|
+
setSpritesheetResolver(resolver: (id: string | number) => any | Promise<any>): void {
|
|
627
1166
|
this.spritesheetResolver = resolver;
|
|
628
1167
|
}
|
|
629
1168
|
|
|
@@ -634,7 +1173,7 @@ export class RpgClientEngine<T = any> {
|
|
|
634
1173
|
* If not found and a resolver is set, it calls the resolver to create the spritesheet.
|
|
635
1174
|
* The resolved spritesheet is automatically cached for future use.
|
|
636
1175
|
*
|
|
637
|
-
* @param id - The spritesheet ID to retrieve
|
|
1176
|
+
* @param id - The spritesheet ID or legacy tile ID to retrieve
|
|
638
1177
|
* @returns The spritesheet if found or created, or undefined if not found and no resolver
|
|
639
1178
|
* @returns Promise<any> if the resolver is asynchronous
|
|
640
1179
|
*
|
|
@@ -647,7 +1186,7 @@ export class RpgClientEngine<T = any> {
|
|
|
647
1186
|
* const spritesheet = await engine.getSpriteSheet('dynamic-sprite');
|
|
648
1187
|
* ```
|
|
649
1188
|
*/
|
|
650
|
-
getSpriteSheet(id: string): any | Promise<any> {
|
|
1189
|
+
getSpriteSheet(id: string | number): any | Promise<any> {
|
|
651
1190
|
// Check cache first
|
|
652
1191
|
if (this.spritesheets.has(id)) {
|
|
653
1192
|
return this.spritesheets.get(id);
|
|
@@ -655,17 +1194,29 @@ export class RpgClientEngine<T = any> {
|
|
|
655
1194
|
|
|
656
1195
|
// If not in cache and resolver exists, use it
|
|
657
1196
|
if (this.spritesheetResolver) {
|
|
1197
|
+
if (this.spritesheetPromises.has(id)) {
|
|
1198
|
+
return this.spritesheetPromises.get(id);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
658
1201
|
const result = this.spritesheetResolver(id);
|
|
659
1202
|
|
|
660
1203
|
// Check if result is a Promise
|
|
661
1204
|
if (result instanceof Promise) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
1205
|
+
const promise = result
|
|
1206
|
+
.then((spritesheet) => {
|
|
1207
|
+
if (spritesheet) {
|
|
1208
|
+
// Cache the resolved spritesheet
|
|
1209
|
+
this.spritesheets.set(id, spritesheet);
|
|
1210
|
+
}
|
|
1211
|
+
this.spritesheetPromises.delete(id);
|
|
1212
|
+
return spritesheet;
|
|
1213
|
+
})
|
|
1214
|
+
.catch((error) => {
|
|
1215
|
+
this.spritesheetPromises.delete(id);
|
|
1216
|
+
throw error;
|
|
1217
|
+
});
|
|
1218
|
+
this.spritesheetPromises.set(id, promise);
|
|
1219
|
+
return promise;
|
|
669
1220
|
} else {
|
|
670
1221
|
// Synchronous result
|
|
671
1222
|
if (result) {
|
|
@@ -941,19 +1492,22 @@ export class RpgClientEngine<T = any> {
|
|
|
941
1492
|
* Set the camera to follow a specific sprite
|
|
942
1493
|
*
|
|
943
1494
|
* This method changes which sprite the camera viewport should follow.
|
|
944
|
-
* The camera
|
|
1495
|
+
* The camera can smoothly animate to the target sprite before continuous follow starts.
|
|
945
1496
|
*
|
|
946
1497
|
* ## Design
|
|
947
1498
|
*
|
|
948
1499
|
* The camera follow target is stored in a signal that is read by sprite components.
|
|
949
1500
|
* Each sprite checks if it should be followed by comparing its ID with the target ID.
|
|
950
|
-
* When smoothMove options are provided, the
|
|
951
|
-
* viewport system.
|
|
1501
|
+
* When smoothMove options are provided, the transition is handled by pixi-viewport's
|
|
1502
|
+
* animation plugin, then continuous follow is handled by CanvasEngine's viewport system.
|
|
952
1503
|
*
|
|
953
1504
|
* @param targetId - The ID of the sprite to follow. Set to null to follow the current player
|
|
954
1505
|
* @param smoothMove - Animation options. Can be a boolean (default: true) or an object with time and ease
|
|
955
1506
|
* @param smoothMove.time - Duration of the animation in milliseconds (optional)
|
|
956
1507
|
* @param smoothMove.ease - Easing function name from https://easings.net (optional)
|
|
1508
|
+
* @param smoothMove.speed - Continuous follow speed after the transition (optional)
|
|
1509
|
+
* @param smoothMove.acceleration - Continuous follow acceleration after the transition (optional)
|
|
1510
|
+
* @param smoothMove.radius - Center radius where the target can move without moving the viewport (optional)
|
|
957
1511
|
*
|
|
958
1512
|
* @example
|
|
959
1513
|
* ```ts
|
|
@@ -975,19 +1529,19 @@ export class RpgClientEngine<T = any> {
|
|
|
975
1529
|
*/
|
|
976
1530
|
setCameraFollow(
|
|
977
1531
|
targetId: string | null,
|
|
978
|
-
smoothMove?:
|
|
1532
|
+
smoothMove?: CameraFollowSmoothMove
|
|
979
1533
|
): void {
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
// The smoothMove options could be used to configure viewport animation if CanvasEngine supports it
|
|
1534
|
+
this.clearCameraFollowViewportPlugins();
|
|
1535
|
+
this.cameraFollowSmoothMove = smoothMove ?? true;
|
|
983
1536
|
this.cameraFollowTargetId.set(targetId);
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1537
|
+
this.cameraFollowRevision.set(this.cameraFollowRevision() + 1);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
private resetCameraFollow(smoothMove: CameraFollowSmoothMove = false): void {
|
|
1541
|
+
this.clearCameraFollowViewportPlugins();
|
|
1542
|
+
this.cameraFollowSmoothMove = smoothMove;
|
|
1543
|
+
this.cameraFollowTargetId.set(null);
|
|
1544
|
+
this.cameraFollowRevision.set(this.cameraFollowRevision() + 1);
|
|
991
1545
|
}
|
|
992
1546
|
|
|
993
1547
|
addParticle(particle: any) {
|
|
@@ -1081,6 +1635,104 @@ export class RpgClientEngine<T = any> {
|
|
|
1081
1635
|
return component
|
|
1082
1636
|
}
|
|
1083
1637
|
|
|
1638
|
+
/**
|
|
1639
|
+
* Register a reusable sprite component that can be addressed by the server.
|
|
1640
|
+
*
|
|
1641
|
+
* Server-side component definitions only carry the component id and
|
|
1642
|
+
* serializable props. The client registry maps that id to the CanvasEngine
|
|
1643
|
+
* component that performs the actual rendering.
|
|
1644
|
+
*
|
|
1645
|
+
* @param id - Stable component id used by server component definitions
|
|
1646
|
+
* @param component - CanvasEngine component to render for this id
|
|
1647
|
+
* @returns The registered component
|
|
1648
|
+
*
|
|
1649
|
+
* @example
|
|
1650
|
+
* ```ts
|
|
1651
|
+
* engine.registerSpriteComponent('guildBadge', GuildBadgeComponent);
|
|
1652
|
+
* ```
|
|
1653
|
+
*/
|
|
1654
|
+
registerSpriteComponent(id: string, component: any) {
|
|
1655
|
+
this.spriteComponents.set(id, component);
|
|
1656
|
+
return component;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/**
|
|
1660
|
+
* Get a reusable sprite component by id.
|
|
1661
|
+
*
|
|
1662
|
+
* @param id - Component id registered on the client
|
|
1663
|
+
* @returns The CanvasEngine component, or undefined when missing
|
|
1664
|
+
*/
|
|
1665
|
+
getSpriteComponent(id: string) {
|
|
1666
|
+
return this.spriteComponents.get(id);
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
/**
|
|
1670
|
+
* Register a custom event component resolver.
|
|
1671
|
+
*
|
|
1672
|
+
* The last resolver returning a component wins. This lets later modules
|
|
1673
|
+
* override earlier defaults without replacing the whole map scene.
|
|
1674
|
+
*
|
|
1675
|
+
* @param resolver - Function receiving the synced event object
|
|
1676
|
+
* @returns The registered resolver
|
|
1677
|
+
*/
|
|
1678
|
+
addEventComponentResolver(resolver: EventComponentResolver) {
|
|
1679
|
+
return this.eventComponentResolvers.add(resolver);
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Resolve the custom CanvasEngine component for an event, if any.
|
|
1684
|
+
*
|
|
1685
|
+
* @param event - Synced client event object
|
|
1686
|
+
* @returns The component/config returned by the last matching resolver
|
|
1687
|
+
*/
|
|
1688
|
+
resolveEventComponent(event: RpgClientEvent): EventComponentConfig | null {
|
|
1689
|
+
return this.eventComponentResolvers.resolve(event);
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
registerProjectileComponent(type: string, component: any) {
|
|
1693
|
+
return this.projectiles.register(type, component);
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
getProjectileComponent(type: string) {
|
|
1697
|
+
return this.projectiles.get(type);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
/**
|
|
1701
|
+
* Register a named client visual macro.
|
|
1702
|
+
*
|
|
1703
|
+
* Client visuals are small client-side functions that group existing visual
|
|
1704
|
+
* primitives such as flash, sound, component animations, sprite animation, or
|
|
1705
|
+
* map shake. The server sends only the visual name and a serializable payload.
|
|
1706
|
+
*
|
|
1707
|
+
* @param name - Stable visual name sent by the server
|
|
1708
|
+
* @param handler - Client-side visual handler
|
|
1709
|
+
* @returns The registered handler
|
|
1710
|
+
*/
|
|
1711
|
+
registerClientVisual(name: string, handler: ClientVisualHandler) {
|
|
1712
|
+
return this.clientVisuals.register(name, handler);
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/**
|
|
1716
|
+
* Register several named client visual macros.
|
|
1717
|
+
*
|
|
1718
|
+
* @param visuals - Map of visual names to client-side handlers
|
|
1719
|
+
*/
|
|
1720
|
+
registerClientVisuals(visuals: ClientVisualMap) {
|
|
1721
|
+
this.clientVisuals.registerMany(visuals);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
/**
|
|
1725
|
+
* Play a registered client visual locally.
|
|
1726
|
+
*
|
|
1727
|
+
* This is also used by the websocket listener when the server calls
|
|
1728
|
+
* `player.clientVisual()` or `map.clientVisual()`.
|
|
1729
|
+
*
|
|
1730
|
+
* @param packet - Visual name and serializable payload
|
|
1731
|
+
*/
|
|
1732
|
+
playClientVisual(packet: ClientVisualPacket) {
|
|
1733
|
+
return this.clientVisuals.play(packet, this);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1084
1736
|
/**
|
|
1085
1737
|
* Add a component animation to the engine
|
|
1086
1738
|
*
|
|
@@ -1164,21 +1816,59 @@ export class RpgClientEngine<T = any> {
|
|
|
1164
1816
|
* duration: 1000,
|
|
1165
1817
|
* onFinish: () => console.log('Fade complete')
|
|
1166
1818
|
* });
|
|
1819
|
+
*
|
|
1820
|
+
* // Wait until the transition component calls onFinish
|
|
1821
|
+
* await engine.startTransition('fade', { duration: 1000 });
|
|
1167
1822
|
* ```
|
|
1168
1823
|
*/
|
|
1169
|
-
startTransition(id: string, props: any = {}) {
|
|
1824
|
+
startTransition(id: string, props: any = {}): Promise<void> {
|
|
1170
1825
|
if (!this.guiService.exists(id)) {
|
|
1171
1826
|
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
1827
|
}
|
|
1173
|
-
|
|
1828
|
+
return new Promise<void>((resolve) => {
|
|
1829
|
+
let finished = false;
|
|
1830
|
+
const finish = (data?: any) => {
|
|
1831
|
+
if (finished) return;
|
|
1832
|
+
finished = true;
|
|
1833
|
+
props?.onFinish?.(data);
|
|
1834
|
+
resolve();
|
|
1835
|
+
};
|
|
1836
|
+
|
|
1837
|
+
this.guiService.display(id, {
|
|
1838
|
+
...props,
|
|
1839
|
+
onFinish: finish,
|
|
1840
|
+
});
|
|
1841
|
+
});
|
|
1174
1842
|
}
|
|
1175
1843
|
|
|
1176
|
-
async processInput({ input }: { input:
|
|
1844
|
+
async processInput({ input }: { input: RpgMovementInput }) {
|
|
1845
|
+
if (this.stopProcessingInput) return;
|
|
1846
|
+
|
|
1847
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1848
|
+
const canMove =
|
|
1849
|
+
!currentPlayer ||
|
|
1850
|
+
getCanMoveValue(currentPlayer);
|
|
1851
|
+
if (!canMove) {
|
|
1852
|
+
this.interruptCurrentPlayerMovement(currentPlayer);
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1177
1856
|
const timestamp = Date.now();
|
|
1857
|
+
const movementInput = isDashInput(input)
|
|
1858
|
+
? normalizeDashInput(input, currentPlayer?.direction?.())
|
|
1859
|
+
: input;
|
|
1860
|
+
if (!movementInput) return;
|
|
1861
|
+
if (isDashInput(movementInput)) {
|
|
1862
|
+
const cooldown = movementInput.cooldown ?? DEFAULT_DASH_COOLDOWN_MS;
|
|
1863
|
+
if (timestamp < this.dashLockedUntil) return;
|
|
1864
|
+
this.dashLockedUntil = timestamp + cooldown;
|
|
1865
|
+
}
|
|
1866
|
+
this.lastLocalMovementInputAt = timestamp;
|
|
1867
|
+
|
|
1178
1868
|
let frame: number;
|
|
1179
1869
|
let tick: number;
|
|
1180
1870
|
if (this.predictionEnabled && this.prediction) {
|
|
1181
|
-
const meta = this.prediction.recordInput(
|
|
1871
|
+
const meta = this.prediction.recordInput(movementInput, timestamp);
|
|
1182
1872
|
frame = meta.frame;
|
|
1183
1873
|
tick = meta.tick;
|
|
1184
1874
|
} else {
|
|
@@ -1186,13 +1876,11 @@ export class RpgClientEngine<T = any> {
|
|
|
1186
1876
|
tick = this.getPhysicsTick();
|
|
1187
1877
|
}
|
|
1188
1878
|
this.inputFrameCounter = frame;
|
|
1189
|
-
this.hooks.callHooks("client-engine-onInput", this, { input, playerId: this.playerId }).subscribe();
|
|
1879
|
+
this.hooks.callHooks("client-engine-onInput", this, { input: movementInput, playerId: this.playerId }).subscribe();
|
|
1190
1880
|
|
|
1191
|
-
const currentPlayer = this.sceneMap.getCurrentPlayer();
|
|
1192
1881
|
const bodyReady = this.ensureCurrentPlayerBody();
|
|
1193
1882
|
if (currentPlayer && bodyReady) {
|
|
1194
|
-
|
|
1195
|
-
(this.sceneMap as any).moveBody(currentPlayer, input);
|
|
1883
|
+
this.applyPredictedMovementInput(currentPlayer, movementInput);
|
|
1196
1884
|
if (this.predictionEnabled && this.prediction) {
|
|
1197
1885
|
this.pendingPredictionFrames.push(frame);
|
|
1198
1886
|
if (this.pendingPredictionFrames.length > 240) {
|
|
@@ -1201,14 +1889,42 @@ export class RpgClientEngine<T = any> {
|
|
|
1201
1889
|
}
|
|
1202
1890
|
}
|
|
1203
1891
|
|
|
1204
|
-
this.emitMovePacket(
|
|
1205
|
-
this.lastInputTime =
|
|
1892
|
+
this.emitMovePacket(movementInput, frame, tick, timestamp, true);
|
|
1893
|
+
this.lastInputTime = isDashInput(movementInput)
|
|
1894
|
+
? Date.now() + (movementInput.duration ?? DEFAULT_DASH_DURATION_MS)
|
|
1895
|
+
: Date.now();
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
async processDash(input: Partial<RpgDashInput> = {}) {
|
|
1899
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1900
|
+
const fallbackDirection =
|
|
1901
|
+
typeof currentPlayer?.direction === "function"
|
|
1902
|
+
? currentPlayer.direction()
|
|
1903
|
+
: currentPlayer?.direction;
|
|
1904
|
+
const dashInput = normalizeDashInput(input, fallbackDirection);
|
|
1905
|
+
if (!dashInput) return;
|
|
1906
|
+
await this.processInput({ input: dashInput });
|
|
1206
1907
|
}
|
|
1207
1908
|
|
|
1208
|
-
processAction(
|
|
1909
|
+
processAction(action: RpgActionName, data?: any): void;
|
|
1910
|
+
processAction(action: RpgActionInput): void;
|
|
1911
|
+
processAction(action: RpgActionName | RpgActionInput, data?: any): void {
|
|
1209
1912
|
if (this.stopProcessingInput) return;
|
|
1210
|
-
|
|
1211
|
-
|
|
1913
|
+
const currentPlayer = this.sceneMap.getCurrentPlayer() as any;
|
|
1914
|
+
const canMove =
|
|
1915
|
+
!currentPlayer ||
|
|
1916
|
+
getCanMoveValue(currentPlayer);
|
|
1917
|
+
if (!canMove) return;
|
|
1918
|
+
|
|
1919
|
+
const payload = normalizeActionInput(action as any, data);
|
|
1920
|
+
|
|
1921
|
+
this.hooks.callHooks("client-engine-onInput", this, {
|
|
1922
|
+
input: payload.action,
|
|
1923
|
+
action: payload.action,
|
|
1924
|
+
data: payload.data,
|
|
1925
|
+
playerId: this.playerId,
|
|
1926
|
+
}).subscribe();
|
|
1927
|
+
this.webSocket.emit('action', payload)
|
|
1212
1928
|
}
|
|
1213
1929
|
|
|
1214
1930
|
get PIXI() {
|
|
@@ -1227,8 +1943,76 @@ export class RpgClientEngine<T = any> {
|
|
|
1227
1943
|
return this.sceneMap
|
|
1228
1944
|
}
|
|
1229
1945
|
|
|
1946
|
+
getObjectById(id: string) {
|
|
1947
|
+
return this.sceneMap?.getObjectById(id);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1230
1950
|
private getPhysicsTick(): number {
|
|
1231
|
-
return this.sceneMap?.getTick?.() ?? 0;
|
|
1951
|
+
return (this.sceneMap as any)?.getTick?.() ?? 0;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
private getPhysicsTickDurationMs(): number {
|
|
1955
|
+
const timeStep = (this.sceneMap as any)?.physic?.getWorld?.()?.getTimeStep?.();
|
|
1956
|
+
return typeof timeStep === "number" && Number.isFinite(timeStep) && timeStep > 0
|
|
1957
|
+
? timeStep * 1000
|
|
1958
|
+
: 1000 / 60;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
private updateServerTickEstimate(serverTick: number | undefined, now = Date.now()): void {
|
|
1962
|
+
if (typeof serverTick !== "number" || !Number.isFinite(serverTick)) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
this.latestServerTick = serverTick;
|
|
1966
|
+
this.latestServerTickAt = now;
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
private estimateServerTick(now = Date.now()): number | undefined {
|
|
1970
|
+
if (typeof this.latestServerTick !== "number" || this.latestServerTickAt <= 0) {
|
|
1971
|
+
return undefined;
|
|
1972
|
+
}
|
|
1973
|
+
const elapsedTicks = Math.max(0, (now - this.latestServerTickAt) / this.getPhysicsTickDurationMs());
|
|
1974
|
+
return this.latestServerTick + elapsedTicks;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
private predictProjectileImpact(projectile: ClientProjectileSpawn): ClientProjectileImpact | null {
|
|
1978
|
+
if (projectile.predictImpact === false) {
|
|
1979
|
+
return null;
|
|
1980
|
+
}
|
|
1981
|
+
const sceneMap = this.sceneMap as any;
|
|
1982
|
+
if (!sceneMap?.physic || !Number.isFinite(projectile.range) || projectile.range <= 0) {
|
|
1983
|
+
return null;
|
|
1984
|
+
}
|
|
1985
|
+
const origin = projectile.origin;
|
|
1986
|
+
const direction = projectile.direction;
|
|
1987
|
+
if (
|
|
1988
|
+
!origin ||
|
|
1989
|
+
!direction ||
|
|
1990
|
+
!Number.isFinite(origin.x) ||
|
|
1991
|
+
!Number.isFinite(origin.y) ||
|
|
1992
|
+
!Number.isFinite(direction.x) ||
|
|
1993
|
+
!Number.isFinite(direction.y) ||
|
|
1994
|
+
(direction.x === 0 && direction.y === 0)
|
|
1995
|
+
) {
|
|
1996
|
+
return null;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
const hit = sceneMap.physic.raycast(
|
|
2000
|
+
new Vector2(origin.x, origin.y),
|
|
2001
|
+
new Vector2(direction.x, direction.y),
|
|
2002
|
+
projectile.range,
|
|
2003
|
+
projectile.collisionMask,
|
|
2004
|
+
(entity) => projectile.ignoreOwner === false || !projectile.ownerId || entity.uuid !== projectile.ownerId,
|
|
2005
|
+
);
|
|
2006
|
+
if (!hit) {
|
|
2007
|
+
return null;
|
|
2008
|
+
}
|
|
2009
|
+
return {
|
|
2010
|
+
id: projectile.id,
|
|
2011
|
+
targetId: hit.entity.uuid,
|
|
2012
|
+
x: hit.point.x,
|
|
2013
|
+
y: hit.point.y,
|
|
2014
|
+
distance: hit.distance,
|
|
2015
|
+
};
|
|
1232
2016
|
}
|
|
1233
2017
|
|
|
1234
2018
|
private ensureCurrentPlayerBody(): boolean {
|
|
@@ -1295,7 +2079,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1295
2079
|
input: entry.direction,
|
|
1296
2080
|
x: state.x,
|
|
1297
2081
|
y: state.y,
|
|
1298
|
-
direction: state.direction ?? entry.direction,
|
|
2082
|
+
direction: state.direction ?? resolveMoveDirection(entry.direction),
|
|
1299
2083
|
});
|
|
1300
2084
|
}
|
|
1301
2085
|
if (trajectory.length > this.MAX_MOVE_TRAJECTORY_POINTS) {
|
|
@@ -1305,7 +2089,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1305
2089
|
}
|
|
1306
2090
|
|
|
1307
2091
|
private emitMovePacket(
|
|
1308
|
-
input:
|
|
2092
|
+
input: RpgMovementInput,
|
|
1309
2093
|
frame: number,
|
|
1310
2094
|
tick: number,
|
|
1311
2095
|
timestamp: number,
|
|
@@ -1337,6 +2121,14 @@ export class RpgClientEngine<T = any> {
|
|
|
1337
2121
|
if (!this.predictionEnabled || !this.prediction) {
|
|
1338
2122
|
return;
|
|
1339
2123
|
}
|
|
2124
|
+
const player = this.sceneMap?.getCurrentPlayer?.() as any;
|
|
2125
|
+
if (
|
|
2126
|
+
player &&
|
|
2127
|
+
!getCanMoveValue(player)
|
|
2128
|
+
) {
|
|
2129
|
+
this.interruptCurrentPlayerMovement(player);
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
1340
2132
|
const pendingInputs = this.prediction.getPendingInputs();
|
|
1341
2133
|
if (pendingInputs.length === 0) {
|
|
1342
2134
|
return;
|
|
@@ -1352,6 +2144,22 @@ export class RpgClientEngine<T = any> {
|
|
|
1352
2144
|
this.emitMovePacket(latest.direction, latest.frame, latest.tick, now, false);
|
|
1353
2145
|
}
|
|
1354
2146
|
|
|
2147
|
+
private applyPredictedMovementInput(
|
|
2148
|
+
player: any,
|
|
2149
|
+
input: RpgMovementInput
|
|
2150
|
+
): boolean {
|
|
2151
|
+
if (isDashInput(input)) {
|
|
2152
|
+
const direction = vectorToDirection(input.direction);
|
|
2153
|
+
player.changeDirection(direction);
|
|
2154
|
+
return Boolean((this.sceneMap as any).dashBody?.(player, input));
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
const direction = resolveMoveDirection(input);
|
|
2158
|
+
if (!direction) return false;
|
|
2159
|
+
player.changeDirection(direction);
|
|
2160
|
+
return Boolean((this.sceneMap as any).moveBody?.(player, direction));
|
|
2161
|
+
}
|
|
2162
|
+
|
|
1355
2163
|
private getLocalPlayerState(): PredictionState<Direction> {
|
|
1356
2164
|
const currentPlayer = this.sceneMap?.getCurrentPlayer();
|
|
1357
2165
|
if (!currentPlayer) {
|
|
@@ -1395,7 +2203,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1395
2203
|
? configuredMaxEntries
|
|
1396
2204
|
: Math.max(600, Math.ceil(historyTtlMs / 16) + 120);
|
|
1397
2205
|
this.sceneMap?.configureClientPrediction?.(true);
|
|
1398
|
-
this.prediction = new PredictionController<Direction>({
|
|
2206
|
+
this.prediction = new PredictionController<RpgMovementInput, Direction>({
|
|
1399
2207
|
correctionThreshold: (this.globalConfig as any)?.prediction?.correctionThreshold ?? this.SERVER_CORRECTION_THRESHOLD,
|
|
1400
2208
|
historyTtlMs,
|
|
1401
2209
|
maxHistoryEntries,
|
|
@@ -1475,6 +2283,34 @@ export class RpgClientEngine<T = any> {
|
|
|
1475
2283
|
this.lastMovePathSentFrame = 0;
|
|
1476
2284
|
}
|
|
1477
2285
|
|
|
2286
|
+
/**
|
|
2287
|
+
* Stop local movement immediately and discard pending predicted movement.
|
|
2288
|
+
*
|
|
2289
|
+
* Use this before a blocking action such as an A-RPG attack, dialog, dash
|
|
2290
|
+
* startup, or any client-side state where already buffered movement inputs
|
|
2291
|
+
* must not be replayed after server reconciliation.
|
|
2292
|
+
*
|
|
2293
|
+
* @param player - Player object to stop. Defaults to the current player.
|
|
2294
|
+
* @returns `true` when a player was found and interrupted.
|
|
2295
|
+
*
|
|
2296
|
+
* @example
|
|
2297
|
+
* ```ts
|
|
2298
|
+
* engine.interruptCurrentPlayerMovement();
|
|
2299
|
+
* ```
|
|
2300
|
+
*/
|
|
2301
|
+
interruptCurrentPlayerMovement(player: any = this.sceneMap?.getCurrentPlayer?.()): boolean {
|
|
2302
|
+
if (!player) {
|
|
2303
|
+
return false;
|
|
2304
|
+
}
|
|
2305
|
+
(this.sceneMap as any)?.stopMovement?.(player);
|
|
2306
|
+
this.prediction?.clearPendingInputs();
|
|
2307
|
+
this.pendingPredictionFrames = [];
|
|
2308
|
+
this.lastInputTime = 0;
|
|
2309
|
+
this.lastMovePathSentAt = Date.now();
|
|
2310
|
+
this.lastMovePathSentFrame = this.inputFrameCounter;
|
|
2311
|
+
return true;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
1478
2314
|
/**
|
|
1479
2315
|
* Trigger a flash animation on a sprite
|
|
1480
2316
|
*
|
|
@@ -1542,12 +2378,14 @@ export class RpgClientEngine<T = any> {
|
|
|
1542
2378
|
}
|
|
1543
2379
|
|
|
1544
2380
|
private applyServerAck(ack: { frame: number; serverTick?: number; x?: number; y?: number; direction?: Direction }) {
|
|
2381
|
+
this.updateServerTickEstimate(ack.serverTick);
|
|
2382
|
+
const keepLocalMovement = this.shouldKeepLocalPlayerMovement();
|
|
1545
2383
|
if (this.predictionEnabled && this.prediction) {
|
|
1546
2384
|
const result = this.prediction.applyServerAck({
|
|
1547
2385
|
frame: ack.frame,
|
|
1548
2386
|
serverTick: ack.serverTick,
|
|
1549
2387
|
state:
|
|
1550
|
-
typeof ack.x === "number" && typeof ack.y === "number"
|
|
2388
|
+
!keepLocalMovement && typeof ack.x === "number" && typeof ack.y === "number"
|
|
1551
2389
|
? { x: ack.x, y: ack.y, direction: ack.direction }
|
|
1552
2390
|
: undefined,
|
|
1553
2391
|
});
|
|
@@ -1560,7 +2398,10 @@ export class RpgClientEngine<T = any> {
|
|
|
1560
2398
|
if (typeof ack.x !== "number" || typeof ack.y !== "number") {
|
|
1561
2399
|
return;
|
|
1562
2400
|
}
|
|
1563
|
-
|
|
2401
|
+
if (keepLocalMovement) {
|
|
2402
|
+
return;
|
|
2403
|
+
}
|
|
2404
|
+
const player = this.getCurrentPlayer() as any;
|
|
1564
2405
|
const myId = this.playerIdSignal();
|
|
1565
2406
|
if (!player || !myId) {
|
|
1566
2407
|
return;
|
|
@@ -1581,12 +2422,16 @@ export class RpgClientEngine<T = any> {
|
|
|
1581
2422
|
|
|
1582
2423
|
private reconcilePrediction(
|
|
1583
2424
|
authoritativeState: PredictionState<Direction>,
|
|
1584
|
-
pendingInputs: PredictionHistoryEntry<Direction>[],
|
|
2425
|
+
pendingInputs: PredictionHistoryEntry<RpgMovementInput, Direction>[],
|
|
1585
2426
|
): void {
|
|
1586
|
-
const player = this.getCurrentPlayer();
|
|
2427
|
+
const player = this.getCurrentPlayer() as any;
|
|
1587
2428
|
if (!player) {
|
|
1588
2429
|
return;
|
|
1589
2430
|
}
|
|
2431
|
+
if (!getCanMoveValue(player)) {
|
|
2432
|
+
this.interruptCurrentPlayerMovement(player);
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
1590
2435
|
|
|
1591
2436
|
(this.sceneMap as any).stopMovement(player);
|
|
1592
2437
|
this.applyAuthoritativeState(authoritativeState);
|
|
@@ -1599,7 +2444,7 @@ export class RpgClientEngine<T = any> {
|
|
|
1599
2444
|
const replayInputs = pendingInputs.slice(-600);
|
|
1600
2445
|
for (const entry of replayInputs) {
|
|
1601
2446
|
if (!entry?.direction) continue;
|
|
1602
|
-
|
|
2447
|
+
this.applyPredictedMovementInput(player, entry.direction);
|
|
1603
2448
|
this.sceneMap.stepPredictionTick();
|
|
1604
2449
|
this.prediction?.attachPredictedState(entry.frame, this.getLocalPlayerState());
|
|
1605
2450
|
}
|
|
@@ -1699,6 +2544,22 @@ export class RpgClientEngine<T = any> {
|
|
|
1699
2544
|
this.resizeHandler = undefined;
|
|
1700
2545
|
}
|
|
1701
2546
|
|
|
2547
|
+
if (this.pointerMoveHandler && this.pointerCanvas) {
|
|
2548
|
+
this.pointerCanvas.removeEventListener('pointermove', this.pointerMoveHandler);
|
|
2549
|
+
this.pointerCanvas.removeEventListener('pointerdown', this.pointerMoveHandler);
|
|
2550
|
+
if (this.pointerUpHandler) {
|
|
2551
|
+
this.pointerCanvas.removeEventListener('pointerup', this.pointerUpHandler);
|
|
2552
|
+
}
|
|
2553
|
+
if (this.pointerCancelHandler) {
|
|
2554
|
+
this.pointerCanvas.removeEventListener('pointercancel', this.pointerCancelHandler);
|
|
2555
|
+
this.pointerCanvas.removeEventListener('pointerleave', this.pointerCancelHandler);
|
|
2556
|
+
}
|
|
2557
|
+
this.pointerMoveHandler = undefined;
|
|
2558
|
+
this.pointerUpHandler = undefined;
|
|
2559
|
+
this.pointerCancelHandler = undefined;
|
|
2560
|
+
this.pointerCanvas = undefined;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
1702
2563
|
// Destroy PIXI app and renderer if they exist
|
|
1703
2564
|
// Destroy the app first, which will destroy the renderer
|
|
1704
2565
|
// Store renderer reference before destroying app (since app.destroy() will destroy the renderer)
|
|
@@ -1769,9 +2630,10 @@ export class RpgClientEngine<T = any> {
|
|
|
1769
2630
|
|
|
1770
2631
|
// Reset signals
|
|
1771
2632
|
this.playerIdSignal.set(null);
|
|
1772
|
-
this.
|
|
2633
|
+
this.resetCameraFollow(false);
|
|
1773
2634
|
this.spriteComponentsBehind.set([]);
|
|
1774
2635
|
this.spriteComponentsInFront.set([]);
|
|
2636
|
+
this.eventComponentResolvers.clear();
|
|
1775
2637
|
|
|
1776
2638
|
// Clear maps and arrays
|
|
1777
2639
|
this.spritesheets.clear();
|