@idosgames/mcp 0.1.18 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -2
- package/package.json +1 -1
- package/registry/host.json +69 -25
- package/registry/index.json +895 -620
- package/registry/modules/ai.json +65 -0
- package/registry/modules/board-game.json +8 -8
- package/registry/modules/boosts.json +66 -0
- package/registry/modules/{character-ui.json → character.json} +8 -12
- package/registry/modules/chat.json +70 -0
- package/registry/modules/collections.json +69 -0
- package/registry/modules/coop-events.json +67 -0
- package/registry/modules/{craft-ui.json → craft.json} +8 -12
- package/registry/modules/{deal-offer-ui.json → deals.json} +7 -11
- package/registry/modules/events.json +72 -0
- package/registry/modules/{friends-ui.json → friends.json} +11 -14
- package/registry/modules/idle-rpg.json +7 -7
- package/registry/modules/inventory.json +67 -0
- package/registry/modules/invite.json +70 -0
- package/registry/modules/{leaderboard-ui.json → leaderboard.json} +8 -12
- package/registry/modules/lootboxes.json +67 -0
- package/registry/modules/mailbox.json +71 -0
- package/registry/modules/marketplace.json +71 -0
- package/registry/modules/mini-game.json +69 -0
- package/registry/modules/multiplayer.json +74 -0
- package/registry/modules/premium.json +67 -0
- package/registry/modules/profile.json +70 -0
- package/registry/modules/{pvp-ui.json → pvp.json} +9 -13
- package/registry/modules/quests.json +69 -0
- package/registry/modules/rewards.json +66 -0
- package/registry/modules/season.json +71 -0
- package/registry/modules/{store-ui.json → store.json} +9 -13
- package/registry/modules/{tutorial-ui.json → tutorials.json} +8 -12
- package/registry/modules/voxelcraft.json +15 -455
- package/registry/modules/workshop.json +10 -6
- package/registry/skills/blockchain-system.json +1 -1
- package/registry/skills/dev-test-loop.json +1 -1
- package/registry/skills/idosgames-compose-modules.json +2 -2
- package/registry/skills/idosgames-getting-started.json +2 -2
- package/registry/skills/idosgames-module-contract.json +2 -2
- package/registry/skills/idosgames-project-structure.json +1 -1
- package/registry/skills/idosgames-ui-kit.json +2 -2
- package/registry/skills/mailbox-system.json +6 -0
- package/registry/skills/multiplayer-realtime.json +2 -2
- package/registry/skills/push-notifications.json +1 -1
- package/registry/skills/workshop-system.json +1 -1
- package/registry/modules/chat-ui.json +0 -74
- package/registry/modules/events-ui.json +0 -80
- package/registry/modules/game-hud.json +0 -104
- package/registry/modules/inventory-ui.json +0 -73
- package/registry/modules/marketplace-ui.json +0 -75
- package/registry/modules/profile-ui.json +0 -74
- package/registry/modules/referral-ui.json +0 -73
- package/registry/modules/rewards-ui.json +0 -75
- package/registry/modules/season-ui.json +0 -79
- package/registry/modules/showcase.json +0 -92
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "store
|
|
2
|
+
"id": "store",
|
|
3
3
|
"meta": {
|
|
4
4
|
"name": "Shop",
|
|
5
5
|
"type": "app",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"catalog": {
|
|
9
9
|
"type": "feature",
|
|
10
10
|
"summary": "Shop screen for any game: the title's storefront with stores as tabs, offer cards, purchase confirmation and the reward animation.",
|
|
11
|
-
"description": "A ready shop for ANY game, built on the @idosgames/react/ui kit. It draws the storefront the server resolves for the player (client.store.getStorefront — rotation, limits, timers and badges are the server's), several stores as tabs, the first section of a store as a carousel and the rest as grids, a free daily gift that shines, sold-out and not-yet-available states with countdowns, a confirmation popup (a bottom sheet on phones) and the 'You got' animation where rewards fly to the balance counters. Registers itself as the 'store' feature of the game (ctx.features): the
|
|
11
|
+
"description": "A ready shop for ANY game, built on the @idosgames/react/ui kit. It draws the storefront the server resolves for the player (client.store.getStorefront — rotation, limits, timers and badges are the server's), several stores as tabs, the first section of a store as a carousel and the rest as grids, a free daily gift that shines, sold-out and not-yet-available states with countdowns, a confirmation popup (a bottom sheet on phones) and the 'You got' animation where rewards fly to the balance counters. Registers itself as the 'store' feature of the game (ctx.features): the lobby of the base shows it as a tab, and any module can open it with ctx.features.open('store'). Its badge counts free offers waiting. Ships title-seed.json: a demo store with a daily gift, items and currency packs.",
|
|
12
12
|
"provides": [
|
|
13
13
|
"shop screen from the title storefront",
|
|
14
14
|
"several stores as tabs",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"version": "0.1.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.19.
|
|
40
|
-
"@idosgames/module-sdk": "0.
|
|
39
|
+
"@idosgames/core": "0.19.1",
|
|
40
|
+
"@idosgames/module-sdk": "0.7.0",
|
|
41
41
|
"@idosgames/react": "0.3.0",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
|
-
"contentHash": "
|
|
44
|
+
"contentHash": "dc935a7e6f6a19abc3a8f03b41d721727f70221ff1c3e3a4debb35dc351a7bcb",
|
|
45
45
|
"files": [
|
|
46
46
|
{
|
|
47
47
|
"path": "i18n.ts",
|
|
@@ -49,19 +49,15 @@
|
|
|
49
49
|
},
|
|
50
50
|
{
|
|
51
51
|
"path": "index.ts",
|
|
52
|
-
"content": "// @idosgames/mod-store
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"path": "model.test.ts",
|
|
56
|
-
"content": "import { describe, expect, it } from \"vitest\";\nimport seed from \"../title-seed.json\";\nimport { grantLines } from \"@idosgames/react/ui\";\nimport {\n earliestRefresh,\n freeOffersWaiting,\n isFree,\n slotsOf,\n sortedSections,\n sortedStores,\n} from \"./model\";\n\n// The storefront the server resolves is not the raw config; the helpers are checked on small\n// resolved shapes, and the seed on \"every offer draws\".\n\nconst offer = (\n id: string,\n free: boolean,\n extra: Record<string, unknown> = {},\n) => ({\n OfferID: id,\n PriceOptions: [\n free\n ? { IsFree: true }\n : {\n Cost: {\n Standard: {\n Entries: [\n { Type: \"VirtualCurrency\", CurrencyID: \"CO\", Amount: 50 },\n ],\n },\n },\n },\n ],\n ...extra,\n});\n\ndescribe(\"shop\", () => {\n it(\"every seeded offer has a reward the shop can draw\", () => {\n const offers = Object.values(\n (\n seed as unknown as {\n Store: {\n StoreOffers: Record<string, { Reward: { Grant: unknown } }>;\n };\n }\n ).Store.StoreOffers,\n );\n expect(offers.length).toBeGreaterThan(0);\n for (const o of offers)\n expect(grantLines(o.Reward.Grant as never).length).toBeGreaterThan(0);\n });\n\n it(\"orders stores, sections and slots by SortOrder\", () => {\n const front = {\n Stores: [\n { StoreID: \"b\", SortOrder: 2 },\n {\n StoreID: \"a\",\n SortOrder: 1,\n Sections: [\n { SectionID: \"y\", SortOrder: 2, Slots: [] },\n {\n SectionID: \"x\",\n SortOrder: 1,\n Slots: [\n { SlotID: \"2\", SortOrder: 2 },\n { SlotID: \"1\", SortOrder: 1 },\n ],\n },\n ],\n },\n ],\n } as never;\n expect(sortedStores(front).map((s) => s.StoreID)).toEqual([\"a\", \"b\"]);\n const sections = sortedSections(sortedStores(front)[0]);\n expect(sections.map((s) => s.SectionID)).toEqual([\"x\", \"y\"]);\n expect(slotsOf(sections[0]!).map((s) => s.SlotID)).toEqual([\"1\", \"2\"]);\n });\n\n it(\"free = explicitly free or no price; the badge counts free offers still available\", () => {\n expect(isFree(offer(\"gift\", true) as never)).toBe(true);\n expect(isFree({ OfferID: \"x\", PriceOptions: [] } as never)).toBe(true);\n expect(isFree(offer(\"potion\", false) as never)).toBe(false);\n const front = {\n Stores: [\n {\n StoreID: \"main\",\n SortOrder: 0,\n Sections: [\n {\n SectionID: \"d\",\n SortOrder: 0,\n Slots: [\n { SlotID: \"1\", SortOrder: 0, Offer: offer(\"gift\", true) },\n {\n SlotID: \"2\",\n SortOrder: 1,\n Offer: offer(\"gift2\", true, { State: { SoldOut: true } }),\n },\n { SlotID: \"3\", SortOrder: 2, Offer: offer(\"potion\", false) },\n { SlotID: \"4\", SortOrder: 3 },\n ],\n },\n ],\n },\n ],\n } as never;\n expect(freeOffersWaiting(front)).toBe(1);\n expect(freeOffersWaiting(null)).toBe(0);\n });\n\n it(\"a section's countdown is its earliest card refresh\", () => {\n expect(\n earliestRefresh({\n SectionID: \"s\",\n SortOrder: 0,\n Slots: [\n { SlotID: \"a\", SortOrder: 0, RefreshesAtUtc: \"2026-09-28T00:00:00Z\" },\n { SlotID: \"b\", SortOrder: 1, RefreshesAtUtc: \"2026-09-27T20:00:00Z\" },\n ],\n } as never),\n ).toBe(\"2026-09-27T20:00:00Z\");\n });\n});\n"
|
|
52
|
+
"content": "// @idosgames/mod-store — Shop screen for any game: the title's storefront with several stores as tabs, a carousel and grids of offers, purchase confirmation and the reward animation.\n\nexport { storeModule } from \"./module\";\n"
|
|
57
53
|
},
|
|
58
54
|
{
|
|
59
55
|
"path": "model.ts",
|
|
60
|
-
"content": "// Pure helpers of the shop screen — covered by model.test.ts.\n\nimport type {\n GetStorefrontResponse,\n StorefrontOfferView,\n StorefrontSectionView,\n StorefrontSlotView,\n} from \"@idosgames/core\";\nimport { costLines } from \"@idosgames/react/ui\";\n\nexport function sortedSections(\n store: { Sections?: StorefrontSectionView[] | null } | null | undefined,\n): StorefrontSectionView[] {\n return (store?.Sections ?? [])\n .slice()\n .sort((a, b) => a.SortOrder - b.SortOrder);\n}\n\nexport function slotsOf(section: StorefrontSectionView): StorefrontSlotView[] {\n return (section.Slots ?? [])\n .slice()\n .sort((a, b) => a.SortOrder - b.SortOrder);\n}\n\n/** When the first card of a section refreshes — the section's countdown. */\nexport function earliestRefresh(section: StorefrontSectionView): string | null {\n const times = slotsOf(section)\n .map((s) => s.RefreshesAtUtc)\n .filter((x): x is string => typeof x === \"string\")\n .sort();\n return times[0] ?? null;\n}\n\nexport function isFree(offer: StorefrontOfferView): boolean {\n const option = offer.PriceOptions?.[0];\n return option?.IsFree === true || costLines(option?.Cost).length === 0;\n}\n\n/** Offers the player can take right now (not sold out, not waiting for their window). */\nexport function isAvailable(offer: StorefrontOfferView): boolean {\n return offer.State?.SoldOut !== true && !offer.State?.AvailableAtUtc;\n}\n\n/** The badge of the shop: free offers waiting to be taken (the daily gift). */\nexport function freeOffersWaiting(\n front: GetStorefrontResponse | null | undefined,\n): number {\n let n = 0;\n for (const store of front?.Stores ?? [])\n for (const section of store.Sections ?? [])\n for (const slot of section.Slots ?? [])\n if (slot.Offer && isFree(slot.Offer) && isAvailable(slot.Offer)) n++;\n return n;\n}\n\n/** Stores by their order; the first is the default tab. */\nexport function sortedStores(\n front: GetStorefrontResponse | null | undefined,\n): NonNullable<GetStorefrontResponse[\"Stores\"]> {\n return (front?.Stores ?? [])\n .slice()\n .sort((a, b) => a.SortOrder - b.SortOrder);\n}\n"
|
|
56
|
+
"content": "// Pure helpers of the shop screen — covered by model.test.ts.\n\nimport type {\n GetStorefrontResponse,\n StorefrontOfferView,\n StorefrontSectionView,\n StorefrontSlotView,\n} from \"@idosgames/core\";\nimport { costLines } from \"@idosgames/react/ui\";\n\nexport function sortedSections(\n store: { Sections?: StorefrontSectionView[] | null } | null | undefined,\n): StorefrontSectionView[] {\n return (store?.Sections ?? [])\n .slice()\n .sort((a, b) => a.SortOrder - b.SortOrder);\n}\n\nexport function slotsOf(section: StorefrontSectionView): StorefrontSlotView[] {\n return (section.Slots ?? [])\n .slice()\n .sort((a, b) => a.SortOrder - b.SortOrder);\n}\n\n/** When the first card of a section refreshes — the section's countdown. */\nexport function earliestRefresh(section: StorefrontSectionView): string | null {\n const times = slotsOf(section)\n .map((s) => s.RefreshesAtUtc)\n .filter((x): x is string => typeof x === \"string\")\n .sort();\n return times[0] ?? null;\n}\n\nexport function isFree(offer: StorefrontOfferView): boolean {\n const option = offer.PriceOptions?.[0];\n return option?.IsFree === true || costLines(option?.Cost).length === 0;\n}\n\n/** Offers the player can take right now (not sold out, not waiting for their window). */\nexport function isAvailable(offer: StorefrontOfferView): boolean {\n return offer.State?.SoldOut !== true && !offer.State?.AvailableAtUtc;\n}\n\n/** The badge of the shop: free offers waiting to be taken (the daily gift). */\nexport function freeOffersWaiting(\n front: GetStorefrontResponse | null | undefined,\n): number {\n let n = 0;\n for (const store of front?.Stores ?? [])\n for (const section of store.Sections ?? [])\n for (const slot of section.Slots ?? [])\n if (slot.Offer && isFree(slot.Offer) && isAvailable(slot.Offer)) n++;\n return n;\n}\n\n/** Stores by their order; the first is the default tab. */\nexport function sortedStores(\n front: GetStorefrontResponse | null | undefined,\n): NonNullable<GetStorefrontResponse[\"Stores\"]> {\n return (front?.Stores ?? [])\n .slice()\n .sort((a, b) => a.SortOrder - b.SortOrder);\n}\n\n/** Whether the storefront has any offer — until it does, the lobby hides the Shop tab. */\nexport function hasOffers(\n front: GetStorefrontResponse | null | undefined,\n): boolean {\n return (front?.Stores ?? []).some((store) =>\n (store.Sections ?? []).some((section) =>\n (section.Slots ?? []).some((slot) => slot.Offer != null),\n ),\n );\n}\n"
|
|
61
57
|
},
|
|
62
58
|
{
|
|
63
59
|
"path": "module.ts",
|
|
64
|
-
"content": "import { createElement, useEffect } from \"react\";\nimport {\n defineModule,\n type FeatureRegistry,\n type Module,\n} from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient } from \"@idosgames/react\";\nimport { StoreScreen } from \"./StoreScreen\";\nimport { freeOffersWaiting } from \"./model\";\nimport { t } from \"./i18n\";\n\n// The shop as a feature of the game (ctx.features): the lobby shows it as a main tab
|
|
60
|
+
"content": "import { createElement, useEffect } from \"react\";\nimport {\n defineModule,\n type FeatureRegistry,\n type Module,\n} from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient } from \"@idosgames/react\";\nimport { StoreScreen } from \"./StoreScreen\";\nimport { freeOffersWaiting, hasOffers } from \"./model\";\nimport { t } from \"./i18n\";\n\n// The shop as a feature of the game (ctx.features): the lobby shows it as a main tab once the\n// storefront has offers. Its badge counts free offers waiting to be taken (the daily gift), kept\n// fresh by an invisible always-on panel — the screen itself is mounted only while open.\nexport const storeModule: Module = defineModule({\n id: \"store\",\n meta: { name: \"Shop\", type: \"app\", engine: \"dom\" },\n setup(ctx) {\n ctx.features.register({\n id: \"store\",\n label: t(\"title\"),\n icon: \"shop\",\n group: \"economy\",\n order: 10,\n primary: true,\n available: false,\n Screen: StoreScreen,\n });\n ctx.registerPanel({\n id: \"badge\",\n slot: \"modal\",\n activeOnly: false,\n component: makeBadgeWatcher(ctx.features),\n });\n ctx.exposeToAgent({\n state: () => ({\n ui: \"dom\",\n feature: \"store\",\n badge: ctx.features.get(\"store\")?.badge ?? 0,\n }),\n describeActions: {},\n });\n },\n});\n\nfunction makeBadgeWatcher(features: FeatureRegistry) {\n return function StoreBadge() {\n const client = useIDosGamesClient();\n useEffect(() => {\n const update = () => {\n const front = client.store.cachedStorefront;\n features.setAvailable(\n \"store\",\n hasOffers(front),\n \"the storefront has no offers (Store section)\",\n );\n features.setBadge(\"store\", freeOffersWaiting(front));\n };\n void client.store.getStorefront().then(update);\n const off = client.on(\"user:anyUpdated\", update);\n // A free gift comes back with the next day: look again every few minutes.\n const id = setInterval(\n () => void client.store.getStorefront().then(update),\n 5 * 60_000,\n );\n return () => {\n off();\n clearInterval(id);\n };\n }, [client]);\n return createElement(\"span\", { hidden: true });\n };\n}\n"
|
|
65
61
|
},
|
|
66
62
|
{
|
|
67
63
|
"path": "StoreScreen.tsx",
|
|
@@ -69,7 +65,7 @@
|
|
|
69
65
|
},
|
|
70
66
|
{
|
|
71
67
|
"path": "module.meta.json",
|
|
72
|
-
"content": "{\n \"id\": \"store
|
|
68
|
+
"content": "{\n \"id\": \"store\",\n \"type\": \"feature\",\n \"summary\": \"Shop screen for any game: the title's storefront with stores as tabs, offer cards, purchase confirmation and the reward animation.\",\n \"description\": \"A ready shop for ANY game, built on the @idosgames/react/ui kit. It draws the storefront the server resolves for the player (client.store.getStorefront — rotation, limits, timers and badges are the server's), several stores as tabs, the first section of a store as a carousel and the rest as grids, a free daily gift that shines, sold-out and not-yet-available states with countdowns, a confirmation popup (a bottom sheet on phones) and the 'You got' animation where rewards fly to the balance counters. Registers itself as the 'store' feature of the game (ctx.features): the lobby of the base shows it as a tab, and any module can open it with ctx.features.open('store'). Its badge counts free offers waiting. Ships title-seed.json: a demo store with a daily gift, items and currency packs.\",\n \"provides\": [\n \"shop screen from the title storefront\",\n \"several stores as tabs\",\n \"offer carousel and grids with countdowns\",\n \"free daily gift offer\",\n \"purchase confirmation popup\",\n \"reward animation with icons flying to balances\",\n \"shop badge for free offers\",\n \"demo store settings\"\n ],\n \"tags\": [\n \"shop\",\n \"store\",\n \"storefront\",\n \"purchase\",\n \"offers\",\n \"ui\",\n \"screen\",\n \"economy\"\n ],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.0\"\n}\n"
|
|
73
69
|
}
|
|
74
70
|
]
|
|
75
71
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "tutorials",
|
|
3
3
|
"meta": {
|
|
4
4
|
"name": "Tutorials",
|
|
5
5
|
"type": "app",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"catalog": {
|
|
9
9
|
"type": "feature",
|
|
10
10
|
"summary": "Onboarding for any game: hint bubbles next to marked elements, step by step, skip, a completion reward and a replay list.",
|
|
11
|
-
"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,
|
|
11
|
+
"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.",
|
|
12
12
|
"provides": [
|
|
13
13
|
"tutorial overlay with a highlighted element",
|
|
14
14
|
"hint bubble with next and skip",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"version": "0.1.0"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.19.
|
|
36
|
-
"@idosgames/module-sdk": "0.
|
|
35
|
+
"@idosgames/core": "0.19.1",
|
|
36
|
+
"@idosgames/module-sdk": "0.7.0",
|
|
37
37
|
"@idosgames/react": "0.3.0",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
|
-
"contentHash": "
|
|
40
|
+
"contentHash": "e5cb4e931ff39a7eb3f4cbc32bddc836d79eb631bcd14a6fe6e567df88079718",
|
|
41
41
|
"files": [
|
|
42
42
|
{
|
|
43
43
|
"path": "i18n.ts",
|
|
@@ -45,11 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
"path": "index.ts",
|
|
48
|
-
"content": "// @idosgames/mod-
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
"path": "model.test.ts",
|
|
52
|
-
"content": "import { describe, expect, it } from \"vitest\";\nimport seed from \"../title-seed.json\";\nimport {\n activeFlow,\n anchorOf,\n bubblePlacement,\n canTapNext,\n stepCounter,\n stepText,\n} from \"./model\";\n\ndescribe(\"tutorial\", () => {\n it(\"the seeded welcome tour: five steps on the lobby's anchors, skippable, replayable\", () => {\n const flow = (\n seed as unknown as {\n Tutorial: {\n Flows: Record<\n string,\n {\n Steps: Record<string, { Identity: { AnchorID?: string } }>;\n Policy: { Skippable: boolean; RestartPolicy: string };\n }\n >;\n };\n }\n ).Tutorial.Flows.welcome!;\n expect(Object.keys(flow.Steps)).toHaveLength(5);\n expect(\n Object.values(flow.Steps).map((s) => s.Identity.AnchorID ?? null),\n ).toEqual([\n null,\n \"tab:store\",\n \"tab:character\",\n \"tab:rewards\",\n \"lobby:profile\",\n ]);\n expect(flow.Policy).toMatchObject({\n Skippable: true,\n RestartPolicy: \"OnRequest\",\n });\n });\n\n it(\"shows the running flow, else the one whose reward waits\", () => {\n const running = { FlowID: \"a\", Status: \"InProgress\", CurrentStep: {} };\n const pending = { FlowID: \"b\", Status: \"Completed\", RewardPending: true };\n expect(activeFlow([pending, running])?.FlowID).toBe(\"a\");\n expect(activeFlow([pending])?.FlowID).toBe(\"b\");\n expect(activeFlow([{ FlowID: \"c\", Status: \"Completed\" }])).toBeNull();\n });\n\n it(\"'Next' only on steps the player closes; the highlight prefers HighlightTarget\", () => {\n expect(canTapNext({ Completion: { Mode: \"ClientAck\" } })).toBe(true);\n expect(canTapNext({})).toBe(true);\n expect(canTapNext({ Completion: { Mode: \"SystemEvent\" } })).toBe(false);\n expect(\n anchorOf({ Identity: { AnchorID: \"a\", HighlightTarget: \"h\" } }),\n ).toBe(\"h\");\n expect(anchorOf({ Identity: { AnchorID: \"a\" } })).toBe(\"a\");\n expect(\n stepText({ Identity: { TitleKey: \"k\", Body: \"b\" } }, (k) => `[${k}]`),\n ).toEqual({ title: \"[k]\", body: \"b\" });\n expect(stepCounter({ FlowID: \"x\", TotalSteps: 5, CompletedSteps: 1 })).toBe(\n \"2 / 5\",\n );\n });\n\n it(\"puts the bubble below the element, above it near the bottom, inside the screen\", () => {\n const screen = { width: 375, height: 812 };\n const bubble = { width: 320, height: 150 };\n expect(\n bubblePlacement(\n { left: 10, top: 100, width: 60, height: 40 },\n screen,\n bubble,\n ),\n ).toEqual({ left: 12, top: 152, above: false });\n const low = bubblePlacement(\n { left: 300, top: 740, width: 70, height: 60 },\n screen,\n bubble,\n );\n expect(low.above).toBe(true);\n expect(low.top).toBe(740 - 12 - 150);\n expect(low.left).toBe(375 - 12 - 320);\n expect(bubblePlacement(null, screen, bubble)).toEqual({\n left: 27.5,\n top: 331,\n above: false,\n });\n });\n});\n"
|
|
48
|
+
"content": "// @idosgames/mod-tutorials — Onboarding for any game: hint bubbles next to marked elements, step by step, with a reward and a replay list.\n\nexport { tutorialsModule } from \"./module\";\n"
|
|
53
49
|
},
|
|
54
50
|
{
|
|
55
51
|
"path": "model.ts",
|
|
@@ -57,7 +53,7 @@
|
|
|
57
53
|
},
|
|
58
54
|
{
|
|
59
55
|
"path": "module.ts",
|
|
60
|
-
"content": "import { defineModule, type Module } from \"@idosgames/module-sdk\";\nimport {\n createTutorialStore,\n makeTutorialOverlay,\n makeTutorialsScreen,\n} from \"./TutorialOverlay\";\nimport { t } from \"./i18n\";\n\n// Onboarding for any game: an always-on overlay shows the current step next to the element marked\n// data-tutorial-anchor=\"{AnchorID}\"; the \"Tutorials\" feature lists the flows and replays them.\nexport const
|
|
56
|
+
"content": "import { defineModule, type Module } from \"@idosgames/module-sdk\";\nimport {\n createTutorialStore,\n makeTutorialOverlay,\n makeTutorialsScreen,\n} from \"./TutorialOverlay\";\nimport { t } from \"./i18n\";\n\n// Onboarding for any game: an always-on overlay shows the current step next to the element marked\n// data-tutorial-anchor=\"{AnchorID}\"; the \"Tutorials\" feature lists the flows and replays them.\nexport const tutorialsModule: Module = defineModule({\n id: \"tutorials\",\n meta: { name: \"Tutorials\", type: \"app\", engine: \"dom\" },\n setup(ctx) {\n const store = createTutorialStore();\n ctx.registerPanel({\n id: \"overlay\",\n slot: \"modal\",\n activeOnly: false,\n component: makeTutorialOverlay(store),\n });\n ctx.features.register({\n id: \"tutorials\",\n label: t(\"title\"),\n icon: \"bulb\",\n group: \"profile\",\n order: 110,\n available: false,\n Screen: makeTutorialsScreen(store),\n });\n // The list shows once the player has a flow to see again — from the state the overlay loads at\n // start anyway. No call of its own: the startup burst of every module's first read already\n // brushes the engine's per-player rate limit.\n store.subscribe(() =>\n ctx.features.setAvailable(\n \"tutorials\",\n (store.get()?.length ?? 0) > 0,\n \"no tutorial flow for this player (Tutorial section empty, or no flow started yet)\",\n ),\n );\n ctx.exposeToAgent({\n state: () => ({\n ui: \"dom\",\n feature: \"tutorials\",\n running:\n store\n .get()\n ?.filter((f) => f.Status === \"InProgress\")\n .map((f) => f.FlowID) ?? [],\n }),\n describeActions: {},\n });\n },\n});\n"
|
|
61
57
|
},
|
|
62
58
|
{
|
|
63
59
|
"path": "TutorialOverlay.tsx",
|
|
@@ -65,7 +61,7 @@
|
|
|
65
61
|
},
|
|
66
62
|
{
|
|
67
63
|
"path": "module.meta.json",
|
|
68
|
-
"content": "{\n \"id\": \"
|
|
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.0\"\n}\n"
|
|
69
65
|
}
|
|
70
66
|
]
|
|
71
67
|
}
|