@idosgames/mcp 0.1.13 → 0.1.14
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/package.json +1 -1
- package/registry/host.json +6 -6
- package/registry/index.json +95 -23
- package/registry/modules/board-game.json +2 -2
- package/registry/modules/game-hud.json +2 -2
- package/registry/modules/idle-rpg.json +2 -2
- package/registry/modules/voxelcraft.json +1399 -79
- package/registry/modules/workshop.json +66 -0
- package/registry/skills/ai-generation-system.json +1 -1
- package/registry/skills/analytics-events.json +6 -0
- package/registry/skills/cloud-code.json +2 -2
- package/registry/skills/data-collections.json +6 -0
- package/registry/skills/experiments-system.json +6 -0
- package/registry/skills/idosgames-module-contract.json +2 -2
- package/registry/skills/idosgames-project-structure.json +1 -1
- package/registry/skills/voxelcraft-worlds.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idosgames/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "MCP server that serves the iDosGames Module & Skills Registry to AI coding agents (Claude Code, Codex, Cursor…): list/pull composable game modules and the host scaffold, and load skills for @idosgames/core, the module contract, and composition.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/registry/host.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"path": "IDOS.md",
|
|
18
|
-
"content": "# IDOS.md — project guide\n\nEvery AI agent working on this game reads this file first: the platform's AI editor loads it into\ncontext on every run, and external agents reach it through `AGENTS.md` / `CLAUDE.md`. Keep it short\nand evergreen — what the game is, how the project is laid out, rules that hold for all of it. The\nhistory of individual features does NOT belong here (see \"Feature history\" at the end).\n\n## This game\n\n<!-- 2–4 lines, filled in once the direction is clear: genre, core loop, target platform, tone.\n Update it when the concept changes — not with every feature. -->\n\n_Not described yet._\n\n## Installed modules\n\nMirror of the `modules` array in `src/modules.ts` (the source of truth), maintained by the\nplatform — do not edit by hand.\n\n<!-- idos:modules:start -->\n\n_None yet._\n<!-- idos:modules:end -->\n\n## How the project is built\n\nA **host shell + composable modules** app on the iDosGames SDK.\n\n- `src/main.tsx` — the host: creates ONE `IDosGamesClient` and calls `mountHost(...)`. The host signs\n the player in (login screen included) before any module runs; modules never create a client or\n log in themselves.\n- `src/modules.ts` — the composition list. **Regenerated by the platform** whenever modules are\n installed or removed, so it holds only the imports and the `modules` array — nothing else.\n- `src/modules/<id>/` — one folder per module: ready-made catalog templates/features and the game's\n own modules alike. With two or more modules the host shows a bottom nav, one tab per module.\n- `src/shared/` — created when needed: code two or more of this game's modules share.\n- `src/idos.title.ts` — **generated, do not edit.** The project's identity: which Title's data this\n game reads and writes, baked in so a packaged mobile build or an iframe embed still knows it.\n- `src/config.ts`
|
|
18
|
+
"content": "# IDOS.md — project guide\n\nEvery AI agent working on this game reads this file first: the platform's AI editor loads it into\ncontext on every run, and external agents reach it through `AGENTS.md` / `CLAUDE.md`. Keep it short\nand evergreen — what the game is, how the project is laid out, rules that hold for all of it. The\nhistory of individual features does NOT belong here (see \"Feature history\" at the end).\n\n## This game\n\n<!-- 2–4 lines, filled in once the direction is clear: genre, core loop, target platform, tone.\n Update it when the concept changes — not with every feature. -->\n\n_Not described yet._\n\n## Installed modules\n\nMirror of the `modules` array in `src/modules.ts` (the source of truth), maintained by the\nplatform — do not edit by hand.\n\n<!-- idos:modules:start -->\n\n_None yet._\n<!-- idos:modules:end -->\n\n## How the project is built\n\nA **host shell + composable modules** app on the iDosGames SDK.\n\n- `src/main.tsx` — the host: creates ONE `IDosGamesClient` and calls `mountHost(...)`. The host signs\n the player in (login screen included) before any module runs; modules never create a client or\n log in themselves.\n- `src/modules.ts` — the composition list. **Regenerated by the platform** whenever modules are\n installed or removed, so it holds only the imports and the `modules` array — nothing else.\n- `src/modules/<id>/` — one folder per module: ready-made catalog templates/features and the game's\n own modules alike. With two or more modules the host shows a bottom nav, one tab per module.\n- `src/shared/` — created when needed: code two or more of this game's modules share.\n- `src/idos.title.ts` — **generated, do not edit.** The project's identity: which Title's data this\n game reads and writes, baked in so a packaged mobile build or an iframe embed still knows it.\n- `src/config.ts` — **platform-owned, do not edit** (the build packs the platform's copy over it).\n Resolves the effective title and environment: identity from `src/idos.title.ts`, DEV vs PROD from\n the address (`{id}-dev.idos.games` vs `{id}.idos.games`) — never from the link's query. Never read\n the title from the URL yourself: use `client.titleID`, or `ctx.titleId` inside a module's `setup()`.\n- `src/env.ts` — build-time values from `.env.local` (local development only).\n- `src/LoginScreen.tsx` — the login screen; restyle it freely.\n- `idos.modules.lock.json` — **platform-owned, do not edit.** Which modules came from the catalog,\n their versions and file fingerprints — so updating a module never silently overwrites your edits.\n\n## Where new code goes\n\nFull standard: the `idosgames-project-structure` skill — load it before creating a module, touching\n`src/shared/`, or moving code around.\n\n- A catalog module already does it → install that module instead of rebuilding it.\n- It extends an existing module's gameplay → change that module.\n- It is its own mode, screen or system → a NEW module `src/modules/<feature-id>/` (kebab-case):\n `index.ts` exporting `<camelCaseId>Module`, `module.ts`, `module.meta.json`, and as needed\n `components/` (React UI), `game/` (engine & simulation), `data/` (static tables), `react/` (hooks,\n context). Register it in `src/modules.ts`.\n- Balances, wallet, status line, Log out / player ID shown in every mode → never rebuilt per module:\n install `game-hud` (or change it). Templates hide their own copies by themselves\n (`ctx.sharedUi.shouldDraw`) — don't edit them for that. Other UI for every mode → a panel with\n `activeOnly: false`.\n- Modules never reach into each other: a module imports only its own folder, `src/shared/` and npm\n packages — never another module's files or host files. Durable shared state lives in the SDK\n client; live signals go through `ctx.events` with typed topics\n (`defineTopic(\"<module-id>:<event>@1\", shape({…}))`) declared in the module's `module.meta.json`;\n to listen to another module, copy its topic, never import it.\n- `src/shared/` holds code TWO or more modules need (code one module needs stays inside it), by\n purpose — `shared/ui/`, `shared/types/`, `shared/utils/`: types, constants, UI, pure helpers; no\n game state or logic. It never imports a module.\n- When a change adds a new responsibility to a file past ~400 lines, put that part in its own file.\n\n## The SDK is npm packages — its source is NOT in this project\n\n`@idosgames/core`, `@idosgames/react`, `@idosgames/module-sdk`, `@idosgames/app-shell` are\ndependencies in `package.json`. There is no `node_modules` to read and no `.d.ts` to inspect here,\nso **you cannot discover the SDK's API by reading files in this project.**\n\n1. **Load the matching skill first.** Skills are the authoritative recipes for this SDK — one per\n service (store, currency, item, quest, leaderboard, auth, blockchain, …).\n2. Then copy the patterns from the modules already in `src/modules/`.\n3. Never invent an SDK method, hook, or type name — it looks plausible and fails the build.\n\n`client.<service>.<action>()` returns `OperationResult<T>` (check `isOk` / `isFail`; it does not\nthrow). In React, reach the client with `useIDosGamesClient()` and player state with\n`useUserState()` from `@idosgames/react`.\n\n## Backend features live in the Title configuration, not in this repo\n\nCurrencies, items, characters, store offers, quests, lootboxes, leaderboards and seasons are\nconfigured per environment in the platform. There is **no config file in this project** — do not\ncreate `config/*.json`; it would be ignored. Write code against ids that exist in the title; if a\nfeature needs an entity that is not configured yet, name exactly what has to be added instead of\ninventing ids.\n\n## Stack & running\n\n- React 19 + TypeScript (strict) + Vite 8. Game engines (three.js, Phaser) arrive with the modules\n that use them.\n- In the platform's AI editor there is no terminal and no package install: the project is built on\n the server after each turn and shown in the live preview. Use only what `package.json` lists.\n- Locally (exported project or an external agent): `npm install`, then `npm run dev`,\n `npm run build`, `npm run typecheck`.\n- Client code ships to players: never put secrets, keys or tokens in any file here.\n- Assets are not stored in the project — generated images/audio/3D are CDN URLs; reference those.\n\n## Feature history — how this project remembers\n\nThis file stays short on purpose: it is loaded on every run, so every paragraph here is paid for\nby all future work. When a game system or feature is done and something about it is worth\nremembering that the code does not show — why it is designed this way, a non-obvious constraint,\na decision the user made — write it to `docs/feature-history/<slug>.md` (one file per system;\nupdate that file when the system changes) and add ONE line to `docs/feature-history/README.md`:\n`- [Title](slug.md) — one-line gist`. Never append a feature write-up to this file. Those entries\nare not loaded automatically: open the relevant one before changing that system.\n"
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"path": "index.html",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "package.json",
|
|
26
|
-
"content": "{\n \"name\": \"@idosgames/host-starter\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The seed project for the AI Coder: a host shell that composes feature modules. Fresh projects start here with zero modules; the developer/agent plugs modules into src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.3.
|
|
26
|
+
"content": "{\n \"name\": \"@idosgames/host-starter\",\n \"version\": \"0.0.0\",\n \"private\": true,\n \"type\": \"module\",\n \"description\": \"The seed project for the AI Coder: a host shell that composes feature modules. Fresh projects start here with zero modules; the developer/agent plugs modules into src/modules.ts.\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit -p tsconfig.json\"\n },\n \"//\": \"Versions are pinned exactly: this is a seed for AI Coder projects, which build offline against a dependency allowlist baked at these versions (see scripts/pack-builder.mjs). Modules bring their own engine deps (three/phaser) when added.\",\n \"dependencies\": {\n \"@idosgames/app-shell\": \"0.3.1\",\n \"@idosgames/core\": \"0.14.1\",\n \"@idosgames/module-sdk\": \"0.3.1\",\n \"@idosgames/react\": \"0.2.7\",\n \"@idosgames/wallet\": \"0.2.7\",\n \"@tanstack/react-query\": \"5.101.2\",\n \"react\": \"19.2.7\",\n \"react-dom\": \"19.2.7\",\n \"wagmi\": \"3.7.2\"\n },\n \"devDependencies\": {\n \"@types/react\": \"19.2.17\",\n \"@types/react-dom\": \"19.2.3\",\n \"@vitejs/plugin-react\": \"6.0.4\",\n \"typescript\": \"5.9.3\",\n \"vite\": \"8.1.5\"\n }\n}\n"
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
"path": "public/sw.js",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
"path": "src/config.ts",
|
|
34
|
-
"content": "// Which Title this app talks to. Always targets the real backend (https://api.idosgames.com).\n//\n// IDENTITY
|
|
34
|
+
"content": "// Which Title this app talks to, and in which environment. Always targets the real backend\n// (https://api.idosgames.com).\n//\n// ⚠ PLATFORM-OWNED. The platform's build packs its own copy of this file over the project's (the\n// same way it regenerates src/idos.title.ts), so an edit here never reaches a published game.\n//\n// Neither IDENTITY nor ENVIRONMENT is read from the link (query or path) — that is the point. A link\n// is something anyone can edit: when `?titleID=` could pick the title, another publisher could open\n// this finished game with their own title in the link and run it on their title's settings.\n//\n// identity — WHICH game. Resolved once, in this order:\n// 1. src/idos.title.ts — the centralized identity file (generated by the platform, or filled\n// by hand on a manual scaffold). Wins over everything, and is the only\n// channel a packaged mobile build has.\n// 2. the host — {id}.idos.games / {id}-dev.idos.games, the title's own addresses. The\n// platform's game host serves those names with that title's files only.\n// 3. VITE_IDOS_TITLE_ID — local development, via .env.local.\n// There is deliberately NO fallback title. A build that cannot tell which game it is must fail\n// loudly rather than quietly sign in to somebody else's data.\n//\n// environment — WHICH copy of that game, prod or dev. A modifier on the identity, never a\n// different identity: the dev title is always `{id}-DEV`. Resolved in this order:\n// 1. the host — {id}-dev.idos.games is dev, {id}.idos.games is prod. That is how one\n// artifact runs on both without a rebuild.\n// 2. the live preview — the AI Coder's in-browser preview marks its sandbox as dev before\n// any project code runs (it lives on a foreign origin, so the host\n// cannot say it).\n// 3. VITE_IDOS_ENV — local development, via .env.local (\"dev\" or \"prod\").\n// 4. IDOS_DEFAULT_ENV — the baked default from src/idos.title.ts.\n\nimport { IDOS_BUILD_KEY, IDOS_DEFAULT_ENV, IDOS_TITLE_ID } from \"./idos.title\";\n\n/** Mirrors DevTitleService.DevSuffix on the backend. */\nconst DEV_SUFFIX = \"-DEV\";\n\n// Build-time values straight from the __IDOS_ENV__ global (see vite.config.ts), not through\n// ./env: this file is packed over the project's own at build time, so it must not depend on what\n// the project's other files export.\ntype BuildEnv = { TITLE_ID?: string; BUILD_KEY?: string; ENV?: string };\ndeclare const __IDOS_ENV__: BuildEnv | undefined;\nconst buildEnv: BuildEnv =\n typeof __IDOS_ENV__ !== \"undefined\" && __IDOS_ENV__ ? __IDOS_ENV__ : {};\n\n/**\n * The title's own addresses: `{titleid}.idos.games` (prod) and `{titleid}-dev.idos.games` (dev).\n * Title ids are 8 characters of [A-Z0-9] and hostnames are case-insensitive, so the lowercase id IS\n * the address. Mirrors the platform's game-host Worker (TitleHostWorker/src/host.ts). A publisher's\n * own domain carries no id — that is what idos.title.ts is for.\n */\nfunction fromHost(): { titleId: string; dev: boolean } | null {\n if (typeof window === \"undefined\") return null;\n const match = window.location.hostname\n .toLowerCase()\n .replace(/\\.$/, \"\")\n .match(/^([a-z0-9]{8})(-dev)?\\.idos\\.games$/);\n const id = match?.[1];\n return id\n ? { titleId: id.toUpperCase(), dev: match?.[2] !== undefined }\n : null;\n}\n\n/**\n * Set by the AI Coder dashboard in its preview's entry point, before this file runs. Nothing\n * outside the dashboard can set it on a hosted game: that page is served from the title's own\n * origin, and a script from anywhere else cannot reach its globals.\n */\nfunction previewEnv(): string | null {\n const marker = (globalThis as { __IDOS_PREVIEW__?: { env?: unknown } })\n .__IDOS_PREVIEW__;\n return typeof marker?.env === \"string\" ? marker.env : null;\n}\n\nconst host = fromHost();\nconst envTitle = buildEnv.TITLE_ID ?? \"\";\nconst envTitleIsDev = envTitle.endsWith(DEV_SUFFIX);\n\nconst canonicalTitle =\n IDOS_TITLE_ID ||\n host?.titleId ||\n (envTitleIsDev ? envTitle.slice(0, -DEV_SUFFIX.length) : envTitle);\n\nif (!canonicalTitle) {\n throw new Error(\n \"[idos] No Title id resolved. Expected src/idos.title.ts (generated by the platform), \" +\n \"a {titleid}.idos.games host, or VITE_IDOS_TITLE_ID in .env.local.\",\n );\n}\n\nfunction resolveIsDev(): boolean {\n if (host) return host.dev;\n const preview = previewEnv();\n if (preview !== null) return preview === \"dev\";\n if (buildEnv.ENV === \"dev\" || buildEnv.ENV === \"prod\")\n return buildEnv.ENV === \"dev\";\n if (envTitleIsDev) return true;\n return IDOS_DEFAULT_ENV === \"dev\";\n}\n\nexport const TITLE_ID = resolveIsDev()\n ? `${canonicalTitle}${DEV_SUFFIX}`\n : canonicalTitle;\n\nexport const BUILD_KEY = IDOS_BUILD_KEY || buildEnv.BUILD_KEY || \"\";\n"
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"path": "src/env.ts",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"path": "src/LoginScreen.tsx",
|
|
46
|
-
"content": "import { useEffect, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { beginSsoRedirect } from \"@idosgames/core\";\nimport type { LoginScreenProps } from \"@idosgames/app-shell\";\nimport { ENV_GOOGLE_CLIENT_ID } from \"./env\";\nimport { LOGO_DATA_URL } from \"./logo\";\n\n// The Login scene. The host runtime owns WHEN this is shown (the auth gate in\n// @idosgames/app-shell); this file owns what it LOOKS like and which providers it offers.\n// Edit freely — branding, layout, copy, buttons.\n//\n// Providers on `client.auth`: loginWithDeviceID (guest), loginWithEmail + registerWithEmail,\n// loginWithGoogle, loginWithTelegram, loginWithWallet, loginWithSsoCode, plus resetPassword.\n//\n// A provider is only rendered when it can actually complete, so players never meet a dead button:\n// guest / email — always available, no external setup.\n// Google — needs VITE_IDOS_GOOGLE_CLIENT_ID and the Google Identity script on the page.\n// wallet — always offered; ./walletLogin owns the wagmi config and the challenge network.\n// iDos Games — only where the platform accepts a return_to (see isSsoAvailable below).\n\n// Куда платформа соглашается вернуть одноразовый код. Список повторяет allowlist на бэкенде\n// (SsoService.AllowedOrigins) НАМЕРЕННО: здесь он решает только, показывать ли кнопку, а\n// настоящий барьер стоит на сервере. Показать кнопку там, где сервер откажет, — значит\n// пообещать игроку вход, который не состоится.\nconst SSO_ORIGINS = [\n \"https://cloud.idosgames.com\",\n \"https://idosgames.com\",\n \"https://www.idosgames.com\",\n];\n\n// Свой адрес игры: {titleid}.idos.games. Сервер примет его ТОЛЬКО для этого же тайтла.\nconst GAME_HOST = /^https:\\/\\/[a-z0-9]{8}\\.idos\\.games$/;\n\nfunction isSsoAvailable(): boolean {\n if (typeof window === \"undefined\") return false;\n const { origin } = window.location;\n if (SSO_ORIGINS.includes(origin) || GAME_HOST.test(origin)) return true;\n\n // Свой домен издателя (play.brand.com) id тайтла не несёт, и по origin не понять, сработает ли\n // вход. Страницу, которую отдаёт платформа, её сервер помечает этой меткой — а на свой домен он\n // отдаёт игру, только когда домен подключён к ЭТОМУ тайтлу. В превью и на чужом хостинге метки нет.\n return (\n typeof document !== \"undefined\" &&\n document.querySelector('meta[name=\"idos-game-host\"]') !== null\n );\n}\n\nexport interface LoginScreenExtras {\n /**\n * Wallet sign-in. Supplied by ./walletLogin (wired in main.tsx), which renders the ready-made\n * `WalletLogin` from `@idosgames/wallet/react`: connect → sign the challenge → session, then\n * `onAuthenticated()`. The screen passes its own `style` so the button matches the theme.\n *\n * Note: a wallet session is never restored silently (a fresh signature is required on every\n * launch), so keep at least one other provider for players who want to come straight back in.\n */\n renderWalletLogin?: (props: {\n client: LoginScreenProps[\"client\"];\n onAuthenticated: () => void;\n disabled: boolean;\n style?: CSSProperties;\n }) => ReactNode;\n}\n\n/** Пауза повторной отправки, когда сервер её не назвал. Совпадает со значением платформы. */\nconst DEFAULT_RESEND_COOLDOWN = 60;\n\n/**\n * Служебный код отказа → фраза, которую можно показать игроку.\n *\n * ⚠ Без этого экран входа показывал коды КАК ЕСТЬ: игрок видел «EMAIL_SENDER_NOT_CONFIGURED» или\n * «VERIFICATION_CODE_ATTEMPTS_EXCEEDED» вместо объяснения. Это шаблон, с которого начинается\n * каждая игра издателя, поэтому такое уезжает сразу всем.\n *\n * Незнакомый код возвращается как есть — намеренно: издателю на стенде он полезнее, чем общая\n * фраза «что-то пошло не так», а список ниже растёт по мере появления новых.\n */\nfunction humanizeAuthError(code: string | undefined): string {\n switch (code) {\n case \"EMAIL_SENDER_NOT_CONFIGURED\":\n return \"Sign-in by e-mail is unavailable in this game right now. Try another way to sign in.\";\n case \"INVALID_VERIFICATION_CODE\":\n return \"That code is not right. Check the e-mail and try again.\";\n case \"VERIFICATION_CODE_ATTEMPTS_EXCEEDED\":\n return \"Too many wrong attempts. Ask for a new code.\";\n case \"INCORRECT_EMAIL_OR_PASSWORD\":\n return \"Wrong e-mail or password.\";\n case \"INCORRECT_EMAIL\":\n return \"That does not look like an e-mail address.\";\n case \"PASSWORD_LENGTH_INVALID\":\n return \"The password must be 8 to 100 characters long.\";\n case \"TOO_MANY_FAILED_ATTEMPTS\":\n return \"Too many attempts. Please try again a little later.\";\n case \"RATE_LIMIT_EXCEEDED\":\n return \"Too many requests. Please try again in a moment.\";\n default:\n return code ?? \"Something went wrong. Please try again.\";\n }\n}\n\ntype Mode = \"menu\" | \"email\" | \"verify\";\n\n/** Minimal Google Identity surface — declared here so the template needs no @types/google.accounts. */\ntype GoogleIdentity = {\n accounts: {\n id: {\n initialize(config: {\n client_id: string;\n callback: (response: { credential?: string }) => void;\n }): void;\n prompt(): void;\n };\n };\n};\n\nexport function LoginScreen({\n client,\n onAuthenticated,\n renderWalletLogin,\n}: LoginScreenProps & LoginScreenExtras): ReactNode {\n const [mode, setMode] = useState<Mode>(\"menu\");\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [registering, setRegistering] = useState(false);\n const [code, setCode] = useState(\"\");\n const [resendIn, setResendIn] = useState(0);\n const [resendCooldown, setResendCooldown] = useState(DEFAULT_RESEND_COOLDOWN);\n\n const [remember, setRemember] = useState(true);\n\n /** Every provider goes through here, so one place owns the busy flag and the error surface. */\n const run = async (\n login: () => Promise<{ ok: boolean; error?: string }>,\n ): Promise<void> => {\n setBusy(true);\n setError(null);\n // \"Remember me\" is read when the login completes, so set it before starting one. Off = this\n // session works normally but is not written to storage, so the next launch lands here again.\n client.auth.setRememberSession(remember);\n const result = await login();\n if (result.ok) {\n onAuthenticated();\n return;\n }\n setError(\n humanizeAuthError(result.error) ?? \"Sign-in failed. Please try again.\",\n );\n setBusy(false);\n };\n\n /**\n * Регистрация — единственный провайдер, который НЕ обязательно заканчивается входом.\n *\n * Когда тайтл требует подтверждения адреса (а это значение платформы), сервер только отправляет\n * код, и аккаунта ещё нет. Поэтому она идёт мимо `run`: тот на успехе сразу зовёт\n * `onAuthenticated()`, а здесь на успехе надо показать экран ввода кода.\n */\n const register = async (): Promise<void> => {\n setBusy(true);\n setError(null);\n client.auth.setRememberSession(remember);\n\n const result = await client.auth.registerWithEmail(email, password);\n\n if (!result.ok) {\n setError(\n humanizeAuthError(result.error) ?? \"Sign-up failed. Please try again.\",\n );\n setBusy(false);\n return;\n }\n\n // Аккаунта ещё нет — он появится на подтверждении. Уйти в игру здесь значило бы показать\n // пустую сессию.\n setCode(\"\");\n\n // Паузу задаёт СЕРВЕР (её настраивает издатель), поэтому запоминаем её и дальше берём\n // отсюда. Раньше первый отсчёт шёл от ответа, а каждый следующий — от захардкоженных 60\n // секунд: у тайтла с другой настройкой кнопка либо открывалась раньше, чем сервер согласен\n // слать (нажатие впустую, ответ всё равно успешный), либо держалась закрытой дольше нужного.\n const cooldown = result.data.resendCooldownSeconds ?? 0;\n setResendCooldown(cooldown > 0 ? cooldown : DEFAULT_RESEND_COOLDOWN);\n\n setResendIn(cooldown);\n setMode(\"verify\");\n setBusy(false);\n };\n\n const resendCode = async (): Promise<void> => {\n setBusy(true);\n setError(null);\n\n const result = await client.auth.resendVerificationCode(email);\n\n // Ответ почти всегда успешный — начата регистрация или нет, выдержана пауза или нет. Иначе\n // эта кнопка отвечала бы на вопрос «заведён ли такой адрес». Поэтому и таймер заводим всегда.\n //\n // ⚠ Но ОДИН отказ отсюда приходит и его нельзя глотать: «слать нечем» (у тайтла и у\n // платформы нет отправителя). Он про конфигурацию сервера, а не про адрес, поэтому и\n // безопасен, и обязателен — иначе игрок жмёт кнопку до посинения, ожидая письма, которого\n // никто не отправлял.\n if (!result.ok) setError(humanizeAuthError(result.error));\n\n setResendIn(resendCooldown);\n setBusy(false);\n };\n\n // Обратный отсчёт до следующей отправки. Без него игрок жмёт «ещё раз» вслепую, а сервер молча\n // отказывает — и выглядит это как сломанная кнопка.\n useEffect(() => {\n if (resendIn <= 0) return;\n const id = setTimeout(() => setResendIn((v) => v - 1), 1000);\n return () => clearTimeout(id);\n }, [resendIn]);\n\n const signInWithGoogle = (): void => {\n const google = (globalThis as { google?: GoogleIdentity }).google;\n if (!google) {\n setError(\n \"Google sign-in is unavailable: the Google Identity script did not load.\",\n );\n return;\n }\n setError(null);\n google.accounts.id.initialize({\n client_id: ENV_GOOGLE_CLIENT_ID,\n callback: (response) => {\n if (!response.credential) {\n setError(\"Google sign-in was cancelled.\");\n return;\n }\n void run(() => client.auth.loginWithGoogle(response.credential ?? \"\"));\n },\n });\n google.accounts.id.prompt();\n };\n\n return (\n <div style={styles.root}>\n {/* Placeholder color is a pseudo-element, unreachable from inline styles — this one rule is\n the whole reason for the style tag. */}\n <style>{`.idos-input::placeholder { color: rgba(255, 255, 255, 0.65); }`}</style>\n <div style={styles.card}>\n <img src={LOGO_DATA_URL} alt=\"iDos Games\" style={styles.logo} />\n <h1 style={styles.title}>Sign in</h1>\n\n {mode === \"menu\" && (\n <div style={styles.stack}>\n {renderWalletLogin?.({\n client,\n onAuthenticated,\n disabled: busy,\n style: { ...styles.button, ...styles.primary },\n })}\n\n {/* Вход платформенным аккаунтом: уходим на idosgames.com/sso и возвращаемся сюда\n с одноразовым кодом, который AuthGate обменяет сам. Кнопка нужна только тем,\n кто открыл игру НАПРЯМУЮ: пришедший с сайта уже вернулся с кодом и этот экран\n не увидит вовсе.\n\n Скрыта там, где SSO заведомо откажет — бэкенд принимает return_to только со\n своих origin'ов, и в превью/на localhost показывать кнопку значило бы обещать\n игроку то, что не сработает. */}\n {isSsoAvailable() && (\n <button\n type=\"button\"\n style={styles.button}\n onClick={() => beginSsoRedirect({ titleID: client.titleID })}\n disabled={busy}\n >\n Continue with iDos Games\n </button>\n )}\n\n {ENV_GOOGLE_CLIENT_ID && (\n <button\n type=\"button\"\n style={styles.button}\n onClick={signInWithGoogle}\n disabled={busy}\n >\n Continue with Google\n </button>\n )}\n\n <button\n type=\"button\"\n style={styles.button}\n onClick={() => setMode(\"email\")}\n disabled={busy}\n >\n Continue with email\n </button>\n\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => void run(() => client.auth.loginWithDeviceID())}\n disabled={busy}\n >\n {busy ? \"Signing in…\" : \"Play as guest\"}\n </button>\n </div>\n )}\n\n {mode === \"email\" && (\n <div style={styles.stack}>\n <input\n className=\"idos-input\"\n style={styles.input}\n type=\"email\"\n placeholder=\"Email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n disabled={busy}\n autoFocus\n />\n <input\n className=\"idos-input\"\n style={styles.input}\n type=\"password\"\n placeholder=\"Password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={busy}\n />\n <button\n type=\"button\"\n style={{ ...styles.button, ...styles.primary }}\n onClick={() =>\n registering\n ? void register()\n : void run(() => client.auth.loginWithEmail(email, password))\n }\n disabled={busy || !email || !password}\n >\n {busy\n ? \"Please wait…\"\n : registering\n ? \"Create account\"\n : \"Sign in\"}\n </button>\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => setRegistering((v) => !v)}\n disabled={busy}\n >\n {registering ? \"I already have an account\" : \"Create an account\"}\n </button>\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => {\n setMode(\"menu\");\n setError(null);\n }}\n disabled={busy}\n >\n Back\n </button>\n </div>\n )}\n\n {mode === \"verify\" && (\n <div style={styles.stack}>\n <p style={styles.hint}>\n We sent a code to <strong>{email}</strong>. Enter it to finish\n creating your account.\n </p>\n <input\n className=\"idos-input\"\n style={styles.input}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n placeholder=\"Confirmation code\"\n value={code}\n onChange={(e) => setCode(e.target.value)}\n disabled={busy}\n autoFocus\n />\n <button\n type=\"button\"\n style={{ ...styles.button, ...styles.primary }}\n onClick={() =>\n void run(() =>\n client.auth.confirmEmailRegistration(email, code),\n )\n }\n disabled={busy || !code}\n >\n {busy ? \"Please wait…\" : \"Confirm\"}\n </button>\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => void resendCode()}\n disabled={busy || resendIn > 0}\n >\n {resendIn > 0\n ? `Send again in ${resendIn}s`\n : \"Send the code again\"}\n </button>\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => {\n setMode(\"email\");\n setError(null);\n }}\n disabled={busy}\n >\n Back\n </button>\n </div>\n )}\n\n {/* Applies to every provider above. A wallet sign-in ignores it — those are never\n restored silently, a fresh signature is required on each launch. */}\n <label style={{ ...styles.remember, opacity: busy ? 0.6 : 1 }}>\n <input\n type=\"checkbox\"\n checked={remember}\n disabled={busy}\n onChange={(e) => setRemember(e.target.checked)}\n style={styles.switchInput}\n />\n <span\n style={{\n ...styles.switchTrack,\n background: remember ? \"#fff\" : \"rgba(255, 255, 255, 0.3)\",\n }}\n >\n <span\n style={{\n ...styles.switchKnob,\n left: remember ? \"21px\" : \"3px\",\n background: remember ? \"#0d66fe\" : \"#fff\",\n }}\n />\n </span>\n Remember me\n </label>\n\n {error && <p style={styles.error}>{error}</p>}\n </div>\n </div>\n );\n}\n\n// Brand look: iDos Games blue with the white logo; controls are translucent white on top of it,\n// the primary action is solid white with blue text.\nconst styles: Record<string, CSSProperties> = {\n root: {\n position: \"absolute\",\n inset: 0,\n display: \"grid\",\n placeItems: \"center\",\n background: \"#0d66fe\",\n color: \"#fff\",\n font: \"14px system-ui, sans-serif\",\n },\n card: { width: \"min(340px, 88vw)\", display: \"grid\", gap: \"18px\" },\n logo: {\n width: \"180px\",\n justifySelf: \"center\",\n userSelect: \"none\",\n pointerEvents: \"none\",\n },\n remember: {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"10px\",\n justifySelf: \"center\",\n cursor: \"pointer\",\n color: \"rgba(255, 255, 255, 0.85)\",\n },\n // The switch: a hidden real checkbox (keyboard/a11y) with a drawn track + knob on top.\n switchInput: { position: \"absolute\", opacity: 0, width: 0, height: 0 },\n switchTrack: {\n position: \"relative\",\n width: \"42px\",\n height: \"24px\",\n borderRadius: \"12px\",\n transition: \"background 0.15s\",\n flexShrink: 0,\n },\n switchKnob: {\n position: \"absolute\",\n top: \"3px\",\n width: \"18px\",\n height: \"18px\",\n borderRadius: \"50%\",\n transition: \"left 0.15s, background 0.15s\",\n },\n title: { margin: 0, fontSize: \"22px\", fontWeight: 600, textAlign: \"center\" },\n stack: { display: \"grid\", gap: \"10px\" },\n button: {\n padding: \"11px 16px\",\n borderRadius: \"8px\",\n border: \"1px solid rgba(255, 255, 255, 0.4)\",\n background: \"rgba(255, 255, 255, 0.14)\",\n color: \"inherit\",\n font: \"inherit\",\n cursor: \"pointer\",\n },\n primary: {\n background: \"#fff\",\n borderColor: \"#fff\",\n color: \"#0d66fe\",\n fontWeight: 600,\n },\n ghost: {\n padding: \"8px\",\n border: \"none\",\n background: \"none\",\n color: \"rgba(255, 255, 255, 0.85)\",\n font: \"inherit\",\n cursor: \"pointer\",\n },\n input: {\n padding: \"11px 12px\",\n borderRadius: \"8px\",\n border: \"1px solid rgba(255, 255, 255, 0.35)\",\n background: \"rgba(255, 255, 255, 0.12)\",\n color: \"inherit\",\n font: \"inherit\",\n },\n error: { margin: 0, color: \"#ffd7d7\", textAlign: \"center\" },\n hint: {\n margin: 0,\n color: \"rgba(255, 255, 255, 0.85)\",\n textAlign: \"center\",\n fontSize: \"14px\",\n lineHeight: 1.45,\n },\n};\n"
|
|
46
|
+
"content": "import { useEffect, useState, type CSSProperties, type ReactNode } from \"react\";\nimport { beginSsoRedirect } from \"@idosgames/core\";\nimport type { LoginScreenProps } from \"@idosgames/app-shell\";\nimport { ENV_GOOGLE_CLIENT_ID } from \"./env\";\nimport { LOGO_DATA_URL } from \"./logo\";\n\n// The Login scene. The host runtime owns WHEN this is shown (the auth gate in\n// @idosgames/app-shell); this file owns what it LOOKS like and which providers it offers.\n// Edit freely — branding, layout, copy, buttons.\n//\n// Providers on `client.auth`: loginWithDeviceID (guest), loginWithEmail + registerWithEmail,\n// loginWithGoogle, loginWithTelegram, loginWithWallet, loginWithSsoCode, plus resetPassword.\n//\n// A provider is only rendered when it can actually complete, so players never meet a dead button:\n// guest / email — always available, no external setup.\n// Google — needs VITE_IDOS_GOOGLE_CLIENT_ID and the Google Identity script on the page.\n// wallet — always offered; ./walletLogin owns the wagmi config and the challenge network.\n// iDos Games — only where the platform accepts a return_to (see isSsoAvailable below).\n\n// Куда платформа соглашается вернуть одноразовый код. Список повторяет allowlist на бэкенде\n// (SsoService.AllowedOrigins) НАМЕРЕННО: здесь он решает только, показывать ли кнопку, а\n// настоящий барьер стоит на сервере. Показать кнопку там, где сервер откажет, — значит\n// пообещать игроку вход, который не состоится.\nconst SSO_ORIGINS = [\n \"https://cloud.idosgames.com\",\n \"https://idosgames.com\",\n \"https://www.idosgames.com\",\n];\n\n// Свои адреса игры: {titleid}.idos.games и {titleid}-dev.idos.games (её DEV-копия). Сервер примет\n// каждый ТОЛЬКО для этого же тайтла.\nconst GAME_HOST = /^https:\\/\\/[a-z0-9]{8}(-dev)?\\.idos\\.games$/;\n\nfunction isSsoAvailable(): boolean {\n if (typeof window === \"undefined\") return false;\n const { origin } = window.location;\n if (SSO_ORIGINS.includes(origin) || GAME_HOST.test(origin)) return true;\n\n // Свой домен издателя (play.brand.com) id тайтла не несёт, и по origin не понять, сработает ли\n // вход. Страницу, которую отдаёт платформа, её сервер помечает этой меткой — а на свой домен он\n // отдаёт игру, только когда домен подключён к ЭТОМУ тайтлу. В превью и на чужом хостинге метки нет.\n return (\n typeof document !== \"undefined\" &&\n document.querySelector('meta[name=\"idos-game-host\"]') !== null\n );\n}\n\nexport interface LoginScreenExtras {\n /**\n * Wallet sign-in. Supplied by ./walletLogin (wired in main.tsx), which renders the ready-made\n * `WalletLogin` from `@idosgames/wallet/react`: connect → sign the challenge → session, then\n * `onAuthenticated()`. The screen passes its own `style` so the button matches the theme.\n *\n * Note: a wallet session is never restored silently (a fresh signature is required on every\n * launch), so keep at least one other provider for players who want to come straight back in.\n */\n renderWalletLogin?: (props: {\n client: LoginScreenProps[\"client\"];\n onAuthenticated: () => void;\n disabled: boolean;\n style?: CSSProperties;\n }) => ReactNode;\n}\n\n/** Пауза повторной отправки, когда сервер её не назвал. Совпадает со значением платформы. */\nconst DEFAULT_RESEND_COOLDOWN = 60;\n\n/**\n * Служебный код отказа → фраза, которую можно показать игроку.\n *\n * ⚠ Без этого экран входа показывал коды КАК ЕСТЬ: игрок видел «EMAIL_SENDER_NOT_CONFIGURED» или\n * «VERIFICATION_CODE_ATTEMPTS_EXCEEDED» вместо объяснения. Это шаблон, с которого начинается\n * каждая игра издателя, поэтому такое уезжает сразу всем.\n *\n * Незнакомый код возвращается как есть — намеренно: издателю на стенде он полезнее, чем общая\n * фраза «что-то пошло не так», а список ниже растёт по мере появления новых.\n */\nfunction humanizeAuthError(code: string | undefined): string {\n switch (code) {\n case \"EMAIL_SENDER_NOT_CONFIGURED\":\n return \"Sign-in by e-mail is unavailable in this game right now. Try another way to sign in.\";\n case \"INVALID_VERIFICATION_CODE\":\n return \"That code is not right. Check the e-mail and try again.\";\n case \"VERIFICATION_CODE_ATTEMPTS_EXCEEDED\":\n return \"Too many wrong attempts. Ask for a new code.\";\n case \"INCORRECT_EMAIL_OR_PASSWORD\":\n return \"Wrong e-mail or password.\";\n case \"INCORRECT_EMAIL\":\n return \"That does not look like an e-mail address.\";\n case \"PASSWORD_LENGTH_INVALID\":\n return \"The password must be 8 to 100 characters long.\";\n case \"TOO_MANY_FAILED_ATTEMPTS\":\n return \"Too many attempts. Please try again a little later.\";\n case \"RATE_LIMIT_EXCEEDED\":\n return \"Too many requests. Please try again in a moment.\";\n default:\n return code ?? \"Something went wrong. Please try again.\";\n }\n}\n\ntype Mode = \"menu\" | \"email\" | \"verify\";\n\n/** Minimal Google Identity surface — declared here so the template needs no @types/google.accounts. */\ntype GoogleIdentity = {\n accounts: {\n id: {\n initialize(config: {\n client_id: string;\n callback: (response: { credential?: string }) => void;\n }): void;\n prompt(): void;\n };\n };\n};\n\nexport function LoginScreen({\n client,\n onAuthenticated,\n renderWalletLogin,\n}: LoginScreenProps & LoginScreenExtras): ReactNode {\n const [mode, setMode] = useState<Mode>(\"menu\");\n const [busy, setBusy] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const [email, setEmail] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n const [registering, setRegistering] = useState(false);\n const [code, setCode] = useState(\"\");\n const [resendIn, setResendIn] = useState(0);\n const [resendCooldown, setResendCooldown] = useState(DEFAULT_RESEND_COOLDOWN);\n\n const [remember, setRemember] = useState(true);\n\n /** Every provider goes through here, so one place owns the busy flag and the error surface. */\n const run = async (\n login: () => Promise<{ ok: boolean; error?: string }>,\n ): Promise<void> => {\n setBusy(true);\n setError(null);\n // \"Remember me\" is read when the login completes, so set it before starting one. Off = this\n // session works normally but is not written to storage, so the next launch lands here again.\n client.auth.setRememberSession(remember);\n const result = await login();\n if (result.ok) {\n onAuthenticated();\n return;\n }\n setError(\n humanizeAuthError(result.error) ?? \"Sign-in failed. Please try again.\",\n );\n setBusy(false);\n };\n\n /**\n * Регистрация — единственный провайдер, который НЕ обязательно заканчивается входом.\n *\n * Когда тайтл требует подтверждения адреса (а это значение платформы), сервер только отправляет\n * код, и аккаунта ещё нет. Поэтому она идёт мимо `run`: тот на успехе сразу зовёт\n * `onAuthenticated()`, а здесь на успехе надо показать экран ввода кода.\n */\n const register = async (): Promise<void> => {\n setBusy(true);\n setError(null);\n client.auth.setRememberSession(remember);\n\n const result = await client.auth.registerWithEmail(email, password);\n\n if (!result.ok) {\n setError(\n humanizeAuthError(result.error) ?? \"Sign-up failed. Please try again.\",\n );\n setBusy(false);\n return;\n }\n\n // Аккаунта ещё нет — он появится на подтверждении. Уйти в игру здесь значило бы показать\n // пустую сессию.\n setCode(\"\");\n\n // Паузу задаёт СЕРВЕР (её настраивает издатель), поэтому запоминаем её и дальше берём\n // отсюда. Раньше первый отсчёт шёл от ответа, а каждый следующий — от захардкоженных 60\n // секунд: у тайтла с другой настройкой кнопка либо открывалась раньше, чем сервер согласен\n // слать (нажатие впустую, ответ всё равно успешный), либо держалась закрытой дольше нужного.\n const cooldown = result.data.resendCooldownSeconds ?? 0;\n setResendCooldown(cooldown > 0 ? cooldown : DEFAULT_RESEND_COOLDOWN);\n\n setResendIn(cooldown);\n setMode(\"verify\");\n setBusy(false);\n };\n\n const resendCode = async (): Promise<void> => {\n setBusy(true);\n setError(null);\n\n const result = await client.auth.resendVerificationCode(email);\n\n // Ответ почти всегда успешный — начата регистрация или нет, выдержана пауза или нет. Иначе\n // эта кнопка отвечала бы на вопрос «заведён ли такой адрес». Поэтому и таймер заводим всегда.\n //\n // ⚠ Но ОДИН отказ отсюда приходит и его нельзя глотать: «слать нечем» (у тайтла и у\n // платформы нет отправителя). Он про конфигурацию сервера, а не про адрес, поэтому и\n // безопасен, и обязателен — иначе игрок жмёт кнопку до посинения, ожидая письма, которого\n // никто не отправлял.\n if (!result.ok) setError(humanizeAuthError(result.error));\n\n setResendIn(resendCooldown);\n setBusy(false);\n };\n\n // Обратный отсчёт до следующей отправки. Без него игрок жмёт «ещё раз» вслепую, а сервер молча\n // отказывает — и выглядит это как сломанная кнопка.\n useEffect(() => {\n if (resendIn <= 0) return;\n const id = setTimeout(() => setResendIn((v) => v - 1), 1000);\n return () => clearTimeout(id);\n }, [resendIn]);\n\n const signInWithGoogle = (): void => {\n const google = (globalThis as { google?: GoogleIdentity }).google;\n if (!google) {\n setError(\n \"Google sign-in is unavailable: the Google Identity script did not load.\",\n );\n return;\n }\n setError(null);\n google.accounts.id.initialize({\n client_id: ENV_GOOGLE_CLIENT_ID,\n callback: (response) => {\n if (!response.credential) {\n setError(\"Google sign-in was cancelled.\");\n return;\n }\n void run(() => client.auth.loginWithGoogle(response.credential ?? \"\"));\n },\n });\n google.accounts.id.prompt();\n };\n\n return (\n <div style={styles.root}>\n {/* Placeholder color is a pseudo-element, unreachable from inline styles — this one rule is\n the whole reason for the style tag. */}\n <style>{`.idos-input::placeholder { color: rgba(255, 255, 255, 0.65); }`}</style>\n <div style={styles.card}>\n <img src={LOGO_DATA_URL} alt=\"iDos Games\" style={styles.logo} />\n <h1 style={styles.title}>Sign in</h1>\n\n {mode === \"menu\" && (\n <div style={styles.stack}>\n {renderWalletLogin?.({\n client,\n onAuthenticated,\n disabled: busy,\n style: { ...styles.button, ...styles.primary },\n })}\n\n {/* Вход платформенным аккаунтом: уходим на idosgames.com/sso и возвращаемся сюда\n с одноразовым кодом, который AuthGate обменяет сам. Кнопка нужна только тем,\n кто открыл игру НАПРЯМУЮ: пришедший с сайта уже вернулся с кодом и этот экран\n не увидит вовсе.\n\n Скрыта там, где SSO заведомо откажет — бэкенд принимает return_to только со\n своих origin'ов, и в превью/на localhost показывать кнопку значило бы обещать\n игроку то, что не сработает. */}\n {isSsoAvailable() && (\n <button\n type=\"button\"\n style={styles.button}\n onClick={() => beginSsoRedirect({ titleID: client.titleID })}\n disabled={busy}\n >\n Continue with iDos Games\n </button>\n )}\n\n {ENV_GOOGLE_CLIENT_ID && (\n <button\n type=\"button\"\n style={styles.button}\n onClick={signInWithGoogle}\n disabled={busy}\n >\n Continue with Google\n </button>\n )}\n\n <button\n type=\"button\"\n style={styles.button}\n onClick={() => setMode(\"email\")}\n disabled={busy}\n >\n Continue with email\n </button>\n\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => void run(() => client.auth.loginWithDeviceID())}\n disabled={busy}\n >\n {busy ? \"Signing in…\" : \"Play as guest\"}\n </button>\n </div>\n )}\n\n {mode === \"email\" && (\n <div style={styles.stack}>\n <input\n className=\"idos-input\"\n style={styles.input}\n type=\"email\"\n placeholder=\"Email\"\n value={email}\n onChange={(e) => setEmail(e.target.value)}\n disabled={busy}\n autoFocus\n />\n <input\n className=\"idos-input\"\n style={styles.input}\n type=\"password\"\n placeholder=\"Password\"\n value={password}\n onChange={(e) => setPassword(e.target.value)}\n disabled={busy}\n />\n <button\n type=\"button\"\n style={{ ...styles.button, ...styles.primary }}\n onClick={() =>\n registering\n ? void register()\n : void run(() => client.auth.loginWithEmail(email, password))\n }\n disabled={busy || !email || !password}\n >\n {busy\n ? \"Please wait…\"\n : registering\n ? \"Create account\"\n : \"Sign in\"}\n </button>\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => setRegistering((v) => !v)}\n disabled={busy}\n >\n {registering ? \"I already have an account\" : \"Create an account\"}\n </button>\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => {\n setMode(\"menu\");\n setError(null);\n }}\n disabled={busy}\n >\n Back\n </button>\n </div>\n )}\n\n {mode === \"verify\" && (\n <div style={styles.stack}>\n <p style={styles.hint}>\n We sent a code to <strong>{email}</strong>. Enter it to finish\n creating your account.\n </p>\n <input\n className=\"idos-input\"\n style={styles.input}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n placeholder=\"Confirmation code\"\n value={code}\n onChange={(e) => setCode(e.target.value)}\n disabled={busy}\n autoFocus\n />\n <button\n type=\"button\"\n style={{ ...styles.button, ...styles.primary }}\n onClick={() =>\n void run(() =>\n client.auth.confirmEmailRegistration(email, code),\n )\n }\n disabled={busy || !code}\n >\n {busy ? \"Please wait…\" : \"Confirm\"}\n </button>\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => void resendCode()}\n disabled={busy || resendIn > 0}\n >\n {resendIn > 0\n ? `Send again in ${resendIn}s`\n : \"Send the code again\"}\n </button>\n <button\n type=\"button\"\n style={styles.ghost}\n onClick={() => {\n setMode(\"email\");\n setError(null);\n }}\n disabled={busy}\n >\n Back\n </button>\n </div>\n )}\n\n {/* Applies to every provider above. A wallet sign-in ignores it — those are never\n restored silently, a fresh signature is required on each launch. */}\n <label style={{ ...styles.remember, opacity: busy ? 0.6 : 1 }}>\n <input\n type=\"checkbox\"\n checked={remember}\n disabled={busy}\n onChange={(e) => setRemember(e.target.checked)}\n style={styles.switchInput}\n />\n <span\n style={{\n ...styles.switchTrack,\n background: remember ? \"#fff\" : \"rgba(255, 255, 255, 0.3)\",\n }}\n >\n <span\n style={{\n ...styles.switchKnob,\n left: remember ? \"21px\" : \"3px\",\n background: remember ? \"#0d66fe\" : \"#fff\",\n }}\n />\n </span>\n Remember me\n </label>\n\n {error && <p style={styles.error}>{error}</p>}\n </div>\n </div>\n );\n}\n\n// Brand look: iDos Games blue with the white logo; controls are translucent white on top of it,\n// the primary action is solid white with blue text.\nconst styles: Record<string, CSSProperties> = {\n root: {\n position: \"absolute\",\n inset: 0,\n display: \"grid\",\n placeItems: \"center\",\n background: \"#0d66fe\",\n color: \"#fff\",\n font: \"14px system-ui, sans-serif\",\n },\n card: { width: \"min(340px, 88vw)\", display: \"grid\", gap: \"18px\" },\n logo: {\n width: \"180px\",\n justifySelf: \"center\",\n userSelect: \"none\",\n pointerEvents: \"none\",\n },\n remember: {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"10px\",\n justifySelf: \"center\",\n cursor: \"pointer\",\n color: \"rgba(255, 255, 255, 0.85)\",\n },\n // The switch: a hidden real checkbox (keyboard/a11y) with a drawn track + knob on top.\n switchInput: { position: \"absolute\", opacity: 0, width: 0, height: 0 },\n switchTrack: {\n position: \"relative\",\n width: \"42px\",\n height: \"24px\",\n borderRadius: \"12px\",\n transition: \"background 0.15s\",\n flexShrink: 0,\n },\n switchKnob: {\n position: \"absolute\",\n top: \"3px\",\n width: \"18px\",\n height: \"18px\",\n borderRadius: \"50%\",\n transition: \"left 0.15s, background 0.15s\",\n },\n title: { margin: 0, fontSize: \"22px\", fontWeight: 600, textAlign: \"center\" },\n stack: { display: \"grid\", gap: \"10px\" },\n button: {\n padding: \"11px 16px\",\n borderRadius: \"8px\",\n border: \"1px solid rgba(255, 255, 255, 0.4)\",\n background: \"rgba(255, 255, 255, 0.14)\",\n color: \"inherit\",\n font: \"inherit\",\n cursor: \"pointer\",\n },\n primary: {\n background: \"#fff\",\n borderColor: \"#fff\",\n color: \"#0d66fe\",\n fontWeight: 600,\n },\n ghost: {\n padding: \"8px\",\n border: \"none\",\n background: \"none\",\n color: \"rgba(255, 255, 255, 0.85)\",\n font: \"inherit\",\n cursor: \"pointer\",\n },\n input: {\n padding: \"11px 12px\",\n borderRadius: \"8px\",\n border: \"1px solid rgba(255, 255, 255, 0.35)\",\n background: \"rgba(255, 255, 255, 0.12)\",\n color: \"inherit\",\n font: \"inherit\",\n },\n error: { margin: 0, color: \"#ffd7d7\", textAlign: \"center\" },\n hint: {\n margin: 0,\n color: \"rgba(255, 255, 255, 0.85)\",\n textAlign: \"center\",\n fontSize: \"14px\",\n lineHeight: 1.45,\n },\n};\n"
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "src/logo.ts",
|
|
@@ -71,11 +71,11 @@
|
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"path": "tsconfig.json",
|
|
74
|
-
"content": "{\n \"$schema\": \"https://json.schemastore.org/tsconfig\",\n // Self-contained on purpose: this template is copied out of the monorepo (as the seed for an AI\n // Coder project) and must typecheck on its own, so it does not `extends` tsconfig.base.json. Keep\n // the shared options below in sync with tsconfig.base.json.\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Bundler\",\n\n \"strict\": true,\n \"noUncheckedIndexedAccess\": true,\n \"noImplicitOverride\": true,\n \"noFallthroughCasesInSwitch\": true,\n \"useUnknownInCatchVariables\": true,\n\n \"verbatimModuleSyntax\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"resolveJsonModule\": true,\n \"skipLibCheck\": true,\n \"forceConsistentCasingInFileNames\": true,\n\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n\n // Inside the monorepo these point typecheck at the SDK/host source, so no SDK build is needed.\n // Outside it the paths do not exist and TypeScript falls back to node_modules resolution.\n \"paths\": {\n \"@idosgames/core\": [\"../../packages/core/src/index.ts\"],\n \"@idosgames/module-sdk\": [\"../../packages/module-sdk/src/index.ts\"],\n \"@idosgames/react\": [\"../../packages/react/src/index.ts\"],\n \"@idosgames/app-shell\": [\"../../packages/app-shell/src/index.ts\"]\n }\n },\n \"include\": [\"src\"]\n}\n"
|
|
74
|
+
"content": "{\n \"$schema\": \"https://json.schemastore.org/tsconfig\",\n // Self-contained on purpose: this template is copied out of the monorepo (as the seed for an AI\n // Coder project) and must typecheck on its own, so it does not `extends` tsconfig.base.json. Keep\n // the shared options below in sync with tsconfig.base.json.\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Bundler\",\n\n \"strict\": true,\n \"noUncheckedIndexedAccess\": true,\n \"noImplicitOverride\": true,\n \"noFallthroughCasesInSwitch\": true,\n \"useUnknownInCatchVariables\": true,\n\n \"verbatimModuleSyntax\": true,\n \"isolatedModules\": true,\n \"esModuleInterop\": true,\n \"resolveJsonModule\": true,\n \"skipLibCheck\": true,\n \"forceConsistentCasingInFileNames\": true,\n\n \"noEmit\": true,\n \"jsx\": \"react-jsx\",\n\n // Inside the monorepo these point typecheck at the SDK/host source, so no SDK build is needed.\n // Outside it the paths do not exist and TypeScript falls back to node_modules resolution.\n \"paths\": {\n \"@idosgames/core\": [\"../../packages/core/src/index.ts\"],\n \"@idosgames/module-sdk\": [\"../../packages/module-sdk/src/index.ts\"],\n \"@idosgames/react\": [\"../../packages/react/src/index.ts\"],\n \"@idosgames/app-shell\": [\"../../packages/app-shell/src/index.ts\"]\n }\n },\n \"include\": [\"src\"],\n // Tests are not part of the game: the platform builds with no test runner installed, so a\n // `*.test.ts` that imports vitest would fail every release build. Keep them out of the typecheck.\n \"exclude\": [\n \"src/**/*.test.ts\",\n \"src/**/*.test.tsx\",\n \"src/**/*.spec.ts\",\n \"src/**/*.spec.tsx\"\n ]\n}\n"
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
"path": "vite.config.ts",
|
|
78
|
-
"content": "import { existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { defineConfig, loadEnv } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\nconst fromHere = (path: string): string =>\n fileURLToPath(new URL(path, import.meta.url));\n\n// Inside this monorepo, resolve the @idosgames/* packages to their source for instant HMR (no SDK/\n// host rebuild needed in dev). A standalone copy of this template — a cloned starter, or an AI Coder\n// project built in a container — has no such paths and resolves each package from node_modules.\nconst sourceAliases: Record<string, string> = {\n \"@idosgames/core\": fromHere(\"../../packages/core/src/index.ts\"),\n \"@idosgames/module-sdk\": fromHere(\"../../packages/module-sdk/src/index.ts\"),\n \"@idosgames/react\": fromHere(\"../../packages/react/src/index.ts\"),\n \"@idosgames/app-shell\": fromHere(\"../../packages/app-shell/src/index.ts\"),\n};\n\nexport default defineConfig(({ mode }) => {\n const env = loadEnv(mode, process.cwd(), \"\");\n const alias = Object.fromEntries(\n Object.entries(sourceAliases).filter(([, path]) => existsSync(path)),\n );\n return {\n plugins: [react()],\n resolve: { alias },\n server: { port: 5180 },\n // Env is injected via the __IDOS_ENV__ global (not import.meta.env) so the same files build under\n // both real vite and the classic preview bundler (Sandpack), which can't parse import.meta.\n // See src/env.ts.\n define: {\n __IDOS_ENV__: JSON.stringify({\n DEV: mode !== \"production\",\n TITLE_ID: env.VITE_IDOS_TITLE_ID ?? \"\",\n BUILD_KEY: env.VITE_IDOS_BUILD_KEY ?? \"\",\n WALLETCONNECT_PROJECT_ID: env.VITE_WALLETCONNECT_PROJECT_ID ?? \"\",\n GOOGLE_CLIENT_ID: env.VITE_IDOS_GOOGLE_CLIENT_ID ?? \"\",\n }),\n },\n };\n});\n"
|
|
78
|
+
"content": "import { existsSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { defineConfig, loadEnv } from \"vite\";\nimport react from \"@vitejs/plugin-react\";\n\nconst fromHere = (path: string): string =>\n fileURLToPath(new URL(path, import.meta.url));\n\n// Inside this monorepo, resolve the @idosgames/* packages to their source for instant HMR (no SDK/\n// host rebuild needed in dev). A standalone copy of this template — a cloned starter, or an AI Coder\n// project built in a container — has no such paths and resolves each package from node_modules.\nconst sourceAliases: Record<string, string> = {\n \"@idosgames/core\": fromHere(\"../../packages/core/src/index.ts\"),\n \"@idosgames/module-sdk\": fromHere(\"../../packages/module-sdk/src/index.ts\"),\n \"@idosgames/react\": fromHere(\"../../packages/react/src/index.ts\"),\n \"@idosgames/app-shell\": fromHere(\"../../packages/app-shell/src/index.ts\"),\n};\n\nexport default defineConfig(({ mode }) => {\n const env = loadEnv(mode, process.cwd(), \"\");\n const alias = Object.fromEntries(\n Object.entries(sourceAliases).filter(([, path]) => existsSync(path)),\n );\n return {\n plugins: [react()],\n resolve: { alias },\n server: { port: 5180 },\n // Env is injected via the __IDOS_ENV__ global (not import.meta.env) so the same files build under\n // both real vite and the classic preview bundler (Sandpack), which can't parse import.meta.\n // See src/env.ts.\n define: {\n __IDOS_ENV__: JSON.stringify({\n DEV: mode !== \"production\",\n TITLE_ID: env.VITE_IDOS_TITLE_ID ?? \"\",\n BUILD_KEY: env.VITE_IDOS_BUILD_KEY ?? \"\",\n // \"dev\" | \"prod\" — which copy of the title a LOCAL run talks to (src/config.ts). Hosted\n // builds take it from their address instead; the link never decides it.\n ENV: env.VITE_IDOS_ENV ?? \"\",\n WALLETCONNECT_PROJECT_ID: env.VITE_WALLETCONNECT_PROJECT_ID ?? \"\",\n GOOGLE_CLIENT_ID: env.VITE_IDOS_GOOGLE_CLIENT_ID ?? \"\",\n }),\n },\n };\n});\n"
|
|
79
79
|
}
|
|
80
80
|
]
|
|
81
81
|
}
|
package/registry/index.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedFromCommit": "
|
|
2
|
+
"generatedFromCommit": "0855010b072723ca1b748a9af3e4153c22627b09",
|
|
3
3
|
"runtimePackages": {
|
|
4
|
-
"@idosgames/core": "0.14.
|
|
4
|
+
"@idosgames/core": "0.14.1",
|
|
5
5
|
"@idosgames/wallet": "0.2.7",
|
|
6
|
-
"@idosgames/module-sdk": "0.3.
|
|
6
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
7
7
|
"@idosgames/react": "0.2.7",
|
|
8
|
-
"@idosgames/app-shell": "0.3.
|
|
8
|
+
"@idosgames/app-shell": "0.3.1"
|
|
9
9
|
},
|
|
10
10
|
"host": {
|
|
11
11
|
"id": "host-starter",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@idosgames/core": "0.14.
|
|
58
|
-
"@idosgames/module-sdk": "0.3.
|
|
57
|
+
"@idosgames/core": "0.14.1",
|
|
58
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
59
59
|
"@idosgames/react": "0.2.7",
|
|
60
60
|
"@idosgames/wallet": "0.2.7",
|
|
61
61
|
"@tanstack/react-query": "5.101.2",
|
|
@@ -113,8 +113,8 @@
|
|
|
113
113
|
]
|
|
114
114
|
},
|
|
115
115
|
"dependencies": {
|
|
116
|
-
"@idosgames/core": "0.14.
|
|
117
|
-
"@idosgames/module-sdk": "0.3.
|
|
116
|
+
"@idosgames/core": "0.14.1",
|
|
117
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
118
118
|
"@idosgames/react": "0.2.7",
|
|
119
119
|
"@idosgames/wallet": "0.2.7",
|
|
120
120
|
"@tanstack/react-query": "5.101.2",
|
|
@@ -132,26 +132,42 @@
|
|
|
132
132
|
"type": "template",
|
|
133
133
|
"engine": "three",
|
|
134
134
|
"genre": "sandbox",
|
|
135
|
-
"summary": "
|
|
136
|
-
"description": "A first-person voxel
|
|
135
|
+
"summary": "Minecraft-like voxel survival game: mine, craft, build, farm and fight from the first night to the dragon — with worlds generated from a picture + description (with or without AI).",
|
|
136
|
+
"description": "A first-person voxel game rendered in Three.js that follows Minecraft's mechanics 1:1 under its own names (every player-facing name lives in one table, so a commercial game can re-skin it; Minecraft names are accepted as aliases). Survival and creative modes: ores and tool tiers, crafting, smelting, armor, hunger, air and drowning, XP, enchanting, anvils, smithing, potions and beacons; farming, animals, taming and riding, fishing, boats and minecarts; villages with trading villagers, raids, and dozens of hostile and neutral mobs. Three dimensions — the overworld (biomes from deserts and jungles to the deep dark, caves, ocean temples, mansions, trial chambers, archaeology), the Inferno (Nether-like: fortresses, its own mobs and biomes) and the Beyond (End-like: citadel portal, the dragon boss, outer islands and cities, wings). Redstone (wire, torches, repeaters, comparators, pistons, observers, hoppers, dispensers, rails), weather, day/night, block light, fluids, achievements, generated music, a chat console with commands, and touch controls. The 'Create world' screen turns a reference picture and a text description into a world — locally in the browser (the picture read as a top-down map or as a mood, the text by keywords) or with InApp AI (client.ai, title feature \"voxel-world\") — via a compact WorldSpec recipe. Worlds autosave to 'My worlds'. The agent sees and drives the game through its debug surface (state, lookingAt, console commands). Covers any 'Minecraft clone', survival / crafting / mining game, block-building sandbox or 'generate a world from a picture' request; by far the largest feature module. Not included yet: multiplayer and AI-driven villagers.",
|
|
137
137
|
"provides": [
|
|
138
|
-
"first-person voxel world
|
|
139
|
-
"
|
|
140
|
-
"
|
|
138
|
+
"first-person voxel world: mine, place and break blocks",
|
|
139
|
+
"survival and creative modes (health, hunger, air, XP, death and respawn)",
|
|
140
|
+
"crafting, smelting, tool and armor tiers",
|
|
141
|
+
"enchanting, anvil, smithing, potions, beacon",
|
|
142
|
+
"farming, animals, taming and riding, fishing, boats",
|
|
143
|
+
"villages, trading villagers, raids",
|
|
144
|
+
"dozens of hostile and neutral mobs, bosses (dragon, ash lord)",
|
|
145
|
+
"three dimensions: overworld, Inferno (Nether-like), Beyond (End-like)",
|
|
146
|
+
"biomes, caves and structures (temples, mansions, fortresses, trial chambers, ruins)",
|
|
147
|
+
"redstone: wire, repeaters, comparators, pistons, observers, hoppers, rails and minecarts",
|
|
148
|
+
"weather, day/night cycle, block light and fluid simulation",
|
|
149
|
+
"achievements and procedurally generated music",
|
|
150
|
+
"chat console with commands (/give, /tp, /locate, /summon, …)",
|
|
151
|
+
"touch controls for mobile",
|
|
141
152
|
"world generation from a picture + description (WorldSpec recipe)",
|
|
142
153
|
"AI world generation via client.ai (feature \"voxel-world\")",
|
|
143
154
|
"AI builds objects from pictures out of coloured blocks (a plane, a car… — models of shapes)",
|
|
144
155
|
"AI recreates landscapes from pictures (terrain sketch, rivers, biomes)",
|
|
145
156
|
"autosave and 'My worlds' (IndexedDB)",
|
|
146
|
-
"
|
|
157
|
+
"own player-facing names for a re-skinnable commercial game (Minecraft names as aliases)",
|
|
147
158
|
"3D block rendering (Three.js)"
|
|
148
159
|
],
|
|
149
160
|
"tags": [
|
|
150
161
|
"voxel",
|
|
151
162
|
"sandbox",
|
|
163
|
+
"survival",
|
|
164
|
+
"crafting",
|
|
152
165
|
"minecraft",
|
|
153
166
|
"3d",
|
|
154
167
|
"first-person",
|
|
168
|
+
"open-world",
|
|
169
|
+
"redstone",
|
|
170
|
+
"mobile",
|
|
155
171
|
"three",
|
|
156
172
|
"ai",
|
|
157
173
|
"world-generation"
|
|
@@ -165,14 +181,14 @@
|
|
|
165
181
|
"name": "iDos Games",
|
|
166
182
|
"url": "https://idosgames.com"
|
|
167
183
|
},
|
|
168
|
-
"version": "0.
|
|
184
|
+
"version": "0.3.0",
|
|
169
185
|
"dependencies": {
|
|
170
|
-
"@idosgames/core": "0.14.
|
|
171
|
-
"@idosgames/module-sdk": "0.3.
|
|
186
|
+
"@idosgames/core": "0.14.1",
|
|
187
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
172
188
|
"three": "0.185.1"
|
|
173
189
|
},
|
|
174
|
-
"fileCount":
|
|
175
|
-
"contentHash": "
|
|
190
|
+
"fileCount": 388,
|
|
191
|
+
"contentHash": "45c798f6bcb03c28ff60e9b60a7d85002f3b031302b2d28c34b9ae633cb45ed9"
|
|
176
192
|
},
|
|
177
193
|
{
|
|
178
194
|
"id": "game-hud",
|
|
@@ -219,8 +235,8 @@
|
|
|
219
235
|
]
|
|
220
236
|
},
|
|
221
237
|
"dependencies": {
|
|
222
|
-
"@idosgames/core": "0.14.
|
|
223
|
-
"@idosgames/module-sdk": "0.3.
|
|
238
|
+
"@idosgames/core": "0.14.1",
|
|
239
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
224
240
|
"@idosgames/react": "0.2.7",
|
|
225
241
|
"@idosgames/wallet": "0.2.7",
|
|
226
242
|
"@tanstack/react-query": "5.101.2",
|
|
@@ -230,6 +246,50 @@
|
|
|
230
246
|
},
|
|
231
247
|
"fileCount": 9,
|
|
232
248
|
"contentHash": "176e3d1a194c398afafe43b05df5283fa0a45acf6a95442b7222bc1ccf52c61a"
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"id": "workshop",
|
|
252
|
+
"name": "Workshop",
|
|
253
|
+
"type": "feature",
|
|
254
|
+
"engine": "dom",
|
|
255
|
+
"summary": "Workshop: players and the publisher share content (maps, levels, skins, models) — free, for in-game resources, or unlocked by holding items.",
|
|
256
|
+
"description": "A ready catalog of user-generated content for ANY game. Players publish what they made (a world, a level, a skin, any file the title allows), set how others get it — free, a price in in-game resources (several options at once, the author receives it minus commission), or 'hold N of this item / currency to unlock' (nothing is charged; while held or once) — and others browse, filter, acquire, open, like, favorite, follow authors and report. The publisher configures content types, formats, limits, commission and moderation in the Workshop section of the title config, curates featured collections and publishes official content from the dashboard. A game plugs its own content in by registering a handler with ctx.content.registerType({ type, label, listLocal, capture, open, modeId }) — the Workshop never imports the game. VoxelCraft registers 'voxelcraft.world'.",
|
|
257
|
+
"provides": [
|
|
258
|
+
"player-made content catalog (maps, levels, skins, models, any file type the title allows)",
|
|
259
|
+
"publish a map or level made in the game",
|
|
260
|
+
"sell content for in-game resources with author royalties minus commission",
|
|
261
|
+
"free content sharing",
|
|
262
|
+
"unlock content by holding an item or currency without spending it",
|
|
263
|
+
"likes, favorites and following content creators",
|
|
264
|
+
"report content and auto-hide after reports",
|
|
265
|
+
"publisher featured collections and official content",
|
|
266
|
+
"download and open shared content in the game"
|
|
267
|
+
],
|
|
268
|
+
"tags": [
|
|
269
|
+
"workshop",
|
|
270
|
+
"ugc",
|
|
271
|
+
"user-generated-content",
|
|
272
|
+
"maps",
|
|
273
|
+
"levels",
|
|
274
|
+
"mods",
|
|
275
|
+
"sharing",
|
|
276
|
+
"marketplace",
|
|
277
|
+
"creators",
|
|
278
|
+
"social"
|
|
279
|
+
],
|
|
280
|
+
"author": {
|
|
281
|
+
"name": "iDos Games",
|
|
282
|
+
"url": "https://idosgames.com"
|
|
283
|
+
},
|
|
284
|
+
"version": "0.1.0",
|
|
285
|
+
"dependencies": {
|
|
286
|
+
"@idosgames/core": "0.14.1",
|
|
287
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
288
|
+
"@idosgames/react": "0.2.7",
|
|
289
|
+
"react": "19.2.7"
|
|
290
|
+
},
|
|
291
|
+
"fileCount": 4,
|
|
292
|
+
"contentHash": "9b0128630509059fdfff91598bdb2e8ab7accc1fba452358caac04248d8165ba"
|
|
233
293
|
}
|
|
234
294
|
],
|
|
235
295
|
"skills": [
|
|
@@ -241,6 +301,10 @@
|
|
|
241
301
|
"name": "ai-generation-system",
|
|
242
302
|
"description": "Add InApp AI generation to a game on the iDosGames TypeScript SDK (@idosgames/core) via client.ai (AIService): AI NPC dialogue and other text generation (optionally with images in — e.g. build a voxel world from a picture the player uploads), image generation and image editing, video, speech (TTS), music and 3D models — every generation is a job the caller waits for (client.ai.waitForGeneration). Use this whenever the user wants AI-generated content at runtime in a game built on the iDosGames TS SDK or its templates (board-game, idle-rpg, voxelcraft) — AI NPCs, generated skins / avatars / items / levels / worlds, voice lines, generated music, 3D props — or touches client.ai, AIService, AIGenerationView, AIPublicDefinitions, AIPublicFeature, AITextInput, AIImageInput, AIInputImage, AIModality, AIAction, AIErrorCode, isAIGenerationTerminal or parseAIErrorCode — even if they don't name the module."
|
|
243
303
|
},
|
|
304
|
+
{
|
|
305
|
+
"name": "analytics-events",
|
|
306
|
+
"description": "Log custom analytics events from a game on the iDosGames TypeScript SDK (@idosgames/core) via client.analytics (AnalyticsService) — the Firebase Analytics equivalent of the platform: logEvent(name, params, value), logScreenView, flush, player consent (setEnabled), automatic events (idos_first_open, idos_session_start, idos_app_update, idos_os_update), and the title's Analytics config section (Enabled, EventNaming Open/Declared, declared and blocked events, custom dimensions, retention days). Use this whenever the user wants to track what players do, funnels of their own events, \"how many players reached level 10\", revenue or score per event, breakdowns by platform / app version / OS / device / language / country, goals of an A/B experiment, or touches client.analytics, logEvent, AnalyticsDefinitions, LogEventsResponse or the analytics:* events — even if they don't name the module explicitly."
|
|
307
|
+
},
|
|
244
308
|
{
|
|
245
309
|
"name": "authentication",
|
|
246
310
|
"description": "Log players into a game on the iDosGames TypeScript SDK (@idosgames/core) via client.auth (AuthenticationService): guest/device-id login, two-step email registration with a confirmation code, email login, Google/Telegram login, SSO-code login from idosgames.com, wallet login, password reset, auto login on relaunch, session refresh, logout, and client-side email/password validation. Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and asks about logging a player in, sessions, registration, email confirmation codes, resending a code, guest accounts, device-id login, Telegram login, Google login, SSO login, wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise touches client.auth, AuthenticationService, or AuthContext — even if they don't name the module explicitly."
|
|
@@ -263,7 +327,7 @@
|
|
|
263
327
|
},
|
|
264
328
|
{
|
|
265
329
|
"name": "cloud-code",
|
|
266
|
-
"description": "Write and call custom server-side game logic on the iDosGames platform: author a CloudCode handler (sandboxed JavaScript with a server.* API) and invoke it from the game via client.cloudCode (CloudCodeService) with an arbitrary JSON payload. Use this whenever the user wants bespoke server logic, a \"cloud script\", \"cloud function\" or \"server callable\", logic that must be authoritative (granting rewards, validating a reported result, anti-cheat), a write to protected player data (UserCustomData ReadOnly / Internal buckets) or to shared title state (TitleCustomData Runtime scope), or otherwise touches client.cloudCode, CloudCodeService, handlers, server.SetUserCustomData, server.IncrementTitleCustomData, server.HttpRequest or ExecuteCloudCodeResponse — even if they don't name the module explicitly. Also covers integrating a title with a third-party service (calling an external API with a stored API key, webhooks out, payment or analytics providers) and the {{secret:NAME}} / {{var:NAME}} placeholders."
|
|
330
|
+
"description": "Write and call custom server-side game logic on the iDosGames platform: author a CloudCode handler (sandboxed JavaScript with a server.* API) and invoke it from the game via client.cloudCode (CloudCodeService) with an arbitrary JSON payload. Use this whenever the user wants bespoke server logic, a \"cloud script\", \"cloud function\" or \"server callable\", logic that must be authoritative (granting rewards, validating a reported result, anti-cheat), a write to protected player data (UserCustomData ReadOnly / Internal buckets) or to shared title state (TitleCustomData Runtime scope), or otherwise touches client.cloudCode, CloudCodeService, handlers, server.SetUserCustomData, server.IncrementTitleCustomData, server.HttpRequest or ExecuteCloudCodeResponse — even if they don't name the module explicitly. Also covers integrating a title with a third-party service (calling an external API with a stored API key, webhooks out, payment or analytics providers) and the {{secret:NAME}} / {{var:NAME}} placeholders, handlers that run on a schedule (cron-like jobs: daily resets, auction settlement) and incoming webhooks from external services (payment callbacks, site form posts, bots)."
|
|
267
331
|
},
|
|
268
332
|
{
|
|
269
333
|
"name": "collection-system",
|
|
@@ -285,6 +349,10 @@
|
|
|
285
349
|
"name": "currency-system",
|
|
286
350
|
"description": "Convert between currencies in a game on the iDosGames TypeScript SDK (@idosgames/core) via client.currency (CurrencyService): virtual-currency to virtual-currency (VC↔VC) conversion, and crypto-source conversion (crypto→VC or crypto→crypto). This is also the canonical home for the SDK-wide shared ResourceConsume/ResourceGrant/ResourceOperation/ResourceEntry cost-and-reward types used by every other module (Store, Character, Craft, Lootbox, Blockchain, …). Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and wants a currency-exchange screen, gold-to-gems conversion, crypto conversion, or otherwise touches client.currency, CurrencyService, ConvertResponse, CryptoConvertResponse, ResourceConsume, ResourceGrant, ResourceOperation, or ResourceEntry — even if they don't name the module explicitly."
|
|
287
351
|
},
|
|
352
|
+
{
|
|
353
|
+
"name": "data-collections",
|
|
354
|
+
"description": "Store, list, search and sort many records of one shape in a game, app or site on the iDosGames TypeScript SDK (@idosgames/core) via client.dataCollections (DataCollectionsService): guilds, auctions/lots, orders, product catalogs, articles/news, saved levels and builds, feeds, mail, invites, lobbies, match history — anything no ready platform module covers. Use this whenever the user wants their OWN data structure with fields, filters, pagination, sharing between users, roles (admin/editor/moderator), public pages readable without login, images or files attached to records (File fields, uploadFile), or touches client.dataCollections, DataCollectionsService, DataQuerySpec, DataCollectionDefinition or the DataCollections config section — even if they don't name it. NOT client.collection (collectible card sets). For one player's simple values use user-custom-data; for one title-wide value use title-custom-data; for currencies/items/store/quests use their modules."
|
|
355
|
+
},
|
|
288
356
|
{
|
|
289
357
|
"name": "deal-offer-system",
|
|
290
358
|
"description": "Build a personalized / targeted deal-offer system in a game on the iDosGames TypeScript SDK (@idosgames/core) via client.dealOffer (DealOfferService): load slot and offer definitions, load the player's deal-offer state, fetch the currently active deals per slot, dismiss a deal, execute a node in an offer's graph (purchase / free claim / rewarded-video / info step), record an impression (show event), and claim a milestone reward (single or batch). Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and wants limited-time offer popups, IAP funnels, \"special offer\" slots, node/graph-based offer chains, rewarded-video offer steps, offer milestone/progress bars, or otherwise touches client.dealOffer, DealOfferService, DealOfferDefinitions, DealNodeDefinition, UserDealOffersState, ActiveDealSlotInfo, or ExecuteNodeResponse — even if they don't name the module explicitly."
|
|
@@ -293,6 +361,10 @@
|
|
|
293
361
|
"name": "dev-test-loop",
|
|
294
362
|
"description": "Run the боево (real) create→configure→generate→test→fix loop when building or improving a game feature on the iDosGames TypeScript SDK (@idosgames/core). Provision the DEV title ({TitleID}-DEV), implement the feature with the SDK and the per-module skills (character-system, store-system, quest-system, …), apply the title config for every module used, generate content (image/audio/3D) if needed, then run `npm run verify` (@idosgames/harness) against the DEV title and fix until it goes green. Use this whenever the user wants to build, add, or improve a game feature end-to-end, test a feature against the backend, close the loop, or otherwise wants the work to end in a green verify run against a real sandbox rather than just written code — even if they don't name the loop."
|
|
295
363
|
},
|
|
364
|
+
{
|
|
365
|
+
"name": "experiments-system",
|
|
366
|
+
"description": "Configure and consume A/B experiments on the iDosGames platform: the title's Experiment config section (variants with weights and Params, audience Gate, Schedule, Salt, LayerID, sticky assignment, Goals, ActivationEvent, RolloutVariantID), gating content by variant through SegmentGate.Experiment in any module, and reading the player's variant and its remote-config Params in a game on the TypeScript SDK via client.experiments (ExperimentsService: load, getVariant, getParam, isInVariant, getStatus). Use this whenever the user wants an A/B test, a split test, remote config per variant, a price / offer / difficulty experiment, rolling out a winner, or touches ExperimentDefinitions, ExperimentGoal, PlayerExperimentView, GetExperimentsResponse or the experiments:* events — even if they don't name the module explicitly."
|
|
367
|
+
},
|
|
296
368
|
{
|
|
297
369
|
"name": "game-loop-system",
|
|
298
370
|
"description": "Build a board-style core game loop on the iDosGames TypeScript SDK (@idosgames/core) via client.gameLoop (GameLoopService): roll dice around a board, attack/raid other players' or bots' cities, build up buildings, resolve Special (Instant/Timed) tile choices, and run the cooperative Community Chest group meter. Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and wants a dice/roll board loop, an attack or raid/heist mini-game, city building, survival/Special tile events, or a co-op group-progress feature — or otherwise touches client.gameLoop, GameLoopService, GameLoopModels, BoardLoopState, BoardLoopDefinition, or CommunityChest — even if they don't name the module explicitly. templates/board-game is built entirely on this module."
|
|
@@ -311,7 +383,7 @@
|
|
|
311
383
|
},
|
|
312
384
|
{
|
|
313
385
|
"name": "idosgames-module-contract",
|
|
314
|
-
"description": "Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk: the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route registration, and the shared controller-box bridge between an imperative scene and React panels. Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg, voxelcraft, game-hud), or asks about defineModule, registerScene/registerPanel/registerRoute, activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic, ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content), ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention."
|
|
386
|
+
"description": "Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk: the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route registration, and the shared controller-box bridge between an imperative scene and React panels. Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg, voxelcraft, game-hud, workshop), or asks about defineModule, registerScene/registerPanel/registerRoute, activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic, ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content), ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention."
|
|
315
387
|
},
|
|
316
388
|
{
|
|
317
389
|
"name": "idosgames-project-structure",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@idosgames/core": "0.14.
|
|
49
|
-
"@idosgames/module-sdk": "0.3.
|
|
48
|
+
"@idosgames/core": "0.14.1",
|
|
49
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
50
50
|
"@idosgames/react": "0.2.7",
|
|
51
51
|
"@idosgames/wallet": "0.2.7",
|
|
52
52
|
"@tanstack/react-query": "5.101.2",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@idosgames/core": "0.14.
|
|
52
|
-
"@idosgames/module-sdk": "0.3.
|
|
51
|
+
"@idosgames/core": "0.14.1",
|
|
52
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
53
53
|
"@idosgames/react": "0.2.7",
|
|
54
54
|
"@idosgames/wallet": "0.2.7",
|
|
55
55
|
"@tanstack/react-query": "5.101.2",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
}
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@idosgames/core": "0.14.
|
|
53
|
-
"@idosgames/module-sdk": "0.3.
|
|
52
|
+
"@idosgames/core": "0.14.1",
|
|
53
|
+
"@idosgames/module-sdk": "0.3.1",
|
|
54
54
|
"@idosgames/react": "0.2.7",
|
|
55
55
|
"@idosgames/wallet": "0.2.7",
|
|
56
56
|
"@tanstack/react-query": "5.101.2",
|