@rpgjs/client 5.0.0-beta.24 → 5.0.0-beta.26

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/Game/Map.d.ts +1 -0
  3. package/dist/Game/Map.js +5 -0
  4. package/dist/Game/Map.js.map +1 -1
  5. package/dist/Gui/Gui.js +5 -0
  6. package/dist/Gui/Gui.js.map +1 -1
  7. package/dist/RpgClient.d.ts +2 -1
  8. package/dist/RpgClientEngine.d.ts +76 -0
  9. package/dist/RpgClientEngine.js +59 -4
  10. package/dist/RpgClientEngine.js.map +1 -1
  11. package/dist/components/gui/dialogbox/index.ce.js +23 -2
  12. package/dist/components/gui/dialogbox/index.ce.js.map +1 -1
  13. package/dist/components/gui/index.d.ts +3 -1
  14. package/dist/components/gui/index.js +2 -0
  15. package/dist/components/gui/input-field.ce.js +123 -0
  16. package/dist/components/gui/input-field.ce.js.map +1 -0
  17. package/dist/components/gui/input.ce.js +40 -0
  18. package/dist/components/gui/input.ce.js.map +1 -0
  19. package/dist/components/scenes/draw-map.ce.js +5 -2
  20. package/dist/components/scenes/draw-map.ce.js.map +1 -1
  21. package/dist/components/scenes/weather-lifecycle-compat.d.ts +0 -0
  22. package/dist/components/scenes/weather-lifecycle-compat.js +11 -0
  23. package/dist/components/scenes/weather-lifecycle-compat.js.map +1 -0
  24. package/dist/components/scenes/weather-tick-lifecycle.d.ts +17 -0
  25. package/dist/components/scenes/weather-tick-lifecycle.js +44 -0
  26. package/dist/components/scenes/weather-tick-lifecycle.js.map +1 -0
  27. package/dist/components/scenes/weather-tick-lifecycle.spec.d.ts +1 -0
  28. package/dist/i18n.d.ts +12 -0
  29. package/dist/i18n.js +12 -0
  30. package/dist/i18n.js.map +1 -1
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +15 -11
  33. package/dist/services/loadMap.d.ts +10 -2
  34. package/dist/services/loadMap.js +2 -1
  35. package/dist/services/loadMap.js.map +1 -1
  36. package/dist/services/mapStreaming.d.ts +85 -0
  37. package/dist/services/mapStreaming.js +210 -0
  38. package/dist/services/mapStreaming.js.map +1 -0
  39. package/dist/services/mapStreaming.spec.d.ts +1 -0
  40. package/dist/services/mmorpg.d.ts +6 -0
  41. package/dist/services/mmorpg.js +2 -2
  42. package/dist/services/mmorpg.js.map +1 -1
  43. package/package.json +4 -4
  44. package/src/Game/Map.ts +7 -0
  45. package/src/Gui/Gui.spec.ts +5 -0
  46. package/src/Gui/Gui.ts +5 -1
  47. package/src/RpgClient.ts +2 -1
  48. package/src/RpgClientEngine.ts +147 -6
  49. package/src/components/gui/dialogbox/index.ce +26 -1
  50. package/src/components/gui/index.ts +5 -1
  51. package/src/components/gui/input-field.ce +106 -0
  52. package/src/components/gui/input.ce +26 -0
  53. package/src/components/scenes/draw-map.ce +4 -1
  54. package/src/components/scenes/weather-lifecycle-compat.ts +8 -0
  55. package/src/components/scenes/weather-tick-lifecycle.spec.ts +60 -0
  56. package/src/components/scenes/weather-tick-lifecycle.ts +69 -0
  57. package/src/i18n.spec.ts +2 -1
  58. package/src/i18n.ts +12 -0
  59. package/src/index.ts +2 -0
  60. package/src/services/loadMap.ts +9 -2
  61. package/src/services/mapStreaming.spec.ts +240 -0
  62. package/src/services/mapStreaming.ts +275 -0
  63. package/src/services/mmorpg.ts +15 -2
  64. package/vite.config.ts +5 -2
package/dist/index.js CHANGED
@@ -2,25 +2,27 @@ import { Context } from "./node_modules/.pnpm/@signe_di@3.1.0/node_modules/@sign
2
2
  import { clearInject, context, inject, setInject } from "./core/inject.js";
3
3
  import { AbstractWebsocket, WebSocketToken } from "./services/AbstractSocket.js";
4
4
  import { getKeyboardControlBind, isKeyboardActionConfig, keyboardEventMatchesBind, normalizeActionInput, resolveKeyboardActionInput, resolveKeyboardDirectionInput } from "./services/actionInput.js";
5
+ import __ce_component$10 from "./components/gui/input-field.ce.js";
5
6
  import __ce_component$2 from "./components/gui/dialogbox/index.ce.js";
6
7
  import __ce_component from "./components/gui/box.ce.js";
7
- import __ce_component$16 from "./components/gui/shop/shop.ce.js";
8
+ import __ce_component$18 from "./components/gui/shop/shop.ce.js";
8
9
  import { getEntityProp } from "./utils/getEntityProp.js";
9
10
  import __ce_component$8 from "./components/gui/hud/hud.ce.js";
10
11
  import { SaveClientService, SaveClientToken, provideSaveClient } from "./services/save.js";
11
- import __ce_component$14 from "./components/gui/save-load.ce.js";
12
- import __ce_component$9 from "./components/gui/menu/items-menu.ce.js";
13
- import __ce_component$17 from "./components/gui/menu/skills-menu.ce.js";
12
+ import __ce_component$16 from "./components/gui/save-load.ce.js";
13
+ import __ce_component$11 from "./components/gui/menu/items-menu.ce.js";
14
+ import __ce_component$19 from "./components/gui/menu/skills-menu.ce.js";
14
15
  import __ce_component$3 from "./components/gui/menu/equip-menu.ce.js";
15
- import __ce_component$13 from "./components/gui/menu/options-menu.ce.js";
16
+ import __ce_component$15 from "./components/gui/menu/options-menu.ce.js";
16
17
  import __ce_component$5 from "./components/gui/menu/exit-menu.ce.js";
17
- import __ce_component$11 from "./components/gui/menu/main-menu.ce.js";
18
- import __ce_component$12 from "./components/gui/notification/notification.ce.js";
19
- import __ce_component$18 from "./components/gui/title-screen.ce.js";
18
+ import __ce_component$13 from "./components/gui/menu/main-menu.ce.js";
19
+ import __ce_component$14 from "./components/gui/notification/notification.ce.js";
20
+ import __ce_component$20 from "./components/gui/title-screen.ce.js";
20
21
  import __ce_component$6 from "./components/gui/gameover.ce.js";
22
+ import __ce_component$9 from "./components/gui/input.ce.js";
21
23
  import "./components/gui/index.js";
22
24
  import { RpgGui } from "./Gui/Gui.js";
23
- import __ce_component$15 from "./components/scenes/draw-map.ce.js";
25
+ import __ce_component$17 from "./components/scenes/draw-map.ce.js";
24
26
  import { LoadMapService, LoadMapToken, provideLoadMap } from "./services/loadMap.js";
25
27
  import { RpgSound, Sound, getSoundMetadata } from "./Sound.js";
26
28
  import { RpgResource } from "./Resource.js";
@@ -39,8 +41,9 @@ import { Control } from "./services/keyboardControls.js";
39
41
  import { provideRpg } from "./services/standalone.js";
40
42
  import { BridgeWebsocket, provideMmorpg } from "./services/mmorpg.js";
41
43
  import { startGame } from "./core/setup.js";
44
+ import { MapStreamClientController, provideClientMapStreaming } from "./services/mapStreaming.js";
42
45
  import __ce_component$7 from "./components/prebuilt/hp-bar.ce.js";
43
- import __ce_component$10 from "./components/prebuilt/light-halo.ce.js";
46
+ import __ce_component$12 from "./components/prebuilt/light-halo.ce.js";
44
47
  import "./components/prebuilt/index.js";
45
48
  import { Presets } from "./presets/index.js";
46
49
  import __ce_component$1 from "./components/character.ce.js";
@@ -49,4 +52,5 @@ import "./components/index.js";
49
52
  import { Spritesheet } from "./decorators/spritesheet.js";
50
53
  import { withMobile } from "./components/gui/mobile/index.js";
51
54
  import { Input, KeyboardControls } from "canvasengine";
52
- export { AbstractWebsocket, __ce_component as BoxComponent, BridgeWebsocket, __ce_component$1 as CharacterComponent, ClientVisualRegistry, Context, Control, __ce_component$2 as DialogboxComponent, __ce_component$3 as EquipMenuComponent, __ce_component$4 as EventLayerComponent, __ce_component$5 as ExitMenuComponent, __ce_component$6 as GameoverComponent, GlobalConfigToken, __ce_component$7 as HpBar, __ce_component$8 as HudComponent, Input, __ce_component$9 as ItemsMenuComponent, KeyboardControls, __ce_component$10 as LightHalo, LoadMapService, LoadMapToken, __ce_component$11 as MainMenuComponent, __ce_component$12 as NotificationComponent, __ce_component$13 as OptionsMenuComponent, PrebuiltComponentAnimations, Presets, ProjectileManager, RpgClientBuiltinI18n, RpgClientEngine, RpgClientEvent, RpgClientInteractions, RpgClientObject, RpgClientPlayer, RpgGui, RpgResource, RpgSound, SaveClientService, SaveClientToken, __ce_component$14 as SaveLoadComponent, __ce_component$15 as SceneMap, __ce_component$16 as ShopComponent, __ce_component$17 as SkillsMenuComponent, Sound, Spritesheet, __ce_component$18 as TitleScreenComponent, WebSocketToken, clearInject, context, createClientPointerContext, dragToTile, draggable, getEntityProp, getKeyboardControlBind, getSoundMetadata, hoverPopover, inject, isKeyboardActionConfig, keyboardEventMatchesBind, normalizeActionInput, provideClientGlobalConfig, provideClientModules, provideGlobalConfig, provideI18n, provideLoadMap, provideMmorpg, provideRpg, provideSaveClient, resolveKeyboardActionInput, resolveKeyboardDirectionInput, selectable, setInject, startGame, withMobile };
55
+ import { defineModule } from "@rpgjs/common";
56
+ export { AbstractWebsocket, __ce_component as BoxComponent, BridgeWebsocket, __ce_component$1 as CharacterComponent, ClientVisualRegistry, Context, Control, __ce_component$2 as DialogboxComponent, __ce_component$3 as EquipMenuComponent, __ce_component$4 as EventLayerComponent, __ce_component$5 as ExitMenuComponent, __ce_component$6 as GameoverComponent, GlobalConfigToken, __ce_component$7 as HpBar, __ce_component$8 as HudComponent, Input, __ce_component$9 as InputComponent, __ce_component$10 as InputFieldComponent, __ce_component$11 as ItemsMenuComponent, KeyboardControls, __ce_component$12 as LightHalo, LoadMapService, LoadMapToken, __ce_component$13 as MainMenuComponent, MapStreamClientController, __ce_component$14 as NotificationComponent, __ce_component$15 as OptionsMenuComponent, PrebuiltComponentAnimations, Presets, ProjectileManager, RpgClientBuiltinI18n, RpgClientEngine, RpgClientEvent, RpgClientInteractions, RpgClientObject, RpgClientPlayer, RpgGui, RpgResource, RpgSound, SaveClientService, SaveClientToken, __ce_component$16 as SaveLoadComponent, __ce_component$17 as SceneMap, __ce_component$18 as ShopComponent, __ce_component$19 as SkillsMenuComponent, Sound, Spritesheet, __ce_component$20 as TitleScreenComponent, WebSocketToken, clearInject, context, createClientPointerContext, defineModule, dragToTile, draggable, getEntityProp, getKeyboardControlBind, getSoundMetadata, hoverPopover, inject, isKeyboardActionConfig, keyboardEventMatchesBind, normalizeActionInput, provideClientGlobalConfig, provideClientMapStreaming, provideClientModules, provideGlobalConfig, provideI18n, provideLoadMap, provideMmorpg, provideRpg, provideSaveClient, resolveKeyboardActionInput, resolveKeyboardDirectionInput, selectable, setInject, startGame, withMobile };
@@ -1,5 +1,6 @@
1
1
  import { Context } from '@signe/di';
2
2
  import { LightingState } from '@rpgjs/common';
3
+ import { RpgClientMap } from '../Game/Map';
3
4
  export declare const LoadMapToken = "LoadMapToken";
4
5
  /**
5
6
  * Represents the structure of map data that should be returned by the load map callback.
@@ -7,7 +8,7 @@ export declare const LoadMapToken = "LoadMapToken";
7
8
  *
8
9
  * @interface MapData
9
10
  */
10
- type MapData = {
11
+ export type MapData = {
11
12
  /** Raw map data that will be passed to the map component */
12
13
  data: any;
13
14
  /** CanvasEngine component that will render the map */
@@ -28,6 +29,13 @@ type MapData = {
28
29
  id?: string;
29
30
  /** Optional initial lighting state for the loaded map */
30
31
  lighting?: LightingState | null;
32
+ /** Optional render parameters passed to the map component. */
33
+ params?: Record<string, unknown>;
34
+ /** Internal controller used by a progressive map provider. */
35
+ streamController?: {
36
+ attach(map: RpgClientMap): void;
37
+ detach(): void;
38
+ };
31
39
  };
32
40
  /**
33
41
  * Callback function type for loading map data.
@@ -44,6 +52,7 @@ export declare class LoadMapService {
44
52
  private options;
45
53
  private updateMapService;
46
54
  constructor(context: Context, options: LoadMapOptions);
55
+ initialize(): void;
47
56
  load(mapId: string): Promise<MapData>;
48
57
  }
49
58
  /**
@@ -141,4 +150,3 @@ export declare function provideLoadMap(options: LoadMapOptions): {
141
150
  provide: string;
142
151
  useFactory: (context: Context) => void;
143
152
  }[];
144
- export {};
@@ -7,9 +7,10 @@ var LoadMapService = class {
7
7
  this.context = context;
8
8
  this.options = options;
9
9
  if (context["side"] === "server") return;
10
- this.updateMapService = inject(context, UpdateMapToken);
11
10
  }
11
+ initialize() {}
12
12
  async load(mapId) {
13
+ this.updateMapService ??= inject(this.context, UpdateMapToken);
13
14
  const map = await this.options(mapId.replace("map-", ""));
14
15
  await this.updateMapService.update(map);
15
16
  return map;
@@ -1 +1 @@
1
- {"version":3,"file":"loadMap.js","names":[],"sources":["../../src/services/loadMap.ts"],"sourcesContent":["import { Context, inject } from \"@signe/di\";\nimport { UpdateMapToken, UpdateMapService, type LightingState } from \"@rpgjs/common\";\n\nexport const LoadMapToken = 'LoadMapToken'\n\n/**\n * Represents the structure of map data that should be returned by the load map callback.\n * This interface defines all the properties that can be provided when loading a custom map.\n * \n * @interface MapData\n */\ntype MapData = {\n /** Raw map data that will be passed to the map component */\n data: any;\n /** CanvasEngine component that will render the map */\n component: any;\n /** Optional map width in pixels, used for viewport calculations */\n width?: number;\n /** Optional map height in pixels, used for viewport calculations */\n height?: number;\n /** Optional map events data (NPCs, interactive objects, etc.) */\n events?: any;\n /** Optional named positions, for example Tiled point objects used by changeMap(\"map\", \"name\") */\n positions?: Record<string, { x: number; y: number; z?: number }>;\n /** Optional map identifier, defaults to the mapId parameter if not provided */\n id?: string;\n /** Optional initial lighting state for the loaded map */\n lighting?: LightingState | null;\n}\n\n/**\n * Callback function type for loading map data.\n * This function receives a map ID and should return either a MapData object directly\n * or a Promise that resolves to a MapData object.\n * \n * @callback LoadMapOptions\n * @param {string} mapId - The identifier of the map to load\n * @returns {Promise<MapData> | MapData} The map data object or a promise resolving to it\n */\nexport type LoadMapOptions = (mapId: string) => Promise<MapData> | MapData \n\nexport class LoadMapService {\n private updateMapService: UpdateMapService;\n\n constructor(private context: Context, private options: LoadMapOptions) {\n if (context['side'] === 'server') {\n return\n }\n this.updateMapService = inject(context, UpdateMapToken);\n }\n\n async load(mapId: string) {\n const map = await this.options(mapId.replace('map-', ''))\n await this.updateMapService.update(map);\n return map;\n }\n}\n\n/**\n * Creates a dependency injection configuration for custom map loading on the client side.\n * \n * This function allows you to customize how maps are loaded and displayed by providing\n * a callback that defines custom map data and rendering components. It's designed to work\n * with the RPG-JS dependency injection system and enables integration of custom map formats\n * like Tiled TMX files or any other map data structure.\n * \n * The function sets up the necessary service providers for map loading, including:\n * - UpdateMapToken: Handles map updates in the client context\n * - LoadMapToken: Provides the LoadMapService with your custom loading logic\n * \n * **Design Concept:**\n * The function follows the provider pattern, creating a modular way to inject custom\n * map loading behavior into the RPG-JS client engine. It separates the concern of\n * map data fetching from map rendering, allowing developers to focus on their specific\n * map format while leveraging the engine's rendering capabilities.\n * \n * @param {LoadMapOptions} options - Callback function that handles map loading logic\n * @returns {Array<Object>} Array of dependency injection provider configurations\n * \n * @example\n * ```typescript\n * import { provideLoadMap } from '@rpgjs/client'\n * import { createModule } from '@rpgjs/common'\n * import MyTiledMapComponent from './MyTiledMapComponent.ce'\n * \n * // Basic usage with JSON map data\n * export function provideCustomMap() {\n * return createModule(\"CustomMap\", [\n * provideLoadMap(async (mapId) => {\n * const response = await fetch(`/maps/${mapId}.json`)\n * const mapData = await response.json()\n * \n * return {\n * data: mapData,\n * component: MyTiledMapComponent,\n * width: mapData.width,\n * height: mapData.height,\n * events: mapData.events\n * }\n * })\n * ])\n * }\n * \n * // Advanced usage with Tiled TMX files\n * export function provideTiledMap() {\n * return createModule(\"TiledMap\", [\n * provideLoadMap(async (mapId) => {\n * // Load TMX file\n * const tmxResponse = await fetch(`/tiled/${mapId}.tmx`)\n * const tmxData = await tmxResponse.text()\n * \n * // Parse TMX data (using a TMX parser)\n * const parsedMap = parseTMX(tmxData)\n * \n * return {\n * data: parsedMap,\n * component: TiledMapRenderer,\n * width: parsedMap.width * parsedMap.tilewidth,\n * height: parsedMap.height * parsedMap.tileheight,\n * events: parsedMap.objectGroups?.find(g => g.name === 'events')?.objects\n * }\n * })\n * ])\n * }\n * \n * // Synchronous usage for static maps\n * export function provideStaticMap() {\n * return createModule(\"StaticMap\", [\n * provideLoadMap((mapId) => {\n * const staticMaps = {\n * 'town': { tiles: [...], npcs: [...] },\n * 'dungeon': { tiles: [...], monsters: [...] }\n * }\n * \n * return {\n * data: staticMaps[mapId],\n * component: StaticMapComponent,\n * width: 800,\n * height: 600\n * }\n * })\n * ])\n * }\n * ```\n * \n * @since 4.0.0\n * @see {@link LoadMapOptions} for callback function signature\n * @see {@link MapData} for return data structure\n */\nexport function provideLoadMap(options: LoadMapOptions) {\n return [\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => {\n if (context['side'] === 'client') {\n console.warn('UpdateMapToken is not overridden')\n }\n return\n },\n },\n {\n provide: LoadMapToken,\n useFactory: (context: Context) => new LoadMapService(context, options),\n },\n ];\n}\n"],"mappings":";;;AAGA,IAAa,eAAe;AAsC5B,IAAa,iBAAb,MAA4B;CAG1B,YAAY,SAA0B,SAAiC;EAAnD,KAAA,UAAA;EAA0B,KAAA,UAAA;EAC5C,IAAI,QAAQ,YAAY,UACtB;EAEF,KAAK,mBAAmB,OAAO,SAAS,cAAc;CACxD;CAEA,MAAM,KAAK,OAAe;EACxB,MAAM,MAAM,MAAM,KAAK,QAAQ,MAAM,QAAQ,QAAQ,EAAE,CAAC;EACxD,MAAM,KAAK,iBAAiB,OAAO,GAAG;EACtC,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,SAAgB,eAAe,SAAyB;CACtD,OAAO,CACL;EACE,SAAS;EACT,aAAa,YAAqB;GAChC,IAAI,QAAQ,YAAY,UACtB,QAAQ,KAAK,kCAAkC;EAGnD;CACF,GACA;EACE,SAAS;EACT,aAAa,YAAqB,IAAI,eAAe,SAAS,OAAO;CACvE,CACF;AACF"}
1
+ {"version":3,"file":"loadMap.js","names":[],"sources":["../../src/services/loadMap.ts"],"sourcesContent":["import { Context, inject } from \"@signe/di\";\nimport { UpdateMapToken, UpdateMapService, type LightingState } from \"@rpgjs/common\";\nimport type { RpgClientMap } from \"../Game/Map\";\n\nexport const LoadMapToken = 'LoadMapToken'\n\n/**\n * Represents the structure of map data that should be returned by the load map callback.\n * This interface defines all the properties that can be provided when loading a custom map.\n * \n * @interface MapData\n */\nexport type MapData = {\n /** Raw map data that will be passed to the map component */\n data: any;\n /** CanvasEngine component that will render the map */\n component: any;\n /** Optional map width in pixels, used for viewport calculations */\n width?: number;\n /** Optional map height in pixels, used for viewport calculations */\n height?: number;\n /** Optional map events data (NPCs, interactive objects, etc.) */\n events?: any;\n /** Optional named positions, for example Tiled point objects used by changeMap(\"map\", \"name\") */\n positions?: Record<string, { x: number; y: number; z?: number }>;\n /** Optional map identifier, defaults to the mapId parameter if not provided */\n id?: string;\n /** Optional initial lighting state for the loaded map */\n lighting?: LightingState | null;\n /** Optional render parameters passed to the map component. */\n params?: Record<string, unknown>;\n /** Internal controller used by a progressive map provider. */\n streamController?: { attach(map: RpgClientMap): void; detach(): void };\n}\n\n/**\n * Callback function type for loading map data.\n * This function receives a map ID and should return either a MapData object directly\n * or a Promise that resolves to a MapData object.\n * \n * @callback LoadMapOptions\n * @param {string} mapId - The identifier of the map to load\n * @returns {Promise<MapData> | MapData} The map data object or a promise resolving to it\n */\nexport type LoadMapOptions = (mapId: string) => Promise<MapData> | MapData \n\nexport class LoadMapService {\n private updateMapService: UpdateMapService;\n\n constructor(private context: Context, private options: LoadMapOptions) {\n if (context['side'] === 'server') {\n return\n }\n }\n\n initialize(): void {}\n\n async load(mapId: string) {\n this.updateMapService ??= inject(this.context, UpdateMapToken);\n const map = await this.options(mapId.replace('map-', ''))\n await this.updateMapService.update(map);\n return map;\n }\n}\n\n/**\n * Creates a dependency injection configuration for custom map loading on the client side.\n * \n * This function allows you to customize how maps are loaded and displayed by providing\n * a callback that defines custom map data and rendering components. It's designed to work\n * with the RPG-JS dependency injection system and enables integration of custom map formats\n * like Tiled TMX files or any other map data structure.\n * \n * The function sets up the necessary service providers for map loading, including:\n * - UpdateMapToken: Handles map updates in the client context\n * - LoadMapToken: Provides the LoadMapService with your custom loading logic\n * \n * **Design Concept:**\n * The function follows the provider pattern, creating a modular way to inject custom\n * map loading behavior into the RPG-JS client engine. It separates the concern of\n * map data fetching from map rendering, allowing developers to focus on their specific\n * map format while leveraging the engine's rendering capabilities.\n * \n * @param {LoadMapOptions} options - Callback function that handles map loading logic\n * @returns {Array<Object>} Array of dependency injection provider configurations\n * \n * @example\n * ```typescript\n * import { provideLoadMap } from '@rpgjs/client'\n * import { createModule } from '@rpgjs/common'\n * import MyTiledMapComponent from './MyTiledMapComponent.ce'\n * \n * // Basic usage with JSON map data\n * export function provideCustomMap() {\n * return createModule(\"CustomMap\", [\n * provideLoadMap(async (mapId) => {\n * const response = await fetch(`/maps/${mapId}.json`)\n * const mapData = await response.json()\n * \n * return {\n * data: mapData,\n * component: MyTiledMapComponent,\n * width: mapData.width,\n * height: mapData.height,\n * events: mapData.events\n * }\n * })\n * ])\n * }\n * \n * // Advanced usage with Tiled TMX files\n * export function provideTiledMap() {\n * return createModule(\"TiledMap\", [\n * provideLoadMap(async (mapId) => {\n * // Load TMX file\n * const tmxResponse = await fetch(`/tiled/${mapId}.tmx`)\n * const tmxData = await tmxResponse.text()\n * \n * // Parse TMX data (using a TMX parser)\n * const parsedMap = parseTMX(tmxData)\n * \n * return {\n * data: parsedMap,\n * component: TiledMapRenderer,\n * width: parsedMap.width * parsedMap.tilewidth,\n * height: parsedMap.height * parsedMap.tileheight,\n * events: parsedMap.objectGroups?.find(g => g.name === 'events')?.objects\n * }\n * })\n * ])\n * }\n * \n * // Synchronous usage for static maps\n * export function provideStaticMap() {\n * return createModule(\"StaticMap\", [\n * provideLoadMap((mapId) => {\n * const staticMaps = {\n * 'town': { tiles: [...], npcs: [...] },\n * 'dungeon': { tiles: [...], monsters: [...] }\n * }\n * \n * return {\n * data: staticMaps[mapId],\n * component: StaticMapComponent,\n * width: 800,\n * height: 600\n * }\n * })\n * ])\n * }\n * ```\n * \n * @since 4.0.0\n * @see {@link LoadMapOptions} for callback function signature\n * @see {@link MapData} for return data structure\n */\nexport function provideLoadMap(options: LoadMapOptions) {\n return [\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => {\n if (context['side'] === 'client') {\n console.warn('UpdateMapToken is not overridden')\n }\n return\n },\n },\n {\n provide: LoadMapToken,\n useFactory: (context: Context) => new LoadMapService(context, options),\n },\n ];\n}\n"],"mappings":";;;AAIA,IAAa,eAAe;AA0C5B,IAAa,iBAAb,MAA4B;CAG1B,YAAY,SAA0B,SAAiC;EAAnD,KAAA,UAAA;EAA0B,KAAA,UAAA;EAC5C,IAAI,QAAQ,YAAY,UACtB;CAEJ;CAEA,aAAmB,CAAC;CAEpB,MAAM,KAAK,OAAe;EACxB,KAAK,qBAAqB,OAAO,KAAK,SAAS,cAAc;EAC7D,MAAM,MAAM,MAAM,KAAK,QAAQ,MAAM,QAAQ,QAAQ,EAAE,CAAC;EACxD,MAAM,KAAK,iBAAiB,OAAO,GAAG;EACtC,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6FA,SAAgB,eAAe,SAAyB;CACtD,OAAO,CACL;EACE,SAAS;EACT,aAAa,YAAqB;GAChC,IAAI,QAAQ,YAAY,UACtB,QAAQ,KAAK,kCAAkC;EAGnD;CACF,GACA;EACE,SAAS;EACT,aAAa,YAAqB,IAAI,eAAe,SAAS,OAAO;CACvE,CACF;AACF"}
@@ -0,0 +1,85 @@
1
+ import { Context } from '@signe/di';
2
+ import { MapChunkHitbox, MapStreamChunk, MapStreamManifest, MapStreamPacket } from '@rpgjs/common';
3
+ import { MapData, LoadMapOptions } from './loadMap';
4
+ export interface ClientMapStreamingAdapter<TManifestData = unknown, TChunkData = unknown, TState = unknown> {
5
+ /** CanvasEngine component that renders the provider-specific state. */
6
+ component: unknown;
7
+ /** Create empty client render state from public manifest data. */
8
+ createState(manifest: MapStreamManifest<TManifestData>): TState;
9
+ /** Apply one disclosed render chunk to the state. */
10
+ applyChunk(state: TState, chunk: MapStreamChunk<TChunkData>): void;
11
+ /** Remove one chunk that left the retention window. */
12
+ removeChunk(state: TState, key: string): void;
13
+ /** Return the serializable/component-ready map value exposed to CanvasEngine. */
14
+ getData(state: TState): unknown;
15
+ /** Optionally derive component parameters from the public manifest. */
16
+ getParams?(manifest: MapStreamManifest<TManifestData>): Record<string, unknown>;
17
+ }
18
+ export interface ClientMapStreamingOptions<TManifestData = unknown, TChunkData = unknown, TState = unknown> {
19
+ /** Format adapter paired with the server compiler. */
20
+ adapter: ClientMapStreamingAdapter<TManifestData, TChunkData, TState>;
21
+ /** Optional direct loader used only by standalone RPG mode. */
22
+ directLoad?: LoadMapOptions;
23
+ /** Maximum wait for the initial authoritative manifest. Defaults to 10 seconds. */
24
+ timeoutMs?: number;
25
+ }
26
+ type StreamingPhysicsMap = {
27
+ data: {
28
+ (): MapData | null;
29
+ set(value: MapData): void;
30
+ };
31
+ replaceStreamedStaticHitboxes(namespace: string, hitboxes: MapChunkHitbox[]): void;
32
+ clearStreamedStaticHitboxes(namespace: string): void;
33
+ };
34
+ export declare class MapStreamClientController<TManifestData, TChunkData, TState> {
35
+ private readonly adapter;
36
+ private state;
37
+ private manifest;
38
+ private readonly chunks;
39
+ private attachedMap?;
40
+ constructor(adapter: ClientMapStreamingAdapter<TManifestData, TChunkData, TState>, manifest: MapStreamManifest<TManifestData>);
41
+ get revision(): string;
42
+ reset(manifest: MapStreamManifest<TManifestData>): void;
43
+ receive(packet: MapStreamPacket<TManifestData, TChunkData>): void;
44
+ toMapData(): MapData;
45
+ attach(map: StreamingPhysicsMap): void;
46
+ detach(): void;
47
+ private publish;
48
+ private updatePredictionBoundary;
49
+ }
50
+ declare class MapStreamingLoadMapService<TManifestData, TChunkData, TState> {
51
+ private readonly context;
52
+ private readonly options;
53
+ private socket?;
54
+ private stream?;
55
+ private updateMap?;
56
+ constructor(context: Context, options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>);
57
+ initialize(): void;
58
+ load(mapId: string): Promise<MapData>;
59
+ }
60
+ /**
61
+ * Provide a transport-neutral client map loader backed by authoritative chunks.
62
+ * Standalone mode may keep a direct loader while MMORPG mode never fetches the
63
+ * private source map.
64
+ *
65
+ * @param options - Client adapter, optional standalone loader, and timeout.
66
+ * @returns Dependency-injection providers for the RPGJS map loader.
67
+ *
68
+ * @example
69
+ * ```ts
70
+ * provideClientMapStreaming({
71
+ * adapter: {
72
+ * component: MyMap,
73
+ * createState: (manifest) => ({ manifest, chunks: new Map() }),
74
+ * applyChunk: (state, chunk) => state.chunks.set(chunk.key, chunk.renderData),
75
+ * removeChunk: (state, key) => state.chunks.delete(key),
76
+ * getData: (state) => state,
77
+ * },
78
+ * })
79
+ * ```
80
+ */
81
+ export declare function provideClientMapStreaming<TManifestData, TChunkData, TState>(options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>): {
82
+ provide: string;
83
+ useFactory: (context: Context) => MapStreamingLoadMapService<TManifestData, TChunkData, TState>;
84
+ }[];
85
+ export {};
@@ -0,0 +1,210 @@
1
+ import { inject } from "../node_modules/.pnpm/@signe_di@3.1.0/node_modules/@signe/di/dist/index.js";
2
+ import { WebSocketToken } from "./AbstractSocket.js";
3
+ import { LoadMapToken } from "./loadMap.js";
4
+ import { MAP_STREAM_EVENT, MAP_STREAM_REQUEST_EVENT, UpdateMapToken } from "@rpgjs/common";
5
+ //#region src/services/mapStreaming.ts
6
+ var MapStreamClientController = class {
7
+ constructor(adapter, manifest) {
8
+ this.adapter = adapter;
9
+ this.chunks = /* @__PURE__ */ new Map();
10
+ this.manifest = manifest;
11
+ this.state = adapter.createState(manifest);
12
+ }
13
+ get revision() {
14
+ return this.manifest.revision;
15
+ }
16
+ reset(manifest) {
17
+ const attachedMap = this.attachedMap;
18
+ this.detach();
19
+ this.manifest = manifest;
20
+ this.state = this.adapter.createState(manifest);
21
+ this.chunks.clear();
22
+ this.attachedMap = attachedMap;
23
+ }
24
+ receive(packet) {
25
+ for (const key of packet.removed) {
26
+ this.chunks.delete(key);
27
+ this.adapter.removeChunk(this.state, key);
28
+ this.attachedMap?.clearStreamedStaticHitboxes(key);
29
+ }
30
+ for (const chunk of packet.chunks) {
31
+ this.chunks.set(chunk.key, chunk);
32
+ this.adapter.applyChunk(this.state, chunk);
33
+ this.attachedMap?.replaceStreamedStaticHitboxes(chunk.key, chunk.hitboxes);
34
+ }
35
+ this.publish();
36
+ }
37
+ toMapData() {
38
+ return {
39
+ id: this.manifest.mapId,
40
+ width: this.manifest.width,
41
+ height: this.manifest.height,
42
+ data: this.adapter.getData(this.state),
43
+ component: this.adapter.component,
44
+ params: this.adapter.getParams?.(this.manifest),
45
+ streamController: this
46
+ };
47
+ }
48
+ attach(map) {
49
+ this.attachedMap = map;
50
+ for (const chunk of this.chunks.values()) map.replaceStreamedStaticHitboxes(chunk.key, chunk.hitboxes);
51
+ this.updatePredictionBoundary();
52
+ }
53
+ detach() {
54
+ if (!this.attachedMap) return;
55
+ for (const key of this.chunks.keys()) this.attachedMap.clearStreamedStaticHitboxes(key);
56
+ this.attachedMap.clearStreamedStaticHitboxes("__boundary__");
57
+ this.attachedMap = void 0;
58
+ }
59
+ publish() {
60
+ if (!this.attachedMap) return;
61
+ const current = this.attachedMap.data();
62
+ if (current) this.attachedMap.data.set({
63
+ ...current,
64
+ data: this.adapter.getData(this.state)
65
+ });
66
+ this.updatePredictionBoundary();
67
+ }
68
+ updatePredictionBoundary() {
69
+ const map = this.attachedMap;
70
+ if (!map) return;
71
+ if (this.chunks.size === 0) {
72
+ map.clearStreamedStaticHitboxes("__boundary__");
73
+ return;
74
+ }
75
+ const chunks = [...this.chunks.values()];
76
+ const left = Math.min(...chunks.map((chunk) => chunk.bounds.x));
77
+ const top = Math.min(...chunks.map((chunk) => chunk.bounds.y));
78
+ const right = Math.max(...chunks.map((chunk) => chunk.bounds.x + chunk.bounds.width));
79
+ const bottom = Math.max(...chunks.map((chunk) => chunk.bounds.y + chunk.bounds.height));
80
+ const thickness = 2;
81
+ const barriers = [];
82
+ if (left > 0) barriers.push({
83
+ x: left - thickness,
84
+ y: top,
85
+ width: thickness,
86
+ height: bottom - top
87
+ });
88
+ if (top > 0) barriers.push({
89
+ x: left,
90
+ y: top - thickness,
91
+ width: right - left,
92
+ height: thickness
93
+ });
94
+ if (right < this.manifest.width) barriers.push({
95
+ x: right,
96
+ y: top,
97
+ width: thickness,
98
+ height: bottom - top
99
+ });
100
+ if (bottom < this.manifest.height) barriers.push({
101
+ x: left,
102
+ y: bottom,
103
+ width: right - left,
104
+ height: thickness
105
+ });
106
+ map.replaceStreamedStaticHitboxes("__boundary__", barriers);
107
+ }
108
+ };
109
+ var MapStreamingClientService = class {
110
+ constructor(socket, options) {
111
+ this.socket = socket;
112
+ this.options = options;
113
+ this.controllers = /* @__PURE__ */ new Map();
114
+ this.waiters = /* @__PURE__ */ new Map();
115
+ socket.on(MAP_STREAM_EVENT, (packet) => this.receive(packet));
116
+ }
117
+ async load(mapId) {
118
+ const normalizedId = mapId.replace(/^map-/, "");
119
+ const existing = this.controllers.get(normalizedId);
120
+ if (existing) {
121
+ this.socket.emit(MAP_STREAM_REQUEST_EVENT, { mapId: normalizedId });
122
+ return existing.toMapData();
123
+ }
124
+ const shouldRequest = !this.waiters.has(normalizedId);
125
+ const controllerPromise = new Promise((resolve, reject) => {
126
+ const timeoutMs = Math.max(1, this.options.timeoutMs ?? 1e4);
127
+ const waiter = {
128
+ resolve,
129
+ reject,
130
+ timer: void 0
131
+ };
132
+ waiter.timer = setTimeout(() => {
133
+ const remaining = (this.waiters.get(normalizedId) ?? []).filter((entry) => entry !== waiter);
134
+ if (remaining.length > 0) this.waiters.set(normalizedId, remaining);
135
+ else this.waiters.delete(normalizedId);
136
+ reject(/* @__PURE__ */ new Error(`Map stream '${normalizedId}' was not received after ${timeoutMs}ms`));
137
+ }, timeoutMs);
138
+ const waiters = this.waiters.get(normalizedId) ?? [];
139
+ waiters.push(waiter);
140
+ this.waiters.set(normalizedId, waiters);
141
+ });
142
+ if (shouldRequest) this.socket.emit(MAP_STREAM_REQUEST_EVENT, { mapId: normalizedId });
143
+ return (await controllerPromise).toMapData();
144
+ }
145
+ receive(packet) {
146
+ if (!packet || typeof packet.mapId !== "string") return;
147
+ const mapId = packet.mapId.replace(/^map-/, "");
148
+ let controller = this.controllers.get(mapId);
149
+ if (!controller) {
150
+ if (!packet.manifest) return;
151
+ controller = new MapStreamClientController(this.options.adapter, packet.manifest);
152
+ this.controllers.set(mapId, controller);
153
+ } else if (packet.manifest && controller.revision !== packet.manifest.revision) controller.reset(packet.manifest);
154
+ controller.receive(packet);
155
+ (this.waiters.get(mapId) ?? []).forEach((waiter) => {
156
+ clearTimeout(waiter.timer);
157
+ waiter.resolve(controller);
158
+ });
159
+ this.waiters.delete(mapId);
160
+ }
161
+ };
162
+ var MapStreamingLoadMapService = class {
163
+ constructor(context, options) {
164
+ this.context = context;
165
+ this.options = options;
166
+ }
167
+ initialize() {
168
+ if (this.stream) return;
169
+ this.socket = inject(this.context, WebSocketToken);
170
+ this.stream = new MapStreamingClientService(this.socket, this.options);
171
+ }
172
+ async load(mapId) {
173
+ this.initialize();
174
+ const map = this.socket?.mode === "standalone" && this.options.directLoad ? await this.options.directLoad(mapId.replace(/^map-/, "")) : await this.stream.load(mapId);
175
+ this.updateMap ??= inject(this.context, UpdateMapToken);
176
+ await this.updateMap.update(map);
177
+ return map;
178
+ }
179
+ };
180
+ /**
181
+ * Provide a transport-neutral client map loader backed by authoritative chunks.
182
+ * Standalone mode may keep a direct loader while MMORPG mode never fetches the
183
+ * private source map.
184
+ *
185
+ * @param options - Client adapter, optional standalone loader, and timeout.
186
+ * @returns Dependency-injection providers for the RPGJS map loader.
187
+ *
188
+ * @example
189
+ * ```ts
190
+ * provideClientMapStreaming({
191
+ * adapter: {
192
+ * component: MyMap,
193
+ * createState: (manifest) => ({ manifest, chunks: new Map() }),
194
+ * applyChunk: (state, chunk) => state.chunks.set(chunk.key, chunk.renderData),
195
+ * removeChunk: (state, key) => state.chunks.delete(key),
196
+ * getData: (state) => state,
197
+ * },
198
+ * })
199
+ * ```
200
+ */
201
+ function provideClientMapStreaming(options) {
202
+ return [{
203
+ provide: LoadMapToken,
204
+ useFactory: (context) => new MapStreamingLoadMapService(context, options)
205
+ }];
206
+ }
207
+ //#endregion
208
+ export { MapStreamClientController, provideClientMapStreaming };
209
+
210
+ //# sourceMappingURL=mapStreaming.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mapStreaming.js","names":[],"sources":["../../src/services/mapStreaming.ts"],"sourcesContent":["import { Context, inject } from \"@signe/di\";\nimport {\n MAP_STREAM_EVENT,\n MAP_STREAM_REQUEST_EVENT,\n type MapChunkHitbox,\n type MapStreamChunk,\n type MapStreamManifest,\n type MapStreamPacket,\n} from \"@rpgjs/common\";\nimport { AbstractWebsocket, WebSocketToken } from \"./AbstractSocket\";\nimport { LoadMapToken, type MapData, type LoadMapOptions } from \"./loadMap\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\n\nexport interface ClientMapStreamingAdapter<TManifestData = unknown, TChunkData = unknown, TState = unknown> {\n /** CanvasEngine component that renders the provider-specific state. */\n component: unknown;\n /** Create empty client render state from public manifest data. */\n createState(manifest: MapStreamManifest<TManifestData>): TState;\n /** Apply one disclosed render chunk to the state. */\n applyChunk(state: TState, chunk: MapStreamChunk<TChunkData>): void;\n /** Remove one chunk that left the retention window. */\n removeChunk(state: TState, key: string): void;\n /** Return the serializable/component-ready map value exposed to CanvasEngine. */\n getData(state: TState): unknown;\n /** Optionally derive component parameters from the public manifest. */\n getParams?(manifest: MapStreamManifest<TManifestData>): Record<string, unknown>;\n}\n\nexport interface ClientMapStreamingOptions<TManifestData = unknown, TChunkData = unknown, TState = unknown> {\n /** Format adapter paired with the server compiler. */\n adapter: ClientMapStreamingAdapter<TManifestData, TChunkData, TState>;\n /** Optional direct loader used only by standalone RPG mode. */\n directLoad?: LoadMapOptions;\n /** Maximum wait for the initial authoritative manifest. Defaults to 10 seconds. */\n timeoutMs?: number;\n}\n\ntype StreamingPhysicsMap = {\n data: { (): MapData | null; set(value: MapData): void };\n replaceStreamedStaticHitboxes(namespace: string, hitboxes: MapChunkHitbox[]): void;\n clearStreamedStaticHitboxes(namespace: string): void;\n};\n\ntype Waiter = {\n resolve(controller: MapStreamClientController<any, any, any>): void;\n reject(error: Error): void;\n timer: ReturnType<typeof setTimeout>;\n};\n\nexport class MapStreamClientController<TManifestData, TChunkData, TState> {\n private state: TState;\n private manifest: MapStreamManifest<TManifestData>;\n private readonly chunks = new Map<string, MapStreamChunk<TChunkData>>();\n private attachedMap?: StreamingPhysicsMap;\n\n constructor(\n private readonly adapter: ClientMapStreamingAdapter<TManifestData, TChunkData, TState>,\n manifest: MapStreamManifest<TManifestData>,\n ) {\n this.manifest = manifest;\n this.state = adapter.createState(manifest);\n }\n\n get revision(): string {\n return this.manifest.revision;\n }\n\n reset(manifest: MapStreamManifest<TManifestData>): void {\n const attachedMap = this.attachedMap;\n this.detach();\n this.manifest = manifest;\n this.state = this.adapter.createState(manifest);\n this.chunks.clear();\n this.attachedMap = attachedMap;\n }\n\n receive(packet: MapStreamPacket<TManifestData, TChunkData>): void {\n for (const key of packet.removed) {\n this.chunks.delete(key);\n this.adapter.removeChunk(this.state, key);\n this.attachedMap?.clearStreamedStaticHitboxes(key);\n }\n for (const chunk of packet.chunks) {\n this.chunks.set(chunk.key, chunk);\n this.adapter.applyChunk(this.state, chunk);\n this.attachedMap?.replaceStreamedStaticHitboxes(chunk.key, chunk.hitboxes);\n }\n this.publish();\n }\n\n toMapData(): MapData {\n return {\n id: this.manifest.mapId,\n width: this.manifest.width,\n height: this.manifest.height,\n data: this.adapter.getData(this.state),\n component: this.adapter.component,\n params: this.adapter.getParams?.(this.manifest),\n streamController: this,\n };\n }\n\n attach(map: StreamingPhysicsMap): void {\n this.attachedMap = map;\n for (const chunk of this.chunks.values()) {\n map.replaceStreamedStaticHitboxes(chunk.key, chunk.hitboxes);\n }\n this.updatePredictionBoundary();\n }\n\n detach(): void {\n if (!this.attachedMap) return;\n for (const key of this.chunks.keys()) {\n this.attachedMap.clearStreamedStaticHitboxes(key);\n }\n this.attachedMap.clearStreamedStaticHitboxes(\"__boundary__\");\n this.attachedMap = undefined;\n }\n\n private publish(): void {\n if (!this.attachedMap) return;\n const current = this.attachedMap.data();\n if (current) {\n this.attachedMap.data.set({ ...current, data: this.adapter.getData(this.state) });\n }\n this.updatePredictionBoundary();\n }\n\n private updatePredictionBoundary(): void {\n const map = this.attachedMap;\n if (!map) return;\n if (this.chunks.size === 0) {\n map.clearStreamedStaticHitboxes(\"__boundary__\");\n return;\n }\n const chunks = [...this.chunks.values()];\n const left = Math.min(...chunks.map((chunk) => chunk.bounds.x));\n const top = Math.min(...chunks.map((chunk) => chunk.bounds.y));\n const right = Math.max(...chunks.map((chunk) => chunk.bounds.x + chunk.bounds.width));\n const bottom = Math.max(...chunks.map((chunk) => chunk.bounds.y + chunk.bounds.height));\n const thickness = 2;\n const barriers: MapChunkHitbox[] = [];\n if (left > 0) barriers.push({ x: left - thickness, y: top, width: thickness, height: bottom - top });\n if (top > 0) barriers.push({ x: left, y: top - thickness, width: right - left, height: thickness });\n if (right < this.manifest.width) barriers.push({ x: right, y: top, width: thickness, height: bottom - top });\n if (bottom < this.manifest.height) barriers.push({ x: left, y: bottom, width: right - left, height: thickness });\n map.replaceStreamedStaticHitboxes(\"__boundary__\", barriers);\n }\n}\n\nclass MapStreamingClientService<TManifestData, TChunkData, TState> {\n private readonly controllers = new Map<string, MapStreamClientController<TManifestData, TChunkData, TState>>();\n private readonly waiters = new Map<string, Waiter[]>();\n\n constructor(\n private readonly socket: AbstractWebsocket,\n private readonly options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n ) {\n socket.on(MAP_STREAM_EVENT, (packet) => this.receive(packet));\n }\n\n async load(mapId: string): Promise<MapData> {\n const normalizedId = mapId.replace(/^map-/, \"\");\n const existing = this.controllers.get(normalizedId);\n if (existing) {\n this.socket.emit(MAP_STREAM_REQUEST_EVENT, { mapId: normalizedId });\n return existing.toMapData();\n }\n\n const shouldRequest = !this.waiters.has(normalizedId);\n const controllerPromise = new Promise<MapStreamClientController<TManifestData, TChunkData, TState>>((resolve, reject) => {\n const timeoutMs = Math.max(1, this.options.timeoutMs ?? 10_000);\n const waiter: Waiter = { resolve, reject, timer: undefined as unknown as ReturnType<typeof setTimeout> };\n const timer = setTimeout(() => {\n const current = this.waiters.get(normalizedId) ?? [];\n const remaining = current.filter((entry) => entry !== waiter);\n if (remaining.length > 0) this.waiters.set(normalizedId, remaining);\n else this.waiters.delete(normalizedId);\n reject(new Error(`Map stream '${normalizedId}' was not received after ${timeoutMs}ms`));\n }, timeoutMs);\n waiter.timer = timer;\n const waiters = this.waiters.get(normalizedId) ?? [];\n waiters.push(waiter);\n this.waiters.set(normalizedId, waiters);\n });\n // The map-room connection is established before load() runs. Request the\n // initial packet only after the waiter is registered so fast local and DO\n // transports cannot deliver it before the client is ready.\n if (shouldRequest) {\n this.socket.emit(MAP_STREAM_REQUEST_EVENT, { mapId: normalizedId });\n }\n return (await controllerPromise).toMapData();\n }\n\n private receive(packet: MapStreamPacket<TManifestData, TChunkData>): void {\n if (!packet || typeof packet.mapId !== \"string\") return;\n const mapId = packet.mapId.replace(/^map-/, \"\");\n let controller = this.controllers.get(mapId);\n if (!controller) {\n if (!packet.manifest) return;\n controller = new MapStreamClientController(this.options.adapter, packet.manifest);\n this.controllers.set(mapId, controller);\n }\n else if (packet.manifest && controller.revision !== packet.manifest.revision) {\n controller.reset(packet.manifest);\n }\n controller.receive(packet);\n\n const waiters = this.waiters.get(mapId) ?? [];\n waiters.forEach((waiter) => {\n clearTimeout(waiter.timer);\n waiter.resolve(controller!);\n });\n this.waiters.delete(mapId);\n }\n}\n\nclass MapStreamingLoadMapService<TManifestData, TChunkData, TState> {\n private socket?: AbstractWebsocket;\n private stream?: MapStreamingClientService<TManifestData, TChunkData, TState>;\n private updateMap?: UpdateMapService;\n\n constructor(\n private readonly context: Context,\n private readonly options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n ) {}\n\n initialize(): void {\n if (this.stream) return;\n this.socket = inject<AbstractWebsocket>(this.context, WebSocketToken);\n this.stream = new MapStreamingClientService(this.socket, this.options);\n }\n\n async load(mapId: string): Promise<MapData> {\n this.initialize();\n const map = this.socket?.mode === \"standalone\" && this.options.directLoad\n ? await this.options.directLoad(mapId.replace(/^map-/, \"\"))\n : await this.stream!.load(mapId);\n this.updateMap ??= inject<UpdateMapService>(this.context, UpdateMapToken);\n await this.updateMap.update(map);\n return map;\n }\n}\n\n/**\n * Provide a transport-neutral client map loader backed by authoritative chunks.\n * Standalone mode may keep a direct loader while MMORPG mode never fetches the\n * private source map.\n *\n * @param options - Client adapter, optional standalone loader, and timeout.\n * @returns Dependency-injection providers for the RPGJS map loader.\n *\n * @example\n * ```ts\n * provideClientMapStreaming({\n * adapter: {\n * component: MyMap,\n * createState: (manifest) => ({ manifest, chunks: new Map() }),\n * applyChunk: (state, chunk) => state.chunks.set(chunk.key, chunk.renderData),\n * removeChunk: (state, key) => state.chunks.delete(key),\n * getData: (state) => state,\n * },\n * })\n * ```\n */\nexport function provideClientMapStreaming<TManifestData, TChunkData, TState>(\n options: ClientMapStreamingOptions<TManifestData, TChunkData, TState>,\n) {\n return [\n {\n provide: LoadMapToken,\n useFactory: (context: Context) => new MapStreamingLoadMapService(context, options),\n },\n ];\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -9,6 +9,12 @@ export interface MmorpgOptions {
9
9
  connectionIdScope?: "local" | "session" | "ephemeral";
10
10
  query?: SocketQuery | (() => SocketQuery | undefined);
11
11
  socketOptions?: Record<string, any>;
12
+ /**
13
+ * Time allowed for a room to restore and send the RPGJS acceptance packet.
14
+ * Increase this for cold edge rooms or local Durable Object startup.
15
+ * Defaults to 10 seconds.
16
+ */
17
+ connectionAcceptanceTimeoutMs?: number;
12
18
  }
13
19
  export declare class BridgeWebsocket extends AbstractWebsocket {
14
20
  protected context: Context;
@@ -51,7 +51,7 @@ var BridgeWebsocket = class extends AbstractWebsocket {
51
51
  const pendingOn = this.pendingOn;
52
52
  this.pendingOn = [];
53
53
  pendingOn.filter(({ event }) => !this.isNativeSocketEvent(event)).forEach(({ event, callback }) => this.attachEvent(event, callback));
54
- await waitForRpgjsConnected(this.socket.conn);
54
+ await waitForRpgjsConnected(this.socket.conn, this.options.connectionAcceptanceTimeoutMs ?? 1e4);
55
55
  pendingOn.filter(({ event }) => this.isNativeSocketEvent(event)).forEach(({ event, callback }) => this.attachEvent(event, callback));
56
56
  this.emitAcceptedOpen();
57
57
  listeners?.(this.socket);
@@ -116,7 +116,7 @@ var BridgeWebsocket = class extends AbstractWebsocket {
116
116
  async reconnect(_listeners) {
117
117
  if (!this.socket?.conn) return;
118
118
  const conn = this.socket.conn;
119
- const connected = waitForRpgjsConnected(conn, 1e4, { ignoreCleanClose: true });
119
+ const connected = waitForRpgjsConnected(conn, this.options.connectionAcceptanceTimeoutMs ?? 1e4, { ignoreCleanClose: true });
120
120
  conn.reconnect();
121
121
  await connected;
122
122
  this.emitAcceptedOpen();
@@ -1 +1 @@
1
- {"version":3,"file":"mmorpg.js","names":[],"sources":["../../src/services/mmorpg.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\nimport { connectionRoom } from \"@signe/sync/client\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { isNativeSocketEvent, waitForRpgjsConnected } from \"./mmorpg-connection\";\n\nexport interface MmorpgOptions {\n host?: string;\n connectionId?: string;\n connectionIdScope?: \"local\" | \"session\" | \"ephemeral\";\n query?: SocketQuery | (() => SocketQuery | undefined);\n socketOptions?: Record<string, any>;\n}\n\nexport class BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"mmorpg\" as const;\n\n private socket: any;\n private privateId: string;\n private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];\n private acceptedOpenListeners = new Set<(data: any) => void>();\n private targetRoom = \"lobby-1\";\n\n constructor(protected context: Context, private options: MmorpgOptions = {}) {\n super(context);\n this.privateId = this.resolveConnectionId();\n }\n\n private resolveConnectionId(): string {\n if (this.options.connectionId) {\n return this.options.connectionId;\n }\n\n const scope = this.options.connectionIdScope ?? \"local\";\n const key = \"rpgjs-user-id\";\n\n if (scope === \"ephemeral\") {\n return crypto.randomUUID();\n }\n\n const storage =\n scope === \"session\"\n ? window.sessionStorage\n : window.localStorage;\n\n const existing = storage.getItem(key);\n if (existing) {\n return existing;\n }\n\n const id = crypto.randomUUID();\n storage.setItem(key, id);\n return id;\n }\n\n private resolveQuery(): SocketQuery {\n const query = typeof this.options.query === \"function\"\n ? this.options.query()\n : this.options.query;\n\n return query ?? {};\n }\n\n async connection(listeners?: (data: any) => void) {\n // tmp\n class Room {\n \n }\n const instance = new Room()\n const host = this.options.host || window.location.host;\n this.socket = await connectionRoom({\n maxRetries: 0,\n ...this.options.socketOptions,\n host,\n room: this.targetRoom,\n id: this.privateId,\n query: {\n ...this.resolveQuery(),\n id: this.privateId,\n },\n }, instance)\n\n const pendingOn = this.pendingOn;\n this.pendingOn = [];\n pendingOn\n .filter(({ event }) => !this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n await waitForRpgjsConnected(this.socket.conn);\n pendingOn\n .filter(({ event }) => this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n this.emitAcceptedOpen();\n listeners?.(this.socket)\n }\n\n on(key: string, callback: (data: any) => void) {\n if (!this.socket) {\n this.pendingOn.push({ event: key, callback });\n return;\n }\n this.attachEvent(key, callback);\n }\n\n off(event: string, callback: (data: any) => void) {\n if (!this.socket) return;\n if (event === \"open\") {\n this.acceptedOpenListeners.delete(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.removeEventListener(event, callback);\n return;\n }\n this.socket.off(event, callback);\n }\n\n emit(event: string, data: any) {\n this.socket.emit(event, data);\n }\n\n private attachEvent(event: string, callback: (data: any) => void) {\n if (event === \"open\") {\n this.acceptedOpenListeners.add(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.addEventListener(event, callback);\n return;\n }\n this.socket.on(event, callback);\n }\n\n private emitAcceptedOpen() {\n const event = new Event(\"open\");\n this.acceptedOpenListeners.forEach((callback) => callback(event));\n }\n\n updateProperties({ room, host, query }: SocketUpdateProperties) {\n if (!this.socket?.conn) return;\n this.targetRoom = room;\n this.socket.conn.updateProperties({\n room,\n id: this.privateId,\n host: host || this.options.host || window.location.host,\n query: {\n ...this.resolveQuery(),\n ...query,\n id: this.privateId,\n },\n })\n }\n\n private isNativeSocketEvent(event: string) {\n return isNativeSocketEvent(event);\n }\n\n async reconnect(_listeners?: (data: any) => void): Promise<void> {\n if (!this.socket?.conn) return;\n const conn = this.socket.conn;\n const connected = waitForRpgjsConnected(conn, 10000, { ignoreCleanClose: true });\n conn.reconnect();\n await connected;\n this.emitAcceptedOpen();\n }\n\n getCurrentRoom(): string {\n return this.targetRoom || this.socket?.conn?.room || \"lobby-1\";\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n constructor(protected context: Context, private _options: MmorpgOptions) {\n super(context);\n }\n\n async update(_map: any) {\n // In MMORPG mode, clients are untrusted and must not push map definitions.\n // Map bootstrap/update is handled server-side by @rpgjs/vite.\n return;\n }\n}\n\nexport function provideMmorpg(options: MmorpgOptions) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, options),\n },\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => new UpdateMapStandaloneService(context, options),\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mmorpg.js","names":[],"sources":["../../src/services/mmorpg.ts"],"sourcesContent":["import { Context } from \"@signe/di\";\nimport { connectionRoom } from \"@signe/sync/client\";\nimport { RpgGui } from \"../Gui/Gui\";\nimport { RpgClientEngine } from \"../RpgClientEngine\";\nimport { AbstractWebsocket, SocketQuery, SocketUpdateProperties, WebSocketToken } from \"./AbstractSocket\";\nimport { UpdateMapService, UpdateMapToken } from \"@rpgjs/common\";\nimport { provideKeyboardControls } from \"./keyboardControls\";\nimport { provideSaveClient } from \"./save\";\nimport { isNativeSocketEvent, waitForRpgjsConnected } from \"./mmorpg-connection\";\n\nexport interface MmorpgOptions {\n host?: string;\n connectionId?: string;\n connectionIdScope?: \"local\" | \"session\" | \"ephemeral\";\n query?: SocketQuery | (() => SocketQuery | undefined);\n socketOptions?: Record<string, any>;\n /**\n * Time allowed for a room to restore and send the RPGJS acceptance packet.\n * Increase this for cold edge rooms or local Durable Object startup.\n * Defaults to 10 seconds.\n */\n connectionAcceptanceTimeoutMs?: number;\n}\n\nexport class BridgeWebsocket extends AbstractWebsocket {\n readonly mode = \"mmorpg\" as const;\n\n private socket: any;\n private privateId: string;\n private pendingOn: Array<{ event: string; callback: (data: any) => void }> = [];\n private acceptedOpenListeners = new Set<(data: any) => void>();\n private targetRoom = \"lobby-1\";\n\n constructor(protected context: Context, private options: MmorpgOptions = {}) {\n super(context);\n this.privateId = this.resolveConnectionId();\n }\n\n private resolveConnectionId(): string {\n if (this.options.connectionId) {\n return this.options.connectionId;\n }\n\n const scope = this.options.connectionIdScope ?? \"local\";\n const key = \"rpgjs-user-id\";\n\n if (scope === \"ephemeral\") {\n return crypto.randomUUID();\n }\n\n const storage =\n scope === \"session\"\n ? window.sessionStorage\n : window.localStorage;\n\n const existing = storage.getItem(key);\n if (existing) {\n return existing;\n }\n\n const id = crypto.randomUUID();\n storage.setItem(key, id);\n return id;\n }\n\n private resolveQuery(): SocketQuery {\n const query = typeof this.options.query === \"function\"\n ? this.options.query()\n : this.options.query;\n\n return query ?? {};\n }\n\n async connection(listeners?: (data: any) => void) {\n // tmp\n class Room {\n \n }\n const instance = new Room()\n const host = this.options.host || window.location.host;\n this.socket = await connectionRoom({\n maxRetries: 0,\n ...this.options.socketOptions,\n host,\n room: this.targetRoom,\n id: this.privateId,\n query: {\n ...this.resolveQuery(),\n id: this.privateId,\n },\n }, instance)\n\n const pendingOn = this.pendingOn;\n this.pendingOn = [];\n pendingOn\n .filter(({ event }) => !this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n await waitForRpgjsConnected(\n this.socket.conn,\n this.options.connectionAcceptanceTimeoutMs ?? 10_000,\n );\n pendingOn\n .filter(({ event }) => this.isNativeSocketEvent(event))\n .forEach(({ event, callback }) => this.attachEvent(event, callback));\n this.emitAcceptedOpen();\n listeners?.(this.socket)\n }\n\n on(key: string, callback: (data: any) => void) {\n if (!this.socket) {\n this.pendingOn.push({ event: key, callback });\n return;\n }\n this.attachEvent(key, callback);\n }\n\n off(event: string, callback: (data: any) => void) {\n if (!this.socket) return;\n if (event === \"open\") {\n this.acceptedOpenListeners.delete(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.removeEventListener(event, callback);\n return;\n }\n this.socket.off(event, callback);\n }\n\n emit(event: string, data: any) {\n this.socket.emit(event, data);\n }\n\n private attachEvent(event: string, callback: (data: any) => void) {\n if (event === \"open\") {\n this.acceptedOpenListeners.add(callback);\n return;\n }\n if (this.isNativeSocketEvent(event)) {\n this.socket.conn.addEventListener(event, callback);\n return;\n }\n this.socket.on(event, callback);\n }\n\n private emitAcceptedOpen() {\n const event = new Event(\"open\");\n this.acceptedOpenListeners.forEach((callback) => callback(event));\n }\n\n updateProperties({ room, host, query }: SocketUpdateProperties) {\n if (!this.socket?.conn) return;\n this.targetRoom = room;\n this.socket.conn.updateProperties({\n room,\n id: this.privateId,\n host: host || this.options.host || window.location.host,\n query: {\n ...this.resolveQuery(),\n ...query,\n id: this.privateId,\n },\n })\n }\n\n private isNativeSocketEvent(event: string) {\n return isNativeSocketEvent(event);\n }\n\n async reconnect(_listeners?: (data: any) => void): Promise<void> {\n if (!this.socket?.conn) return;\n const conn = this.socket.conn;\n const connected = waitForRpgjsConnected(\n conn,\n this.options.connectionAcceptanceTimeoutMs ?? 10_000,\n { ignoreCleanClose: true },\n );\n conn.reconnect();\n await connected;\n this.emitAcceptedOpen();\n }\n\n getCurrentRoom(): string {\n return this.targetRoom || this.socket?.conn?.room || \"lobby-1\";\n }\n}\n\nclass UpdateMapStandaloneService extends UpdateMapService {\n constructor(protected context: Context, private _options: MmorpgOptions) {\n super(context);\n }\n\n async update(_map: any) {\n // In MMORPG mode, clients are untrusted and must not push map definitions.\n // Map bootstrap/update is handled server-side by @rpgjs/vite.\n return;\n }\n}\n\nexport function provideMmorpg(options: MmorpgOptions) {\n return [\n {\n provide: WebSocketToken,\n useFactory: (context: Context) => new BridgeWebsocket(context, options),\n },\n {\n provide: UpdateMapToken,\n useFactory: (context: Context) => new UpdateMapStandaloneService(context, options),\n },\n provideKeyboardControls(),\n provideSaveClient(),\n RpgGui,\n RpgClientEngine,\n ];\n}\n"],"mappings":";;;;;;;;;AAwBA,IAAa,kBAAb,cAAqC,kBAAkB;CASrD,YAAY,SAA4B,UAAiC,CAAC,GAAG;EAC3E,MAAM,OAAO;EADO,KAAA,UAAA;EAA0B,KAAA,UAAA;cARhC;mBAI6D,CAAC;+CAC9C,IAAI,IAAyB;oBACxC;EAInB,KAAK,YAAY,KAAK,oBAAoB;CAC5C;CAEA,sBAAsC;EACpC,IAAI,KAAK,QAAQ,cACf,OAAO,KAAK,QAAQ;EAGtB,MAAM,QAAQ,KAAK,QAAQ,qBAAqB;EAChD,MAAM,MAAM;EAEZ,IAAI,UAAU,aACZ,OAAO,OAAO,WAAW;EAG3B,MAAM,UACJ,UAAU,YACN,OAAO,iBACP,OAAO;EAEb,MAAM,WAAW,QAAQ,QAAQ,GAAG;EACpC,IAAI,UACF,OAAO;EAGT,MAAM,KAAK,OAAO,WAAW;EAC7B,QAAQ,QAAQ,KAAK,EAAE;EACvB,OAAO;CACT;CAEA,eAAoC;EAKlC,QAJc,OAAO,KAAK,QAAQ,UAAU,aACxC,KAAK,QAAQ,MAAM,IACnB,KAAK,QAAQ,UAED,CAAC;CACnB;CAEA,MAAM,WAAW,WAAiC;EAEhD,MAAM,KAAK,CAEX;EACA,MAAM,WAAW,IAAI,KAAK;EAC1B,MAAM,OAAO,KAAK,QAAQ,QAAQ,OAAO,SAAS;EAClD,KAAK,SAAS,MAAM,eAAe;GAC/B,YAAY;GACZ,GAAG,KAAK,QAAQ;GAChB;GACA,MAAM,KAAK;GACX,IAAI,KAAK;GACT,OAAO;IACL,GAAG,KAAK,aAAa;IACrB,IAAI,KAAK;GACX;EACJ,GAAG,QAAQ;EAEX,MAAM,YAAY,KAAK;EACvB,KAAK,YAAY,CAAC;EAClB,UACG,QAAQ,EAAE,YAAY,CAAC,KAAK,oBAAoB,KAAK,CAAC,EACtD,SAAS,EAAE,OAAO,eAAe,KAAK,YAAY,OAAO,QAAQ,CAAC;EACrE,MAAM,sBACJ,KAAK,OAAO,MACZ,KAAK,QAAQ,iCAAiC,GAChD;EACA,UACG,QAAQ,EAAE,YAAY,KAAK,oBAAoB,KAAK,CAAC,EACrD,SAAS,EAAE,OAAO,eAAe,KAAK,YAAY,OAAO,QAAQ,CAAC;EACrE,KAAK,iBAAiB;EACtB,YAAY,KAAK,MAAM;CACzB;CAEA,GAAG,KAAa,UAA+B;EAC7C,IAAI,CAAC,KAAK,QAAQ;GAChB,KAAK,UAAU,KAAK;IAAE,OAAO;IAAK;GAAS,CAAC;GAC5C;EACF;EACA,KAAK,YAAY,KAAK,QAAQ;CAChC;CAEA,IAAI,OAAe,UAA+B;EAChD,IAAI,CAAC,KAAK,QAAQ;EAClB,IAAI,UAAU,QAAQ;GACpB,KAAK,sBAAsB,OAAO,QAAQ;GAC1C;EACF;EACA,IAAI,KAAK,oBAAoB,KAAK,GAAG;GACnC,KAAK,OAAO,KAAK,oBAAoB,OAAO,QAAQ;GACpD;EACF;EACA,KAAK,OAAO,IAAI,OAAO,QAAQ;CACjC;CAEA,KAAK,OAAe,MAAW;EAC7B,KAAK,OAAO,KAAK,OAAO,IAAI;CAC9B;CAEA,YAAoB,OAAe,UAA+B;EAChE,IAAI,UAAU,QAAQ;GACpB,KAAK,sBAAsB,IAAI,QAAQ;GACvC;EACF;EACA,IAAI,KAAK,oBAAoB,KAAK,GAAG;GACnC,KAAK,OAAO,KAAK,iBAAiB,OAAO,QAAQ;GACjD;EACF;EACA,KAAK,OAAO,GAAG,OAAO,QAAQ;CAChC;CAEA,mBAA2B;EACzB,MAAM,QAAQ,IAAI,MAAM,MAAM;EAC9B,KAAK,sBAAsB,SAAS,aAAa,SAAS,KAAK,CAAC;CAClE;CAEA,iBAAiB,EAAE,MAAM,MAAM,SAAiC;EAC9D,IAAI,CAAC,KAAK,QAAQ,MAAM;EACxB,KAAK,aAAa;EAClB,KAAK,OAAO,KAAK,iBAAiB;GAChC;GACA,IAAI,KAAK;GACT,MAAM,QAAQ,KAAK,QAAQ,QAAQ,OAAO,SAAS;GACnD,OAAO;IACL,GAAG,KAAK,aAAa;IACrB,GAAG;IACH,IAAI,KAAK;GACX;EACF,CAAC;CACH;CAEA,oBAA4B,OAAe;EACzC,OAAO,oBAAoB,KAAK;CAClC;CAEA,MAAM,UAAU,YAAiD;EAC/D,IAAI,CAAC,KAAK,QAAQ,MAAM;EACxB,MAAM,OAAO,KAAK,OAAO;EACzB,MAAM,YAAY,sBAChB,MACA,KAAK,QAAQ,iCAAiC,KAC9C,EAAE,kBAAkB,KAAK,CAC3B;EACA,KAAK,UAAU;EACf,MAAM;EACN,KAAK,iBAAiB;CACxB;CAEA,iBAAyB;EACvB,OAAO,KAAK,cAAc,KAAK,QAAQ,MAAM,QAAQ;CACvD;AACF;AAEA,IAAM,6BAAN,cAAyC,iBAAiB;CACxD,YAAY,SAA4B,UAAiC;EACvE,MAAM,OAAO;EADO,KAAA,UAAA;EAA0B,KAAA,WAAA;CAEhD;CAEA,MAAM,OAAO,MAAW,CAIxB;AACF;AAEA,SAAgB,cAAc,SAAwB;CACpD,OAAO;EACL;GACE,SAAS;GACT,aAAa,YAAqB,IAAI,gBAAgB,SAAS,OAAO;EACxE;EACA;GACE,SAAS;GACT,aAAa,YAAqB,IAAI,2BAA2B,SAAS,OAAO;EACnF;EACA,wBAAwB;EACxB,kBAAkB;EAClB;EACA;CACF;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rpgjs/client",
3
- "version": "5.0.0-beta.24",
3
+ "version": "5.0.0-beta.26",
4
4
  "description": "RPGJS is a framework for creating RPG/MMORPG games",
5
5
  "main": "dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -22,9 +22,9 @@
22
22
  "pixi.js": "^8.9.2"
23
23
  },
24
24
  "dependencies": {
25
- "@rpgjs/common": "5.0.0-beta.23",
26
- "@rpgjs/server": "5.0.0-beta.24",
27
- "@rpgjs/ui-css": "5.0.0-beta.22",
25
+ "@rpgjs/common": "5.0.0-beta.25",
26
+ "@rpgjs/server": "5.0.0-beta.26",
27
+ "@rpgjs/ui-css": "5.0.0-beta.23",
28
28
  "@signe/di": "3.1.0",
29
29
  "@signe/room": "3.1.0",
30
30
  "@signe/sync": "3.1.0",
package/src/Game/Map.ts CHANGED
@@ -59,6 +59,7 @@ export class RpgClientMap extends RpgCommonMap<any> {
59
59
  currentPlayer = computed(() => this.players()[this.engine.playerIdSignal()!])
60
60
  weatherState = signal<WeatherState | null>(null);
61
61
  localWeatherOverride = signal<WeatherState | null>(null);
62
+ private localWeatherValue: WeatherState | null = null;
62
63
  lightingState = signal<LightingState | null>(null);
63
64
  localLightSpots = signal<Record<string, LightSpot>>({});
64
65
  weather = computed<WeatherState | null>(() => {
@@ -132,6 +133,7 @@ export class RpgClientMap extends RpgCommonMap<any> {
132
133
  );
133
134
  this.events.set({})
134
135
  this.weatherState.set(null);
136
+ this.localWeatherValue = null;
135
137
  this.localWeatherOverride.set(null);
136
138
  this.lightingState.set(null);
137
139
  this.localLightSpots.set({});
@@ -143,10 +145,15 @@ export class RpgClientMap extends RpgCommonMap<any> {
143
145
  }
144
146
 
145
147
  setLocalWeather(next: WeatherState | null): void {
148
+ if (this.localWeatherValue === next) {
149
+ return;
150
+ }
151
+ this.localWeatherValue = next;
146
152
  this.localWeatherOverride.set(next);
147
153
  }
148
154
 
149
155
  clearLocalWeather(): void {
156
+ this.localWeatherValue = null;
150
157
  this.localWeatherOverride.set(null);
151
158
  }
152
159
 
@@ -14,6 +14,7 @@ vi.mock("../components/gui", () => {
14
14
  NotificationComponent: component,
15
15
  TitleScreenComponent: component,
16
16
  GameoverComponent: component,
17
+ InputComponent: component,
17
18
  };
18
19
  });
19
20
 
@@ -64,6 +65,10 @@ const VueTooltip = {
64
65
  };
65
66
 
66
67
  describe("RpgGui Vue integration", () => {
68
+ test("registers the prebuilt input GUI", async () => {
69
+ const { gui } = await createGui();
70
+ expect(gui.get(PrebuiltGui.Input)).toBeDefined();
71
+ });
67
72
  test("tracks GUI open ids and sends them back when closing", async () => {
68
73
  const { gui, socket } = await createGui();
69
74
  await gui._initialize();