@idosgames/mcp 0.1.19 → 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 +7 -7
- package/registry/index.json +437 -110
- package/registry/modules/ai.json +65 -0
- package/registry/modules/board-game.json +3 -3
- package/registry/modules/boosts.json +66 -0
- package/registry/modules/character.json +4 -5
- package/registry/modules/chat.json +4 -5
- package/registry/modules/collections.json +69 -0
- package/registry/modules/coop-events.json +67 -0
- package/registry/modules/craft.json +4 -5
- package/registry/modules/deals.json +4 -5
- package/registry/modules/events.json +8 -13
- package/registry/modules/friends.json +5 -5
- package/registry/modules/idle-rpg.json +3 -3
- package/registry/modules/inventory.json +15 -18
- package/registry/modules/invite.json +7 -7
- package/registry/modules/leaderboard.json +4 -5
- package/registry/modules/lootboxes.json +67 -0
- package/registry/modules/mailbox.json +71 -0
- package/registry/modules/marketplace.json +4 -5
- package/registry/modules/mini-game.json +2 -2
- package/registry/modules/multiplayer.json +74 -0
- package/registry/modules/premium.json +67 -0
- package/registry/modules/profile.json +9 -10
- package/registry/modules/pvp.json +4 -5
- package/registry/modules/quests.json +69 -0
- package/registry/modules/rewards.json +15 -21
- package/registry/modules/season.json +8 -13
- package/registry/modules/store.json +4 -5
- package/registry/modules/tutorials.json +4 -5
- package/registry/modules/voxelcraft.json +15 -11
- package/registry/modules/workshop.json +4 -5
- package/registry/skills/blockchain-system.json +1 -1
- package/registry/skills/idosgames-compose-modules.json +2 -2
- package/registry/skills/idosgames-getting-started.json +1 -1
- package/registry/skills/idosgames-module-contract.json +2 -2
- 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/workshop-system.json +1 -1
|
@@ -7,16 +7,15 @@
|
|
|
7
7
|
},
|
|
8
8
|
"catalog": {
|
|
9
9
|
"type": "feature",
|
|
10
|
-
"inBase": true,
|
|
11
10
|
"summary": "Seasons for any game: the active season with its timer, a status bar, a tier ladder with a reward each, claim all and a VIP second prize.",
|
|
12
|
-
"description": "A ready season screen (Unity Core/Window MilestoneRewards) for ANY game, built on the @idosgames/react/ui kit and client.season. Every season chain of the title is a tab showing its active season: name and timer, the player's tier, the status bar toward the next tier and the tier ladder. Each tier pays its reward once ('Claim all' takes every reached tier in one call); a tier with premium tiers pays a VIP second prize when the pass is active at the moment of the claim.
|
|
11
|
+
"description": "A ready season screen (Unity Core/Window MilestoneRewards) for ANY game, built on the @idosgames/react/ui kit and client.season. Every season chain of the title is a tab showing its active season: name and timer, the player's tier, the status bar toward the next tier and the tier ladder. Each tier pays its reward once ('Claim all' takes every reached tier in one call); a tier with premium tiers pays a VIP second prize when the pass is active at the moment of the claim. 'Get VIP' opens the VIP system (ctx.features.open('premium', { premiumID })) and shows only when it is installed and has a pass. The server decides the tier — including status tokens that arrive as rewards of other systems. Registers the 'season' feature; its badge counts tier rewards waiting and it reloads when status tokens are granted. Status comes from the game: a chain open to clients takes client.season.grantStatusTokens (the mini-game module grants status for every round). Ships title-seed.json: two alternating 28-day seasons with five tiers and a VIP prize on each.",
|
|
13
12
|
"provides": [
|
|
14
13
|
"season screen with a tab per chain",
|
|
15
14
|
"season timer and status bar",
|
|
16
15
|
"tier ladder with rewards",
|
|
17
16
|
"claim all reached tiers",
|
|
18
17
|
"VIP second prize on tiers",
|
|
19
|
-
"
|
|
18
|
+
"hand-off to the VIP system",
|
|
20
19
|
"season badge",
|
|
21
20
|
"demo seasons"
|
|
22
21
|
],
|
|
@@ -37,16 +36,16 @@
|
|
|
37
36
|
"version": "0.1.0"
|
|
38
37
|
},
|
|
39
38
|
"dependencies": {
|
|
40
|
-
"@idosgames/core": "0.19.
|
|
41
|
-
"@idosgames/module-sdk": "0.
|
|
39
|
+
"@idosgames/core": "0.19.1",
|
|
40
|
+
"@idosgames/module-sdk": "0.7.0",
|
|
42
41
|
"@idosgames/react": "0.3.0",
|
|
43
42
|
"react": "19.2.7"
|
|
44
43
|
},
|
|
45
|
-
"contentHash": "
|
|
44
|
+
"contentHash": "71faf793fef491d7e27dd6e3a5e7072d5e32159f60bff658bae3d68f2bdfd74c",
|
|
46
45
|
"files": [
|
|
47
46
|
{
|
|
48
47
|
"path": "i18n.ts",
|
|
49
|
-
"content": "import { makeT } from \"@idosgames/react/ui\";\n\nconst EN = {\n title: \"Season\",\n noSeason: \"No season is running right now.\",\n endsIn: \"Ends in\",\n tier: \"Tier\",\n claim: \"Claim\",\n claimAll: \"Claim all\",\n claimed: \"Claimed\",\n locked: \"Locked\",\n current: \"You are here\",\n status: \"Status\",\n nextTier: \"Next tier at\",\n maxTier: \"Top tier reached!\",\n
|
|
48
|
+
"content": "import { makeT } from \"@idosgames/react/ui\";\n\nconst EN = {\n title: \"Season\",\n noSeason: \"No season is running right now.\",\n endsIn: \"Ends in\",\n tier: \"Tier\",\n claim: \"Claim\",\n claimAll: \"Claim all\",\n claimed: \"Claimed\",\n locked: \"Locked\",\n current: \"You are here\",\n status: \"Status\",\n nextTier: \"Next tier at\",\n maxTier: \"Top tier reached!\",\n getVip: \"Get VIP\",\n vipActive: \"VIP active\",\n vipBenefit: \"A second prize on every tier you claim while VIP is active.\",\n vipMissed: \"Claimed without VIP\",\n howToEarn: \"Earn status with quests, events and play.\",\n} as const;\n\nexport const t = makeT<keyof typeof EN>(EN, {\n title: \"Сезон\",\n noSeason: \"Сейчас сезон не идёт.\",\n endsIn: \"До конца\",\n tier: \"Ступень\",\n claim: \"Забрать\",\n claimAll: \"Забрать всё\",\n claimed: \"Получено\",\n locked: \"Закрыто\",\n current: \"Вы здесь\",\n status: \"Статус\",\n nextTier: \"Следующая ступень на\",\n maxTier: \"Высшая ступень!\",\n getVip: \"Купить VIP\",\n vipActive: \"VIP активен\",\n vipBenefit:\n \"Второй приз на каждой ступени, которую вы забираете с активным VIP.\",\n vipMissed: \"Забрано без VIP\",\n howToEarn: \"Статус дают задания, ивенты и игра.\",\n});\n"
|
|
50
49
|
},
|
|
51
50
|
{
|
|
52
51
|
"path": "index.ts",
|
|
@@ -62,15 +61,11 @@
|
|
|
62
61
|
},
|
|
63
62
|
{
|
|
64
63
|
"path": "SeasonScreen.tsx",
|
|
65
|
-
"content": "import {\n useEffect,\n useRef,\n useState,\n useSyncExternalStore,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport type { ActiveSeasonInfo, IDosGamesClient } from \"@idosgames/core\";\nimport type {\n FeatureRegistry,\n FeatureScreenProps,\n} from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n Button,\n Card,\n Center,\n EmptyState,\n Icon,\n ProgressBar,\n ResourceList,\n Spinner,\n Tabs,\n Timer,\n errorText,\n formatAmount,\n grantedBy,\n outlined,\n panel,\n useCatalog,\n useCelebrate,\n useNow,\n useStagger,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport {\n claimedLines,\n readyTiers,\n seasonBadge,\n seasonTokens,\n tierProgress,\n tierRows,\n type PremiumView,\n type TierRow,\n} from \"./model\";\nimport { VipPopup } from \"./VipPopup\";\nimport { t } from \"./i18n\";\n\n// Seasons (Unity Core/Window MilestoneRewards): the active season of every chain — its timer, the\n// status bar toward the next tier and the tier ladder. Each tier pays once; a VIP gets a second\n// prize on the tiers claimed while the pass is active. The VIP pass is a popup.\n\ntype SeasonEntry = { info: ActiveSeasonInfo };\n\n/** Active seasons of every chain, shared by the screen and the badge watcher. */\nexport function createSeasonStore(features: FeatureRegistry) {\n let seasons: SeasonEntry[] | null = null;\n const listeners = new Set<() => void>();\n let premium: () => PremiumView | null | undefined = () => null;\n const badge = () =>\n features.setBadge(\n \"season\",\n seasons\n ? seasonBadge(\n seasons.map((s) => s.info),\n premium(),\n Date.now(),\n )\n : 0,\n );\n return {\n get: () => seasons,\n subscribe: (l: () => void) => {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n async load(client: IDosGamesClient): Promise<void> {\n premium = () =>\n client.data.user.state?.Premium as PremiumView | undefined;\n const defs = await client.season.getDefinitions();\n const chains = defs.ok ? Object.keys(defs.data.Chains ?? {}) : [];\n const active = await Promise.all(\n chains.map((c) => client.season.getActiveSeason(c)),\n );\n seasons = active.flatMap((r) => (r.ok ? [{ info: r.data }] : []));\n badge();\n for (const l of listeners) l();\n },\n refreshBadge: badge,\n };\n}\nexport type SeasonStore = ReturnType<typeof createSeasonStore>;\n\nexport function makeSeasonScreen(store: SeasonStore) {\n return function SeasonScreen(_: FeatureScreenProps): ReactNode {\n const client = useIDosGamesClient();\n const seasons = useSyncExternalStore(store.subscribe, store.get, store.get);\n const catalog = useCatalog();\n const [tab, setTab] = useState<string | null>(null);\n useEffect(() => {\n void store.load(client);\n }, [client]);\n if (!seasons)\n return (\n <Center>\n <Spinner />\n </Center>\n );\n if (seasons.length === 0)\n return <EmptyState glyph=\"trophy\" text={t(\"noSeason\")} />;\n const current =\n seasons.find((s) => s.info.SeasonChainID === tab) ?? seasons[0]!;\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n }}\n >\n <Tabs\n tabs={seasons.map((s) => ({\n id: s.info.SeasonChainID,\n label: catalog.localize(\n s.info.Season?.DisplayName ?? s.info.SeasonChainID,\n ),\n }))}\n value={current.info.SeasonChainID}\n onChange={setTab}\n />\n <SeasonView\n key={current.info.SeasonChainID}\n info={current.info}\n store={store}\n />\n </div>\n );\n };\n}\n\nexport function makeSeasonWatcher(store: SeasonStore) {\n return function SeasonWatcher(): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n useEffect(() => {\n void store.load(client);\n const id = setInterval(() => void store.load(client), 5 * 60_000);\n const off = client.on(\n \"season:statusTokensGranted\",\n () => void store.load(client),\n );\n return () => {\n clearInterval(id);\n off();\n };\n }, [client]);\n useEffect(() => store.refreshBadge(), [state?.Premium]);\n return null;\n };\n}\n\nfunction SeasonView({\n info,\n store,\n}: {\n info: ActiveSeasonInfo;\n store: SeasonStore;\n}): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const catalog = useCatalog();\n const celebrate = useCelebrate();\n const toast = useToast();\n const now = useNow();\n const stagger = useStagger();\n const [vip, setVip] = useState<string | null>(null);\n const [busyAll, setBusyAll] = useState(false);\n const premium = state?.Premium as PremiumView | undefined;\n const rows = tierRows(info, premium, now);\n const cached = (\n state?.EventToken?.Season as\n Record<string, { Balance?: { TotalEarned?: number } }> | undefined\n )?.[info.SeasonChainID]?.Balance?.TotalEarned;\n const tokens = seasonTokens(info, cached);\n const bar = tierProgress(rows, tokens);\n const ready = readyTiers(rows);\n const vipID = rows.find((r) => r.vipPremiumID)?.vipPremiumID ?? null;\n const vipHeld = rows.some((r) => r.vipHeld);\n const currentRow = rows.find((r) => r.current);\n\n const claimAll = async () => {\n setBusyAll(true);\n const res = await client.season.claimTierRewardsBatch(\n info.SeasonChainID,\n ready,\n );\n setBusyAll(false);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n const got = claimedLines(res.data) ?? grantedBy(res.data);\n celebrate(\n got.length > 0\n ? got\n : rows\n .filter((r) => r.state === \"ready\")\n .flatMap((r) => [...r.reward, ...(r.vipHeld ? r.vip : [])]),\n );\n await store.load(client);\n };\n\n return (\n <div style={{ display: \"grid\", gap: 12 }}>\n <Card\n style={{\n display: \"grid\",\n gap: 10,\n background: \"linear-gradient(180deg, #3b7be0 0%, #1d3f8f 100%)\",\n }}\n >\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10 }}>\n <div className=\"idos-bounce\">\n <Icon glyph={info.Season?.AssetPaths?.icon ?? \"trophy\"} size={46} />\n </div>\n <div style={{ flex: 1, minWidth: 0 }}>\n <div style={{ ...outlined, fontSize: 18 }}>\n {catalog.localize(info.Season?.DisplayName ?? info.SeasonChainID)}\n </div>\n <Timer at={info.ComputedEndUtc} now={now} label={t(\"endsIn\")} />\n </div>\n {currentRow ? (\n <div style={{ textAlign: \"right\", ...outlined }}>\n <div style={{ fontSize: 11, color: \"rgba(255,255,255,.8)\" }}>\n {t(\"tier\")}\n </div>\n <div style={{ fontSize: 20 }}>\n {currentRow.name\n ? catalog.localize(currentRow.name)\n : currentRow.tier}\n </div>\n </div>\n ) : null}\n </div>\n {info.Season?.Description ? (\n <div\n style={{\n ...outlined,\n fontSize: 13,\n fontWeight: 600,\n color: \"rgba(255,255,255,.85)\",\n }}\n >\n {catalog.localize(info.Season.Description)}\n </div>\n ) : null}\n <div style={{ display: \"grid\", gap: 4 }}>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n ...outlined,\n fontSize: 13,\n }}\n >\n <span>\n {t(\"status\")}: {formatAmount(tokens)}\n </span>\n <span>\n {bar.next\n ? `${t(\"nextTier\")} ${formatAmount(bar.goal)}`\n : t(\"maxTier\")}\n </span>\n </div>\n <ProgressBar\n value={bar.next ? tokens : 1}\n max={bar.next ? Math.max(1, bar.goal) : 1}\n tone=\"gold\"\n />\n </div>\n </Card>\n\n {vipID ? (\n vipHeld ? (\n <div\n style={{\n ...panel,\n padding: 10,\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n ...outlined,\n fontSize: 13,\n color: v.gold,\n }}\n >\n <Icon glyph=\"crown\" size={22} /> {t(\"vipActive\")}\n </div>\n ) : (\n <Card\n onClick={() => setVip(vipID)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n padding: 10,\n border: `2px solid ${v.gold}`,\n }}\n >\n <Icon glyph=\"crown\" size={30} />\n <div style={{ flex: 1, minWidth: 0, ...outlined, fontSize: 13 }}>\n {t(\"vipBenefit\")}\n </div>\n <Button size=\"sm\" tone=\"gold\" attract>\n {t(\"getVip\")}\n </Button>\n </Card>\n )\n ) : null}\n\n {ready.length > 1 ? (\n <Button\n tone=\"green\"\n attract\n busy={busyAll}\n onClick={() => void claimAll()}\n data-tutorial-anchor=\"season:claim-all\"\n >\n {t(\"claimAll\")} · {ready.length}\n </Button>\n ) : null}\n\n <div style={{ display: \"grid\", gap: 8 }}>\n {rows.map((row, i) => (\n <TierCard\n key={row.tier}\n info={info}\n row={row}\n store={store}\n style={stagger(i)}\n />\n ))}\n </div>\n\n {vip ? <VipPopup premiumID={vip} onClose={() => setVip(null)} /> : null}\n </div>\n );\n}\n\nfunction TierCard({\n info,\n row,\n store,\n style,\n}: {\n info: ActiveSeasonInfo;\n row: TierRow;\n store: SeasonStore;\n style: CSSProperties;\n}): ReactNode {\n const client = useIDosGamesClient();\n const catalog = useCatalog();\n const celebrate = useCelebrate();\n const toast = useToast();\n const kit = useUiKit();\n const ref = useRef<HTMLDivElement>(null);\n const [busy, setBusy] = useState(false);\n\n const claim = async () => {\n setBusy(true);\n const res = await client.season.claimTierReward(\n info.SeasonChainID,\n row.tier,\n );\n setBusy(false);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n kit.play(\"levelUp\");\n kit.fx.burst(ref.current);\n const got = claimedLines(res.data) ?? grantedBy(res.data);\n celebrate(\n got.length > 0 ? got : [...row.reward, ...(row.vipHeld ? row.vip : [])],\n );\n await store.load(client);\n };\n\n return (\n <div\n ref={ref}\n style={{\n ...panel,\n padding: 10,\n display: \"grid\",\n gridTemplateColumns: \"auto minmax(0, 1fr) auto\",\n alignItems: \"center\",\n gap: 10,\n border:\n row.state === \"ready\"\n ? `2px solid ${v.gold}`\n : row.current\n ? `2px solid ${v.blue}`\n : panel.border,\n opacity: row.state === \"claimed\" ? 0.6 : 1,\n ...style,\n }}\n >\n <div style={{ display: \"grid\", justifyItems: \"center\", minWidth: 56 }}>\n <Icon glyph={row.state === \"locked\" ? \"lock\" : \"trophy\"} size={24} />\n <span style={{ ...outlined, fontSize: 12 }}>\n {row.name ? catalog.localize(row.name) : `${t(\"tier\")} ${row.tier}`}\n </span>\n <span style={{ ...outlined, fontSize: 11, color: v.textDim }}>\n {formatAmount(row.required)}\n </span>\n </div>\n <div style={{ display: \"grid\", gap: 6, minWidth: 0 }}>\n {row.current ? (\n <div style={{ ...outlined, fontSize: 11, color: v.blue }}>\n {t(\"current\")}\n </div>\n ) : null}\n <ResourceList lines={row.reward} size={26} />\n {row.vip.length > 0 ? (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n opacity: row.vipHeld && row.state !== \"claimed\" ? 1 : 0.5,\n }}\n >\n <Icon glyph=\"crown\" size={18} />\n <ResourceList lines={row.vip} size={20} />\n </div>\n ) : null}\n </div>\n <div>\n {row.state === \"ready\" ? (\n <Button\n size=\"sm\"\n tone=\"green\"\n attract\n busy={busy}\n onClick={() => void claim()}\n data-tutorial-anchor={`season:claim:${row.tier}`}\n >\n {t(\"claim\")}\n </Button>\n ) : row.state === \"claimed\" ? (\n <span\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 4,\n ...outlined,\n fontSize: 12,\n color: v.green,\n }}\n >\n <Icon glyph=\"check\" size={18} /> {t(\"claimed\")}\n </span>\n ) : (\n <span style={{ ...outlined, fontSize: 12, color: v.textDim }}>\n {t(\"locked\")}\n </span>\n )}\n </div>\n </div>\n );\n}\n"
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
"path": "VipPopup.tsx",
|
|
69
|
-
"content": "import { useEffect, useState, type ReactNode } from \"react\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n Button,\n Center,\n Icon,\n Popup,\n ResourceList,\n Spinner,\n canAfford,\n costLines,\n errorText,\n outlined,\n panel,\n usePopupClose,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport { t } from \"./i18n\";\n\n// The VIP pass (Unity PremiumPopUp): what it gives and a price option to buy it with currencies.\n// A pass paid in a store (real money) is not sold here — that is the platform's checkout.\n\n/** The part of a premium definition (config section Premium) the popup reads. */\ntype PremiumDefinition = {\n PremiumID?: string | null;\n DisplayName?: string | null;\n DurationDays?: number | null;\n Benefits?: Record<string, string> | null;\n PriceOptions?: Record<\n string,\n { OptionID?: string | null; Cost?: unknown } | null\n > | null;\n};\n\nexport function VipPopup({\n premiumID,\n onClose,\n}: {\n premiumID: string;\n onClose: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const [def, setDef] = useState<PremiumDefinition | null | undefined>(\n undefined,\n );\n useEffect(() => {\n void client.premium\n .getDefinitions()\n .then((res) =>\n setDef(\n res.ok\n ? ((res.data.Premium?.Definitions?.[premiumID] as\n PremiumDefinition | undefined) ?? null)\n : null,\n ),\n );\n }, [client, premiumID]);\n return (\n <Popup\n title={def?.DisplayName ?? t(\"vip\")}\n onClose={onClose}\n width={420}\n variant=\"center\"\n >\n {def === undefined ? (\n <Center minHeight={120}>\n <Spinner />\n </Center>\n ) : def === null ? (\n <div\n style={{\n ...outlined,\n fontSize: 14,\n textAlign: \"center\",\n color: v.textDim,\n }}\n >\n —\n </div>\n ) : (\n <VipOffer def={def} />\n )}\n </Popup>\n );\n}\n\nfunction VipOffer({ def }: { def: PremiumDefinition }): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const toast = useToast();\n const kit = useUiKit();\n const close = usePopupClose();\n const [busy, setBusy] = useState<string | null>(null);\n const balances = state?.InventoryV2?.VirtualCurrencies as\n Record<string, { Amount?: number }> | undefined;\n const options = Object.entries(def.PriceOptions ?? {})\n .filter(([, o]) => o)\n .map(([id, o]) => ({\n id: o?.OptionID || id,\n price: costLines(o?.Cost as never).filter((l) => l.kind !== \"other\"),\n }))\n .filter((o) => o.price.length > 0);\n\n const buy = async (optionID: string) => {\n setBusy(optionID);\n const res = await client.premium.purchaseItemOrCurrency(\n def.PremiumID!,\n crypto.randomUUID(),\n optionID,\n );\n setBusy(null);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n kit.play(\"purchase\");\n toast(t(\"vipActive\"), \"success\");\n close();\n };\n\n return (\n <div style={{ display: \"grid\", gap: 12, justifyItems: \"center\" }}>\n <div className=\"idos-bounce\">\n <Icon glyph=\"crown\" size={64} />\n </div>\n <div style={{ ...outlined, fontSize: 14, textAlign: \"center\" }}>\n {t(\"vipBenefit\")}\n </div>\n {Object.values(def.Benefits ?? {}).length > 0 ? (\n <div style={{ display: \"grid\", gap: 4, width: \"100%\" }}>\n {Object.values(def.Benefits ?? {}).map((b, i) => (\n <div\n key={i}\n style={{\n ...panel,\n padding: 8,\n display: \"flex\",\n gap: 8,\n alignItems: \"center\",\n ...outlined,\n fontSize: 13,\n }}\n >\n <Icon glyph=\"check\" size={18} /> {b}\n </div>\n ))}\n </div>\n ) : null}\n {Number(def.DurationDays ?? 0) > 0 ? (\n <div style={{ ...outlined, fontSize: 13, color: v.textDim }}>\n {def.DurationDays} {t(\"days\")}\n </div>\n ) : null}\n <div style={{ display: \"grid\", gap: 8, width: \"100%\" }}>\n {options.map((o) => {\n const affordable = canAfford(o.price, balances);\n return (\n <Button\n key={o.id}\n tone=\"gold\"\n attract={affordable}\n disabled={!affordable}\n busy={busy === o.id}\n onClick={() => void buy(o.id)}\n data-tutorial-anchor=\"vip:buy\"\n >\n {t(\"buy\")}{\" \"}\n <ResourceList\n lines={o.price}\n size={18}\n dimIf={() => !affordable}\n />\n </Button>\n );\n })}\n </div>\n </div>\n );\n}\n"
|
|
64
|
+
"content": "import {\n useEffect,\n useRef,\n useState,\n useSyncExternalStore,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport type { ActiveSeasonInfo, IDosGamesClient } from \"@idosgames/core\";\nimport type {\n FeatureRegistry,\n FeatureScreenProps,\n} from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n Button,\n Card,\n Center,\n EmptyState,\n Icon,\n ProgressBar,\n ResourceList,\n Spinner,\n Tabs,\n Timer,\n errorText,\n formatAmount,\n grantedBy,\n outlined,\n panel,\n useCatalog,\n useCelebrate,\n useNow,\n useStagger,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport {\n claimedLines,\n readyTiers,\n seasonBadge,\n seasonTokens,\n tierProgress,\n tierRows,\n type PremiumView,\n type TierRow,\n} from \"./model\";\nimport { t } from \"./i18n\";\n\n// Seasons (Unity Core/Window MilestoneRewards): the active season of every chain — its timer, the\n// status bar toward the next tier and the tier ladder. Each tier pays once; a VIP gets a second\n// prize on the tiers claimed while the pass is active. The VIP pass is a popup.\n\ntype SeasonEntry = { info: ActiveSeasonInfo };\n\n/** Active seasons of every chain, shared by the screen and the badge watcher. */\nexport function createSeasonStore(features: FeatureRegistry) {\n let seasons: SeasonEntry[] | null = null;\n const listeners = new Set<() => void>();\n let premium: () => PremiumView | null | undefined = () => null;\n const badge = () =>\n features.setBadge(\n \"season\",\n seasons\n ? seasonBadge(\n seasons.map((s) => s.info),\n premium(),\n Date.now(),\n )\n : 0,\n );\n return {\n get: () => seasons,\n subscribe: (l: () => void) => {\n listeners.add(l);\n return () => listeners.delete(l);\n },\n async load(client: IDosGamesClient): Promise<void> {\n premium = () =>\n client.data.user.state?.Premium as PremiumView | undefined;\n const defs = await client.season.getDefinitions();\n const chains = defs.ok ? Object.keys(defs.data.Chains ?? {}) : [];\n const active = await Promise.all(\n chains.map((c) => client.season.getActiveSeason(c)),\n );\n seasons = active.flatMap((r) => (r.ok ? [{ info: r.data }] : []));\n badge();\n for (const l of listeners) l();\n },\n refreshBadge: badge,\n /** The VIP system is installed and has a pass on the title — the VIP prize can be bought. */\n vipOnSale: () => features.get(\"premium\")?.available === true,\n openVip: (premiumID: string) => features.open(\"premium\", { premiumID }),\n };\n}\nexport type SeasonStore = ReturnType<typeof createSeasonStore>;\n\nexport function makeSeasonScreen(store: SeasonStore) {\n return function SeasonScreen(_: FeatureScreenProps): ReactNode {\n const client = useIDosGamesClient();\n const seasons = useSyncExternalStore(store.subscribe, store.get, store.get);\n const catalog = useCatalog();\n const [tab, setTab] = useState<string | null>(null);\n useEffect(() => {\n void store.load(client);\n }, [client]);\n if (!seasons)\n return (\n <Center>\n <Spinner />\n </Center>\n );\n if (seasons.length === 0)\n return <EmptyState glyph=\"trophy\" text={t(\"noSeason\")} />;\n const current =\n seasons.find((s) => s.info.SeasonChainID === tab) ?? seasons[0]!;\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n }}\n >\n <Tabs\n tabs={seasons.map((s) => ({\n id: s.info.SeasonChainID,\n label: catalog.localize(\n s.info.Season?.DisplayName ?? s.info.SeasonChainID,\n ),\n }))}\n value={current.info.SeasonChainID}\n onChange={setTab}\n />\n <SeasonView\n key={current.info.SeasonChainID}\n info={current.info}\n store={store}\n />\n </div>\n );\n };\n}\n\nexport function makeSeasonWatcher(store: SeasonStore) {\n return function SeasonWatcher(): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n useEffect(() => {\n void store.load(client);\n const id = setInterval(() => void store.load(client), 5 * 60_000);\n const off = client.on(\n \"season:statusTokensGranted\",\n () => void store.load(client),\n );\n return () => {\n clearInterval(id);\n off();\n };\n }, [client]);\n useEffect(() => store.refreshBadge(), [state?.Premium]);\n return null;\n };\n}\n\nfunction SeasonView({\n info,\n store,\n}: {\n info: ActiveSeasonInfo;\n store: SeasonStore;\n}): ReactNode {\n const client = useIDosGamesClient();\n const state = useUserState();\n const catalog = useCatalog();\n const celebrate = useCelebrate();\n const toast = useToast();\n const now = useNow();\n const stagger = useStagger();\n const [busyAll, setBusyAll] = useState(false);\n const premium = state?.Premium as PremiumView | undefined;\n const rows = tierRows(info, premium, now);\n const cached = (\n state?.EventToken?.Season as\n Record<string, { Balance?: { TotalEarned?: number } }> | undefined\n )?.[info.SeasonChainID]?.Balance?.TotalEarned;\n const tokens = seasonTokens(info, cached);\n const bar = tierProgress(rows, tokens);\n const ready = readyTiers(rows);\n const vipID = rows.find((r) => r.vipPremiumID)?.vipPremiumID ?? null;\n const vipHeld = rows.some((r) => r.vipHeld);\n const currentRow = rows.find((r) => r.current);\n\n const claimAll = async () => {\n setBusyAll(true);\n const res = await client.season.claimTierRewardsBatch(\n info.SeasonChainID,\n ready,\n );\n setBusyAll(false);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n const got = claimedLines(res.data) ?? grantedBy(res.data);\n celebrate(\n got.length > 0\n ? got\n : rows\n .filter((r) => r.state === \"ready\")\n .flatMap((r) => [...r.reward, ...(r.vipHeld ? r.vip : [])]),\n );\n await store.load(client);\n };\n\n return (\n <div style={{ display: \"grid\", gap: 12 }}>\n <Card\n style={{\n display: \"grid\",\n gap: 10,\n background: \"linear-gradient(180deg, #3b7be0 0%, #1d3f8f 100%)\",\n }}\n >\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 10 }}>\n <div className=\"idos-bounce\">\n <Icon glyph={info.Season?.AssetPaths?.icon ?? \"trophy\"} size={46} />\n </div>\n <div style={{ flex: 1, minWidth: 0 }}>\n <div style={{ ...outlined, fontSize: 18 }}>\n {catalog.localize(info.Season?.DisplayName ?? info.SeasonChainID)}\n </div>\n <Timer at={info.ComputedEndUtc} now={now} label={t(\"endsIn\")} />\n </div>\n {currentRow ? (\n <div style={{ textAlign: \"right\", ...outlined }}>\n <div style={{ fontSize: 11, color: \"rgba(255,255,255,.8)\" }}>\n {t(\"tier\")}\n </div>\n <div style={{ fontSize: 20 }}>\n {currentRow.name\n ? catalog.localize(currentRow.name)\n : currentRow.tier}\n </div>\n </div>\n ) : null}\n </div>\n {info.Season?.Description ? (\n <div\n style={{\n ...outlined,\n fontSize: 13,\n fontWeight: 600,\n color: \"rgba(255,255,255,.85)\",\n }}\n >\n {catalog.localize(info.Season.Description)}\n </div>\n ) : null}\n <div style={{ display: \"grid\", gap: 4 }}>\n <div\n style={{\n display: \"flex\",\n justifyContent: \"space-between\",\n ...outlined,\n fontSize: 13,\n }}\n >\n <span>\n {t(\"status\")}: {formatAmount(tokens)}\n </span>\n <span>\n {bar.next\n ? `${t(\"nextTier\")} ${formatAmount(bar.goal)}`\n : t(\"maxTier\")}\n </span>\n </div>\n <ProgressBar\n value={bar.next ? tokens : 1}\n max={bar.next ? Math.max(1, bar.goal) : 1}\n tone=\"gold\"\n />\n </div>\n </Card>\n\n {vipID && (vipHeld || store.vipOnSale()) ? (\n vipHeld ? (\n <div\n style={{\n ...panel,\n padding: 10,\n display: \"flex\",\n alignItems: \"center\",\n gap: 8,\n ...outlined,\n fontSize: 13,\n color: v.gold,\n }}\n >\n <Icon glyph=\"crown\" size={22} /> {t(\"vipActive\")}\n </div>\n ) : (\n <Card\n onClick={() => store.openVip(vipID)}\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 10,\n padding: 10,\n border: `2px solid ${v.gold}`,\n }}\n >\n <Icon glyph=\"crown\" size={30} />\n <div style={{ flex: 1, minWidth: 0, ...outlined, fontSize: 13 }}>\n {t(\"vipBenefit\")}\n </div>\n <Button size=\"sm\" tone=\"gold\" attract>\n {t(\"getVip\")}\n </Button>\n </Card>\n )\n ) : null}\n\n {ready.length > 1 ? (\n <Button\n tone=\"green\"\n attract\n busy={busyAll}\n onClick={() => void claimAll()}\n data-tutorial-anchor=\"season:claim-all\"\n >\n {t(\"claimAll\")} · {ready.length}\n </Button>\n ) : null}\n\n <div style={{ display: \"grid\", gap: 8 }}>\n {rows.map((row, i) => (\n <TierCard\n key={row.tier}\n info={info}\n row={row}\n store={store}\n style={stagger(i)}\n />\n ))}\n </div>\n </div>\n );\n}\n\nfunction TierCard({\n info,\n row,\n store,\n style,\n}: {\n info: ActiveSeasonInfo;\n row: TierRow;\n store: SeasonStore;\n style: CSSProperties;\n}): ReactNode {\n const client = useIDosGamesClient();\n const catalog = useCatalog();\n const celebrate = useCelebrate();\n const toast = useToast();\n const kit = useUiKit();\n const ref = useRef<HTMLDivElement>(null);\n const [busy, setBusy] = useState(false);\n\n const claim = async () => {\n setBusy(true);\n const res = await client.season.claimTierReward(\n info.SeasonChainID,\n row.tier,\n );\n setBusy(false);\n if (!res.ok) return toast(errorText(res.error), \"error\");\n kit.play(\"levelUp\");\n kit.fx.burst(ref.current);\n const got = claimedLines(res.data) ?? grantedBy(res.data);\n celebrate(\n got.length > 0 ? got : [...row.reward, ...(row.vipHeld ? row.vip : [])],\n );\n await store.load(client);\n };\n\n return (\n <div\n ref={ref}\n style={{\n ...panel,\n padding: 10,\n display: \"grid\",\n gridTemplateColumns: \"auto minmax(0, 1fr) auto\",\n alignItems: \"center\",\n gap: 10,\n border:\n row.state === \"ready\"\n ? `2px solid ${v.gold}`\n : row.current\n ? `2px solid ${v.blue}`\n : panel.border,\n opacity: row.state === \"claimed\" ? 0.6 : 1,\n ...style,\n }}\n >\n <div style={{ display: \"grid\", justifyItems: \"center\", minWidth: 56 }}>\n <Icon glyph={row.state === \"locked\" ? \"lock\" : \"trophy\"} size={24} />\n <span style={{ ...outlined, fontSize: 12 }}>\n {row.name ? catalog.localize(row.name) : `${t(\"tier\")} ${row.tier}`}\n </span>\n <span style={{ ...outlined, fontSize: 11, color: v.textDim }}>\n {formatAmount(row.required)}\n </span>\n </div>\n <div style={{ display: \"grid\", gap: 6, minWidth: 0 }}>\n {row.current ? (\n <div style={{ ...outlined, fontSize: 11, color: v.blue }}>\n {t(\"current\")}\n </div>\n ) : null}\n <ResourceList lines={row.reward} size={26} />\n {row.vip.length > 0 ? (\n <div\n style={{\n display: \"flex\",\n alignItems: \"center\",\n gap: 6,\n opacity: row.vipHeld && row.state !== \"claimed\" ? 1 : 0.5,\n }}\n >\n <Icon glyph=\"crown\" size={18} />\n <ResourceList lines={row.vip} size={20} />\n </div>\n ) : null}\n </div>\n <div>\n {row.state === \"ready\" ? (\n <Button\n size=\"sm\"\n tone=\"green\"\n attract\n busy={busy}\n onClick={() => void claim()}\n data-tutorial-anchor={`season:claim:${row.tier}`}\n >\n {t(\"claim\")}\n </Button>\n ) : row.state === \"claimed\" ? (\n <span\n style={{\n display: \"inline-flex\",\n alignItems: \"center\",\n gap: 4,\n ...outlined,\n fontSize: 12,\n color: v.green,\n }}\n >\n <Icon glyph=\"check\" size={18} /> {t(\"claimed\")}\n </span>\n ) : (\n <span style={{ ...outlined, fontSize: 12, color: v.textDim }}>\n {t(\"locked\")}\n </span>\n )}\n </div>\n </div>\n );\n}\n"
|
|
70
65
|
},
|
|
71
66
|
{
|
|
72
67
|
"path": "module.meta.json",
|
|
73
|
-
"content": "{\n \"id\": \"season\",\n \"type\": \"feature\",\n \"
|
|
68
|
+
"content": "{\n \"id\": \"season\",\n \"type\": \"feature\",\n \"summary\": \"Seasons for any game: the active season with its timer, a status bar, a tier ladder with a reward each, claim all and a VIP second prize.\",\n \"description\": \"A ready season screen (Unity Core/Window MilestoneRewards) for ANY game, built on the @idosgames/react/ui kit and client.season. Every season chain of the title is a tab showing its active season: name and timer, the player's tier, the status bar toward the next tier and the tier ladder. Each tier pays its reward once ('Claim all' takes every reached tier in one call); a tier with premium tiers pays a VIP second prize when the pass is active at the moment of the claim. 'Get VIP' opens the VIP system (ctx.features.open('premium', { premiumID })) and shows only when it is installed and has a pass. The server decides the tier — including status tokens that arrive as rewards of other systems. Registers the 'season' feature; its badge counts tier rewards waiting and it reloads when status tokens are granted. Status comes from the game: a chain open to clients takes client.season.grantStatusTokens (the mini-game module grants status for every round). Ships title-seed.json: two alternating 28-day seasons with five tiers and a VIP prize on each.\",\n \"provides\": [\n \"season screen with a tab per chain\",\n \"season timer and status bar\",\n \"tier ladder with rewards\",\n \"claim all reached tiers\",\n \"VIP second prize on tiers\",\n \"hand-off to the VIP system\",\n \"season badge\",\n \"demo seasons\"\n ],\n \"tags\": [\n \"season\",\n \"battle-pass\",\n \"tiers\",\n \"progression\",\n \"live-ops\",\n \"vip\",\n \"ui\",\n \"screen\"\n ],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.0\"\n}\n"
|
|
74
69
|
}
|
|
75
70
|
]
|
|
76
71
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
},
|
|
8
8
|
"catalog": {
|
|
9
9
|
"type": "feature",
|
|
10
|
-
"inBase": true,
|
|
11
10
|
"summary": "Shop screen for any game: the title's storefront with stores as tabs, offer cards, purchase confirmation and the reward animation.",
|
|
12
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.",
|
|
13
12
|
"provides": [
|
|
@@ -37,12 +36,12 @@
|
|
|
37
36
|
"version": "0.1.0"
|
|
38
37
|
},
|
|
39
38
|
"dependencies": {
|
|
40
|
-
"@idosgames/core": "0.19.
|
|
41
|
-
"@idosgames/module-sdk": "0.
|
|
39
|
+
"@idosgames/core": "0.19.1",
|
|
40
|
+
"@idosgames/module-sdk": "0.7.0",
|
|
42
41
|
"@idosgames/react": "0.3.0",
|
|
43
42
|
"react": "19.2.7"
|
|
44
43
|
},
|
|
45
|
-
"contentHash": "
|
|
44
|
+
"contentHash": "dc935a7e6f6a19abc3a8f03b41d721727f70221ff1c3e3a4debb35dc351a7bcb",
|
|
46
45
|
"files": [
|
|
47
46
|
{
|
|
48
47
|
"path": "i18n.ts",
|
|
@@ -66,7 +65,7 @@
|
|
|
66
65
|
},
|
|
67
66
|
{
|
|
68
67
|
"path": "module.meta.json",
|
|
69
|
-
"content": "{\n \"id\": \"store\",\n \"type\": \"feature\",\n \"
|
|
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"
|
|
70
69
|
}
|
|
71
70
|
]
|
|
72
71
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
},
|
|
8
8
|
"catalog": {
|
|
9
9
|
"type": "feature",
|
|
10
|
-
"inBase": true,
|
|
11
10
|
"summary": "Onboarding for any game: hint bubbles next to marked elements, step by step, skip, a completion reward and a replay list.",
|
|
12
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.",
|
|
13
12
|
"provides": [
|
|
@@ -33,12 +32,12 @@
|
|
|
33
32
|
"version": "0.1.0"
|
|
34
33
|
},
|
|
35
34
|
"dependencies": {
|
|
36
|
-
"@idosgames/core": "0.19.
|
|
37
|
-
"@idosgames/module-sdk": "0.
|
|
35
|
+
"@idosgames/core": "0.19.1",
|
|
36
|
+
"@idosgames/module-sdk": "0.7.0",
|
|
38
37
|
"@idosgames/react": "0.3.0",
|
|
39
38
|
"react": "19.2.7"
|
|
40
39
|
},
|
|
41
|
-
"contentHash": "
|
|
40
|
+
"contentHash": "e5cb4e931ff39a7eb3f4cbc32bddc836d79eb631bcd14a6fe6e567df88079718",
|
|
42
41
|
"files": [
|
|
43
42
|
{
|
|
44
43
|
"path": "i18n.ts",
|
|
@@ -62,7 +61,7 @@
|
|
|
62
61
|
},
|
|
63
62
|
{
|
|
64
63
|
"path": "module.meta.json",
|
|
65
|
-
"content": "{\n \"id\": \"tutorials\",\n \"type\": \"feature\",\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.0\"\n}\n"
|
|
66
65
|
}
|
|
67
66
|
]
|
|
68
67
|
}
|