@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.
Files changed (42) hide show
  1. package/dist/cli.js +1 -1
  2. package/package.json +1 -1
  3. package/registry/host.json +1 -1
  4. package/registry/index.json +159 -149
  5. package/registry/modules/ai-media-app.json +77 -0
  6. package/registry/modules/ai.json +3 -3
  7. package/registry/modules/board-game.json +3 -3
  8. package/registry/modules/boosts.json +3 -3
  9. package/registry/modules/character.json +3 -3
  10. package/registry/modules/chat.json +3 -3
  11. package/registry/modules/collections.json +3 -3
  12. package/registry/modules/coop-events.json +3 -3
  13. package/registry/modules/craft.json +3 -3
  14. package/registry/modules/deals.json +3 -3
  15. package/registry/modules/events.json +6 -6
  16. package/registry/modules/friends.json +3 -3
  17. package/registry/modules/hero-run.json +3 -3
  18. package/registry/modules/idle-rpg.json +3 -3
  19. package/registry/modules/inventory.json +3 -3
  20. package/registry/modules/invite.json +3 -3
  21. package/registry/modules/kit-game.json +17 -11
  22. package/registry/modules/leaderboard.json +3 -3
  23. package/registry/modules/lootboxes.json +3 -3
  24. package/registry/modules/mailbox.json +3 -3
  25. package/registry/modules/marketplace.json +3 -3
  26. package/registry/modules/multiplayer.json +3 -3
  27. package/registry/modules/premium.json +3 -3
  28. package/registry/modules/profile.json +3 -3
  29. package/registry/modules/pvp.json +3 -3
  30. package/registry/modules/quests.json +3 -3
  31. package/registry/modules/rewards.json +3 -3
  32. package/registry/modules/season.json +6 -6
  33. package/registry/modules/store.json +3 -3
  34. package/registry/modules/tutorials.json +6 -6
  35. package/registry/modules/video-studio.json +3 -3
  36. package/registry/modules/voxelcraft.json +3 -3
  37. package/registry/modules/workshop.json +3 -3
  38. package/registry/skills/ai-generation-system.json +1 -1
  39. package/registry/skills/idosgames-compose-modules.json +1 -1
  40. package/registry/skills/idosgames-getting-started.json +2 -2
  41. package/registry/skills/idosgames-module-contract.json +2 -2
  42. package/registry/modules/mini-game.json +0 -69
@@ -0,0 +1,77 @@
1
+ {
2
+ "id": "ai-media-app",
3
+ "meta": {
4
+ "name": "AI generator",
5
+ "type": "game",
6
+ "engine": "dom",
7
+ "genre": "ai-media"
8
+ },
9
+ "catalog": {
10
+ "type": "template",
11
+ "baseSystems": false,
12
+ "summary": "An AI generator app: players repeat trends and templates of the platform AI studio with their own photos (video, pictures, voice, music, 3D), paying the author's price; a few free generations a day at the author's cost.",
13
+ "description": "A whole app on the full screen in the look of idosgames.com (plain React and CSS: dark zinc, the site's blue, Onest; no engine, no game look), the 'AI generator' base. No lobby and no systems of the base (\"baseSystems\": false): no inventory, quests, friends or mail; after sign-in the app opens straight on its cards. Everything the app does is the title's AI section, not code: every AI feature with a PlatformTemplateId is a card of a platform template or trend (name, the sample, photo slots, choices), a plain feature of a kind is 'describe it yourself'. A new title gets up to six ready trends with a default Aspis price. A kind (video, picture, voice, music, 3D) shows only when it has features; several kinds get chips on top. As few buttons as possible: one grid of big cards (trends first), a tap opens the card with its photo slot and ONE button with the price (free while the feature's FreePerDay lasts, the author pays; then Aspis confirmed on idosgames.com or in-game currency, charged only when the result is ready); the result plays right away with Download and Again; 'My works' is one icon in the header. The template's prompt and the model never reach the player: the engine runs the template (GenerateFromTemplate). Edit the app by editing the AI features (the dashboard's AI generator page or LiveOps → AI), not the code.",
14
+ "provides": [
15
+ "an AI generator app from the title's AI features (no code per app)",
16
+ "the look of idosgames.com, no lobby and no systems of the base",
17
+ "ready trends in a new title: repeat a trending video with your own photo",
18
+ "templates of the platform AI studio as cards: video, picture, voice, music, 3D",
19
+ "'describe it yourself' for a kind with a plain AI feature",
20
+ "photo slots with downscaling on the device",
21
+ "one button with the price: in-game currency or Aspis, free generations per day at the author's cost",
22
+ "result with download and again, the player's works"
23
+ ],
24
+ "tags": [
25
+ "ai",
26
+ "generation",
27
+ "trends",
28
+ "templates",
29
+ "video",
30
+ "photo",
31
+ "app",
32
+ "ugc"
33
+ ],
34
+ "author": {
35
+ "name": "iDos Games",
36
+ "url": "https://idosgames.com"
37
+ },
38
+ "version": "0.1.0"
39
+ },
40
+ "dependencies": {
41
+ "@idosgames/core": "0.26.2",
42
+ "@idosgames/module-sdk": "0.13.4",
43
+ "@idosgames/react": "0.7.2",
44
+ "react": "19.2.7"
45
+ },
46
+ "contentHash": "cbe4819d76f43427c02c27358065e1afba58996ec755b416559940fe6ee47093",
47
+ "files": [
48
+ {
49
+ "path": "App.tsx",
50
+ "content": "import {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport type {\n AIGenerationView,\n AIInputImage,\n AIPublicDefinitions,\n AIPublicTemplate,\n IDosGamesClient,\n} from \"@idosgames/core\";\nimport { canPayWithPlatformCredits } from \"@idosgames/core\";\nimport { useIDosGamesClient, useUserState } from \"@idosgames/react\";\nimport {\n canAfford,\n errorText,\n formatLineAmount,\n Icon,\n isCreditsCancel,\n payWithCredits,\n pickLanguage,\n PlatformCurrencyIcon,\n useCatalog,\n useToast,\n type ResourceLine,\n} from \"@idosgames/react/ui\";\nimport {\n appItems,\n fitSize,\n kindsOf,\n localized,\n missingInputs,\n missingValues,\n priceLabelOf,\n resultKind,\n type AppItem,\n type Kind,\n} from \"./model\";\nimport { t } from \"./i18n\";\nimport { APP_CSS, FONT_URL } from \"./styles\";\n\n// The AI generator app: the title's AI features as cards (trends and templates of the platform AI studio, and\n// \"describe it yourself\" for a kind with a plain feature). Prices, free generations and the cards come from the\n// engine's sanitized AI definitions; the template's prompt never reaches the player.\n//\n// The look is idosgames.com, not a game (owner 09.11.2026): dark zinc, the site's blue, Onest, flat cards; and as few\n// buttons as possible: a card → a photo → ONE button with the price → the result with Download and Again.\n\ntype Screen = { at: \"home\" } | { at: \"run\"; key: string } | { at: \"works\" };\n\n/**\n * The AI definitions of the title: read once, read again after a generation (the free count moves). Always from the\n * engine (`forceRefresh`), never the cached config section: only the engine's answer carries the template cards\n * (`PlatformTemplate`) and the player's `FreeLeftToday`; the config copy has neither, and every template card would\n * be dropped as \"taken off the site\".\n */\nfunction useDefinitions(client: IDosGamesClient): {\n defs: AIPublicDefinitions | null;\n refresh: () => void;\n} {\n const [defs, setDefs] = useState<AIPublicDefinitions | null>(null);\n const refresh = useCallback(() => {\n void client.ai\n .getDefinitions({ forceRefresh: true })\n .then((r) => setDefs(r.ok ? r.data : { Enabled: false }));\n }, [client]);\n useEffect(refresh, [refresh]);\n return { defs, refresh };\n}\n\n/** The site's font, once per page. */\nfunction useSiteFont(): void {\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n if (document.querySelector(`link[href=\"${FONT_URL}\"]`)) return;\n const link = document.createElement(\"link\");\n link.rel = \"stylesheet\";\n link.href = FONT_URL;\n document.head.appendChild(link);\n }, []);\n}\n\nexport function MediaApp(): ReactNode {\n useSiteFont();\n const client = useIDosGamesClient();\n const { defs, refresh } = useDefinitions(client);\n // In the iOS app Aspis cannot be paid (App Store 3.1.1): its prices are dropped, those cards only show the preview.\n const items = appItems(defs, canPayWithPlatformCredits());\n const kinds = kindsOf(items);\n const [kind, setKind] = useState<Kind | null>(null);\n const [screen, setScreen] = useState<Screen>({ at: \"home\" });\n const home = () => setScreen({ at: \"home\" });\n\n const current = kind && kinds.includes(kind) ? kind : kinds[0];\n const item =\n screen.at === \"run\" ? items.find((i) => i.key === screen.key) : undefined;\n\n return (\n <div className=\"iam\">\n <style>{APP_CSS}</style>\n {!defs ? (\n <div className=\"iam-center\">\n <span className=\"iam-spin\" />\n </div>\n ) : item ? (\n <Run key={item.key} item={item} onBack={home} onDone={refresh} />\n ) : screen.at === \"works\" ? (\n <Works items={items} onBack={home} />\n ) : (\n <>\n <header className=\"iam-bar\">\n <span className=\"iam-title\">{appTitle()}</span>\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"history\")}\n onClick={() => setScreen({ at: \"works\" })}\n >\n <Svg d={ICON.works} />\n </button>\n </header>\n {kinds.length > 1 ? (\n <nav className=\"iam-chips\">\n {kinds.map((k) => (\n <button\n key={k}\n type=\"button\"\n className={\n k === current ? \"iam-chip iam-chip-on\" : \"iam-chip\"\n }\n onClick={() => setKind(k)}\n >\n {t(`k_${k}`)}\n </button>\n ))}\n </nav>\n ) : null}\n {current ? (\n <Grid\n items={items.filter((i) => i.kind === current)}\n onOpen={(key) => setScreen({ at: \"run\", key })}\n />\n ) : (\n <p className=\"iam-empty\">{t(\"empty\")}</p>\n )}\n </>\n )}\n </div>\n );\n}\n\n/** The app's name: the page title the author set, else \"AI generator\". */\nfunction appTitle(): string {\n const title = typeof document === \"undefined\" ? \"\" : document.title.trim();\n return title || t(\"title\");\n}\n\n// ── Home: one grid of big cards, trends first ────────────────────────────────────────────────\n\nfunction Grid({\n items,\n onOpen,\n}: {\n items: AppItem[];\n onOpen: (key: string) => void;\n}): ReactNode {\n // Trends first, then templates, \"describe it yourself\" last: a ready card converts better than an empty box.\n const ordered = [\n ...items.filter((i) => i.trend),\n ...items.filter((i) => i.template && !i.trend),\n ...items.filter((i) => !i.template),\n ];\n return (\n <div className=\"iam-grid\">\n {ordered.map((i) => (\n <button\n key={i.key}\n type=\"button\"\n className={i.kind === \"Video\" ? \"iam-card iam-tall\" : \"iam-card\"}\n onClick={() => onOpen(i.key)}\n >\n {i.template ? (\n <Preview template={i.template} cover />\n ) : (\n <span className=\"iam-own\">\n <Svg d={ICON.pen} size={28} />\n </span>\n )}\n {i.trend ? <span className=\"iam-badge\">{t(\"trend\")}</span> : null}\n <span className=\"iam-card-foot\">\n <span className=\"iam-card-name\">\n {i.template ? localized(i.template.Name, lang()) : t(\"own\")}\n </span>\n <PriceChip item={i} />\n </span>\n </button>\n ))}\n </div>\n );\n}\n\nconst lang = (): string => pickLanguage();\n\n/** The sample of a template: a silent looping video or a picture; the robot when there is none. */\nfunction Preview({\n template,\n cover,\n}: {\n template: AIPublicTemplate;\n cover?: boolean;\n}): ReactNode {\n const p = template.Preview;\n const cls = cover ? \"iam-media iam-cover\" : \"iam-media\";\n const type = (p?.Type ?? \"\").toLowerCase();\n if (!p?.Url || type === \"audio\")\n return (\n <span className=\"iam-own\">\n <Svg d={ICON.bot} size={32} />\n </span>\n );\n if (type === \"video\")\n return (\n <video\n className={cls}\n src={p.Url}\n poster={p.PosterUrl ?? undefined}\n muted\n loop\n autoPlay\n playsInline\n />\n );\n return <img className={cls} src={p.Url} alt=\"\" />;\n}\n\nfunction useBalances(): Record<string, { Amount?: number }> | undefined {\n const state = useUserState();\n return state?.InventoryV2?.VirtualCurrencies as\n Record<string, { Amount?: number }> | undefined;\n}\n\n/** \"Free\" or the price: Aspis with its icon, a game currency with its icon. */\nfunction PriceChip({ item }: { item: AppItem }): ReactNode {\n const balances = useBalances();\n const label = priceLabelOf(item, (p) => canAfford(p, balances));\n if (label.free)\n return <span className=\"iam-price iam-free\">{t(\"free\")}</span>;\n return label.option ? (\n <span className=\"iam-price\">\n <Price lines={label.option.price} />\n </span>\n ) : null;\n}\n\nfunction Price({ lines }: { lines: ResourceLine[] }): ReactNode {\n const catalog = useCatalog();\n return (\n <>\n {lines.map((line) => (\n <span key={`${line.kind}:${line.id}`} className=\"iam-price-line\">\n {line.kind === \"credits\" ? (\n <PlatformCurrencyIcon size={14} />\n ) : (\n <Icon glyph={catalog.iconOf(line)} size={14} />\n )}\n {formatLineAmount(line)}\n </span>\n ))}\n </>\n );\n}\n\n// ── One card: photos, choices, the button with the price, the result ─────────────────────────\n\n/** A photo from the device, downscaled to {@link fitSize} and sent as JPEG. null: not a picture. */\nasync function readPhoto(file: File): Promise<AIInputImage | null> {\n if (!file.type.startsWith(\"image/\")) return null;\n const bitmap = await createImageBitmap(file).catch(() => null);\n if (!bitmap) return null;\n const size = fitSize(bitmap.width, bitmap.height);\n const canvas = document.createElement(\"canvas\");\n canvas.width = size.width;\n canvas.height = size.height;\n canvas.getContext(\"2d\")?.drawImage(bitmap, 0, 0, size.width, size.height);\n bitmap.close();\n return {\n Base64: canvas.toDataURL(\"image/jpeg\", 0.9),\n ContentType: \"image/jpeg\",\n };\n}\n\n/** Starts the generation (an Aspis price is confirmed by the player on the way) and waits for the result. */\nasync function generate(\n client: IDosGamesClient,\n item: AppItem,\n files: Record<string, AIInputImage>,\n values: Record<string, string>,\n prompt: string,\n optionID: string | undefined,\n): Promise<\n { ok: true; data: AIGenerationView } | { ok: false; error: string }\n> {\n // One key for both calls of an Aspis price: the request, then the confirmed generation.\n const relatedEntityID = crypto.randomUUID();\n const opts = (creditsConfirmationID?: string) => ({\n ...(optionID ? { selectedOptionID: optionID } : {}),\n relatedEntityID,\n ...(creditsConfirmationID ? { creditsConfirmationID } : {}),\n ...(item.template ? { variables: values } : {}),\n });\n const call = (id?: string) => {\n if (item.template)\n return client.ai.generateFromTemplate(item.key, files, opts(id));\n switch (item.kind) {\n case \"Image\":\n return client.ai.generateImage(item.key, { Prompt: prompt }, opts(id));\n case \"Video\":\n return client.ai.generateVideo(item.key, { Prompt: prompt }, opts(id));\n case \"Audio\":\n return client.ai.generateAudio(item.key, { Text: prompt }, opts(id));\n case \"Music\":\n return client.ai.generateMusic(item.key, { Prompt: prompt }, opts(id));\n default:\n return client.ai.generateThreeD(item.key, { Prompt: prompt }, opts(id));\n }\n };\n const started = await payWithCredits(client, call);\n return started.ok\n ? client.ai.waitForGeneration(started.data, { timeoutMs: 15 * 60_000 })\n : started;\n}\n\nfunction Run({\n item,\n onBack,\n onDone,\n}: {\n item: AppItem;\n onBack: () => void;\n onDone: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const balances = useBalances();\n const tpl = item.template;\n const [files, setFiles] = useState<Record<string, AIInputImage>>({});\n const [values, setValues] = useState<Record<string, string>>({});\n const [prompt, setPrompt] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [result, setResult] = useState<AIGenerationView | null>(null);\n const alive = useRef(true);\n useEffect(\n () => () => {\n alive.current = false;\n },\n [],\n );\n\n const label = priceLabelOf(item, (p) => canAfford(p, balances));\n /** Nothing to pay with here (Aspis inside the iOS app): the card is only looked at, no photos and no button. */\n const viewOnly = !label.free && !label.option;\n const ready = tpl\n ? missingInputs(tpl, files).length === 0 &&\n missingValues(tpl, values).length === 0\n : !!prompt.trim();\n\n const go = async () => {\n setBusy(true);\n setResult(null);\n const res = await generate(\n client,\n item,\n files,\n values,\n prompt.trim(),\n label.free ? undefined : label.option?.id,\n );\n if (!alive.current) return;\n setBusy(false);\n onDone();\n if (!res.ok) {\n if (!isCreditsCancel(res.error)) toast(errorText(res.error), \"error\");\n return;\n }\n if (res.data.Status !== \"completed\")\n return toast(res.data.Error?.Message || t(\"failed\"), \"error\");\n setResult(res.data);\n };\n\n const name = tpl ? localized(tpl.Name, lang()) : t(\"own\");\n const download = result ? firstAssetUrl(result) : \"\";\n\n return (\n <>\n <header className=\"iam-bar\">\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"back\")}\n onClick={onBack}\n >\n <Svg d={ICON.back} />\n </button>\n <span className=\"iam-title\">{name}</span>\n <span className=\"iam-icon-btn\" aria-hidden />\n </header>\n <div className=\"iam-page\">\n {result ? (\n <Result generation={result} />\n ) : tpl ? (\n <div\n className={item.kind === \"Video\" ? \"iam-hero iam-tall\" : \"iam-hero\"}\n >\n <Preview template={tpl} cover />\n {busy ? <Working video={item.kind === \"Video\"} /> : null}\n </div>\n ) : busy ? (\n <div className=\"iam-hero\">\n <Working video={item.kind === \"Video\"} />\n </div>\n ) : null}\n\n {result ? null : tpl ? (\n <>\n {localized(tpl.Description, lang()) ? (\n <p className=\"iam-muted\">{localized(tpl.Description, lang())}</p>\n ) : null}\n {viewOnly ? null : (\n <>\n <Photos template={tpl} files={files} onChange={setFiles} />\n <Choices template={tpl} values={values} onChange={setValues} />\n </>\n )}\n </>\n ) : viewOnly ? null : (\n <textarea\n className=\"iam-input\"\n value={prompt}\n maxLength={item.maxChars > 0 ? item.maxChars : undefined}\n placeholder={item.kind === \"Audio\" ? t(\"sayWhat\") : t(\"describe\")}\n onChange={(e) => setPrompt(e.target.value)}\n rows={5}\n />\n )}\n </div>\n\n {viewOnly && !result ? null : (\n <footer className=\"iam-dock\">\n {result ? (\n <>\n {download ? (\n <a\n className=\"iam-btn\"\n href={download}\n target=\"_blank\"\n rel=\"noreferrer\"\n download\n >\n <Svg d={ICON.download} size={18} /> {t(\"download\")}\n </a>\n ) : null}\n <button\n type=\"button\"\n className=\"iam-btn iam-btn-ghost\"\n onClick={() => setResult(null)}\n >\n {t(\"again\")}\n </button>\n </>\n ) : (\n <button\n type=\"button\"\n className=\"iam-btn\"\n disabled={!ready || busy}\n onClick={() => void go()}\n data-tutorial-anchor={`ai-media-app:${item.key}`}\n >\n {busy ? (\n <>\n <span className=\"iam-spin iam-spin-sm\" /> {t(\"working\")}\n </>\n ) : (\n <>\n {t(\"create\")}\n {label.free ? (\n <span className=\"iam-btn-price\">\n · {freeText(label.left)}\n </span>\n ) : label.option ? (\n <span className=\"iam-btn-price\">\n · <Price lines={label.option.price} />\n </span>\n ) : null}\n </>\n )}\n </button>\n )}\n </footer>\n )}\n </>\n );\n}\n\nfunction freeText(left: number): string {\n return Number.isFinite(left)\n ? t(\"freeLeft\").replace(\"{n}\", String(left))\n : t(\"free\");\n}\n\nfunction Working({ video }: { video: boolean }): ReactNode {\n return (\n <span className=\"iam-working\">\n <span className=\"iam-spin\" />\n <span>{t(\"working\")}</span>\n {video ? <span className=\"iam-muted\">{t(\"videoWait\")}</span> : null}\n </span>\n );\n}\n\nfunction Photos({\n template,\n files,\n onChange,\n}: {\n template: AIPublicTemplate;\n files: Record<string, AIInputImage>;\n onChange: (next: Record<string, AIInputImage>) => void;\n}): ReactNode {\n const toast = useToast();\n const inputs = (template.Inputs ?? []).filter((i) => i.Key);\n if (inputs.length === 0) return null;\n return (\n <div className=\"iam-stack\">\n <div\n className={inputs.length === 1 ? \"iam-photos iam-one\" : \"iam-photos\"}\n >\n {inputs.map((input) => {\n const key = input.Key!;\n const file = files[key];\n return (\n <label key={key} className=\"iam-photo\">\n {file?.Base64 ? (\n <img className=\"iam-media iam-cover\" src={file.Base64} alt=\"\" />\n ) : (\n <span className=\"iam-photo-empty\">\n <Svg d={ICON.plus} size={26} />\n <span>{localized(input.Label, lang()) || t(\"addPhoto\")}</span>\n {!input.Required ? (\n <span className=\"iam-muted\">{t(\"optional\")}</span>\n ) : null}\n </span>\n )}\n <input\n type=\"file\"\n accept=\"image/*\"\n hidden\n onChange={(e) => {\n const picked = e.target.files?.[0];\n e.target.value = \"\";\n if (!picked) return;\n void readPhoto(picked).then((img) => {\n if (!img) return toast(t(\"photoBad\"), \"error\");\n onChange({ ...files, [key]: img });\n });\n }}\n />\n </label>\n );\n })}\n </div>\n <p className=\"iam-note\">{t(\"consent\")}</p>\n </div>\n );\n}\n\nfunction Choices({\n template,\n values,\n onChange,\n}: {\n template: AIPublicTemplate;\n values: Record<string, string>;\n onChange: (next: Record<string, string>) => void;\n}): ReactNode {\n const vars = (template.Variables ?? []).filter((x) => x.Name);\n if (vars.length === 0) return null;\n return (\n <div className=\"iam-stack\">\n {vars.map((x) => {\n const name = x.Name!;\n const value = values[name] ?? x.Default ?? \"\";\n return (\n <div key={name} className=\"iam-field\">\n <span className=\"iam-label\">\n {localized(x.Label, lang()) || name}\n </span>\n {x.Type === \"Choice\" ? (\n <div className=\"iam-chips iam-chips-wrap\">\n {(x.Choices ?? []).map((c) => (\n <button\n key={c.Value ?? \"\"}\n type=\"button\"\n className={\n c.Value === value ? \"iam-chip iam-chip-on\" : \"iam-chip\"\n }\n onClick={() =>\n onChange({ ...values, [name]: c.Value ?? \"\" })\n }\n >\n {localized(c.Label, lang()) || c.Value}\n </button>\n ))}\n </div>\n ) : (\n <textarea\n className=\"iam-input\"\n value={value}\n maxLength={x.MaxLength && x.MaxLength > 0 ? x.MaxLength : 500}\n placeholder={localized(x.Placeholder, lang())}\n onChange={(e) =>\n onChange({ ...values, [name]: e.target.value })\n }\n rows={x.Multiline ? 4 : 1}\n />\n )}\n </div>\n );\n })}\n </div>\n );\n}\n\nfunction firstAssetUrl(generation: AIGenerationView): string {\n return (generation.Assets ?? []).find((a) => a.Url)?.Url ?? \"\";\n}\n\nfunction Result({ generation }: { generation: AIGenerationView }): ReactNode {\n return (\n <div className=\"iam-stack\">\n {generation.OutputText ? (\n <p className=\"iam-text\">{generation.OutputText}</p>\n ) : null}\n {(generation.Assets ?? []).map((a, i) => {\n const kind = resultKind(a.ContentType);\n const url = a.Url ?? \"\";\n return kind === \"image\" ? (\n <img key={i} className=\"iam-result\" src={url} alt=\"\" />\n ) : kind === \"audio\" ? (\n <audio key={i} src={url} controls className=\"iam-audio\" />\n ) : kind === \"video\" ? (\n <video\n key={i}\n className=\"iam-result\"\n src={url}\n controls\n autoPlay\n loop\n playsInline\n />\n ) : null;\n })}\n </div>\n );\n}\n\n// ── My works: a grid of what the player made ─────────────────────────────────────────────────\n\nfunction Works({\n items,\n onBack,\n}: {\n items: AppItem[];\n onBack: () => void;\n}): ReactNode {\n const client = useIDosGamesClient();\n const [list, setList] = useState<AIGenerationView[] | null>(null);\n const [open, setOpen] = useState<AIGenerationView | null>(null);\n useEffect(() => {\n void client.ai\n .listGenerations({ Limit: 30 })\n .then((r) => setList(r.ok ? (r.data.Data ?? []) : []));\n }, [client]);\n\n const nameOf = (key: string | null | undefined) => {\n const item = items.find((i) => i.key === key);\n return item?.template\n ? localized(item.template.Name, lang())\n : item\n ? t(\"own\")\n : (key ?? \"\");\n };\n const download = open ? firstAssetUrl(open) : \"\";\n\n return (\n <>\n <header className=\"iam-bar\">\n <button\n type=\"button\"\n className=\"iam-icon-btn\"\n aria-label={t(\"back\")}\n onClick={open ? () => setOpen(null) : onBack}\n >\n <Svg d={ICON.back} />\n </button>\n <span className=\"iam-title\">\n {open ? nameOf(open.FeatureKey) : t(\"history\")}\n </span>\n <span className=\"iam-icon-btn\" aria-hidden />\n </header>\n {open ? (\n <>\n <div className=\"iam-page\">\n <Result generation={open} />\n </div>\n {download ? (\n <footer className=\"iam-dock\">\n <a\n className=\"iam-btn\"\n href={download}\n target=\"_blank\"\n rel=\"noreferrer\"\n download\n >\n <Svg d={ICON.download} size={18} /> {t(\"download\")}\n </a>\n </footer>\n ) : null}\n </>\n ) : !list ? (\n <div className=\"iam-center\">\n <span className=\"iam-spin\" />\n </div>\n ) : list.length === 0 ? (\n <p className=\"iam-empty\">{t(\"noHistory\")}</p>\n ) : (\n <div className=\"iam-grid\">\n {list.map((g) => {\n const asset = (g.Assets ?? []).find((a) => a.Url);\n const kind = resultKind(asset?.ContentType);\n const done = g.Status === \"completed\";\n return (\n <button\n key={g.GenerationID}\n type=\"button\"\n className=\"iam-card\"\n disabled={!done}\n onClick={() => setOpen(g)}\n >\n {done && asset?.Url && kind === \"image\" ? (\n <img className=\"iam-media iam-cover\" src={asset.Url} alt=\"\" />\n ) : done && asset?.Url && kind === \"video\" ? (\n <video\n className=\"iam-media iam-cover\"\n src={asset.Url}\n muted\n playsInline\n preload=\"metadata\"\n />\n ) : (\n <span className=\"iam-own\">\n {g.Status === \"failed\" ? (\n <span className=\"iam-muted\">{t(\"s_failed\")}</span>\n ) : done ? (\n <Svg d={ICON.bot} size={28} />\n ) : (\n <span className=\"iam-spin\" />\n )}\n </span>\n )}\n <span className=\"iam-card-foot\">\n <span className=\"iam-card-name\">{nameOf(g.FeatureKey)}</span>\n </span>\n </button>\n );\n })}\n </div>\n )}\n </>\n );\n}\n\n// ── Icons (lucide shapes, drawn with the current color) ──────────────────────────────────────\n\nconst ICON = {\n back: \"M19 12H5M12 19l-7-7 7-7\",\n works: \"M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z\",\n plus: \"M12 5v14M5 12h14\",\n pen: \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\",\n download: \"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\",\n bot: \"M12 8V4H8M6 8h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2ZM2 14h2M20 14h2M15 13v2M9 13v2\",\n} as const;\n\nfunction Svg({ d, size = 22 }: { d: string; size?: number }): ReactNode {\n return (\n <svg\n width={size}\n height={size}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n >\n <path d={d} />\n </svg>\n );\n}\n"
51
+ },
52
+ {
53
+ "path": "i18n.ts",
54
+ "content": "import { makeT } from \"@idosgames/react/ui\";\n\nconst EN = {\n title: \"AI generator\",\n k_Video: \"Video\",\n k_Image: \"Pictures\",\n k_Audio: \"Voice\",\n k_Music: \"Music\",\n k_ThreeD: \"3D\",\n history: \"My works\",\n trend: \"Trend\",\n own: \"Describe it yourself\",\n describe: \"Describe what you want…\",\n sayWhat: \"Text to say…\",\n back: \"Back\",\n addPhoto: \"Add a photo\",\n optional: \"optional\",\n consent: \"Upload only your own photos or photos of people who agreed to it.\",\n create: \"Create\",\n free: \"Free\",\n freeLeft: \"{n} free today\",\n working: \"Creating…\",\n videoWait:\n \"A video takes a few minutes. You can leave: it will be in My works.\",\n failed: \"Could not create it. Try again.\",\n download: \"Download\",\n again: \"Again\",\n empty: \"The author has not added anything to create yet.\",\n noHistory: \"Nothing created yet.\",\n photoBad: \"This file is not a picture.\",\n s_failed: \"failed\",\n} as const;\n\nexport const t = makeT<keyof typeof EN>(EN, {\n title: \"ИИ-генератор\",\n k_Video: \"Видео\",\n k_Image: \"Картинки\",\n k_Audio: \"Голос\",\n k_Music: \"Музыка\",\n k_ThreeD: \"3D\",\n history: \"Мои работы\",\n trend: \"Тренд\",\n own: \"Своё описание\",\n describe: \"Опишите, что хотите получить…\",\n sayWhat: \"Текст, который нужно озвучить…\",\n back: \"Назад\",\n addPhoto: \"Добавить фото\",\n optional: \"необязательно\",\n consent:\n \"Загружайте только свои фото или фото людей, которые на это согласны.\",\n create: \"Создать\",\n free: \"Бесплатно\",\n freeLeft: \"Сегодня бесплатно: {n}\",\n working: \"Создаём…\",\n videoWait:\n \"Ролик готовится несколько минут. Можно закрыть: он появится в «Мои работы».\",\n failed: \"Не получилось. Попробуйте ещё раз.\",\n download: \"Скачать\",\n again: \"Ещё раз\",\n empty: \"Автор пока ничего не добавил для создания.\",\n noHistory: \"Вы ещё ничего не создали.\",\n photoBad: \"Этот файл не картинка.\",\n s_failed: \"не вышло\",\n});\n"
55
+ },
56
+ {
57
+ "path": "index.ts",
58
+ "content": "// @idosgames/mod-ai-media-app — an AI generator app made of the title's AI features: trends and templates of the\n// platform AI studio with the player's photos, the author's price and free generations per day.\n\nexport { aiMediaAppModule, MODE } from \"./module\";\nexport {\n appItems,\n kindsOf,\n localized,\n missingInputs,\n priceLabelOf,\n type AppItem,\n type Kind,\n} from \"./model\";\n"
59
+ },
60
+ {
61
+ "path": "model.ts",
62
+ "content": "// Pure helpers of the AI generator app: covered by model.test.ts. The app is the title's AI section: the server\n// sends the features sanitized (no prompts, no models), each with its price, its free generations and, for a\n// platform template, the template's card.\n\nimport type {\n AIPublicDefinitions,\n AIPublicFeature,\n AIPublicTemplate,\n AIInputImage,\n} from \"@idosgames/core\";\nimport { costLines, type ResourceLine } from \"@idosgames/react/ui\";\n\n/** What the app makes. Text features are for the game's own code, not for this app. */\nexport type Kind = \"Video\" | \"Image\" | \"Audio\" | \"Music\" | \"ThreeD\";\n\n/** Tab order: video first (trends live there), then pictures, voice, music, 3D. */\nexport const KINDS: readonly Kind[] = [\n \"Video\",\n \"Image\",\n \"Audio\",\n \"Music\",\n \"ThreeD\",\n];\n\n/** A feature kept for the game's own code (NPC lines and the like) carries this tag, as in the AI studio system. */\nexport const HIDDEN_TAG = \"hidden\";\n\nexport interface AppItem {\n key: string;\n kind: Kind;\n /** The platform template the feature runs; absent: \"describe it yourself\" (a plain feature of the kind). */\n template?: AIPublicTemplate;\n trend: boolean;\n /** Ways to pay; empty and not `unpayable`: free for the player. */\n options: Array<{ id: string; price: ResourceLine[] }>;\n /** The feature has a price, but none of its ways to pay works here (Aspis inside the iOS app): no button. */\n unpayable: boolean;\n /** Free generations left today; null: the feature gives none. */\n freeLeft: number | null;\n order: number;\n maxChars: number;\n}\n\n/**\n * The cards of the app, in the order of the tabs, then the template's order. A feature whose template was taken off\n * the site (a `PlatformTemplateId` without its card) is not shown: running it would only answer \"unavailable\".\n */\nexport function appItems(\n section: AIPublicDefinitions | null | undefined,\n /** false: Aspis cannot be paid here (the iOS app, `canPayWithPlatformCredits`), its prices are dropped. */\n creditsPayable = true,\n): AppItem[] {\n if (!section || section.Enabled === false) return [];\n const items: AppItem[] = [];\n for (const [key, f] of Object.entries(section.Features ?? {})) {\n if (!f || (f.Tags ?? []).includes(HIDDEN_TAG)) continue;\n const kind = f.Modality as Kind;\n if (!KINDS.includes(kind)) continue;\n if (f.PlatformTemplateId && !f.PlatformTemplate) continue;\n const template = f.PlatformTemplate ?? undefined;\n const all = optionsOf(f);\n const options = creditsPayable\n ? all\n : all.filter((o) => !o.price.some((l) => l.kind === \"credits\"));\n items.push({\n key,\n kind,\n ...(template ? { template } : {}),\n trend: template?.IsTrend === true,\n options,\n unpayable: all.length > 0 && options.length === 0,\n freeLeft:\n f.FreePerDay && f.FreePerDay > 0\n ? Math.max(0, f.FreeLeftToday ?? 0)\n : null,\n order: template?.Order ?? Number.MAX_SAFE_INTEGER,\n maxChars: Math.max(0, Number(f.MaxPromptChars ?? 0)),\n });\n }\n return items.sort(\n (a, b) =>\n KINDS.indexOf(a.kind) - KINDS.indexOf(b.kind) ||\n Number(!!a.template) - Number(!!b.template) ||\n a.order - b.order ||\n a.key.localeCompare(b.key),\n );\n}\n\nfunction optionsOf(f: AIPublicFeature): AppItem[\"options\"] {\n return Object.entries(f.PriceOptions ?? {})\n .filter(([, o]) => o)\n .map(([oid, o]) => ({\n id: o?.OptionID || oid,\n price: costLines(o?.Cost as never).filter((l) => l.kind !== \"other\"),\n }))\n .filter((o) => o.price.length > 0);\n}\n\n/** Kinds that have at least one card, in tab order. One kind: the app opens straight on it, without tabs. */\nexport function kindsOf(items: readonly AppItem[]): Kind[] {\n return KINDS.filter((k) => items.some((i) => i.kind === k));\n}\n\n/**\n * A text of the template in the player's language: the interface language, then the device's, then English, then\n * any. Templates of the platform carry nine languages; the UI kit speaks English and Russian.\n */\nexport function localized(\n text: Record<string, string> | null | undefined,\n language: string,\n deviceLanguage: string = typeof navigator === \"undefined\"\n ? \"en\"\n : navigator.language,\n): string {\n if (!text) return \"\";\n const device = deviceLanguage.slice(0, 2).toLowerCase();\n return (\n text[language] ||\n text[device] ||\n text.en ||\n Object.values(text).find((v) => !!v) ||\n \"\"\n );\n}\n\n/** Required photo slots the player has not filled yet (their keys). */\nexport function missingInputs(\n template: AIPublicTemplate | undefined,\n files: Readonly<Record<string, AIInputImage | undefined>>,\n): string[] {\n return (template?.Inputs ?? [])\n .filter((i) => i.Required && i.Key && !files[i.Key])\n .map((i) => i.Key!);\n}\n\n/** Required variables left empty (no value and no default). */\nexport function missingValues(\n template: AIPublicTemplate | undefined,\n values: Readonly<Record<string, string | undefined>>,\n): string[] {\n return (template?.Variables ?? [])\n .filter(\n (v) =>\n v.Required && v.Name && !(values[v.Name]?.trim() || v.Default?.trim()),\n )\n .map((v) => v.Name!);\n}\n\n/** What the button costs right now: free (with how many are left), or the first option the player can pay. */\nexport type PriceLabel =\n | { free: true; left: number }\n | { free: false; option?: AppItem[\"options\"][number] };\n\nexport function priceLabelOf(\n item: AppItem,\n affordable: (price: ResourceLine[]) => boolean,\n): PriceLabel {\n if (item.freeLeft !== null && item.freeLeft > 0)\n return { free: true, left: item.freeLeft };\n if (item.options.length === 0)\n return item.unpayable\n ? { free: false }\n : { free: true, left: Number.POSITIVE_INFINITY };\n const option =\n item.options.find((o) => affordable(o.price)) ?? item.options[0];\n return option ? { free: false, option } : { free: false };\n}\n\n/** The longest side of a photo sent to the server: enough for every model, small enough for a phone upload. */\nexport const MAX_PHOTO_SIDE = 1536;\n\n/** The size a photo is scaled to (never up). */\nexport function fitSize(\n width: number,\n height: number,\n max = MAX_PHOTO_SIDE,\n): { width: number; height: number } {\n const longest = Math.max(width, height);\n if (longest <= max || longest <= 0) return { width, height };\n const k = max / longest;\n return { width: Math.round(width * k), height: Math.round(height * k) };\n}\n\n/** What a finished generation shows. */\nexport type ResultKind = \"image\" | \"audio\" | \"video\" | \"file\";\nexport function resultKind(contentType: string | null | undefined): ResultKind {\n const ct = (contentType ?? \"\").toLowerCase();\n if (ct.startsWith(\"image/\")) return \"image\";\n if (ct.startsWith(\"audio/\")) return \"audio\";\n if (ct.startsWith(\"video/\")) return \"video\";\n return \"file\";\n}\n"
63
+ },
64
+ {
65
+ "path": "module.ts",
66
+ "content": "import { defineModule, type Module } from \"@idosgames/module-sdk\";\nimport { MediaApp } from \"./App\";\nimport { t } from \"./i18n\";\n\nexport const MODE = \"ai-media-app\";\n\n// The AI generator app: the whole screen, no lobby around it and no systems of the base (\"baseSystems\": false in\n// module.meta.json, owner 09.11.2026: an app looks like idosgames.com, not like a game, and the fewer buttons the\n// better). The app starts in it after sign-in and never leads anywhere else (`ownExit`: no button back).\n// Everything it does is the title's AI section: a feature with a PlatformTemplateId is a card of a platform template\n// or trend, a plain feature of a kind is \"describe it yourself\"; the price, the free generations per day and the\n// cards come from the engine. Decisions: docs/feature-history/ai-media-app.md at the platform root.\nexport const aiMediaAppModule: Module = defineModule({\n id: \"ai-media-app\",\n // \"game\": for the base this is the mode the app starts in.\n meta: {\n name: \"AI generator\",\n type: \"game\",\n genre: \"ai-media\",\n engine: \"dom\",\n // Sign-in and loading look like idosgames.com too, not like a game.\n look: \"site\",\n },\n setup(ctx) {\n ctx.registerPanel({\n id: \"media-app\",\n slot: \"overlay\",\n component: MediaApp,\n });\n ctx.registerRoute({\n id: MODE,\n label: t(\"title\"),\n icon: \"🤖\",\n ownExit: true,\n });\n ctx.exposeToAgent({\n state: () => ({ ui: \"dom\", mode: MODE }),\n describeActions: {},\n });\n },\n});\n"
67
+ },
68
+ {
69
+ "path": "styles.ts",
70
+ "content": "// The look of idosgames.com (owner 09.11.2026: an app looks like the site, not like a game). Tokens are the site's\n// dark theme (iDosGames_com/app/config.reui.css): zinc background and cards, the site's blue, Onest. Plain CSS under\n// one `.iam` root, so nothing of the UI kit's game look (outlined text, gold buttons) leaks in.\n\n/** Onest, the site's font. Fallbacks are the same as on the site. */\nexport const FONT_URL =\n \"https://fonts.googleapis.com/css2?family=Onest:wght@400;500;600;700&display=swap\";\n\nexport const APP_CSS = `\n.iam {\n --iam-bg: #09090b;\n --iam-card: #18181b;\n --iam-card-hover: #1f1f23;\n --iam-border: #27272a;\n --iam-fg: #fafafa;\n --iam-muted: #a1a1aa;\n --iam-dim: #71717a;\n --iam-primary: #0d66fe;\n --iam-primary-hover: #2b7bff;\n --iam-green: #22c55e;\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n background: var(--iam-bg);\n color: var(--iam-fg);\n font-family: 'Onest', 'Segoe UI', system-ui, -apple-system, sans-serif;\n font-size: 15px;\n line-height: 1.45;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n -webkit-font-smoothing: antialiased;\n}\n.iam *, .iam *::before, .iam *::after { box-sizing: border-box; }\n/* :where: zero weight, so the classes below (chips, buttons) set their own colour. */\n.iam :where(button) { font: inherit; color: inherit; }\n\n.iam-bar {\n position: sticky; top: 0; z-index: 2;\n display: flex; align-items: center; gap: 8px;\n height: 56px; padding: 0 12px;\n background: rgba(9, 9, 11, 0.86);\n backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);\n border-bottom: 1px solid var(--iam-border);\n}\n.iam-title {\n flex: 1; min-width: 0;\n font-size: 17px; font-weight: 600;\n white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n}\n.iam-bar .iam-icon-btn:first-child + .iam-title { text-align: center; }\n.iam-icon-btn {\n width: 40px; height: 40px; flex: none;\n display: grid; place-items: center;\n border: none; border-radius: 10px; background: transparent;\n color: var(--iam-fg); cursor: pointer;\n}\nbutton.iam-icon-btn:hover { background: var(--iam-card); }\n\n.iam-chips {\n display: flex; gap: 8px; padding: 12px 12px 0;\n overflow-x: auto; scrollbar-width: none;\n}\n.iam-chips::-webkit-scrollbar { display: none; }\n.iam-chips-wrap { flex-wrap: wrap; padding: 0; overflow: visible; }\n.iam-chip {\n flex: none; height: 36px; padding: 0 14px;\n border: 1px solid var(--iam-border); border-radius: 999px;\n background: transparent; color: var(--iam-muted);\n font-size: 14px; font-weight: 500; cursor: pointer;\n}\n.iam-chip:hover { color: var(--iam-fg); }\n.iam-chip-on { background: var(--iam-fg); border-color: var(--iam-fg); color: var(--iam-bg); }\n.iam-chip-on:hover { color: var(--iam-bg); }\n\n.iam-grid {\n display: grid; gap: 10px; padding: 12px;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n width: 100%; max-width: 1100px; margin: 0 auto;\n}\n@media (min-width: 640px) { .iam-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }\n@media (min-width: 960px) { .iam-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }\n\n.iam-card {\n position: relative; display: block; width: 100%;\n aspect-ratio: 3 / 4; padding: 0; overflow: hidden;\n border: 1px solid var(--iam-border); border-radius: 14px;\n background: var(--iam-card); cursor: pointer; text-align: left;\n transition: transform .15s ease, border-color .15s ease;\n}\n.iam-card:hover { border-color: #3f3f46; }\n.iam-card:active { transform: scale(.98); }\n.iam-card:disabled { cursor: default; }\n.iam-tall { aspect-ratio: 9 / 16; }\n.iam-media { display: block; width: 100%; height: 100%; object-fit: contain; }\n.iam-cover { object-fit: cover; }\n.iam-own {\n position: absolute; inset: 0;\n display: grid; place-items: center;\n color: var(--iam-dim);\n}\n.iam-badge {\n position: absolute; top: 8px; left: 8px;\n padding: 2px 8px; border-radius: 999px;\n background: var(--iam-primary); color: #fff;\n font-size: 12px; font-weight: 600;\n}\n.iam-card-foot {\n position: absolute; left: 0; right: 0; bottom: 0;\n display: flex; flex-direction: column; align-items: flex-start; gap: 6px;\n padding: 28px 10px 10px;\n background: linear-gradient(to top, rgba(0,0,0,.82), rgba(0,0,0,0));\n}\n.iam-card-name {\n font-size: 14px; font-weight: 600; color: #fff;\n display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;\n}\n.iam-price {\n display: inline-flex; align-items: center; gap: 6px;\n padding: 2px 8px; border-radius: 999px;\n background: rgba(24, 24, 27, .82); border: 1px solid var(--iam-border);\n font-size: 12px; font-weight: 600; color: var(--iam-fg);\n}\n.iam-free { color: var(--iam-green); }\n.iam-price-line { display: inline-flex; align-items: center; gap: 4px; }\n\n.iam-page {\n display: flex; flex-direction: column; gap: 14px;\n width: 100%; max-width: 560px; margin: 0 auto; padding: 14px 16px 24px;\n flex: 1;\n}\n.iam-hero {\n position: relative; width: 100%; max-height: 40vh; aspect-ratio: 3 / 4;\n border-radius: 16px; overflow: hidden;\n background: var(--iam-card); border: 1px solid var(--iam-border);\n margin: 0 auto;\n}\n.iam-hero.iam-tall { aspect-ratio: 9 / 16; width: auto; height: 40vh; max-width: 100%; }\n.iam-working {\n position: absolute; inset: 0;\n display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;\n padding: 16px; text-align: center;\n background: rgba(9, 9, 11, .72); font-weight: 600;\n}\n.iam-stack { display: flex; flex-direction: column; gap: 10px; }\n.iam-field { display: flex; flex-direction: column; gap: 8px; }\n.iam-label { font-size: 14px; font-weight: 500; }\n.iam-muted { margin: 0; color: var(--iam-muted); font-size: 14px; font-weight: 400; }\n.iam-note { margin: 0; color: var(--iam-dim); font-size: 12px; }\n.iam-text { margin: 0; white-space: pre-wrap; }\n.iam-empty { margin: 48px 16px; text-align: center; color: var(--iam-muted); }\n\n.iam-photos { display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }\n.iam-photos.iam-one { grid-template-columns: minmax(0, 1fr); }\n.iam-photo {\n position: relative; aspect-ratio: 3 / 4; overflow: hidden; cursor: pointer;\n border: 1.5px dashed #3f3f46; border-radius: 14px; background: var(--iam-card);\n}\n.iam-one .iam-photo { aspect-ratio: auto; height: 132px; }\n.iam-photo:hover { border-color: var(--iam-primary); }\n.iam-photo-empty {\n position: absolute; inset: 0;\n display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;\n padding: 8px; text-align: center; font-size: 14px; font-weight: 500; color: var(--iam-fg);\n}\n.iam-photo-empty svg { color: var(--iam-primary); }\n\n.iam-input {\n width: 100%; resize: vertical; padding: 12px;\n border: 1px solid var(--iam-border); border-radius: 12px;\n background: var(--iam-card); color: var(--iam-fg);\n font: inherit; font-size: 16px; outline: none;\n}\n.iam-input:focus { border-color: var(--iam-primary); }\n\n.iam-result { width: 100%; border-radius: 16px; display: block; background: var(--iam-card); }\n.iam-audio { width: 100%; }\n\n.iam-dock {\n position: sticky; bottom: 0; z-index: 2;\n display: flex; gap: 10px;\n padding: 12px 16px calc(12px + env(safe-area-inset-bottom));\n background: linear-gradient(to top, var(--iam-bg) 70%, rgba(9,9,11,0));\n}\n.iam-dock > * { flex: 1; }\n.iam-btn {\n height: 52px; padding: 0 18px;\n display: inline-flex; align-items: center; justify-content: center; gap: 8px;\n border: none; border-radius: 12px;\n background: var(--iam-primary); color: #fff;\n font-size: 16px; font-weight: 600; text-decoration: none; cursor: pointer;\n width: 100%; max-width: 528px; margin: 0 auto;\n}\n.iam-btn:hover { background: var(--iam-primary-hover); }\n.iam-btn:disabled { background: #27272a; color: var(--iam-dim); cursor: default; }\n.iam-btn-ghost { background: var(--iam-card); border: 1px solid var(--iam-border); color: var(--iam-fg); }\n.iam-btn-ghost:hover { background: var(--iam-card-hover); }\n.iam-btn-price { display: inline-flex; align-items: center; gap: 6px; font-weight: 500; opacity: .92; }\n\n.iam-center { flex: 1; display: grid; place-items: center; }\n.iam-spin {\n width: 28px; height: 28px; border-radius: 50%;\n border: 3px solid rgba(255,255,255,.15); border-top-color: var(--iam-primary);\n animation: iam-spin .8s linear infinite;\n}\n.iam-spin-sm { width: 18px; height: 18px; border-width: 2px; border-top-color: #fff; }\n@keyframes iam-spin { to { transform: rotate(360deg); } }\n`;\n"
71
+ },
72
+ {
73
+ "path": "module.meta.json",
74
+ "content": "{\n \"id\": \"ai-media-app\",\n \"type\": \"template\",\n \"baseSystems\": false,\n \"summary\": \"An AI generator app: players repeat trends and templates of the platform AI studio with their own photos (video, pictures, voice, music, 3D), paying the author's price; a few free generations a day at the author's cost.\",\n \"description\": \"A whole app on the full screen in the look of idosgames.com (plain React and CSS: dark zinc, the site's blue, Onest; no engine, no game look), the 'AI generator' base. No lobby and no systems of the base (\\\"baseSystems\\\": false): no inventory, quests, friends or mail; after sign-in the app opens straight on its cards. Everything the app does is the title's AI section, not code: every AI feature with a PlatformTemplateId is a card of a platform template or trend (name, the sample, photo slots, choices), a plain feature of a kind is 'describe it yourself'. A new title gets up to six ready trends with a default Aspis price. A kind (video, picture, voice, music, 3D) shows only when it has features; several kinds get chips on top. As few buttons as possible: one grid of big cards (trends first), a tap opens the card with its photo slot and ONE button with the price (free while the feature's FreePerDay lasts, the author pays; then Aspis confirmed on idosgames.com or in-game currency, charged only when the result is ready); the result plays right away with Download and Again; 'My works' is one icon in the header. The template's prompt and the model never reach the player: the engine runs the template (GenerateFromTemplate). Edit the app by editing the AI features (the dashboard's AI generator page or LiveOps → AI), not the code.\",\n \"provides\": [\n \"an AI generator app from the title's AI features (no code per app)\",\n \"the look of idosgames.com, no lobby and no systems of the base\",\n \"ready trends in a new title: repeat a trending video with your own photo\",\n \"templates of the platform AI studio as cards: video, picture, voice, music, 3D\",\n \"'describe it yourself' for a kind with a plain AI feature\",\n \"photo slots with downscaling on the device\",\n \"one button with the price: in-game currency or Aspis, free generations per day at the author's cost\",\n \"result with download and again, the player's works\"\n ],\n \"tags\": [\n \"ai\",\n \"generation\",\n \"trends\",\n \"templates\",\n \"video\",\n \"photo\",\n \"app\",\n \"ugc\"\n ],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.0\"\n}\n"
75
+ }
76
+ ]
77
+ }
@@ -30,9 +30,9 @@
30
30
  "version": "0.1.3"
31
31
  },
32
32
  "dependencies": {
33
- "@idosgames/core": "0.26.1",
34
- "@idosgames/module-sdk": "0.13.3",
35
- "@idosgames/react": "0.7.1",
33
+ "@idosgames/core": "0.26.2",
34
+ "@idosgames/module-sdk": "0.13.4",
35
+ "@idosgames/react": "0.7.2",
36
36
  "react": "19.2.7"
37
37
  },
38
38
  "contentHash": "94fa2be0fc5eff91bef20eb4ee063955c64d6df105066bafe384f5ec0be924a7",
@@ -45,9 +45,9 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@idosgames/core": "0.26.1",
49
- "@idosgames/module-sdk": "0.13.3",
50
- "@idosgames/react": "0.7.1",
48
+ "@idosgames/core": "0.26.2",
49
+ "@idosgames/module-sdk": "0.13.4",
50
+ "@idosgames/react": "0.7.2",
51
51
  "@idosgames/wallet": "0.6.10",
52
52
  "@tanstack/react-query": "5.101.2",
53
53
  "react": "19.2.7",
@@ -31,9 +31,9 @@
31
31
  "version": "0.1.1"
32
32
  },
33
33
  "dependencies": {
34
- "@idosgames/core": "0.26.1",
35
- "@idosgames/module-sdk": "0.13.3",
36
- "@idosgames/react": "0.7.1",
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": "d79e71eb438f32329a34d0aeb4b9dcf7e7a8c6dac168224951a85a50f7bf84b0",
@@ -54,9 +54,9 @@
54
54
  }
55
55
  },
56
56
  "dependencies": {
57
- "@idosgames/core": "0.26.1",
58
- "@idosgames/module-sdk": "0.13.3",
59
- "@idosgames/react": "0.7.1",
57
+ "@idosgames/core": "0.26.2",
58
+ "@idosgames/module-sdk": "0.13.4",
59
+ "@idosgames/react": "0.7.2",
60
60
  "react": "19.2.7"
61
61
  },
62
62
  "contentHash": "92f7f5539093cf9793ff5a366db0583ed1e250b9bab95763a14ee49e0739e015",
@@ -35,9 +35,9 @@
35
35
  "version": "0.1.2"
36
36
  },
37
37
  "dependencies": {
38
- "@idosgames/core": "0.26.1",
39
- "@idosgames/module-sdk": "0.13.3",
40
- "@idosgames/react": "0.7.1",
38
+ "@idosgames/core": "0.26.2",
39
+ "@idosgames/module-sdk": "0.13.4",
40
+ "@idosgames/react": "0.7.2",
41
41
  "react": "19.2.7"
42
42
  },
43
43
  "contentHash": "8ee2540d83607f36c770ed998f2a3806edac1c0373b89a3425549f3f80f0e9ff",
@@ -34,9 +34,9 @@
34
34
  "version": "0.1.3"
35
35
  },
36
36
  "dependencies": {
37
- "@idosgames/core": "0.26.1",
38
- "@idosgames/module-sdk": "0.13.3",
39
- "@idosgames/react": "0.7.1",
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": "4a0b7e2e2ee14fd0ebf5ae7cb09246895b158e9ff62d372a7e18a041a3d54fbe",
@@ -32,9 +32,9 @@
32
32
  "version": "0.1.3"
33
33
  },
34
34
  "dependencies": {
35
- "@idosgames/core": "0.26.1",
36
- "@idosgames/module-sdk": "0.13.3",
37
- "@idosgames/react": "0.7.1",
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": "6af34c48b5056a655586dd2351d4b370756b9244b1aeda980c667b1c962428e5",
@@ -35,9 +35,9 @@
35
35
  "version": "0.1.2"
36
36
  },
37
37
  "dependencies": {
38
- "@idosgames/core": "0.26.1",
39
- "@idosgames/module-sdk": "0.13.3",
40
- "@idosgames/react": "0.7.1",
38
+ "@idosgames/core": "0.26.2",
39
+ "@idosgames/module-sdk": "0.13.4",
40
+ "@idosgames/react": "0.7.2",
41
41
  "react": "19.2.7"
42
42
  },
43
43
  "contentHash": "f59536188295910902674239c662a7ed6eca971ba4820ade716a25e56f21ccf3",
@@ -34,9 +34,9 @@
34
34
  "version": "0.1.3"
35
35
  },
36
36
  "dependencies": {
37
- "@idosgames/core": "0.26.1",
38
- "@idosgames/module-sdk": "0.13.3",
39
- "@idosgames/react": "0.7.1",
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": "c4c7f012dbd1a629ebfd9276d872fad99f9b7254bf87279c6b2fcfa5fa7d0e53",
@@ -8,7 +8,7 @@
8
8
  "catalog": {
9
9
  "type": "feature",
10
10
  "summary": "Limited-time events for any game: event tabs with a timer, a token bar, a milestone track, claim all and a VIP second prize.",
11
- "description": "A ready events screen (Unity Core/Window LimitedTimeEvent) for ANY game, built on the @idosgames/react/ui kit and client.timedEvent. Every active event the server resolves (scheduled, cyclic or chained) is a tab: its name and timer, the token bar toward the next prize and the milestone track in threshold order. A milestone pays its reward and, when the reward has premium tiers, a VIP second prize; a pass bought later tops up the milestones already claimed. 'Claim all' takes everything waiting in one call. How the tokens are earned (the event's token sources with their daily caps) is a popup; 'Get VIP' opens the VIP system (ctx.features.open('premium', { premiumID })) and shows only when it is installed and has a pass. Registers the 'events' feature; its badge counts milestones and VIP top-ups waiting. Tokens come from the game: a round of play is a CustomAction the game grants with client.timedEvent.grantTokens (the mini-game module does it for every active event that lists the action). Ships title-seed.json: a weekly 'Star festival' with five milestones and a VIP prize on each.",
11
+ "description": "A ready events screen (Unity Core/Window LimitedTimeEvent) for ANY game, built on the @idosgames/react/ui kit and client.timedEvent. Every active event the server resolves (scheduled, cyclic or chained) is a tab: its name and timer, the token bar toward the next prize and the milestone track in threshold order. A milestone pays its reward and, when the reward has premium tiers, a VIP second prize; a pass bought later tops up the milestones already claimed. 'Claim all' takes everything waiting in one call. How the tokens are earned (the event's token sources with their daily caps) is a popup; 'Get VIP' opens the VIP system (ctx.features.open('premium', { premiumID })) and shows only when it is installed and has a pass. Registers the 'events' feature; its badge counts milestones and VIP top-ups waiting. Tokens come from the game: a round of play is a CustomAction the game grants with client.timedEvent.grantTokens (kit-game does it at the end of each round, for every active event that lists the action play_round). Ships title-seed.json: a weekly 'Star festival' with five milestones and a VIP prize on each.",
12
12
  "provides": [
13
13
  "events screen with a tab per active event",
14
14
  "event timer and token progress bar",
@@ -37,12 +37,12 @@
37
37
  "version": "0.1.5"
38
38
  },
39
39
  "dependencies": {
40
- "@idosgames/core": "0.26.1",
41
- "@idosgames/module-sdk": "0.13.3",
42
- "@idosgames/react": "0.7.1",
40
+ "@idosgames/core": "0.26.2",
41
+ "@idosgames/module-sdk": "0.13.4",
42
+ "@idosgames/react": "0.7.2",
43
43
  "react": "19.2.7"
44
44
  },
45
- "contentHash": "97ba661e41bf3aa20fa109b3a102907fb94cac6bb66a4e101e7a0d539c78e4e9",
45
+ "contentHash": "c68e0d562de87cd69c79ee3d60e1636875da6883ca24e45f320e9861e0b319ff",
46
46
  "files": [
47
47
  {
48
48
  "path": "EventsScreen.tsx",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  {
68
68
  "path": "module.meta.json",
69
- "content": "{\n \"id\": \"events\",\n \"type\": \"feature\",\n \"summary\": \"Limited-time events for any game: event tabs with a timer, a token bar, a milestone track, claim all and a VIP second prize.\",\n \"description\": \"A ready events screen (Unity Core/Window LimitedTimeEvent) for ANY game, built on the @idosgames/react/ui kit and client.timedEvent. Every active event the server resolves (scheduled, cyclic or chained) is a tab: its name and timer, the token bar toward the next prize and the milestone track in threshold order. A milestone pays its reward and, when the reward has premium tiers, a VIP second prize; a pass bought later tops up the milestones already claimed. 'Claim all' takes everything waiting in one call. How the tokens are earned (the event's token sources with their daily caps) is a popup; 'Get VIP' opens the VIP system (ctx.features.open('premium', { premiumID })) and shows only when it is installed and has a pass. Registers the 'events' feature; its badge counts milestones and VIP top-ups waiting. Tokens come from the game: a round of play is a CustomAction the game grants with client.timedEvent.grantTokens (the mini-game module does it for every active event that lists the action). Ships title-seed.json: a weekly 'Star festival' with five milestones and a VIP prize on each.\",\n \"provides\": [\n \"events screen with a tab per active event\",\n \"event timer and token progress bar\",\n \"milestone track with rewards\",\n \"VIP second prize and top-up after purchase\",\n \"claim all milestones\",\n \"how-to-earn popup with daily caps\",\n \"hand-off to the VIP system\",\n \"events badge\",\n \"demo weekly event\"\n ],\n \"tags\": [\n \"events\",\n \"live-ops\",\n \"limited-time\",\n \"milestones\",\n \"battle-pass\",\n \"vip\",\n \"ui\",\n \"screen\"\n ],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.5\"\n}\n"
69
+ "content": "{\n \"id\": \"events\",\n \"type\": \"feature\",\n \"summary\": \"Limited-time events for any game: event tabs with a timer, a token bar, a milestone track, claim all and a VIP second prize.\",\n \"description\": \"A ready events screen (Unity Core/Window LimitedTimeEvent) for ANY game, built on the @idosgames/react/ui kit and client.timedEvent. Every active event the server resolves (scheduled, cyclic or chained) is a tab: its name and timer, the token bar toward the next prize and the milestone track in threshold order. A milestone pays its reward and, when the reward has premium tiers, a VIP second prize; a pass bought later tops up the milestones already claimed. 'Claim all' takes everything waiting in one call. How the tokens are earned (the event's token sources with their daily caps) is a popup; 'Get VIP' opens the VIP system (ctx.features.open('premium', { premiumID })) and shows only when it is installed and has a pass. Registers the 'events' feature; its badge counts milestones and VIP top-ups waiting. Tokens come from the game: a round of play is a CustomAction the game grants with client.timedEvent.grantTokens (kit-game does it at the end of each round, for every active event that lists the action play_round). Ships title-seed.json: a weekly 'Star festival' with five milestones and a VIP prize on each.\",\n \"provides\": [\n \"events screen with a tab per active event\",\n \"event timer and token progress bar\",\n \"milestone track with rewards\",\n \"VIP second prize and top-up after purchase\",\n \"claim all milestones\",\n \"how-to-earn popup with daily caps\",\n \"hand-off to the VIP system\",\n \"events badge\",\n \"demo weekly event\"\n ],\n \"tags\": [\n \"events\",\n \"live-ops\",\n \"limited-time\",\n \"milestones\",\n \"battle-pass\",\n \"vip\",\n \"ui\",\n \"screen\"\n ],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.5\"\n}\n"
70
70
  }
71
71
  ]
72
72
  }
@@ -35,9 +35,9 @@
35
35
  "version": "0.1.3"
36
36
  },
37
37
  "dependencies": {
38
- "@idosgames/core": "0.26.1",
39
- "@idosgames/module-sdk": "0.13.3",
40
- "@idosgames/react": "0.7.1",
38
+ "@idosgames/core": "0.26.2",
39
+ "@idosgames/module-sdk": "0.13.4",
40
+ "@idosgames/react": "0.7.2",
41
41
  "react": "19.2.7"
42
42
  },
43
43
  "contentHash": "cb4b66936fa31747a116946730b9fc2576262e68220570b149de0dbf3bb9a943",
@@ -70,9 +70,9 @@
70
70
  },
71
71
  "dependencies": {
72
72
  "@idosgames/characters": "0.1.3",
73
- "@idosgames/core": "0.26.1",
74
- "@idosgames/module-sdk": "0.13.3",
75
- "@idosgames/react": "0.7.1",
73
+ "@idosgames/core": "0.26.2",
74
+ "@idosgames/module-sdk": "0.13.4",
75
+ "@idosgames/react": "0.7.2",
76
76
  "phaser": "4.2.1",
77
77
  "react": "19.2.7"
78
78
  },
@@ -76,9 +76,9 @@
76
76
  },
77
77
  "dependencies": {
78
78
  "@idosgames/characters": "0.1.3",
79
- "@idosgames/core": "0.26.1",
80
- "@idosgames/module-sdk": "0.13.3",
81
- "@idosgames/react": "0.7.1",
79
+ "@idosgames/core": "0.26.2",
80
+ "@idosgames/module-sdk": "0.13.4",
81
+ "@idosgames/react": "0.7.2",
82
82
  "phaser": "4.2.1",
83
83
  "react": "19.2.7"
84
84
  },
@@ -32,9 +32,9 @@
32
32
  "version": "0.1.1"
33
33
  },
34
34
  "dependencies": {
35
- "@idosgames/core": "0.26.1",
36
- "@idosgames/module-sdk": "0.13.3",
37
- "@idosgames/react": "0.7.1",
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": "0b02bc09a1a29ecb6d08fa281dfe316b01f8f341350ba27f386a05572827700e",
@@ -35,9 +35,9 @@
35
35
  "version": "0.1.3"
36
36
  },
37
37
  "dependencies": {
38
- "@idosgames/core": "0.26.1",
39
- "@idosgames/module-sdk": "0.13.3",
40
- "@idosgames/react": "0.7.1",
38
+ "@idosgames/core": "0.26.2",
39
+ "@idosgames/module-sdk": "0.13.4",
40
+ "@idosgames/react": "0.7.2",
41
41
  "react": "19.2.7"
42
42
  },
43
43
  "contentHash": "0e2ffb620e408e8a97d16de8ab88b36c09e7295d3e84d4f36f746e9beb08666b",