@idosgames/module-sdk 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- 'use strict';var x=/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*@[1-9]\d*$/;function S(e){return typeof e=="string"&&x.test(e)}function T(e){let t=e.indexOf(":");return t<0?"":e.slice(0,t)}var c=/^(string|number|boolean)(\[\])?(\?)?$/;function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function l(e,t){return e==="number"?typeof t=="number"&&Number.isFinite(t):typeof t===e}function f(e,t,r){if(!u(t))return `${r||"payload"} must be an object`;for(let[n,o]of Object.entries(e)){let i=r?`${r}.${n}`:n,a=t[n];if(typeof o!="string"){let p=f(o,a,i);if(p)return p;continue}let d=c.exec(o);if(!d)return `${i}: invalid descriptor "${o}"`;let[,s="",g,m]=d;if(a===void 0){if(m)continue;return `${i} is missing`}if(g){if(!Array.isArray(a))return `${i} must be ${s}[]`;if(!a.every(p=>l(s,p)))return `${i} must contain only ${s} values`;continue}if(!l(s,a))return `${i} must be a ${s}`}return null}function y(e,t){for(let[r,n]of Object.entries(e)){let o=t?`${t}.${r}`:r;if(typeof n=="string"){if(!c.test(n))throw new Error(`shape(): field "${o}" has invalid type "${n}" \u2014 use "string" | "number" | "boolean", optionally with "[]" and/or a trailing "?"`)}else if(u(n))y(n,o);else throw new Error(`shape(): field "${o}" must be a type string or an object`)}}function h(e){return y(e,""),{descriptor:e,check:t=>f(e,t,"")}}function v(e,t){if(!S(e))throw new Error(`defineTopic(): "${e}" is not a valid topic name \u2014 expected "<module-id>:<event>@<major>", e.g. "idle-rpg:character-upgraded@1"`);return {name:e,shape:t}}var b={modeChanged:v("host:mode-changed@1",h({from:"string?",to:"string"}))};var M=["currency-bar","wallet","status","account","menu","modes"];function R(e){return typeof e=="string"&&M.includes(e)}function D(e){return e}exports.SHARED_UI_ROLES=M;exports.TOPIC_NAME_RE=x;exports.defineModule=D;exports.defineTopic=v;exports.hostTopics=b;exports.isSharedUiRole=R;exports.isTopicName=S;exports.shape=h;exports.topicNamespace=T;
1
+ 'use strict';var x=/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*@[1-9]\d*$/;function S(e){return typeof e=="string"&&x.test(e)}function M(e){let t=e.indexOf(":");return t<0?"":e.slice(0,t)}var c=/^(string|number|boolean)(\[\])?(\?)?$/;function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function l(e,t){return e==="number"?typeof t=="number"&&Number.isFinite(t):typeof t===e}function f(e,t,r){if(!u(t))return `${r||"payload"} must be an object`;for(let[n,o]of Object.entries(e)){let i=r?`${r}.${n}`:n,a=t[n];if(typeof o!="string"){let p=f(o,a,i);if(p)return p;continue}let d=c.exec(o);if(!d)return `${i}: invalid descriptor "${o}"`;let[,s="",g,m]=d;if(a===void 0){if(m)continue;return `${i} is missing`}if(g){if(!Array.isArray(a))return `${i} must be ${s}[]`;if(!a.every(p=>l(s,p)))return `${i} must contain only ${s} values`;continue}if(!l(s,a))return `${i} must be a ${s}`}return null}function y(e,t){for(let[r,n]of Object.entries(e)){let o=t?`${t}.${r}`:r;if(typeof n=="string"){if(!c.test(n))throw new Error(`shape(): field "${o}" has invalid type "${n}" \u2014 use "string" | "number" | "boolean", optionally with "[]" and/or a trailing "?"`)}else if(u(n))y(n,o);else throw new Error(`shape(): field "${o}" must be a type string or an object`)}}function h(e){return y(e,""),{descriptor:e,check:t=>f(e,t,"")}}function v(e,t){if(!S(e))throw new Error(`defineTopic(): "${e}" is not a valid topic name \u2014 expected "<module-id>:<event>@<major>", e.g. "idle-rpg:character-upgraded@1"`);return {name:e,shape:t}}var T={modeChanged:v("host:mode-changed@1",h({from:"string?",to:"string"}))};var b=["currency-bar","wallet","status","account","menu","modes"];function R(e){return typeof e=="string"&&b.includes(e)}function D(e){return e}exports.SHARED_UI_ROLES=b;exports.TOPIC_NAME_RE=x;exports.defineModule=D;exports.defineTopic=v;exports.hostTopics=T;exports.isSharedUiRole=R;exports.isTopicName=S;exports.shape=h;exports.topicNamespace=M;
package/dist/index.d.cts CHANGED
@@ -217,8 +217,12 @@ interface FeatureScreenProps {
217
217
  interface FeatureDefinition {
218
218
  /** Unique across the game — conventionally the owning module's id ("store"). */
219
219
  id: string;
220
- /** Menu label, already in the player's language. */
221
- label: string;
220
+ /**
221
+ * Menu label in the player's language. Prefer a function (`label: () => t("title")`): the lobby
222
+ * calls it on every render, so the tab follows the language switch (UI kit `setLanguage`) without
223
+ * a reload. A plain string stays as registered.
224
+ */
225
+ label: string | (() => string);
222
226
  /** Glyph name of @idosgames/react/ui ("shop", "hero"…), an emoji, or an image URL. */
223
227
  icon?: string;
224
228
  group?: FeatureGroup;
@@ -236,7 +240,9 @@ interface FeatureDefinition {
236
240
  available?: boolean;
237
241
  }
238
242
  /** A registered feature plus its live badge and availability. */
239
- interface FeatureEntry extends FeatureDefinition {
243
+ interface FeatureEntry extends Omit<FeatureDefinition, "label"> {
244
+ /** The label now — a function label is called on every read. */
245
+ readonly label: string;
240
246
  badge: number;
241
247
  /** The module that registered it. */
242
248
  moduleId: string;
@@ -357,6 +363,84 @@ interface MultiplayerRegistry {
357
363
  subscribe(listener: () => void): () => void;
358
364
  }
359
365
 
366
+ /** A button under the showcase or on the "Play" card. */
367
+ interface LobbyHeroAction {
368
+ id: string;
369
+ /** Already in the player's language. */
370
+ label: string;
371
+ /** Glyph name of @idosgames/react/ui ("edit", "star"…), an emoji, or an image URL. */
372
+ icon?: string;
373
+ /** Pressed. Omit when the button only opens `menu`. */
374
+ run?(): void;
375
+ /** A short list shown when the button is pressed (an emote picker) — each entry runs on its own. */
376
+ menu?: LobbyHeroAction[];
377
+ disabled?: boolean;
378
+ }
379
+ /** What the lobby lays out around the showcase. Read through `view()`, redrawn on `subscribe`. */
380
+ interface LobbyHeroView {
381
+ /** Big name under the showcase (the character's name). */
382
+ title?: string;
383
+ /** A line under it. */
384
+ subtitle?: string;
385
+ /** Buttons under the name ("Edit", "Emote"). */
386
+ actions?: LobbyHeroAction[];
387
+ /**
388
+ * The card next to "Play" — WHAT will be played: a caption ("World"), a name ("Island"), buttons
389
+ * to change it ("Change", "New"). Omitted — "Play" alone.
390
+ */
391
+ play?: {
392
+ caption?: string;
393
+ title?: string;
394
+ actions?: LobbyHeroAction[];
395
+ /** Overflow menu of the card ("⋯": download, upload…). */
396
+ more?: LobbyHeroAction[];
397
+ };
398
+ }
399
+ /** Handed to `mount`: where on the host the main figure should stand. */
400
+ interface LobbyHeroStage {
401
+ /**
402
+ * The free spot between the lobby's interface where the figure should stand, in CSS pixels of the
403
+ * host element (it moves with the layout — read it every frame). `null` — not laid out yet.
404
+ */
405
+ spot(): {
406
+ x: number;
407
+ y: number;
408
+ w: number;
409
+ h: number;
410
+ } | null;
411
+ }
412
+ interface LobbyHeroDefinition {
413
+ /** The game mode "Play" starts (a route id of the game). */
414
+ mode: string;
415
+ /**
416
+ * Put the showcase into `host` — an element filling the lobby's home behind its interface. Called
417
+ * when the home is shown, the returned function when it is left (stop the render loop there). The
418
+ * same hero may be mounted again later.
419
+ */
420
+ mount(host: HTMLElement, stage: LobbyHeroStage): () => void;
421
+ /** The current texts and buttons. Cheap — called on every redraw. */
422
+ view(): LobbyHeroView;
423
+ /** Called by the hero whenever `view()` would answer differently. Returns the unsubscribe. */
424
+ subscribe(listener: () => void): () => void;
425
+ /**
426
+ * "Play" was pressed: return false to stay in the lobby (nothing to play yet). Default: switch to
427
+ * `mode`.
428
+ */
429
+ beforePlay?(): boolean;
430
+ }
431
+ /** `ctx.lobby` — the app's lobby as seen by a module. */
432
+ interface LobbyRegistry {
433
+ /** Put this game's showcase on the lobby's home. Returns the unregister function. */
434
+ setHero(hero: LobbyHeroDefinition): () => void;
435
+ /** The hero on the home now (the lobby reads it). */
436
+ hero(): LobbyHeroDefinition | null;
437
+ subscribe(listener: () => void): () => void;
438
+ /** Whether the app has a lobby to return to (the base is installed). */
439
+ available(): boolean;
440
+ /** Go to the lobby — a game's "To the lobby" in its pause menu. False — there is no lobby. */
441
+ open(): boolean;
442
+ }
443
+
360
444
  /** One piece of work on the loading screen. Call `done()` exactly once, also on failure. */
361
445
  interface LoadingTask {
362
446
  /**
@@ -594,6 +678,19 @@ interface ModuleContext {
594
678
  registerPanel(panel: UiPanel): void;
595
679
  /** Register a nav / mode entry so the player can switch to this module. */
596
680
  registerRoute(entry: RouteEntry): void;
681
+ /**
682
+ * Runs `run` now and keeps what it started alive until the module is torn down (logout, a new
683
+ * login): the function it returns is called then. Use it for background work that needs no screen -
684
+ * keeping a menu badge in step with the player's state (`client.on("user:anyUpdated", …)` returns
685
+ * its unsubscribe, return that), a timer, a document listener. Do not mount a hidden React panel
686
+ * for this.
687
+ */
688
+ effect(run: () => void | (() => void)): void;
689
+ /**
690
+ * Calls `run` now and then every `ms` milliseconds until the module is torn down - the polling
691
+ * form of `effect`, with the timer cleared for you.
692
+ */
693
+ every(ms: number, run: () => void): void;
597
694
  /**
598
695
  * Content types other modules can publish and open (see `ContentTypeHandler`). A game registers
599
696
  * its type here; the Workshop module reads the registry — neither imports the other.
@@ -616,6 +713,12 @@ interface ModuleContext {
616
713
  * builds its "Play" buttons from it; a game can tell whether a lobby exists to return to.
617
714
  */
618
715
  modes: ModeRegistry;
716
+ /**
717
+ * The app's lobby: a game puts its showcase on the lobby's home (`setHero` — its character on a
718
+ * pedestal, "Play" next to it) and sends the player back to it from its pause menu (`open`). See
719
+ * `LobbyHeroDefinition`.
720
+ */
721
+ lobby: LobbyRegistry;
619
722
  /**
620
723
  * Switch the host to another mode (a route id registered by any module) — e.g. the lobby starts a
621
724
  * game, the Workshop opens a world and hands the screen to the game. Unknown ids are ignored. Called
@@ -777,9 +880,15 @@ interface RouteEntry {
777
880
  * A game that lives INSIDE the lobby: the base keeps the lobby's balances on top and its tabs at
778
881
  * the bottom around it, "Play" is this game, and the app starts in it after sign-in (idle-rpg).
779
882
  * Without it (the default) a game takes the whole screen, launched by "Play" in the lobby, with a
780
- * small button back (voxelcraft, board-game).
883
+ * small button back (board-game).
781
884
  */
782
885
  inLobby?: boolean;
886
+ /**
887
+ * A full-screen game with its OWN way back to the lobby (a pause menu with "To the lobby" that
888
+ * calls `ctx.lobby.open()` — voxelcraft): the base does not draw its round button back over it.
889
+ * The game must then offer that way on every device (a pause button on touch screens).
890
+ */
891
+ ownExit?: boolean;
783
892
  }
784
893
  /** A mode of the app as every module sees it (`ctx.modes`): a registered route plus its owner. */
785
894
  interface ModeEntry {
@@ -794,6 +903,8 @@ interface ModeEntry {
794
903
  moduleType: ModuleType;
795
904
  /** The route's `inLobby`: the game sits inside the lobby rather than taking the whole screen. */
796
905
  inLobby?: boolean;
906
+ /** The route's `ownExit`: the game leads back to the lobby itself — no button back over it. */
907
+ ownExit?: boolean;
797
908
  }
798
909
  /** `ctx.modes` — the modes of the app, live. Switch with `ctx.navigate(id)`. */
799
910
  interface ModeRegistry {
@@ -819,4 +930,4 @@ interface SharedEventBus<Events extends Record<string, unknown> = Record<string,
819
930
  /** Identity helper that pins a module literal to the `Module` type for editor help and errors. */
820
931
  declare function defineModule(module: Module): Module;
821
932
 
822
- export { type AppNotifications, type CapturedContent, type ContentFileData, type ContentRegistry, type ContentTypeHandler, type DownloadedContent, type EngineScene, type FeatureDefinition, type FeatureEntry, type FeatureGroup, type FeaturePresenter, type FeatureRegistry, type FeatureScreenProps, type InferShape, type LoadingReporter, type LoadingTask, type LocalContentItem, type ModeEntry, type ModeRegistry, type Module, type ModuleAgentApi, type ModuleAuthor, type ModuleContext, type ModuleEngine, type ModuleEventEmit, type ModuleEventListen, type ModuleEvents, type ModuleEventsManifest, type ModuleManifest, type ModuleMedia, type ModuleMeta, type ModuleSharedUi, type ModuleType, type MultiplayerGame, type MultiplayerRegistry, type MultiplayerRoom, type MultiplayerScreenArgs, type MultiplayerSetting, type NotificationAction, type NotificationInput, type NotificationLevel, type PanelSlot, type RouteEntry, SHARED_UI_ROLES, type SceneMountContext, type Shape, type ShapeDescriptor, type ShapeLeaf, type ShapeLeafSpec, type SharedEventBus, type SharedUiContext, type SharedUiRole, type SurfaceAllocator, type SurfaceHandle, type SurfaceKind, TOPIC_NAME_RE, type Topic, type TopicName, type TopicPayload, type UiPanel, defineModule, defineTopic, hostTopics, isSharedUiRole, isTopicName, shape, topicNamespace };
933
+ export { type AppNotifications, type CapturedContent, type ContentFileData, type ContentRegistry, type ContentTypeHandler, type DownloadedContent, type EngineScene, type FeatureDefinition, type FeatureEntry, type FeatureGroup, type FeaturePresenter, type FeatureRegistry, type FeatureScreenProps, type InferShape, type LoadingReporter, type LoadingTask, type LobbyHeroAction, type LobbyHeroDefinition, type LobbyHeroStage, type LobbyHeroView, type LobbyRegistry, type LocalContentItem, type ModeEntry, type ModeRegistry, type Module, type ModuleAgentApi, type ModuleAuthor, type ModuleContext, type ModuleEngine, type ModuleEventEmit, type ModuleEventListen, type ModuleEvents, type ModuleEventsManifest, type ModuleManifest, type ModuleMedia, type ModuleMeta, type ModuleSharedUi, type ModuleType, type MultiplayerGame, type MultiplayerRegistry, type MultiplayerRoom, type MultiplayerScreenArgs, type MultiplayerSetting, type NotificationAction, type NotificationInput, type NotificationLevel, type PanelSlot, type RouteEntry, SHARED_UI_ROLES, type SceneMountContext, type Shape, type ShapeDescriptor, type ShapeLeaf, type ShapeLeafSpec, type SharedEventBus, type SharedUiContext, type SharedUiRole, type SurfaceAllocator, type SurfaceHandle, type SurfaceKind, TOPIC_NAME_RE, type Topic, type TopicName, type TopicPayload, type UiPanel, defineModule, defineTopic, hostTopics, isSharedUiRole, isTopicName, shape, topicNamespace };
package/dist/index.d.ts CHANGED
@@ -217,8 +217,12 @@ interface FeatureScreenProps {
217
217
  interface FeatureDefinition {
218
218
  /** Unique across the game — conventionally the owning module's id ("store"). */
219
219
  id: string;
220
- /** Menu label, already in the player's language. */
221
- label: string;
220
+ /**
221
+ * Menu label in the player's language. Prefer a function (`label: () => t("title")`): the lobby
222
+ * calls it on every render, so the tab follows the language switch (UI kit `setLanguage`) without
223
+ * a reload. A plain string stays as registered.
224
+ */
225
+ label: string | (() => string);
222
226
  /** Glyph name of @idosgames/react/ui ("shop", "hero"…), an emoji, or an image URL. */
223
227
  icon?: string;
224
228
  group?: FeatureGroup;
@@ -236,7 +240,9 @@ interface FeatureDefinition {
236
240
  available?: boolean;
237
241
  }
238
242
  /** A registered feature plus its live badge and availability. */
239
- interface FeatureEntry extends FeatureDefinition {
243
+ interface FeatureEntry extends Omit<FeatureDefinition, "label"> {
244
+ /** The label now — a function label is called on every read. */
245
+ readonly label: string;
240
246
  badge: number;
241
247
  /** The module that registered it. */
242
248
  moduleId: string;
@@ -357,6 +363,84 @@ interface MultiplayerRegistry {
357
363
  subscribe(listener: () => void): () => void;
358
364
  }
359
365
 
366
+ /** A button under the showcase or on the "Play" card. */
367
+ interface LobbyHeroAction {
368
+ id: string;
369
+ /** Already in the player's language. */
370
+ label: string;
371
+ /** Glyph name of @idosgames/react/ui ("edit", "star"…), an emoji, or an image URL. */
372
+ icon?: string;
373
+ /** Pressed. Omit when the button only opens `menu`. */
374
+ run?(): void;
375
+ /** A short list shown when the button is pressed (an emote picker) — each entry runs on its own. */
376
+ menu?: LobbyHeroAction[];
377
+ disabled?: boolean;
378
+ }
379
+ /** What the lobby lays out around the showcase. Read through `view()`, redrawn on `subscribe`. */
380
+ interface LobbyHeroView {
381
+ /** Big name under the showcase (the character's name). */
382
+ title?: string;
383
+ /** A line under it. */
384
+ subtitle?: string;
385
+ /** Buttons under the name ("Edit", "Emote"). */
386
+ actions?: LobbyHeroAction[];
387
+ /**
388
+ * The card next to "Play" — WHAT will be played: a caption ("World"), a name ("Island"), buttons
389
+ * to change it ("Change", "New"). Omitted — "Play" alone.
390
+ */
391
+ play?: {
392
+ caption?: string;
393
+ title?: string;
394
+ actions?: LobbyHeroAction[];
395
+ /** Overflow menu of the card ("⋯": download, upload…). */
396
+ more?: LobbyHeroAction[];
397
+ };
398
+ }
399
+ /** Handed to `mount`: where on the host the main figure should stand. */
400
+ interface LobbyHeroStage {
401
+ /**
402
+ * The free spot between the lobby's interface where the figure should stand, in CSS pixels of the
403
+ * host element (it moves with the layout — read it every frame). `null` — not laid out yet.
404
+ */
405
+ spot(): {
406
+ x: number;
407
+ y: number;
408
+ w: number;
409
+ h: number;
410
+ } | null;
411
+ }
412
+ interface LobbyHeroDefinition {
413
+ /** The game mode "Play" starts (a route id of the game). */
414
+ mode: string;
415
+ /**
416
+ * Put the showcase into `host` — an element filling the lobby's home behind its interface. Called
417
+ * when the home is shown, the returned function when it is left (stop the render loop there). The
418
+ * same hero may be mounted again later.
419
+ */
420
+ mount(host: HTMLElement, stage: LobbyHeroStage): () => void;
421
+ /** The current texts and buttons. Cheap — called on every redraw. */
422
+ view(): LobbyHeroView;
423
+ /** Called by the hero whenever `view()` would answer differently. Returns the unsubscribe. */
424
+ subscribe(listener: () => void): () => void;
425
+ /**
426
+ * "Play" was pressed: return false to stay in the lobby (nothing to play yet). Default: switch to
427
+ * `mode`.
428
+ */
429
+ beforePlay?(): boolean;
430
+ }
431
+ /** `ctx.lobby` — the app's lobby as seen by a module. */
432
+ interface LobbyRegistry {
433
+ /** Put this game's showcase on the lobby's home. Returns the unregister function. */
434
+ setHero(hero: LobbyHeroDefinition): () => void;
435
+ /** The hero on the home now (the lobby reads it). */
436
+ hero(): LobbyHeroDefinition | null;
437
+ subscribe(listener: () => void): () => void;
438
+ /** Whether the app has a lobby to return to (the base is installed). */
439
+ available(): boolean;
440
+ /** Go to the lobby — a game's "To the lobby" in its pause menu. False — there is no lobby. */
441
+ open(): boolean;
442
+ }
443
+
360
444
  /** One piece of work on the loading screen. Call `done()` exactly once, also on failure. */
361
445
  interface LoadingTask {
362
446
  /**
@@ -594,6 +678,19 @@ interface ModuleContext {
594
678
  registerPanel(panel: UiPanel): void;
595
679
  /** Register a nav / mode entry so the player can switch to this module. */
596
680
  registerRoute(entry: RouteEntry): void;
681
+ /**
682
+ * Runs `run` now and keeps what it started alive until the module is torn down (logout, a new
683
+ * login): the function it returns is called then. Use it for background work that needs no screen -
684
+ * keeping a menu badge in step with the player's state (`client.on("user:anyUpdated", …)` returns
685
+ * its unsubscribe, return that), a timer, a document listener. Do not mount a hidden React panel
686
+ * for this.
687
+ */
688
+ effect(run: () => void | (() => void)): void;
689
+ /**
690
+ * Calls `run` now and then every `ms` milliseconds until the module is torn down - the polling
691
+ * form of `effect`, with the timer cleared for you.
692
+ */
693
+ every(ms: number, run: () => void): void;
597
694
  /**
598
695
  * Content types other modules can publish and open (see `ContentTypeHandler`). A game registers
599
696
  * its type here; the Workshop module reads the registry — neither imports the other.
@@ -616,6 +713,12 @@ interface ModuleContext {
616
713
  * builds its "Play" buttons from it; a game can tell whether a lobby exists to return to.
617
714
  */
618
715
  modes: ModeRegistry;
716
+ /**
717
+ * The app's lobby: a game puts its showcase on the lobby's home (`setHero` — its character on a
718
+ * pedestal, "Play" next to it) and sends the player back to it from its pause menu (`open`). See
719
+ * `LobbyHeroDefinition`.
720
+ */
721
+ lobby: LobbyRegistry;
619
722
  /**
620
723
  * Switch the host to another mode (a route id registered by any module) — e.g. the lobby starts a
621
724
  * game, the Workshop opens a world and hands the screen to the game. Unknown ids are ignored. Called
@@ -777,9 +880,15 @@ interface RouteEntry {
777
880
  * A game that lives INSIDE the lobby: the base keeps the lobby's balances on top and its tabs at
778
881
  * the bottom around it, "Play" is this game, and the app starts in it after sign-in (idle-rpg).
779
882
  * Without it (the default) a game takes the whole screen, launched by "Play" in the lobby, with a
780
- * small button back (voxelcraft, board-game).
883
+ * small button back (board-game).
781
884
  */
782
885
  inLobby?: boolean;
886
+ /**
887
+ * A full-screen game with its OWN way back to the lobby (a pause menu with "To the lobby" that
888
+ * calls `ctx.lobby.open()` — voxelcraft): the base does not draw its round button back over it.
889
+ * The game must then offer that way on every device (a pause button on touch screens).
890
+ */
891
+ ownExit?: boolean;
783
892
  }
784
893
  /** A mode of the app as every module sees it (`ctx.modes`): a registered route plus its owner. */
785
894
  interface ModeEntry {
@@ -794,6 +903,8 @@ interface ModeEntry {
794
903
  moduleType: ModuleType;
795
904
  /** The route's `inLobby`: the game sits inside the lobby rather than taking the whole screen. */
796
905
  inLobby?: boolean;
906
+ /** The route's `ownExit`: the game leads back to the lobby itself — no button back over it. */
907
+ ownExit?: boolean;
797
908
  }
798
909
  /** `ctx.modes` — the modes of the app, live. Switch with `ctx.navigate(id)`. */
799
910
  interface ModeRegistry {
@@ -819,4 +930,4 @@ interface SharedEventBus<Events extends Record<string, unknown> = Record<string,
819
930
  /** Identity helper that pins a module literal to the `Module` type for editor help and errors. */
820
931
  declare function defineModule(module: Module): Module;
821
932
 
822
- export { type AppNotifications, type CapturedContent, type ContentFileData, type ContentRegistry, type ContentTypeHandler, type DownloadedContent, type EngineScene, type FeatureDefinition, type FeatureEntry, type FeatureGroup, type FeaturePresenter, type FeatureRegistry, type FeatureScreenProps, type InferShape, type LoadingReporter, type LoadingTask, type LocalContentItem, type ModeEntry, type ModeRegistry, type Module, type ModuleAgentApi, type ModuleAuthor, type ModuleContext, type ModuleEngine, type ModuleEventEmit, type ModuleEventListen, type ModuleEvents, type ModuleEventsManifest, type ModuleManifest, type ModuleMedia, type ModuleMeta, type ModuleSharedUi, type ModuleType, type MultiplayerGame, type MultiplayerRegistry, type MultiplayerRoom, type MultiplayerScreenArgs, type MultiplayerSetting, type NotificationAction, type NotificationInput, type NotificationLevel, type PanelSlot, type RouteEntry, SHARED_UI_ROLES, type SceneMountContext, type Shape, type ShapeDescriptor, type ShapeLeaf, type ShapeLeafSpec, type SharedEventBus, type SharedUiContext, type SharedUiRole, type SurfaceAllocator, type SurfaceHandle, type SurfaceKind, TOPIC_NAME_RE, type Topic, type TopicName, type TopicPayload, type UiPanel, defineModule, defineTopic, hostTopics, isSharedUiRole, isTopicName, shape, topicNamespace };
933
+ export { type AppNotifications, type CapturedContent, type ContentFileData, type ContentRegistry, type ContentTypeHandler, type DownloadedContent, type EngineScene, type FeatureDefinition, type FeatureEntry, type FeatureGroup, type FeaturePresenter, type FeatureRegistry, type FeatureScreenProps, type InferShape, type LoadingReporter, type LoadingTask, type LobbyHeroAction, type LobbyHeroDefinition, type LobbyHeroStage, type LobbyHeroView, type LobbyRegistry, type LocalContentItem, type ModeEntry, type ModeRegistry, type Module, type ModuleAgentApi, type ModuleAuthor, type ModuleContext, type ModuleEngine, type ModuleEventEmit, type ModuleEventListen, type ModuleEvents, type ModuleEventsManifest, type ModuleManifest, type ModuleMedia, type ModuleMeta, type ModuleSharedUi, type ModuleType, type MultiplayerGame, type MultiplayerRegistry, type MultiplayerRoom, type MultiplayerScreenArgs, type MultiplayerSetting, type NotificationAction, type NotificationInput, type NotificationLevel, type PanelSlot, type RouteEntry, SHARED_UI_ROLES, type SceneMountContext, type Shape, type ShapeDescriptor, type ShapeLeaf, type ShapeLeafSpec, type SharedEventBus, type SharedUiContext, type SharedUiRole, type SurfaceAllocator, type SurfaceHandle, type SurfaceKind, TOPIC_NAME_RE, type Topic, type TopicName, type TopicPayload, type UiPanel, defineModule, defineTopic, hostTopics, isSharedUiRole, isTopicName, shape, topicNamespace };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- var x=/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*@[1-9]\d*$/;function S(e){return typeof e=="string"&&x.test(e)}function T(e){let t=e.indexOf(":");return t<0?"":e.slice(0,t)}var c=/^(string|number|boolean)(\[\])?(\?)?$/;function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function l(e,t){return e==="number"?typeof t=="number"&&Number.isFinite(t):typeof t===e}function f(e,t,r){if(!u(t))return `${r||"payload"} must be an object`;for(let[n,o]of Object.entries(e)){let i=r?`${r}.${n}`:n,a=t[n];if(typeof o!="string"){let p=f(o,a,i);if(p)return p;continue}let d=c.exec(o);if(!d)return `${i}: invalid descriptor "${o}"`;let[,s="",g,m]=d;if(a===void 0){if(m)continue;return `${i} is missing`}if(g){if(!Array.isArray(a))return `${i} must be ${s}[]`;if(!a.every(p=>l(s,p)))return `${i} must contain only ${s} values`;continue}if(!l(s,a))return `${i} must be a ${s}`}return null}function y(e,t){for(let[r,n]of Object.entries(e)){let o=t?`${t}.${r}`:r;if(typeof n=="string"){if(!c.test(n))throw new Error(`shape(): field "${o}" has invalid type "${n}" \u2014 use "string" | "number" | "boolean", optionally with "[]" and/or a trailing "?"`)}else if(u(n))y(n,o);else throw new Error(`shape(): field "${o}" must be a type string or an object`)}}function h(e){return y(e,""),{descriptor:e,check:t=>f(e,t,"")}}function v(e,t){if(!S(e))throw new Error(`defineTopic(): "${e}" is not a valid topic name \u2014 expected "<module-id>:<event>@<major>", e.g. "idle-rpg:character-upgraded@1"`);return {name:e,shape:t}}var b={modeChanged:v("host:mode-changed@1",h({from:"string?",to:"string"}))};var M=["currency-bar","wallet","status","account","menu","modes"];function R(e){return typeof e=="string"&&M.includes(e)}function D(e){return e}export{M as SHARED_UI_ROLES,x as TOPIC_NAME_RE,D as defineModule,v as defineTopic,b as hostTopics,R as isSharedUiRole,S as isTopicName,h as shape,T as topicNamespace};
1
+ var x=/^[a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9-]*@[1-9]\d*$/;function S(e){return typeof e=="string"&&x.test(e)}function M(e){let t=e.indexOf(":");return t<0?"":e.slice(0,t)}var c=/^(string|number|boolean)(\[\])?(\?)?$/;function u(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function l(e,t){return e==="number"?typeof t=="number"&&Number.isFinite(t):typeof t===e}function f(e,t,r){if(!u(t))return `${r||"payload"} must be an object`;for(let[n,o]of Object.entries(e)){let i=r?`${r}.${n}`:n,a=t[n];if(typeof o!="string"){let p=f(o,a,i);if(p)return p;continue}let d=c.exec(o);if(!d)return `${i}: invalid descriptor "${o}"`;let[,s="",g,m]=d;if(a===void 0){if(m)continue;return `${i} is missing`}if(g){if(!Array.isArray(a))return `${i} must be ${s}[]`;if(!a.every(p=>l(s,p)))return `${i} must contain only ${s} values`;continue}if(!l(s,a))return `${i} must be a ${s}`}return null}function y(e,t){for(let[r,n]of Object.entries(e)){let o=t?`${t}.${r}`:r;if(typeof n=="string"){if(!c.test(n))throw new Error(`shape(): field "${o}" has invalid type "${n}" \u2014 use "string" | "number" | "boolean", optionally with "[]" and/or a trailing "?"`)}else if(u(n))y(n,o);else throw new Error(`shape(): field "${o}" must be a type string or an object`)}}function h(e){return y(e,""),{descriptor:e,check:t=>f(e,t,"")}}function v(e,t){if(!S(e))throw new Error(`defineTopic(): "${e}" is not a valid topic name \u2014 expected "<module-id>:<event>@<major>", e.g. "idle-rpg:character-upgraded@1"`);return {name:e,shape:t}}var T={modeChanged:v("host:mode-changed@1",h({from:"string?",to:"string"}))};var b=["currency-bar","wallet","status","account","menu","modes"];function R(e){return typeof e=="string"&&b.includes(e)}function D(e){return e}export{b as SHARED_UI_ROLES,x as TOPIC_NAME_RE,D as defineModule,v as defineTopic,T as hostTopics,R as isSharedUiRole,S as isTopicName,h as shape,M as topicNamespace};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idosgames/module-sdk",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "The composable-module contract for the iDosGames host shell: Module / ModuleContext types plus tiny authoring helpers. Framework-neutral at runtime — a module can be a game (Three/Phaser), a plain app, or an AI app.",
5
5
  "type": "module",
6
6
  "license": "MIT",