@idosgames/module-sdk 0.10.1 → 0.12.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 b(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 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=T;exports.isSharedUiRole=R;exports.isTopicName=S;exports.shape=h;exports.topicNamespace=b;
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
@@ -1,4 +1,4 @@
1
- import { RealtimeSession, IDosGamesClient } from '@idosgames/core';
1
+ import { RealtimeSession, VoiceController, IDosGamesClient } from '@idosgames/core';
2
2
  export { IDosGamesClient, SdkEvents } from '@idosgames/core';
3
3
  import { ComponentType } from 'react';
4
4
 
@@ -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;
@@ -339,6 +345,13 @@ interface MultiplayerRoom {
339
345
  readonly lateJoin: boolean;
340
346
  /** Leave the room for good: closes the session and the rooms screen forgets it. */
341
347
  leave(): Promise<void>;
348
+ /**
349
+ * The room's voice chat, or `null` when the room has none. The rooms system owns it and draws its
350
+ * controls (the mic button over the game included); a game reads it for its own touches — who is
351
+ * speaking (`isSpeaking`, `on("change")`), a talking player's marker, proximity volume
352
+ * (`setPeerVolume` by distance).
353
+ */
354
+ readonly voice: VoiceController | null;
342
355
  }
343
356
  /** What `ctx.features.open("multiplayer", args)` understands. */
344
357
  interface MultiplayerScreenArgs {
@@ -357,6 +370,84 @@ interface MultiplayerRegistry {
357
370
  subscribe(listener: () => void): () => void;
358
371
  }
359
372
 
373
+ /** A button under the showcase or on the "Play" card. */
374
+ interface LobbyHeroAction {
375
+ id: string;
376
+ /** Already in the player's language. */
377
+ label: string;
378
+ /** Glyph name of @idosgames/react/ui ("edit", "star"…), an emoji, or an image URL. */
379
+ icon?: string;
380
+ /** Pressed. Omit when the button only opens `menu`. */
381
+ run?(): void;
382
+ /** A short list shown when the button is pressed (an emote picker) — each entry runs on its own. */
383
+ menu?: LobbyHeroAction[];
384
+ disabled?: boolean;
385
+ }
386
+ /** What the lobby lays out around the showcase. Read through `view()`, redrawn on `subscribe`. */
387
+ interface LobbyHeroView {
388
+ /** Big name under the showcase (the character's name). */
389
+ title?: string;
390
+ /** A line under it. */
391
+ subtitle?: string;
392
+ /** Buttons under the name ("Edit", "Emote"). */
393
+ actions?: LobbyHeroAction[];
394
+ /**
395
+ * The card next to "Play" — WHAT will be played: a caption ("World"), a name ("Island"), buttons
396
+ * to change it ("Change", "New"). Omitted — "Play" alone.
397
+ */
398
+ play?: {
399
+ caption?: string;
400
+ title?: string;
401
+ actions?: LobbyHeroAction[];
402
+ /** Overflow menu of the card ("⋯": download, upload…). */
403
+ more?: LobbyHeroAction[];
404
+ };
405
+ }
406
+ /** Handed to `mount`: where on the host the main figure should stand. */
407
+ interface LobbyHeroStage {
408
+ /**
409
+ * The free spot between the lobby's interface where the figure should stand, in CSS pixels of the
410
+ * host element (it moves with the layout — read it every frame). `null` — not laid out yet.
411
+ */
412
+ spot(): {
413
+ x: number;
414
+ y: number;
415
+ w: number;
416
+ h: number;
417
+ } | null;
418
+ }
419
+ interface LobbyHeroDefinition {
420
+ /** The game mode "Play" starts (a route id of the game). */
421
+ mode: string;
422
+ /**
423
+ * Put the showcase into `host` — an element filling the lobby's home behind its interface. Called
424
+ * when the home is shown, the returned function when it is left (stop the render loop there). The
425
+ * same hero may be mounted again later.
426
+ */
427
+ mount(host: HTMLElement, stage: LobbyHeroStage): () => void;
428
+ /** The current texts and buttons. Cheap — called on every redraw. */
429
+ view(): LobbyHeroView;
430
+ /** Called by the hero whenever `view()` would answer differently. Returns the unsubscribe. */
431
+ subscribe(listener: () => void): () => void;
432
+ /**
433
+ * "Play" was pressed: return false to stay in the lobby (nothing to play yet). Default: switch to
434
+ * `mode`.
435
+ */
436
+ beforePlay?(): boolean;
437
+ }
438
+ /** `ctx.lobby` — the app's lobby as seen by a module. */
439
+ interface LobbyRegistry {
440
+ /** Put this game's showcase on the lobby's home. Returns the unregister function. */
441
+ setHero(hero: LobbyHeroDefinition): () => void;
442
+ /** The hero on the home now (the lobby reads it). */
443
+ hero(): LobbyHeroDefinition | null;
444
+ subscribe(listener: () => void): () => void;
445
+ /** Whether the app has a lobby to return to (the base is installed). */
446
+ available(): boolean;
447
+ /** Go to the lobby — a game's "To the lobby" in its pause menu. False — there is no lobby. */
448
+ open(): boolean;
449
+ }
450
+
360
451
  /** One piece of work on the loading screen. Call `done()` exactly once, also on failure. */
361
452
  interface LoadingTask {
362
453
  /**
@@ -441,6 +532,32 @@ interface AppNotifications {
441
532
  dismiss(id: string): void;
442
533
  }
443
534
 
535
+ /** The room's voices, as the recording takes them (the multiplayer system provides this). */
536
+ interface RecordingVoiceSource {
537
+ /** This player's microphone track (silent while not transmitting); null — mic off. */
538
+ mic(): MediaStreamTrack | null;
539
+ /** Other players' voice tracks. */
540
+ others(): MediaStreamTrack[];
541
+ /** The publisher allows other players' voices in videos (`Multiplayer.RecordVoices`). */
542
+ recordOthers(): boolean;
543
+ /** Called when tracks come or go (a player joined, the mic went on). */
544
+ subscribe(listener: () => void): () => void;
545
+ }
546
+ /** `ctx.clips`. */
547
+ interface ClipsRegistry {
548
+ /**
549
+ * A bright moment right now. `weight` 1–10 (default 5): how much it is worth showing — a boss
550
+ * kill 8, a level-up 4. Close marks merge into one clip (their weights add up).
551
+ */
552
+ mark(name: string, weight?: number): void;
553
+ /** The multiplayer system registers the room's voices here; `null` when the room is gone. */
554
+ setVoiceSource(source: RecordingVoiceSource | null): void;
555
+ /** This player is making a long recording right now. */
556
+ isRecording(): boolean;
557
+ /** Called when `isRecording` changes. */
558
+ subscribe(listener: () => void): () => void;
559
+ }
560
+
444
561
  /** What a module is, for catalog/UI purposes. Not every module is a game. */
445
562
  type ModuleType = "game" | "app" | "ai-app";
446
563
  /**
@@ -629,6 +746,12 @@ interface ModuleContext {
629
746
  * builds its "Play" buttons from it; a game can tell whether a lobby exists to return to.
630
747
  */
631
748
  modes: ModeRegistry;
749
+ /**
750
+ * The app's lobby: a game puts its showcase on the lobby's home (`setHero` — its character on a
751
+ * pedestal, "Play" next to it) and sends the player back to it from its pause menu (`open`). See
752
+ * `LobbyHeroDefinition`.
753
+ */
754
+ lobby: LobbyRegistry;
632
755
  /**
633
756
  * Switch the host to another mode (a route id registered by any module) — e.g. the lobby starts a
634
757
  * game, the Workshop opens a world and hands the screen to the game. Unknown ids are ignored. Called
@@ -660,6 +783,12 @@ interface ModuleContext {
660
783
  * one look, one queue, one sound for the whole app. The module's own id is the default `source`.
661
784
  */
662
785
  notifications: AppNotifications;
786
+ /**
787
+ * The platform's video recording (see `ClipsRegistry`): a game marks bright moments
788
+ * (`ctx.clips.mark("boss_down", 8)`) — they become the player's "Best moments" clips on
789
+ * idosgames.com. A no-op anywhere else.
790
+ */
791
+ clips: ClipsRegistry;
663
792
  }
664
793
  /**
665
794
  * A module's debug surface for the agent: read its state, drive it with named actions.
@@ -752,6 +881,21 @@ interface EngineScene {
752
881
  * host falls back to reading the canvas itself, which is correct for 2D canvases.
753
882
  */
754
883
  capture?(): Promise<Blob | null>;
884
+ /**
885
+ * Where the action is right now, 0–1 on both axes of the canvas (the player, the explosion) — for
886
+ * the platform's video recording: a vertical (TikTok) cut of a landscape game follows this point
887
+ * instead of cutting the middle. Called a few times a second; keep it cheap. Omit — the centre.
888
+ */
889
+ focus?(): {
890
+ x: number;
891
+ y: number;
892
+ } | null;
893
+ /**
894
+ * The game's own sound as a MediaStream, for the platform's video recording — e.g. a
895
+ * `MediaStreamAudioDestinationNode` your audio graph also feeds (Three.js: `listener.context`).
896
+ * Omit — videos carry the voices but not the game's sound.
897
+ */
898
+ audio?(): MediaStream | null;
755
899
  /** Permanent teardown — dispose GPU resources, remove listeners. */
756
900
  destroy(): void;
757
901
  }
@@ -790,9 +934,15 @@ interface RouteEntry {
790
934
  * A game that lives INSIDE the lobby: the base keeps the lobby's balances on top and its tabs at
791
935
  * the bottom around it, "Play" is this game, and the app starts in it after sign-in (idle-rpg).
792
936
  * Without it (the default) a game takes the whole screen, launched by "Play" in the lobby, with a
793
- * small button back (voxelcraft, board-game).
937
+ * small button back (board-game).
794
938
  */
795
939
  inLobby?: boolean;
940
+ /**
941
+ * A full-screen game with its OWN way back to the lobby (a pause menu with "To the lobby" that
942
+ * calls `ctx.lobby.open()` — voxelcraft): the base does not draw its round button back over it.
943
+ * The game must then offer that way on every device (a pause button on touch screens).
944
+ */
945
+ ownExit?: boolean;
796
946
  }
797
947
  /** A mode of the app as every module sees it (`ctx.modes`): a registered route plus its owner. */
798
948
  interface ModeEntry {
@@ -807,6 +957,8 @@ interface ModeEntry {
807
957
  moduleType: ModuleType;
808
958
  /** The route's `inLobby`: the game sits inside the lobby rather than taking the whole screen. */
809
959
  inLobby?: boolean;
960
+ /** The route's `ownExit`: the game leads back to the lobby itself — no button back over it. */
961
+ ownExit?: boolean;
810
962
  }
811
963
  /** `ctx.modes` — the modes of the app, live. Switch with `ctx.navigate(id)`. */
812
964
  interface ModeRegistry {
@@ -832,4 +984,4 @@ interface SharedEventBus<Events extends Record<string, unknown> = Record<string,
832
984
  /** Identity helper that pins a module literal to the `Module` type for editor help and errors. */
833
985
  declare function defineModule(module: Module): Module;
834
986
 
835
- 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 };
987
+ export { type AppNotifications, type CapturedContent, type ClipsRegistry, 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 RecordingVoiceSource, 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
@@ -1,4 +1,4 @@
1
- import { RealtimeSession, IDosGamesClient } from '@idosgames/core';
1
+ import { RealtimeSession, VoiceController, IDosGamesClient } from '@idosgames/core';
2
2
  export { IDosGamesClient, SdkEvents } from '@idosgames/core';
3
3
  import { ComponentType } from 'react';
4
4
 
@@ -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;
@@ -339,6 +345,13 @@ interface MultiplayerRoom {
339
345
  readonly lateJoin: boolean;
340
346
  /** Leave the room for good: closes the session and the rooms screen forgets it. */
341
347
  leave(): Promise<void>;
348
+ /**
349
+ * The room's voice chat, or `null` when the room has none. The rooms system owns it and draws its
350
+ * controls (the mic button over the game included); a game reads it for its own touches — who is
351
+ * speaking (`isSpeaking`, `on("change")`), a talking player's marker, proximity volume
352
+ * (`setPeerVolume` by distance).
353
+ */
354
+ readonly voice: VoiceController | null;
342
355
  }
343
356
  /** What `ctx.features.open("multiplayer", args)` understands. */
344
357
  interface MultiplayerScreenArgs {
@@ -357,6 +370,84 @@ interface MultiplayerRegistry {
357
370
  subscribe(listener: () => void): () => void;
358
371
  }
359
372
 
373
+ /** A button under the showcase or on the "Play" card. */
374
+ interface LobbyHeroAction {
375
+ id: string;
376
+ /** Already in the player's language. */
377
+ label: string;
378
+ /** Glyph name of @idosgames/react/ui ("edit", "star"…), an emoji, or an image URL. */
379
+ icon?: string;
380
+ /** Pressed. Omit when the button only opens `menu`. */
381
+ run?(): void;
382
+ /** A short list shown when the button is pressed (an emote picker) — each entry runs on its own. */
383
+ menu?: LobbyHeroAction[];
384
+ disabled?: boolean;
385
+ }
386
+ /** What the lobby lays out around the showcase. Read through `view()`, redrawn on `subscribe`. */
387
+ interface LobbyHeroView {
388
+ /** Big name under the showcase (the character's name). */
389
+ title?: string;
390
+ /** A line under it. */
391
+ subtitle?: string;
392
+ /** Buttons under the name ("Edit", "Emote"). */
393
+ actions?: LobbyHeroAction[];
394
+ /**
395
+ * The card next to "Play" — WHAT will be played: a caption ("World"), a name ("Island"), buttons
396
+ * to change it ("Change", "New"). Omitted — "Play" alone.
397
+ */
398
+ play?: {
399
+ caption?: string;
400
+ title?: string;
401
+ actions?: LobbyHeroAction[];
402
+ /** Overflow menu of the card ("⋯": download, upload…). */
403
+ more?: LobbyHeroAction[];
404
+ };
405
+ }
406
+ /** Handed to `mount`: where on the host the main figure should stand. */
407
+ interface LobbyHeroStage {
408
+ /**
409
+ * The free spot between the lobby's interface where the figure should stand, in CSS pixels of the
410
+ * host element (it moves with the layout — read it every frame). `null` — not laid out yet.
411
+ */
412
+ spot(): {
413
+ x: number;
414
+ y: number;
415
+ w: number;
416
+ h: number;
417
+ } | null;
418
+ }
419
+ interface LobbyHeroDefinition {
420
+ /** The game mode "Play" starts (a route id of the game). */
421
+ mode: string;
422
+ /**
423
+ * Put the showcase into `host` — an element filling the lobby's home behind its interface. Called
424
+ * when the home is shown, the returned function when it is left (stop the render loop there). The
425
+ * same hero may be mounted again later.
426
+ */
427
+ mount(host: HTMLElement, stage: LobbyHeroStage): () => void;
428
+ /** The current texts and buttons. Cheap — called on every redraw. */
429
+ view(): LobbyHeroView;
430
+ /** Called by the hero whenever `view()` would answer differently. Returns the unsubscribe. */
431
+ subscribe(listener: () => void): () => void;
432
+ /**
433
+ * "Play" was pressed: return false to stay in the lobby (nothing to play yet). Default: switch to
434
+ * `mode`.
435
+ */
436
+ beforePlay?(): boolean;
437
+ }
438
+ /** `ctx.lobby` — the app's lobby as seen by a module. */
439
+ interface LobbyRegistry {
440
+ /** Put this game's showcase on the lobby's home. Returns the unregister function. */
441
+ setHero(hero: LobbyHeroDefinition): () => void;
442
+ /** The hero on the home now (the lobby reads it). */
443
+ hero(): LobbyHeroDefinition | null;
444
+ subscribe(listener: () => void): () => void;
445
+ /** Whether the app has a lobby to return to (the base is installed). */
446
+ available(): boolean;
447
+ /** Go to the lobby — a game's "To the lobby" in its pause menu. False — there is no lobby. */
448
+ open(): boolean;
449
+ }
450
+
360
451
  /** One piece of work on the loading screen. Call `done()` exactly once, also on failure. */
361
452
  interface LoadingTask {
362
453
  /**
@@ -441,6 +532,32 @@ interface AppNotifications {
441
532
  dismiss(id: string): void;
442
533
  }
443
534
 
535
+ /** The room's voices, as the recording takes them (the multiplayer system provides this). */
536
+ interface RecordingVoiceSource {
537
+ /** This player's microphone track (silent while not transmitting); null — mic off. */
538
+ mic(): MediaStreamTrack | null;
539
+ /** Other players' voice tracks. */
540
+ others(): MediaStreamTrack[];
541
+ /** The publisher allows other players' voices in videos (`Multiplayer.RecordVoices`). */
542
+ recordOthers(): boolean;
543
+ /** Called when tracks come or go (a player joined, the mic went on). */
544
+ subscribe(listener: () => void): () => void;
545
+ }
546
+ /** `ctx.clips`. */
547
+ interface ClipsRegistry {
548
+ /**
549
+ * A bright moment right now. `weight` 1–10 (default 5): how much it is worth showing — a boss
550
+ * kill 8, a level-up 4. Close marks merge into one clip (their weights add up).
551
+ */
552
+ mark(name: string, weight?: number): void;
553
+ /** The multiplayer system registers the room's voices here; `null` when the room is gone. */
554
+ setVoiceSource(source: RecordingVoiceSource | null): void;
555
+ /** This player is making a long recording right now. */
556
+ isRecording(): boolean;
557
+ /** Called when `isRecording` changes. */
558
+ subscribe(listener: () => void): () => void;
559
+ }
560
+
444
561
  /** What a module is, for catalog/UI purposes. Not every module is a game. */
445
562
  type ModuleType = "game" | "app" | "ai-app";
446
563
  /**
@@ -629,6 +746,12 @@ interface ModuleContext {
629
746
  * builds its "Play" buttons from it; a game can tell whether a lobby exists to return to.
630
747
  */
631
748
  modes: ModeRegistry;
749
+ /**
750
+ * The app's lobby: a game puts its showcase on the lobby's home (`setHero` — its character on a
751
+ * pedestal, "Play" next to it) and sends the player back to it from its pause menu (`open`). See
752
+ * `LobbyHeroDefinition`.
753
+ */
754
+ lobby: LobbyRegistry;
632
755
  /**
633
756
  * Switch the host to another mode (a route id registered by any module) — e.g. the lobby starts a
634
757
  * game, the Workshop opens a world and hands the screen to the game. Unknown ids are ignored. Called
@@ -660,6 +783,12 @@ interface ModuleContext {
660
783
  * one look, one queue, one sound for the whole app. The module's own id is the default `source`.
661
784
  */
662
785
  notifications: AppNotifications;
786
+ /**
787
+ * The platform's video recording (see `ClipsRegistry`): a game marks bright moments
788
+ * (`ctx.clips.mark("boss_down", 8)`) — they become the player's "Best moments" clips on
789
+ * idosgames.com. A no-op anywhere else.
790
+ */
791
+ clips: ClipsRegistry;
663
792
  }
664
793
  /**
665
794
  * A module's debug surface for the agent: read its state, drive it with named actions.
@@ -752,6 +881,21 @@ interface EngineScene {
752
881
  * host falls back to reading the canvas itself, which is correct for 2D canvases.
753
882
  */
754
883
  capture?(): Promise<Blob | null>;
884
+ /**
885
+ * Where the action is right now, 0–1 on both axes of the canvas (the player, the explosion) — for
886
+ * the platform's video recording: a vertical (TikTok) cut of a landscape game follows this point
887
+ * instead of cutting the middle. Called a few times a second; keep it cheap. Omit — the centre.
888
+ */
889
+ focus?(): {
890
+ x: number;
891
+ y: number;
892
+ } | null;
893
+ /**
894
+ * The game's own sound as a MediaStream, for the platform's video recording — e.g. a
895
+ * `MediaStreamAudioDestinationNode` your audio graph also feeds (Three.js: `listener.context`).
896
+ * Omit — videos carry the voices but not the game's sound.
897
+ */
898
+ audio?(): MediaStream | null;
755
899
  /** Permanent teardown — dispose GPU resources, remove listeners. */
756
900
  destroy(): void;
757
901
  }
@@ -790,9 +934,15 @@ interface RouteEntry {
790
934
  * A game that lives INSIDE the lobby: the base keeps the lobby's balances on top and its tabs at
791
935
  * the bottom around it, "Play" is this game, and the app starts in it after sign-in (idle-rpg).
792
936
  * Without it (the default) a game takes the whole screen, launched by "Play" in the lobby, with a
793
- * small button back (voxelcraft, board-game).
937
+ * small button back (board-game).
794
938
  */
795
939
  inLobby?: boolean;
940
+ /**
941
+ * A full-screen game with its OWN way back to the lobby (a pause menu with "To the lobby" that
942
+ * calls `ctx.lobby.open()` — voxelcraft): the base does not draw its round button back over it.
943
+ * The game must then offer that way on every device (a pause button on touch screens).
944
+ */
945
+ ownExit?: boolean;
796
946
  }
797
947
  /** A mode of the app as every module sees it (`ctx.modes`): a registered route plus its owner. */
798
948
  interface ModeEntry {
@@ -807,6 +957,8 @@ interface ModeEntry {
807
957
  moduleType: ModuleType;
808
958
  /** The route's `inLobby`: the game sits inside the lobby rather than taking the whole screen. */
809
959
  inLobby?: boolean;
960
+ /** The route's `ownExit`: the game leads back to the lobby itself — no button back over it. */
961
+ ownExit?: boolean;
810
962
  }
811
963
  /** `ctx.modes` — the modes of the app, live. Switch with `ctx.navigate(id)`. */
812
964
  interface ModeRegistry {
@@ -832,4 +984,4 @@ interface SharedEventBus<Events extends Record<string, unknown> = Record<string,
832
984
  /** Identity helper that pins a module literal to the `Module` type for editor help and errors. */
833
985
  declare function defineModule(module: Module): Module;
834
986
 
835
- 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 };
987
+ export { type AppNotifications, type CapturedContent, type ClipsRegistry, 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 RecordingVoiceSource, 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 b(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 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,T as hostTopics,R as isSharedUiRole,S as isTopicName,h as shape,b 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.1",
3
+ "version": "0.12.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",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "//": "@idosgames/core is a real range, not '*': a published contract must name the version of core whose IDosGamesClient/SdkEvents shapes it references. Bump in step with core (see RELEASING.md). react is an OPTIONAL peer — the contract references React's ComponentType type only (a UI panel is a React component), which is erased at build, so a runtime-only module needs no React.",
48
48
  "dependencies": {
49
- "@idosgames/core": "^0.21.0"
49
+ "@idosgames/core": "^0.22.0"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "react": "^18.3.1 || ^19.0.0"