@idosgames/mcp 0.1.46 → 0.1.47
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 +1 -1
- package/package.json +1 -1
- package/registry/host.json +1 -1
- package/registry/index.json +159 -149
- package/registry/modules/ai-media-app.json +77 -0
- package/registry/modules/ai.json +3 -3
- package/registry/modules/board-game.json +3 -3
- package/registry/modules/boosts.json +3 -3
- package/registry/modules/character.json +3 -3
- package/registry/modules/chat.json +3 -3
- package/registry/modules/collections.json +3 -3
- package/registry/modules/coop-events.json +3 -3
- package/registry/modules/craft.json +3 -3
- package/registry/modules/deals.json +3 -3
- package/registry/modules/events.json +6 -6
- package/registry/modules/friends.json +3 -3
- package/registry/modules/hero-run.json +3 -3
- package/registry/modules/idle-rpg.json +3 -3
- package/registry/modules/inventory.json +3 -3
- package/registry/modules/invite.json +3 -3
- package/registry/modules/kit-game.json +17 -11
- package/registry/modules/leaderboard.json +3 -3
- package/registry/modules/lootboxes.json +3 -3
- package/registry/modules/mailbox.json +3 -3
- package/registry/modules/marketplace.json +3 -3
- package/registry/modules/multiplayer.json +3 -3
- package/registry/modules/premium.json +3 -3
- package/registry/modules/profile.json +3 -3
- package/registry/modules/pvp.json +3 -3
- package/registry/modules/quests.json +3 -3
- package/registry/modules/rewards.json +3 -3
- package/registry/modules/season.json +6 -6
- package/registry/modules/store.json +3 -3
- package/registry/modules/tutorials.json +6 -6
- package/registry/modules/video-studio.json +3 -3
- package/registry/modules/voxelcraft.json +3 -3
- package/registry/modules/workshop.json +3 -3
- package/registry/skills/ai-generation-system.json +1 -1
- package/registry/skills/idosgames-compose-modules.json +1 -1
- package/registry/skills/idosgames-getting-started.json +2 -2
- package/registry/skills/idosgames-module-contract.json +2 -2
- package/registry/modules/mini-game.json +0 -69
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
"palette, texts and parameters from the spec",
|
|
20
20
|
"deterministic runs from the spec's seed",
|
|
21
21
|
"round flow: tap to start, result screen, play again",
|
|
22
|
-
"reward window after a round: claim, x3 for a rewarded ad (in the app), x5 for VIP (server-side multiplier)"
|
|
22
|
+
"reward window after a round: claim, x3 for a rewarded ad (in the app), x5 for VIP (server-side multiplier)",
|
|
23
|
+
"score submitted to the leaderboard (a board that takes a game score)",
|
|
24
|
+
"rounds counted by quests, events (CustomAction play_round) and the season"
|
|
23
25
|
],
|
|
24
26
|
"tags": [
|
|
25
27
|
"constructor",
|
|
@@ -38,14 +40,14 @@
|
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
42
|
"@idosgames/characters": "0.1.3",
|
|
41
|
-
"@idosgames/core": "0.26.
|
|
42
|
-
"@idosgames/game-kit": "0.1.
|
|
43
|
-
"@idosgames/module-sdk": "0.13.
|
|
44
|
-
"@idosgames/react": "0.7.
|
|
43
|
+
"@idosgames/core": "0.26.2",
|
|
44
|
+
"@idosgames/game-kit": "0.1.1",
|
|
45
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
46
|
+
"@idosgames/react": "0.7.2",
|
|
45
47
|
"phaser": "4.2.1",
|
|
46
48
|
"react": "19.2.7"
|
|
47
49
|
},
|
|
48
|
-
"contentHash": "
|
|
50
|
+
"contentHash": "fba64c4563247b03567e105a4b16e42d611100a6dfbd1c57494d5bb496aa5040",
|
|
49
51
|
"files": [
|
|
50
52
|
{
|
|
51
53
|
"path": "GameMenu.tsx",
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
},
|
|
54
56
|
{
|
|
55
57
|
"path": "i18n.ts",
|
|
56
|
-
"content": "import { makeT } from \"@idosgames/react/ui\";\n\n// Strings of the runtime itself. The game's own words (its title, hints, resource names) come from\n// the spec's texts.\n\nconst EN = {\n title: \"Game\",\n tapToPlay: \"Tap to play\",\n score: \"Score\",\n best: \"Best\",\n coins: \"Coins\",\n lives: \"Lives\",\n gameOver: \"Round over\",\n won: \"Victory!\",\n lost: \"Defeat\",\n playAgain: \"Play again\",\n continue: \"Continue\",\n home: \"Upgrades\",\n menu: \"Menu\",\n newBest: \"New best!\",\n specFallback: \"This title has no game spec yet: playing the demo.\",\n} as const;\n\nexport type StringKey = keyof typeof EN;\n\nexport const t = makeT<StringKey>(EN, {\n title: \"Игра\",\n tapToPlay: \"Нажмите, чтобы играть\",\n score: \"Очки\",\n best: \"Рекорд\",\n coins: \"Монеты\",\n lives: \"Жизни\",\n gameOver: \"Раунд окончен\",\n won: \"Победа!\",\n lost: \"Поражение\",\n playAgain: \"Ещё раз\",\n continue: \"Продолжить\",\n home: \"Прокачка\",\n menu: \"Меню\",\n newBest: \"Новый рекорд!\",\n specFallback: \"У этого тайтла ещё нет спеки игры: играет демо.\",\n});\n"
|
|
58
|
+
"content": "import { makeT } from \"@idosgames/react/ui\";\n\n// Strings of the runtime itself. The game's own words (its title, hints, resource names) come from\n// the spec's texts.\n\nconst EN = {\n title: \"Game\",\n tapToPlay: \"Tap to play\",\n score: \"Score\",\n best: \"Best\",\n coins: \"Coins\",\n lives: \"Lives\",\n gameOver: \"Round over\",\n won: \"Victory!\",\n lost: \"Defeat\",\n playAgain: \"Play again\",\n continue: \"Continue\",\n home: \"Upgrades\",\n menu: \"Menu\",\n newBest: \"New best!\",\n seasonStatus: \"season status\",\n specFallback: \"This title has no game spec yet: playing the demo.\",\n} as const;\n\nexport type StringKey = keyof typeof EN;\n\nexport const t = makeT<StringKey>(EN, {\n title: \"Игра\",\n tapToPlay: \"Нажмите, чтобы играть\",\n score: \"Очки\",\n best: \"Рекорд\",\n coins: \"Монеты\",\n lives: \"Жизни\",\n gameOver: \"Раунд окончен\",\n won: \"Победа!\",\n lost: \"Поражение\",\n playAgain: \"Ещё раз\",\n continue: \"Продолжить\",\n home: \"Прокачка\",\n menu: \"Меню\",\n newBest: \"Новый рекорд!\",\n seasonStatus: \"статуса сезона\",\n specFallback: \"У этого тайтла ещё нет спеки игры: играет демо.\",\n});\n"
|
|
57
59
|
},
|
|
58
60
|
{
|
|
59
61
|
"path": "index.ts",
|
|
@@ -63,6 +65,10 @@
|
|
|
63
65
|
"path": "KitPanel.tsx",
|
|
64
66
|
"content": "import { useEffect, useState, useSyncExternalStore, type ComponentType, type ReactNode } from \"react\";\nimport type { FeatureRegistry } from \"@idosgames/module-sdk\";\nimport { useUserState } from \"@idosgames/react\";\nimport { useLanguage, v } from \"@idosgames/react/ui\";\nimport type { KitRuntime } from \"./runtime\";\nimport { t } from \"./i18n\";\nimport { GameMenu } from \"./GameMenu\";\nimport { chipStyle, GameTopBar, type BarBalance } from \"./TopBar\";\nimport { RoundResult } from \"./RoundResult\";\nimport { RunOverlay } from \"./survivors/RunOverlay\";\nimport { SurvivorsLobby } from \"./survivors/Lobby\";\nimport { RunnerLobby } from \"./runner/Lobby\";\nimport { PlatformerLobby } from \"./platformer/Lobby\";\nimport { TowerDefenseLobby } from \"./towerDefense/Lobby\";\nimport { Match3Lobby } from \"./match3/Lobby\";\nimport { MergeLobby } from \"./merge/Lobby\";\nimport { TycoonLobby } from \"./tycoon/Lobby\";\nimport { TouchPad } from \"./TouchPad\";\nimport { ReviveDialog } from \"./meta/ReviveDialog\";\n\nexport interface KitPanelOptions {\n /** The app's systems: all of them are behind the one menu button, opened over the game. */\n readonly features: FeatureRegistry;\n}\n\n// The game's whole UI, drawn as part of the game (owner, 07.11.2026: minimal, native, no separate\n// bars): ONE menu button top-left (every system of the app, over the game: ./GameMenu.tsx), the title's currencies top-right in the same chips, the score in\n// the middle while playing; the title and \"tap to play\" before a round, the result after. The overlay\n// lets taps through to the canvas except on its buttons. It re-renders only when a HUD value or a\n// balance changed, never per frame.\n/** The words of a genre's revive offer. */\nexport const reviveTextOf = (presetId: string) => (presetId === \"survivors\" ? \"reviveText\" : presetId === \"runner\" ? \"reviveRun\" : presetId === \"tower-defense\" ? \"reviveTd\" : presetId === \"match3\" ? \"reviveM3\" : presetId === \"merge\" ? \"reviveMg\" : presetId === \"tycoon\" ? \"reviveTy\" : \"reviveLevel\");\n\nexport function makeKitPanel(runtime: KitRuntime, options: KitPanelOptions): ComponentType {\n return function KitPanel(): ReactNode {\n const hud = useSyncExternalStore(runtime.subscribe, runtime.snapshot, runtime.snapshot);\n const state = useUserState();\n const [menuOpen, setMenuOpen] = useState(false);\n const features = useSyncExternalStore(\n options.features.subscribe,\n options.features.list,\n options.features.list,\n );\n const badges = features.reduce((n, f) => n + (f.available && f.badge > 0 ? 1 : 0), 0);\n // The field freezes while the menu is over it.\n useEffect(() => {\n runtime.setPaused(menuOpen);\n return () => runtime.setPaused(false);\n }, [menuOpen]);\n const language = useLanguage();\n const title = runtime.text(\"title\", language, t(\"title\"));\n const hint = runtime.text(\"hint\", language, \"\");\n // The spec's currencies that are real title currencies (`ref`), in the spec's order.\n const currencies = (runtime.spec?.resolved.spec.resources ?? []).filter(\n (r) => r.kind === \"currency\" && r.ref,\n );\n\n // Balances of the bar: the spec's real currencies, and the gems of the meta (a revive, a hero) beside them.\n const hardID = runtime.meta?.snapshot().hardID;\n const ids = [...currencies.map((r) => r.ref!), ...(hardID && !currencies.some((r) => r.ref === hardID) ? [hardID] : [])];\n const balances: BarBalance[] = ids.map((id, i) => ({\n key: id,\n currencyID: id,\n amount: Number(state?.InventoryV2?.VirtualCurrencies?.[id]?.Amount ?? 0),\n ...(i === 0 && hud.state === \"playing\" ? { plus: hud.coins } : {}),\n }));\n // a game that draws its own hearts on the field (the meta's genres) does not repeat them here\n const center = hud.state === \"playing\" && (\n <>\n <div style={chipStyle}>\n <span style={{ fontSize: 20 }}>{hud.score}</span>\n </div>\n {hud.lives > 1 && !runtime.meta && <div style={chipStyle}>{`♥ ${hud.lives}`}</div>}\n </>\n );\n\n return (\n <div\n style={{\n position: \"absolute\",\n inset: 0,\n pointerEvents: \"none\",\n fontFamily: v.font,\n color: \"#fff\",\n textShadow: \"0 1px 3px rgba(0,0,0,0.5)\",\n }}\n >\n <GameTopBar onMenu={() => setMenuOpen(true)} badges={badges} center={center} balances={balances} />\n\n {/* before the round: the title up top; the start is the big button on the field with a hand\n tapping it (the game draws it: no \"tap to play\" words needed) */}\n {/* a game with a meta: its home between rounds (level, upgrades, heroes), its cards and the revive */}\n {runtime.meta && hud.state === \"ready\" && runtime.presetId === \"survivors\" && (\n <SurvivorsLobby meta={runtime.meta} look={runtime.artLook()} baseSeconds={Number(runtime.world?.vars[\"survivors_base\"] ?? 300)} language={language} />\n )}\n {runtime.meta && hud.state === \"ready\" && runtime.presetId === \"runner\" && (\n <RunnerLobby meta={runtime.meta} look={runtime.artLook()} lengthScale={Number(runtime.world?.vars[\"runner_length_scale\"] ?? 1)} language={language} />\n )}\n {runtime.meta && hud.state === \"ready\" && runtime.presetId === \"platformer\" && <PlatformerLobby meta={runtime.meta} look={runtime.artLook()} language={language} />}\n {runtime.meta && hud.state === \"ready\" && runtime.presetId === \"tower-defense\" && <TowerDefenseLobby meta={runtime.meta} look={runtime.artLook()} language={language} />}\n {runtime.meta && hud.state === \"ready\" && runtime.presetId === \"match3\" && <Match3Lobby meta={runtime.meta} look={runtime.artLook()} language={language} />}\n {runtime.meta && hud.state === \"ready\" && runtime.presetId === \"merge\" && <MergeLobby meta={runtime.meta} look={runtime.artLook()} language={language} />}\n {runtime.meta && hud.state === \"ready\" && runtime.presetId === \"tycoon\" && <TycoonLobby meta={runtime.meta} look={runtime.artLook()} language={language} />}\n {runtime.presetId === \"platformer\" && hud.state === \"playing\" && runtime.world && (\n <TouchPad world={runtime.world} dash={String(runtime.world.vars[\"abilities\"] ?? \"\").includes(\"dash\")} />\n )}\n {runtime.meta && runtime.presetId === \"survivors\" && <RunOverlay world={runtime.world} meta={runtime.meta} look={runtime.artLook()} />}\n {runtime.meta && <ReviveDialog world={runtime.world} meta={runtime.meta} text={reviveTextOf(runtime.presetId)} />}\n\n {hud.state === \"ready\" && !runtime.meta && (\n <div style={{ ...centered, justifyContent: \"flex-start\", paddingTop: \"18%\" }}>\n <div style={{ fontSize: 36, fontWeight: 800 }}>{title}</div>\n {hint && <div style={{ opacity: 0.85, maxWidth: 320 }}>{hint}</div>}\n {hud.best > 0 && <div style={{ opacity: 0.8, fontWeight: 700 }}>{`${t(\"best\")}: ${hud.best}`}</div>}\n </div>\n )}\n\n {hud.state === \"ended\" && <RoundResult runtime={runtime} hud={hud} features={options.features} />}\n <div style={{ pointerEvents: \"auto\" }}>\n <GameMenu features={options.features} open={menuOpen} setOpen={setMenuOpen} />\n </div>\n </div>\n );\n };\n}\n\nconst centered = {\n position: \"absolute\",\n inset: 0,\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n justifyContent: \"center\",\n gap: 8,\n textAlign: \"center\",\n padding: 24,\n} as const;\n"
|
|
65
67
|
},
|
|
68
|
+
{
|
|
69
|
+
"path": "liveops.ts",
|
|
70
|
+
"content": "// A round's share of the title's live-ops (owner, 09.11.2026: kit-game took the place of the removed mini-game, and\n// with it what a round fed): the score goes to the title's leaderboard, a round pays tokens of every active event\n// that counts rounds (a client-safe CustomAction) and status in every season chain open to clients. Everything is\n// found in the title config, so the game needs neither the events nor the season module installed. Quests count\n// rounds already (meta.ts, `games_played`). Best-effort: a refusal (a cap reached, no active season) grants nothing.\n\nimport type { IDosGamesClient } from \"@idosgames/core\";\nimport { configSection } from \"@idosgames/react/ui\";\n\n/** Leaderboard a round submits to when the title has it; otherwise the first enabled one that takes a game score. */\nexport const PREFERRED_LEADERBOARD = \"weekly_score\";\n/** The CustomAction a round reports to timed events (the same name the removed mini-game used: old configs keep working). */\nexport const PLAY_ROUND_ACTION = \"play_round\";\n/** Status a round grants to every season chain open to clients. */\nexport const ROUND_STATUS = 10;\n\nexport interface LeaderboardSection {\n Definitions?: Record<\n string,\n {\n LeaderboardID?: string;\n DisplayName?: unknown;\n IsEnabled?: boolean | null;\n ScoreSources?: Array<{ SourceType?: string | null } | null> | null;\n } | null\n > | null;\n}\n\n/**\n * The leaderboard a round plays for: the preferred id if enabled, else the first enabled one (by id). Only a board\n * with a `ClientScore` source: the server refuses a submitted score into any other one.\n */\nexport function pickLeaderboard(section: LeaderboardSection | undefined | null): string | null {\n const ids = Object.entries(section?.Definitions ?? {})\n .filter(([, d]) => !!d && d.IsEnabled !== false && (d.ScoreSources ?? []).some((s) => s?.SourceType === \"ClientScore\"))\n .map(([id, d]) => d?.LeaderboardID ?? id)\n .sort((a, b) => a.localeCompare(b));\n return ids.includes(PREFERRED_LEADERBOARD) ? PREFERRED_LEADERBOARD : (ids[0] ?? null);\n}\n\ntype EventContentView = {\n TokenSources?: Array<{ SourceType?: string | null; Params?: Record<string, string> | null } | null> | null;\n};\n\nexport interface TimedEventSection {\n Definitions?: Record<\n string,\n { Content?: EventContentView | null; Events?: Array<{ Content?: EventContentView | null } | null> | null } | null\n > | null;\n}\n\nconst countsRounds = (content: EventContentView | null | undefined): boolean =>\n (content?.TokenSources ?? []).some((s) => s?.SourceType === \"CustomAction\" && s.Params?.ActionName === PLAY_ROUND_ACTION);\n\n/** Events (by id) whose token sources pay for a round: their own content or any phase of a chain. */\nexport function eventsCountingRounds(section: TimedEventSection | undefined | null): string[] {\n return Object.entries(section?.Definitions ?? {})\n .filter(([, d]) => !!d && (countsRounds(d.Content) || (d.Events ?? []).some((p) => countsRounds(p?.Content))))\n .map(([id]) => id);\n}\n\nexport interface SeasonSection {\n Chains?: Record<string, { GrantTokensAccessMode?: string | null } | null> | null;\n}\n\n/** Season chains the client may grant status to: only an explicit \"Both\" / \"ClientOnly\" (the engine's default is server only). */\nexport function seasonChainsForRounds(section: SeasonSection | undefined | null): string[] {\n return Object.entries(section?.Chains ?? {})\n .filter(([, c]) => c?.GrantTokensAccessMode === \"Both\" || c?.GrantTokensAccessMode === \"ClientOnly\")\n .map(([id]) => id);\n}\n\n/** What a round granted, for the result window: event tokens (with the event's token name) and season status. */\nexport interface RoundBonus {\n readonly kind: \"event\" | \"season\";\n readonly amount: number;\n /** The name of the event's token (a title text the catalog localizes), null for season status. */\n readonly label: string | null;\n}\n\n/** Sends a finished round to the leaderboard, the events and the seasons of the title. Never throws. */\nexport async function reportRound(client: IDosGamesClient, score: number): Promise<RoundBonus[]> {\n const out: RoundBonus[] = [];\n if (score <= 0) return out;\n try {\n const board = pickLeaderboard(configSection<LeaderboardSection>(client, \"Leaderboard\"));\n if (board) await client.leaderboard.submitScore(board, score);\n\n const eventIDs = eventsCountingRounds(configSection<TimedEventSection>(client, \"TimedEvent\"));\n if (eventIDs.length > 0) {\n const active = await client.timedEvent.getActiveEvents();\n for (const e of active.ok ? (active.data.ActiveEvents ?? []) : []) {\n if (!e.TimedEventID || !eventIDs.includes(e.TimedEventID) || e.CanEarn === false || !e.Type) continue;\n const res = await client.timedEvent.grantTokens(e.Type, e.TimedEventID, \"CustomAction\", undefined, undefined, {\n ActionName: PLAY_ROUND_ACTION,\n });\n const op = res.ok\n ? (res.data.Grant?.Standard?.EventTokens?.[0] as { Applied?: number; Amount?: number } | undefined)\n : undefined;\n const amount = Number(op?.Applied ?? op?.Amount ?? 0);\n if (amount > 0)\n out.push({ kind: \"event\", amount, label: e.Content?.Token?.DisplayName ?? e.Content?.DisplayName ?? e.TimedEventID });\n }\n }\n\n for (const chain of seasonChainsForRounds(configSection<SeasonSection>(client, \"Season\"))) {\n const res = await client.season.grantStatusTokens(chain, ROUND_STATUS);\n const amount = res.ok ? Number(res.data.AmountGranted ?? 0) : 0;\n if (amount > 0) out.push({ kind: \"season\", amount, label: null });\n }\n } catch {\n // live-ops of a round never break the round\n }\n return out;\n}\n"
|
|
71
|
+
},
|
|
66
72
|
{
|
|
67
73
|
"path": "match3/Lobby.tsx",
|
|
68
74
|
"content": "import { useSyncExternalStore, type ReactNode } from \"react\";\r\nimport { M3_ELEMENTS, M3_HEROES, M3_LEVELS, M3_OBSTACLES, M3_PIECES, m3EnemyOf, m3LevelOf, m3SkillOf, m3StarsToEnter, type MetaHeroDef } from \"@idosgames/game-kit\";\r\nimport { Icon, v } from \"@idosgames/react/ui\";\r\nimport { st, stAny } from \"../meta/i18n\";\r\nimport { starsTotal, type GameMeta } from \"../meta/meta\";\r\nimport { ArtIcon, type ArtLookProps } from \"../meta/ArtIcon\";\r\nimport { GameLobby } from \"../meta/Lobby\";\r\nimport { Stars } from \"../runner/Lobby\";\r\n\r\n/**\r\n * Match-3's home between levels: the level (its world, its stars), what it is — a battle (the foes, the boss) or\r\n * goals (what to collect or clear, the moves) — the next world's gate, the first-clear reward; the hero's upgrades\r\n * and heroes below (each hero: an element and a skill).\r\n */\r\nexport function Match3Lobby({ meta, look, language }: { meta: GameMeta; look: ArtLookProps; language: string }): ReactNode {\r\n const p = useSyncExternalStore(meta.subscribe, meta.snapshot, meta.snapshot).progress;\r\n const level = m3LevelOf(p.chapter);\r\n const cleared = p.cleared.includes(p.chapter);\r\n const stars = p.stars[String(p.chapter)] ?? 0;\r\n const total = starsTotal(p);\r\n const gate = p.chapter === p.unlocked && cleared && p.chapter < M3_LEVELS ? m3StarsToEnter(p.chapter + 1) : 0;\r\n const def = level.def;\r\n const card = (\r\n <>\r\n <div style={{ display: \"flex\", gap: 10, alignItems: \"center\", fontWeight: 900, fontSize: 18 }}>\r\n <span>{`${st(\"world\")} ${level.world}-${level.index}`}</span>\r\n {def.kind === \"battle\" ? (\r\n <span style={{ display: \"inline-flex\", gap: 4, alignItems: \"center\", color: v.red }}>\r\n <Icon glyph=\"swords\" size={16} /> {level.boss ? st(\"boss\") : st(\"battle\")}\r\n </span>\r\n ) : (\r\n <span style={{ fontSize: 14, opacity: 0.9 }}>{`${def.moves} ${st(\"movesN\")}`}</span>\r\n )}\r\n <Stars n={stars} size={20} />\r\n </div>\r\n <div style={{ fontSize: 12, display: \"flex\", gap: 10, flexWrap: \"wrap\", justifyContent: \"center\", alignItems: \"center\" }}>\r\n {def.kind === \"battle\"\r\n ? def.enemies.map((id, i) => {\r\n const e = m3EnemyOf(id);\r\n const el = e ? M3_ELEMENTS[e.element] : undefined;\r\n return (\r\n <span key={`${id}${i}`} style={{ display: \"inline-flex\", gap: 4, alignItems: \"center\", fontWeight: 800 }}>\r\n {el && <span style={{ width: 10, height: 10, borderRadius: 5, background: look.palette[el.color] ?? \"#888\", display: \"inline-block\" }} />}\r\n {stAny(`m.${id}`)}\r\n </span>\r\n );\r\n })\r\n : def.goals.map((g, i) => {\r\n const art = g.kind === \"color\" ? M3_PIECES.find((x) => x.index === g.color)?.art : g.kind === \"clear\" ? M3_OBSTACLES[g.obstacle]?.art : g.kind === \"drops\" ? \"m3_crate\" : \"coin\";\r\n const color = g.kind === \"color\" ? M3_PIECES.find((x) => x.index === g.color)?.color : undefined;\r\n const label = g.kind === \"color\" ? `× ${g.count}` : g.kind === \"clear\" ? stAny(`g.${g.obstacle}`) : g.kind === \"drops\" ? `${stAny(\"g.drops\")} × ${g.count}` : String(g.value);\r\n return (\r\n <span key={i} style={{ display: \"inline-flex\", gap: 4, alignItems: \"center\", fontWeight: 800 }}>\r\n {art && <ArtIcon id={art} {...(color ? { color } : {})} size={20} look={look} />}\r\n {label}\r\n </span>\r\n );\r\n })}\r\n </div>\r\n <div style={{ fontSize: 12, opacity: 0.9, display: \"flex\", gap: 10, flexWrap: \"wrap\", justifyContent: \"center\" }}>\r\n <span>{`${st(\"starsAll\")}: ★ ${total}`}</span>\r\n {gate > total && <span style={{ color: v.gold, fontWeight: 800 }}>{`${st(\"world\")} ${level.world + 1}: ${st(\"needStars\")} ★ ${gate}`}</span>}\r\n {!cleared && (\r\n <span style={{ display: \"inline-flex\", gap: 4, alignItems: \"center\" }}>\r\n {st(\"firstClear\")}: <Icon glyph=\"coin\" size={13} /> {level.firstClearGold}\r\n </span>\r\n )}\r\n </div>\r\n </>\r\n );\r\n const heroInfo = (m: MetaHeroDef) => {\r\n const h = M3_HEROES.find((x) => x.id === m.id) ?? M3_HEROES[0]!;\r\n const sk = m3SkillOf(h.skill);\r\n return (\r\n <>\r\n <div style={{ fontSize: 12, opacity: 0.8 }}>{`${st(\"element\")}: ${stAny(`el.${h.element}`)}`}</div>\r\n <div style={{ fontSize: 13, fontWeight: 800, minHeight: 34, display: \"flex\", gap: 4, alignItems: \"center\", justifyContent: \"center\" }}>\r\n {sk && <ArtIcon id={sk.art} size={18} look={look} />}\r\n {stAny(`sk.${h.skill}`)}\r\n </div>\r\n </>\r\n );\r\n };\r\n return <GameLobby meta={meta} look={look} language={language} label=\"levelN\" card={card} heroInfo={heroInfo} />;\r\n}\r\n"
|
|
@@ -109,11 +115,11 @@
|
|
|
109
115
|
},
|
|
110
116
|
{
|
|
111
117
|
"path": "RoundResult.tsx",
|
|
112
|
-
"content": "import { useState, type ReactNode } from \"react\";\nimport type { FeatureRegistry } from \"@idosgames/module-sdk\";\nimport { useRewardWindow } from \"@idosgames/react\";\nimport { grantedBy, useCelebrate } from \"@idosgames/react/ui\";\nimport type { Hud, KitRuntime } from \"./runtime\";\nimport { outcomeOf, RoundResultView } from \"./RoundResultView\";\nimport { resultDetails } from \"./meta/details\";\n\n// The result screen of a round (owner, 07.11.2026): won or lost, the score, then the reward window — \"Claim N\",\n// \"x3 for an ad\" (only in the app), \"x5 VIP\" — and only after it \"Play again\", so the round's coins are never left\n// behind. The coins reach the balance only through the server (`client.reward.claimRewardWindow`): the round's\n// count is the game's word, the cap and the multiplier are the server's. A title without the window (its config\n// has no `Reward.Windows[spec.rewards.window]`) shows the result and \"Play again\" as before. The window itself is\n// RoundResultView (the constructor's test page draws the same one).\nexport function RoundResult({\n runtime,\n hud,\n features,\n}: {\n runtime: KitRuntime;\n hud: Hud;\n features: FeatureRegistry;\n}): ReactNode {\n const result = hud.result;\n const windowID = runtime.spec?.resolved.rewards.window ?? \"round\";\n const rw = useRewardWindow(windowID);\n const celebrate = useCelebrate();\n const [claimedRound, setClaimedRound] = useState<string | null>(null);\n\n const coins = result?.coins ?? hud.coins;\n const roundKey = result?.roundKey ?? \"\";\n const offerWindow = !!rw.window && coins > 0 && claimedRound !== roundKey;\n\n const claim = async (choice: \"None\" | \"Ad\" | \"Vip\") => {\n // One key per round whatever the choice: a retried claim (lost answer) pays once.\n const data = await rw.claim(coins, choice, roundKey || undefined);\n if (!data) return;\n setClaimedRound(roundKey);\n celebrate(grantedBy(data));\n };\n\n return (\n <RoundResultView\n outcome={outcomeOf(result?.reason)}\n score={result?.score ?? hud.score}\n best={result?.best ?? hud.best}\n newBest={!!result?.newBest}\n reward={\n offerWindow && rw.window\n ? {\n amount: Math.min(coins, rw.window.maxBaseAmount),\n currencyID: rw.window.currencyID,\n adMultiplier: rw.window.adMultiplier,\n adAvailable: rw.adAvailable,\n vipMultiplier: rw.window.vipMultiplier,\n isVip: rw.isVip,\n busy: rw.busy,\n message: rw.message,\n // Without VIP the button opens the VIP offer over the game (the premium system's screen).\n ...(features.get(\"premium\")?.available ? { onVipOffer: () => features.open(\"premium\") } : {}),\n }\n : null\n }\n onClaim={(choice) => void claim(choice)}\n onPlayAgain={() => runtime.playAgain()}\n {...(runtime.meta ? { onHome: () => runtime.home() } : {})}\n {...(result?.stats && runtime.meta ? { details: resultDetails(runtime.presetId, result.stats) } : {})}\n />\n );\n}\n"
|
|
118
|
+
"content": "import { useState, type ReactNode } from \"react\";\nimport type { FeatureRegistry } from \"@idosgames/module-sdk\";\nimport { useRewardWindow } from \"@idosgames/react\";\nimport { formatAmount, grantedBy, useCatalog, useCelebrate } from \"@idosgames/react/ui\";\nimport type { Hud, KitRuntime } from \"./runtime\";\nimport { outcomeOf, RoundResultView } from \"./RoundResultView\";\nimport { resultDetails } from \"./meta/details\";\nimport { t } from \"./i18n\";\n\n// The result screen of a round (owner, 07.11.2026): won or lost, the score, then the reward window — \"Claim N\",\n// \"x3 for an ad\" (only in the app), \"x5 VIP\" — and only after it \"Play again\", so the round's coins are never left\n// behind. The coins reach the balance only through the server (`client.reward.claimRewardWindow`): the round's\n// count is the game's word, the cap and the multiplier are the server's. A title without the window (its config\n// has no `Reward.Windows[spec.rewards.window]`) shows the result and \"Play again\" as before. The window itself is\n// RoundResultView (the constructor's test page draws the same one).\nexport function RoundResult({\n runtime,\n hud,\n features,\n}: {\n runtime: KitRuntime;\n hud: Hud;\n features: FeatureRegistry;\n}): ReactNode {\n const result = hud.result;\n const windowID = runtime.spec?.resolved.rewards.window ?? \"round\";\n const rw = useRewardWindow(windowID);\n const celebrate = useCelebrate();\n const [claimedRound, setClaimedRound] = useState<string | null>(null);\n const catalog = useCatalog();\n // The round's grants in events and seasons (liveops.ts): \"+30 Candy\", \"+10 season status\".\n const bonus = (result?.bonus ?? []).map((b) =>\n b.kind === \"season\"\n ? `+${formatAmount(b.amount)} ${t(\"seasonStatus\")}`\n : `+${formatAmount(b.amount)} ${catalog.localize(b.label)}`.trim(),\n );\n\n const coins = result?.coins ?? hud.coins;\n const roundKey = result?.roundKey ?? \"\";\n const offerWindow = !!rw.window && coins > 0 && claimedRound !== roundKey;\n\n const claim = async (choice: \"None\" | \"Ad\" | \"Vip\") => {\n // One key per round whatever the choice: a retried claim (lost answer) pays once.\n const data = await rw.claim(coins, choice, roundKey || undefined);\n if (!data) return;\n setClaimedRound(roundKey);\n celebrate(grantedBy(data));\n };\n\n return (\n <RoundResultView\n outcome={outcomeOf(result?.reason)}\n score={result?.score ?? hud.score}\n best={result?.best ?? hud.best}\n newBest={!!result?.newBest}\n reward={\n offerWindow && rw.window\n ? {\n amount: Math.min(coins, rw.window.maxBaseAmount),\n currencyID: rw.window.currencyID,\n adMultiplier: rw.window.adMultiplier,\n adAvailable: rw.adAvailable,\n vipMultiplier: rw.window.vipMultiplier,\n isVip: rw.isVip,\n busy: rw.busy,\n message: rw.message,\n // Without VIP the button opens the VIP offer over the game (the premium system's screen).\n ...(features.get(\"premium\")?.available ? { onVipOffer: () => features.open(\"premium\") } : {}),\n }\n : null\n }\n onClaim={(choice) => void claim(choice)}\n onPlayAgain={() => runtime.playAgain()}\n {...(runtime.meta ? { onHome: () => runtime.home() } : {})}\n {...(result?.stats && runtime.meta ? { details: resultDetails(runtime.presetId, result.stats) } : {})}\n {...(bonus.length > 0 ? { bonus } : {})}\n />\n );\n}\n"
|
|
113
119
|
},
|
|
114
120
|
{
|
|
115
121
|
"path": "RoundResultView.tsx",
|
|
116
|
-
"content": "import type { ReactNode } from \"react\";\nimport { Button, Icon, Ribbon, RewardWindowPanel, panel, v, type RewardWindowPanelProps } from \"@idosgames/react/ui\";\nimport { t } from \"./i18n\";\n\n/** How a round ended, by the reason its genre gave (`round-over` { reason }). */\nexport type RoundOutcome = \"won\" | \"lost\" | \"over\";\n\nconst WON = new Set([\"cleared\", \"survived\", \"defended\"]);\nconst LOST = new Set([\"crashed\", \"fell\", \"died\", \"overrun\", \"out-of-moves\", \"no-moves\", \"out-of-energy\", \"out-of-time\"]);\n\nexport const outcomeOf = (reason: string | undefined): RoundOutcome =>\n reason && WON.has(reason) ? \"won\" : reason && LOST.has(reason) ? \"lost\" : \"over\";\n\nexport interface RoundResultViewProps {\n outcome: RoundOutcome;\n score: number;\n best: number;\n newBest: boolean;\n /** The reward window of the title (null: none, or the round's coins are already claimed). */\n reward: Omit<RewardWindowPanelProps, \"onClaim\"> | null;\n onClaim: RewardWindowPanelProps[\"onClaim\"];\n onPlayAgain: () => void;\n /** Back to the game's home (its meta: upgrades, chapters): a second button next to \"Play again\". */\n onHome?: () => void;\n /** Numbers of the run under the score (kills, time…). */\n details?: ReadonlyArray<{ label: string; value: string }>;\n}\n\n// The round's result as a window of the game (owner, 07.11.2026): a ribbon says won or lost, the score with the\n// best, then the reward window (\"Claim\", \"x3 for an ad\" in the app, \"x5 VIP\") and only after it \"Play again\", so\n// the round's coins are never left behind. After a win the button says \"Continue\" (owner, 08.11.2026): the meta has\n// already moved to the next level. Pure view: the game (RoundResult) and the constructor's test page\n// (playground kit.html) draw the same window.\nexport function RoundResultView({ outcome, score, best, newBest, reward, onClaim, onPlayAgain, onHome, details }: RoundResultViewProps): ReactNode {\n const ribbon =\n outcome === \"won\" ? (\n <Ribbon tone=\"green\" size={26}>\n {t(\"won\")}\n </Ribbon>\n ) : outcome === \"lost\" ? (\n <Ribbon tone=\"red\" size={26} stars={false}>\n {t(\"lost\")}\n </Ribbon>\n ) : (\n <Ribbon tone=\"blue\" size={26} stars={false}>\n {t(\"gameOver\")}\n </Ribbon>\n );\n return (\n <div style={backdrop}>\n <div style={{ ...panel, ...card }}>\n <div style={{ marginTop: -46, display: \"flex\", justifyContent: \"center\" }}>{ribbon}</div>\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 10, marginTop: 10 }}>\n <Icon glyph=\"star\" size={34} />\n <span style={{ fontSize: 48, fontWeight: 900, lineHeight: 1 }}>{score}</span>\n </div>\n {newBest ? (\n <div className=\"idos-bounce\" style={{ color: v.gold, fontWeight: 800, fontSize: 18 }}>\n {t(\"newBest\")}\n </div>\n ) : (\n <div style={{ opacity: 0.8, fontWeight: 700 }}>{`${t(\"best\")}: ${best}`}</div>\n )}\n {details && details.length > 0 && (\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\", justifyContent: \"center\", marginTop: 4 }}>\n {details.map((d) => (\n <div key={d.label} style={{ background: \"rgba(0,0,0,0.28)\", borderRadius: 10, padding: \"4px 10px\", fontSize: 13 }}>\n <span style={{ opacity: 0.75 }}>{d.label}</span> <b>{d.value}</b>\n </div>\n ))}\n </div>\n )}\n <div style={{ marginTop: 8, width: \"100%\", display: \"grid\", justifyItems: \"center\", gap: 12 }}>\n {reward ? (\n <RewardWindowPanel {...reward} onClaim={onClaim} />\n ) : (\n <div style={{ display: \"flex\", gap: 10, flexWrap: \"wrap\", justifyContent: \"center\" }}>\n {onHome && (\n <Button size=\"lg\" tone=\"blue\" onClick={onHome}>\n {t(\"home\")}\n </Button>\n )}\n <Button size=\"lg\" tone=\"green\" onClick={onPlayAgain}>\n {outcome === \"won\" ? t(\"continue\") : t(\"playAgain\")}\n </Button>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n\nconst backdrop = {\n position: \"absolute\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: 20,\n background: \"rgba(8,6,16,0.55)\",\n pointerEvents: \"auto\",\n animation: \"idos-fade-in .2s both\",\n} as const;\n\nconst card = {\n position: \"relative\",\n width: \"min(360px, 100%)\",\n padding: \"34px 18px 20px\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: 6,\n textAlign: \"center\",\n color: \"#fff\",\n animation: \"idos-pop-in .35s both\",\n} as const;\n"
|
|
122
|
+
"content": "import type { ReactNode } from \"react\";\nimport { Button, Icon, Ribbon, RewardWindowPanel, panel, v, type RewardWindowPanelProps } from \"@idosgames/react/ui\";\nimport { t } from \"./i18n\";\n\n/** How a round ended, by the reason its genre gave (`round-over` { reason }). */\nexport type RoundOutcome = \"won\" | \"lost\" | \"over\";\n\nconst WON = new Set([\"cleared\", \"survived\", \"defended\"]);\nconst LOST = new Set([\"crashed\", \"fell\", \"died\", \"overrun\", \"out-of-moves\", \"no-moves\", \"out-of-energy\", \"out-of-time\"]);\n\nexport const outcomeOf = (reason: string | undefined): RoundOutcome =>\n reason && WON.has(reason) ? \"won\" : reason && LOST.has(reason) ? \"lost\" : \"over\";\n\nexport interface RoundResultViewProps {\n outcome: RoundOutcome;\n score: number;\n best: number;\n newBest: boolean;\n /** The reward window of the title (null: none, or the round's coins are already claimed). */\n reward: Omit<RewardWindowPanelProps, \"onClaim\"> | null;\n onClaim: RewardWindowPanelProps[\"onClaim\"];\n onPlayAgain: () => void;\n /** Back to the game's home (its meta: upgrades, chapters): a second button next to \"Play again\". */\n onHome?: () => void;\n /** Numbers of the run under the score (kills, time…). */\n details?: ReadonlyArray<{ label: string; value: string }>;\n /** What the round granted in the title's events and seasons (\"+30 Candy\", \"+10 season status\"). */\n bonus?: ReadonlyArray<string>;\n}\n\n// The round's result as a window of the game (owner, 07.11.2026): a ribbon says won or lost, the score with the\n// best, then the reward window (\"Claim\", \"x3 for an ad\" in the app, \"x5 VIP\") and only after it \"Play again\", so\n// the round's coins are never left behind. After a win the button says \"Continue\" (owner, 08.11.2026): the meta has\n// already moved to the next level. Pure view: the game (RoundResult) and the constructor's test page\n// (playground kit.html) draw the same window.\nexport function RoundResultView({ outcome, score, best, newBest, reward, onClaim, onPlayAgain, onHome, details, bonus }: RoundResultViewProps): ReactNode {\n const ribbon =\n outcome === \"won\" ? (\n <Ribbon tone=\"green\" size={26}>\n {t(\"won\")}\n </Ribbon>\n ) : outcome === \"lost\" ? (\n <Ribbon tone=\"red\" size={26} stars={false}>\n {t(\"lost\")}\n </Ribbon>\n ) : (\n <Ribbon tone=\"blue\" size={26} stars={false}>\n {t(\"gameOver\")}\n </Ribbon>\n );\n return (\n <div style={backdrop}>\n <div style={{ ...panel, ...card }}>\n <div style={{ marginTop: -46, display: \"flex\", justifyContent: \"center\" }}>{ribbon}</div>\n <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 10, marginTop: 10 }}>\n <Icon glyph=\"star\" size={34} />\n <span style={{ fontSize: 48, fontWeight: 900, lineHeight: 1 }}>{score}</span>\n </div>\n {newBest ? (\n <div className=\"idos-bounce\" style={{ color: v.gold, fontWeight: 800, fontSize: 18 }}>\n {t(\"newBest\")}\n </div>\n ) : (\n <div style={{ opacity: 0.8, fontWeight: 700 }}>{`${t(\"best\")}: ${best}`}</div>\n )}\n {details && details.length > 0 && (\n <div style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\", justifyContent: \"center\", marginTop: 4 }}>\n {details.map((d) => (\n <div key={d.label} style={{ background: \"rgba(0,0,0,0.28)\", borderRadius: 10, padding: \"4px 10px\", fontSize: 13 }}>\n <span style={{ opacity: 0.75 }}>{d.label}</span> <b>{d.value}</b>\n </div>\n ))}\n </div>\n )}\n {bonus && bonus.length > 0 && (\n <div className=\"idos-bounce\" style={{ display: \"flex\", gap: 8, flexWrap: \"wrap\", justifyContent: \"center\", color: v.gold, fontWeight: 800 }}>\n {bonus.map((line) => (\n <span key={line}>{line}</span>\n ))}\n </div>\n )}\n <div style={{ marginTop: 8, width: \"100%\", display: \"grid\", justifyItems: \"center\", gap: 12 }}>\n {reward ? (\n <RewardWindowPanel {...reward} onClaim={onClaim} />\n ) : (\n <div style={{ display: \"flex\", gap: 10, flexWrap: \"wrap\", justifyContent: \"center\" }}>\n {onHome && (\n <Button size=\"lg\" tone=\"blue\" onClick={onHome}>\n {t(\"home\")}\n </Button>\n )}\n <Button size=\"lg\" tone=\"green\" onClick={onPlayAgain}>\n {outcome === \"won\" ? t(\"continue\") : t(\"playAgain\")}\n </Button>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n\nconst backdrop = {\n position: \"absolute\",\n inset: 0,\n display: \"flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: 20,\n background: \"rgba(8,6,16,0.55)\",\n pointerEvents: \"auto\",\n animation: \"idos-fade-in .2s both\",\n} as const;\n\nconst card = {\n position: \"relative\",\n width: \"min(360px, 100%)\",\n padding: \"34px 18px 20px\",\n display: \"flex\",\n flexDirection: \"column\",\n alignItems: \"center\",\n gap: 6,\n textAlign: \"center\",\n color: \"#fff\",\n animation: \"idos-pop-in .35s both\",\n} as const;\n"
|
|
117
123
|
},
|
|
118
124
|
{
|
|
119
125
|
"path": "runner/Lobby.tsx",
|
|
@@ -121,7 +127,7 @@
|
|
|
121
127
|
},
|
|
122
128
|
{
|
|
123
129
|
"path": "runtime.ts",
|
|
124
|
-
"content": "// The constructor game's runtime state, shared by the scene (Phaser) and the panel (React): which\n// spec runs, the running world, and a tiny store of the HUD values the panel shows.\n//\n// The HUD store notifies React only when a watched value CHANGED (owner's rule: no per-frame DOM\n// writes while the game plays): the scene calls `afterStep` every stepped frame, and that compares\n// a handful of numbers, nothing more.\n\nimport type { IDosGamesClient } from \"@idosgames/core\";\nimport {\n createRegistry,\n DEMO_SPEC,\n DEFAULT_LOOK,\n mergePalette,\n type ArtLook,\n registerPlatformKit,\n RUNNER_META,\n PLATFORMER_META,\n TD_META,\n M3_META,\n MG_META,\n TY_META,\n ROUND_ENDED,\n SURVIVORS_META,\n type MetaGameDef,\n START_ROUND,\n SHOW_START,\n startMode,\n validateSpec,\n type Issue,\n type Registry,\n type ResolvedSpec,\n type RunningMode,\n type World,\n} from \"@idosgames/game-kit\";\nimport { configSection } from \"@idosgames/react/ui\";\nimport { applyMeta, learnedOf, runStatsOf, ServerGameMeta } from \"./meta/meta\";\nimport type { ArtLookProps } from \"./meta/ArtIcon\";\n\n/** Title config section that holds the game (written by the constructor on title creation). */\nexport const GAME_SPEC_SECTION = \"GameSpec\";\n\nexport interface Hud {\n readonly state: \"ready\" | \"playing\" | \"ended\";\n readonly score: number;\n readonly coins: number;\n readonly lives: number;\n readonly best: number;\n /** The last finished round (shown on the result screen). */\n readonly result: {\n score: number;\n coins: number;\n best: number;\n newBest: boolean;\n /** Why it ended (the genre's reason: \"cleared\", \"crashed\"…): won or lost on the result window. */\n reason: string;\n /** Stable id of the round (this mount + round number): a retried reward claim pays once. */\n roundKey: string;\n /** What the genre counted (survivors: kills, seconds, bosses, chapter, cleared). */\n stats: Readonly<Record<string, unknown>> | null;\n } | null;\n}\n\nconst EMPTY_HUD: Hud = { state: \"ready\", score: 0, coins: 0, lives: 0, best: 0, result: null };\n\nexport interface LoadedSpec {\n readonly resolved: ResolvedSpec;\n /** The title had no usable spec: the demo runs. */\n readonly fallback: boolean;\n readonly issues: readonly Issue[];\n}\n\n/** The title's spec, or the demo when there is none or it does not validate (the reasons go to `issues`). */\nexport function loadSpec(client: IDosGamesClient, registry: Registry): LoadedSpec {\n const raw = configSection<unknown>(client, GAME_SPEC_SECTION);\n // The engine delivers the section as JSON text (TitlePublicConfigurationModel.GameSpec is a string);\n // an object is accepted too.\n let parsed: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n const demo = validateSpec(DEMO_SPEC, registry);\n return {\n resolved: demo.resolved!,\n fallback: true,\n issues: [{ level: \"error\", path: \"\", code: \"SPEC_JSON\", message: `GameSpec is not valid JSON: ${String(e)}` }],\n };\n }\n }\n if (parsed !== undefined && parsed !== null) {\n const res = validateSpec(parsed, registry);\n if (res.ok && res.resolved) return { resolved: res.resolved, fallback: false, issues: res.issues };\n const demo = validateSpec(DEMO_SPEC, registry);\n return { resolved: demo.resolved!, fallback: true, issues: res.issues };\n }\n const demo = validateSpec(DEMO_SPEC, registry);\n return { resolved: demo.resolved!, fallback: true, issues: [] };\n}\n\n/** The genres with a meta between rounds (levels, upgrades, heroes), by preset id. */\nexport const META_GAMES: Readonly<Record<string, MetaGameDef>> = { survivors: SURVIVORS_META, runner: RUNNER_META, platformer: PLATFORMER_META, \"tower-defense\": TD_META, match3: M3_META, merge: MG_META, tycoon: TY_META };\n\nexport class KitRuntime {\n readonly registry: Registry;\n private loaded: LoadedSpec | null = null;\n private running: RunningMode | null = null;\n private hud: Hud = EMPTY_HUD;\n private readonly listeners = new Set<() => void>();\n /** The game's meta (levels, upgrades, heroes) when the core mode's genre has one (META_GAMES). */\n meta: ServerGameMeta | null = null;\n private offMeta: (() => void) | null = null;\n\n constructor(private readonly client: IDosGamesClient) {\n this.registry = createRegistry();\n registerPlatformKit(this.registry);\n }\n\n /** Load the spec (once per mount) and start its core mode. Returns the new world. */\n start(): World {\n this.stop();\n this.loaded = loadSpec(this.client, this.registry);\n if (this.loaded.issues.length > 0)\n console.warn(\"[kit-game] GameSpec issues\", this.loaded.issues);\n const core = this.loaded.resolved.modes.find((m) => m.role === \"core\") ?? this.loaded.resolved.modes[0]!;\n this.running = startMode(this.loaded.resolved, core.id, this.registry, {\n onRuleError: (rule, error) => console.warn(`[kit-game] rule \"${rule.id}\" failed`, error),\n });\n const world = this.running.world;\n const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;\n const def = META_GAMES[core.preset.id];\n if (def) {\n const soft = this.loaded.resolved.spec.resources?.find((r) => r.kind === \"currency\" && r.ref)?.ref ?? \"CO\";\n if (this.meta?.def !== def) {\n this.meta?.dispose();\n this.meta = new ServerGameMeta(this.client, soft, def);\n }\n const meta = this.meta;\n // the next round reads the meta (stats, level, hero) from the world's vars\n applyMeta(world, meta);\n this.offMeta = meta.subscribe(() => applyMeta(world, meta));\n world.on(ROUND_ENDED, (p) => {\n const s = p[\"stats\"] as Record<string, unknown> | undefined;\n if (s) meta.roundEnded(runStatsOf(s, Number(p[\"coins\"] ?? 0)), learnedOf(world));\n });\n } else {\n this.meta?.dispose();\n this.meta = null;\n }\n world.on(ROUND_ENDED, (p) => {\n const score = Number(p[\"score\"] ?? 0);\n const best = Number(p[\"best\"] ?? score);\n this.setHud({\n ...this.read(world),\n result: {\n score,\n coins: Number(p[\"coins\"] ?? 0),\n best,\n newBest: score > 0 && score >= best && score > this.hud.best,\n reason: typeof p[\"reason\"] === \"string\" ? p[\"reason\"] : \"over\",\n roundKey: `kit:${runId}:${String(world.vars[\"round\"] ?? 0)}`,\n stats: (p[\"stats\"] as Record<string, unknown> | undefined) ?? null,\n },\n });\n });\n this.setHud({ ...this.read(world), result: null });\n return world;\n }\n\n stop(): void {\n this.offMeta?.();\n this.offMeta = null;\n this.running?.stop();\n this.running = null;\n }\n\n /** The id of the core mode's preset (\"survivors\"…). */\n get presetId(): string {\n const core = this.loaded?.resolved.modes.find((m) => m.role === \"core\") ?? this.loaded?.resolved.modes[0];\n return core?.preset.id ?? \"\";\n }\n\n /** How the kit's art is drawn in the UI (the cards, the meta): the pack's style and the palette. */\n artLook(): ArtLookProps {\n return { style: this.look().style, palette: this.palette(), cast: this.look().cast };\n }\n\n /** Back to the game's home after a result (the meta, the start button). */\n home(): void {\n const w = this.world;\n if (!w) return;\n this.setHud({ ...this.hud, result: null });\n w.emit(SHOW_START);\n }\n\n get spec(): LoadedSpec | null {\n return this.loaded;\n }\n\n get world(): World | null {\n return this.running?.world ?? null;\n }\n\n /** Every palette role: the defaults ← the spec's asset pack ← the spec's own colours. */\n palette(): Record<string, string> {\n return { ...(this.loaded?.resolved.palette ?? mergePalette(undefined)) };\n }\n\n /** The art style and the cast of the spec's asset pack (the kit's own without one). */\n look(): ArtLook {\n return this.loaded?.resolved.look ?? DEFAULT_LOOK;\n }\n\n /** A text of the spec (or its asset pack) in the language, falling back to English, then to `fallback`. */\n text(key: string, language: string, fallback: string): string {\n const byLang = this.loaded?.resolved.texts[key];\n return byLang?.[language] ?? byLang?.[\"en\"] ?? fallback;\n }\n\n /** The scene's hook: freeze / unfreeze the field (the game's menu is open over it). */\n pauseHook: ((paused: boolean) => void) | null = null;\n\n /** The game's menu opened or closed over the field. */\n setPaused(paused: boolean): void {\n this.pauseHook?.(paused);\n }\n\n playAgain(): void {\n const w = this.world;\n if (!w) return;\n this.setHud({ ...this.hud, result: null });\n w.emit(START_ROUND);\n }\n\n /** Called by the scene after each stepped frame. */\n afterStep(world: World): void {\n const next = this.read(world);\n const h = this.hud;\n if (next.state !== h.state || next.score !== h.score || next.coins !== h.coins || next.lives !== h.lives || next.best !== h.best)\n this.setHud({ ...next, result: next.state === \"playing\" ? null : h.result });\n }\n\n // useSyncExternalStore\n subscribe = (fn: () => void): (() => void) => {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n };\n snapshot = (): Hud => this.hud;\n\n private read(world: World): Omit<Hud, \"result\"> {\n const n = (k: string) => {\n const v = world.vars[k];\n return typeof v === \"number\" ? Math.floor(v) : 0;\n };\n const s = world.vars[\"state\"];\n return {\n state: s === \"playing\" || s === \"ended\" ? s : \"ready\",\n score: n(\"score\"),\n coins: n(\"coins\"),\n lives: n(\"lives\"),\n best: n(\"best\"),\n };\n }\n\n private setHud(hud: Hud): void {\n this.hud = hud;\n for (const fn of this.listeners) fn();\n }\n}\n"
|
|
130
|
+
"content": "// The constructor game's runtime state, shared by the scene (Phaser) and the panel (React): which\n// spec runs, the running world, and a tiny store of the HUD values the panel shows.\n//\n// The HUD store notifies React only when a watched value CHANGED (owner's rule: no per-frame DOM\n// writes while the game plays): the scene calls `afterStep` every stepped frame, and that compares\n// a handful of numbers, nothing more.\n\nimport type { IDosGamesClient } from \"@idosgames/core\";\nimport {\n createRegistry,\n DEMO_SPEC,\n DEFAULT_LOOK,\n mergePalette,\n type ArtLook,\n registerPlatformKit,\n RUNNER_META,\n PLATFORMER_META,\n TD_META,\n M3_META,\n MG_META,\n TY_META,\n ROUND_ENDED,\n SURVIVORS_META,\n type MetaGameDef,\n START_ROUND,\n SHOW_START,\n startMode,\n validateSpec,\n type Issue,\n type Registry,\n type ResolvedSpec,\n type RunningMode,\n type World,\n} from \"@idosgames/game-kit\";\nimport { configSection } from \"@idosgames/react/ui\";\nimport { applyMeta, learnedOf, runStatsOf, ServerGameMeta } from \"./meta/meta\";\nimport { reportRound, type RoundBonus } from \"./liveops\";\nimport type { ArtLookProps } from \"./meta/ArtIcon\";\n\n/** Title config section that holds the game (written by the constructor on title creation). */\nexport const GAME_SPEC_SECTION = \"GameSpec\";\n\nexport interface Hud {\n readonly state: \"ready\" | \"playing\" | \"ended\";\n readonly score: number;\n readonly coins: number;\n readonly lives: number;\n readonly best: number;\n /** The last finished round (shown on the result screen). */\n readonly result: {\n score: number;\n coins: number;\n best: number;\n newBest: boolean;\n /** Why it ended (the genre's reason: \"cleared\", \"crashed\"…): won or lost on the result window. */\n reason: string;\n /** Stable id of the round (this mount + round number): a retried reward claim pays once. */\n roundKey: string;\n /** What the genre counted (survivors: kills, seconds, bosses, chapter, cleared). */\n stats: Readonly<Record<string, unknown>> | null;\n /** What the round granted in the title's events and seasons (arrives a moment after the round ends). */\n bonus?: readonly RoundBonus[];\n } | null;\n}\n\nconst EMPTY_HUD: Hud = { state: \"ready\", score: 0, coins: 0, lives: 0, best: 0, result: null };\n\nexport interface LoadedSpec {\n readonly resolved: ResolvedSpec;\n /** The title had no usable spec: the demo runs. */\n readonly fallback: boolean;\n readonly issues: readonly Issue[];\n}\n\n/** The title's spec, or the demo when there is none or it does not validate (the reasons go to `issues`). */\nexport function loadSpec(client: IDosGamesClient, registry: Registry): LoadedSpec {\n const raw = configSection<unknown>(client, GAME_SPEC_SECTION);\n // The engine delivers the section as JSON text (TitlePublicConfigurationModel.GameSpec is a string);\n // an object is accepted too.\n let parsed: unknown = raw;\n if (typeof raw === \"string\") {\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n const demo = validateSpec(DEMO_SPEC, registry);\n return {\n resolved: demo.resolved!,\n fallback: true,\n issues: [{ level: \"error\", path: \"\", code: \"SPEC_JSON\", message: `GameSpec is not valid JSON: ${String(e)}` }],\n };\n }\n }\n if (parsed !== undefined && parsed !== null) {\n const res = validateSpec(parsed, registry);\n if (res.ok && res.resolved) return { resolved: res.resolved, fallback: false, issues: res.issues };\n const demo = validateSpec(DEMO_SPEC, registry);\n return { resolved: demo.resolved!, fallback: true, issues: res.issues };\n }\n const demo = validateSpec(DEMO_SPEC, registry);\n return { resolved: demo.resolved!, fallback: true, issues: [] };\n}\n\n/** The genres with a meta between rounds (levels, upgrades, heroes), by preset id. */\nexport const META_GAMES: Readonly<Record<string, MetaGameDef>> = { survivors: SURVIVORS_META, runner: RUNNER_META, platformer: PLATFORMER_META, \"tower-defense\": TD_META, match3: M3_META, merge: MG_META, tycoon: TY_META };\n\nexport class KitRuntime {\n readonly registry: Registry;\n private loaded: LoadedSpec | null = null;\n private running: RunningMode | null = null;\n private hud: Hud = EMPTY_HUD;\n private readonly listeners = new Set<() => void>();\n /** The game's meta (levels, upgrades, heroes) when the core mode's genre has one (META_GAMES). */\n meta: ServerGameMeta | null = null;\n private offMeta: (() => void) | null = null;\n\n constructor(private readonly client: IDosGamesClient) {\n this.registry = createRegistry();\n registerPlatformKit(this.registry);\n }\n\n /** Load the spec (once per mount) and start its core mode. Returns the new world. */\n start(): World {\n this.stop();\n this.loaded = loadSpec(this.client, this.registry);\n if (this.loaded.issues.length > 0)\n console.warn(\"[kit-game] GameSpec issues\", this.loaded.issues);\n const core = this.loaded.resolved.modes.find((m) => m.role === \"core\") ?? this.loaded.resolved.modes[0]!;\n this.running = startMode(this.loaded.resolved, core.id, this.registry, {\n onRuleError: (rule, error) => console.warn(`[kit-game] rule \"${rule.id}\" failed`, error),\n });\n const world = this.running.world;\n const runId = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;\n const def = META_GAMES[core.preset.id];\n if (def) {\n const soft = this.loaded.resolved.spec.resources?.find((r) => r.kind === \"currency\" && r.ref)?.ref ?? \"CO\";\n if (this.meta?.def !== def) {\n this.meta?.dispose();\n this.meta = new ServerGameMeta(this.client, soft, def);\n }\n const meta = this.meta;\n // the next round reads the meta (stats, level, hero) from the world's vars\n applyMeta(world, meta);\n this.offMeta = meta.subscribe(() => applyMeta(world, meta));\n world.on(ROUND_ENDED, (p) => {\n const s = p[\"stats\"] as Record<string, unknown> | undefined;\n if (s) meta.roundEnded(runStatsOf(s, Number(p[\"coins\"] ?? 0)), learnedOf(world));\n });\n } else {\n this.meta?.dispose();\n this.meta = null;\n }\n world.on(ROUND_ENDED, (p) => {\n const score = Number(p[\"score\"] ?? 0);\n const best = Number(p[\"best\"] ?? score);\n const roundKey = `kit:${runId}:${String(world.vars[\"round\"] ?? 0)}`;\n // The round's share of live-ops: leaderboard, events, seasons (liveops.ts). The grants come to the open result.\n void reportRound(this.client, score).then((bonus) => {\n const r = this.hud.result;\n if (bonus.length > 0 && r && r.roundKey === roundKey) this.setHud({ ...this.hud, result: { ...r, bonus } });\n });\n this.setHud({\n ...this.read(world),\n result: {\n score,\n coins: Number(p[\"coins\"] ?? 0),\n best,\n newBest: score > 0 && score >= best && score > this.hud.best,\n reason: typeof p[\"reason\"] === \"string\" ? p[\"reason\"] : \"over\",\n roundKey,\n stats: (p[\"stats\"] as Record<string, unknown> | undefined) ?? null,\n },\n });\n });\n this.setHud({ ...this.read(world), result: null });\n return world;\n }\n\n stop(): void {\n this.offMeta?.();\n this.offMeta = null;\n this.running?.stop();\n this.running = null;\n }\n\n /** The id of the core mode's preset (\"survivors\"…). */\n get presetId(): string {\n const core = this.loaded?.resolved.modes.find((m) => m.role === \"core\") ?? this.loaded?.resolved.modes[0];\n return core?.preset.id ?? \"\";\n }\n\n /** How the kit's art is drawn in the UI (the cards, the meta): the pack's style and the palette. */\n artLook(): ArtLookProps {\n return { style: this.look().style, palette: this.palette(), cast: this.look().cast };\n }\n\n /** Back to the game's home after a result (the meta, the start button). */\n home(): void {\n const w = this.world;\n if (!w) return;\n this.setHud({ ...this.hud, result: null });\n w.emit(SHOW_START);\n }\n\n get spec(): LoadedSpec | null {\n return this.loaded;\n }\n\n get world(): World | null {\n return this.running?.world ?? null;\n }\n\n /** Every palette role: the defaults ← the spec's asset pack ← the spec's own colours. */\n palette(): Record<string, string> {\n return { ...(this.loaded?.resolved.palette ?? mergePalette(undefined)) };\n }\n\n /** The art style and the cast of the spec's asset pack (the kit's own without one). */\n look(): ArtLook {\n return this.loaded?.resolved.look ?? DEFAULT_LOOK;\n }\n\n /** A text of the spec (or its asset pack) in the language, falling back to English, then to `fallback`. */\n text(key: string, language: string, fallback: string): string {\n const byLang = this.loaded?.resolved.texts[key];\n return byLang?.[language] ?? byLang?.[\"en\"] ?? fallback;\n }\n\n /** The scene's hook: freeze / unfreeze the field (the game's menu is open over it). */\n pauseHook: ((paused: boolean) => void) | null = null;\n\n /** The game's menu opened or closed over the field. */\n setPaused(paused: boolean): void {\n this.pauseHook?.(paused);\n }\n\n playAgain(): void {\n const w = this.world;\n if (!w) return;\n this.setHud({ ...this.hud, result: null });\n w.emit(START_ROUND);\n }\n\n /** Called by the scene after each stepped frame. */\n afterStep(world: World): void {\n const next = this.read(world);\n const h = this.hud;\n if (next.state !== h.state || next.score !== h.score || next.coins !== h.coins || next.lives !== h.lives || next.best !== h.best)\n this.setHud({ ...next, result: next.state === \"playing\" ? null : h.result });\n }\n\n // useSyncExternalStore\n subscribe = (fn: () => void): (() => void) => {\n this.listeners.add(fn);\n return () => this.listeners.delete(fn);\n };\n snapshot = (): Hud => this.hud;\n\n private read(world: World): Omit<Hud, \"result\"> {\n const n = (k: string) => {\n const v = world.vars[k];\n return typeof v === \"number\" ? Math.floor(v) : 0;\n };\n const s = world.vars[\"state\"];\n return {\n state: s === \"playing\" || s === \"ended\" ? s : \"ready\",\n score: n(\"score\"),\n coins: n(\"coins\"),\n lives: n(\"lives\"),\n best: n(\"best\"),\n };\n }\n\n private setHud(hud: Hud): void {\n this.hud = hud;\n for (const fn of this.listeners) fn();\n }\n}\n"
|
|
125
131
|
},
|
|
126
132
|
{
|
|
127
133
|
"path": "scene.ts",
|
|
@@ -165,7 +171,7 @@
|
|
|
165
171
|
},
|
|
166
172
|
{
|
|
167
173
|
"path": "module.meta.json",
|
|
168
|
-
"content": "{\n \"id\": \"kit-game\",\n \"type\": \"template\",\n \"uses\": [\"premium\"],\n \"summary\": \"The game constructor's runtime: plays the title's GameSpec (a whole game as data) on Phaser; without one, a demo lane runner.\",\n \"description\": \"One prebuilt runtime for every 2D game of the platform's constructor. The title's config section GameSpec describes the game as data: genre presets as modes, their parameters (difficulty, lanes, lives…), the palette, texts in every language, resources and how modes produce and spend them, rules (trigger → condition → effect) and reward multipliers. The module validates the spec against the platform's blocks and presets from @idosgames/game-kit (numbers out of range are clamped, unknown fields kept), runs the core mode in a deterministic world and draws it through the Phaser adapter. Genres so far: an endless lane runner (swipe, arrows or a tap on a side to change lanes; coins, obstacles, growing speed). A title without a GameSpec plays the built-in demo spec. Edit the game by editing the spec, not the code; a new genre is a new preset over the kit's blocks.\",\n \"provides\": [\n \"a game from a GameSpec: no code per game\",\n \"endless lane runner (swipe / arrows / tap a side)\",\n \"palette, texts and parameters from the spec\",\n \"deterministic runs from the spec's seed\",\n \"round flow: tap to start, result screen, play again\",\n \"reward window after a round: claim, x3 for a rewarded ad (in the app), x5 for VIP (server-side multiplier)\"\n ],\n \"tags\": [\"constructor\"
|
|
174
|
+
"content": "{\n \"id\": \"kit-game\",\n \"type\": \"template\",\n \"uses\": [\n \"premium\"\n ],\n \"summary\": \"The game constructor's runtime: plays the title's GameSpec (a whole game as data) on Phaser; without one, a demo lane runner.\",\n \"description\": \"One prebuilt runtime for every 2D game of the platform's constructor. The title's config section GameSpec describes the game as data: genre presets as modes, their parameters (difficulty, lanes, lives…), the palette, texts in every language, resources and how modes produce and spend them, rules (trigger → condition → effect) and reward multipliers. The module validates the spec against the platform's blocks and presets from @idosgames/game-kit (numbers out of range are clamped, unknown fields kept), runs the core mode in a deterministic world and draws it through the Phaser adapter. Genres so far: an endless lane runner (swipe, arrows or a tap on a side to change lanes; coins, obstacles, growing speed). A title without a GameSpec plays the built-in demo spec. Edit the game by editing the spec, not the code; a new genre is a new preset over the kit's blocks.\",\n \"provides\": [\n \"a game from a GameSpec: no code per game\",\n \"endless lane runner (swipe / arrows / tap a side)\",\n \"palette, texts and parameters from the spec\",\n \"deterministic runs from the spec's seed\",\n \"round flow: tap to start, result screen, play again\",\n \"reward window after a round: claim, x3 for a rewarded ad (in the app), x5 for VIP (server-side multiplier)\",\n \"score submitted to the leaderboard (a board that takes a game score)\",\n \"rounds counted by quests, events (CustomAction play_round) and the season\"\n ],\n \"tags\": [\n \"constructor\",\n \"game-kit\",\n \"runner\",\n \"arcade\",\n \"phaser\",\n \"2d\",\n \"data-driven\"\n ],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.0\"\n}\n"
|
|
169
175
|
}
|
|
170
176
|
]
|
|
171
177
|
}
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.5"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.26.
|
|
40
|
-
"@idosgames/module-sdk": "0.13.
|
|
41
|
-
"@idosgames/react": "0.7.
|
|
39
|
+
"@idosgames/core": "0.26.2",
|
|
40
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
41
|
+
"@idosgames/react": "0.7.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "8aa99b6b9980f06e2a66adf38ff0c1a20d5e2eefc197a5a81b75c1140faf0b66",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"version": "0.1.4"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.26.
|
|
36
|
-
"@idosgames/module-sdk": "0.13.
|
|
37
|
-
"@idosgames/react": "0.7.
|
|
35
|
+
"@idosgames/core": "0.26.2",
|
|
36
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
37
|
+
"@idosgames/react": "0.7.2",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
40
|
"contentHash": "55544f5bae173482aaa22e92612cade99a356547ee80b68fc096dde8fb700b47",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.3"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.26.
|
|
40
|
-
"@idosgames/module-sdk": "0.13.
|
|
41
|
-
"@idosgames/react": "0.7.
|
|
39
|
+
"@idosgames/core": "0.26.2",
|
|
40
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
41
|
+
"@idosgames/react": "0.7.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "12cf31156c913e7275c83bbff9b555821f7eb8dff6241ceb569da20ed87de1eb",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.3"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.26.
|
|
40
|
-
"@idosgames/module-sdk": "0.13.
|
|
41
|
-
"@idosgames/react": "0.7.
|
|
39
|
+
"@idosgames/core": "0.26.2",
|
|
40
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
41
|
+
"@idosgames/react": "0.7.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "158824605f43164105e68883333c347fd5110a14b1ccb43c68036db930bbfe5f",
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"version": "0.2.1"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@idosgames/core": "0.26.
|
|
43
|
-
"@idosgames/module-sdk": "0.13.
|
|
44
|
-
"@idosgames/react": "0.7.
|
|
42
|
+
"@idosgames/core": "0.26.2",
|
|
43
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
44
|
+
"@idosgames/react": "0.7.2",
|
|
45
45
|
"react": "19.2.7"
|
|
46
46
|
},
|
|
47
47
|
"contentHash": "03c011db581a59f3e98ea8696f113e02021db798e9a871be151d3e65726db2f8",
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
"version": "0.1.3"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.26.
|
|
36
|
-
"@idosgames/module-sdk": "0.13.
|
|
37
|
-
"@idosgames/react": "0.7.
|
|
35
|
+
"@idosgames/core": "0.26.2",
|
|
36
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
37
|
+
"@idosgames/react": "0.7.2",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
40
|
"contentHash": "d835f2923e281c625bd1888202c6e2df9005bd05ba54ed5793fbac2e03aab4dc",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"version": "0.1.5"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@idosgames/core": "0.26.
|
|
38
|
-
"@idosgames/module-sdk": "0.13.
|
|
39
|
-
"@idosgames/react": "0.7.
|
|
37
|
+
"@idosgames/core": "0.26.2",
|
|
38
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
39
|
+
"@idosgames/react": "0.7.2",
|
|
40
40
|
"react": "19.2.7"
|
|
41
41
|
},
|
|
42
42
|
"contentHash": "2448dc72d4408919336d2f4011a8dd8903a913811c8ef00db2b64c21b77f5b52",
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"version": "0.1.2"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@idosgames/core": "0.26.
|
|
37
|
-
"@idosgames/module-sdk": "0.13.
|
|
38
|
-
"@idosgames/react": "0.7.
|
|
36
|
+
"@idosgames/core": "0.26.2",
|
|
37
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
38
|
+
"@idosgames/react": "0.7.2",
|
|
39
39
|
"react": "19.2.7"
|
|
40
40
|
},
|
|
41
41
|
"contentHash": "c374268f2fc7cfb1fb4a6073cceca7cb0666f85bc2f1c37bc4ba818233207b61",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"version": "0.1.3"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@idosgames/core": "0.26.
|
|
38
|
-
"@idosgames/module-sdk": "0.13.
|
|
39
|
-
"@idosgames/react": "0.7.
|
|
37
|
+
"@idosgames/core": "0.26.2",
|
|
38
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
39
|
+
"@idosgames/react": "0.7.2",
|
|
40
40
|
"react": "19.2.7"
|
|
41
41
|
},
|
|
42
42
|
"contentHash": "f30292cdadf0595a434aaae91598c279add865f03c5ae9767d5b4227e9d479ba",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
"version": "0.1.3"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@idosgames/core": "0.26.
|
|
35
|
-
"@idosgames/module-sdk": "0.13.
|
|
36
|
-
"@idosgames/react": "0.7.
|
|
34
|
+
"@idosgames/core": "0.26.2",
|
|
35
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
36
|
+
"@idosgames/react": "0.7.2",
|
|
37
37
|
"react": "19.2.7"
|
|
38
38
|
},
|
|
39
39
|
"contentHash": "ab99622e4ba739d6e189e21c2e41c38125b2b2d140c2c63f670dd2205dca3efc",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"catalog": {
|
|
9
9
|
"type": "feature",
|
|
10
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.",
|
|
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 (
|
|
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 (kit-game grants status at the end of each round). Ships title-seed.json: two alternating 28-day seasons with five tiers and a VIP prize on each.",
|
|
12
12
|
"provides": [
|
|
13
13
|
"season screen with a tab per chain",
|
|
14
14
|
"season timer and status bar",
|
|
@@ -36,12 +36,12 @@
|
|
|
36
36
|
"version": "0.1.3"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.26.
|
|
40
|
-
"@idosgames/module-sdk": "0.13.
|
|
41
|
-
"@idosgames/react": "0.7.
|
|
39
|
+
"@idosgames/core": "0.26.2",
|
|
40
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
41
|
+
"@idosgames/react": "0.7.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
|
-
"contentHash": "
|
|
44
|
+
"contentHash": "ad95a47f070763556c554c73428e7b1c921f7c987a9960294f8464c5794315b2",
|
|
45
45
|
"files": [
|
|
46
46
|
{
|
|
47
47
|
"path": "i18n.ts",
|
|
@@ -65,7 +65,7 @@
|
|
|
65
65
|
},
|
|
66
66
|
{
|
|
67
67
|
"path": "module.meta.json",
|
|
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 (
|
|
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 (kit-game grants status at the end of each 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.3\"\n}\n"
|
|
69
69
|
}
|
|
70
70
|
]
|
|
71
71
|
}
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"version": "0.1.4"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@idosgames/core": "0.26.
|
|
40
|
-
"@idosgames/module-sdk": "0.13.
|
|
41
|
-
"@idosgames/react": "0.7.
|
|
39
|
+
"@idosgames/core": "0.26.2",
|
|
40
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
41
|
+
"@idosgames/react": "0.7.2",
|
|
42
42
|
"react": "19.2.7"
|
|
43
43
|
},
|
|
44
44
|
"contentHash": "e702aa4d3c44024f0c964d4b3c791599a8724bece9020895df2de3286ad204ff",
|
|
@@ -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, a game's Play button play:{modeID}
|
|
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}) — 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.4"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@idosgames/core": "0.26.
|
|
36
|
-
"@idosgames/module-sdk": "0.13.
|
|
37
|
-
"@idosgames/react": "0.7.
|
|
35
|
+
"@idosgames/core": "0.26.2",
|
|
36
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
37
|
+
"@idosgames/react": "0.7.2",
|
|
38
38
|
"react": "19.2.7"
|
|
39
39
|
},
|
|
40
|
-
"contentHash": "
|
|
40
|
+
"contentHash": "013277c81a5a5fe1d7454bb7ffe7da69ee5afab4b050cccf59d2e73a5cf48487",
|
|
41
41
|
"files": [
|
|
42
42
|
{
|
|
43
43
|
"path": "i18n.ts",
|
|
@@ -61,7 +61,7 @@
|
|
|
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}
|
|
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}) — 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.4\"\n}\n"
|
|
65
65
|
}
|
|
66
66
|
]
|
|
67
67
|
}
|
|
@@ -55,9 +55,9 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@idosgames/characters": "0.1.3",
|
|
58
|
-
"@idosgames/core": "0.26.
|
|
59
|
-
"@idosgames/module-sdk": "0.13.
|
|
60
|
-
"@idosgames/react": "0.7.
|
|
58
|
+
"@idosgames/core": "0.26.2",
|
|
59
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
60
|
+
"@idosgames/react": "0.7.2",
|
|
61
61
|
"@idosgames/timeline": "0.1.0",
|
|
62
62
|
"react": "19.2.7",
|
|
63
63
|
"three": "0.185.1"
|
|
@@ -85,9 +85,9 @@
|
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
87
|
"@idosgames/characters": "0.1.3",
|
|
88
|
-
"@idosgames/core": "0.26.
|
|
89
|
-
"@idosgames/module-sdk": "0.13.
|
|
90
|
-
"@idosgames/react": "0.7.
|
|
88
|
+
"@idosgames/core": "0.26.2",
|
|
89
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
90
|
+
"@idosgames/react": "0.7.2",
|
|
91
91
|
"react": "19.2.7",
|
|
92
92
|
"three": "0.185.1"
|
|
93
93
|
},
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"version": "0.1.3"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@idosgames/core": "0.26.
|
|
43
|
-
"@idosgames/module-sdk": "0.13.
|
|
44
|
-
"@idosgames/react": "0.7.
|
|
42
|
+
"@idosgames/core": "0.26.2",
|
|
43
|
+
"@idosgames/module-sdk": "0.13.4",
|
|
44
|
+
"@idosgames/react": "0.7.2",
|
|
45
45
|
"react": "19.2.7"
|
|
46
46
|
},
|
|
47
47
|
"contentHash": "c3a4b38be15260152a5244a06539639a1c7c08c207faf76d03baab43e639d148",
|