@idosgames/mcp 0.1.36 → 0.1.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +6 -0
  2. package/dist/cli.js +2204 -27
  3. package/package.json +4 -1
  4. package/registry/host.json +7 -7
  5. package/registry/index.json +310 -156
  6. package/registry/modules/ai.json +7 -7
  7. package/registry/modules/board-game.json +14 -14
  8. package/registry/modules/boosts.json +3 -3
  9. package/registry/modules/character.json +32 -15
  10. package/registry/modules/chat.json +3 -3
  11. package/registry/modules/collections.json +7 -7
  12. package/registry/modules/coop-events.json +7 -7
  13. package/registry/modules/craft.json +3 -3
  14. package/registry/modules/deals.json +7 -7
  15. package/registry/modules/events.json +8 -8
  16. package/registry/modules/friends.json +9 -9
  17. package/registry/modules/hero-run.json +498 -0
  18. package/registry/modules/idle-rpg.json +69 -17
  19. package/registry/modules/inventory.json +3 -3
  20. package/registry/modules/invite.json +7 -7
  21. package/registry/modules/leaderboard.json +10 -10
  22. package/registry/modules/lootboxes.json +7 -7
  23. package/registry/modules/mailbox.json +8 -8
  24. package/registry/modules/marketplace.json +8 -8
  25. package/registry/modules/mini-game.json +8 -8
  26. package/registry/modules/multiplayer.json +7 -7
  27. package/registry/modules/premium.json +7 -7
  28. package/registry/modules/profile.json +7 -7
  29. package/registry/modules/pvp.json +7 -7
  30. package/registry/modules/quests.json +8 -8
  31. package/registry/modules/rewards.json +8 -8
  32. package/registry/modules/season.json +7 -7
  33. package/registry/modules/store.json +7 -7
  34. package/registry/modules/tutorials.json +7 -7
  35. package/registry/modules/video-studio.json +228 -0
  36. package/registry/modules/voxelcraft.json +42 -125
  37. package/registry/modules/workshop.json +3 -3
  38. package/registry/skills/authentication.json +1 -1
  39. package/registry/skills/hero-run.json +6 -0
  40. package/registry/skills/idosgames-module-contract.json +2 -2
  41. package/registry/skills/idosgames-project-structure.json +1 -1
  42. package/registry/skills/idosgames-ui-kit.json +1 -1
  43. package/registry/skills/video-studio.json +6 -0
  44. package/registry/video.json +9800 -0
@@ -29,15 +29,15 @@
29
29
  "name": "iDos Games",
30
30
  "url": "https://idosgames.com"
31
31
  },
32
- "version": "0.1.2"
32
+ "version": "0.1.3"
33
33
  },
34
34
  "dependencies": {
35
- "@idosgames/core": "0.22.0",
36
- "@idosgames/module-sdk": "0.12.0",
37
- "@idosgames/react": "0.5.2",
35
+ "@idosgames/core": "0.23.0",
36
+ "@idosgames/module-sdk": "0.13.0",
37
+ "@idosgames/react": "0.6.0",
38
38
  "react": "19.2.7"
39
39
  },
40
- "contentHash": "86cdde5c496c220eda1de6c970c871421b61aca9aefb605227075895a8adf5cf",
40
+ "contentHash": "03421dee0e738bed7dbe18114b3609e4fe6fcf830f909827907d785c4bfaaa18",
41
41
  "files": [
42
42
  {
43
43
  "path": "i18n.ts",
@@ -57,11 +57,11 @@
57
57
  },
58
58
  {
59
59
  "path": "TutorialOverlay.tsx",
60
- "content": "import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport type { FeatureScreenProps } from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient } from \"@idosgames/react\";\nimport {\n bundleLines,\n Button,\n Center,\n EmptyState,\n errorText,\n Icon,\n outlined,\n panel,\n Spinner,\n useCatalog,\n useCelebrate,\n useStagger,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport {\n activeFlow,\n anchorOf,\n bubblePlacement,\n canTapNext,\n stepCounter,\n stepText,\n type FlowView,\n type Rect,\n} from \"./model\";\nimport { t } from \"./i18n\";\n\n// The tutorial overlay (Unity Runtime/UI/Tutorial): the current step of the running flow as a hint\n// bubble next to the element marked data-tutorial-anchor=\"{AnchorID}\" — any element of any module,\n// like Unity's TutorialAnchorRegistry. It waits while a popup is open, \"Next\" only on steps the\n// player closes, and the reward when the flow ends. The \"Tutorials\" feature replays a flow.\n\n/** Tutorial state shared by the overlay and the replay screen. */\nexport function createTutorialStore() {\n let flows: FlowView[] | null = null;\n const listeners = new Set<() => void>();\n return {\n get: () => flows,\n set(next: FlowView[] | null) {\n flows = next;\n for (const l of listeners) l();\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => {\n listeners.delete(l);\n };\n },\n };\n}\nexport type TutorialStore = ReturnType<typeof createTutorialStore>;\n\nconst BUBBLE = { width: 320, height: 150 };\n\n/** What an applied operation granted: the standard part plus premium bundles. */\nfunction operationLines(op: unknown) {\n const grant = (\n op as\n | {\n Grant?: {\n Standard?: unknown;\n PremiumTiers?: Array<{ Resources?: unknown } | null> | null;\n } | null;\n }\n | null\n | undefined\n )?.Grant;\n return [\n ...bundleLines(grant?.Standard as never),\n ...(grant?.PremiumTiers ?? []).flatMap((t) =>\n bundleLines(t?.Resources as never),\n ),\n ];\n}\n\nfunction modalOpen(): boolean {\n return document.querySelector(\"[data-idos-ui-layer] [role=dialog]\") !== null;\n}\n\nexport function makeTutorialOverlay(store: TutorialStore) {\n return function TutorialOverlay(): ReactNode {\n const client = useIDosGamesClient();\n const catalog = useCatalog();\n const celebrate = useCelebrate();\n const toast = useToast();\n const kit = useUiKit();\n const [flows, setFlows] = useState<FlowView[] | null>(store.get());\n const [rect, setRect] = useState<Rect | null>(null);\n const [blocked, setBlocked] = useState(true);\n const [busy, setBusy] = useState(false);\n const shown = useRef(new Set<string>());\n\n const load = useCallback(async () => {\n const res = await client.tutorial.getUserTutorialState();\n store.set(res.ok ? ((res.data.Flows ?? []) as FlowView[]) : []);\n }, [client]);\n\n useEffect(() => store.subscribe(() => setFlows(store.get())), []);\n useEffect(() => {\n void load();\n // A SystemEvent step closes on a game action; the progress arrives with that action's reply.\n const off = client.on(\"tutorial:systemProgress\", () => void load());\n return () => off();\n }, [client, load]);\n\n const flow = activeFlow(flows);\n const step = flow?.Status === \"InProgress\" ? flow.CurrentStep : null;\n const anchor = anchorOf(step);\n\n // Follow the anchor (it moves with layout, scroll and screen changes) and step aside for popups.\n useEffect(() => {\n if (!flow) return;\n const tick = () => {\n setBlocked(modalOpen());\n const el = anchor\n ? document.querySelector(\n `[data-tutorial-anchor=\"${CSS.escape(anchor)}\"]`,\n )\n : null;\n const r = el?.getBoundingClientRect();\n setRect(\n r && r.width > 0 && r.height > 0\n ? { left: r.left, top: r.top, width: r.width, height: r.height }\n : null,\n );\n };\n tick();\n const id = setInterval(tick, 250);\n return () => clearInterval(id);\n }, [flow?.FlowID, flow?.CurrentStepID, anchor]);\n\n // The funnel measures time on step from this report — once per step shown.\n useEffect(() => {\n if (!flow || !flow.CurrentStepID || blocked) return;\n const key = `${flow.FlowID}:${flow.CurrentStepID}`;\n if (shown.current.has(key)) return;\n shown.current.add(key);\n kit.play(\"notify\");\n void client.tutorial.reportStepShown(flow.FlowID, flow.CurrentStepID);\n }, [flow?.FlowID, flow?.CurrentStepID, blocked]);\n\n if (!flow || blocked) return null;\n\n const act = async (\n fn: () => Promise<{ ok: boolean; error?: string; data?: unknown }>,\n ) => {\n setBusy(true);\n const res = await fn();\n setBusy(false);\n if (!res.ok) return toast(errorText(res.error ?? \"\"), \"error\");\n await load();\n };\n\n const text = step\n ? stepText(step, (k) => catalog.localize(k))\n : { title: t(\"done\"), body: \"\" };\n const place = bubblePlacement(\n step ? rect : null,\n { width: window.innerWidth, height: window.innerHeight },\n BUBBLE,\n );\n const pad = 6;\n\n return (\n <div\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 45,\n pointerEvents: \"none\",\n }}\n data-idos-tutorial\n >\n {step && rect ? (\n <div\n className=\"idos-pulse\"\n style={{\n position: \"fixed\",\n left: rect.left - pad,\n top: rect.top - pad,\n width: rect.width + pad * 2,\n height: rect.height + pad * 2,\n borderRadius: 14,\n border: `3px solid ${v.gold}`,\n boxShadow: `0 0 0 9999px ${v.backdrop}`,\n transition: \"all .25s ease\",\n }}\n />\n ) : (\n <div\n style={{\n position: \"fixed\",\n inset: 0,\n background: v.backdrop,\n }}\n />\n )}\n <div\n style={{\n ...panel,\n position: \"fixed\",\n left: place.left,\n top: place.top,\n width: Math.min(BUBBLE.width, window.innerWidth - 24),\n padding: 14,\n display: \"grid\",\n gap: 8,\n pointerEvents: \"auto\",\n animation: `idos-pop-in ${v.slow} ease both`,\n }}\n >\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n <Icon glyph={step ? \"bulb\" : \"trophy\"} size={26} />\n <div style={{ flex: 1, ...outlined, fontSize: 16 }}>\n {text.title}\n </div>\n {step ? (\n <span style={{ ...outlined, fontSize: 12, color: v.textDim }}>\n {stepCounter(flow)}\n </span>\n ) : null}\n </div>\n {text.body ? (\n <div style={{ fontSize: 14, fontWeight: 600 }}>{text.body}</div>\n ) : null}\n <div\n style={{\n display: \"flex\",\n gap: 8,\n justifyContent: \"flex-end\",\n alignItems: \"center\",\n }}\n >\n {step && flow.CanSkip ? (\n <Button\n size=\"sm\"\n tone=\"grey\"\n disabled={busy}\n onClick={() =>\n void act(() => client.tutorial.skipFlow(flow.FlowID))\n }\n >\n {t(\"skip\")}\n </Button>\n ) : null}\n {step ? (\n canTapNext(step) ? (\n <Button\n size=\"sm\"\n tone=\"green\"\n attract\n busy={busy}\n onClick={() =>\n void act(() =>\n client.tutorial.completeStep(\n flow.FlowID,\n flow.CurrentStepID!,\n ),\n )\n }\n data-tutorial-anchor=\"tutorial:next\"\n >\n {t(\"next\")}\n </Button>\n ) : (\n <span style={{ ...outlined, fontSize: 12, color: v.textDim }}>\n {t(\"waiting\")}\n </span>\n )\n ) : (\n <Button\n size=\"sm\"\n tone=\"gold\"\n attract\n busy={busy}\n onClick={() =>\n void act(async () => {\n const res = await client.tutorial.claimFlowReward(\n flow.FlowID,\n );\n // The payout sits in `Granted` (an operation), not where the generic reader looks.\n if (res.ok) celebrate(operationLines(res.data.Granted));\n return res;\n })\n }\n >\n {t(\"claim\")}\n </Button>\n )}\n </div>\n </div>\n </div>\n );\n };\n}\n\ntype FlowDef = {\n Identity?: { DisplayName?: string | null; SortOrder?: number | null } | null;\n Policy?: { RestartPolicy?: string | null } | null;\n};\n\n/** \"Tutorials\": every flow of the title with its status; replay where the title allows it. */\nexport function makeTutorialsScreen(store: TutorialStore) {\n return function TutorialsScreen(_: FeatureScreenProps): ReactNode {\n const client = useIDosGamesClient();\n const catalog = useCatalog();\n const toast = useToast();\n const stagger = useStagger();\n const [defs, setDefs] = useState<Record<string, FlowDef | null> | null>(\n null,\n );\n const [flows, setFlows] = useState<FlowView[]>([]);\n const [busy, setBusy] = useState<string | null>(null);\n const load = useCallback(async () => {\n // Listing must not start anything — autoStart off.\n const [d, s] = await Promise.all([\n client.tutorial.getTutorialDefinitions(),\n client.tutorial.getUserTutorialState(false),\n ]);\n setDefs(\n d.ok ? ((d.data.Flows ?? {}) as Record<string, FlowDef | null>) : {},\n );\n setFlows(s.ok ? ((s.data.Flows ?? []) as FlowView[]) : []);\n }, [client]);\n useEffect(() => {\n void load();\n }, [load]);\n if (!defs)\n return (\n <Center>\n <Spinner />\n </Center>\n );\n const ids = Object.keys(defs).sort(\n (a, b) =>\n Number(defs[a]?.Identity?.SortOrder ?? 0) -\n Number(defs[b]?.Identity?.SortOrder ?? 0),\n );\n if (ids.length === 0)\n return <EmptyState glyph=\"bulb\" text={t(\"noTutorials\")} />;\n const replay = async (id: string) => {\n setBusy(id);\n const reset = await client.tutorial.resetFlow(id);\n const started = reset.ok ? await client.tutorial.startFlow(id) : reset;\n setBusy(null);\n if (!started.ok) return toast(errorText(started.error), \"error\");\n const s = await client.tutorial.getUserTutorialState(false);\n if (s.ok) store.set((s.data.Flows ?? []) as FlowView[]);\n await load();\n };\n return (\n <div style={{ display: \"grid\", gap: 8 }}>\n {ids.map((id, i) => {\n const view = flows.find((f) => f.FlowID === id);\n const status = view?.Status ?? \"NotStarted\";\n const label =\n status === \"Completed\"\n ? t(\"completed\")\n : status === \"InProgress\"\n ? t(\"inProgress\")\n : status === \"Skipped\"\n ? t(\"skipped\")\n : t(\"notStarted\");\n const canReplay =\n defs[id]?.Policy?.RestartPolicy === \"OnRequest\" &&\n (status === \"Completed\" || status === \"Skipped\");\n return (\n <div\n key={id}\n style={{\n ...panel,\n padding: 10,\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n ...stagger(i),\n }}\n >\n <Icon glyph=\"bulb\" size={28} />\n <div style={{ flex: 1, minWidth: 0 }}>\n <div style={{ ...outlined, fontSize: 15 }}>\n {catalog.localize(defs[id]?.Identity?.DisplayName ?? id)}\n </div>\n <div style={{ ...outlined, fontSize: 12, color: v.textDim }}>\n {label}\n </div>\n </div>\n {canReplay ? (\n <Button\n size=\"sm\"\n tone=\"blue\"\n busy={busy === id}\n onClick={() => void replay(id)}\n >\n {t(\"replay\")}\n </Button>\n ) : null}\n </div>\n );\n })}\n </div>\n );\n };\n}\n"
60
+ "content": "import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport type { FeatureScreenProps } from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient } from \"@idosgames/react\";\nimport {\n Columns,\n bundleLines,\n Button,\n Center,\n EmptyState,\n errorText,\n Icon,\n inked,\n outlined,\n panel,\n Spinner,\n useCatalog,\n useCelebrate,\n useStagger,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport {\n activeFlow,\n anchorOf,\n bubblePlacement,\n canTapNext,\n stepCounter,\n stepText,\n type FlowView,\n type Rect,\n} from \"./model\";\nimport { t } from \"./i18n\";\n\n// The tutorial overlay (Unity Runtime/UI/Tutorial): the current step of the running flow as a hint\n// bubble next to the element marked data-tutorial-anchor=\"{AnchorID}\" — any element of any module,\n// like Unity's TutorialAnchorRegistry. It waits while a popup is open, \"Next\" only on steps the\n// player closes, and the reward when the flow ends. The \"Tutorials\" feature replays a flow.\n\n/** Tutorial state shared by the overlay and the replay screen. */\nexport function createTutorialStore() {\n let flows: FlowView[] | null = null;\n const listeners = new Set<() => void>();\n return {\n get: () => flows,\n set(next: FlowView[] | null) {\n flows = next;\n for (const l of listeners) l();\n },\n subscribe(l: () => void) {\n listeners.add(l);\n return () => {\n listeners.delete(l);\n };\n },\n };\n}\nexport type TutorialStore = ReturnType<typeof createTutorialStore>;\n\nconst BUBBLE = { width: 320, height: 150 };\n\n/** What an applied operation granted: the standard part plus premium bundles. */\nfunction operationLines(op: unknown) {\n const grant = (\n op as\n | {\n Grant?: {\n Standard?: unknown;\n PremiumTiers?: Array<{ Resources?: unknown } | null> | null;\n } | null;\n }\n | null\n | undefined\n )?.Grant;\n return [\n ...bundleLines(grant?.Standard as never),\n ...(grant?.PremiumTiers ?? []).flatMap((t) =>\n bundleLines(t?.Resources as never),\n ),\n ];\n}\n\nfunction modalOpen(): boolean {\n return document.querySelector(\"[data-idos-ui-layer] [role=dialog]\") !== null;\n}\n\nexport function makeTutorialOverlay(store: TutorialStore) {\n return function TutorialOverlay(): ReactNode {\n const client = useIDosGamesClient();\n const catalog = useCatalog();\n const celebrate = useCelebrate();\n const toast = useToast();\n const kit = useUiKit();\n const [flows, setFlows] = useState<FlowView[] | null>(store.get());\n const [rect, setRect] = useState<Rect | null>(null);\n const [blocked, setBlocked] = useState(true);\n const [busy, setBusy] = useState(false);\n const shown = useRef(new Set<string>());\n\n const load = useCallback(async () => {\n const res = await client.tutorial.getUserTutorialState();\n store.set(res.ok ? ((res.data.Flows ?? []) as FlowView[]) : []);\n }, [client]);\n\n useEffect(() => store.subscribe(() => setFlows(store.get())), []);\n useEffect(() => {\n void load();\n // A SystemEvent step closes on a game action; the progress arrives with that action's reply.\n const off = client.on(\"tutorial:systemProgress\", () => void load());\n return () => off();\n }, [client, load]);\n\n const flow = activeFlow(flows);\n const step = flow?.Status === \"InProgress\" ? flow.CurrentStep : null;\n const anchor = anchorOf(step);\n\n // Follow the anchor (it moves with layout, scroll and screen changes) and step aside for popups.\n useEffect(() => {\n if (!flow) return;\n const tick = () => {\n setBlocked(modalOpen());\n const el = anchor\n ? document.querySelector(\n `[data-tutorial-anchor=\"${CSS.escape(anchor)}\"]`,\n )\n : null;\n const r = el?.getBoundingClientRect();\n setRect(\n r && r.width > 0 && r.height > 0\n ? { left: r.left, top: r.top, width: r.width, height: r.height }\n : null,\n );\n };\n tick();\n const id = setInterval(tick, 250);\n return () => clearInterval(id);\n }, [flow?.FlowID, flow?.CurrentStepID, anchor]);\n\n // The funnel measures time on step from this report — once per step shown.\n useEffect(() => {\n if (!flow || !flow.CurrentStepID || blocked) return;\n const key = `${flow.FlowID}:${flow.CurrentStepID}`;\n if (shown.current.has(key)) return;\n shown.current.add(key);\n kit.play(\"notify\");\n void client.tutorial.reportStepShown(flow.FlowID, flow.CurrentStepID);\n }, [flow?.FlowID, flow?.CurrentStepID, blocked]);\n\n if (!flow || blocked) return null;\n\n const act = async (\n fn: () => Promise<{ ok: boolean; error?: string; data?: unknown }>,\n ) => {\n setBusy(true);\n const res = await fn();\n setBusy(false);\n if (!res.ok) return toast(errorText(res.error ?? \"\"), \"error\");\n await load();\n };\n\n const text = step\n ? stepText(step, (k) => catalog.localize(k))\n : { title: t(\"done\"), body: \"\" };\n const place = bubblePlacement(\n step ? rect : null,\n { width: window.innerWidth, height: window.innerHeight },\n BUBBLE,\n );\n const pad = 6;\n\n return (\n <div\n style={{\n position: \"fixed\",\n inset: 0,\n zIndex: 45,\n pointerEvents: \"none\",\n }}\n data-idos-tutorial\n >\n {step && rect ? (\n <div\n className=\"idos-pulse\"\n style={{\n position: \"fixed\",\n left: rect.left - pad,\n top: rect.top - pad,\n width: rect.width + pad * 2,\n height: rect.height + pad * 2,\n borderRadius: 14,\n border: `3px solid ${v.gold}`,\n boxShadow: `0 0 0 9999px ${v.backdrop}`,\n transition: \"all .25s ease\",\n }}\n />\n ) : (\n <div\n style={{\n position: \"fixed\",\n inset: 0,\n background: v.backdrop,\n }}\n />\n )}\n {/* The sheet's \"Welcome!\" bubble: white in the ink frame, dark letters, a light-blue lip. */}\n <div\n style={{\n position: \"fixed\",\n left: place.left,\n top: place.top,\n width: Math.min(BUBBLE.width, window.innerWidth - 24),\n padding: \"14px 16px 12px\",\n display: \"grid\",\n gap: 8,\n background: \"#ffffff\",\n border: `${v.outline} solid ${v.ink}`,\n borderRadius: 14,\n boxShadow: `inset 0 -5px 0 color-mix(in srgb, ${v.blue} 35%, #fff), 0 4px 0 ${v.shadow}`,\n color: \"#17193a\",\n pointerEvents: \"auto\",\n animation: `idos-pop-in ${v.slow} ease both`,\n boxSizing: \"border-box\",\n }}\n >\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 8 }}>\n <Icon glyph={step ? \"bulb\" : \"trophy\"} size={26} />\n <div style={{ flex: 1, fontWeight: 900, fontSize: 17 }}>\n {text.title}\n </div>\n {step ? (\n <span\n style={{ fontWeight: 900, fontSize: 12, color: v.blueDeep }}\n >\n {stepCounter(flow)}\n </span>\n ) : null}\n </div>\n {text.body ? (\n <div style={{ fontSize: 15, fontWeight: 800, lineHeight: 1.35 }}>\n {text.body}\n </div>\n ) : null}\n <div\n style={{\n display: \"flex\",\n gap: 8,\n justifyContent: \"flex-end\",\n alignItems: \"center\",\n }}\n >\n {step && flow.CanSkip ? (\n // The sheet's \"SKIP ≫\": outlined white letters, no plate.\n <button\n type=\"button\"\n className=\"idos-press\"\n disabled={busy}\n onClick={() =>\n void act(() => client.tutorial.skipFlow(flow.FlowID))\n }\n style={{\n ...inked,\n border: \"none\",\n background: \"none\",\n cursor: \"pointer\",\n fontSize: 16,\n textTransform: \"uppercase\",\n marginRight: \"auto\",\n padding: \"4px 2px\",\n color: v.blue,\n }}\n >\n {t(\"skip\")} ≫\n </button>\n ) : null}\n {step ? (\n canTapNext(step) ? (\n <Button\n size=\"sm\"\n tone=\"green\"\n attract\n busy={busy}\n onClick={() =>\n void act(() =>\n client.tutorial.completeStep(\n flow.FlowID,\n flow.CurrentStepID!,\n ),\n )\n }\n data-tutorial-anchor=\"tutorial:next\"\n >\n {t(\"next\")}\n </Button>\n ) : (\n <span\n style={{ fontWeight: 800, fontSize: 12, color: v.blueDeep }}\n >\n {t(\"waiting\")}\n </span>\n )\n ) : (\n <Button\n size=\"sm\"\n tone=\"gold\"\n attract\n busy={busy}\n onClick={() =>\n void act(async () => {\n const res = await client.tutorial.claimFlowReward(\n flow.FlowID,\n );\n // The payout sits in `Granted` (an operation), not where the generic reader looks.\n if (res.ok) celebrate(operationLines(res.data.Granted));\n return res;\n })\n }\n >\n {t(\"claim\")}\n </Button>\n )}\n </div>\n </div>\n </div>\n );\n };\n}\n\ntype FlowDef = {\n Identity?: { DisplayName?: string | null; SortOrder?: number | null } | null;\n Policy?: { RestartPolicy?: string | null } | null;\n};\n\n/** \"Tutorials\": every flow of the title with its status; replay where the title allows it. */\nexport function makeTutorialsScreen(store: TutorialStore) {\n return function TutorialsScreen(_: FeatureScreenProps): ReactNode {\n const client = useIDosGamesClient();\n const catalog = useCatalog();\n const toast = useToast();\n const stagger = useStagger();\n const [defs, setDefs] = useState<Record<string, FlowDef | null> | null>(\n null,\n );\n const [flows, setFlows] = useState<FlowView[]>([]);\n const [busy, setBusy] = useState<string | null>(null);\n const load = useCallback(async () => {\n // Listing must not start anything — autoStart off.\n const [d, s] = await Promise.all([\n client.tutorial.getTutorialDefinitions(),\n client.tutorial.getUserTutorialState(false),\n ]);\n setDefs(\n d.ok ? ((d.data.Flows ?? {}) as Record<string, FlowDef | null>) : {},\n );\n setFlows(s.ok ? ((s.data.Flows ?? []) as FlowView[]) : []);\n }, [client]);\n useEffect(() => {\n void load();\n }, [load]);\n if (!defs)\n return (\n <Center>\n <Spinner />\n </Center>\n );\n const ids = Object.keys(defs).sort(\n (a, b) =>\n Number(defs[a]?.Identity?.SortOrder ?? 0) -\n Number(defs[b]?.Identity?.SortOrder ?? 0),\n );\n if (ids.length === 0)\n return <EmptyState glyph=\"bulb\" text={t(\"noTutorials\")} />;\n const replay = async (id: string) => {\n setBusy(id);\n const reset = await client.tutorial.resetFlow(id);\n const started = reset.ok ? await client.tutorial.startFlow(id) : reset;\n setBusy(null);\n if (!started.ok) return toast(errorText(started.error), \"error\");\n const s = await client.tutorial.getUserTutorialState(false);\n if (s.ok) store.set((s.data.Flows ?? []) as FlowView[]);\n await load();\n };\n return (\n <Columns gap={8} min={340}>\n {ids.map((id, i) => {\n const view = flows.find((f) => f.FlowID === id);\n const status = view?.Status ?? \"NotStarted\";\n const label =\n status === \"Completed\"\n ? t(\"completed\")\n : status === \"InProgress\"\n ? t(\"inProgress\")\n : status === \"Skipped\"\n ? t(\"skipped\")\n : t(\"notStarted\");\n const canReplay =\n defs[id]?.Policy?.RestartPolicy === \"OnRequest\" &&\n (status === \"Completed\" || status === \"Skipped\");\n return (\n <div\n key={id}\n style={{\n ...panel,\n padding: 10,\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n ...stagger(i),\n }}\n >\n <Icon glyph=\"bulb\" size={28} />\n <div style={{ flex: 1, minWidth: 0 }}>\n <div style={{ ...outlined, fontSize: 15 }}>\n {catalog.localize(defs[id]?.Identity?.DisplayName ?? id)}\n </div>\n <div style={{ ...outlined, fontSize: 12, color: v.textDim }}>\n {label}\n </div>\n </div>\n {canReplay ? (\n <Button\n size=\"sm\"\n tone=\"blue\"\n busy={busy === id}\n onClick={() => void replay(id)}\n >\n {t(\"replay\")}\n </Button>\n ) : null}\n </div>\n );\n })}\n </Columns>\n );\n };\n}\n"
61
61
  },
62
62
  {
63
63
  "path": "module.meta.json",
64
- "content": "{\n \"id\": \"tutorials\",\n \"type\": \"feature\",\n \"summary\": \"Onboarding for any game: hint bubbles next to marked elements, step by step, skip, a completion reward and a replay list.\",\n \"description\": \"A ready onboarding overlay (Unity Runtime/UI/Tutorial, TutorialAnchorRegistry) for ANY game, built on the @idosgames/react/ui kit and client.tutorial. It shows the current step of the running flow as a hint bubble next to the element marked data-tutorial-anchor=\\\"{AnchorID}\\\" — any element of any module (lobby tabs are tab:{featureID}, the profile button lobby:profile, a game's Play button play:{modeID}, the mini-game's round play:start) — with the rest of the screen dimmed. It follows the element as the layout changes, steps aside while a popup is open, offers 'Next' only on steps the player closes (a SystemEvent step waits for the real game action), 'Skip' when the flow allows it, and the reward when the flow ends; every shown step is reported for the funnel. Flows start by themselves when the title says so. The 'Tutorials' feature lists the flows and replays the ones the title lets the player restart. Ships title-seed.json: a five-step welcome tour of the lobby with a gem reward.\",\n \"provides\": [\n \"tutorial overlay with a highlighted element\",\n \"hint bubble with next and skip\",\n \"any element as an anchor via data-tutorial-anchor\",\n \"tutorial completion reward\",\n \"tutorials list with replay\",\n \"demo welcome tour\"\n ],\n \"tags\": [\"tutorial\", \"onboarding\", \"ftue\", \"coach-marks\", \"hints\", \"ui\"],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.2\"\n}\n"
64
+ "content": "{\n \"id\": \"tutorials\",\n \"type\": \"feature\",\n \"summary\": \"Onboarding for any game: hint bubbles next to marked elements, step by step, skip, a completion reward and a replay list.\",\n \"description\": \"A ready onboarding overlay (Unity Runtime/UI/Tutorial, TutorialAnchorRegistry) for ANY game, built on the @idosgames/react/ui kit and client.tutorial. It shows the current step of the running flow as a hint bubble next to the element marked data-tutorial-anchor=\\\"{AnchorID}\\\" — any element of any module (lobby tabs are tab:{featureID}, the profile button lobby:profile, a game's Play button play:{modeID}, the mini-game's round play:start) — with the rest of the screen dimmed. It follows the element as the layout changes, steps aside while a popup is open, offers 'Next' only on steps the player closes (a SystemEvent step waits for the real game action), 'Skip' when the flow allows it, and the reward when the flow ends; every shown step is reported for the funnel. Flows start by themselves when the title says so. The 'Tutorials' feature lists the flows and replays the ones the title lets the player restart. Ships title-seed.json: a five-step welcome tour of the lobby with a gem reward.\",\n \"provides\": [\n \"tutorial overlay with a highlighted element\",\n \"hint bubble with next and skip\",\n \"any element as an anchor via data-tutorial-anchor\",\n \"tutorial completion reward\",\n \"tutorials list with replay\",\n \"demo welcome tour\"\n ],\n \"tags\": [\"tutorial\", \"onboarding\", \"ftue\", \"coach-marks\", \"hints\", \"ui\"],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.3\"\n}\n"
65
65
  }
66
66
  ]
67
67
  }