@idosgames/mcp 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -48,7 +48,7 @@ function moduleExportName(id) {
48
48
  var TOOLS = [
49
49
  {
50
50
  name: "list_modules",
51
- description: "List the composable iDosGames game/app modules in the catalog (id, name, type, engine, genre, dependency count). Use before get_module to pick what to add.",
51
+ description: "List the composable iDosGames modules in the catalog. Each entry has id, name, type (template = a complete ready-to-run game to start FROM, e.g. voxelcraft; feature = a capability to add on top), engine, genre, a short summary, what it 'provides' (its features), tags, preview media, a live demo url, and its dependency count. Read 'provides' to reuse a ready module instead of building the feature from scratch, then get_module to pull its source.",
52
52
  inputSchema: { type: "object", properties: {} }
53
53
  },
54
54
  {
@@ -93,7 +93,7 @@ var TOOLS = [
93
93
  },
94
94
  {
95
95
  name: "search",
96
- description: "Keyword search across modules and skills (id, name, genre, engine, description). Returns matching catalog entries.",
96
+ description: "Keyword search across modules and skills. Matches a module on its id, name, type, engine, genre, summary, description, what it 'provides', and tags \u2014 so you can find a ready module by the feature you need (e.g. 'dice board', 'idle income', 'voxel building') rather than by name. Skills match on name and description. Returns the matching catalog entries.",
97
97
  inputSchema: {
98
98
  type: "object",
99
99
  properties: { query: { type: "string", description: "Free-text query" } },
@@ -112,10 +112,25 @@ function argString(args, key) {
112
112
  const v = args?.[key];
113
113
  return typeof v === "string" && v.trim() ? v.trim() : null;
114
114
  }
115
+ var SERVER_INSTRUCTIONS = [
116
+ "This MCP serves the iDosGames CODE registry. Use it to WRITE a game's source:",
117
+ "get_host_scaffold (the host shell for a new project), list_modules / search / get_module (composable",
118
+ "game/app module source you write under src/modules/{id}/), get_manifest (exact @idosgames/* runtime",
119
+ "versions), and list_skills / get_skill (how to code against @idosgames/core and the module contract).",
120
+ "It does NOT read or change any live Title's data.",
121
+ "To configure a live Title's settings (TitlePublicConfiguration) or generate assets",
122
+ "(image / audio / 3D / video / text), that is a SEPARATE server \u2014 the iDosGames Title-configuration MCP:",
123
+ "HTTP JSON-RPC at POST https://site.idosgames.com/api/v2/mcp, authenticated with an X-MCP-API-Key",
124
+ "header (the publisher issues the key per Title on platform.idosgames.com), tools get_<field> /",
125
+ "save_<field> and generate_*. Connect it as an HTTP MCP server; keep the key out of committed config",
126
+ 'via env expansion \u2014 .mcp.json: {"idosgames-title": {"type": "http", "url":',
127
+ '"https://site.idosgames.com/api/v2/mcp", "headers": {"X-MCP-API-Key": "${IDOS_MCP_API_KEY}"}}}.',
128
+ "Rule of thumb: game CODE \u2192 this server; a Title's live config DATA and generated ASSETS \u2192 the backend v2/mcp server."
129
+ ].join(" ");
115
130
  function createServer() {
116
131
  const server = new Server(
117
- { name: "idosgames", version: "0.1.0" },
118
- { capabilities: { tools: {} } }
132
+ { name: "idosgames", version: "0.1.2" },
133
+ { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS }
119
134
  );
120
135
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
121
136
  tools: TOOLS
@@ -173,7 +188,17 @@ function createServer() {
173
188
  const hit = (text) => tokens.every((t) => text.includes(t));
174
189
  const modules = index.modules.filter(
175
190
  (m) => hit(
176
- `${m.id} ${m.name} ${m.genre ?? ""} ${m.engine} ${m.type}`.toLowerCase()
191
+ [
192
+ m.id,
193
+ m.name,
194
+ m.type,
195
+ m.engine,
196
+ m.genre ?? "",
197
+ m.summary ?? "",
198
+ m.description ?? "",
199
+ (m.provides ?? []).join(" "),
200
+ (m.tags ?? []).join(" ")
201
+ ].join(" ").toLowerCase()
177
202
  )
178
203
  );
179
204
  const skills = index.skills.filter(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@idosgames/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
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",
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "bin": {
29
- "idosgames-mcp": "./dist/cli.js"
29
+ "idosgames-mcp": "dist/cli.js"
30
30
  },
31
31
  "files": [
32
32
  "dist",
@@ -1,25 +1,37 @@
1
1
  {
2
2
  "id": "host-starter",
3
3
  "files": [
4
+ {
5
+ "path": "IDOS.md",
6
+ "content": "# IDOS.md — project memory\n\nThis file is loaded into the AI editor's context on every run. Keep it short and current: record\ndurable conventions and constraints here, delete anything that goes stale.\n\n## What this project is\n\nA **host shell + feature modules** app built on the iDosGames SDK.\n\n- `src/main.tsx` — the host: creates ONE `IDosGamesClient`, logs the player in, then calls\n `mountHost({ container, client, modules })`. Modules never create their own client or log in.\n- `src/modules.ts` — the composition list. Every module the project uses is imported and listed\n here; a fresh project starts empty.\n- `src/modules/<id>/` — one folder per feature module (its source, already copied in).\n- `src/idos.title.ts` — **generated, do not edit.** The project's identity: which Title's data this\n game reads and writes. Baked into the bundle so the build still knows its Title where there is no\n URL to read — packaged as a mobile app, embedded in an iframe, or opened from a shared link.\n- `src/config.ts` / `src/env.ts` — resolve the effective title (identity from `idos.title.ts`,\n environment on top) and the build key. Do not hardcode either elsewhere, and never read the title\n from the URL yourself: use `client.titleID`, or `ctx.titleId` inside a module's `setup()`.\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\nBecause of that:\n\n1. **Load the matching skill first.** Skills are the authoritative recipes for this SDK — there is\n one per service (store, currency, item, quest, leaderboard, auth, blockchain, …). Load the\n relevant one _before_ writing code that touches `@idosgames/*`.\n2. Then copy the patterns from the modules already in `src/modules/`.\n3. Never invent an SDK method, hook, or type name. An invented API looks plausible and fails the\n build.\n\nCommon entry points: `client.<service>.<action>()` returns `OperationResult<T>` (check `isOk` /\n`isFail`; it does not throw). In React, reach the client with `useIDosGamesClient()` and player\nstate with `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 by the publisher in the platform UI, per environment. There is **no config file in this\nproject** — do not create `config/public-configuration.json` or any other `config/*.json`; they are\nignored.\n\nWrite code against ids/keys that already exist in the title. If a feature needs an entity that is\nnot configured yet, say so explicitly and name exactly what has to be added, instead of inventing\nids. Code/configuration mismatch is the most common cause of a feature that builds but does nothing.\n\n## Constraints\n\n- No package installs, no native dependencies — use what `package.json` already lists.\n- No terminal. The build runs on the server after each turn and its errors come back to you.\n- Client-side code shipped to players: never put secrets, keys or tokens in any file here.\n- Assets are not stored in the project — generated images/audio/3D return CDN URLs; reference those.\n\n## Project notes\n\n<!-- Record durable decisions and conventions below as the project grows. -->\n"
7
+ },
4
8
  {
5
9
  "path": "index.html",
6
10
  "content": "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>Host Shell · IDosGames SDK</title>\n <style>\n html,\n body,\n #app {\n margin: 0;\n height: 100%;\n background: #0c0a18;\n }\n </style>\n </head>\n <body>\n <div id=\"app\"></div>\n <script type=\"module\" src=\"/src/main.tsx\"></script>\n </body>\n</html>\n"
7
11
  },
8
12
  {
9
13
  "path": "package.json",
10
- "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.1.0\",\n \"@idosgames/core\": \"0.1.1\",\n \"@idosgames/module-sdk\": \"0.1.0\",\n \"@idosgames/react\": \"0.1.0\",\n \"react\": \"19.2.7\",\n \"react-dom\": \"19.2.7\"\n },\n \"devDependencies\": {\n \"@types/react\": \"19.2.17\",\n \"@types/react-dom\": \"19.2.3\",\n \"@vitejs/plugin-react\": \"4.7.0\",\n \"typescript\": \"5.9.3\",\n \"vite\": \"5.4.21\"\n }\n}\n"
14
+ "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.1.2\",\n \"@idosgames/core\": \"0.1.3\",\n \"@idosgames/module-sdk\": \"0.1.1\",\n \"@idosgames/react\": \"0.1.0\",\n \"react\": \"19.2.7\",\n \"react-dom\": \"19.2.7\"\n },\n \"devDependencies\": {\n \"@types/react\": \"19.2.17\",\n \"@types/react-dom\": \"19.2.3\",\n \"@vitejs/plugin-react\": \"4.7.0\",\n \"typescript\": \"5.9.3\",\n \"vite\": \"5.4.21\"\n }\n}\n"
11
15
  },
12
16
  {
13
17
  "path": "src/config.ts",
14
- "content": "// Demo configuration always targets the real backend (https://api.idosgames.com).\n// The title is resolved at runtime, in this order:\n// 1. ?titleID= / ?buildKey= on the launch URL how the platform parameterizes a hosted build\n// 2. templates/host-starter/.env.local (VITE_IDOS_TITLE_ID / VITE_IDOS_BUILD_KEY)\n// 3. the public demo title\n// Resolving at runtime rather than build time lets one build serve both a staged preview and prod.\n\nimport { ENV_TITLE_ID, ENV_BUILD_KEY } from \"./env\";\n\nconst launchParams =\n typeof window === \"undefined\"\n ? null\n : new URLSearchParams(window.location.search);\n\nexport const TITLE_ID =\n launchParams?.get(\"titleID\") ?? (ENV_TITLE_ID || \"URLV9SUP\");\nexport const BUILD_KEY = launchParams?.get(\"buildKey\") ?? ENV_BUILD_KEY;\n"
18
+ "content": "// Which Title this app talks to. Always targets the real backend (https://api.idosgames.com).\n//\n// IDENTITY and ENVIRONMENT are resolved separately, and that separation is the point:\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.\n// 2. /drive/app/{id}/… — the hosted artifact's own path.\n// 3. ?titleID= — explicit override, for debugging and staged previews.\n// 4. 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`. That is what lets a staged preview run\n// the production artifact against dev data without the baked identity getting in the way.\n//\n// Baking identity into a file instead of reading the URL is what lets the same source ship as a\n// packaged mobile app, where there is no URL to read at all.\n\nimport { IDOS_BUILD_KEY, IDOS_DEFAULT_ENV, IDOS_TITLE_ID } from \"./idos.title\";\nimport { ENV_BUILD_KEY, ENV_TITLE_ID } from \"./env\";\n\n/** Mirrors DevTitleService.DevSuffix on the backend. */\nconst DEV_SUFFIX = \"-DEV\";\n\nconst launchParams =\n typeof window === \"undefined\"\n ? null\n : new URLSearchParams(window.location.search);\n\n/**\n * Hosted AI Coder artifacts live at `/drive/app/{titleId}/…` (live pointer) and\n * `/drive/app/{titleId}/v/{buildId}/…` (a pinned version), so the path already identifies the\n * title. Deliberately narrow: the shared per-template builds under `/bld/{templateId}/` serve many\n * titles from one artifact, and guessing a title from that path would be wrong.\n */\nfunction titleFromPath(): string | null {\n if (typeof window === \"undefined\") return null;\n const match = window.location.pathname.match(\n /\\/drive\\/app\\/([A-Za-z0-9_-]{1,64})\\//,\n );\n return match?.[1] ?? null;\n}\n\n/** Identity is always canonical; the dev suffix is an environment, so strip it if an override carries one. */\nfunction toCanonical(id: string): string {\n return id.endsWith(DEV_SUFFIX) ? id.slice(0, -DEV_SUFFIX.length) : id;\n}\n\nconst overrideTitle = launchParams?.get(\"titleID\") || null;\nconst envParam = launchParams?.get(\"env\") || null;\n\nconst canonicalTitle =\n IDOS_TITLE_ID ||\n titleFromPath() ||\n (overrideTitle ? toCanonical(overrideTitle) : null) ||\n ENV_TITLE_ID;\n\nif (!canonicalTitle) {\n throw new Error(\n \"[idos] No Title id resolved. Expected src/idos.title.ts (generated by the platform), \" +\n \"a /drive/app/{titleID}/ artifact path, ?titleID= on the URL, or VITE_IDOS_TITLE_ID in .env.local.\",\n );\n}\n\n/**\n * Precedence: explicit `?env` → the suffix on an explicit `?titleID` (how staged previews run the\n * prod artifact against dev data) → the baked default, which is the only channel a packaged mobile\n * build has.\n */\nfunction resolveIsDev(): boolean {\n if (envParam !== null) return envParam === \"dev\";\n if (overrideTitle !== null) return overrideTitle.endsWith(DEV_SUFFIX);\n return IDOS_DEFAULT_ENV === \"dev\";\n}\n\nconst isDev = resolveIsDev();\n\nexport const TITLE_ID = isDev\n ? `${canonicalTitle}${DEV_SUFFIX}`\n : canonicalTitle;\n\nexport const BUILD_KEY =\n IDOS_BUILD_KEY || launchParams?.get(\"buildKey\") || ENV_BUILD_KEY;\n"
15
19
  },
16
20
  {
17
21
  "path": "src/env.ts",
18
- "content": "// Env совместимое СРАЗУ с двумя сборщиками:\n// 1. настоящий vite (standalone-dev, прод, контейнер-билд build-runner) — значения приезжают из\n// vite.config через define в глобал __IDOS_ENV__ (см. vite.config.ts);\n// 2. классический бандлер превью (Sandpack) — он падает на самом токене `import.meta`, поэтому\n// его здесь быть не должно. Глобала __IDOS_ENV__ у него нет → `typeof` вернёт \"undefined\",\n// берём дефолты.\ntype IdosEnv = {\n DEV?: boolean;\n TITLE_ID?: string;\n BUILD_KEY?: string;\n WALLETCONNECT_PROJECT_ID?: string;\n};\n\ndeclare const __IDOS_ENV__: IdosEnv | undefined;\n\nconst env: IdosEnv =\n typeof __IDOS_ENV__ !== \"undefined\" && __IDOS_ENV__ ? __IDOS_ENV__ : {};\n\nexport const IS_DEV = env.DEV ?? false;\nexport const ENV_TITLE_ID = env.TITLE_ID ?? \"\";\nexport const ENV_BUILD_KEY = env.BUILD_KEY ?? \"\";\nexport const ENV_WALLETCONNECT_PROJECT_ID = env.WALLETCONNECT_PROJECT_ID ?? \"\";\n"
22
+ "content": "// Env совместимое СРАЗУ с двумя сборщиками:\n// 1. настоящий vite (standalone-dev, прод, контейнер-билд build-runner) — значения приезжают из\n// vite.config через define в глобал __IDOS_ENV__ (см. vite.config.ts);\n// 2. классический бандлер превью (Sandpack) — он падает на самом токене `import.meta`, поэтому\n// его здесь быть не должно. Глобала __IDOS_ENV__ у него нет → `typeof` вернёт \"undefined\",\n// берём дефолты.\ntype IdosEnv = {\n DEV?: boolean;\n TITLE_ID?: string;\n BUILD_KEY?: string;\n WALLETCONNECT_PROJECT_ID?: string;\n GOOGLE_CLIENT_ID?: string;\n};\n\ndeclare const __IDOS_ENV__: IdosEnv | undefined;\n\nconst env: IdosEnv =\n typeof __IDOS_ENV__ !== \"undefined\" && __IDOS_ENV__ ? __IDOS_ENV__ : {};\n\nexport const IS_DEV = env.DEV ?? false;\nexport const ENV_TITLE_ID = env.TITLE_ID ?? \"\";\nexport const ENV_BUILD_KEY = env.BUILD_KEY ?? \"\";\nexport const ENV_WALLETCONNECT_PROJECT_ID = env.WALLETCONNECT_PROJECT_ID ?? \"\";\n\n/** Google Identity client id. Empty = the login screen hides the Google button rather than\n * offering one that cannot work (a Google sign-in needs a real ID token). */\nexport const ENV_GOOGLE_CLIENT_ID = env.GOOGLE_CLIENT_ID ?? \"\";\n"
23
+ },
24
+ {
25
+ "path": "src/idos.title.ts",
26
+ "content": "// The project's IDENTITY file — the ONE centralized place the Title id lives.\n//\n// On platform-created projects the platform GENERATES this file when it creates the project; there\n// the AI editor is denied write access to this path on purpose — do not edit it by hand. On a\n// manually scaffolded project (get_host_scaffold / copied template) there is no generator: YOU fill\n// IDOS_TITLE_ID here yourself. Either way, do not import anything into this file, and do not bind\n// the title anywhere else (.env.local is a local-dev fallback for the raw template only).\n//\n// It is the highest-priority source of the Title id (see config.ts for the full chain). It is baked\n// into the bundle rather than read from the URL because a build has to keep working where there is\n// no URL to read — packaged as a mobile app, embedded in an iframe, or opened from a shared link\n// that dropped its query string.\n//\n// It holds the CANONICAL (production) title. The DEV title is derived from it, never stored here —\n// one project has one identity, and DEV/PROD is an environment on top of that identity.\n\n/** Canonical (production) Title id. Empty only in the raw template, before the platform seeds it. */\nexport const IDOS_TITLE_ID = \"\";\n\n/** Build key for this title, if the title enforces one. */\nexport const IDOS_BUILD_KEY = \"\";\n\n/** Environment this artifact defaults to. Web builds may override it (see config.ts); a packaged\n * mobile build cannot, so a DEV app is produced by generating this file with \"dev\". */\nexport const IDOS_DEFAULT_ENV: \"prod\" | \"dev\" = \"prod\";\n\n/**\n * Whether this title was created as web3. Set by the platform from the same toggle the publisher\n * used at title creation.\n *\n * It is baked here rather than read from the title's blockchain config because the login screen\n * needs it BEFORE there is a session, and `client.title.getTitlePublicConfiguration()` requires one.\n * LoginScreen.tsx uses it only as the default for which providers to offer — that list is ordinary\n * editable code, so a title that goes web3 later just gets the wallet button added there.\n */\nexport const IDOS_WEB3 = false;\n\n/**\n * NetworkID the wallet login challenge is issued for (e.g. \"bsc\", \"base\", \"solana\"). Empty on\n * web2 titles. Baked for the same reason as everything else here: the login screen needs it before\n * there is a session, and the title's blockchain config is only readable once logged in.\n */\nexport const IDOS_WEB3_NETWORK_ID = \"\";\n"
27
+ },
28
+ {
29
+ "path": "src/LoginScreen.tsx",
30
+ "content": "import { useState, type CSSProperties, type ReactNode } from \"react\";\nimport type { LoginScreenProps } from \"@idosgames/app-shell\";\nimport { ENV_GOOGLE_CLIENT_ID } from \"./env\";\nimport { IDOS_WEB3 } from \"./idos.title\";\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, loginWithPlatformToken, 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 — needs the connector; pass `renderWalletLogin` from main.tsx (see below).\n\nexport interface LoginScreenExtras {\n /**\n * Wallet sign-in, supplied by the project when it has the connector installed.\n *\n * Wiring it: add `@idosgames/wallet` to package.json, then render a button that connects the\n * wallet (wagmi) and calls `loginWithWalletEvm({ client, clients, networkID })` from\n * `@idosgames/wallet` — it runs requestWalletChallenge → personal_sign → loginWithWallet and\n * logs the client in. Call `onAuthenticated()` when it resolves ok.\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 }) => ReactNode;\n}\n\ntype Mode = \"menu\" | \"email\";\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\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(result.error ?? \"Sign-in failed. Please try again.\");\n setBusy(false);\n };\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 <div style={styles.card}>\n <h1 style={styles.title}>Sign in</h1>\n\n {mode === \"menu\" && (\n <div style={styles.stack}>\n {IDOS_WEB3 &&\n renderWalletLogin?.({ client, onAuthenticated, disabled: busy })}\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 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 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 void run(() =>\n registering\n ? client.auth.registerWithEmail(email, password)\n : client.auth.loginWithEmail(email, password),\n )\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 {/* 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 />\n Remember me\n </label>\n\n {error && <p style={styles.error}>{error}</p>}\n </div>\n </div>\n );\n}\n\nconst styles: Record<string, CSSProperties> = {\n root: {\n position: \"absolute\",\n inset: 0,\n display: \"grid\",\n placeItems: \"center\",\n background: \"#0c0a18\",\n color: \"#e8e6f3\",\n font: \"14px system-ui, sans-serif\",\n },\n card: { width: \"min(340px, 88vw)\", display: \"grid\", gap: \"18px\" },\n remember: {\n display: \"flex\",\n alignItems: \"center\",\n gap: \"8px\",\n justifySelf: \"center\",\n cursor: \"pointer\",\n color: \"#a9a4c7\",\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 #4c4470\",\n background: \"#221d3d\",\n color: \"inherit\",\n font: \"inherit\",\n cursor: \"pointer\",\n },\n primary: { background: \"#4c3fa8\", borderColor: \"#6152c7\" },\n ghost: {\n padding: \"8px\",\n border: \"none\",\n background: \"none\",\n color: \"#a49dc8\",\n font: \"inherit\",\n cursor: \"pointer\",\n },\n input: {\n padding: \"11px 12px\",\n borderRadius: \"8px\",\n border: \"1px solid #3a3358\",\n background: \"#15122a\",\n color: \"inherit\",\n font: \"inherit\",\n },\n error: { margin: 0, color: \"#ff9b9b\", textAlign: \"center\" },\n};\n"
19
31
  },
20
32
  {
21
33
  "path": "src/main.tsx",
22
- "content": "import { createIDosGamesClient } from \"@idosgames/core\";\nimport { mountHost } from \"@idosgames/app-shell\";\nimport { modules } from \"./modules\";\nimport { TITLE_ID, BUILD_KEY } from \"./config\";\nimport { IS_DEV } from \"./env\";\n\nconst app = document.getElementById(\"app\");\nif (!app) throw new Error(\"#app container not found\");\n\n// Always runs against the real backend (https://api.idosgames.com) via the global fetch.\nconst client = createIDosGamesClient({\n titleID: TITLE_ID,\n buildKey: BUILD_KEY.length > 0 ? BUILD_KEY : undefined,\n throttleMs: 0,\n});\n\nclient.on(\"error:global\", (message) => {\n console.error(\"[idos] global error:\", message);\n});\nclient.on(\"error:connection\", (message) => {\n console.error(\"[idos] connection error:\", message);\n});\n\nvoid (async () => {\n app.textContent = `Connecting to api.idosgames.com (title ${TITLE_ID})…`;\n\n const login = await client.auth.loginWithDeviceID();\n if (!login.ok) {\n app.textContent = `Login failed [${login.reason}]: ${login.error}`;\n return;\n }\n app.textContent = \"\";\n if (IS_DEV) {\n (\n globalThis as typeof globalThis & { idosClient?: typeof client }\n ).idosClient = client;\n }\n\n // Host owns the single client + login; modules are plugged in and share this session.\n mountHost({ container: app, client, modules });\n})();\n"
34
+ "content": "import { createIDosGamesClient } from \"@idosgames/core\";\nimport { mountHost } from \"@idosgames/app-shell\";\nimport { modules } from \"./modules\";\nimport { LoginScreen } from \"./LoginScreen\";\nimport { renderWalletLogin } from \"./walletLogin\";\nimport { TITLE_ID, BUILD_KEY } from \"./config\";\nimport { IS_DEV } from \"./env\";\n\nconst app = document.getElementById(\"app\");\nif (!app) throw new Error(\"#app container not found\");\n\n// Always runs against the real backend (https://api.idosgames.com) via the global fetch.\nconst client = createIDosGamesClient({\n titleID: TITLE_ID,\n buildKey: BUILD_KEY.length > 0 ? BUILD_KEY : undefined,\n throttleMs: 0,\n});\n\nclient.on(\"error:global\", (message) => {\n console.error(\"[idos] global error:\", message);\n});\nclient.on(\"error:connection\", (message) => {\n console.error(\"[idos] connection error:\", message);\n});\n\nif (IS_DEV) {\n (\n globalThis as typeof globalThis & { idosClient?: typeof client }\n ).idosClient = client;\n}\n\n// The host owns sign-in: mountHost replays the previous session (autoLogin) and, when there is\n// none, renders the login screen. Do NOT log in here that would skip the screen, and with it the\n// player's ability to pick a provider or switch accounts.\n//\n// Wallet sign-in comes from ./walletLogin a no-op on web2 titles, the real button on web3 ones.\nmountHost({\n container: app,\n client,\n modules,\n renderLogin: (props) => (\n <LoginScreen {...props} renderWalletLogin={renderWalletLogin} />\n ),\n});\n"
23
35
  },
24
36
  {
25
37
  "path": "src/modules.ts",
@@ -29,13 +41,17 @@
29
41
  "path": "src/vite-env.d.ts",
30
42
  "content": "/// <reference types=\"vite/client\" />\n"
31
43
  },
44
+ {
45
+ "path": "src/walletLogin.tsx",
46
+ "content": "import type { LoginScreenExtras } from \"./LoginScreen\";\n\n// Wallet sign-in seam.\n//\n// This is the web2 version: no wallet button, and — the point of keeping it in its own file —\n// no import of `@idosgames/wallet`, so a non-crypto game never pulls wagmi/viem/solana into its\n// bundle just to render a login screen.\n//\n// For a title created with the web3 toggle the platform REPLACES this file at project creation\n// with a version that renders `WalletLogin` from `@idosgames/wallet/react`, and adds that package\n// to package.json. To add wallet sign-in to a title that was not created as web3, install\n// `@idosgames/wallet` and write that version here yourself:\n//\n// import { bsc } from \"wagmi/chains\";\n// import { WalletLogin, createEvmWalletConfig } from \"@idosgames/wallet/react\";\n// const wagmiConfig = createEvmWalletConfig({ chains: [bsc] });\n// export const renderWalletLogin: LoginScreenExtras[\"renderWalletLogin\"] = (props) => (\n// <WalletLogin {...props} networkID=\"bsc\" wagmiConfig={wagmiConfig} />\n// );\n\nexport const renderWalletLogin: LoginScreenExtras[\"renderWalletLogin\"] =\n undefined;\n"
47
+ },
32
48
  {
33
49
  "path": "tsconfig.json",
34
50
  "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"
35
51
  },
36
52
  {
37
53
  "path": "vite.config.ts",
38
- "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 }),\n },\n };\n});\n"
54
+ "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"
39
55
  }
40
56
  ]
41
57
  }
@@ -1,41 +1,55 @@
1
1
  {
2
- "generatedFromCommit": "626cd3965e7bcafd925bf0ddccdcb92359469a4e",
2
+ "generatedFromCommit": "0e6d7a0b1db746a991f118ffb6dc42c07ec5ac9d",
3
3
  "runtimePackages": {
4
- "@idosgames/core": "0.1.1",
5
- "@idosgames/wallet": "0.1.1",
6
- "@idosgames/module-sdk": "0.1.0",
4
+ "@idosgames/core": "0.1.4",
5
+ "@idosgames/wallet": "0.1.2",
6
+ "@idosgames/module-sdk": "0.1.1",
7
7
  "@idosgames/react": "0.1.0",
8
- "@idosgames/app-shell": "0.1.0"
8
+ "@idosgames/app-shell": "0.1.2"
9
9
  },
10
10
  "host": {
11
11
  "id": "host-starter",
12
- "fileCount": 9
12
+ "fileCount": 13
13
13
  },
14
14
  "modules": [
15
- {
16
- "id": "currency-hud",
17
- "name": "Wallet HUD",
18
- "type": "app",
19
- "engine": "dom",
20
- "dependencies": {
21
- "@idosgames/core": "0.1.1",
22
- "@idosgames/module-sdk": "0.1.0",
23
- "@idosgames/react": "0.1.0",
24
- "react": "19.2.7"
25
- },
26
- "fileCount": 3
27
- },
28
15
  {
29
16
  "id": "board-game",
30
17
  "name": "Board Game",
31
- "type": "game",
18
+ "type": "template",
32
19
  "engine": "three",
33
20
  "genre": "board",
21
+ "summary": "3D tycoon board game: roll dice, move around the board, buy and upgrade city tiles.",
22
+ "description": "A turn-based property board game rendered in Three.js with a React overlay. Players roll dice, move around a looping board, and buy, sell and upgrade tiles to build a city and earn income. The board layout, tiles and interactions are data-driven, so the theme and economy are easy to re-skin.",
23
+ "provides": [
24
+ "turn-based dice movement around a looping board",
25
+ "buy / sell / upgrade board tiles",
26
+ "player-vs-player property economy",
27
+ "3D board rendering with a React HUD overlay",
28
+ "data-driven board layout and tile configuration"
29
+ ],
30
+ "tags": [
31
+ "board",
32
+ "tycoon",
33
+ "3d",
34
+ "turn-based",
35
+ "economy",
36
+ "three"
37
+ ],
38
+ "media": {
39
+ "image": "https://cloud.idosgames.com/drive/modules/board-game/cover.png",
40
+ "video": "https://cloud.idosgames.com/drive/modules/board-game/demo.mp4"
41
+ },
42
+ "demoUrl": "https://cloud.idosgames.com/drive/modules/board-game/demo/",
43
+ "author": {
44
+ "name": "iDos Games",
45
+ "url": "https://idosgames.com"
46
+ },
47
+ "version": "0.1.0",
34
48
  "dependencies": {
35
- "@idosgames/core": "0.1.1",
36
- "@idosgames/module-sdk": "0.1.0",
49
+ "@idosgames/core": "0.1.3",
50
+ "@idosgames/module-sdk": "0.1.1",
37
51
  "@idosgames/react": "0.1.0",
38
- "@idosgames/wallet": "0.1.1",
52
+ "@idosgames/wallet": "0.1.2",
39
53
  "@solana/wallet-adapter-base": "0.9.27",
40
54
  "@solana/wallet-adapter-react": "0.15.39",
41
55
  "@tanstack/react-query": "5.101.2",
@@ -50,14 +64,41 @@
50
64
  {
51
65
  "id": "idle-rpg",
52
66
  "name": "Idle RPG",
53
- "type": "game",
67
+ "type": "template",
54
68
  "engine": "phaser",
55
69
  "genre": "idle-rpg",
70
+ "summary": "Idle RPG: heroes auto-battle and earn resources over time, with upgrades and offline progress.",
71
+ "description": "An incremental idle RPG built on Phaser. Heroes fight automatically, generate currency and loot over time, and keep progressing while the player is away. Includes an upgrade loop and offline-income accrual, so it fits any 'tap to grow / auto-battler / idle economy' request.",
72
+ "provides": [
73
+ "auto-battling heroes",
74
+ "idle resource generation over time",
75
+ "offline income accrual",
76
+ "upgrade / progression loop",
77
+ "2D sprite rendering (Phaser)"
78
+ ],
79
+ "tags": [
80
+ "idle",
81
+ "rpg",
82
+ "incremental",
83
+ "auto-battler",
84
+ "phaser",
85
+ "2d"
86
+ ],
87
+ "media": {
88
+ "image": "https://cloud.idosgames.com/drive/modules/idle-rpg/cover.png",
89
+ "video": "https://cloud.idosgames.com/drive/modules/idle-rpg/demo.mp4"
90
+ },
91
+ "demoUrl": "https://cloud.idosgames.com/drive/modules/idle-rpg/demo/",
92
+ "author": {
93
+ "name": "iDos Games",
94
+ "url": "https://idosgames.com"
95
+ },
96
+ "version": "0.1.0",
56
97
  "dependencies": {
57
- "@idosgames/core": "0.1.1",
58
- "@idosgames/module-sdk": "0.1.0",
98
+ "@idosgames/core": "0.1.3",
99
+ "@idosgames/module-sdk": "0.1.1",
59
100
  "@idosgames/react": "0.1.0",
60
- "@idosgames/wallet": "0.1.1",
101
+ "@idosgames/wallet": "0.1.2",
61
102
  "@solana/wallet-adapter-base": "0.9.27",
62
103
  "@solana/wallet-adapter-react": "0.15.39",
63
104
  "@tanstack/react-query": "5.101.2",
@@ -72,11 +113,38 @@
72
113
  {
73
114
  "id": "voxelcraft",
74
115
  "name": "VoxelCraft",
75
- "type": "game",
116
+ "type": "template",
76
117
  "engine": "three",
77
118
  "genre": "sandbox",
119
+ "summary": "Voxel sandbox (Minecraft-like): walk a first-person world, place and break blocks.",
120
+ "description": "A first-person voxel sandbox rendered in Three.js. Players explore a procedurally chunked world, place and break blocks, and move with WASD + mouse-look. Covers any 'build / mine / explore a block world' or open-world sandbox request; the largest of the feature modules.",
121
+ "provides": [
122
+ "first-person voxel world exploration",
123
+ "place and break blocks",
124
+ "procedural chunked terrain",
125
+ "WASD + mouse-look player controller",
126
+ "3D block rendering (Three.js)"
127
+ ],
128
+ "tags": [
129
+ "voxel",
130
+ "sandbox",
131
+ "minecraft",
132
+ "3d",
133
+ "first-person",
134
+ "three"
135
+ ],
136
+ "media": {
137
+ "image": "https://cloud.idosgames.com/drive/modules/voxelcraft/cover.png",
138
+ "video": "https://cloud.idosgames.com/drive/modules/voxelcraft/demo.mp4"
139
+ },
140
+ "demoUrl": "https://cloud.idosgames.com/drive/modules/voxelcraft/demo/",
141
+ "author": {
142
+ "name": "iDos Games",
143
+ "url": "https://idosgames.com"
144
+ },
145
+ "version": "0.1.0",
78
146
  "dependencies": {
79
- "@idosgames/module-sdk": "0.1.0",
147
+ "@idosgames/module-sdk": "0.1.1",
80
148
  "three": "0.185.1"
81
149
  },
82
150
  "fileCount": 37
@@ -133,11 +201,15 @@
133
201
  },
134
202
  {
135
203
  "name": "idosgames-getting-started",
136
- "description": "Start a new game or app on the iDosGames composable-module architecture: scaffold the host shell and plug in feature modules (board-game, idle-rpg, voxelcraft, currency-hud, …). Use this whenever a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and idosgames-compose-modules (merging several). If pulling modules over MCP, use the @idosgames/mcp tools get_host_scaffold / get_module / get_manifest."
204
+ "description": "Start a new game or app on the iDosGames composable-module architecture: scaffold the host shell and plug in feature modules (board-game, idle-rpg, voxelcraft, …). Use this whenever a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and idosgames-compose-modules (merging several). If pulling modules over MCP, use the @idosgames/mcp tools get_host_scaffold / get_module / get_manifest."
137
205
  },
138
206
  {
139
207
  "name": "idosgames-module-contract",
140
- "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, currency-hud), or asks about defineModule, registerScene/registerPanel/registerRoute, activate/suspend, SceneMountContext, or the {camelCase(id)}Module export convention."
208
+ "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), or asks about defineModule, registerScene/registerPanel/registerRoute, activate/suspend, SceneMountContext, or the {camelCase(id)}Module export convention."
209
+ },
210
+ {
211
+ "name": "idosgames-title-bootstrap",
212
+ "description": "Configure a FRESH (empty) iDosGames Title so a game can actually run against it: currencies with starting balances, then the game-loop board config, then verify with a real login. Use this when a newly created Title answers \"Board not found\", \"Board not enabled\", \"Bots config is not configured\", or \"SpecialMode ... must be configured\", when starting balances don't appear, or whenever you scaffold a project for a Title that was just created and has no config yet. All writes go through the Title-configuration MCP (see idosgames-getting-started for how to connect it)."
141
213
  },
142
214
  {
143
215
  "name": "item-system",
@@ -6,11 +6,41 @@
6
6
  "engine": "three",
7
7
  "genre": "board"
8
8
  },
9
+ "catalog": {
10
+ "type": "template",
11
+ "summary": "3D tycoon board game: roll dice, move around the board, buy and upgrade city tiles.",
12
+ "description": "A turn-based property board game rendered in Three.js with a React overlay. Players roll dice, move around a looping board, and buy, sell and upgrade tiles to build a city and earn income. The board layout, tiles and interactions are data-driven, so the theme and economy are easy to re-skin.",
13
+ "provides": [
14
+ "turn-based dice movement around a looping board",
15
+ "buy / sell / upgrade board tiles",
16
+ "player-vs-player property economy",
17
+ "3D board rendering with a React HUD overlay",
18
+ "data-driven board layout and tile configuration"
19
+ ],
20
+ "tags": [
21
+ "board",
22
+ "tycoon",
23
+ "3d",
24
+ "turn-based",
25
+ "economy",
26
+ "three"
27
+ ],
28
+ "media": {
29
+ "image": "https://cloud.idosgames.com/drive/modules/board-game/cover.png",
30
+ "video": "https://cloud.idosgames.com/drive/modules/board-game/demo.mp4"
31
+ },
32
+ "demoUrl": "https://cloud.idosgames.com/drive/modules/board-game/demo/",
33
+ "author": {
34
+ "name": "iDos Games",
35
+ "url": "https://idosgames.com"
36
+ },
37
+ "version": "0.1.0"
38
+ },
9
39
  "dependencies": {
10
- "@idosgames/core": "0.1.1",
11
- "@idosgames/module-sdk": "0.1.0",
40
+ "@idosgames/core": "0.1.3",
41
+ "@idosgames/module-sdk": "0.1.1",
12
42
  "@idosgames/react": "0.1.0",
13
- "@idosgames/wallet": "0.1.1",
43
+ "@idosgames/wallet": "0.1.2",
14
44
  "@solana/wallet-adapter-base": "0.9.27",
15
45
  "@solana/wallet-adapter-react": "0.15.39",
16
46
  "@tanstack/react-query": "5.101.2",
@@ -63,7 +93,7 @@
63
93
  },
64
94
  {
65
95
  "path": "components/WalletPanel.tsx",
66
- "content": "import {\n useEffect,\n useMemo,\n useReducer,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport { parseUnits } from \"viem\";\nimport {\n arbitrum,\n base,\n bsc,\n mainnet,\n optimism,\n polygon,\n polygonAmoy,\n sepolia,\n} from \"viem/chains\";\nimport { useAccount, useConnect, useDisconnect, useSwitchChain } from \"wagmi\";\nimport {\n createEvmWalletConfig,\n IDosGamesWalletProvider,\n useEvmBridge,\n} from \"@idosgames/wallet/react\";\nimport type { BridgeResult } from \"@idosgames/wallet\";\nimport type {\n BlockchainNetworkDefinition,\n CryptoCurrencyDefinition,\n} from \"@idosgames/core\";\nimport { useIDosGamesClient } from \"../react/context\";\nimport { TITLE_ID } from \"../env\";\nimport { ENV_WALLETCONNECT_PROJECT_ID } from \"../env\";\n\n// A curated EVM chain set for the demo — enough to cover the networks a title is likely to use.\n// wagmi needs at least one chain up front; the actual network you deposit to comes from the\n// title's blockchain config (the picker below), and we switch the wallet's chain to match.\nconst SUPPORTED_CHAINS = [\n mainnet,\n polygon,\n bsc,\n arbitrum,\n base,\n optimism,\n sepolia,\n polygonAmoy,\n] as const;\n\n// Built once. Set VITE_WALLETCONNECT_PROJECT_ID (get one at cloud.walletconnect.com) to enable\n// MOBILE wallets via the WalletConnect QR/deep-link modal; without it, browser extensions still work.\nconst wagmiConfig = createEvmWalletConfig({\n chains: SUPPORTED_CHAINS,\n walletConnectProjectId: ENV_WALLETCONNECT_PROJECT_ID || undefined,\n appName: \"iDosGames Board\",\n});\n\nconst card: CSSProperties = {\n pointerEvents: \"auto\",\n background: \"#1b1730f5\",\n border: \"1px solid #34294f\",\n borderRadius: 12,\n padding: 16,\n color: \"#fff\",\n width: 320,\n fontFamily: \"system-ui, sans-serif\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: 10,\n maxHeight: \"80vh\",\n overflow: \"auto\",\n};\nconst title: CSSProperties = { fontWeight: 700, fontSize: 16 };\nconst label: CSSProperties = { fontSize: 12, opacity: 0.7 };\nconst input: CSSProperties = {\n background: \"#241d40\",\n border: \"1px solid #34294f\",\n borderRadius: 8,\n color: \"#fff\",\n padding: \"8px 10px\",\n width: \"100%\",\n boxSizing: \"border-box\",\n};\nconst primary: CSSProperties = {\n background: \"#6c5ce7\",\n color: \"#fff\",\n border: \"none\",\n borderRadius: 8,\n padding: \"9px 14px\",\n fontWeight: 700,\n cursor: \"pointer\",\n};\nconst ghost: CSSProperties = {\n background: \"#34294f\",\n color: \"#fff\",\n border: \"none\",\n borderRadius: 8,\n padding: \"7px 12px\",\n fontWeight: 600,\n cursor: \"pointer\",\n};\nconst row: CSSProperties = { display: \"flex\", gap: 8 };\n\n/** Public entry: the wallet screen wrapped in its own wagmi/react-query provider. */\nexport function WalletPanel({ onClose }: { onClose?: () => void }): ReactNode {\n return (\n <IDosGamesWalletProvider wagmiConfig={wagmiConfig}>\n <WalletPanelInner onClose={onClose} />\n </IDosGamesWalletProvider>\n );\n}\n\nfunction WalletPanelInner({ onClose }: { onClose?: () => void }): ReactNode {\n const client = useIDosGamesClient();\n const bridge = useEvmBridge(client, TITLE_ID);\n const { address, isConnected, chainId } = useAccount();\n const { connect, connectors } = useConnect();\n const { disconnect } = useDisconnect();\n const { switchChainAsync } = useSwitchChain();\n const [, force] = useReducer((n: number) => n + 1, 0);\n\n const [networks, setNetworks] = useState<\n Record<string, BlockchainNetworkDefinition>\n >({});\n const [currencies, setCurrencies] = useState<\n Record<string, CryptoCurrencyDefinition>\n >({});\n const [networkID, setNetworkID] = useState<string>(\"\");\n const [currencyID, setCurrencyID] = useState<string>(\"\");\n const [amount, setAmount] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [result, setResult] = useState<BridgeResult<unknown> | null>(null);\n\n // Load blockchain config + on-chain state once; re-render on cache changes for the balance.\n useEffect(() => {\n let active = true;\n void (async () => {\n const defs = await client.blockchain.getDefinitions();\n await client.blockchain.getUserState();\n if (!active || !defs.ok) return;\n const nets = defs.data.Blockchain?.Networks ?? {};\n const evmNets: Record<string, BlockchainNetworkDefinition> = {};\n for (const [id, net] of Object.entries(nets))\n if (net.Type === \"EVM\") evmNets[id] = net;\n setNetworks(evmNets);\n setCurrencies(defs.data.CryptoCurrencies ?? {});\n const firstNet = Object.keys(evmNets)[0] ?? \"\";\n setNetworkID(firstNet);\n })();\n return () => {\n active = false;\n };\n }, [client]);\n\n useEffect(() => client.on(\"user:anyUpdated\", force), [client]);\n\n // Currencies that have an ERC-20 binding on the selected network (a contract we can deposit).\n const eligibleCurrencies = useMemo(() => {\n return Object.entries(currencies).filter(([, def]) =>\n def.Networks?.some(\n (b) => b.NetworkID === networkID && !!b.ContractAddress,\n ),\n );\n }, [currencies, networkID]);\n\n useEffect(() => {\n const first = eligibleCurrencies[0]?.[0] ?? \"\";\n setCurrencyID((prev) =>\n eligibleCurrencies.some(([id]) => id === prev) ? prev : first,\n );\n }, [eligibleCurrencies]);\n\n const network = networkID ? networks[networkID] : undefined;\n const currency = currencyID ? currencies[currencyID] : undefined;\n const binding = currency?.Networks?.find((b) => b.NetworkID === networkID);\n const balance = currencyID\n ? client.data.user.getCryptoCurrencyAmount(currencyID)\n : \"0\";\n\n async function ensureChain(): Promise<boolean> {\n if (!network?.ChainID || chainId === network.ChainID) return true;\n try {\n await switchChainAsync({ chainId: network.ChainID });\n return true;\n } catch {\n setResult({\n ok: false,\n stage: \"approve\",\n error: `Switch your wallet to chain ${network.ChainID} to continue.`,\n });\n return false;\n }\n }\n\n async function runDeposit(): Promise<void> {\n if (!network || !binding?.ContractAddress) return;\n setBusy(true);\n setResult(null);\n if (await ensureChain()) {\n const decimals = binding.Decimals ?? 18;\n let raw: bigint;\n try {\n raw = parseUnits(amount || \"0\", decimals);\n } catch {\n setResult({ ok: false, stage: \"approve\", error: \"Invalid amount.\" });\n setBusy(false);\n return;\n }\n const res = await bridge.depositToken({\n network,\n tokenAddress: binding.ContractAddress as `0x${string}`,\n amount: raw,\n });\n setResult(res);\n if (res.ok) await client.blockchain.getUserState();\n }\n setBusy(false);\n }\n\n async function runWithdraw(): Promise<void> {\n if (!network || !bridge.account) return;\n setBusy(true);\n setResult(null);\n if (await ensureChain()) {\n const res = await bridge.withdrawToken({\n currencyID,\n networkID,\n walletAddress: bridge.account,\n amount: amount || \"0\",\n });\n setResult(res);\n if (res.ok) await client.blockchain.getUserState();\n }\n setBusy(false);\n }\n\n return (\n <div style={card}>\n <div style={{ ...row, justifyContent: \"space-between\" }}>\n <span style={title}>Crypto wallet</span>\n {onClose && (\n <button type=\"button\" style={ghost} onClick={onClose}>\n ✕\n </button>\n )}\n </div>\n\n {/* Connect */}\n {isConnected ? (\n <div style={row}>\n <span style={{ ...label, flex: 1, alignSelf: \"center\" }}>\n {address?.slice(0, 6)}…{address?.slice(-4)}\n </span>\n <button type=\"button\" style={ghost} onClick={() => disconnect()}>\n Disconnect\n </button>\n </div>\n ) : (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 6 }}>\n <span style={label}>Connect a browser or mobile wallet</span>\n {connectors.map((c) => (\n <button\n key={c.uid}\n type=\"button\"\n style={ghost}\n onClick={() => connect({ connector: c })}\n >\n {c.name}\n </button>\n ))}\n </div>\n )}\n\n {Object.keys(networks).length === 0 ? (\n <span style={label}>No EVM networks configured for this title.</span>\n ) : (\n <>\n <div>\n <div style={label}>Network</div>\n <select\n style={input}\n value={networkID}\n onChange={(e) => setNetworkID(e.target.value)}\n >\n {Object.entries(networks).map(([id, net]) => (\n <option key={id} value={id}>\n {net.DisplayName ?? id}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <div style={label}>Token</div>\n <select\n style={input}\n value={currencyID}\n onChange={(e) => setCurrencyID(e.target.value)}\n >\n {eligibleCurrencies.length === 0 && (\n <option value=\"\">— no depositable tokens —</option>\n )}\n {eligibleCurrencies.map(([id, def]) => (\n <option key={id} value={id}>\n {def.DisplayName ?? id}\n </option>\n ))}\n </select>\n </div>\n\n <div style={label}>\n In-game balance: <b>{balance}</b> {currencyID}\n </div>\n\n <div>\n <div style={label}>Amount</div>\n <input\n style={input}\n inputMode=\"decimal\"\n placeholder=\"0.0\"\n value={amount}\n onChange={(e) => setAmount(e.target.value)}\n />\n </div>\n\n <div style={row}>\n <button\n type=\"button\"\n style={{ ...primary, flex: 1, opacity: busy ? 0.6 : 1 }}\n disabled={busy || !isConnected || !binding?.ContractAddress}\n onClick={() => void runDeposit()}\n >\n Deposit\n </button>\n <button\n type=\"button\"\n style={{ ...ghost, flex: 1, opacity: busy ? 0.6 : 1 }}\n disabled={busy || !isConnected || !currencyID}\n onClick={() => void runWithdraw()}\n >\n Withdraw\n </button>\n </div>\n </>\n )}\n\n {result && <ResultLine result={result} />}\n </div>\n );\n}\n\nfunction ResultLine({ result }: { result: BridgeResult<unknown> }): ReactNode {\n const style: CSSProperties = {\n fontSize: 12,\n borderRadius: 8,\n padding: \"8px 10px\",\n background: result.ok ? \"#1e3a2a\" : \"#3a1e28\",\n color: result.ok ? \"#8ef0b0\" : \"#f2a0b4\",\n wordBreak: \"break-all\",\n };\n if (result.ok)\n return <div style={style}>✓ Done · tx {result.onChainTxHash}</div>;\n return (\n <div style={style}>\n ✕ [{result.stage}] {result.error}\n {result.titleTransactionID\n ? ` · already debited (tx ${result.titleTransactionID}) — retry/confirm, don't re-request`\n : \"\"}\n </div>\n );\n}\n"
96
+ "content": "import {\n useEffect,\n useMemo,\n useReducer,\n useState,\n type CSSProperties,\n type ReactNode,\n} from \"react\";\nimport { parseUnits } from \"viem\";\nimport {\n arbitrum,\n base,\n bsc,\n mainnet,\n optimism,\n polygon,\n polygonAmoy,\n sepolia,\n} from \"viem/chains\";\nimport { useAccount, useConnect, useDisconnect, useSwitchChain } from \"wagmi\";\nimport {\n createEvmWalletConfig,\n IDosGamesWalletProvider,\n useEvmBridge,\n} from \"@idosgames/wallet/react\";\nimport type { BridgeResult } from \"@idosgames/wallet\";\nimport type {\n BlockchainNetworkDefinition,\n CryptoCurrencyDefinition,\n} from \"@idosgames/core\";\nimport { useIDosGamesClient } from \"../react/context\";\nimport { ENV_WALLETCONNECT_PROJECT_ID } from \"../env\";\n\n// A curated EVM chain set for the demo — enough to cover the networks a title is likely to use.\n// wagmi needs at least one chain up front; the actual network you deposit to comes from the\n// title's blockchain config (the picker below), and we switch the wallet's chain to match.\nconst SUPPORTED_CHAINS = [\n mainnet,\n polygon,\n bsc,\n arbitrum,\n base,\n optimism,\n sepolia,\n polygonAmoy,\n] as const;\n\n// Built once. Set VITE_WALLETCONNECT_PROJECT_ID (get one at cloud.walletconnect.com) to enable\n// MOBILE wallets via the WalletConnect QR/deep-link modal; without it, browser extensions still work.\nconst wagmiConfig = createEvmWalletConfig({\n chains: SUPPORTED_CHAINS,\n walletConnectProjectId: ENV_WALLETCONNECT_PROJECT_ID || undefined,\n appName: \"iDosGames Board\",\n});\n\nconst card: CSSProperties = {\n pointerEvents: \"auto\",\n background: \"#1b1730f5\",\n border: \"1px solid #34294f\",\n borderRadius: 12,\n padding: 16,\n color: \"#fff\",\n width: 320,\n fontFamily: \"system-ui, sans-serif\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: 10,\n maxHeight: \"80vh\",\n overflow: \"auto\",\n};\nconst title: CSSProperties = { fontWeight: 700, fontSize: 16 };\nconst label: CSSProperties = { fontSize: 12, opacity: 0.7 };\nconst input: CSSProperties = {\n background: \"#241d40\",\n border: \"1px solid #34294f\",\n borderRadius: 8,\n color: \"#fff\",\n padding: \"8px 10px\",\n width: \"100%\",\n boxSizing: \"border-box\",\n};\nconst primary: CSSProperties = {\n background: \"#6c5ce7\",\n color: \"#fff\",\n border: \"none\",\n borderRadius: 8,\n padding: \"9px 14px\",\n fontWeight: 700,\n cursor: \"pointer\",\n};\nconst ghost: CSSProperties = {\n background: \"#34294f\",\n color: \"#fff\",\n border: \"none\",\n borderRadius: 8,\n padding: \"7px 12px\",\n fontWeight: 600,\n cursor: \"pointer\",\n};\nconst row: CSSProperties = { display: \"flex\", gap: 8 };\n\n/** Public entry: the wallet screen wrapped in its own wagmi/react-query provider. */\nexport function WalletPanel({ onClose }: { onClose?: () => void }): ReactNode {\n return (\n <IDosGamesWalletProvider wagmiConfig={wagmiConfig}>\n <WalletPanelInner onClose={onClose} />\n </IDosGamesWalletProvider>\n );\n}\n\nfunction WalletPanelInner({ onClose }: { onClose?: () => void }): ReactNode {\n const client = useIDosGamesClient();\n // The title comes from the host's client, never re-derived here: a module that resolved its own\n // title could disagree with the host and bridge deposits into a different title's wallet.\n const bridge = useEvmBridge(client, client.titleID);\n const { address, isConnected, chainId } = useAccount();\n const { connect, connectors } = useConnect();\n const { disconnect } = useDisconnect();\n const { switchChainAsync } = useSwitchChain();\n const [, force] = useReducer((n: number) => n + 1, 0);\n\n const [networks, setNetworks] = useState<\n Record<string, BlockchainNetworkDefinition>\n >({});\n const [currencies, setCurrencies] = useState<\n Record<string, CryptoCurrencyDefinition>\n >({});\n const [networkID, setNetworkID] = useState<string>(\"\");\n const [currencyID, setCurrencyID] = useState<string>(\"\");\n const [amount, setAmount] = useState(\"\");\n const [busy, setBusy] = useState(false);\n const [result, setResult] = useState<BridgeResult<unknown> | null>(null);\n\n // Load blockchain config + on-chain state once; re-render on cache changes for the balance.\n useEffect(() => {\n let active = true;\n void (async () => {\n const defs = await client.blockchain.getDefinitions();\n await client.blockchain.getUserState();\n if (!active || !defs.ok) return;\n const nets = defs.data.Blockchain?.Networks ?? {};\n const evmNets: Record<string, BlockchainNetworkDefinition> = {};\n for (const [id, net] of Object.entries(nets))\n if (net.Type === \"EVM\") evmNets[id] = net;\n setNetworks(evmNets);\n setCurrencies(defs.data.CryptoCurrencies ?? {});\n const firstNet = Object.keys(evmNets)[0] ?? \"\";\n setNetworkID(firstNet);\n })();\n return () => {\n active = false;\n };\n }, [client]);\n\n useEffect(() => client.on(\"user:anyUpdated\", force), [client]);\n\n // Currencies that have an ERC-20 binding on the selected network (a contract we can deposit).\n const eligibleCurrencies = useMemo(() => {\n return Object.entries(currencies).filter(([, def]) =>\n def.Networks?.some(\n (b) => b.NetworkID === networkID && !!b.ContractAddress,\n ),\n );\n }, [currencies, networkID]);\n\n useEffect(() => {\n const first = eligibleCurrencies[0]?.[0] ?? \"\";\n setCurrencyID((prev) =>\n eligibleCurrencies.some(([id]) => id === prev) ? prev : first,\n );\n }, [eligibleCurrencies]);\n\n const network = networkID ? networks[networkID] : undefined;\n const currency = currencyID ? currencies[currencyID] : undefined;\n const binding = currency?.Networks?.find((b) => b.NetworkID === networkID);\n const balance = currencyID\n ? client.data.user.getCryptoCurrencyAmount(currencyID)\n : \"0\";\n\n async function ensureChain(): Promise<boolean> {\n if (!network?.ChainID || chainId === network.ChainID) return true;\n try {\n await switchChainAsync({ chainId: network.ChainID });\n return true;\n } catch {\n setResult({\n ok: false,\n stage: \"approve\",\n error: `Switch your wallet to chain ${network.ChainID} to continue.`,\n });\n return false;\n }\n }\n\n async function runDeposit(): Promise<void> {\n if (!network || !binding?.ContractAddress) return;\n setBusy(true);\n setResult(null);\n if (await ensureChain()) {\n const decimals = binding.Decimals ?? 18;\n let raw: bigint;\n try {\n raw = parseUnits(amount || \"0\", decimals);\n } catch {\n setResult({ ok: false, stage: \"approve\", error: \"Invalid amount.\" });\n setBusy(false);\n return;\n }\n const res = await bridge.depositToken({\n network,\n tokenAddress: binding.ContractAddress as `0x${string}`,\n amount: raw,\n });\n setResult(res);\n if (res.ok) await client.blockchain.getUserState();\n }\n setBusy(false);\n }\n\n async function runWithdraw(): Promise<void> {\n if (!network || !bridge.account) return;\n setBusy(true);\n setResult(null);\n if (await ensureChain()) {\n const res = await bridge.withdrawToken({\n currencyID,\n networkID,\n walletAddress: bridge.account,\n amount: amount || \"0\",\n });\n setResult(res);\n if (res.ok) await client.blockchain.getUserState();\n }\n setBusy(false);\n }\n\n return (\n <div style={card}>\n <div style={{ ...row, justifyContent: \"space-between\" }}>\n <span style={title}>Crypto wallet</span>\n {onClose && (\n <button type=\"button\" style={ghost} onClick={onClose}>\n ✕\n </button>\n )}\n </div>\n\n {/* Connect */}\n {isConnected ? (\n <div style={row}>\n <span style={{ ...label, flex: 1, alignSelf: \"center\" }}>\n {address?.slice(0, 6)}…{address?.slice(-4)}\n </span>\n <button type=\"button\" style={ghost} onClick={() => disconnect()}>\n Disconnect\n </button>\n </div>\n ) : (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 6 }}>\n <span style={label}>Connect a browser or mobile wallet</span>\n {connectors.map((c) => (\n <button\n key={c.uid}\n type=\"button\"\n style={ghost}\n onClick={() => connect({ connector: c })}\n >\n {c.name}\n </button>\n ))}\n </div>\n )}\n\n {Object.keys(networks).length === 0 ? (\n <span style={label}>No EVM networks configured for this title.</span>\n ) : (\n <>\n <div>\n <div style={label}>Network</div>\n <select\n style={input}\n value={networkID}\n onChange={(e) => setNetworkID(e.target.value)}\n >\n {Object.entries(networks).map(([id, net]) => (\n <option key={id} value={id}>\n {net.DisplayName ?? id}\n </option>\n ))}\n </select>\n </div>\n\n <div>\n <div style={label}>Token</div>\n <select\n style={input}\n value={currencyID}\n onChange={(e) => setCurrencyID(e.target.value)}\n >\n {eligibleCurrencies.length === 0 && (\n <option value=\"\">— no depositable tokens —</option>\n )}\n {eligibleCurrencies.map(([id, def]) => (\n <option key={id} value={id}>\n {def.DisplayName ?? id}\n </option>\n ))}\n </select>\n </div>\n\n <div style={label}>\n In-game balance: <b>{balance}</b> {currencyID}\n </div>\n\n <div>\n <div style={label}>Amount</div>\n <input\n style={input}\n inputMode=\"decimal\"\n placeholder=\"0.0\"\n value={amount}\n onChange={(e) => setAmount(e.target.value)}\n />\n </div>\n\n <div style={row}>\n <button\n type=\"button\"\n style={{ ...primary, flex: 1, opacity: busy ? 0.6 : 1 }}\n disabled={busy || !isConnected || !binding?.ContractAddress}\n onClick={() => void runDeposit()}\n >\n Deposit\n </button>\n <button\n type=\"button\"\n style={{ ...ghost, flex: 1, opacity: busy ? 0.6 : 1 }}\n disabled={busy || !isConnected || !currencyID}\n onClick={() => void runWithdraw()}\n >\n Withdraw\n </button>\n </div>\n </>\n )}\n\n {result && <ResultLine result={result} />}\n </div>\n );\n}\n\nfunction ResultLine({ result }: { result: BridgeResult<unknown> }): ReactNode {\n const style: CSSProperties = {\n fontSize: 12,\n borderRadius: 8,\n padding: \"8px 10px\",\n background: result.ok ? \"#1e3a2a\" : \"#3a1e28\",\n color: result.ok ? \"#8ef0b0\" : \"#f2a0b4\",\n wordBreak: \"break-all\",\n };\n if (result.ok)\n return <div style={style}>✓ Done · tx {result.onChainTxHash}</div>;\n return (\n <div style={style}>\n ✕ [{result.stage}] {result.error}\n {result.titleTransactionID\n ? ` · already debited (tx ${result.titleTransactionID}) — retry/confirm, don't re-request`\n : \"\"}\n </div>\n );\n}\n"
67
97
  },
68
98
  {
69
99
  "path": "controller-box.ts",
@@ -79,7 +109,7 @@
79
109
  },
80
110
  {
81
111
  "path": "env.ts",
82
- "content": "// Runtime config the module reads from the page it runs in the same source the host used: URL\n// params and the __IDOS_ENV__ global the host's bundler defines. Present because the wallet bridge\n// needs titleID explicitly and the SDK client doesn't expose it publicly. Missing safe defaults.\n\ntype IdosEnv = {\n TITLE_ID?: string;\n WALLETCONNECT_PROJECT_ID?: string;\n};\n\ndeclare const __IDOS_ENV__: IdosEnv | undefined;\n\nconst env: IdosEnv =\n typeof __IDOS_ENV__ !== \"undefined\" && __IDOS_ENV__ ? __IDOS_ENV__ : {};\n\nconst launchParams =\n typeof window === \"undefined\"\n ? null\n : new URLSearchParams(window.location.search);\n\nexport const TITLE_ID =\n launchParams?.get(\"titleID\") ?? (env.TITLE_ID || \"URLV9SUP\");\nexport const ENV_WALLETCONNECT_PROJECT_ID = env.WALLETCONNECT_PROJECT_ID ?? \"\";\n"
112
+ "content": "// Runtime config the module reads from the page it runs in: the __IDOS_ENV__ global the host's\n// bundler defines (see the host template's vite.config). A module copied into a project runs in\n// that same page, so it is available; when absent (e.g. the classic preview bundler), the default\n// applies.\n//\n// The TITLE is deliberately NOT here. It comes from the host — `client.titleID`, or `ctx.titleId`\n// in a module's setup(). A module that re-derived the title from the URL could disagree with its\n// host, which is how a build ends up reading one title's data and writing another's.\n\ntype IdosEnv = {\n WALLETCONNECT_PROJECT_ID?: string;\n};\n\ndeclare const __IDOS_ENV__: IdosEnv | undefined;\n\nconst env: IdosEnv =\n typeof __IDOS_ENV__ !== \"undefined\" && __IDOS_ENV__ ? __IDOS_ENV__ : {};\n\nexport const ENV_WALLETCONNECT_PROJECT_ID = env.WALLETCONNECT_PROJECT_ID ?? \"\";\n"
83
113
  },
84
114
  {
85
115
  "path": "game/BoardController.ts",