@seriousgaming/sdk-tools 0.0.0-stage → 1.0.0-alpha.1
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/LICENSE +93 -0
- package/README.md +36 -2
- package/dist/bundling/logic-exports.d.ts +34 -0
- package/dist/bundling/node-builtins.d.ts +21 -0
- package/dist/bundling/transpiler.d.ts +7 -0
- package/dist/cli.js +27368 -0
- package/dist/client/dev-server.d.ts +59 -0
- package/dist/client/index.d.ts +28 -0
- package/dist/client/stub-services.d.ts +32 -0
- package/dist/client/test-session.d.ts +195 -0
- package/dist/client.js +16884 -0
- package/dist/core/asset-reference-translator.d.ts +46 -0
- package/dist/core/clock.d.ts +57 -0
- package/dist/core/faults.d.ts +65 -0
- package/dist/core/game-settings.d.ts +65 -0
- package/dist/core/history.d.ts +89 -0
- package/dist/core/listen-with-fallback.d.ts +30 -0
- package/dist/core/mutation-queue.d.ts +61 -0
- package/dist/core/package-scenario.d.ts +89 -0
- package/dist/core/participant-registry.d.ts +109 -0
- package/dist/core/pending-changes.d.ts +14 -0
- package/dist/core/protocol.d.ts +83 -0
- package/dist/core/scenario-drafts.d.ts +32 -0
- package/dist/core/session-endpoints.d.ts +108 -0
- package/dist/core/session-gates.d.ts +75 -0
- package/dist/core/session-host.d.ts +94 -0
- package/dist/core/session-refusals.d.ts +56 -0
- package/dist/core/session-rewind.d.ts +32 -0
- package/dist/core/session-roster.d.ts +128 -0
- package/dist/core/session-surfaces.d.ts +32 -0
- package/dist/core/session-ticks.d.ts +18 -0
- package/dist/core/session-timers.d.ts +84 -0
- package/dist/core/session-writes.d.ts +40 -0
- package/dist/core/session.d.ts +385 -0
- package/dist/core/settings-locks.d.ts +22 -0
- package/dist/core/team-names.d.ts +52 -0
- package/dist/core/team-state-store.d.ts +209 -0
- package/dist/core/tick-timer.d.ts +75 -0
- package/dist/dev/blob-links.d.ts +10 -0
- package/dist/dev/console/assets/index-DgFglsnA.js +204 -0
- package/dist/dev/console/index.html +12 -0
- package/dist/dev/console-path.d.ts +19 -0
- package/dist/dev/lockfile.d.ts +124 -0
- package/dist/dev/package-watcher.d.ts +21 -0
- package/dist/dev/routes.d.ts +67 -0
- package/dist/dev/shell.d.ts +260 -0
- package/dist/dev/surfaces.d.ts +41 -0
- package/dist/dev/tools-identity.d.ts +23 -0
- package/dist/dev/view/batch.d.ts +172 -0
- package/dist/dev/view/commands.d.ts +75 -0
- package/dist/dev/view/route.d.ts +58 -0
- package/dist/dev/view/view.d.ts +78 -0
- package/dist/dev/view/viewer.d.ts +32 -0
- package/dist/discovery/config.d.ts +9 -0
- package/dist/discovery/manifest-paths.d.ts +21 -0
- package/dist/discovery/removed-fields.d.ts +90 -0
- package/dist/docs/api/deck.md +386 -0
- package/dist/docs/api/index.md +67 -0
- package/dist/docs/api/sdk-commands.md +286 -0
- package/dist/docs/api/sdk-e2e.md +433 -0
- package/dist/docs/api/sdk-projection.md +207 -0
- package/dist/docs/api/sdk-testing.md +214 -0
- package/dist/docs/api/sdk-tools-client.md +560 -0
- package/dist/docs/api/sdk-ui-testing.md +231 -0
- package/dist/docs/api/sdk-ui.md +1269 -0
- package/dist/docs/api/sdk.md +2140 -0
- package/dist/docs/changelog-0.x.md +1156 -0
- package/dist/docs/changelog.md +236 -0
- package/dist/docs/cli-and-dev-server.md +217 -0
- package/dist/docs/cli-error-codes.md +154 -0
- package/dist/docs/cli-reference.md +2488 -0
- package/dist/docs/cli-simulation-testing.md +222 -0
- package/dist/docs/concepts/index.md +46 -0
- package/dist/docs/concepts/terminology.md +322 -0
- package/dist/docs/environment-variables.md +102 -0
- package/dist/docs/getting-started.md +145 -0
- package/dist/docs/guides/ai-facilitation.md +220 -0
- package/dist/docs/guides/capabilities.md +460 -0
- package/dist/docs/guides/endpoints.md +335 -0
- package/dist/docs/guides/engine-testing.md +477 -0
- package/dist/docs/guides/engine.md +527 -0
- package/dist/docs/guides/file-handling.md +405 -0
- package/dist/docs/guides/games.md +176 -0
- package/dist/docs/guides/global-settings.md +94 -0
- package/dist/docs/guides/guide.md +123 -0
- package/dist/docs/guides/hooks.md +183 -0
- package/dist/docs/guides/index.md +51 -0
- package/dist/docs/guides/internationalization.md +288 -0
- package/dist/docs/guides/llm-integration.md +635 -0
- package/dist/docs/guides/multi-team-games.md +586 -0
- package/dist/docs/guides/patterns.md +391 -0
- package/dist/docs/guides/presentation.md +196 -0
- package/dist/docs/guides/production-constraints.md +130 -0
- package/dist/docs/guides/scenario-editor.md +143 -0
- package/dist/docs/guides/scenarios.md +515 -0
- package/dist/docs/guides/session.md +972 -0
- package/dist/docs/guides/settings.md +458 -0
- package/dist/docs/guides/setup.md +193 -0
- package/dist/docs/guides/speech-generation.md +233 -0
- package/dist/docs/guides/starter-game.md +164 -0
- package/dist/docs/guides/testing.md +575 -0
- package/dist/docs/guides/tick-controller.md +569 -0
- package/dist/docs/guides/type-safety.md +516 -0
- package/dist/docs/guides/ui.md +1389 -0
- package/dist/docs/guides/upgrading.md +636 -0
- package/dist/docs/guides/validators.md +217 -0
- package/dist/docs/guides/waiting-room.md +244 -0
- package/dist/docs/index.md +41 -0
- package/dist/docs/recipes/act-on-the-session-from-an-endpoint.md +95 -0
- package/dist/docs/recipes/adapt-a-game-for-walkthroughs.md +133 -0
- package/dist/docs/recipes/adapt-ui-for-observers.md +74 -0
- package/dist/docs/recipes/call-endpoint-from-ui.md +109 -0
- package/dist/docs/recipes/check-a-package-from-a-script.md +117 -0
- package/dist/docs/recipes/create-capability.md +66 -0
- package/dist/docs/recipes/create-endpoint.md +127 -0
- package/dist/docs/recipes/create-engine.md +111 -0
- package/dist/docs/recipes/create-game.md +70 -0
- package/dist/docs/recipes/create-guide.md +75 -0
- package/dist/docs/recipes/create-hook.md +80 -0
- package/dist/docs/recipes/create-presentation.md +142 -0
- package/dist/docs/recipes/create-scenario-editor.md +102 -0
- package/dist/docs/recipes/create-scenario.md +98 -0
- package/dist/docs/recipes/create-session-ui.md +116 -0
- package/dist/docs/recipes/create-settings.md +88 -0
- package/dist/docs/recipes/create-setup.md +115 -0
- package/dist/docs/recipes/create-tick-controller.md +65 -0
- package/dist/docs/recipes/create-user-interface.md +96 -0
- package/dist/docs/recipes/create-validator.md +67 -0
- package/dist/docs/recipes/create-waiting-room-ui.md +109 -0
- package/dist/docs/recipes/declare-api-uses.md +67 -0
- package/dist/docs/recipes/design-state-for-concurrent-writes.md +96 -0
- package/dist/docs/recipes/distribute-package.md +80 -0
- package/dist/docs/recipes/e2e-testing.md +116 -0
- package/dist/docs/recipes/end-game-on-condition.md +154 -0
- package/dist/docs/recipes/fix-duplicate-packages.md +101 -0
- package/dist/docs/recipes/index.md +86 -0
- package/dist/docs/recipes/make-an-endpoint-external.md +101 -0
- package/dist/docs/recipes/negotiate-across-teams.md +164 -0
- package/dist/docs/recipes/observe-other-teams-state.md +119 -0
- package/dist/docs/recipes/publish-a-package.md +139 -0
- package/dist/docs/recipes/remember-a-viewers-choice.md +55 -0
- package/dist/docs/recipes/review-a-past-day.md +73 -0
- package/dist/docs/recipes/share-assets-across-scenarios.md +120 -0
- package/dist/docs/recipes/share-logic-between-engine-and-ui.md +130 -0
- package/dist/docs/recipes/show-all-teams-participants.md +109 -0
- package/dist/docs/recipes/start-from-an-empty-package.md +169 -0
- package/dist/docs/recipes/take-a-file-in-an-endpoint.md +82 -0
- package/dist/docs/recipes/test-a-multi-role-game.md +51 -0
- package/dist/docs/recipes/test-user-interface.md +95 -0
- package/dist/docs/recipes/test-with-engine.md +115 -0
- package/dist/docs/recipes/upload-and-display-files.md +148 -0
- package/dist/docs/recipes/use-request-play.md +65 -0
- package/dist/docs/recipes/use-request-tick.md +75 -0
- package/dist/docs/troubleshooting.md +816 -0
- package/dist/execution/blob-inputs.d.ts +57 -0
- package/dist/execution/bundle-reload.d.ts +42 -0
- package/dist/execution/config-resolver.d.ts +127 -0
- package/dist/execution/configuration-builder.d.ts +66 -0
- package/dist/execution/context-composition.d.ts +161 -0
- package/dist/execution/endpoint-actions.d.ts +9 -0
- package/dist/execution/endpoint-payloads.d.ts +32 -0
- package/dist/execution/endpoint-runner.d.ts +129 -0
- package/dist/execution/engine-state.d.ts +40 -0
- package/dist/execution/executor-callbacks.d.ts +56 -0
- package/dist/execution/executor.d.ts +436 -0
- package/dist/execution/gates.d.ts +45 -0
- package/dist/execution/hook-chain.d.ts +25 -0
- package/dist/execution/logic-calls.d.ts +190 -0
- package/dist/execution/logic-failure.d.ts +84 -0
- package/dist/execution/logic-loader.d.ts +54 -0
- package/dist/execution/logic-result.d.ts +76 -0
- package/dist/execution/tick-controller-call.d.ts +32 -0
- package/dist/execution/types.d.ts +48 -0
- package/dist/generation/ts-type.d.ts +39 -0
- package/dist/guide/locale.d.ts +12 -0
- package/dist/guide/video-registry.d.ts +42 -0
- package/dist/help-examples.d.ts +125 -0
- package/dist/host/adapter.d.ts +67 -0
- package/dist/host/host-channel.d.ts +222 -0
- package/dist/host/index.d.ts +20 -0
- package/dist/host/protocol.d.ts +783 -0
- package/dist/host/surfaces.d.ts +42 -0
- package/dist/host/theme.d.ts +55 -0
- package/dist/host.js +583 -0
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/schema-builder.d.ts +29 -0
- package/dist/prompts/types.d.ts +108 -0
- package/dist/prompts/validators.d.ts +32 -0
- package/dist/runtime/capability-scope.d.ts +6 -0
- package/dist/runtime/context-merge.d.ts +29 -0
- package/dist/runtime/context-projection.d.ts +41 -0
- package/dist/runtime/guarded-callbacks.d.ts +15 -0
- package/dist/runtime/index.d.ts +27 -0
- package/dist/runtime/manifest-version.d.ts +55 -0
- package/dist/runtime/tick-validation.d.ts +25 -0
- package/dist/runtime.js +346 -0
- package/dist/schema.json +1546 -0
- package/dist/server/vite-plugin.d.ts +46 -0
- package/dist/services/tts/client.d.ts +51 -0
- package/dist/services/tts/elevenlabs.d.ts +24 -0
- package/dist/services/tts/types.d.ts +44 -0
- package/dist/storage/asset-resolver.d.ts +49 -0
- package/dist/storage/blob-store.d.ts +41 -0
- package/dist/templates/endpoint/logic.test.ts.liquid +61 -0
- package/dist/templates/endpoint/logic.ts.liquid +25 -0
- package/dist/templates/engine/init.json.liquid +7 -0
- package/dist/templates/engine/logic-aggregate.test.ts.liquid +90 -0
- package/dist/templates/engine/logic-aggregate.ts.liquid +46 -0
- package/dist/templates/engine/logic.test.ts.liquid +61 -0
- package/dist/templates/engine/logic.ts.liquid +32 -0
- package/dist/templates/engine/run.json.liquid +7 -0
- package/dist/templates/guide/01-at-a-glance.md.liquid +19 -0
- package/dist/templates/guide/02-preparation.md.liquid +18 -0
- package/dist/templates/guide/03-running-the-session.md.liquid +37 -0
- package/dist/templates/hook/logic.test.ts.liquid +46 -0
- package/dist/templates/hook/logic.ts.liquid +12 -0
- package/dist/templates/package/CLAUDE.md.template +338 -0
- package/dist/templates/package/DESIGN.md.liquid +144 -0
- package/dist/templates/package/README.md.liquid +143 -0
- package/dist/templates/package/claude/rules/workflow-create-capability.md +26 -0
- package/dist/templates/package/claude/rules/workflow-create-endpoint.md +35 -0
- package/dist/templates/package/claude/rules/workflow-create-engine.md +35 -0
- package/dist/templates/package/claude/rules/workflow-create-guide.md +40 -0
- package/dist/templates/package/claude/rules/workflow-create-presentation.md +37 -0
- package/dist/templates/package/claude/rules/workflow-create-user-interface.md +31 -0
- package/dist/templates/package/claude/rules/workflow-publish-package.md +54 -0
- package/dist/templates/package/claude/rules/workflow-test-engine-or-ui.md +30 -0
- package/dist/templates/package/claude/rules/workflow-update-sdk-version.md +43 -0
- package/dist/templates/package/claude/settings.json.template +16 -0
- package/dist/templates/package/gitignore.template +15 -0
- package/dist/templates/package/global-setup.ts.template +21 -0
- package/dist/templates/package/globals.d.ts.template +2 -0
- package/dist/templates/package/oxfmtrc.json.template +14 -0
- package/dist/templates/package/oxlintrc.json.template +238 -0
- package/dist/templates/package/package.json.liquid +65 -0
- package/dist/templates/package/playwright.config.ts.liquid +19 -0
- package/dist/templates/package/pnpm-workspace.yaml.template +27 -0
- package/dist/templates/package/test-setup.ts.liquid +7 -0
- package/dist/templates/package/tsconfig.json.liquid +15 -0
- package/dist/templates/package/vite.config.ts.liquid +19 -0
- package/dist/templates/package/vitest.config.ts.liquid +25 -0
- package/dist/templates/package/vitest.engine.config.ts.liquid +12 -0
- package/dist/templates/presentation/index.html.liquid +20 -0
- package/dist/templates/presentation/src/App.tsx.liquid +49 -0
- package/dist/templates/presentation/src/Slides.tsx.liquid +81 -0
- package/dist/templates/presentation/src/i18n.ts.liquid +54 -0
- package/dist/templates/presentation/src/main.tsx.liquid +14 -0
- package/dist/templates/scenario_editor/src/App.test.tsx.liquid +30 -0
- package/dist/templates/scenario_editor/src/App.tsx.liquid +95 -0
- package/dist/templates/scenario_editor/src/i18n.ts.liquid +44 -0
- package/dist/templates/session/src/App.test.tsx.liquid +60 -0
- package/dist/templates/session/src/App.tsx.liquid +128 -0
- package/dist/templates/session/src/i18n.ts.liquid +53 -0
- package/dist/templates/session/src/main.tsx.liquid +48 -0
- package/dist/templates/session/src/theme.ts.liquid +46 -0
- package/dist/templates/settings-editor/index.html.liquid +12 -0
- package/dist/templates/settings-editor/src/main.tsx.liquid +48 -0
- package/dist/templates/settings-editor/src/theme.ts.liquid +24 -0
- package/dist/templates/setup/src/App.test.tsx.liquid +39 -0
- package/dist/templates/setup/src/App.tsx.liquid +102 -0
- package/dist/templates/setup/src/i18n.ts.liquid +47 -0
- package/dist/templates/starter-game/capabilities/briefing/capability.sdk.json +18 -0
- package/dist/templates/starter-game/capabilities/plan/capability.sdk.json +17 -0
- package/dist/templates/starter-game/capabilities/results/capability.sdk.json +61 -0
- package/dist/templates/starter-game/capabilities/roles/capability.sdk.json +17 -0
- package/dist/templates/starter-game/capabilities/weather/capability.sdk.json +23 -0
- package/dist/templates/starter-game/engine/defaults.ts +81 -0
- package/dist/templates/starter-game/engine/engine.sdk.json +12 -0
- package/dist/templates/starter-game/engine/logic.test.ts +221 -0
- package/dist/templates/starter-game/engine/logic.ts +149 -0
- package/dist/templates/starter-game/engine/test-helpers.ts +74 -0
- package/dist/templates/starter-game/engine/types.ts +139 -0
- package/dist/templates/starter-game/guide/content/en/01-at-a-glance.md +51 -0
- package/dist/templates/starter-game/guide/content/en/02-preparation.md +35 -0
- package/dist/templates/starter-game/guide/content/en/03-running-the-day.md +58 -0
- package/dist/templates/starter-game/guide/guide.sdk.json +10 -0
- package/dist/templates/starter-game/guide/guide.test.ts +108 -0
- package/dist/templates/starter-game/hooks/assignproducts/defaults.ts +37 -0
- package/dist/templates/starter-game/hooks/assignproducts/hook.sdk.json +13 -0
- package/dist/templates/starter-game/hooks/assignproducts/logic/logic.test.ts +77 -0
- package/dist/templates/starter-game/hooks/assignproducts/logic/logic.ts +47 -0
- package/dist/templates/starter-game/hooks/assignproducts/types.ts +55 -0
- package/dist/templates/starter-game/package.json +68 -0
- package/dist/templates/starter-game/playwright.config.ts +19 -0
- package/dist/templates/starter-game/scenarios/heatwave/scenario.sdk.json +20 -0
- package/dist/templates/starter-game/scenarios/ordinary_week/scenario.sdk.json +12 -0
- package/dist/templates/starter-game/seriousgaming.sdk.json +23 -0
- package/dist/templates/starter-game/session/defaults.ts +80 -0
- package/dist/templates/starter-game/session/index.html +12 -0
- package/dist/templates/starter-game/session/session.sdk.json +11 -0
- package/dist/templates/starter-game/session/src/App.test.tsx +307 -0
- package/dist/templates/starter-game/session/src/App.tsx +309 -0
- package/dist/templates/starter-game/session/src/Section.tsx +37 -0
- package/dist/templates/starter-game/session/src/TeamDoor.tsx +96 -0
- package/dist/templates/starter-game/session/src/format.ts +8 -0
- package/dist/templates/starter-game/session/src/i18n.ts +51 -0
- package/dist/templates/starter-game/session/src/main.tsx +48 -0
- package/dist/templates/starter-game/session/src/theme.ts +45 -0
- package/dist/templates/starter-game/session/types.ts +135 -0
- package/dist/templates/starter-game/settings/engine/settings.sdk.json +50 -0
- package/dist/templates/starter-game/settings/menu/settings.sdk.json +78 -0
- package/dist/templates/starter-game/settings/session/settings.sdk.json +23 -0
- package/dist/templates/starter-game/settings/ui/settings.sdk.json +25 -0
- package/dist/templates/starter-game/test-setup.ts +7 -0
- package/dist/templates/starter-game/testing/e2e/game.spec.ts +139 -0
- package/dist/templates/starter-game/testing/e2e/global-setup.ts +21 -0
- package/dist/templates/starter-game/testing/engine/game.engine.test.ts +171 -0
- package/dist/templates/starter-game/tsconfig.json +19 -0
- package/dist/templates/starter-game/ui/defaults.ts +60 -0
- package/dist/templates/starter-game/ui/index.html +12 -0
- package/dist/templates/starter-game/ui/src/App.test.tsx +138 -0
- package/dist/templates/starter-game/ui/src/App.tsx +175 -0
- package/dist/templates/starter-game/ui/src/DayBar.test.tsx +78 -0
- package/dist/templates/starter-game/ui/src/DayBar.tsx +72 -0
- package/dist/templates/starter-game/ui/src/HistoryPanel.test.tsx +74 -0
- package/dist/templates/starter-game/ui/src/HistoryPanel.tsx +120 -0
- package/dist/templates/starter-game/ui/src/MakeRow.tsx +129 -0
- package/dist/templates/starter-game/ui/src/Section.tsx +37 -0
- package/dist/templates/starter-game/ui/src/ThemeToggle.tsx +49 -0
- package/dist/templates/starter-game/ui/src/TodayPanel.test.tsx +179 -0
- package/dist/templates/starter-game/ui/src/TodayPanel.tsx +92 -0
- package/dist/templates/starter-game/ui/src/YesterdayPanel.tsx +142 -0
- package/dist/templates/starter-game/ui/src/format.ts +12 -0
- package/dist/templates/starter-game/ui/src/i18n.ts +95 -0
- package/dist/templates/starter-game/ui/src/main.tsx +48 -0
- package/dist/templates/starter-game/ui/src/menu.ts +18 -0
- package/dist/templates/starter-game/ui/src/test-helpers.ts +58 -0
- package/dist/templates/starter-game/ui/src/theme.ts +45 -0
- package/dist/templates/starter-game/ui/src/writes.ts +43 -0
- package/dist/templates/starter-game/ui/types.ts +137 -0
- package/dist/templates/starter-game/ui/ui.sdk.json +11 -0
- package/dist/templates/starter-game/validators/capacity/defaults.ts +37 -0
- package/dist/templates/starter-game/validators/capacity/logic/logic.test.ts +96 -0
- package/dist/templates/starter-game/validators/capacity/logic/logic.ts +45 -0
- package/dist/templates/starter-game/validators/capacity/types.ts +55 -0
- package/dist/templates/starter-game/validators/capacity/validator.sdk.json +11 -0
- package/dist/templates/starter-game/vite.config.ts +19 -0
- package/dist/templates/starter-game/vitest.config.ts +15 -0
- package/dist/templates/starter-game/vitest.engine.config.ts +19 -0
- package/dist/templates/starter-game/waiting_room/defaults.ts +40 -0
- package/dist/templates/starter-game/waiting_room/index.html +12 -0
- package/dist/templates/starter-game/waiting_room/src/App.test.tsx +102 -0
- package/dist/templates/starter-game/waiting_room/src/App.tsx +194 -0
- package/dist/templates/starter-game/waiting_room/src/i18n.ts +33 -0
- package/dist/templates/starter-game/waiting_room/src/main.tsx +48 -0
- package/dist/templates/starter-game/waiting_room/src/theme.ts +45 -0
- package/dist/templates/starter-game/waiting_room/types.ts +59 -0
- package/dist/templates/starter-game/waiting_room/waiting_room.sdk.json +10 -0
- package/dist/templates/tick-controller/logic.ts.liquid +8 -0
- package/dist/templates/ui/index.html.liquid +12 -0
- package/dist/templates/ui/src/App.test.tsx.liquid +41 -0
- package/dist/templates/ui/src/App.tsx.liquid +103 -0
- package/dist/templates/ui/src/i18n.ts.liquid +47 -0
- package/dist/templates/ui/src/main.tsx.liquid +48 -0
- package/dist/templates/ui/src/theme.ts.liquid +46 -0
- package/dist/templates/validator/logic.test.ts.liquid +42 -0
- package/dist/templates/validator/logic.ts.liquid +21 -0
- package/dist/templates/waiting-room/index.html.liquid +12 -0
- package/dist/templates/waiting-room/src/App.test.tsx.liquid +57 -0
- package/dist/templates/waiting-room/src/App.tsx.liquid +118 -0
- package/dist/templates/waiting-room/src/i18n.ts.liquid +44 -0
- package/dist/templates/waiting-room/src/main.tsx.liquid +48 -0
- package/dist/templates/waiting-room/src/theme.ts.liquid +46 -0
- package/dist/templating/resource-metadata.d.ts +131 -0
- package/dist/types.d.ts +14 -0
- package/dist/utils/atomic-write.d.ts +40 -0
- package/dist/utils/cli-error.d.ts +64 -0
- package/dist/utils/document-parser-client.d.ts +20 -0
- package/dist/utils/env-loader.d.ts +40 -0
- package/dist/utils/error-codes.d.ts +335 -0
- package/dist/utils/errors.d.ts +19 -0
- package/dist/utils/global-settings.d.ts +65 -0
- package/dist/utils/llm-client.d.ts +34 -0
- package/dist/utils/logger.d.ts +36 -0
- package/dist/utils/output.d.ts +109 -0
- package/dist/utils/platform.d.ts +14 -0
- package/dist/utils/prompt.d.ts +36 -0
- package/dist/utils/real-path.d.ts +6 -0
- package/dist/utils/schema-properties.d.ts +35 -0
- package/dist/validation/ajv.d.ts +17 -0
- package/dist/validation/facilitator-guide-validator.d.ts +27 -0
- package/dist/validation/findings.d.ts +40 -0
- package/dist/validation/game-constraints.d.ts +10 -0
- package/dist/validation/game-validator.d.ts +34 -0
- package/dist/validation/package-validation.d.ts +41 -0
- package/dist/validation/package-validator.d.ts +19 -0
- package/dist/validation/resource-validator.d.ts +79 -0
- package/dist/validation/schema-defaults-validator.d.ts +29 -0
- package/dist/validation/schema-validation.d.ts +115 -0
- package/dist/validation/semver.d.ts +9 -0
- package/package.json +122 -3
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
# Building session UIs
|
|
2
|
+
|
|
3
|
+
Build facilitator-only UIs that operate across all teams in a session at once: session-wide dashboards, cross-team comparisons, and global game controls.
|
|
4
|
+
|
|
5
|
+
**Keywords:** manage session, session UI, facilitator UI, session-level, cross-team, multi-team dashboard, facilitator controls, session dashboard, all teams, independent mode, shared-state mode
|
|
6
|
+
|
|
7
|
+
## What you'll learn
|
|
8
|
+
|
|
9
|
+
- How to create a session UI
|
|
10
|
+
- How to read and write per-team state from a single session-level view
|
|
11
|
+
- How to target game flow controls at individual teams or all teams at once
|
|
12
|
+
- How to handle both independent and shared-state interaction modes
|
|
13
|
+
- How to configure the session UI per game and per scenario
|
|
14
|
+
|
|
15
|
+
## What are session UIs?
|
|
16
|
+
|
|
17
|
+
The session UI is the facilitator's whole-session screen. The host renders it full-bleed beneath a thin band of its own chrome, and it owns everything below that: teams, participants, and game controls. A game wires in at most one, and a package that ships none falls back to the host's own roster page.
|
|
18
|
+
|
|
19
|
+
| Aspect | Session UI | Game UI |
|
|
20
|
+
| --------------------- | ----------------------------------------------------- | -------------------- |
|
|
21
|
+
| **Who sees it** | Facilitator only | Participants |
|
|
22
|
+
| **Scope** | Every team in the session | Single team |
|
|
23
|
+
| **Data model** | `SessionData` (shared store and teams map) | Flat context |
|
|
24
|
+
| **Where shown** | Full-bleed under the host's chrome | During game play |
|
|
25
|
+
| **Cardinality** | At most one per game | At most one per game |
|
|
26
|
+
| **Tick control** | Per-team or all-teams tick, undo, pause, reset | `requestTick` only |
|
|
27
|
+
| **Roster control** | Create and rename teams, rename and move participants | None |
|
|
28
|
+
| **Interaction modes** | Aware of independent vs shared-state | Not mode-aware |
|
|
29
|
+
|
|
30
|
+
Capability is inferred, not declared: shipping a session UI claims the whole canvas. There is no partial takeover and no per-capability negotiation.
|
|
31
|
+
|
|
32
|
+
## Creating a session UI
|
|
33
|
+
|
|
34
|
+
The starter game already fills this slot, and `new` refuses a filled one with `RESOURCE_EXISTS`. In a package made by `new package`, change it with `update session` instead, or start from `new package --empty`.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pnpm run sdk new session '{
|
|
38
|
+
"definition": {
|
|
39
|
+
"reads": ["price", "workers", "production"],
|
|
40
|
+
"writes": ["price", "workers"]
|
|
41
|
+
},
|
|
42
|
+
"rootDirectory": "."
|
|
43
|
+
}'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This creates:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
session/
|
|
50
|
+
├── session.sdk.json
|
|
51
|
+
├── index.html
|
|
52
|
+
├── types.ts
|
|
53
|
+
└── src/
|
|
54
|
+
├── main.tsx
|
|
55
|
+
├── App.tsx
|
|
56
|
+
├── i18n.ts
|
|
57
|
+
└── theme.ts
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Configuring the surface
|
|
61
|
+
|
|
62
|
+
Give the session UI values a game can vary without a code change by putting them in a settings resource, `settings/session/settings.sdk.json`:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"definition": {
|
|
67
|
+
"settingsSchema": {
|
|
68
|
+
"type": "object",
|
|
69
|
+
"properties": {
|
|
70
|
+
"NoticeboardLabel": { "type": "string", "default": "Noticeboard" },
|
|
71
|
+
"ShowJoinLinks": { "type": "boolean", "default": true }
|
|
72
|
+
},
|
|
73
|
+
"additionalProperties": false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
and listing it in `session/session.sdk.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"definition": {
|
|
84
|
+
"reads": ["noticeboard"],
|
|
85
|
+
"writes": ["noticeboard"],
|
|
86
|
+
"settings": ["session"]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The resolved values arrive as `settings` on
|
|
92
|
+
[`useSessionUserInterfaceData`](#usesessionuserinterfacedata), keyed by
|
|
93
|
+
settings identifier (`settings.session.ShowJoinLinks`) and typed as the
|
|
94
|
+
generated `Settings` from the session UI's `types.ts`. The surface receives
|
|
95
|
+
exactly the settings it lists, so it can also read the engine's by listing
|
|
96
|
+
`engine`, and never sees one it does not list.
|
|
97
|
+
|
|
98
|
+
The values resolve like any settings (see [Overriding settings](./settings.md#overriding-settings)).
|
|
99
|
+
To change a value for every session, edit its default. To vary it by
|
|
100
|
+
scenario, set it under the settings identifier in the scenario's
|
|
101
|
+
`settings`:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"definition": {
|
|
106
|
+
"locales": {
|
|
107
|
+
"en": {
|
|
108
|
+
"name": "Executive briefing"
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"settings": {
|
|
112
|
+
"session": {
|
|
113
|
+
"ShowJoinLinks": false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"rootDirectory": "."
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Every layer is partial: set only the keys you want to change, and the rest
|
|
122
|
+
fall through. Settings are fixed when play starts. See
|
|
123
|
+
[Settings](./settings.md) for the whole model.
|
|
124
|
+
|
|
125
|
+
## Building the session UI
|
|
126
|
+
|
|
127
|
+
Use `useSessionUserInterfaceData` to read session data and write per-team overrides. The data model carries the shared store once, and wraps each team's own context, tick count, and timer state, into a `SessionData` structure:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
type SharedState = {
|
|
131
|
+
// The shared-scope capabilities this UI declares in reads and writes
|
|
132
|
+
context: Record<string, unknown>;
|
|
133
|
+
// Wrapper plumbing; nothing on screen needs it
|
|
134
|
+
revision: number;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type TeamState = {
|
|
138
|
+
// This team's own capabilities (team scope only), projected onto
|
|
139
|
+
// the capabilities this UI declares in reads and writes
|
|
140
|
+
context: Record<string, unknown>;
|
|
141
|
+
completedTicks: number; // ticks this team has completed
|
|
142
|
+
timer: TimerState; // the timer, as a game UI reads it
|
|
143
|
+
// Wrapper plumbing; nothing on screen needs it
|
|
144
|
+
revision: number;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type SessionData = {
|
|
148
|
+
shared: SharedState; // present even in a session with no teams
|
|
149
|
+
teams: Record<string, TeamState>;
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`timer` is the one [`TimerState`](../api/sdk-ui.md#timerstate) shape a game UI reads from `useUserInterfaceTimer()`, so a countdown is drawn as in [Displaying the tick timer](./ui.md#displaying-the-tick-timer).
|
|
154
|
+
|
|
155
|
+
Three notes on that shape. Each `context` carries only the capabilities this session UI declared (see [Capabilities](./capabilities.md#a-declaration-is-what-gets-delivered)), so declare everything the screen renders. A `scope: 'shared'` capability arrives once, in `shared.context`, and never in a team's `context`: read it there rather than off whichever team comes first. And `revision` is how the wrapper tracks updates: on a large session the host sends one team's state (or the shared store) rather than every team's, and the wrapper merges each one into the `SessionData` you read, asking for a fresh snapshot if it notices one went missing. You never need to read it, and you can leave it out of hand-written test fixtures.
|
|
156
|
+
|
|
157
|
+
The SDK types are generic (`SessionData<TTeamContext, TSharedContext>`, `TeamState<TTeamContext>` and `SharedState<TSharedContext>`), and the generated `types.ts` beside your session UI exports `SessionData` already filled in with its `TeamContext` and `SharedContext`. Pass it as the first generic of `useSessionUserInterfaceData` and both halves are typed:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import type { SessionData, Settings } from '../types';
|
|
161
|
+
|
|
162
|
+
const { sessionData } = useSessionUserInterfaceData<SessionData, Settings>();
|
|
163
|
+
const briefing = sessionData.shared.context.briefing.Message; // once, not off teams[0]
|
|
164
|
+
const price = sessionData.teams['team-1']?.context.price.Price; // a team's own
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The keys of `sessionData.teams` are stable team ids (`team-1`, `team-2`, and so on). Each id carries the team's zero-based `index` (`team-N` carries index N-1), and the id never renumbers. Because removing a team leaves a gap, the key set can be sparse (for example `team-1` and `team-3` with no `team-2`); iterate the keys rather than assuming a contiguous `1..N` range. Each `TeamInfo` from `useSessionUserInterfaceTeams` carries the matching `index`, so a UI can find its own slot with `teams.find((t) => t.teamId === participant.teamId)?.index`.
|
|
168
|
+
|
|
169
|
+
### Reading session data
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { useSessionUserInterfaceData } from '@seriousgaming/sdk/ui';
|
|
173
|
+
import { Typography, Paper, Grid } from '@mui/material';
|
|
174
|
+
|
|
175
|
+
function App() {
|
|
176
|
+
const { sessionData, settings, setData } = useSessionUserInterfaceData();
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
<Grid container spacing={2}>
|
|
180
|
+
{Object.entries(sessionData.teams).map(([teamId, team]) => (
|
|
181
|
+
<Grid item xs={6} key={teamId}>
|
|
182
|
+
<Paper sx={{ p: 2 }}>
|
|
183
|
+
<Typography variant="h6">{teamId} (Tick {team.completedTicks})</Typography>
|
|
184
|
+
<Typography>
|
|
185
|
+
Price: {(team.context.price as any)?.Price}
|
|
186
|
+
</Typography>
|
|
187
|
+
<Typography>
|
|
188
|
+
Production: {(team.context.production as any)?.Production}
|
|
189
|
+
</Typography>
|
|
190
|
+
</Paper>
|
|
191
|
+
</Grid>
|
|
192
|
+
))}
|
|
193
|
+
</Grid>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export default App;
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Writing per-team overrides
|
|
201
|
+
|
|
202
|
+
The session-level `setData` requires a `teamId` parameter to specify which team to update, since the session UI is not scoped to one team:
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
import { useSessionUserInterfaceData } from '@seriousgaming/sdk/ui';
|
|
206
|
+
import { TextField } from '@mui/material';
|
|
207
|
+
|
|
208
|
+
function TeamPriceEditor({ teamId }: { teamId: string }) {
|
|
209
|
+
const { sessionData, setData } = useSessionUserInterfaceData();
|
|
210
|
+
const team = sessionData.teams[teamId];
|
|
211
|
+
|
|
212
|
+
return (
|
|
213
|
+
<TextField
|
|
214
|
+
label={`${teamId} Price`}
|
|
215
|
+
type="number"
|
|
216
|
+
value={(team.context.price as any)?.Price}
|
|
217
|
+
onChange={(e) =>
|
|
218
|
+
setData(teamId, {
|
|
219
|
+
price: { Price: Number(e.target.value) },
|
|
220
|
+
})
|
|
221
|
+
}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Changes made via `setData` are sent immediately to the dev server and merged into the target team's context, so there is no 'Apply' button to press.
|
|
228
|
+
|
|
229
|
+
A team write may change only the capabilities this UI declares in `writes`. One that changes any other capability, or names a key that is not a capability, is refused as `denied` and none of it is applied. Sending a team's whole context back with one field edited is fine: a capability the write carries unchanged is not a change.
|
|
230
|
+
|
|
231
|
+
The one-argument form, `setData(update)`, writes `scope: 'shared'` capabilities session-wide instead, and merges optimistically into `sessionData.shared.context`. See [Facilitator-writable shared capabilities](./multi-team-games.md#facilitator-writable-shared-capabilities).
|
|
232
|
+
|
|
233
|
+
Type both forms with the generated types in the surface's `types.ts`. `SharedWrites` is the shared capabilities in this UI's `writes`, and `TeamWrites` the rest of them, so a team write naming a shared capability fails to compile rather than being refused when the session holds more than one team:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
import type { SessionData, Settings, SharedWrites, TeamWrites } from '../types';
|
|
237
|
+
|
|
238
|
+
const { setData } = useSessionUserInterfaceData<SessionData, Settings, SharedWrites, TeamWrites>();
|
|
239
|
+
|
|
240
|
+
setData(teamId, { price: { Price: 12 } }); // TeamWrites
|
|
241
|
+
setData({ dials: { BasePrice: 12 } }); // SharedWrites
|
|
242
|
+
setData(teamId, { dials: { BasePrice: 12 } }); // compile error: dials is shared
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Removing a key
|
|
246
|
+
|
|
247
|
+
A merge cannot remove a key, so both forms take `deletes` as their last argument, as a player surface's `setData` does. The paths are removed after the update merges:
|
|
248
|
+
|
|
249
|
+
```typescript
|
|
250
|
+
// Take one team's note down
|
|
251
|
+
setData(teamId, {}, { deletes: [['pinboard', 'Notes', noteId]] });
|
|
252
|
+
|
|
253
|
+
// Remove an offer from the shared store, for every team
|
|
254
|
+
setData({}, { deletes: [`market.Offers.${offerId}`] });
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
The host holds each path to this UI's `writes`: a team delete must name a team-scope capability it writes, and a shared delete a shared one it writes. Anything else is refused as `denied`, and none of the write is applied. A shared capability has one value for the session, so remove from it with the shared form, never through a team. See [Design state for concurrent writes](../recipes/design-state-for-concurrent-writes.md) for when to delete rather than write `null`.
|
|
258
|
+
|
|
259
|
+
### Per-team views
|
|
260
|
+
|
|
261
|
+
A common pattern is to iterate over teams and render a card or panel for each one:
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
import { useSessionUserInterfaceData } from '@seriousgaming/sdk/ui';
|
|
265
|
+
import { useSessionUserInterfaceTeams } from '@seriousgaming/sdk/ui';
|
|
266
|
+
import { Card, CardContent, Typography, Grid, TextField } from '@mui/material';
|
|
267
|
+
|
|
268
|
+
function SessionDashboard() {
|
|
269
|
+
const { sessionData, setData } = useSessionUserInterfaceData();
|
|
270
|
+
const teams = useSessionUserInterfaceTeams();
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<Grid container spacing={2}>
|
|
274
|
+
{teams.map((team) => {
|
|
275
|
+
const state = sessionData.teams[team.teamId];
|
|
276
|
+
if (!state) return null;
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<Grid item xs={12} md={6} key={team.teamId}>
|
|
280
|
+
<Card>
|
|
281
|
+
<CardContent>
|
|
282
|
+
<Typography variant="h6">{team.name}</Typography>
|
|
283
|
+
<Typography variant="body2">Tick: {state.completedTicks}</Typography>
|
|
284
|
+
<TextField
|
|
285
|
+
label="Workers"
|
|
286
|
+
type="number"
|
|
287
|
+
value={(state.context.workers as any)?.WorkerCount}
|
|
288
|
+
onChange={(e) =>
|
|
289
|
+
setData(team.teamId, {
|
|
290
|
+
workers: {
|
|
291
|
+
WorkerCount: Number(e.target.value),
|
|
292
|
+
},
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
/>
|
|
296
|
+
</CardContent>
|
|
297
|
+
</Card>
|
|
298
|
+
</Grid>
|
|
299
|
+
);
|
|
300
|
+
})}
|
|
301
|
+
</Grid>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Interaction mode awareness
|
|
307
|
+
|
|
308
|
+
The interaction mode lives on `useSessionUserInterfaceTeamConfiguration()`, alongside the team bounds: one hook for how teams work, one for which teams exist. Use it to adapt the UI, for example by hiding per-team tick controls in shared-state mode (where all teams tick together):
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { useSessionUserInterfaceTeamConfiguration } from '@seriousgaming/sdk/ui';
|
|
312
|
+
import { Alert } from '@mui/material';
|
|
313
|
+
|
|
314
|
+
function ModeIndicator() {
|
|
315
|
+
const { mode } = useSessionUserInterfaceTeamConfiguration();
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<Alert severity="info">
|
|
319
|
+
{mode === 'shared-state'
|
|
320
|
+
? 'Shared-state mode: all teams tick together.'
|
|
321
|
+
: 'Independent mode: each team ticks on its own schedule.'}
|
|
322
|
+
</Alert>
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
In **shared-state** mode, calling `requestTick()` without a `teamId` advances all teams simultaneously. In **independent** mode, you can target individual teams or advance all teams at once: with no `teamId`, every team that can still move advances by one from its own round, so a team that is behind stays behind, and a team that is complete or at the round limit is skipped.
|
|
328
|
+
|
|
329
|
+
## Controlling game flow
|
|
330
|
+
|
|
331
|
+
Session UIs have full control over the game lifecycle via callbacks. Every game-flow callback accepts an optional `teamId`; when it is omitted the action applies to all teams.
|
|
332
|
+
|
|
333
|
+
Every game-flow callback except `pauseTimer` and `resumeTimer` returns a promise that resolves once the host has committed the change, and rejects with a `MutationError` when it will not. Await it when you want to report the outcome honestly; ignore it when you do not. Either way the resulting state arrives through the normal session data channel.
|
|
334
|
+
|
|
335
|
+
### Triggering ticks
|
|
336
|
+
|
|
337
|
+
```typescript
|
|
338
|
+
import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
|
|
339
|
+
import { useSessionUserInterfaceTeams } from '@seriousgaming/sdk/ui';
|
|
340
|
+
import { useSessionUserInterfaceTeamConfiguration } from '@seriousgaming/sdk/ui';
|
|
341
|
+
import { Button, Stack } from '@mui/material';
|
|
342
|
+
|
|
343
|
+
function TickControls() {
|
|
344
|
+
const { requestTick } = useSessionUserInterfaceCallbacks();
|
|
345
|
+
const teams = useSessionUserInterfaceTeams();
|
|
346
|
+
const { mode } = useSessionUserInterfaceTeamConfiguration();
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<Stack spacing={1}>
|
|
350
|
+
<Button onClick={() => requestTick()} variant="contained">
|
|
351
|
+
Advance All Teams
|
|
352
|
+
</Button>
|
|
353
|
+
{mode === 'independent' &&
|
|
354
|
+
teams.map((team) => (
|
|
355
|
+
<Button
|
|
356
|
+
key={team.teamId}
|
|
357
|
+
onClick={() => requestTick(team.teamId)}
|
|
358
|
+
variant="outlined"
|
|
359
|
+
>
|
|
360
|
+
Advance {team.name}
|
|
361
|
+
</Button>
|
|
362
|
+
))}
|
|
363
|
+
</Stack>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
The session UI's `requestTick` control is how facilitators advance the game: there is no built-in next tick button. The scaffolded session UI wires a session-wide tick button out of the box; during development the console's dev drawer also has a developer-only **Tick** button.
|
|
369
|
+
|
|
370
|
+
### Pausing and resuming the timer
|
|
371
|
+
|
|
372
|
+
When a game uses a [tick controller](./tick-controller.md) with automatic ticking, facilitators can pause and resume per-team or globally:
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
|
|
376
|
+
import { Button, Stack } from '@mui/material';
|
|
377
|
+
|
|
378
|
+
function TimerControls({ teamId }: { teamId?: string }) {
|
|
379
|
+
const { pauseTimer, resumeTimer } = useSessionUserInterfaceCallbacks();
|
|
380
|
+
|
|
381
|
+
return (
|
|
382
|
+
<Stack direction="row" spacing={1}>
|
|
383
|
+
<Button onClick={() => pauseTimer(teamId)} variant="outlined" color="warning">
|
|
384
|
+
{teamId ? `Pause ${teamId}` : 'Pause All'}
|
|
385
|
+
</Button>
|
|
386
|
+
<Button onClick={() => resumeTimer(teamId)} variant="contained" color="success">
|
|
387
|
+
{teamId ? `Resume ${teamId}` : 'Resume All'}
|
|
388
|
+
</Button>
|
|
389
|
+
</Stack>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
While paused, manual ticks via `requestTick` still work. Timer state for each team is available in `sessionData.teams[teamId].timer`.
|
|
395
|
+
|
|
396
|
+
### Extending a tick
|
|
397
|
+
|
|
398
|
+
Pausing is the wrong tool for 'give them two more minutes'. It stops the clock until someone remembers to start it again, and it does not move the deadline your countdown is drawn from. `extendTick` moves the deadline instead. It takes milliseconds:
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
|
|
402
|
+
import { Button, Stack } from '@mui/material';
|
|
403
|
+
|
|
404
|
+
function ExtendControls({ teamId }: { teamId?: string }) {
|
|
405
|
+
const { extendTick } = useSessionUserInterfaceCallbacks();
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<Stack direction="row" spacing={1}>
|
|
409
|
+
<Button onClick={() => void extendTick(120_000, teamId)}>+2 min</Button>
|
|
410
|
+
<Button onClick={() => void extendTick(-30_000, teamId)}>-30 sec</Button>
|
|
411
|
+
</Stack>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
Three things worth knowing:
|
|
417
|
+
|
|
418
|
+
- **It moves one window, not the cadence.** The interval your tick controller returns for later ticks is untouched, so a session does not run permanently late because one tick ran long
|
|
419
|
+
- **A negative value pulls the deadline in**, clamped at now. It never fires a tick retroactively
|
|
420
|
+
- **Read the result back from `extendedMs`** on the team's timer rather than inferring it from a moved `nextTickAt`. A deadline that jumped is equally the signature of a freshly scheduled tick, so `extendedMs` is the only honest source for 'extended by 2m'
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
const { extendedMs } = sessionData.teams[teamId].timer;
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
It rejects with a `MutationError` carrying `{ kind: 'denied' }` when there is no tick to move: automatic ticking is off, the game is complete, or the timer is paused. Resume first, then extend.
|
|
427
|
+
|
|
428
|
+
Deliberately, the [tick interval bounds](./tick-controller.md) do not apply. They bound the cadence a tick controller may schedule; a facilitator in the room already reaches outside both with an immediate `requestTick` and an indefinite `pauseTimer`.
|
|
429
|
+
|
|
430
|
+
### Opening host overlays
|
|
431
|
+
|
|
432
|
+
Some things a facilitator wants are drawn by the host rather than by your game: the [facilitator guide](./guide.md), a team's print sheet. Your session UI owns the whole canvas, so without a way to ask, a facilitator has to leave it to reach them.
|
|
433
|
+
|
|
434
|
+
`openHostOverlay` asks. The host renders the overlay in its own chrome, above your session UI, where you can neither restyle nor suppress it:
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
import {
|
|
438
|
+
useSessionUserInterfaceCallbacks,
|
|
439
|
+
useSessionUserInterfaceHostOverlays,
|
|
440
|
+
} from '@seriousgaming/sdk/ui';
|
|
441
|
+
import { Button, Stack } from '@mui/material';
|
|
442
|
+
|
|
443
|
+
function OverlayControls() {
|
|
444
|
+
const overlays = useSessionUserInterfaceHostOverlays();
|
|
445
|
+
const { openHostOverlay } = useSessionUserInterfaceCallbacks();
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<Stack direction="row" spacing={1}>
|
|
449
|
+
{overlays.includes('facilitator-guide') && (
|
|
450
|
+
<Button onClick={() => void openHostOverlay('facilitator-guide')}>Guide</Button>
|
|
451
|
+
)}
|
|
452
|
+
{overlays.includes('print-sheets') && (
|
|
453
|
+
<Button onClick={() => void openHostOverlay('print-sheets')}>Print sheets</Button>
|
|
454
|
+
)}
|
|
455
|
+
</Stack>
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Always gate on `useSessionUserInterfaceHostOverlays()`.** Which overlays exist is the host's answer, not yours: a production host and the dev server do not offer the same set, and the guide is only available when the active game declares one. A button for an overlay the host does not have is a button that cannot work. The list is empty until the host answers, and it can change mid-session when a scenario switch changes which guides the game declares.
|
|
461
|
+
|
|
462
|
+
The second argument narrows the overlay:
|
|
463
|
+
|
|
464
|
+
| Overlay | Second argument | Omitted means |
|
|
465
|
+
| --------------------- | ------------------------------------- | ------------------------------- |
|
|
466
|
+
| `'facilitator-guide'` | A guide identifier your game declares | The game's first declared guide |
|
|
467
|
+
| `'print-sheets'` | A team id | Every team |
|
|
468
|
+
|
|
469
|
+
It rejects with a `MutationError` carrying `{ kind: 'denied' }` for an overlay the host does not offer and for an argument that does not resolve.
|
|
470
|
+
|
|
471
|
+
### Undoing and resetting
|
|
472
|
+
|
|
473
|
+
Facilitators can revert the last tick or reset games, targeted at a specific team or all teams:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
|
|
477
|
+
import { Button, Stack } from '@mui/material';
|
|
478
|
+
|
|
479
|
+
function GameControls() {
|
|
480
|
+
const { undoTick, resetGame } = useSessionUserInterfaceCallbacks();
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<Stack direction="row" spacing={1}>
|
|
484
|
+
<Button onClick={() => undoTick()} variant="outlined">
|
|
485
|
+
Undo All Teams
|
|
486
|
+
</Button>
|
|
487
|
+
<Button onClick={() => resetGame()} variant="outlined" color="error">
|
|
488
|
+
Reset All Teams
|
|
489
|
+
</Button>
|
|
490
|
+
</Stack>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
- **`undoTick(teamId?)`** puts teams back on the board they were holding when the last tick was taken, decisions and all: it un-runs the engine rather than re-dealing the tick. When `teamId` is omitted, reverts all teams; naming one is independent mode only, since shared-state teams advance together. With no history to undo, the promise rejects with a `MutationError` whose `outcome.kind` is `denied`.
|
|
496
|
+
- **`resetGame(teamId?)`** clears all context, deletes game-scoped blobs, and re-initialises from scratch. When `teamId` is omitted, resets all teams. Settings are preserved.
|
|
497
|
+
|
|
498
|
+
### Opening and closing the waiting room
|
|
499
|
+
|
|
500
|
+
`setWaitingRoom` parks the session: every participant's game UI is replaced with the game's [waiting room UI](./waiting-room.md) until you close it again.
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
const { setWaitingRoom } = useSessionUserInterfaceCallbacks();
|
|
504
|
+
|
|
505
|
+
<Button onClick={() => setWaitingRoom(true)} variant="outlined">
|
|
506
|
+
Open waiting room
|
|
507
|
+
</Button>
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
- **`setWaitingRoom(open)`** takes **no `teamId`**, unlike every other control above. The waiting room is one flag for the whole session, so there is no per-team variant to target
|
|
511
|
+
- Game state is untouched in both directions. Per-team context, the current tick, and timers all survive, which is the point: park a live session for a discussion and resume exactly where teams were
|
|
512
|
+
- It does not stop the clock. Automatic ticks keep firing while participants wait; halting play is the tick controller's decision, via `waitingRoomOpen`
|
|
513
|
+
- It resolves once the host confirms the change and rejects with a `MutationError` otherwise
|
|
514
|
+
|
|
515
|
+
The host provides its own toggle too, so building one is optional. Reach for this when you want the control to sit inside your own facilitator layout, or to pair it with a note you set on a shared capability at the same moment.
|
|
516
|
+
|
|
517
|
+
### Controlling the roster
|
|
518
|
+
|
|
519
|
+
The session UI owns the roster. Five callbacks create, rename and delete teams, and rename and move participants:
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
const { createTeam, renameTeam, deleteTeam, renameParticipant, moveParticipant } =
|
|
523
|
+
useSessionUserInterfaceCallbacks();
|
|
524
|
+
|
|
525
|
+
await createTeam('Marketing'); // name is optional
|
|
526
|
+
await renameTeam('team-1', 'Sales');
|
|
527
|
+
await renameParticipant('participant-3', 'Sam');
|
|
528
|
+
await moveParticipant('participant-3', 'team-2');
|
|
529
|
+
await deleteTeam('team-3'); // independent mode, empty teams only
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
Each resolves on commit and rejects with a `MutationError`. Three refusals arrive as `{ kind: 'denied' }`: the team count is at the effective maximum, a team is being added to a shared-state session (whose team count is fixed at creation, though the dev server relaxes that before the first tick as an authoring convenience), or the session is read-only. **A denial is terminal**: unlike `conflict`, waiting and re-sending cannot change the outcome, so surface it rather than looping.
|
|
533
|
+
|
|
534
|
+
Creating or erasing a participant is deliberately not exposed.
|
|
535
|
+
|
|
536
|
+
#### Deleting a team
|
|
537
|
+
|
|
538
|
+
`deleteTeam(teamId)` removes a team from an **independent-mode** session. Every runtime supports it, unlike `createTeam`, which a runtime may refuse. It is refused with `{ kind: 'denied' }` when:
|
|
539
|
+
|
|
540
|
+
- The session is shared-state, at any point, before the first tick included. A shared-state engine initialised over the whole cohort, so its team count is fixed for the session
|
|
541
|
+
- The team still has participants, connected or not. A participant is never erased, so move every one out with `moveParticipant` first
|
|
542
|
+
- It is the last team in the session
|
|
543
|
+
- The session does not hold the team, or is read-only
|
|
544
|
+
|
|
545
|
+
A deleted team's index is left as a gap, and no other team's index or id changes. The next `createTeam` takes the lowest free index back, as a new team with the default name: nothing of the deleted team returns with it. Undo never brings a deleted team back, and no hook fires.
|
|
546
|
+
|
|
547
|
+
Offer the control only where it can succeed, so a facilitator is not shown a button the host will refuse:
|
|
548
|
+
|
|
549
|
+
```tsx
|
|
550
|
+
const { mode } = useSessionUserInterfaceTeamConfiguration();
|
|
551
|
+
const participants = useSessionUserInterfaceParticipants();
|
|
552
|
+
const { deleteTeam } = useSessionUserInterfaceCallbacks();
|
|
553
|
+
|
|
554
|
+
{
|
|
555
|
+
mode === 'independent' && (
|
|
556
|
+
<Button
|
|
557
|
+
disabled={teams.length <= 1 || participants.some((p) => p.teamId === team.teamId)}
|
|
558
|
+
onClick={() => void deleteTeam(team.teamId).catch(showDenial)}
|
|
559
|
+
>
|
|
560
|
+
Delete team
|
|
561
|
+
</Button>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
The host enforces every rule itself, so the disabled state is a convenience, not a guard. In the dev server every team starts with a placeholder participant, so move it out first, or add the team with `pnpm run sdk team add --participants 0`.
|
|
567
|
+
|
|
568
|
+
`renameTeam` is not the only writer. A team's own participants can rename it from a game UI or waiting room UI, so a session UI should render the name from `setTeamList` rather than from its own optimistic state: there is one name per team, not one per writer, and the last write wins. A blank name and one past the host's cap (60 characters) are both refused as terminal denials, for either writer.
|
|
569
|
+
|
|
570
|
+
#### Moving a participant
|
|
571
|
+
|
|
572
|
+
A move has a consequence worth designing for, and one that is easy to confuse it with:
|
|
573
|
+
|
|
574
|
+
- The participant's **browser loses local component state**, because their game UI remounts against the destination team. Ordinary remount behaviour, and rarely worth warning anyone about
|
|
575
|
+
- Their **earlier writes stay with the source team**. Anything they wrote through `setData` is in that team's context and does not travel, so they arrive with none of their own state
|
|
576
|
+
|
|
577
|
+
A move does not reset either team. The source team's context, including every pending pre-tick decision in it, is untouched.
|
|
578
|
+
|
|
579
|
+
The second consequence is what a game handles, and the mechanism is the [`participantMove` hook](./hooks.md): seed the destination on `phase: 'arrival'` the way `participantJoin` seeds a new participant. There is no transfer channel: a hook returns one team's context, so the departure hook's return value is invisible to arrival. A shared-state game carries state across through the shared store; an independent-mode game re-derives it. Warning a facilitator is the fallback for the case where it genuinely cannot be re-derived, not the default: they cannot implement a hook mid-session, so the warning is only useful where you already know re-seeding is impossible.
|
|
580
|
+
|
|
581
|
+
## Working with participants
|
|
582
|
+
|
|
583
|
+
Session UIs can enumerate participants across all teams in the session. The hook uses the `Participant` type, which includes a `teamId` field for grouping participants by team.
|
|
584
|
+
|
|
585
|
+
### Reading the participant list
|
|
586
|
+
|
|
587
|
+
```typescript
|
|
588
|
+
import {
|
|
589
|
+
useSessionUserInterfaceParticipants,
|
|
590
|
+
type Participant,
|
|
591
|
+
} from '@seriousgaming/sdk/ui';
|
|
592
|
+
import { List, ListItem, ListItemText, Chip, Typography, Stack } from '@mui/material';
|
|
593
|
+
|
|
594
|
+
function AllParticipantsPanel() {
|
|
595
|
+
const participants = useSessionUserInterfaceParticipants();
|
|
596
|
+
|
|
597
|
+
// Group by team
|
|
598
|
+
const byTeam = participants.reduce<Record<string, Participant[]>>(
|
|
599
|
+
(acc, p) => {
|
|
600
|
+
(acc[p.teamId] ??= []).push(p);
|
|
601
|
+
return acc;
|
|
602
|
+
},
|
|
603
|
+
{}
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
<>
|
|
608
|
+
{Object.entries(byTeam).map(([teamId, members]) => (
|
|
609
|
+
<div key={teamId}>
|
|
610
|
+
<Typography variant="h6">{teamId}</Typography>
|
|
611
|
+
<List>
|
|
612
|
+
{members.map((p) => (
|
|
613
|
+
<ListItem key={p.id}>
|
|
614
|
+
<ListItemText
|
|
615
|
+
primary={p.name}
|
|
616
|
+
/>
|
|
617
|
+
<Chip
|
|
618
|
+
size="small"
|
|
619
|
+
label={p.connected ? 'Online' : 'Offline'}
|
|
620
|
+
color={p.connected ? 'success' : 'default'}
|
|
621
|
+
/>
|
|
622
|
+
</ListItem>
|
|
623
|
+
))}
|
|
624
|
+
</List>
|
|
625
|
+
</div>
|
|
626
|
+
))}
|
|
627
|
+
</>
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
The hook updates reactively whenever:
|
|
633
|
+
|
|
634
|
+
- A new participant joins any team
|
|
635
|
+
- A participant disconnects (`connected` flips to `false`; the entry stays so you can see who was here)
|
|
636
|
+
|
|
637
|
+
`Participant` exposes:
|
|
638
|
+
|
|
639
|
+
| Field | Type | Notes |
|
|
640
|
+
| ----------- | --------- | ------------------------------------------------------- |
|
|
641
|
+
| `id` | `string` | Stable identifier; survives reconnects within a session |
|
|
642
|
+
| `name` | `string` | Display name (defaults to `Participant N`) |
|
|
643
|
+
| `index` | `number` | Zero-based join order within the team |
|
|
644
|
+
| `connected` | `boolean` | Whether the participant has a live platform connection |
|
|
645
|
+
| `teamId` | `string` | The team this participant belongs to (e.g. `"team-1"`) |
|
|
646
|
+
|
|
647
|
+
## Rendering inside the host
|
|
648
|
+
|
|
649
|
+
A session UI is not a standalone page. The host mounts it in an iframe under a thin band of its own chrome, and that changes several things a full-page layout takes for granted. None of it is enforced, and all of it is visible to a facilitator the moment it goes wrong.
|
|
650
|
+
|
|
651
|
+
### Leave the ground to the host
|
|
652
|
+
|
|
653
|
+
Do not paint a page background. The host paints the canvas behind your session UI, and a surface painting its own reads as a panel bolted onto the page rather than part of it. It also never quite matches: the host's background is not a colour your theme exposes, so any value you pick is a guess that drifts the next time the host's palette moves.
|
|
654
|
+
|
|
655
|
+
MUI's `CssBaseline` paints `background.default` on the body, so override it:
|
|
656
|
+
|
|
657
|
+
```tsx
|
|
658
|
+
import { CssBaseline, GlobalStyles, ThemeProvider } from '@mui/material';
|
|
659
|
+
|
|
660
|
+
const TRANSPARENT_BODY = { body: { backgroundColor: 'transparent' } };
|
|
661
|
+
|
|
662
|
+
<ThemeProvider theme={theme}>
|
|
663
|
+
<CssBaseline enableColorScheme />
|
|
664
|
+
<GlobalStyles styles={TRANSPARENT_BODY} />
|
|
665
|
+
{/* your surface */}
|
|
666
|
+
</ThemeProvider>;
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
Paint backgrounds on the things that are meant to look raised, like cards and panels, not on the page.
|
|
670
|
+
|
|
671
|
+
### Prefer full-screen dialogs
|
|
672
|
+
|
|
673
|
+
A centred modal inside an iframe is centred within the frame, not the viewport, and its backdrop dims only the frame. The host's chrome above stays bright, so the result reads as a dialog inside a box rather than a dialog over the page. Use `fullScreen`:
|
|
674
|
+
|
|
675
|
+
```tsx
|
|
676
|
+
<Dialog open={open} onClose={close} fullScreen>
|
|
677
|
+
…
|
|
678
|
+
</Dialog>
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
The same applies to anything anchored to the viewport. A toast pinned to the top, a drawer sliding in from an edge, a sticky bar: each lands against the frame's edge rather than the screen's. Your session UI cannot escape its bounds, so design the overlay to sit inside them deliberately instead of discovering where it lands.
|
|
682
|
+
|
|
683
|
+
### Do not reproduce host chrome
|
|
684
|
+
|
|
685
|
+
The Game view / Platform view toggle, the language picker, the theme control, and the guide button all live in the chrome above your session UI. A second copy inside it looks like the same control and is not: yours cannot switch the host's view, and two language menus on one screen is a bug report waiting to happen.
|
|
686
|
+
|
|
687
|
+
When you want one of the host's own overlays, ask for it with [`openHostOverlay`](#opening-host-overlays) rather than rebuilding it.
|
|
688
|
+
|
|
689
|
+
### Own your internal scroll
|
|
690
|
+
|
|
691
|
+
The session UI fills the canvas beneath the chrome at a fixed height. There is no resize handshake, so a surface that grows past the canvas is clipped rather than accommodated. Give your layout a real height and let the part that grows scroll:
|
|
692
|
+
|
|
693
|
+
```tsx
|
|
694
|
+
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column', minHeight: 0 }}>
|
|
695
|
+
<Header />
|
|
696
|
+
<Box sx={{ flex: 1, overflowY: 'auto', minHeight: 0 }}>{/* the long part */}</Box>
|
|
697
|
+
</Box>
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
### Take colour from the theme
|
|
701
|
+
|
|
702
|
+
Read colours from the theme hook below rather than hard-coding them, so your session UI follows the host's light and dark scheme. A surface with a fixed palette is the one thing on screen that does not change when a facilitator switches theme in the room's lighting.
|
|
703
|
+
|
|
704
|
+
## Using the theme
|
|
705
|
+
|
|
706
|
+
Edit `src/theme.ts` to use the session theme hook:
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
import { useSessionUserInterfaceTheme } from '@seriousgaming/sdk/ui';
|
|
710
|
+
import { createTheme } from '@mui/material/styles';
|
|
711
|
+
import { useMemo } from 'react';
|
|
712
|
+
|
|
713
|
+
export function useAppTheme() {
|
|
714
|
+
const sdkTheme = useSessionUserInterfaceTheme();
|
|
715
|
+
return useMemo(
|
|
716
|
+
() =>
|
|
717
|
+
createTheme({
|
|
718
|
+
palette: {
|
|
719
|
+
mode: sdkTheme.mode,
|
|
720
|
+
primary: {
|
|
721
|
+
main: (sdkTheme.mode === 'light' ? sdkTheme.light : sdkTheme.dark).primary,
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
}),
|
|
725
|
+
[sdkTheme]
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
## Dev server behaviour
|
|
731
|
+
|
|
732
|
+
When you run `pnpm run sdk dev` for a game with session UIs:
|
|
733
|
+
|
|
734
|
+
1. Your session UI **is** the console. The dev server renders it full-bleed in its canvas, under a band of chrome, and opens on it by default whenever the package ships one
|
|
735
|
+
2. The chrome's **Game view / Platform view** toggle switches between your session UI and the dev server's own roster page, which is the fallback for a package that ships no session UI. It sits outside your iframe, so your session UI cannot suppress it
|
|
736
|
+
3. The chrome also carries the language picker, the theme control, the facilitator guide and print sheets, the scenario and setup entries, and undo / reset / reload. Do not rebuild these inside your session UI; to open the guide or a print sheet, use [`openHostOverlay`](#opening-host-overlays)
|
|
737
|
+
4. The route `/session` also works directly, which is useful for iterating on the session UI alone. Opened that way there is no chrome above it, so a host overlay opens in a new tab rather than in a dialog
|
|
738
|
+
5. Any `setData` calls from the UI iframe are immediately forwarded to the dev server and reflected in the relevant team's game UI
|
|
739
|
+
|
|
740
|
+
Every surface (the console, each game UI, and the session UI) stays in sync via WebSocket.
|
|
741
|
+
|
|
742
|
+
## Managing session UIs
|
|
743
|
+
|
|
744
|
+
### Update definition
|
|
745
|
+
|
|
746
|
+
```bash
|
|
747
|
+
pnpm run sdk update session '{
|
|
748
|
+
"definition": {
|
|
749
|
+
"reads": ["price", "workers"]
|
|
750
|
+
}
|
|
751
|
+
}'
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
### Delete
|
|
755
|
+
|
|
756
|
+
Empties the `session/` slot. A package declares whatever is on disk, so removing the directory is the whole of the removal:
|
|
757
|
+
|
|
758
|
+
```bash
|
|
759
|
+
pnpm run sdk delete session
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## Testing
|
|
763
|
+
|
|
764
|
+
`renderSessionUserInterface` from `@seriousgaming/sdk/ui/testing` renders a session UI with every provider it reads, seeded with `createMockTeamState` and `createMockSharedState`: see [Session UIs](./testing.md#session-uis) in the testing guide.
|
|
765
|
+
|
|
766
|
+
## Hooks reference
|
|
767
|
+
|
|
768
|
+
### useSessionUserInterfaceData
|
|
769
|
+
|
|
770
|
+
Read session-wide data and write per-team overrides:
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
import { useSessionUserInterfaceData } from '@seriousgaming/sdk/ui';
|
|
774
|
+
|
|
775
|
+
const { sessionData, settings, setData } = useSessionUserInterfaceData();
|
|
776
|
+
// sessionData: SessionData, the shared store once (shared.context), and a teams map with each team's own context, tick, and timer
|
|
777
|
+
// setData(partial): writes shared capabilities session-wide
|
|
778
|
+
// settings: the settings resources this surface lists in `settings`, keyed by identifier (see Configuring the surface)
|
|
779
|
+
// setData(teamId, partial): merges partial context into the specified team's state
|
|
780
|
+
// Both forms take { deletes } last: setData(teamId, {}, { deletes: ['pinboard.Notes.p1'] })
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
### useSessionUserInterfaceTeams
|
|
784
|
+
|
|
785
|
+
Get the team list:
|
|
786
|
+
|
|
787
|
+
```typescript
|
|
788
|
+
import { useSessionUserInterfaceTeams } from '@seriousgaming/sdk/ui';
|
|
789
|
+
|
|
790
|
+
const teams = useSessionUserInterfaceTeams();
|
|
791
|
+
// TeamInfo[]: an array of { teamId, index, name }, sorted by index
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### useSessionUserInterfaceTeamConfiguration
|
|
795
|
+
|
|
796
|
+
Get the interaction mode and the bounds the host will hold the session UI to:
|
|
797
|
+
|
|
798
|
+
```typescript
|
|
799
|
+
import { useSessionUserInterfaceTeamConfiguration } from '@seriousgaming/sdk/ui';
|
|
800
|
+
|
|
801
|
+
const { mode, min, max, perTeam } = useSessionUserInterfaceTeamConfiguration();
|
|
802
|
+
// mode: 'independent' | 'shared-state'
|
|
803
|
+
// max: the effective maximum team count, already resolved as the lower of the
|
|
804
|
+
// game's declared maximum and the platform's own ceiling
|
|
805
|
+
// min, perTeam: advisory guidance to display, not rules to block on
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
Every bound here is for rendering honest affordances. The host enforces all of them itself, so a surface that disables 'add team' at the maximum is being polite, not authoritative, and must still handle a denial.
|
|
809
|
+
|
|
810
|
+
### useSessionUserInterfaceMeta
|
|
811
|
+
|
|
812
|
+
Read the session itself:
|
|
813
|
+
|
|
814
|
+
```typescript
|
|
815
|
+
import { useSessionUserInterfaceMeta } from '@seriousgaming/sdk/ui';
|
|
816
|
+
|
|
817
|
+
const { sessionId, name, readOnly, waitingRoomOpen, joinPin } = useSessionUserInterfaceMeta();
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
`joinPin` is the code participants enter to join, or `null` when joining needs none. How joining is secured is up to the runtime, so it need not be a PIN and some runtimes never set it: render it as an opaque string, and hide the panel when it is `null`. `readOnly` marks a frozen session, so a surface can disable its own controls and explain why rather than offering affordances that will be denied. `waitingRoomOpen` is the current value of a flag the session UI can also write through `setWaitingRoom`; since the host usually renders its own waiting-room control too, read the result back rather than assuming your write won.
|
|
821
|
+
|
|
822
|
+
### useSessionUserInterfaceTeamLinks
|
|
823
|
+
|
|
824
|
+
Get each team's facilitator-only URLs, so a game draws its own QR codes and join sheets:
|
|
825
|
+
|
|
826
|
+
```typescript
|
|
827
|
+
import { useSessionUserInterfaceTeamLinks } from '@seriousgaming/sdk/ui';
|
|
828
|
+
|
|
829
|
+
const teamLinks = useSessionUserInterfaceTeamLinks();
|
|
830
|
+
// TeamLinks[]: each { teamId, joinUrl, fallbackJoinUrl?, observeUrl }
|
|
831
|
+
```
|
|
832
|
+
|
|
833
|
+
Encode `joinUrl` in a QR code: the host chose that form because a denser code is harder to scan across a room. Show `fallbackJoinUrl` on request rather than beside it, for the networks that block URL shorteners. `observeUrl` opens the team's game UI as an observer, with no participant identity and no presence.
|
|
834
|
+
|
|
835
|
+
### useSessionUserInterfaceHostOverlays
|
|
836
|
+
|
|
837
|
+
Find out which host overlays this host offers, before rendering a control for one:
|
|
838
|
+
|
|
839
|
+
```typescript
|
|
840
|
+
import { useSessionUserInterfaceHostOverlays } from '@seriousgaming/sdk/ui';
|
|
841
|
+
|
|
842
|
+
const overlays = useSessionUserInterfaceHostOverlays();
|
|
843
|
+
// HostOverlay[]: 'facilitator-guide' | 'print-sheets'
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
Empty until the host answers, and empty is also the correct answer for a host that delegates none. It can change mid-session. See [Opening host overlays](#opening-host-overlays).
|
|
847
|
+
|
|
848
|
+
### useSessionUserInterfaceHookFailures
|
|
849
|
+
|
|
850
|
+
Report hook chains that threw, on the surface the facilitator is actually looking at:
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
import { useSessionUserInterfaceHookFailures } from '@seriousgaming/sdk/ui';
|
|
854
|
+
|
|
855
|
+
const { failures, clearFailures } = useSessionUserInterfaceHookFailures();
|
|
856
|
+
// failures: { signal, teamId, message }[], kept until cleared
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
A hook failure is deliberately not a failed mutation: hooks fire after the change they react to is applied, so the mutation that triggered them has already settled. Reporting it as failed would invite retrying an action that worked.
|
|
860
|
+
|
|
861
|
+
### useSessionUserInterfaceLocale
|
|
862
|
+
|
|
863
|
+
Get the current locale:
|
|
864
|
+
|
|
865
|
+
```typescript
|
|
866
|
+
import { useSessionUserInterfaceLocale } from '@seriousgaming/sdk/ui';
|
|
867
|
+
|
|
868
|
+
const { locale } = useSessionUserInterfaceLocale();
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### useSessionUserInterfaceTheme
|
|
872
|
+
|
|
873
|
+
Read the host's colour scheme and palettes (`{ mode, light, dark }`, the same shape as the game UI's theme). See [Using the theme](#using-the-theme) for turning it into an MUI theme:
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
import { useSessionUserInterfaceTheme } from '@seriousgaming/sdk/ui';
|
|
877
|
+
|
|
878
|
+
function MyComponent() {
|
|
879
|
+
const theme = useSessionUserInterfaceTheme();
|
|
880
|
+
const colours = theme.mode === 'light' ? theme.light : theme.dark;
|
|
881
|
+
return <div style={{ color: colours.primary }}>Session Dashboard</div>;
|
|
882
|
+
}
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
### useSessionUserInterfaceParticipants
|
|
886
|
+
|
|
887
|
+
Reactive list of participants across all teams in the session:
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
import { useSessionUserInterfaceParticipants } from '@seriousgaming/sdk/ui';
|
|
891
|
+
|
|
892
|
+
function Roster() {
|
|
893
|
+
const participants = useSessionUserInterfaceParticipants();
|
|
894
|
+
return <div>{participants.length} participants across all teams</div>;
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
Use this to enumerate participants for facilitator workflows like cross-team comparisons or session-wide reporting. See [Working with participants](#working-with-participants) above for the full field reference.
|
|
899
|
+
|
|
900
|
+
### useSessionUserInterfaceCallbacks
|
|
901
|
+
|
|
902
|
+
Session-level facilitator callbacks. This is a standalone interface (`SessionUserInterfaceCallbacks`): it does not extend `UserInterfaceCallbacks`. All game-flow callbacks accept an optional `teamId` parameter; omitting it targets all teams.
|
|
903
|
+
|
|
904
|
+
```typescript
|
|
905
|
+
import { useSessionUserInterfaceCallbacks } from '@seriousgaming/sdk/ui';
|
|
906
|
+
|
|
907
|
+
const { requestTick, pauseTimer, resumeTimer, undoTick, resetGame } =
|
|
908
|
+
useSessionUserInterfaceCallbacks();
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
| Callback | Signature | Description |
|
|
912
|
+
| ------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
913
|
+
| `requestTick` | `(teamId?: string) => Promise<void>` | Advance the game by one tick; omit `teamId` to advance all teams |
|
|
914
|
+
| `pauseTimer` | `(teamId?: string) => void` | Pause automatic ticking; omit `teamId` to pause all teams |
|
|
915
|
+
| `resumeTimer` | `(teamId?: string) => void` | Resume automatic ticking; omit `teamId` to resume all teams |
|
|
916
|
+
| `undoTick` | `(teamId?: string) => Promise<void>` | Un-run the last tick, keeping the tick's decisions; omit `teamId` to undo all teams. Naming one is independent mode only |
|
|
917
|
+
| `resetGame` | `(teamId?: string) => Promise<void>` | Re-initialise the game from scratch; omit `teamId` to reset all teams |
|
|
918
|
+
| `extendTick` | `(byMs, teamId?) => Promise<void>` | Move the currently scheduled tick's deadline by `byMs` milliseconds; negative pulls it in |
|
|
919
|
+
| `openHostOverlay` | `(overlay, target?) => Promise<void>` | Ask the host to open the guide or a print sheet |
|
|
920
|
+
| `uploadBlob` | `(file: File) => Promise<string>` | Upload a file, returns a `blob:<id>` reference |
|
|
921
|
+
| `getAssetUrl` | `(reference: string) => string` | Resolve an `asset:`, `shared:`, or `blob:` reference to a URL |
|
|
922
|
+
| `readAsset` | `(reference: string) => Promise<{ data, mimeType, name }>` | Read bytes for an `asset:`, `shared:`, or `blob:` reference |
|
|
923
|
+
| `endpoints` | `Record<string, (request) => Promise<Response>>` | Call a declared endpoint by identifier |
|
|
924
|
+
| `setWaitingRoom` | `(open: boolean) => Promise<void>` | Open or close the session's waiting room; no `teamId` |
|
|
925
|
+
| `createTeam` | `(name?: string) => Promise<void>` | Add a team; the host assigns the index |
|
|
926
|
+
| `renameTeam` | `(teamId, name) => Promise<void>` | Rename a team |
|
|
927
|
+
| `renameParticipant` | `(participantId, name) => Promise<void>` | Rename a participant; the host resolves their team |
|
|
928
|
+
| `moveParticipant` | `(participantId, teamId) => Promise<void>` | Reassign a participant to another team |
|
|
929
|
+
| `deleteTeam` | `(teamId) => Promise<void>` | Delete an empty team; independent mode only, and never the last team |
|
|
930
|
+
|
|
931
|
+
The promise-returning callbacks resolve and reject as described in [Controlling game flow](#controlling-game-flow). The wrapper supplies every callback, so none needs an optional-chaining guard.
|
|
932
|
+
|
|
933
|
+
To generate text, extract document text, or synthesise speech, declare an [endpoint](../recipes/create-endpoint.md) that wraps `generateText`, `extractText`, or `generateSpeech`, then call it through `callbacks.endpoints['…']`. These callbacks are not exposed to UI iframes. See [Call an endpoint from a UI](../recipes/call-endpoint-from-ui.md).
|
|
934
|
+
|
|
935
|
+
### Message protocol
|
|
936
|
+
|
|
937
|
+
The `SessionUserInterfaceWrapper` holds the fallback until every required message has arrived:
|
|
938
|
+
|
|
939
|
+
| Required message | Description |
|
|
940
|
+
| ---------------------- | ------------------------------------------------ |
|
|
941
|
+
| `setSessionData` | The shared store and all teams' state |
|
|
942
|
+
| `setSessionMeta` | Session identity and lifecycle state |
|
|
943
|
+
| `setSettings` | The settings this session UI declares, resolved |
|
|
944
|
+
| `setTheme` | Colour scheme and palettes |
|
|
945
|
+
| `setLocale` | Current locale string |
|
|
946
|
+
| `setAssetUrls` | Prefixes for resolving asset and blob references |
|
|
947
|
+
| `setTeamConfiguration` | Interaction mode and team bounds |
|
|
948
|
+
|
|
949
|
+
`setTeamConfiguration` gates first paint because its default cannot be rendered honestly: a surface painting before bounds arrive would hold a maximum of zero and disable the controls that read it.
|
|
950
|
+
|
|
951
|
+
Additional messages sent after initialisation:
|
|
952
|
+
|
|
953
|
+
| Additional message | Description |
|
|
954
|
+
| -------------------- | -------------------------------------------------------------------------------------------------------- |
|
|
955
|
+
| `setTeamList` | Updates the list of teams in the session (`TeamInfo[]`, each `{ teamId, index, name }`, sorted by index) |
|
|
956
|
+
| `setTeamLinks` | Updates each team's facilitator-only join and observe URLs |
|
|
957
|
+
| `setParticipantList` | Updates the participant roster across all teams |
|
|
958
|
+
| `hookFailed` | Reports a hook chain that threw |
|
|
959
|
+
|
|
960
|
+
## Examples
|
|
961
|
+
|
|
962
|
+
The session UI follows the same file structure conventions as other UI resource types. The starter game's `session/` is a complete one: per-team status, join and observe links, a shared briefing, and the waiting room and guide toggles (see [A tour of the starter game](./starter-game.md#session)).
|
|
963
|
+
|
|
964
|
+
## See also
|
|
965
|
+
|
|
966
|
+
- [Multi-team games](./multi-team-games.md) - Independent and shared-state interaction modes
|
|
967
|
+
- [Building user interfaces](./ui.md) - Building participant-facing game UIs
|
|
968
|
+
- [Building a setup screen](./setup.md) - The form at session creation
|
|
969
|
+
- [Building a scenario editor](./scenario-editor.md) - Authoring lasting scenarios
|
|
970
|
+
- [Tick controllers](./tick-controller.md) - Automatic tick timing
|
|
971
|
+
- [Testing](./testing.md) - Writing UI tests
|
|
972
|
+
- [CLI reference](../cli-reference.md) - `new`, `update`, and `delete session` commands
|