@idosgames/mcp 0.1.45 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1597 -1542
- package/package.json +1 -1
- package/registry/host.json +3 -3
- package/registry/index.json +193 -136
- package/registry/modules/ai-media-app.json +77 -0
- package/registry/modules/ai.json +3 -3
- package/registry/modules/board-game.json +4 -4
- package/registry/modules/boosts.json +3 -3
- package/registry/modules/character.json +3 -3
- package/registry/modules/chat.json +3 -3
- package/registry/modules/collections.json +3 -3
- package/registry/modules/coop-events.json +3 -3
- package/registry/modules/craft.json +3 -3
- package/registry/modules/deals.json +3 -3
- package/registry/modules/events.json +6 -6
- package/registry/modules/friends.json +3 -3
- package/registry/modules/hero-run.json +14 -233
- package/registry/modules/idle-rpg.json +4 -4
- package/registry/modules/inventory.json +3 -3
- package/registry/modules/invite.json +3 -3
- package/registry/modules/kit-game.json +177 -0
- package/registry/modules/leaderboard.json +3 -3
- package/registry/modules/lootboxes.json +3 -3
- package/registry/modules/mailbox.json +3 -3
- package/registry/modules/marketplace.json +3 -3
- package/registry/modules/multiplayer.json +3 -3
- package/registry/modules/premium.json +3 -3
- package/registry/modules/profile.json +3 -3
- package/registry/modules/pvp.json +3 -3
- package/registry/modules/quests.json +3 -3
- package/registry/modules/rewards.json +3 -3
- package/registry/modules/season.json +6 -6
- package/registry/modules/store.json +3 -3
- package/registry/modules/tutorials.json +6 -6
- package/registry/modules/video-studio.json +4 -4
- package/registry/modules/voxelcraft.json +4 -4
- package/registry/modules/workshop.json +3 -3
- package/registry/skills/ai-generation-system.json +1 -1
- package/registry/skills/currency-system.json +1 -1
- package/registry/skills/idosgames-compose-modules.json +1 -1
- package/registry/skills/idosgames-getting-started.json +2 -2
- package/registry/skills/idosgames-module-contract.json +2 -2
- package/registry/video.json +1 -1
- package/registry/modules/mini-game.json +0 -69
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idosgames-getting-started",
|
|
3
|
-
"description": "Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and the games (board-game, idle-rpg, voxelcraft,
|
|
4
|
-
"content": "---\nname: idosgames-getting-started\ndescription: >-\n Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in\n + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and\n the games (board-game, idle-rpg, voxelcraft, mini-game, …). Use this whenever\n a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a\n project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or\n src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and\n idosgames-compose-modules (merging several) and idosgames-project-structure (where code goes as\n the project grows). If pulling modules over MCP, use the @idosgames/mcp\n tools get_host_scaffold / get_module / get_manifest.\n---\n\n# Getting started (iDosGames composable modules)\n\nAn iDosGames project is **one host shell + N feature modules**. The host owns everything that exists\nonce — the SDK client, login, the React root, the screen, a module registry, and a Mode Router that\nswitches between modules. A module is a library (a game or app) that plugs into the host; it never\ncreates the client, logs in, or owns the root.\n\n## Runtime packages (npm)\n\n- `@idosgames/core` — the SDK client (`createIDosGamesClient`): auth, currency, store, characters,\n blockchain, ~28 services. See the per-service skills (currency-system, store-system, …). Its\n **central cache** `client.cache` decides every read: the login brings the title config (by version)\n and the player's state (by each container's `Rev`) in ONE call; afterwards read state through\n `client.cache.ensureState([...containers])`, definitions through `getDefinitions()` (served from the\n config cache), server views through their service (cached by policy) — never re-fetch on start-up.\n The server views the game reads ride on that same login call (`ClientState.Views`): the client\n option `loginViews` — `\"auto\"` (default, what the game read last time), `\"all\"`, `\"none\"` or a list.\n- `@idosgames/module-sdk` — the module contract types (`Module`, `ModuleContext`, `EngineScene`,\n `UiPanel`). See idosgames-module-contract.\n- `@idosgames/react` — shared React glue (`IDosGamesProvider`, `useIDosGamesClient`, `useUserState`,\n `StatusProvider`, `createControllerContext`).\n- `@idosgames/app-shell` — the host runtime (`mountHost`, the Mode Router, the module registry).\n- `@idosgames/wallet` — optional wallet bridge (EVM/Solana) for on-chain deposits/withdrawals.\n\nInstall the exact versions from `get_manifest` (MCP) or the registry `index.json` `runtimePackages`.\n\n## Project shape\n\n```\nIDOS.md # the project guide every agent reads first (short, evergreen)\nAGENTS.md, CLAUDE.md # pointers to IDOS.md for external tools\ndocs/feature-history/ # one file per game system + README.md index (the project's memory)\nindex.html # mounts #app\nsrc/main.tsx # creates ONE client and calls mountHost({container, client, modules})\nsrc/modules.ts # the registry: export const modules: Module[] = [ … ] — imports + array only\nsrc/modules/{id}/ # each module's source + its module.meta.json (from get_module)\n```\n\nPlatform-created projects also carry `idos.modules.lock.json` (which modules came from the catalog,\nwith file fingerprints) — it is platform-owned; don't edit it. Where new code goes as the game grows,\nand how the project documents itself, is **idosgames-project-structure**.\n\n`src/main.tsx` (host-owned) is the only place that creates the client — but it does **not** sign in.\n`mountHost` owns sign-in: it replays a previous session (`autoLogin`) and renders the login screen\nwhen there is nothing to replay. Calling a `login*` method here skips that screen for good, taking\n\"switch account\" and wallet sign-in with it:\n\n```ts\nconst client = createIDosGamesClient({ titleID, buildKey, throttleMs: 0 });\n// Do NOT log in here — the host does it.\nmountHost({\n container: app,\n client,\n modules, // from ./modules\n renderLogin, // optional: your own screen; omitted = a plain guest-only default\n});\n```\n\nWhether a returning player is signed back in silently is the login screen's business, not the\nhost's: it calls `client.auth.setRememberSession(remember)` before a `login*` method. See the\nauthentication skill.\n\n`src/modules.ts` registers what the project composes:\n\n```ts\nimport type { Module } from \"@idosgames/module-sdk\";\nimport { boardGameModule } from \"./modules/board-game\";\nexport const modules: Module[] = [boardGameModule];\n```\n\n## Steps to scaffold\n\n1. **Base** — write the host scaffold to the project root (`get_host_scaffold`, or copy\n `templates/base`): sign-in and the lobby come from `@idosgames/app-shell` (mounted in `src/main.tsx`), `src/app.config.ts`\n (start in the lobby or a game, guest sign-in), an empty `src/modules.ts` → an empty lobby.\n2. **Bind the Title** — open `src/idos.title.ts` and set `IDOS_TITLE_ID` to the game's canonical\n Title id (and `IDOS_BUILD_KEY` if the title enforces one). This file is the project's single\n centralized identity — it is the highest-priority source and the only channel a packaged mobile\n build, iframe embed, or shared link has. On platform-created projects the platform generates it;\n on a manually scaffolded project **you fill it yourself**. Do NOT bind the title via `.env.local`\n (`VITE_IDOS_TITLE_ID`) — that is a local-dev fallback for the raw template only, and it does not\n travel with the code.\n3. **Pick modules** — `list_modules` / `search`, then `get_module {id}` for each. Write its files\n into `src/modules/{id}/`. The base's systems are the modules with `inBase: true` (profile,\n inventory, invite, friends, quests, mailbox); add the game(s) with the modules each game `uses`, and any\n other system (shop, VIP, leaderboards…) only when the app needs it.\n4. **Register** — for each module add `import { {camelCase(id)}Module } from \"./modules/{id}\"` and\n push it into the `modules` array (e.g. `board-game` → `boardGameModule`). Then add the module to\n the \"Installed modules\" block of `IDOS.md` (one line: ``- `board-game` (0.1.0) — src/modules/board-game/``)\n — on platform projects the platform maintains that block itself.\n5. **Install deps** — the union of `runtimePackages` + each module's `dependencies`. Modules bring\n their own engine (three, phaser); the host brings react/react-dom/app-shell.\n6. **Run** — sign-in → the lobby: the systems' screens are its tabs (a tab hides itself while the\n title has nothing for it) and \"Play\" launches a game on the whole screen with a small button back\n — or, for a game whose route says `inLobby: true` (idle-rpg), straight into that game inside the\n lobby, the lobby's balances and tabs around it. The lobby draws the balances and the wallet; games hide their own copies by\n themselves (`sharedUi` roles).\n\nTo combine multiple genres into one game, see **idosgames-compose-modules**. To write or edit a\nmodule, see **idosgames-module-contract**.\n\n## Where state lives (decide this before writing the first save)\n\nThe project is client-side code in the player's browser. `localStorage`, module fields, and React\nstate are **not storage** — nothing there survives a device change, and nothing there is trusted.\n\n1. **A dedicated module owns it?** Use that module. Currencies, inventory, quests, characters,\n leaderboards, store purchases each have a service that enforces the rules server-side.\n2. **Otherwise, per-player data → `client.userCustomData`** — buckets `Private`/`Public` are\n client-writable (settings, cosmetics), `ReadOnly`/`Internal` are server-only. Anything a player\n could cheat by editing goes in the server-only buckets. See **user-custom-data**.\n3. **Shared by all players → `client.titleCustomData`** (event state, global counters, server\n thresholds, feature toggles). Read-only for clients. See **title-custom-data**.\n4. **Writing any of the server-only data, or any rule the player must not be able to fake** →\n a CloudCode handler, called with `client.cloudCode.execute(...)`. See **cloud-code**.\n\n## Two MCP surfaces: game CODE vs. a Title's live DATA\n\nThe platform exposes **two independent MCP servers** — don't confuse them:\n\n- **`@idosgames/mcp`** (this one) serves the **CODE registry**. Use it to WRITE a game's source:\n `get_host_scaffold`, `list_modules` / `search` / `get_module`, `get_manifest`, `list_skills` /\n `get_skill`. Transport: stdio (`npx -y @idosgames/mcp`). Its registry is bundled offline; to use the\n hosted copy set `IDOSGAMES_REGISTRY_URL=https://cloud.idosgames.com/drive/registry/latest` — a **base**\n the loader appends `/index.json`, `/modules/{id}.json`, etc. to (the base itself is not fetchable on\n R2; open `.../latest/index.json` to browse the catalog). Read-only, no auth. It never reads or changes\n a live Title's data.\n- **The Title-configuration MCP** (a separate backend server) owns a **live Title's DATA**. Use it to\n read/write the Title's `TitlePublicConfiguration` (`get_<field>` / `save_<field>`, or the whole\n model) and to generate assets (`generate_image` / `generate_audio` / `generate_text` /\n `generate_three_d` / `generate_video`). Transport: HTTP JSON-RPC at\n `POST https://site.idosgames.com/api/v2/mcp`; every tool call takes a `title_id` argument.\n Authorization is **OAuth 2.1** — there is no API key and nothing to paste. Connect it as a plain\n HTTP MCP server with **no headers**: your client gets a `401`, discovers the authorization\n server, registers itself, and opens a browser where the publisher picks which Titles and which\n permissions to grant. The token lives in your client's own credential store, so committed config\n holds only the URL:\n\n ```json\n {\n \"mcpServers\": {\n \"idosgames-title\": {\n \"type\": \"http\",\n \"url\": \"https://site.idosgames.com/api/v2/mcp\"\n }\n }\n }\n ```\n\n Permissions the publisher can grant: `config:read`, `config:write`, `cloudcode:write`,\n `ai:generate`, `kanban:read`, `kanban:write`, `build:write`. A grant is scoped to the Titles ticked on the consent screen, and the publisher\n can revoke it any time from **Connected apps** in the dashboard. If a call comes back\n `SCOPE_NOT_ALLOWED` or `TITLE_NOT_ALLOWED`, the token is fine — that permission or that Title\n simply was not granted; ask the publisher to re-authorize rather than retrying.\n\n **Shipping the game itself** (permission `build:write`) also goes through this server — the\n platform hosts it on the Title's own address (`{titleid}.idos.games`) and its idosgames.com page:\n\n 1. `begin_build_upload { title_id }` → `build_id`, a one-hour `upload_url`, `asset_base`.\n 2. Build the game as static files with that base: `vite build --base=<asset_base>` (a relative\n `./` base also works — Unity WebGL builds already are). Use the LIVE title id, never `-DEV`.\n 3. Zip the CONTENTS of the output folder (`index.html` at the zip root; a single `dist/` top folder\n is accepted) and upload it: `curl -T build.zip \"<upload_url>\"` — a plain PUT, no headers.\n 4. `finish_build_upload { title_id, build_id, deploy }` — unpacks it into a new version (it shows up\n in the dashboard's version history) and returns `TestUrl` (DEV address). `deploy: true` puts it\n live for players right away; without it, check `TestUrl` and call it again with `deploy: true`.\n Still unpacking when it returns → poll `get_build`. `list_builds` shows recent versions.\n\n A game that is live but whose Title is not published yet has no idosgames.com page — the\n response says so; the publisher publishes it from the dashboard.\n\n ⚠ **The build must be the game itself.** On the Title's idosgames.com page the game may embed\n (`<iframe>`) only platform hosts — never another website, with no setting to allow one. A\n \"wrapper\" build that signs in and then frames the publisher's own site shows **\"Content\n unavailable\"** there instead of the game: build and upload the real game. Network calls\n (`fetch`/WebSocket) from the embedded game reach platform hosts plus the Title's\n `Blockchain.IframeConnectHosts` (`save_blockchain`; wallet-connection hosts are always refused).\n\n Configuring a **fresh (empty) Title** so a game can actually run against it (currencies → game\n loop → bots) is its own checklist: see **idosgames-title-bootstrap**.\n\nRule of thumb: **game CODE → `@idosgames/mcp`; a Title's live config DATA and generated ASSETS → the\nbackend `v2/mcp` server.** Scaffolding a project and configuring/populating the Title it runs as are\ntwo different jobs on two different servers — connect the one that matches the task (or both).\n",
|
|
3
|
+
"description": "Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and the games (board-game, idle-rpg, voxelcraft, kit-game, …). 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) and idosgames-project-structure (where code goes as the project grows). If pulling modules over MCP, use the @idosgames/mcp tools get_host_scaffold / get_module / get_manifest.",
|
|
4
|
+
"content": "---\nname: idosgames-getting-started\ndescription: >-\n Start a new game or app on the iDosGames composable-module architecture: scaffold the base (sign-in\n + the lobby on the host shell) and plug in modules — the systems (shop, heroes, leaderboards…) and\n the games (board-game, idle-rpg, voxelcraft, kit-game, …). Use this whenever\n a developer wants to CREATE an iDosGames project from scratch, add an iDosGames game module to a\n project, or asks how @idosgames/app-shell, @idosgames/module-sdk, the host shell, mountHost, or\n src/modules.ts fit together. Pairs with idosgames-module-contract (writing a module) and\n idosgames-compose-modules (merging several) and idosgames-project-structure (where code goes as\n the project grows). If pulling modules over MCP, use the @idosgames/mcp\n tools get_host_scaffold / get_module / get_manifest.\n---\n\n# Getting started (iDosGames composable modules)\n\nAn iDosGames project is **one host shell + N feature modules**. The host owns everything that exists\nonce — the SDK client, login, the React root, the screen, a module registry, and a Mode Router that\nswitches between modules. A module is a library (a game or app) that plugs into the host; it never\ncreates the client, logs in, or owns the root.\n\n## Runtime packages (npm)\n\n- `@idosgames/core` — the SDK client (`createIDosGamesClient`): auth, currency, store, characters,\n blockchain, ~28 services. See the per-service skills (currency-system, store-system, …). Its\n **central cache** `client.cache` decides every read: the login brings the title config (by version)\n and the player's state (by each container's `Rev`) in ONE call; afterwards read state through\n `client.cache.ensureState([...containers])`, definitions through `getDefinitions()` (served from the\n config cache), server views through their service (cached by policy) — never re-fetch on start-up.\n The server views the game reads ride on that same login call (`ClientState.Views`): the client\n option `loginViews` — `\"auto\"` (default, what the game read last time), `\"all\"`, `\"none\"` or a list.\n- `@idosgames/module-sdk` — the module contract types (`Module`, `ModuleContext`, `EngineScene`,\n `UiPanel`). See idosgames-module-contract.\n- `@idosgames/react` — shared React glue (`IDosGamesProvider`, `useIDosGamesClient`, `useUserState`,\n `StatusProvider`, `createControllerContext`).\n- `@idosgames/app-shell` — the host runtime (`mountHost`, the Mode Router, the module registry).\n- `@idosgames/wallet` — optional wallet bridge (EVM/Solana) for on-chain deposits/withdrawals.\n\nInstall the exact versions from `get_manifest` (MCP) or the registry `index.json` `runtimePackages`.\n\n## Project shape\n\n```\nIDOS.md # the project guide every agent reads first (short, evergreen)\nAGENTS.md, CLAUDE.md # pointers to IDOS.md for external tools\ndocs/feature-history/ # one file per game system + README.md index (the project's memory)\nindex.html # mounts #app\nsrc/main.tsx # creates ONE client and calls mountHost({container, client, modules})\nsrc/modules.ts # the registry: export const modules: Module[] = [ … ] — imports + array only\nsrc/modules/{id}/ # each module's source + its module.meta.json (from get_module)\n```\n\nPlatform-created projects also carry `idos.modules.lock.json` (which modules came from the catalog,\nwith file fingerprints) — it is platform-owned; don't edit it. Where new code goes as the game grows,\nand how the project documents itself, is **idosgames-project-structure**.\n\n`src/main.tsx` (host-owned) is the only place that creates the client — but it does **not** sign in.\n`mountHost` owns sign-in: it replays a previous session (`autoLogin`) and renders the login screen\nwhen there is nothing to replay. Calling a `login*` method here skips that screen for good, taking\n\"switch account\" and wallet sign-in with it:\n\n```ts\nconst client = createIDosGamesClient({ titleID, buildKey, throttleMs: 0 });\n// Do NOT log in here — the host does it.\nmountHost({\n container: app,\n client,\n modules, // from ./modules\n renderLogin, // optional: your own screen; omitted = a plain guest-only default\n});\n```\n\nWhether a returning player is signed back in silently is the login screen's business, not the\nhost's: it calls `client.auth.setRememberSession(remember)` before a `login*` method. See the\nauthentication skill.\n\n`src/modules.ts` registers what the project composes:\n\n```ts\nimport type { Module } from \"@idosgames/module-sdk\";\nimport { boardGameModule } from \"./modules/board-game\";\nexport const modules: Module[] = [boardGameModule];\n```\n\n## Steps to scaffold\n\n1. **Base** — write the host scaffold to the project root (`get_host_scaffold`, or copy\n `templates/base`): sign-in and the lobby come from `@idosgames/app-shell` (mounted in `src/main.tsx`), `src/app.config.ts`\n (start in the lobby or a game, guest sign-in), an empty `src/modules.ts` → an empty lobby.\n2. **Bind the Title** — open `src/idos.title.ts` and set `IDOS_TITLE_ID` to the game's canonical\n Title id (and `IDOS_BUILD_KEY` if the title enforces one). This file is the project's single\n centralized identity — it is the highest-priority source and the only channel a packaged mobile\n build, iframe embed, or shared link has. On platform-created projects the platform generates it;\n on a manually scaffolded project **you fill it yourself**. Do NOT bind the title via `.env.local`\n (`VITE_IDOS_TITLE_ID`) — that is a local-dev fallback for the raw template only, and it does not\n travel with the code.\n3. **Pick modules** — `list_modules` / `search`, then `get_module {id}` for each. Write its files\n into `src/modules/{id}/`. The base's systems are the modules with `inBase: true` (profile,\n inventory, invite, friends, quests, mailbox); add the game(s) with the modules each game `uses`, and any\n other system (shop, VIP, leaderboards…) only when the app needs it.\n4. **Register** — for each module add `import { {camelCase(id)}Module } from \"./modules/{id}\"` and\n push it into the `modules` array (e.g. `board-game` → `boardGameModule`). Then add the module to\n the \"Installed modules\" block of `IDOS.md` (one line: ``- `board-game` (0.1.0) — src/modules/board-game/``)\n — on platform projects the platform maintains that block itself.\n5. **Install deps** — the union of `runtimePackages` + each module's `dependencies`. Modules bring\n their own engine (three, phaser); the host brings react/react-dom/app-shell.\n6. **Run** — sign-in → straight INTO the game, never a \"Play\" click (owner's rule 07.11.2026): a game\n whose route says `inLobby: true` (idle-rpg) sits inside the lobby, the lobby's balances and tabs\n (the systems' screens; a tab hides itself while the title has nothing for it) around it; else the\n first game on the whole screen with a small button back to the lobby. The lobby draws the balances and the wallet; games hide their own copies by\n themselves (`sharedUi` roles).\n\nTo combine multiple genres into one game, see **idosgames-compose-modules**. To write or edit a\nmodule, see **idosgames-module-contract**.\n\n## Where state lives (decide this before writing the first save)\n\nThe project is client-side code in the player's browser. `localStorage`, module fields, and React\nstate are **not storage** — nothing there survives a device change, and nothing there is trusted.\n\n1. **A dedicated module owns it?** Use that module. Currencies, inventory, quests, characters,\n leaderboards, store purchases each have a service that enforces the rules server-side.\n2. **Otherwise, per-player data → `client.userCustomData`** — buckets `Private`/`Public` are\n client-writable (settings, cosmetics), `ReadOnly`/`Internal` are server-only. Anything a player\n could cheat by editing goes in the server-only buckets. See **user-custom-data**.\n3. **Shared by all players → `client.titleCustomData`** (event state, global counters, server\n thresholds, feature toggles). Read-only for clients. See **title-custom-data**.\n4. **Writing any of the server-only data, or any rule the player must not be able to fake** →\n a CloudCode handler, called with `client.cloudCode.execute(...)`. See **cloud-code**.\n\n## Two MCP surfaces: game CODE vs. a Title's live DATA\n\nThe platform exposes **two independent MCP servers** — don't confuse them:\n\n- **`@idosgames/mcp`** (this one) serves the **CODE registry**. Use it to WRITE a game's source:\n `get_host_scaffold`, `list_modules` / `search` / `get_module`, `get_manifest`, `list_skills` /\n `get_skill`. Transport: stdio (`npx -y @idosgames/mcp`). Its registry is bundled offline; to use the\n hosted copy set `IDOSGAMES_REGISTRY_URL=https://cloud.idosgames.com/drive/registry/latest` — a **base**\n the loader appends `/index.json`, `/modules/{id}.json`, etc. to (the base itself is not fetchable on\n R2; open `.../latest/index.json` to browse the catalog). Read-only, no auth. It never reads or changes\n a live Title's data.\n- **The Title-configuration MCP** (a separate backend server) owns a **live Title's DATA**. Use it to\n read/write the Title's `TitlePublicConfiguration` (`get_<field>` / `save_<field>`, or the whole\n model) and to generate assets (`generate_image` / `generate_audio` / `generate_text` /\n `generate_three_d` / `generate_video`). Transport: HTTP JSON-RPC at\n `POST https://site.idosgames.com/api/v2/mcp`; every tool call takes a `title_id` argument.\n Authorization is **OAuth 2.1** — there is no API key and nothing to paste. Connect it as a plain\n HTTP MCP server with **no headers**: your client gets a `401`, discovers the authorization\n server, registers itself, and opens a browser where the publisher picks which Titles and which\n permissions to grant. The token lives in your client's own credential store, so committed config\n holds only the URL:\n\n ```json\n {\n \"mcpServers\": {\n \"idosgames-title\": {\n \"type\": \"http\",\n \"url\": \"https://site.idosgames.com/api/v2/mcp\"\n }\n }\n }\n ```\n\n Permissions the publisher can grant: `config:read`, `config:write`, `cloudcode:write`,\n `ai:generate`, `kanban:read`, `kanban:write`, `build:write`. A grant is scoped to the Titles ticked on the consent screen, and the publisher\n can revoke it any time from **Connected apps** in the dashboard. If a call comes back\n `SCOPE_NOT_ALLOWED` or `TITLE_NOT_ALLOWED`, the token is fine — that permission or that Title\n simply was not granted; ask the publisher to re-authorize rather than retrying.\n\n **Shipping the game itself** (permission `build:write`) also goes through this server — the\n platform hosts it on the Title's own address (`{titleid}.idos.games`) and its idosgames.com page:\n\n 1. `begin_build_upload { title_id }` → `build_id`, a one-hour `upload_url`, `asset_base`.\n 2. Build the game as static files with that base: `vite build --base=<asset_base>` (a relative\n `./` base also works — Unity WebGL builds already are). Use the LIVE title id, never `-DEV`.\n 3. Zip the CONTENTS of the output folder (`index.html` at the zip root; a single `dist/` top folder\n is accepted) and upload it: `curl -T build.zip \"<upload_url>\"` — a plain PUT, no headers.\n 4. `finish_build_upload { title_id, build_id, deploy }` — unpacks it into a new version (it shows up\n in the dashboard's version history) and returns `TestUrl` (DEV address). `deploy: true` puts it\n live for players right away; without it, check `TestUrl` and call it again with `deploy: true`.\n Still unpacking when it returns → poll `get_build`. `list_builds` shows recent versions.\n\n A game that is live but whose Title is not published yet has no idosgames.com page — the\n response says so; the publisher publishes it from the dashboard.\n\n ⚠ **The build must be the game itself.** On the Title's idosgames.com page the game may embed\n (`<iframe>`) only platform hosts — never another website, with no setting to allow one. A\n \"wrapper\" build that signs in and then frames the publisher's own site shows **\"Content\n unavailable\"** there instead of the game: build and upload the real game. Network calls\n (`fetch`/WebSocket) from the embedded game reach platform hosts plus the Title's\n `Blockchain.IframeConnectHosts` (`save_blockchain`; wallet-connection hosts are always refused).\n\n Configuring a **fresh (empty) Title** so a game can actually run against it (currencies → game\n loop → bots) is its own checklist: see **idosgames-title-bootstrap**.\n\nRule of thumb: **game CODE → `@idosgames/mcp`; a Title's live config DATA and generated ASSETS → the\nbackend `v2/mcp` server.** Scaffolding a project and configuring/populating the Title it runs as are\ntwo different jobs on two different servers — connect the one that matches the task (or both).\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idosgames-module-contract",
|
|
3
|
-
"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,
|
|
4
|
-
"content": "---\nname: idosgames-module-contract\ndescription: >-\n Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk:\n the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route\n registration, and the shared controller-box bridge between an imperative scene and React panels.\n Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg,\n voxelcraft, mini-game, store, character, workshop…), or asks about defineModule,\n registerScene/registerPanel/registerRoute, ctx.effect / ctx.every (background work: badges, polling),\n ctx.features (register / open / setBadge / setAvailable),\n ctx.modes,\n activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic,\n ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content),\n ctx.multiplayer / registerGame (playing online through the lobby's rooms system),\n ctx.lobby / setHero (a game's showcase on the lobby's home — its character on a pedestal + \"Play\"),\n ctx.characterArt (the game's own hero drawn on the Heroes screen and other modules' screens),\n ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.\n---\n\n# The module contract (@idosgames/module-sdk)\n\nA module is a manifest the host installs once. It exports `{camelCase(id)}Module` from its\n`index.ts` (e.g. `board-game` → `boardGameModule`, `idle-rpg` → `idleRpgModule`) — the host seeder\nderives the import name from the id, so this convention is required.\n\n```ts\nimport { defineModule } from \"@idosgames/module-sdk\";\n\nexport const boardGameModule = defineModule({\n id: \"board-game\",\n meta: { name: \"Board Game\", type: \"game\", genre: \"board\", engine: \"three\" }, // engine: three|phaser|dom\n setup(ctx) {\n // ctx.client (shared, authed) · ctx.events (typed cross-module bus) · ctx.sharedUi · ctx.surface\n ctx.registerScene(createScene(box)); // rendered engine scene (optional)\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: RootPanel }); // React UI (optional)\n ctx.registerRoute({ id: \"board-game\", label: \"Board\", icon: \"🎲\" }); // a mode → the lobby's \"Play\"\n },\n});\n```\n\nTwo kinds of module plug into the base (sign-in and the lobby, from `@idosgames/app-shell`):\n\n- a **game** (`meta.type: \"game\"`) registers a route: the lobby's \"Play\" launches it on the whole\n screen with a small \"To the lobby\" button over it (none with `ownExit: true` — the game leads back\n by itself, voxelcraft's pause menu); with `inLobby: true` on the route (idle-rpg) it\n instead lives INSIDE the lobby — the base keeps its balances and tabs around the game through the\n host's `header`/`footer` panel slots (the canvas and every other slot are laid out between them,\n so a game needs nothing for it) and the app starts in it;\n- a **system** (`meta.type: \"app\"`) registers a feature — its screen becomes a lobby tab:\n `ctx.features.register({ id, label, icon, group, order, primary?, available?, fullBleed?, Screen })`\n (`fullBleed: true` — the screen paints edge to edge, the lobby gives it the whole area). A tab\n that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows (details: **idosgames-ui-kit**).\n Give `label` as a function — `label: () => t(\"title\")` — so the tab follows the player's language\n switch without a reload (a plain string stays as registered).\n\n`ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type),\nfilled once all modules are installed (read it at render time, not inside `setup()`).\n`ctx.navigate(id)` switches (`\"lobby\"` is the base); called inside `setup()` it picks the mode the app\nstarts in. `ctx.features.open(id, args)` from a game (the lobby not on screen) switches to the lobby\nand opens that tab there.\n\n`ctx.lobby` — the base's lobby as seen by a game. A game NEVER draws a lobby of its own (no own\n\"Play\" screen, no own menu of creation screens): sign-in, balances, profile and every system are the\nbase's, and the game plugs into it —\n\n- `ctx.lobby.setHero({ mode, mount(host, stage), view(), subscribe(fn), beforePlay? })` puts its\n showcase on the lobby's home (\"Play\" tab), Fortnite-style: `mount` draws into an element filling\n the home behind the lobby's interface (any engine; stand the figure in `stage.spot()` — CSS px of\n the host, read it every frame), returns the cleanup; `view()` describes what the lobby lays out\n with the UI kit around it — `title` / `subtitle` under the figure, `actions` there (\"Edit\",\n \"Emote\" with a `menu`), and the `play` card next to the big \"Play\" (`caption`, `title`, `actions`,\n `more` for its \"⋯\"). The lobby's \"Play\" switches to `mode`. Reference: voxelcraft `src/lobby/hero.ts`;\n- the game's own sections (worlds, character editor, items…) are ordinary features\n (`ctx.features.register`, screens on the UI kit) — tabs next to the shop and the quests;\n- `ctx.lobby.available()` / `open()` — \"To the lobby\" in the game's pause menu (hide it when false).\n Works before the game's scene is mounted: keep what the tabs and the hero need outside the scene\n (voxelcraft: `src/hub.ts`) and hand the choice made there to the scene when it starts.\n\n`ctx.characterArt` — the game's hero on OTHER modules' screens (the `character` system's Heroes\nscreen: the hub, the hero card, the hero list). A game with a character of its own sets it once in\nsetup — `ctx.characterArt.set({ mount(host, subject, { mode }) })`: draw the hero described by\n`subject` (`characterId`, `classId`, `equipment` slot → `ItemID`, `skinId`, `locked`) filling `host`,\nstanding on its bottom edge; `mode` is `\"live\"` (a big view: it may move) or `\"still\"` (one frame, a\nlist of many); return `{ update(subject), destroy() }` — `update` comes on every change (a piece\nequipped). A screen reads `ctx.characterArt.get()` and falls back to its own picture when it is null.\nReference: idle-rpg `src/heroArt.ts` (its layered hero on a WebGL canvas, `CharacterCanvas`).\n\n`ctx.multiplayer` — a game that plays online registers itself (`registerGame({ mode, topology,\nchatMode, maxPlayers, joinInProgress, settings, start(room) })`) and takes the room the lobby's\nrooms system hands it on \"Play\"; it never creates or joins rooms (skill **multiplayer-realtime**).\n\nOptional static field: `sharedUi: { provides?: SharedUiRole[]; requires?: SharedUiRole[] }` — see\n\"Shared chrome\" below.\n\n`meta.type` is `game | app | ai-app`; `meta.engine` is `three | phaser | dom` (`dom` = no renderer —\na pure React/DOM app module). `setup` is called ONCE with `ctx: ModuleContext`.\n\n## EngineScene (the rendered part)\n\nA scene mounts into a bare `HTMLElement` (framework-free — this is why a vanilla Three game like\nvoxelcraft fits). The host's Mode Router drives it; only the active mode runs.\n\n```ts\nconst scene: EngineScene = {\n surface: \"fullbleed-canvas\",\n mount(ctx: SceneMountContext) {\n controller = new Controller(ctx.host);\n box.set(controller);\n },\n activate() {\n controller?.setRunning(true);\n }, // became the active mode → resume RAF\n suspend() {\n controller?.setRunning(false);\n }, // hidden → stop RAF (invariant: only active ticks)\n destroy() {\n controller?.destroy();\n }, // permanent teardown\n};\n```\n\n`mount` takes a context object (not a bare element) so it can grow — e.g. a host-shared renderer in\nthe composition era — without breaking the contract. A scene MUST stop its RAF on `suspend`.\n\n### Loading — never a still screen\n\nA scene's FIRST start goes behind the app's loading screen by itself: the host shows it (themed,\n\"Loading «<mode>»…\"), lets it paint, THEN calls `mount` — so heavy synchronous work in `mount`\n(generating terrain, building meshes) never looks like a frozen app. The screen goes away after the\nfirst frame, or — when the scene has more to do — when its optional `ready(): Promise<void>` resolves\n(voxelcraft: the last played world restored from storage). Never draw your own start-up spinner.\n\nLong work AFTER the start goes through `ctx.loading` — the same screen:\n\n- `await ctx.loading.track(fetchWorld(), \"Loading the world…\")` — async work;\n- `await ctx.loading.run(() => buildWorld(seed), \"Building the world…\")` — work that BLOCKS the page:\n the screen is shown and painted first (calling heavy code right after `begin()` freezes the page\n before the screen appears);\n- `const task = ctx.loading.begin(\"…\"); task.progress(0.4, \"Textures…\"); task.done();` — with progress.\n\n### Notifications — `ctx.notifications`\n\nThe app's one system of toasts and alert windows (the UI kit draws them, themed): never draw a banner\nof your own. `ctx.notifications.toast({ level, title, message, icon, image, source, key, actions })`\n— `level`: `info | success | warning | error | reward`; `source` defaults to the module's id (the\ntoast shows that screen's name and a tap opens it). `await ctx.notifications.alert({ level, title,\nmessage, actions: [{ id: \"go\", label: \"Open\", primary: true, openFeature: \"season\" }, { label: \"Later\" }] })`\nresolves with the pressed action's id (`null` — closed). React screens: `useNotify()` from the kit.\n\nA module that registers a scene MUST also publish its debug surface — `ctx.exposeToAgent({ state,\nactions, describeActions })` — and must NOT gate controls on Pointer Lock (unavailable in the\npreview's cross-origin iframe). Nothing inside a `<canvas>` is observable from the DOM, so without\nthe surface neither the AI Coder nor a human reviewer can tell what the game is doing. See the\n`idosgames-agent-debug-surface` skill.\n\n### Videos on idosgames.com — `focus()`, `audio()`, `ctx.clips.mark()`\n\nOn idosgames.com the player records the game: **Clip** (the last 30/60 s, always kept in the\nbackground), **Record** (start/stop), **Best moments** (made automatically), then trims, picks\nYouTube 16:9 / TikTok 9:16 / square and shares. The host (`@idosgames/app-shell`) records the active\nscene's `<canvas>` by itself — no code is needed for a basic video. Three optional touches make the\nvideos good; add them to every canvas game:\n\n```ts\n// in the scene object\nfocus() { // where the action is, 0–1 on the canvas — the vertical cut follows it\n const p = worldToScreen(player.position); // your own projection\n return { x: p.x / canvas.width, y: p.y / canvas.height };\n},\naudio() { // the game's sound, so videos are not silent\n return musicAndSfxDestination.stream; // a MediaStreamAudioDestinationNode your\n}, // master gain ALSO feeds (keep the speakers)\n\n// in setup() / gameplay — bright moments become the player's \"Best moments\" clips\nctx.clips.mark(\"boss_down\", 8); // weight 1–10: a boss 8, a record 6, a level-up 3, a silly death 4\n```\n\n- A first-person game needs no `focus` (the action is in the centre). Do not call it per frame —\n the host samples it a few times a second.\n- `audio()` returns `null` until your AudioContext exists (it is created on the first click) — fine,\n the host asks again. Route every sound through ONE master node and tap that (VoxelCraft:\n `systems/Audio.ts` `output()` / `recordStream()`).\n- Mark real moments only — the session keeps the 3 heaviest; close marks merge and add up.\n- An analytics event the publisher flagged as a highlight (`Analytics.Events[name].Highlight`) and\n loud voice chat are marks too, without any code.\n- DOM HUD panels are NOT in the video (only the canvas) — a clean video reads better in a feed.\n- Reference: `modules/voxelcraft` (`scene.ts` `audio`, `game/wire.ts` marks).\n\n### `capture()` — one still frame\n\nA scene may also answer the platform's still-frame request through the optional\n`capture?(): Promise<Blob | null>`. The host (`@idosgames/app-shell`) wires the request itself —\na module only has to return a picture of its active scene.\n\n**Implement it in any scene that renders to a canvas.** Without it the host falls back to reading\nthe `<canvas>` from outside, and for WebGL that is a trap: a renderer created WITHOUT\n`preserveDrawingBuffer` (the default, and the right default — the flag costs memory and a copy every\nframe) has already cleared its buffer by the time anyone reads it, so `toBlob()` returns a **fully\nblack** image and reports nothing. The host rejects single-colour frames, so the player gets the\ngame's cover instead of their moment.\n\nRender and read in the SAME tick:\n\n```ts\n// three.js\ncapture() {\n return new Promise((resolve) => {\n renderer.render(scene, camera); // draw now…\n renderer.domElement.toBlob(resolve, \"image/png\"); // …and read before the buffer is cleared\n });\n},\n\n// Phaser — use its own snapshot, it handles the timing\ncapture() {\n return new Promise((resolve) => {\n setTimeout(() => resolve(null), 1500); // no frame came (context lost) → answer \"no\", don't hang\n game.loop.wake(); // a sleeping loop never draws again → snapshot never fires\n game.renderer.snapshot((image) => {\n if (!(image instanceof HTMLImageElement)) return resolve(null);\n const c = document.createElement(\"canvas\");\n c.width = image.naturalWidth || image.width;\n c.height = image.naturalHeight || image.height;\n c.getContext(\"2d\")?.drawImage(image, 0, 0);\n c.toBlob(resolve, \"image/png\");\n });\n });\n},\n```\n\nAlways answer — with a Blob or `null`. A `capture()` that never settles makes the site wait out its\ntimeout before it falls back, and the player stares at a spinner.\n\nReturn `null` when there is nothing worth showing (loading, no scene yet) — the site then uses the\ncover. Only the scene is captured, not the React panels over it: a shot for a feed reads better\nwithout the HUD. DOM-only modules (no canvas) simply omit `capture`. Working references:\n`modules/board-game` and `modules/voxelcraft` (three), `modules/idle-rpg` (Phaser).\n\n## UiPanel (the React part)\n\nPanels are React components the host renders inside its provider stack (client + status already\nprovided). Use `@idosgames/react` hooks (`useIDosGamesClient`, `useUserState`) — do NOT re-create\nproviders.\n\n- `slot`: `hud | sidebar | overlay | modal`.\n- `activeOnly` (default true): show only while this module's mode is active. Set `false` for shared\n chrome that stays across every mode (e.g. a persistent HUD).\n- `overlay`/`sidebar` layers sit between the shared HUD and the nav (the host measures both), so a\n panel positioned `absolute` inside its layer never ends up under them. Things drawn outside the\n layers use `var(--idos-safe-top, 0px)` / `var(--idos-safe-bottom, 0px)`.\n- A `hud` panel's wrapper is `display: contents`: the panel is a direct flex child of the HUD row\n (can `flex: 1`) and must set `pointer-events: auto` only on its controls.\n\n## Shared chrome (`sharedUi`)\n\nRoles: `currency-bar` (virtual-currency balances), `wallet` (crypto wallet entry — `LazyWalletPanel`),\n`status` (the `useStatus()` line), `account` (Log out + player ID), `menu` (the menu of features),\n`modes` (the switch between modes). The base takes `currency-bar`, `wallet`, `account`, `menu` and\n`modes` — its lobby draws them; `status` stays with each game. A module that draws one for every\nmode declares `sharedUi: { provides: [...] }` on the module object — statically, so the host resolves\nowners before any `setup()` and the answer never depends on module order.\n\n**Rule for templates: yield every role you draw yourself.** Read it once in `setup()` and close over\nthe answer (it is fixed for the session):\n\n```ts\nsetup(ctx) {\n const chrome = {\n wallet: ctx.sharedUi.shouldDraw(\"wallet\"),\n balances: ctx.sharedUi.shouldDraw(\"currency-bar\"),\n status: ctx.sharedUi.shouldDraw(\"status\"),\n };\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: makeRootPanel(box, chrome) });\n}\n```\n\nHide only the SHARED pieces — genre UI (the board's stage/tile/cycle chips) always stays. Never\nwrap your UI in your own `<StatusProvider>`: the host provides ONE, and a nested one would swallow\nyour messages so the shared status line never sees them. A module that needs a role but does not\ndraw it declares `sharedUi: { requires: [\"currency-bar\"] }`. `ctx.sharedUi.ownerOf(role)` names the\nprovider (or `null`). A full-screen mode (its own bottom tab bar) that shows the player ID and\n\"Log out\" in its own header declares `sharedUi: { drawsOwn: [\"account\"] }` and draws them only while\n`shouldDraw(\"account\")`: the host then hides its floating bottom-left copy while that mode is on\nscreen (it would cover the tab bar) and keeps it in the other modes. `drawsOwn` takes no role —\nwith the base installed the lobby draws the account and the mode hides its copy.\n\n## Events (`ctx.events`)\n\nTyped topic tokens — `defineTopic(\"<your-id>:<event>@1\", shape({ … }))` from\n`@idosgames/module-sdk`; emit only your own namespace; to listen to another module, copy its topic\nand payload descriptor from the catalog into your own `defineTopic` (never import it); declare both\nin `module.meta.json` (`events.emits` / `events.listens`). **Subscribe in `setup()`**, not in a panel\neffect — `activeOnly` panels unmount with their mode and miss events; store what arrives in a small\nstore the panel reads. Full rules and examples: **idosgames-compose-modules** (\"Signals between\nmodules\"). The string overloads (`emit(\"x\", …)`) are deprecated.\n\n## Content types (`ctx.content`) and switching modes (`ctx.navigate`)\n\nA game whose players make things (worlds, levels, skins) registers a **content type handler** so the\nshared Workshop module can publish and open them — the Workshop never imports the game:\n\n```ts\nsetup(ctx) {\n ctx.content.registerType({\n type: \"voxelcraft.world\", // = a key of Workshop.ContentTypes in the title config\n label: \"VoxelCraft world\",\n icon: \"⛏️\",\n modeId: \"voxelcraft\", // route to switch to after open()\n async listLocal() { return saves.map((s) => ({ id: s.id, name: s.name, updatedAt: s.at })); },\n async capture(localId) { // roles/MIME types must fit the title's content type config\n return { files: [{ role: \"main\", contentType: \"application/json\", data: json }],\n thumbnail: { contentType: \"image/webp\", data: webp }, suggestedTitle: name };\n },\n async open(content) { importSave(content.files[0].data); },\n });\n}\n```\n\nHandlers registered in `setup()` are removed with the module; `registerType` returns an unregister\nfunction for anything dynamic. `ctx.navigate(modeId)` switches the host to another module's route\n(the Workshop uses it to jump into the game after `open`); an unknown id is ignored. Details:\n**workshop-system**.\n\n## Bridging scene ↔ panel\n\nThe scene creates its controller at `mount` (needs the canvas), but panels render before that. Share\nit with a one-slot observable and read it with `useSyncExternalStore`:\n\n```ts\nexport function createControllerBox<T>() {\n /* get/set/subscribe */\n}\n// panel: const controller = useSyncExternalStore(box.subscribe, box.get); if (!controller) return <Loading/>;\n```\n\nFor the module's controller React context use the shared factory instead of hand-writing it:\n\n```ts\nexport const [BoardControllerProvider, useBoardController] =\n createControllerContext<BoardController>(\"BoardController\"); // from @idosgames/react\n```\n\n## Layout and boundaries\n\nA module is one folder, `src/modules/<id>/`. It imports only its own files, the game's shared code\nin `src/shared/` and npm packages — never another module's files or host files. Talk to other\nmodules through the shared `ctx.client` (durable state) and `ctx.events` (live signals, typed\ntopics `<module-id>:<event>@<major>` declared in `module.meta.json`). A module shipped in the catalog is fully self-contained — it never uses\n`src/shared/`, so it installs into any game; a game's own module may use it, and its shared code is\ncopied into it when it is published for other creators.\n\n```\nsrc/modules/<id>/\n index.ts export { <camelCaseId>Module } from \"./module\";\n module.ts defineModule({ … })\n module.meta.json manifest: type (template|feature), summary, provides, tags, version, author\n components/ game/ data/ react/ — as needed\n```\n\nEvery module carries its own `module.meta.json` (the `ModuleManifest` shape), including a game's own\nmodules. The full standard — where a new feature goes, file size, documentation — is\n**idosgames-project-structure**.\n\n## Dependencies\n\nDeclare only the module's UNIQUE deps (e.g. `phaser` for idle-rpg, `three` for board/voxel) plus the\nshared baseline (react, @idosgames/*). The platform pins shared libs identically across modules; two\nmodules must not request different versions of one package (the build allowlist rejects it).\n\nStudy a real module via `get_module {id}` (MCP) before writing a new one.\n",
|
|
3
|
+
"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, kit-game, store, character, workshop…), or asks about defineModule, registerScene/registerPanel/registerRoute, ctx.effect / ctx.every (background work: badges, polling), ctx.features (register / open / setBadge / setAvailable), ctx.modes, activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic, ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content), ctx.multiplayer / registerGame (playing online through the lobby's rooms system), ctx.lobby / setHero (a game's showcase on the lobby's home — its character on a pedestal + \"Play\"), ctx.characterArt (the game's own hero drawn on the Heroes screen and other modules' screens), ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.",
|
|
4
|
+
"content": "---\nname: idosgames-module-contract\ndescription: >-\n Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk:\n the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route\n registration, and the shared controller-box bridge between an imperative scene and React panels.\n Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg,\n voxelcraft, kit-game, store, character, workshop…), or asks about defineModule,\n registerScene/registerPanel/registerRoute, ctx.effect / ctx.every (background work: badges, polling),\n ctx.features (register / open / setBadge / setAvailable),\n ctx.modes,\n activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic,\n ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content),\n ctx.multiplayer / registerGame (playing online through the lobby's rooms system),\n ctx.lobby / setHero (a game's showcase on the lobby's home — its character on a pedestal + \"Play\"),\n ctx.characterArt (the game's own hero drawn on the Heroes screen and other modules' screens),\n ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.\n---\n\n# The module contract (@idosgames/module-sdk)\n\nA module is a manifest the host installs once. It exports `{camelCase(id)}Module` from its\n`index.ts` (e.g. `board-game` → `boardGameModule`, `idle-rpg` → `idleRpgModule`) — the host seeder\nderives the import name from the id, so this convention is required.\n\n```ts\nimport { defineModule } from \"@idosgames/module-sdk\";\n\nexport const boardGameModule = defineModule({\n id: \"board-game\",\n meta: { name: \"Board Game\", type: \"game\", genre: \"board\", engine: \"three\" }, // engine: three|phaser|dom\n setup(ctx) {\n // ctx.client (shared, authed) · ctx.events (typed cross-module bus) · ctx.sharedUi · ctx.surface\n ctx.registerScene(createScene(box)); // rendered engine scene (optional)\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: RootPanel }); // React UI (optional)\n ctx.registerRoute({ id: \"board-game\", label: \"Board\", icon: \"🎲\" }); // a mode → the lobby's \"Play\"\n },\n});\n```\n\nTwo kinds of module plug into the base (sign-in and the lobby, from `@idosgames/app-shell`):\n\n- a **game** (`meta.type: \"game\"`) registers a route: the lobby's \"Play\" launches it on the whole\n screen with a small \"To the lobby\" button over it (none with `ownExit: true` — the game leads back\n by itself, voxelcraft's pause menu); with `inLobby: true` on the route (idle-rpg) it\n instead lives INSIDE the lobby — the base keeps its balances and tabs around the game through the\n host's `header`/`footer` panel slots (the canvas and every other slot are laid out between them,\n so a game needs nothing for it) and the app starts in it;\n- a **system** (`meta.type: \"app\"`) registers a feature — its screen becomes a lobby tab:\n `ctx.features.register({ id, label, icon, group, order, primary?, available?, fullBleed?, Screen })`\n (`fullBleed: true` — the screen paints edge to edge, the lobby gives it the whole area). A tab\n that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows (details: **idosgames-ui-kit**).\n Give `label` as a function — `label: () => t(\"title\")` — so the tab follows the player's language\n switch without a reload (a plain string stays as registered).\n\n`ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type),\nfilled once all modules are installed (read it at render time, not inside `setup()`).\n`ctx.navigate(id)` switches (`\"lobby\"` is the base); called inside `setup()` it picks the mode the app\nstarts in. `ctx.features.open(id, args)` from a game (the lobby not on screen) switches to the lobby\nand opens that tab there.\n\n`ctx.lobby` — the base's lobby as seen by a game. A game NEVER draws a lobby of its own (no own\n\"Play\" screen, no own menu of creation screens): sign-in, balances, profile and every system are the\nbase's, and the game plugs into it —\n\n- `ctx.lobby.setHero({ mode, mount(host, stage), view(), subscribe(fn), beforePlay? })` puts its\n showcase on the lobby's home (\"Play\" tab), Fortnite-style: `mount` draws into an element filling\n the home behind the lobby's interface (any engine; stand the figure in `stage.spot()` — CSS px of\n the host, read it every frame), returns the cleanup; `view()` describes what the lobby lays out\n with the UI kit around it — `title` / `subtitle` under the figure, `actions` there (\"Edit\",\n \"Emote\" with a `menu`), and the `play` card next to the big \"Play\" (`caption`, `title`, `actions`,\n `more` for its \"⋯\"). The lobby's \"Play\" switches to `mode`. Reference: voxelcraft `src/lobby/hero.ts`;\n- the game's own sections (worlds, character editor, items…) are ordinary features\n (`ctx.features.register`, screens on the UI kit) — tabs next to the shop and the quests;\n- `ctx.lobby.available()` / `open()` — \"To the lobby\" in the game's pause menu (hide it when false).\n Works before the game's scene is mounted: keep what the tabs and the hero need outside the scene\n (voxelcraft: `src/hub.ts`) and hand the choice made there to the scene when it starts.\n\n`ctx.characterArt` — the game's hero on OTHER modules' screens (the `character` system's Heroes\nscreen: the hub, the hero card, the hero list). A game with a character of its own sets it once in\nsetup — `ctx.characterArt.set({ mount(host, subject, { mode }) })`: draw the hero described by\n`subject` (`characterId`, `classId`, `equipment` slot → `ItemID`, `skinId`, `locked`) filling `host`,\nstanding on its bottom edge; `mode` is `\"live\"` (a big view: it may move) or `\"still\"` (one frame, a\nlist of many); return `{ update(subject), destroy() }` — `update` comes on every change (a piece\nequipped). A screen reads `ctx.characterArt.get()` and falls back to its own picture when it is null.\nReference: idle-rpg `src/heroArt.ts` (its layered hero on a WebGL canvas, `CharacterCanvas`).\n\n`ctx.multiplayer` — a game that plays online registers itself (`registerGame({ mode, topology,\nchatMode, maxPlayers, joinInProgress, settings, start(room) })`) and takes the room the lobby's\nrooms system hands it on \"Play\"; it never creates or joins rooms (skill **multiplayer-realtime**).\n\nOptional static field: `sharedUi: { provides?: SharedUiRole[]; requires?: SharedUiRole[] }` — see\n\"Shared chrome\" below.\n\n`meta.type` is `game | app | ai-app`; `meta.engine` is `three | phaser | dom` (`dom` = no renderer —\na pure React/DOM app module). `setup` is called ONCE with `ctx: ModuleContext`.\n\n## EngineScene (the rendered part)\n\nA scene mounts into a bare `HTMLElement` (framework-free — this is why a vanilla Three game like\nvoxelcraft fits). The host's Mode Router drives it; only the active mode runs.\n\n```ts\nconst scene: EngineScene = {\n surface: \"fullbleed-canvas\",\n mount(ctx: SceneMountContext) {\n controller = new Controller(ctx.host);\n box.set(controller);\n },\n activate() {\n controller?.setRunning(true);\n }, // became the active mode → resume RAF\n suspend() {\n controller?.setRunning(false);\n }, // hidden → stop RAF (invariant: only active ticks)\n destroy() {\n controller?.destroy();\n }, // permanent teardown\n};\n```\n\n`mount` takes a context object (not a bare element) so it can grow — e.g. a host-shared renderer in\nthe composition era — without breaking the contract. A scene MUST stop its RAF on `suspend`.\n\n### Loading — never a still screen\n\nA scene's FIRST start goes behind the app's loading screen by itself: the host shows it (themed,\n\"Loading «<mode>»…\"), lets it paint, THEN calls `mount` — so heavy synchronous work in `mount`\n(generating terrain, building meshes) never looks like a frozen app. The screen goes away after the\nfirst frame, or — when the scene has more to do — when its optional `ready(): Promise<void>` resolves\n(voxelcraft: the last played world restored from storage). Never draw your own start-up spinner.\n\nLong work AFTER the start goes through `ctx.loading` — the same screen:\n\n- `await ctx.loading.track(fetchWorld(), \"Loading the world…\")` — async work;\n- `await ctx.loading.run(() => buildWorld(seed), \"Building the world…\")` — work that BLOCKS the page:\n the screen is shown and painted first (calling heavy code right after `begin()` freezes the page\n before the screen appears);\n- `const task = ctx.loading.begin(\"…\"); task.progress(0.4, \"Textures…\"); task.done();` — with progress.\n\n### Notifications — `ctx.notifications`\n\nThe app's one system of toasts and alert windows (the UI kit draws them, themed): never draw a banner\nof your own. `ctx.notifications.toast({ level, title, message, icon, image, source, key, actions })`\n— `level`: `info | success | warning | error | reward`; `source` defaults to the module's id (the\ntoast shows that screen's name and a tap opens it). `await ctx.notifications.alert({ level, title,\nmessage, actions: [{ id: \"go\", label: \"Open\", primary: true, openFeature: \"season\" }, { label: \"Later\" }] })`\nresolves with the pressed action's id (`null` — closed). React screens: `useNotify()` from the kit.\n\nA module that registers a scene MUST also publish its debug surface — `ctx.exposeToAgent({ state,\nactions, describeActions })` — and must NOT gate controls on Pointer Lock (unavailable in the\npreview's cross-origin iframe). Nothing inside a `<canvas>` is observable from the DOM, so without\nthe surface neither the AI Coder nor a human reviewer can tell what the game is doing. See the\n`idosgames-agent-debug-surface` skill.\n\n### Videos on idosgames.com — `focus()`, `audio()`, `ctx.clips.mark()`\n\nOn idosgames.com the player records the game: **Clip** (the last 30/60 s, always kept in the\nbackground), **Record** (start/stop), **Best moments** (made automatically), then trims, picks\nYouTube 16:9 / TikTok 9:16 / square and shares. The host (`@idosgames/app-shell`) records the active\nscene's `<canvas>` by itself — no code is needed for a basic video. Three optional touches make the\nvideos good; add them to every canvas game:\n\n```ts\n// in the scene object\nfocus() { // where the action is, 0–1 on the canvas — the vertical cut follows it\n const p = worldToScreen(player.position); // your own projection\n return { x: p.x / canvas.width, y: p.y / canvas.height };\n},\naudio() { // the game's sound, so videos are not silent\n return musicAndSfxDestination.stream; // a MediaStreamAudioDestinationNode your\n}, // master gain ALSO feeds (keep the speakers)\n\n// in setup() / gameplay — bright moments become the player's \"Best moments\" clips\nctx.clips.mark(\"boss_down\", 8); // weight 1–10: a boss 8, a record 6, a level-up 3, a silly death 4\n```\n\n- A first-person game needs no `focus` (the action is in the centre). Do not call it per frame —\n the host samples it a few times a second.\n- `audio()` returns `null` until your AudioContext exists (it is created on the first click) — fine,\n the host asks again. Route every sound through ONE master node and tap that (VoxelCraft:\n `systems/Audio.ts` `output()` / `recordStream()`).\n- Mark real moments only — the session keeps the 3 heaviest; close marks merge and add up.\n- An analytics event the publisher flagged as a highlight (`Analytics.Events[name].Highlight`) and\n loud voice chat are marks too, without any code.\n- DOM HUD panels are NOT in the video (only the canvas) — a clean video reads better in a feed.\n- Reference: `modules/voxelcraft` (`scene.ts` `audio`, `game/wire.ts` marks).\n\n### `capture()` — one still frame\n\nA scene may also answer the platform's still-frame request through the optional\n`capture?(): Promise<Blob | null>`. The host (`@idosgames/app-shell`) wires the request itself —\na module only has to return a picture of its active scene.\n\n**Implement it in any scene that renders to a canvas.** Without it the host falls back to reading\nthe `<canvas>` from outside, and for WebGL that is a trap: a renderer created WITHOUT\n`preserveDrawingBuffer` (the default, and the right default — the flag costs memory and a copy every\nframe) has already cleared its buffer by the time anyone reads it, so `toBlob()` returns a **fully\nblack** image and reports nothing. The host rejects single-colour frames, so the player gets the\ngame's cover instead of their moment.\n\nRender and read in the SAME tick:\n\n```ts\n// three.js\ncapture() {\n return new Promise((resolve) => {\n renderer.render(scene, camera); // draw now…\n renderer.domElement.toBlob(resolve, \"image/png\"); // …and read before the buffer is cleared\n });\n},\n\n// Phaser — use its own snapshot, it handles the timing\ncapture() {\n return new Promise((resolve) => {\n setTimeout(() => resolve(null), 1500); // no frame came (context lost) → answer \"no\", don't hang\n game.loop.wake(); // a sleeping loop never draws again → snapshot never fires\n game.renderer.snapshot((image) => {\n if (!(image instanceof HTMLImageElement)) return resolve(null);\n const c = document.createElement(\"canvas\");\n c.width = image.naturalWidth || image.width;\n c.height = image.naturalHeight || image.height;\n c.getContext(\"2d\")?.drawImage(image, 0, 0);\n c.toBlob(resolve, \"image/png\");\n });\n });\n},\n```\n\nAlways answer — with a Blob or `null`. A `capture()` that never settles makes the site wait out its\ntimeout before it falls back, and the player stares at a spinner.\n\nReturn `null` when there is nothing worth showing (loading, no scene yet) — the site then uses the\ncover. Only the scene is captured, not the React panels over it: a shot for a feed reads better\nwithout the HUD. DOM-only modules (no canvas) simply omit `capture`. Working references:\n`modules/board-game` and `modules/voxelcraft` (three), `modules/idle-rpg` (Phaser).\n\n## UiPanel (the React part)\n\nPanels are React components the host renders inside its provider stack (client + status already\nprovided). Use `@idosgames/react` hooks (`useIDosGamesClient`, `useUserState`) — do NOT re-create\nproviders.\n\n- `slot`: `hud | sidebar | overlay | modal`.\n- `activeOnly` (default true): show only while this module's mode is active. Set `false` for shared\n chrome that stays across every mode (e.g. a persistent HUD).\n- `overlay`/`sidebar` layers sit between the shared HUD and the nav (the host measures both), so a\n panel positioned `absolute` inside its layer never ends up under them. Things drawn outside the\n layers use `var(--idos-safe-top, 0px)` / `var(--idos-safe-bottom, 0px)`.\n- A `hud` panel's wrapper is `display: contents`: the panel is a direct flex child of the HUD row\n (can `flex: 1`) and must set `pointer-events: auto` only on its controls.\n\n## Shared chrome (`sharedUi`)\n\nRoles: `currency-bar` (virtual-currency balances), `wallet` (crypto wallet entry — `LazyWalletPanel`),\n`status` (the `useStatus()` line), `account` (Log out + player ID), `menu` (the menu of features),\n`modes` (the switch between modes). The base takes `currency-bar`, `wallet`, `account`, `menu` and\n`modes` — its lobby draws them; `status` stays with each game. A module that draws one for every\nmode declares `sharedUi: { provides: [...] }` on the module object — statically, so the host resolves\nowners before any `setup()` and the answer never depends on module order.\n\n**Rule for templates: yield every role you draw yourself.** Read it once in `setup()` and close over\nthe answer (it is fixed for the session):\n\n```ts\nsetup(ctx) {\n const chrome = {\n wallet: ctx.sharedUi.shouldDraw(\"wallet\"),\n balances: ctx.sharedUi.shouldDraw(\"currency-bar\"),\n status: ctx.sharedUi.shouldDraw(\"status\"),\n };\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: makeRootPanel(box, chrome) });\n}\n```\n\nHide only the SHARED pieces — genre UI (the board's stage/tile/cycle chips) always stays. Never\nwrap your UI in your own `<StatusProvider>`: the host provides ONE, and a nested one would swallow\nyour messages so the shared status line never sees them. A module that needs a role but does not\ndraw it declares `sharedUi: { requires: [\"currency-bar\"] }`. `ctx.sharedUi.ownerOf(role)` names the\nprovider (or `null`). A full-screen mode (its own bottom tab bar) that shows the player ID and\n\"Log out\" in its own header declares `sharedUi: { drawsOwn: [\"account\"] }` and draws them only while\n`shouldDraw(\"account\")`: the host then hides its floating bottom-left copy while that mode is on\nscreen (it would cover the tab bar) and keeps it in the other modes. `drawsOwn` takes no role —\nwith the base installed the lobby draws the account and the mode hides its copy.\n\n## Events (`ctx.events`)\n\nTyped topic tokens — `defineTopic(\"<your-id>:<event>@1\", shape({ … }))` from\n`@idosgames/module-sdk`; emit only your own namespace; to listen to another module, copy its topic\nand payload descriptor from the catalog into your own `defineTopic` (never import it); declare both\nin `module.meta.json` (`events.emits` / `events.listens`). **Subscribe in `setup()`**, not in a panel\neffect — `activeOnly` panels unmount with their mode and miss events; store what arrives in a small\nstore the panel reads. Full rules and examples: **idosgames-compose-modules** (\"Signals between\nmodules\"). The string overloads (`emit(\"x\", …)`) are deprecated.\n\n## Content types (`ctx.content`) and switching modes (`ctx.navigate`)\n\nA game whose players make things (worlds, levels, skins) registers a **content type handler** so the\nshared Workshop module can publish and open them — the Workshop never imports the game:\n\n```ts\nsetup(ctx) {\n ctx.content.registerType({\n type: \"voxelcraft.world\", // = a key of Workshop.ContentTypes in the title config\n label: \"VoxelCraft world\",\n icon: \"⛏️\",\n modeId: \"voxelcraft\", // route to switch to after open()\n async listLocal() { return saves.map((s) => ({ id: s.id, name: s.name, updatedAt: s.at })); },\n async capture(localId) { // roles/MIME types must fit the title's content type config\n return { files: [{ role: \"main\", contentType: \"application/json\", data: json }],\n thumbnail: { contentType: \"image/webp\", data: webp }, suggestedTitle: name };\n },\n async open(content) { importSave(content.files[0].data); },\n });\n}\n```\n\nHandlers registered in `setup()` are removed with the module; `registerType` returns an unregister\nfunction for anything dynamic. `ctx.navigate(modeId)` switches the host to another module's route\n(the Workshop uses it to jump into the game after `open`); an unknown id is ignored. Details:\n**workshop-system**.\n\n## Bridging scene ↔ panel\n\nThe scene creates its controller at `mount` (needs the canvas), but panels render before that. Share\nit with a one-slot observable and read it with `useSyncExternalStore`:\n\n```ts\nexport function createControllerBox<T>() {\n /* get/set/subscribe */\n}\n// panel: const controller = useSyncExternalStore(box.subscribe, box.get); if (!controller) return <Loading/>;\n```\n\nFor the module's controller React context use the shared factory instead of hand-writing it:\n\n```ts\nexport const [BoardControllerProvider, useBoardController] =\n createControllerContext<BoardController>(\"BoardController\"); // from @idosgames/react\n```\n\n## Layout and boundaries\n\nA module is one folder, `src/modules/<id>/`. It imports only its own files, the game's shared code\nin `src/shared/` and npm packages — never another module's files or host files. Talk to other\nmodules through the shared `ctx.client` (durable state) and `ctx.events` (live signals, typed\ntopics `<module-id>:<event>@<major>` declared in `module.meta.json`). A module shipped in the catalog is fully self-contained — it never uses\n`src/shared/`, so it installs into any game; a game's own module may use it, and its shared code is\ncopied into it when it is published for other creators.\n\n```\nsrc/modules/<id>/\n index.ts export { <camelCaseId>Module } from \"./module\";\n module.ts defineModule({ … })\n module.meta.json manifest: type (template|feature), summary, provides, tags, version, author\n components/ game/ data/ react/ — as needed\n```\n\nEvery module carries its own `module.meta.json` (the `ModuleManifest` shape), including a game's own\nmodules. The full standard — where a new feature goes, file size, documentation — is\n**idosgames-project-structure**.\n\n## Dependencies\n\nDeclare only the module's UNIQUE deps (e.g. `phaser` for idle-rpg, `three` for board/voxel) plus the\nshared baseline (react, @idosgames/*). The platform pins shared libs identically across modules; two\nmodules must not request different versions of one package (the build allowlist rejects it).\n\nStudy a real module via `get_module {id}` (MCP) before writing a new one.\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|
package/registry/video.json
CHANGED
|
@@ -9660,7 +9660,7 @@
|
|
|
9660
9660
|
"rig2d-character": {
|
|
9661
9661
|
"feature": "rig2d-character",
|
|
9662
9662
|
"modality": "text",
|
|
9663
|
-
"system": "You design a 2D cartoon hero for a short video: a layered chibi character on a skeleton, seen from the side. Reply with ONE JSON object — a hero recipe — and nothing else: no markdown, no comments.\n\n{ \"name\": string (≤ 40 chars, in the player's language), \"description\": string (≤ 300 chars),\n \"look\": { \"parts\": { \"<part type>\": \"<part id>\" | null, … }, \"colors\": { \"<colour target>\": \"#rrggbb\", … } },\n \"art\": { \"<picture path>\": { \"svg\": \"<svg markup without the svg wrapper>\" }, … } (optional) }\n\n- Pick parts ONLY from the lists below; null takes a part off where it is allowed. Choose colours that make the hero instantly readable.\n- \"art\" redraws a picture of a part you picked with your own SVG of exactly that canvas (width × height in px, y down): only <path>, <circle>, <ellipse>, <rect>, <line>, <polygon>, <polyline>, <g>, gradients and clipPath — no text, no images, no links, no scripts; ≤ 12000 characters each, ≤ 16 pictures. Draw in the same style: a heavy dark outline (#1f1a21, width 3–4), flat fills with one darker shade. Use it only when no part fits the idea (a chef's apron, a superhero suit).\n\nParts and pictures of the rig:\nBack (or null): back/1, back/2, back/3, back/4, back/5, back/6, back/7, back/8, back/9, back/10, back/11, back/12, back/13, back/14, back/15, back/16, back/17, back/18, back/19, back/20, back/21, back/22, back/23, back/24, back/25, back/26, back/27, back/28, back/29, back/30, back/knight_cape, back/mage_cape\nBeard (or null): beard/1, beard/2, beard/3, beard/4, beard/5, beard/6, beard/7, beard/8, beard/9, beard/10\nBoots (or null): boots/1, boots/2, boots/3, boots/4, boots/5, boots/6, boots/7, boots/8, boots/9, boots/10, boots/11, boots/12, boots/13, boots/14, boots/15, boots/16, boots/17, boots/18, boots/19, boots/20, boots/21, boots/22, boots/23, boots/24, boots/25, boots/26, boots/27, boots/28, boots/29, boots/30, boots/plate_boots, boots/leather_boots\nPants (or null): pants/1, pants/2, pants/3, pants/4, pants/5, pants/6, pants/7, pants/8, pants/9, pants/10, pants/11, pants/12, pants/13, pants/14, pants/15, pants/16, pants/17, pants/18, pants/19, pants/20, pants/21, pants/22, pants/23, pants/24, pants/25, pants/26, pants/27, pants/28, pants/29, pants/30, pants/31, pants/32, pants/33, pants/34, pants/35, pants/36, pants/37, pants/38, pants/39, pants/40, pants/41, pants/42, pants/43, pants/44, pants/45, pants/46, pants/47, pants/48, pants/49, pants/50\nBrows: brows/1, brows/2, brows/3, brows/4, brows/5, brows/6, brows/7, brows/8, brows/9, brows/10\nEyes: eyes/1, eyes/2, eyes/3, eyes/4, eyes/5, eyes/6, eyes/7, eyes/8, eyes/9, eyes/10, eyes/11, eyes/12, eyes/13, eyes/14, eyes/15, eyes/16, eyes/17, eyes/18, eyes/19, eyes/20\nGloves (or null): gloves/1, gloves/2, gloves/3, gloves/4, gloves/5, gloves/6, gloves/7, gloves/8, gloves/9, gloves/10, gloves/11, gloves/12, gloves/13, gloves/14, gloves/15, gloves/16, gloves/17, gloves/18, gloves/19, gloves/20, gloves/21, gloves/22, gloves/23, gloves/24, gloves/25, gloves/26, gloves/27, gloves/28, gloves/29, gloves/30, gloves/gauntlets, gloves/bracers\nHair (or null): hair/1, hair/2, hair/3, hair/4, hair/5, hair/6, hair/7, hair/8, hair/9, hair/10, hair/11, hair/12, hair/13, hair/14, hair/15, hair/16, hair/17, hair/18, hair/19, hair/20, hair/21, hair/22, hair/23, hair/24, hair/25, hair/26, hair/27, hair/28, hair/29, hair/30\nHat (or null): hat/1, hat/2, hat/3, hat/4, hat/5, hat/6, hat/7, hat/8, hat/9, hat/10, hat/11, hat/12, hat/13, hat/14, hat/15, hat/16, hat/17, hat/18, hat/19, hat/20, hat/21, hat/22, hat/23, hat/24, hat/25, hat/26, hat/27, hat/28, hat/29, hat/30, hat/31, hat/32, hat/33, hat/34, hat/35, hat/36, hat/37, hat/38, hat/39, hat/40, hat/41, hat/42, hat/43, hat/44, hat/45, hat/46, hat/47, hat/48, hat/49, hat/50, hat/51, hat/52, hat/53, hat/54, hat/55, hat/56, hat/57, hat/58, hat/59, hat/60, hat/great_helm, hat/knight_helm, hat/winged_helm, hat/wizard_hat, hat/ranger_hood, hat/iron_helm\nMouth: mouth/1, mouth/2, mouth/3, mouth/4, mouth/5, mouth/6, mouth/7, mouth/8, mouth/9, mouth/10\nGlasses (or null): glasses/1, glasses/2, glasses/3, glasses/4, glasses/5, glasses/6, glasses/7, glasses/8, glasses/9, glasses/10, glasses/11, glasses/12, glasses/13, glasses/14, glasses/15, glasses/16, glasses/17, glasses/18, glasses/19, glasses/20, glasses/21, glasses/22, glasses/23, glasses/24, glasses/25, glasses/26, glasses/27, glasses/28, glasses/29, glasses/30\nOffhand (or null): offhand/1, offhand/2, offhand/3, offhand/4, offhand/5, offhand/6, offhand/7, offhand/8, offhand/9, offhand/10, offhand/11, offhand/12, offhand/13, offhand/14, offhand/15, offhand/16, offhand/17, offhand/18, offhand/19, offhand/20, offhand/21, offhand/22, offhand/23, offhand/24, offhand/25, offhand/26, offhand/27, offhand/28, offhand/29, offhand/30, offhand/31, offhand/32, offhand/33, offhand/34, offhand/35, offhand/36, offhand/37, offhand/38, offhand/39, offhand/40, offhand/kite_shield, offhand/heater_shield, offhand/round_shield, offhand/tower_shield, offhand/buckler\nWeapon (or null): weapon/1, weapon/2, weapon/3, weapon/4, weapon/5, weapon/6, weapon/7, weapon/10, weapon/11, weapon/12, weapon/13, weapon/14, weapon/15, weapon/16, weapon/17, weapon/18, weapon/19, weapon/20, weapon/21, weapon/22, weapon/23, weapon/24, weapon/25, weapon/26, weapon/27, weapon/28, weapon/29, weapon/30, weapon/31, weapon/32, weapon/33, weapon/34, weapon/35, weapon/36, weapon/37, weapon/38, weapon/39, weapon/40, weapon/knight_sword, weapon/dark_blade, weapon/broadsword, weapon/flame_sword, weapon/dagger, weapon/battle_axe, weapon/double_axe, weapon/mace, weapon/warhammer, weapon/spear, weapon/wizard_staff, weapon/druid_staff, weapon/hunting_bow, weapon/elven_bow, weapon/pistol, weapon/crossbow, weapon/musket, weapon/rifle\nTop: top/1, top/2, top/3, top/4, top/5, top/6, top/7, top/8, top/9, top/10, top/11, top/12, top/13, top/14, top/15, top/16, top/17, top/18, top/19, top/20, top/21, top/22, top/23, top/24, top/25, top/26, top/27, top/28, top/29, top/30, top/31, top/32, top/33, top/34, top/35, top/36, top/37, top/38, top/39, top/40, top/41, top/42, top/43, top/44, top/45, top/46, top/47, top/48, top/49, top/50, top/51, top/52, top/53, top/54, top/55, top/56, top/57, top/58, top/59, top/60, top/plate_armor, top/chainmail, top/leather_armor, top/mage_robe\nSkin: skin/1\ncolors: Skin, Hair, Beard, Brows, Eyes (#rrggbb)\npictures (path WxH, y down): back/1 84x76; back/2 75x74; back/3 71x71; back/4 74x83; back/5 81x95; back/6 76x88; back/7 79x78; back/8 55x109; back/9 85x100; back/10 63x94; back/11 105x99; back/12 73x98; back/13 92x93; back/14 76x75; back/15 95x124; back/16 79x86; back/17 76x83; back/18 96x90; back/19 102x97; back/20 65x87; back/21 84x76; back/22 71x71; back/23 75x74; back/24 95x124; back/25 96x90; back/26 65x87; back/27 73x98; back/28 76x83; back/29 55x109; back/30 79x86; beard/1 43x27; beard/2 42x39; beard/3 89x30; beard/4 85x46; beard/5 73x44; beard/6 39x42; beard/7 54x20; beard/8 80x18; beard/9 29x25; beard/10 88x44; boots/1_near 31x20; boots/1_far 31x20; boots/2_near 30x23; boots/2_far 30x23; boots/3_near 32x23; boots/3_far 32x23; boots/4_near 32x22; boots/4_far 32x22; boots/5_near 26x30; boots/5_far 26x30; boots/6_near 30x18; boots/6_far 30x18; boots/7_near 33x25; boots/7_far 33x25; boots/8_near 32x20; boots/8_far 32x20; boots/9_near 32x20; boots/9_far 32x20; boots/10_near 35x19; boots/10_far 35x19; boots/11_near 32x26; boots/11_far 32x26; boots/12_near 30x22; boots/12_far 30x22; boots/13_near 28x23; boots/13_far 28x23; boots/14_near 31x17; boots/14_far 31x17; boots/15_near 28x28; boots/15_far 28x28; boots/16_near 30x25; boots/16_far 30x25; boots/17_near 30x24; boots/17_far 30x24; boots/18_near 31x18; boots/18_far 31x18; boots/19_near 28x22; boots/19_far 28x22; boots/20_near 35x24; boots/20_far 35x24; boots/21_near 31x20; boots/21_far 31x20; boots/22_near 28x22; boots/22_far 28x22; boots/23_near 28x22; boots/23_far 28x22; boots/24_near 28x28; boots/24_far 28x28; boots/25_near 28x28; boots/25_far 28x28; boots/26_near 30x24; boots/26_far 30x24; boots/27_near 26x30; boots/27_far 26x30; boots/28_near 26x30; boots/28_far 26x30; boots/29_near 30x23; boots/29_far 30x23; boots/30_near 32x20; boots/30_far 32x20; pants/1 79x26; pants/1_near 25x28; pants/1_far 25x28; pants/2 79x26; pants/2_near 25x28; pants/2_far 25x28; pants/3 79x26; pants/3_near 25x41; pants/3_far 25x41; pants/4 79x26; pants/4_near 31x28; pants/4_far 31x28; pants/5 79x26; pants/5_near 36x42; pants/5_far 38x43; pants/6 92x40; pants/7 92x40; pants/8 93x46; pants/9 92x41; pants/10 92x40; pants/11 88x40; pants/12 79x26; pants/13 79x26; pants/13_near 25x41; pants/13_far 25x41; pants/14 79x26; pants/14_near 36x42; pants/14_far 38x43; pants/15 79x26; pants/15_near 25x41; pants/15_far 25x41; pants/16 79x26; pants/16_near 25x28; pants/16_far 25x40; pants/17 92x39; pants/17_near 25x41; pants/17_far 25x41; pants/18 96x36; pants/19 79x26; pants/19_near 25x41; pants/19_far 25x41; pants/20 79x26; pants/20_near 25x28; pants/20_far 25x28; pants/21 79x26; pants/21_near 25x41; pants/21_far 25x41; pants/22 79x26; pants/22_near 25x41; pants/22_far 25x41; pants/23 79x26; pants/23_near 25x41; pants/23_far 25x41; pants/24 79x26; pants/24_near 25x41; pants/24_far 25x41; pants/25 79x26; pants/25_near 25x41; pants/25_far 25x41; pants/26 82x37; pants/27 79x26; pants/27_near 25x41; pants/27_far 25x41; pants/28 79x26; pants/28_near 25x28; pants/28_far 25x28; pants/29 79x26; pants/29_near 25x28; pants/29_far 25x40; pants/30 79x26; pants/30_near 25x41; pants/30_far 25x41; pants/31 79x26; pants/31_near 25x41; pants/31_far 25x41; pants/32 79x26; pants/32_near 31x28; pants/32_far 31x28; pants/33 79x26; pants/33_near 31x28; pants/33_far 31x28; pants/34 79x26; pants/34_near 31x28; pants/34_far 31x28; pants/35 79x26; pants/35_near 31x28; pants/35_far 31x28; pants/36 92x39; pants/37 79x26; pants/37_near 25x41; pants/37_far 25x41; pants/38 79x26; pants/38_near 25x28; pants/38_far 25x28; pants/39 79x26; pants/39_near 25x28; pants/39_far 25x28; pants/40 79x26; pants/40_near 25x41; pants/40_far 25x41; pants/41 79x26; pants/41_near 25x41; pants/41_far 25x41; pants/42 82x37; pants/43 82x37; pants/44 88x40; pants/45 79x26; pants/45_near 36x42; pants/45_far 38x43; pants/46 79x26; pants/46_near 36x42; pants/46_far 38x43; pants/47 96x36; pants/47_near 25x41; pants/47_far 25x41; pants/48 96x38; pants/49 79x26; pants/50 79x26; pants/50_near 31x28; pants/50_far 31x28; brows/1 51x11; brows/2 65x18; brows/3 61x16; brows/4 66x19; brows/5 72x23; brows/6 44x10; brows/7 62x18; brows/8 57x18; brows/9 53x13; brows/10 17x23; eyes/1 61x20; eyes/2 63x22; eyes/3 66x30; eyes/4 64x31; eyes/5 64x24; eyes/6 63x28; eyes/7 69x29; eyes/8 44x22; eyes/9 48x20; eyes/10 61x23; eyes/11 60x21; eyes/12 65x27; eyes/13 59x21; eyes/14 62x22; eyes/15 63x23; eyes/16 59x26; eyes/17 55x22; eyes/18 57x6; eyes/19 59x23; eyes/20 59x26; glasses/1 85x48; glasses/2 71x30; glasses/3 90x38; glasses/4 96x40; glasses/5 80x44; glasses/6 91x41; glasses/7 80x35; glasses/8 58x27; glasses/9 84x47; glasses/10 81x58; glasses/11 72x21; glasses/12 83x36; glasses/13 81x24; glasses/14 72x37; glasses/15 83x44; glasses/16 86x39; glasses/17 89x58; glasses/18 74x39; glasses/19 89x37; glasses/20 90x54; glasses/21 79x40; glasses/22 71x32; glasses/23 82x38; glasses/24 77x34; glasses/25 73x34; glasses/26 70x33; glasses/27 74x64; glasses/28 86x77; glasses/29 72x41; glasses/30 71x51; offhand/1 101x83; offhand/2 79x87; offhand/3 87x87; offhand/4 96x81; offhand/5 109x87; offhand/6 73x73; offhand/7 91x70; offhand/8 89x89; offhand/9 95x70; offhand/10 85x82; offhand/11_low 56x56; offhand/12_low 56x56; offhand/13_low 65x64; offhand/14_low 61x46; offhand/15_low 78x36; offhand/16_low 65x30; offhand/17_low 55x44; offhand/18_low 37x37; offhand/19_low 46x43; offhand/20_low 43x41; offhand/21_low 56x44; offhand/22_low 56x66; offhand/23_low 49x72; offhand/24_low 56x55; offhand/25_low 77x51; offhand/26_low 94x50; offhand/27_low 78x59; offhand/28_low 63x35; offhand/29_low 78x35; offhand/30_low 46x59; offhand/31_low 53x44; offhand/32_low 57x54; offhand/33_low 65x29; offhand/34_low 92x34; offhand/35_low 54x48; offhand/36_low 83x31; offhand/37_low 51x42; offhand/38_low 65x32; offhand/39_low 53x37; offhand/40_low 53x37; weapon/1 111x77; weapon/2 112x92; weapon/3 115x60; weapon/4 96x78; weapon/5 144x71; weapon/6 135x43; weapon/7 141x40; weapon/10 100x66; weapon/11 101x87; weapon/12 124x58; weapon/13 144x33; weapon/14 131x45; weapon/15 143x60; weapon/16 142x44; weapon/17 133x35; weapon/18 103x65; weapon/19 132x39; weapon/20 133x33; weapon/21 112x29; weapon/22 116x45; weapon/23 88x58; weapon/24 137x28; weapon/25 153x43; weapon/26 160x50; weapon/27 156x51; weapon/28 174x47; weapon/29 160x65; weapon/30 147x40; weapon/31 112x92; weapon/32 115x59; weapon/33 157x51; weapon/34 143x44; weapon/35 143x44; weapon/36 175x47; weapon/37 124x58; weapon/38 135x43; weapon/39 143x60; weapon/40 87x58; gloves/1_near 28x25; gloves/1_far 27x25; gloves/2_near 34x35; gloves/2_far 32x35; gloves/3_near 28x25; gloves/3_far 27x25; gloves/4_near 31x34; gloves/4_far 32x33; gloves/5_near 36x36; gloves/5_far 35x36; gloves/6_near 31x32; gloves/6_far 32x33; gloves/7_near 35x34; gloves/7_far 34x34; gloves/8_near 30x14; gloves/8_far 28x14; gloves/9_near 38x18; gloves/9_far 38x18; gloves/10_near 34x35; gloves/10_far 32x35; gloves/11_near 30x23; gloves/11_far 28x23; gloves/12_near 33x35; gloves/12_far 33x36; gloves/13_near 36x36; gloves/13_far 32x37; gloves/14_near 32x27; gloves/14_far 29x24; gloves/15_near 36x40; gloves/15_far 35x40; gloves/16_near 31x36; gloves/16_far 30x37; gloves/17_near 31x36; gloves/17_far 30x37; gloves/18_near 32x31; gloves/18_far 30x30; gloves/19_near 32x34; gloves/19_far 29x34; gloves/20_near 34x38; gloves/20_far 31x38; gloves/21_near 28x25; gloves/21_far 27x25; gloves/22_near 28x25; gloves/22_far 27x25; gloves/23_near 36x40; gloves/23_far 35x40; gloves/24_near 36x40; gloves/24_far 35x40; gloves/25_near 36x36; gloves/25_far 32x37; gloves/26_near 30x14; gloves/26_far 28x14; gloves/27_near 38x18; gloves/27_far 38x18; gloves/28_near 38x18; gloves/28_far 38x18; gloves/29_near 31x34; gloves/29_far 32x33; gloves/30_near 32x27; gloves/30_far 29x24; hair_under/1 87x54; hair_under/2 111x60; hair_under/3 97x54; hair_under/4 103x55; hair_under/5 101x64; hair_under/6 96x54; hair_under/7 92x52; hair_under/8 123x75; hair_under/9 105x78; hair_under/10 130x72; hair_under/11 97x87; hair_under/12 126x64; hair_under/13 104x40; hair_under/14 96x55; hair_under/15 92x49; hair_under/16 95x55; hair_under/17 102x57; hair_under/18 118x69; hair_under/19 118x88; hair_under/20 96x52; hair_under/21 101x60; hair_under/22 90x53; hair_under/23 94x53; hair_under/24 122x66; hair_under/25 118x67; hair_under/26 98x55; hair_under/27 103x52; hair_under/28 122x79; hair_under/29 99x90; hair_under/30 116x96; hair/1 87x54; hair/2 113x61; hair/3 98x65; hair/4 119x94; hair/5 108x75; hair/6 115x70; hair/7 97x67; hair/8 147x120; hair/9 105x113; hair/10 136x84; hair/11 133x113; hair/12 134x74; hair/13 105x40; hair/14 114x80; hair/15 96x57; hair/16 97x86; hair/17 105x63; hair/18 128x83; hair/19 128x107; hair/20 97x59; hair/20_back 67x91; hair/21 101x65; hair/21_back 96x105; hair/22 114x71; hair/23 124x84; hair/24 127x74; hair/25 135x102; hair/26 103x68; hair/27 103x53; hair/28 125x86; hair/28_back 97x102; hair/29 99x111; hair/29_back 79x112; hair/30 119x114; hat/1 120x110; hat/1_back 95x59; hat/2 154x75; hat/3_low 126x82; hat/4 122x78; hat/5 118x120; hat/6 130x82; hat/7 121x105; hat/7_back 33x53; hat/8_low 136x75; hat/9_low 117x93; hat/10_low 140x94; hat/11_low 114x115; hat/12_low 145x69; hat/13_low 117x131; hat/14_low 140x120; hat/15 145x102; hat/16 129x97; hat/16_back 36x56; hat/17_low 126x84; hat/18 142x93; hat/19_low 122x106; hat/20_low 106x71; hat/20_back 68x44; hat/21_low 117x75; hat/22_low 123x94; hat/22_back 37x46; hat/23 137x110; hat/23_back 134x64; hat/24 133x81; hat/25 126x100; hat/25_back 43x60; hat/26 108x70; hat/26_back 46x52; hat/27_low 133x83; hat/28 126x107; hat/28_back 112x56; hat/29_low 124x117; hat/30_low 117x78; hat/31_low 136x75; hat/32 120x110; hat/32_back 95x59; hat/33_low 145x69; hat/34_low 106x71; hat/34_back 68x44; hat/35_low 106x71; hat/35_back 68x44; hat/36 154x75; hat/37 133x81; hat/38 133x81; hat/39 118x120; hat/40_low 117x75; hat/41_low 117x75; hat/42_low 117x93; hat/43_low 123x94; hat/43_back 37x46; hat/44_low 133x79; hat/45_low 114x115; hat/46_low 114x115; hat/47_low 140x94; hat/48 122x78; hat/49_low 107x70; hat/49_back 46x52; hat/50_low 124x117; hat/51_low 117x131; hat/52 126x107; hat/52_back 112x56; hat/53_low 140x120; hat/54_low 140x120; hat/55 137x110; hat/55_back 134x64; hat/56 137x110; hat/56_back 134x64; hat/57 130x82; hat/58 129x97; hat/58_back 36x56; hat/59 121x105; hat/59_back 33x53; hat/60 145x102; mouth/1 15x7; mouth/2 27x19; mouth/3 23x12; mouth/4 24x11; mouth/5 33x16; mouth/6 20x18; mouth/7 20x19; mouth/8 23x13; mouth/9 24x19; mouth/10 23x15; skin/1_arm_near 23x72; skin/1_arm_far 29x71; skin/1_torso 77x50; skin/1_head 103x90; skin/1_leg 27x55; top/1 87x71; top/2 91x88; top/2_sleeve_near 37x34; top/2_sleeve_far 34x35; top/3 90x88; top/3_sleeve_near 31x64; top/3_sleeve_far 32x63; top/4 87x88; top/4_sleeve_near 37x34; top/4_sleeve_far 34x35; top/5 87x81; top/5_sleeve_near 37x34; top/5_sleeve_far 34x35; top/6 88x82; top/6_sleeve_near 29x37; top/6_sleeve_far 29x38; top/7 87x83; top/7_sleeve_near 29x37; top/7_sleeve_far 29x38; top/8 87x80; top/9 87x81; top/9_sleeve_near 28x58; top/9_cap_near 37x34; top/9_sleeve_far 34x59; top/9_cap_far 34x36; top/10 56x59; top/10_coat 88x106; top/10_sleeve_near 28x58; top/10_sleeve_far 32x59; top/11 87x72; top/11_sleeve_near 37x34; top/11_sleeve_far 34x35; top/12 86x81; top/12_sleeve_near 28x58; top/12_sleeve_far 32x59; top/13 88x82; top/13_sleeve_near 28x58; top/13_sleeve_far 32x59; top/14 87x81; top/15 90x88; top/15_sleeve_near 31x64; top/15_sleeve_far 32x63; top/16 86x84; top/16_sleeve_near 31x59; top/16_sleeve_far 34x59; top/17 87x83; top/17_sleeve_near 34x37; top/17_sleeve_far 33x36; top/18 89x74; top/18_sleeve_near 37x34; top/18_sleeve_far 34x35; top/19 89x92; top/19_sleeve_near 37x34; top/19_sleeve_far 34x35; top/20 87x71; top/20_sleeve_near 28x58; top/20_sleeve_far 32x59; top/21 87x71; top/21_sleeve_near 28x58; top/21_sleeve_far 32x59; top/22 87x81; top/22_sleeve_near 28x58; top/22_sleeve_far 32x59; top/23 87x81; top/23_sleeve_near 28x58; top/23_sleeve_far 32x59; top/24 87x81; top/24_sleeve_near 28x58; top/24_sleeve_far 32x59; top/25 88x72; top/25_sleeve_near 28x58; top/25_sleeve_far 32x59; top/26 92x93; top/27 87x82; top/27_sleeve_near 28x58; top/27_sleeve_far 32x59; top/28 86x74; top/29 63x65; top/29_coat 88x106; top/29_sleeve_near 28x58; top/29_sleeve_far 32x59; top/30 87x80; top/30_sleeve_near 28x58; top/30_sleeve_far 32x59; top/31 90x88; top/31_sleeve_near 31x64; top/31_sleeve_far 32x63; top/32 88x82; top/32_sleeve_near 28x58; top/32_sleeve_far 32x59; top/33 87x81; top/33_sleeve_near 37x34; top/33_sleeve_far 34x35; top/34 87x81; top/34_sleeve_near 37x34; top/34_sleeve_far 34x35; top/35 87x81; top/36 87x71; top/36_sleeve_near 28x58; top/36_sleeve_far 32x59; top/37 87x81; top/37_sleeve_near 29x37; top/37_sleeve_far 29x38; top/38 87x82; top/38_sleeve_near 28x58; top/38_sleeve_far 32x59; top/39 87x83; top/39_sleeve_near 34x37; top/39_sleeve_far 33x36; top/40 90x88; top/40_sleeve_near 31x64; top/40_sleeve_far 32x63; top/41 91x87; top/41_sleeve_near 31x64; top/41_sleeve_far 32x63; top/42 87x81; top/42_sleeve_near 28x58; top/42_sleeve_far 32x59; top/43 87x79; top/43_sleeve_near 29x37; top/43_sleeve_far 29x38; top/44 87x72; top/44_sleeve_near 31x64; top/44_sleeve_far 32x63; top/45 87x88; top/45_sleeve_near 28x58; top/45_sleeve_far 32x59; top/46 87x72; top/46_sleeve_near 28x58; top/46_cap_near 37x34; top/46_sleeve_far 32x59; top/46_cap_far 34x35; top/47 88x82; top/47_sleeve_near 29x37; top/47_sleeve_far 29x38; top/48 87x71; top/48_sleeve_near 28x58; top/48_sleeve_far 32x59; top/49 86x81; top/49_sleeve_near 28x58; top/49_sleeve_far 32x59; top/50 89x74; top/50_sleeve_near 37x34; top/50_sleeve_far 34x35; top/51 87x72; top/51_sleeve_near 31x64; top/51_sleeve_far 32x63; top/52 87x72; top/52_sleeve_near 29x37; top/52_sleeve_far 29x38; top/53 87x72; top/53_sleeve_near 29x37; top/53_sleeve_far 29x38; top/54 89x92; top/54_sleeve_near 37x34; top/54_sleeve_far 34x35; top/55 87x81; top/56 86x84; top/56_sleeve_near 31x59; top/56_sleeve_far 34x59; top/57 87x80; top/57_sleeve_near 28x58; top/57_sleeve_far 32x59; top/58 92x93; top/59 90x88; top/59_sleeve_near 31x64; top/59_sleeve_far 32x63; top/60 87x80; weapon/knight_sword 156x51; weapon/dark_blade 160x50; weapon/broadsword 160x65; weapon/flame_sword 174x47; weapon/dagger 112x29; weapon/battle_axe 115x60; weapon/double_axe 112x92; weapon/mace 144x71; weapon/warhammer 103x65; weapon/spear 147x40; weapon/wizard_staff 135x43; weapon/druid_staff 144x33; offhand/kite_shield 85x82; offhand/heater_shield 96x81; offhand/round_shield 89x89; offhand/tower_shield 109x87; offhand/buckler 73x73; hat/great_helm 118x120; hat/knight_helm_low 114x115; hat/winged_helm_low 133x83; hat/wizard_hat 154x75; hat/ranger_hood_low 114x115; hat/iron_helm 130x82; top/plate_armor 87x88; top/plate_armor_sleeve_near 37x34; top/plate_armor_sleeve_far 34x35; top/chainmail 91x88; top/chainmail_sleeve_near 37x34; top/chainmail_sleeve_far 34x35; top/leather_armor 89x92; top/leather_armor_sleeve_near 37x34; top/leather_armor_sleeve_far 34x35; top/mage_robe 89x74; top/mage_robe_sleeve_near 37x34; top/mage_robe_sleeve_far 34x35; gloves/gauntlets_near 34x35; gloves/gauntlets_far 32x35; gloves/bracers_near 36x40; gloves/bracers_far 35x40; boots/plate_boots_near 30x23; boots/plate_boots_far 30x23; boots/leather_boots_near 26x30; boots/leather_boots_far 26x30; back/knight_cape 95x124; back/mage_cape 95x124; weapon/hunting_bow 70x112; weapon/hunting_bow_drawn 70x112; weapon/hunting_bow_shot 70x112; weapon/elven_bow 72x128; weapon/elven_bow_drawn 72x128; weapon/elven_bow_shot 72x128; weapon/pistol 96x40; weapon/pistol_shot 96x40; weapon/crossbow 104x56; weapon/crossbow_shot 104x56; weapon/musket 180x40; weapon/musket_shot 180x40; weapon/rifle 160x52; weapon/rifle_shot 160x52",
|
|
9663
|
+
"system": "You design a 2D cartoon hero for a short video: a layered chibi character on a skeleton, seen from the side. Reply with ONE JSON object — a hero recipe — and nothing else: no markdown, no comments.\n\n{ \"name\": string (≤ 40 chars, in the player's language), \"description\": string (≤ 300 chars),\n \"look\": { \"parts\": { \"<part type>\": \"<part id>\" | null, … }, \"colors\": { \"<colour target>\": \"#rrggbb\", … } },\n \"art\": { \"<picture path>\": { \"svg\": \"<svg markup without the svg wrapper>\" }, … } (optional) }\n\n- Pick parts ONLY from the lists below; null takes a part off where it is allowed. Choose colours that make the hero instantly readable.\n- \"art\" redraws a picture of a part you picked with your own SVG of exactly that canvas (width × height in px, y down): only <path>, <circle>, <ellipse>, <rect>, <line>, <polygon>, <polyline>, <g>, gradients and clipPath — no text, no images, no links, no scripts; ≤ 12000 characters each, ≤ 16 pictures. Draw in the same style: a heavy dark outline (#1f1a21, width 3–4), flat fills with one darker shade. Use it only when no part fits the idea (a chef's apron, a superhero suit).\n\nParts and pictures of the rig:\nBack (or null): back/1, back/2, back/3, back/4, back/5, back/6, back/7, back/8, back/9, back/10, back/11, back/12, back/13, back/14, back/15, back/16, back/17, back/18, back/19, back/20, back/21, back/22, back/23, back/24, back/25, back/26, back/27, back/28, back/29, back/30, back/knight_cape, back/mage_cape\nBeard (or null): beard/1, beard/2, beard/3, beard/4, beard/5, beard/6, beard/7, beard/8, beard/9, beard/10\nBoots (or null): boots/1, boots/2, boots/3, boots/4, boots/5, boots/6, boots/7, boots/8, boots/9, boots/10, boots/11, boots/12, boots/13, boots/14, boots/15, boots/16, boots/17, boots/18, boots/19, boots/20, boots/21, boots/22, boots/23, boots/24, boots/25, boots/26, boots/27, boots/28, boots/29, boots/30, boots/plate_boots, boots/leather_boots\nPants (or null): pants/1, pants/2, pants/3, pants/4, pants/5, pants/6, pants/7, pants/8, pants/9, pants/10, pants/11, pants/12, pants/13, pants/14, pants/15, pants/16, pants/17, pants/18, pants/19, pants/20, pants/21, pants/22, pants/23, pants/24, pants/25, pants/26, pants/27, pants/28, pants/29, pants/30, pants/31, pants/32, pants/33, pants/34, pants/35, pants/36, pants/37, pants/38, pants/39, pants/40, pants/41, pants/42, pants/43, pants/44, pants/45, pants/46, pants/47, pants/48, pants/49, pants/50\nBrows: brows/1, brows/2, brows/3, brows/4, brows/5, brows/6, brows/7, brows/8, brows/9, brows/10\nEyes: eyes/1, eyes/2, eyes/3, eyes/4, eyes/5, eyes/6, eyes/7, eyes/8, eyes/9, eyes/10, eyes/11, eyes/12, eyes/13, eyes/14, eyes/15, eyes/16, eyes/17, eyes/18, eyes/19, eyes/20\nGloves (or null): gloves/1, gloves/2, gloves/3, gloves/4, gloves/5, gloves/6, gloves/7, gloves/8, gloves/9, gloves/10, gloves/11, gloves/12, gloves/13, gloves/14, gloves/15, gloves/16, gloves/17, gloves/18, gloves/19, gloves/20, gloves/21, gloves/22, gloves/23, gloves/24, gloves/25, gloves/26, gloves/27, gloves/28, gloves/29, gloves/30, gloves/gauntlets, gloves/bracers\nHair (or null): hair/1, hair/2, hair/3, hair/4, hair/5, hair/6, hair/7, hair/8, hair/9, hair/10, hair/11, hair/12, hair/13, hair/14, hair/15, hair/16, hair/17, hair/18, hair/19, hair/20, hair/21, hair/22, hair/23, hair/24, hair/25, hair/26, hair/27, hair/28, hair/29, hair/30\nHat (or null): hat/1, hat/2, hat/3, hat/4, hat/5, hat/6, hat/7, hat/8, hat/9, hat/10, hat/11, hat/12, hat/13, hat/14, hat/15, hat/16, hat/17, hat/18, hat/19, hat/20, hat/21, hat/22, hat/23, hat/24, hat/25, hat/26, hat/27, hat/28, hat/29, hat/30, hat/31, hat/32, hat/33, hat/34, hat/35, hat/36, hat/37, hat/38, hat/39, hat/40, hat/41, hat/42, hat/43, hat/44, hat/45, hat/46, hat/47, hat/48, hat/49, hat/50, hat/51, hat/52, hat/53, hat/54, hat/55, hat/56, hat/57, hat/58, hat/59, hat/60, hat/great_helm, hat/knight_helm, hat/winged_helm, hat/wizard_hat, hat/ranger_hood, hat/iron_helm\nMouth: mouth/1, mouth/2, mouth/3, mouth/4, mouth/5, mouth/6, mouth/7, mouth/8, mouth/9, mouth/10\nGlasses (or null): glasses/1, glasses/2, glasses/3, glasses/4, glasses/5, glasses/6, glasses/7, glasses/8, glasses/9, glasses/10, glasses/11, glasses/12, glasses/13, glasses/14, glasses/15, glasses/16, glasses/17, glasses/18, glasses/19, glasses/20, glasses/21, glasses/22, glasses/23, glasses/24, glasses/25, glasses/26, glasses/27, glasses/28, glasses/29, glasses/30\nOffhand (or null): offhand/1, offhand/2, offhand/3, offhand/4, offhand/5, offhand/6, offhand/7, offhand/8, offhand/9, offhand/10, offhand/11, offhand/12, offhand/13, offhand/14, offhand/15, offhand/16, offhand/17, offhand/18, offhand/19, offhand/20, offhand/21, offhand/22, offhand/23, offhand/24, offhand/25, offhand/26, offhand/27, offhand/28, offhand/29, offhand/30, offhand/31, offhand/32, offhand/33, offhand/34, offhand/35, offhand/36, offhand/37, offhand/38, offhand/39, offhand/40, offhand/kite_shield, offhand/heater_shield, offhand/round_shield, offhand/tower_shield, offhand/buckler\nWeapon (or null): weapon/1, weapon/2, weapon/3, weapon/4, weapon/5, weapon/6, weapon/7, weapon/10, weapon/11, weapon/12, weapon/13, weapon/14, weapon/15, weapon/16, weapon/17, weapon/18, weapon/19, weapon/20, weapon/21, weapon/22, weapon/23, weapon/24, weapon/25, weapon/26, weapon/27, weapon/28, weapon/29, weapon/30, weapon/31, weapon/32, weapon/33, weapon/34, weapon/35, weapon/36, weapon/37, weapon/38, weapon/39, weapon/40, weapon/knight_sword, weapon/dark_blade, weapon/broadsword, weapon/flame_sword, weapon/dagger, weapon/battle_axe, weapon/double_axe, weapon/mace, weapon/warhammer, weapon/spear, weapon/wizard_staff, weapon/druid_staff, weapon/hunting_bow, weapon/elven_bow, weapon/pistol, weapon/crossbow, weapon/musket, weapon/rifle\nTop: top/1, top/2, top/3, top/4, top/5, top/6, top/7, top/8, top/9, top/10, top/11, top/12, top/13, top/14, top/15, top/16, top/17, top/18, top/19, top/20, top/21, top/22, top/23, top/24, top/25, top/26, top/27, top/28, top/29, top/30, top/31, top/32, top/33, top/34, top/35, top/36, top/37, top/38, top/39, top/40, top/41, top/42, top/43, top/44, top/45, top/46, top/47, top/48, top/49, top/50, top/51, top/52, top/53, top/54, top/55, top/56, top/57, top/58, top/59, top/60, top/plate_armor, top/chainmail, top/leather_armor, top/mage_robe\nSkin: skin/1\ncolors: Skin, Hair, Beard, Brows, Eyes (#rrggbb)\npictures (path WxH, y down): back/1 84x76; back/2 75x74; back/3 71x71; back/4 74x83; back/5 81x95; back/6 76x88; back/7 79x78; back/8 55x109; back/9 85x100; back/10 63x94; back/11 105x99; back/12 73x98; back/13 92x93; back/14 76x75; back/15 95x124; back/16 79x86; back/17 76x83; back/18 96x90; back/19 102x97; back/20 65x87; back/21 84x76; back/22 71x71; back/23 75x74; back/24 95x124; back/25 96x90; back/26 65x87; back/27 73x98; back/28 76x83; back/29 55x109; back/30 79x86; beard/1 43x27; beard/2 42x39; beard/3 89x30; beard/4 85x46; beard/5 73x44; beard/6 39x42; beard/7 54x20; beard/8 80x18; beard/9 29x25; beard/10 88x44; boots/1_near 31x20; boots/1_far 31x20; boots/2_near 30x23; boots/2_far 30x23; boots/3_near 32x23; boots/3_far 32x23; boots/4_near 32x22; boots/4_far 32x22; boots/5_near 26x30; boots/5_far 26x30; boots/6_near 30x18; boots/6_far 30x18; boots/7_near 33x25; boots/7_far 33x25; boots/8_near 32x20; boots/8_far 32x20; boots/9_near 32x20; boots/9_far 32x20; boots/10_near 35x19; boots/10_far 35x19; boots/11_near 32x26; boots/11_far 32x26; boots/12_near 30x22; boots/12_far 30x22; boots/13_near 28x23; boots/13_far 28x23; boots/14_near 31x17; boots/14_far 31x17; boots/15_near 28x28; boots/15_far 28x28; boots/16_near 30x25; boots/16_far 30x25; boots/17_near 30x24; boots/17_far 30x24; boots/18_near 31x18; boots/18_far 31x18; boots/19_near 28x22; boots/19_far 28x22; boots/20_near 35x24; boots/20_far 35x24; boots/21_near 31x20; boots/21_far 31x20; boots/22_near 28x22; boots/22_far 28x22; boots/23_near 28x22; boots/23_far 28x22; boots/24_near 28x28; boots/24_far 28x28; boots/25_near 28x28; boots/25_far 28x28; boots/26_near 30x24; boots/26_far 30x24; boots/27_near 26x30; boots/27_far 26x30; boots/28_near 26x30; boots/28_far 26x30; boots/29_near 30x23; boots/29_far 30x23; boots/30_near 32x20; boots/30_far 32x20; pants/1 79x26; pants/1_near 25x28; pants/1_far 25x28; pants/2 79x26; pants/2_near 25x28; pants/2_far 25x28; pants/3 79x26; pants/3_near 25x41; pants/3_far 25x41; pants/4 79x26; pants/4_near 31x28; pants/4_far 31x28; pants/5 79x26; pants/5_near 36x42; pants/5_far 38x43; pants/6 92x40; pants/7 92x40; pants/8 93x46; pants/9 92x41; pants/10 92x40; pants/11 88x40; pants/12 79x26; pants/13 79x26; pants/13_near 25x41; pants/13_far 25x41; pants/14 79x26; pants/14_near 36x42; pants/14_far 38x43; pants/15 79x26; pants/15_near 25x41; pants/15_far 25x41; pants/16 79x26; pants/16_near 25x28; pants/16_far 25x40; pants/17 92x39; pants/17_near 25x41; pants/17_far 25x41; pants/18 96x36; pants/19 79x26; pants/19_near 25x41; pants/19_far 25x41; pants/20 79x26; pants/20_near 25x28; pants/20_far 25x28; pants/21 79x26; pants/21_near 25x41; pants/21_far 25x41; pants/22 79x26; pants/22_near 25x41; pants/22_far 25x41; pants/23 79x26; pants/23_near 25x41; pants/23_far 25x41; pants/24 79x26; pants/24_near 25x41; pants/24_far 25x41; pants/25 79x26; pants/25_near 25x41; pants/25_far 25x41; pants/26 82x37; pants/27 79x26; pants/27_near 25x41; pants/27_far 25x41; pants/28 79x26; pants/28_near 25x28; pants/28_far 25x28; pants/29 79x26; pants/29_near 25x28; pants/29_far 25x40; pants/30 79x26; pants/30_near 25x41; pants/30_far 25x41; pants/31 79x26; pants/31_near 25x41; pants/31_far 25x41; pants/32 79x26; pants/32_near 31x28; pants/32_far 31x28; pants/33 79x26; pants/33_near 31x28; pants/33_far 31x28; pants/34 79x26; pants/34_near 31x28; pants/34_far 31x28; pants/35 79x26; pants/35_near 31x28; pants/35_far 31x28; pants/36 92x39; pants/37 79x26; pants/37_near 25x41; pants/37_far 25x41; pants/38 79x26; pants/38_near 25x28; pants/38_far 25x28; pants/39 79x26; pants/39_near 25x28; pants/39_far 25x28; pants/40 79x26; pants/40_near 25x41; pants/40_far 25x41; pants/41 79x26; pants/41_near 25x41; pants/41_far 25x41; pants/42 82x37; pants/43 82x37; pants/44 88x40; pants/45 79x26; pants/45_near 36x42; pants/45_far 38x43; pants/46 79x26; pants/46_near 36x42; pants/46_far 38x43; pants/47 96x36; pants/47_near 25x41; pants/47_far 25x41; pants/48 96x38; pants/49 79x26; pants/50 79x26; pants/50_near 31x28; pants/50_far 31x28; brows/1 51x11; brows/2 65x18; brows/3 61x16; brows/4 66x19; brows/5 72x23; brows/6 44x10; brows/7 62x18; brows/8 57x18; brows/9 53x13; brows/10 17x23; eyes/1 61x20; eyes/2 63x22; eyes/3 66x30; eyes/4 64x31; eyes/5 64x24; eyes/6 63x28; eyes/7 69x29; eyes/8 44x22; eyes/9 48x20; eyes/10 61x23; eyes/11 60x21; eyes/12 65x27; eyes/13 59x21; eyes/14 62x22; eyes/15 63x23; eyes/16 59x26; eyes/17 55x22; eyes/18 57x6; eyes/19 59x23; eyes/20 59x26; glasses/1 85x48; glasses/2 71x30; glasses/3 90x38; glasses/4 96x40; glasses/5 80x44; glasses/6 91x41; glasses/7 80x35; glasses/8 58x27; glasses/9 84x47; glasses/10 81x58; glasses/11 72x21; glasses/12 83x36; glasses/13 81x24; glasses/14 72x37; glasses/15 83x44; glasses/16 86x39; glasses/17 89x58; glasses/18 74x39; glasses/19 89x37; glasses/20 90x54; glasses/21 79x40; glasses/22 71x32; glasses/23 82x38; glasses/24 77x34; glasses/25 73x34; glasses/26 70x33; glasses/27 74x64; glasses/28 86x77; glasses/29 72x41; glasses/30 71x51; offhand/1 101x83; offhand/2 79x87; offhand/3 87x87; offhand/4 96x81; offhand/5 109x87; offhand/6 73x73; offhand/7 91x70; offhand/8 89x89; offhand/9 95x70; offhand/10 85x82; offhand/11_low 56x56; offhand/12_low 56x56; offhand/13_low 65x64; offhand/14_low 61x46; offhand/15_low 78x36; offhand/16_low 65x30; offhand/17_low 55x44; offhand/18_low 37x37; offhand/19_low 46x43; offhand/20_low 43x41; offhand/21_low 56x44; offhand/22_low 56x66; offhand/23_low 49x72; offhand/24_low 56x55; offhand/25_low 77x51; offhand/26_low 94x50; offhand/27_low 78x59; offhand/28_low 63x35; offhand/29_low 78x35; offhand/30_low 46x59; offhand/31_low 53x44; offhand/32_low 57x54; offhand/33_low 65x29; offhand/34_low 92x34; offhand/35_low 54x48; offhand/36_low 83x31; offhand/37_low 51x42; offhand/38_low 65x32; offhand/39_low 53x37; offhand/40_low 53x37; weapon/1 111x77; weapon/2 112x92; weapon/3 115x60; weapon/4 96x78; weapon/5 144x71; weapon/6 135x43; weapon/7 141x40; weapon/10 100x66; weapon/11 101x87; weapon/12 124x58; weapon/13 144x33; weapon/14 131x45; weapon/15 143x60; weapon/16 142x44; weapon/17 133x35; weapon/18 103x65; weapon/19 132x39; weapon/20 133x33; weapon/21 112x29; weapon/22 116x45; weapon/23 88x58; weapon/24 137x28; weapon/25 153x43; weapon/26 160x50; weapon/27 156x51; weapon/28 174x47; weapon/29 160x65; weapon/30 147x40; weapon/31 112x92; weapon/32 115x59; weapon/33 157x51; weapon/34 143x44; weapon/35 143x44; weapon/36 175x47; weapon/37 124x58; weapon/38 135x43; weapon/39 143x60; weapon/40 87x58; gloves/1_near 28x25; gloves/1_far 27x25; gloves/2_near 34x35; gloves/2_far 32x35; gloves/3_near 28x25; gloves/3_far 27x25; gloves/4_near 31x34; gloves/4_far 32x33; gloves/5_near 36x36; gloves/5_far 35x36; gloves/6_near 31x32; gloves/6_far 32x33; gloves/7_near 35x34; gloves/7_far 34x34; gloves/8_near 30x14; gloves/8_far 28x14; gloves/9_near 38x18; gloves/9_far 38x18; gloves/10_near 34x35; gloves/10_far 32x35; gloves/11_near 30x23; gloves/11_far 28x23; gloves/12_near 33x35; gloves/12_far 33x36; gloves/13_near 36x36; gloves/13_far 32x37; gloves/14_near 32x27; gloves/14_far 29x24; gloves/15_near 36x40; gloves/15_far 35x40; gloves/16_near 31x36; gloves/16_far 30x37; gloves/17_near 31x36; gloves/17_far 30x37; gloves/18_near 32x31; gloves/18_far 30x30; gloves/19_near 32x34; gloves/19_far 29x34; gloves/20_near 34x38; gloves/20_far 31x38; gloves/21_near 28x25; gloves/21_far 27x25; gloves/22_near 28x25; gloves/22_far 27x25; gloves/23_near 36x40; gloves/23_far 35x40; gloves/24_near 36x40; gloves/24_far 35x40; gloves/25_near 36x36; gloves/25_far 32x37; gloves/26_near 30x14; gloves/26_far 28x14; gloves/27_near 38x18; gloves/27_far 38x18; gloves/28_near 38x18; gloves/28_far 38x18; gloves/29_near 31x34; gloves/29_far 32x33; gloves/30_near 32x27; gloves/30_far 29x24; hair_under/1 87x54; hair_under/2 111x60; hair_under/3 97x54; hair_under/4 103x55; hair_under/5 101x64; hair_under/6 96x54; hair_under/7 92x52; hair_under/8 123x75; hair_under/9 105x78; hair_under/10 130x72; hair_under/11 97x87; hair_under/12 126x64; hair_under/13 104x40; hair_under/14 96x55; hair_under/15 92x49; hair_under/16 95x55; hair_under/17 102x57; hair_under/18 118x69; hair_under/19 118x88; hair_under/20 96x52; hair_under/21 101x60; hair_under/22 90x53; hair_under/23 94x53; hair_under/24 122x66; hair_under/25 118x67; hair_under/26 98x55; hair_under/27 103x52; hair_under/28 122x79; hair_under/29 99x90; hair_under/30 116x96; hair/1 87x54; hair/2 113x61; hair/3 98x65; hair/4 119x94; hair/5 108x75; hair/6 115x70; hair/7 97x67; hair/8 147x120; hair/9 105x113; hair/10 136x84; hair/11 133x113; hair/12 134x74; hair/13 105x40; hair/14 114x80; hair/15 96x57; hair/16 97x86; hair/17 105x63; hair/18 128x83; hair/19 128x107; hair/20 97x59; hair/20_back 67x91; hair/21 101x65; hair/21_back 96x105; hair/22 114x71; hair/23 124x84; hair/24 127x74; hair/25 135x102; hair/26 103x68; hair/27 103x53; hair/28 125x86; hair/28_back 97x102; hair/29 99x111; hair/29_back 79x112; hair/30 119x114; hat/1 120x110; hat/1_back 95x59; hat/2 154x75; hat/3_low 126x82; hat/4 122x78; hat/5 118x120; hat/6 130x82; hat/7 121x105; hat/7_back 33x53; hat/8_low 136x75; hat/9_low 117x93; hat/10_low 140x94; hat/11_low 114x115; hat/12_low 145x69; hat/13_low 117x131; hat/14_low 140x120; hat/15 145x102; hat/16 129x97; hat/16_back 36x56; hat/17_low 126x84; hat/18 142x93; hat/19_low 122x106; hat/20_low 106x71; hat/20_back 68x44; hat/21_low 117x75; hat/22_low 123x94; hat/22_back 37x46; hat/23 137x110; hat/23_back 134x64; hat/24 133x81; hat/25 126x100; hat/25_back 43x60; hat/26 108x70; hat/26_back 46x52; hat/27_low 133x83; hat/28 126x107; hat/28_back 112x56; hat/29_low 124x117; hat/30_low 117x78; hat/31_low 136x75; hat/32 120x110; hat/32_back 95x59; hat/33_low 145x69; hat/34_low 106x71; hat/34_back 68x44; hat/35_low 106x71; hat/35_back 68x44; hat/36 154x75; hat/37 133x81; hat/38 133x81; hat/39 118x120; hat/40_low 117x75; hat/41_low 117x75; hat/42_low 117x93; hat/43_low 123x94; hat/43_back 37x46; hat/44_low 133x79; hat/45_low 114x115; hat/46_low 114x115; hat/47_low 140x94; hat/48 122x78; hat/49_low 107x70; hat/49_back 46x52; hat/50_low 124x117; hat/51_low 117x131; hat/52 126x107; hat/52_back 112x56; hat/53_low 140x120; hat/54_low 140x120; hat/55 137x110; hat/55_back 134x64; hat/56 137x110; hat/56_back 134x64; hat/57 130x82; hat/58 129x97; hat/58_back 36x56; hat/59 121x105; hat/59_back 33x53; hat/60 145x102; mouth/1 15x7; mouth/2 27x19; mouth/3 23x12; mouth/4 24x11; mouth/5 33x16; mouth/6 20x18; mouth/7 20x19; mouth/8 23x13; mouth/9 24x19; mouth/10 23x15; skin/1_arm_near 23x72; skin/1_arm_far 29x71; skin/1_torso 77x50; skin/1_head 103x90; skin/1_leg 27x55; race_skin/elf_head 103x90; race_skin/dwarf_head 103x90; race_skin/orc_head 103x90; race_skin/undead_head 103x90; ears/elf 103x90; ears/orc 103x90; tusks/1 28x18; tusks/2 28x18; top/1 87x71; top/2 91x88; top/2_sleeve_near 37x34; top/2_sleeve_far 34x35; top/3 90x88; top/3_sleeve_near 31x64; top/3_sleeve_far 32x63; top/4 87x88; top/4_sleeve_near 37x34; top/4_sleeve_far 34x35; top/5 87x81; top/5_sleeve_near 37x34; top/5_sleeve_far 34x35; top/6 88x82; top/6_sleeve_near 29x37; top/6_sleeve_far 29x38; top/7 87x83; top/7_sleeve_near 29x37; top/7_sleeve_far 29x38; top/8 87x80; top/9 87x81; top/9_sleeve_near 28x58; top/9_cap_near 37x34; top/9_sleeve_far 34x59; top/9_cap_far 34x36; top/10 56x59; top/10_coat 88x106; top/10_sleeve_near 28x58; top/10_sleeve_far 32x59; top/11 87x72; top/11_sleeve_near 37x34; top/11_sleeve_far 34x35; top/12 86x81; top/12_sleeve_near 28x58; top/12_sleeve_far 32x59; top/13 88x82; top/13_sleeve_near 28x58; top/13_sleeve_far 32x59; top/14 87x81; top/15 90x88; top/15_sleeve_near 31x64; top/15_sleeve_far 32x63; top/16 86x84; top/16_sleeve_near 31x59; top/16_sleeve_far 34x59; top/17 87x83; top/17_sleeve_near 34x37; top/17_sleeve_far 33x36; top/18 89x74; top/18_sleeve_near 37x34; top/18_sleeve_far 34x35; top/19 89x92; top/19_sleeve_near 37x34; top/19_sleeve_far 34x35; top/20 87x71; top/20_sleeve_near 28x58; top/20_sleeve_far 32x59; top/21 87x71; top/21_sleeve_near 28x58; top/21_sleeve_far 32x59; top/22 87x81; top/22_sleeve_near 28x58; top/22_sleeve_far 32x59; top/23 87x81; top/23_sleeve_near 28x58; top/23_sleeve_far 32x59; top/24 87x81; top/24_sleeve_near 28x58; top/24_sleeve_far 32x59; top/25 88x72; top/25_sleeve_near 28x58; top/25_sleeve_far 32x59; top/26 92x93; top/27 87x82; top/27_sleeve_near 28x58; top/27_sleeve_far 32x59; top/28 86x74; top/29 63x65; top/29_coat 88x106; top/29_sleeve_near 28x58; top/29_sleeve_far 32x59; top/30 87x80; top/30_sleeve_near 28x58; top/30_sleeve_far 32x59; top/31 90x88; top/31_sleeve_near 31x64; top/31_sleeve_far 32x63; top/32 88x82; top/32_sleeve_near 28x58; top/32_sleeve_far 32x59; top/33 87x81; top/33_sleeve_near 37x34; top/33_sleeve_far 34x35; top/34 87x81; top/34_sleeve_near 37x34; top/34_sleeve_far 34x35; top/35 87x81; top/36 87x71; top/36_sleeve_near 28x58; top/36_sleeve_far 32x59; top/37 87x81; top/37_sleeve_near 29x37; top/37_sleeve_far 29x38; top/38 87x82; top/38_sleeve_near 28x58; top/38_sleeve_far 32x59; top/39 87x83; top/39_sleeve_near 34x37; top/39_sleeve_far 33x36; top/40 90x88; top/40_sleeve_near 31x64; top/40_sleeve_far 32x63; top/41 91x87; top/41_sleeve_near 31x64; top/41_sleeve_far 32x63; top/42 87x81; top/42_sleeve_near 28x58; top/42_sleeve_far 32x59; top/43 87x79; top/43_sleeve_near 29x37; top/43_sleeve_far 29x38; top/44 87x72; top/44_sleeve_near 31x64; top/44_sleeve_far 32x63; top/45 87x88; top/45_sleeve_near 28x58; top/45_sleeve_far 32x59; top/46 87x72; top/46_sleeve_near 28x58; top/46_cap_near 37x34; top/46_sleeve_far 32x59; top/46_cap_far 34x35; top/47 88x82; top/47_sleeve_near 29x37; top/47_sleeve_far 29x38; top/48 87x71; top/48_sleeve_near 28x58; top/48_sleeve_far 32x59; top/49 86x81; top/49_sleeve_near 28x58; top/49_sleeve_far 32x59; top/50 89x74; top/50_sleeve_near 37x34; top/50_sleeve_far 34x35; top/51 87x72; top/51_sleeve_near 31x64; top/51_sleeve_far 32x63; top/52 87x72; top/52_sleeve_near 29x37; top/52_sleeve_far 29x38; top/53 87x72; top/53_sleeve_near 29x37; top/53_sleeve_far 29x38; top/54 89x92; top/54_sleeve_near 37x34; top/54_sleeve_far 34x35; top/55 87x81; top/56 86x84; top/56_sleeve_near 31x59; top/56_sleeve_far 34x59; top/57 87x80; top/57_sleeve_near 28x58; top/57_sleeve_far 32x59; top/58 92x93; top/59 90x88; top/59_sleeve_near 31x64; top/59_sleeve_far 32x63; top/60 87x80; weapon/knight_sword 156x51; weapon/dark_blade 160x50; weapon/broadsword 160x65; weapon/flame_sword 174x47; weapon/dagger 112x29; weapon/battle_axe 115x60; weapon/double_axe 112x92; weapon/mace 144x71; weapon/warhammer 103x65; weapon/spear 147x40; weapon/wizard_staff 135x43; weapon/druid_staff 144x33; offhand/kite_shield 85x82; offhand/heater_shield 96x81; offhand/round_shield 89x89; offhand/tower_shield 109x87; offhand/buckler 73x73; hat/great_helm 118x120; hat/knight_helm_low 114x115; hat/winged_helm_low 133x83; hat/wizard_hat 154x75; hat/ranger_hood_low 114x115; hat/iron_helm 130x82; top/plate_armor 87x88; top/plate_armor_sleeve_near 37x34; top/plate_armor_sleeve_far 34x35; top/chainmail 91x88; top/chainmail_sleeve_near 37x34; top/chainmail_sleeve_far 34x35; top/leather_armor 89x92; top/leather_armor_sleeve_near 37x34; top/leather_armor_sleeve_far 34x35; top/mage_robe 89x74; top/mage_robe_sleeve_near 37x34; top/mage_robe_sleeve_far 34x35; gloves/gauntlets_near 34x35; gloves/gauntlets_far 32x35; gloves/bracers_near 36x40; gloves/bracers_far 35x40; boots/plate_boots_near 30x23; boots/plate_boots_far 30x23; boots/leather_boots_near 26x30; boots/leather_boots_far 26x30; back/knight_cape 95x124; back/mage_cape 95x124; weapon/hunting_bow 70x112; weapon/hunting_bow_drawn 70x112; weapon/hunting_bow_shot 70x112; weapon/elven_bow 72x128; weapon/elven_bow_drawn 72x128; weapon/elven_bow_shot 72x128; weapon/pistol 96x40; weapon/pistol_shot 96x40; weapon/crossbow 104x56; weapon/crossbow_shot 104x56; weapon/musket 180x40; weapon/musket_shot 180x40; weapon/rifle 160x52; weapon/rifle_shot 160x52",
|
|
9664
9664
|
"style": "chibi-2d",
|
|
9665
9665
|
"asset": {
|
|
9666
9666
|
"kind": "actor",
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "mini-game",
|
|
3
|
-
"meta": {
|
|
4
|
-
"name": "Mini-game",
|
|
5
|
-
"type": "game",
|
|
6
|
-
"engine": "dom",
|
|
7
|
-
"genre": "casual"
|
|
8
|
-
},
|
|
9
|
-
"catalog": {
|
|
10
|
-
"type": "template",
|
|
11
|
-
"summary": "The smallest complete game: a 15-second 'catch the stars' round launched from the lobby's Play button, with a leaderboard score.",
|
|
12
|
-
"description": "A tiny game of its own mode (React/DOM, no engine): tap as many stars as you can in 15 seconds, gold stars are worth 3. The score goes to the title's leaderboard, and a round counts for quests that ask for games_played, for timed events that pay for the 'play_round' action and for season chains open to clients. The platform gives it to a new title when no other game fits the idea — it shows where a game plugs into the base (sign-in → lobby → Play) and is meant to be replaced by the real game. No demo settings of its own: it plays on the base's world.",
|
|
13
|
-
"provides": [
|
|
14
|
-
"a small tap game launched from the lobby",
|
|
15
|
-
"score submitted to the leaderboard",
|
|
16
|
-
"rounds counted by quests, events and the season",
|
|
17
|
-
"placeholder game to replace with the real one"
|
|
18
|
-
],
|
|
19
|
-
"tags": [
|
|
20
|
-
"starter",
|
|
21
|
-
"mini-game",
|
|
22
|
-
"casual",
|
|
23
|
-
"tap",
|
|
24
|
-
"leaderboard"
|
|
25
|
-
],
|
|
26
|
-
"author": {
|
|
27
|
-
"name": "iDos Games",
|
|
28
|
-
"url": "https://idosgames.com"
|
|
29
|
-
},
|
|
30
|
-
"version": "0.1.4"
|
|
31
|
-
},
|
|
32
|
-
"dependencies": {
|
|
33
|
-
"@idosgames/core": "0.26.0",
|
|
34
|
-
"@idosgames/module-sdk": "0.13.3",
|
|
35
|
-
"@idosgames/react": "0.7.0",
|
|
36
|
-
"react": "19.2.7"
|
|
37
|
-
},
|
|
38
|
-
"contentHash": "4e855049c6c583010b8e9b39d552de30cd8544278690fe903fcc8d2a7e451382",
|
|
39
|
-
"files": [
|
|
40
|
-
{
|
|
41
|
-
"path": "i18n.ts",
|
|
42
|
-
"content": "import { makeT } from \"@idosgames/react/ui\";\n\n// Strings of the mini-game. The leaderboard's name is the publisher's word (title config).\n\nconst EN = {\n title: \"Star catch\",\n play: \"Play\",\n playAgain: \"Play again\",\n tapStars: \"Tap the stars!\",\n timeLeft: \"Time\",\n score: \"Score\",\n yourScore: \"Your score\",\n submitted: \"Submitted to the leaderboard\",\n seasonStatus: \"season status\",\n toTable: \"Leaderboard\",\n best: \"Tap as many stars as you can in 15 seconds. Gold stars are worth 3.\",\n} as const;\n\nexport type StringKey = keyof typeof EN;\n\nexport const t = makeT<StringKey>(EN, {\n title: \"Ловец звёзд\",\n play: \"Играть\",\n playAgain: \"Ещё раз\",\n tapStars: \"Ловите звёзды!\",\n timeLeft: \"Время\",\n score: \"Очки\",\n yourScore: \"Ваш результат\",\n submitted: \"Результат отправлен в таблицу\",\n seasonStatus: \"статуса сезона\",\n toTable: \"Таблица лидеров\",\n best: \"Поймайте как можно больше звёзд за 15 секунд. Золотая — 3 очка.\",\n});\n"
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"path": "index.ts",
|
|
46
|
-
"content": "// @idosgames/mod-mini-game — \"Star catch\", the smallest complete game module: a 15-second tap round\n// launched from the lobby's Play button, with a leaderboard score.\n//\n// Primary export is the module manifest; the pieces are exported too, so a project can reuse the\n// round inside its own game after copying this into src/modules/.\n\nexport { miniGameModule } from \"./module\";\nexport { makeMiniGamePanel } from \"./MiniGamePanel\";\nexport { MiniGame } from \"./MiniGame\";\nexport * from \"./model\";\n"
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
"path": "MiniGame.tsx",
|
|
50
|
-
"content": "import { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport {\n Button,\n Icon,\n outlined,\n panel,\n useSound,\n v,\n} from \"@idosgames/react/ui\";\nimport { t } from \"./i18n\";\n\n// \"Tap the stars\": a 15-second round that produces a score for the leaderboard. Deliberately tiny —\n// the placeholder a project replaces with its real game, keeping the same contract: play, then hand\n// the score to onFinish (the play screen submits it and reports the round to quests).\n\nconst ROUND_MS = 15_000;\nconst SPAWN_MS = 520;\nconst LIFE_MS = 1_150;\n\ninterface Star {\n id: number;\n x: number;\n y: number;\n golden: boolean;\n bornAt: number;\n}\n\nexport function MiniGame({\n onFinish,\n onCancel,\n height = 380,\n}: {\n onFinish: (score: number) => void;\n onCancel: () => void;\n height?: number;\n}): ReactNode {\n const play = useSound();\n const [stars, setStars] = useState<Star[]>([]);\n const [score, setScore] = useState(0);\n const [leftMs, setLeftMs] = useState(ROUND_MS);\n const [pops, setPops] = useState<\n Array<{ id: number; x: number; y: number; value: number }>\n >([]);\n const scoreRef = useRef(0);\n const nextId = useRef(1);\n const finished = useRef(false);\n // Latest callback without restarting the round when the parent re-renders.\n const finish = useRef(onFinish);\n finish.current = onFinish;\n\n useEffect(() => {\n const started = performance.now();\n const spawn = setInterval(() => {\n const now = performance.now();\n setStars((list) => [\n ...list.filter((s) => now - s.bornAt < LIFE_MS),\n {\n id: nextId.current++,\n x: 8 + Math.random() * 78,\n y: 10 + Math.random() * 72,\n golden: Math.random() < 0.15,\n bornAt: now,\n },\n ]);\n }, SPAWN_MS);\n const clock = setInterval(() => {\n const left = Math.max(0, ROUND_MS - (performance.now() - started));\n setLeftMs(left);\n if (left === 0 && !finished.current) {\n finished.current = true;\n clearInterval(spawn);\n clearInterval(clock);\n finish.current(scoreRef.current);\n }\n }, 100);\n return () => {\n clearInterval(spawn);\n clearInterval(clock);\n };\n }, []);\n\n const hit = (star: Star) => {\n const value = star.golden ? 3 : 1;\n play(star.golden ? \"coin\" : \"countTick\");\n scoreRef.current += value;\n setScore(scoreRef.current);\n setStars((list) => list.filter((s) => s.id !== star.id));\n const popID = nextId.current++;\n setPops((list) => [...list, { id: popID, x: star.x, y: star.y, value }]);\n setTimeout(\n () => setPops((list) => list.filter((p) => p.id !== popID)),\n 600,\n );\n };\n\n return (\n <div\n style={{\n ...panel,\n position: \"relative\",\n height: `min(${height}px, 70vh)`,\n minHeight: 300,\n overflow: \"hidden\",\n // The playfield: the theme's screen colours, lighter in the middle.\n background: `radial-gradient(circle at 50% 30%, color-mix(in srgb, ${v.bgTop} 75%, #fff) 0%, ${v.bgTop} 45%, ${v.bgBottom} 100%)`,\n touchAction: \"manipulation\",\n userSelect: \"none\",\n }}\n >\n <div\n style={{\n position: \"absolute\",\n top: 10,\n left: 12,\n right: 12,\n display: \"flex\",\n justifyContent: \"space-between\",\n zIndex: 2,\n }}\n >\n <span style={{ ...outlined, fontSize: 18 }}>\n {t(\"score\")}: {score}\n </span>\n <span\n style={{\n ...outlined,\n fontSize: 18,\n color: leftMs < 4000 ? v.red : v.text,\n }}\n >\n {t(\"timeLeft\")}: {Math.ceil(leftMs / 1000)}\n </span>\n </div>\n <div\n style={{\n position: \"absolute\",\n top: 40,\n left: 0,\n right: 0,\n textAlign: \"center\",\n ...outlined,\n fontSize: 14,\n color: v.textDim,\n opacity: score === 0 ? 1 : 0,\n transition: \"opacity .4s\",\n }}\n >\n {t(\"tapStars\")}\n </div>\n {stars.map((s) => (\n <button\n key={s.id}\n type=\"button\"\n aria-label=\"star\"\n onPointerDown={() => hit(s)}\n style={{\n position: \"absolute\",\n left: `${s.x}%`,\n top: `${s.y}%`,\n width: 56,\n height: 56,\n marginLeft: -28,\n marginTop: -28,\n border: \"none\",\n background: \"none\",\n padding: 0,\n cursor: \"pointer\",\n animation: `idos-star ${v.normal} ease-out`,\n }}\n >\n <Icon\n glyph=\"star\"\n size={s.golden ? 56 : 44}\n color={s.golden ? \"#ffe066\" : \"#fff27a\"}\n />\n </button>\n ))}\n {pops.map((p) => (\n <span\n key={p.id}\n style={{\n position: \"absolute\",\n left: `${p.x}%`,\n top: `${p.y}%`,\n ...outlined,\n fontSize: 22,\n color: v.gold,\n pointerEvents: \"none\",\n animation: \"idos-rise-in .6s ease reverse\",\n }}\n >\n +{p.value}\n </span>\n ))}\n <div\n style={{\n position: \"absolute\",\n bottom: 10,\n left: 0,\n right: 0,\n display: \"flex\",\n justifyContent: \"center\",\n }}\n >\n <Button tone=\"grey\" size=\"sm\" onClick={onCancel}>\n ✕\n </Button>\n </div>\n </div>\n );\n}\n"
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
"path": "MiniGamePanel.tsx",
|
|
54
|
-
"content": "import {\n useCallback,\n useState,\n type ComponentType,\n type ReactNode,\n} from \"react\";\nimport type { IDosGamesClient } from \"@idosgames/core\";\nimport type { FeatureRegistry } from \"@idosgames/module-sdk\";\nimport { useIDosGamesClient } from \"@idosgames/react\";\nimport {\n Button,\n Icon,\n configSection,\n errorText,\n formatAmount,\n outlined,\n panel,\n screenBackground,\n useCatalog,\n useGameLayout,\n useToast,\n useUiKit,\n v,\n} from \"@idosgames/react/ui\";\nimport { MiniGame } from \"./MiniGame\";\nimport {\n GAMES_PLAYED_METRIC,\n PLAY_ROUND_ACTION,\n ROUND_STATUS,\n eventsCountingRounds,\n pickLeaderboard,\n questsCountGames,\n seasonChainsForRounds,\n type LeaderboardSection,\n type QuestSection,\n type SeasonSection,\n type TimedEventSection,\n} from \"./model\";\nimport { t } from \"./i18n\";\n\n// The mini-game's mode — the whole screen while it is on (the lobby's \"Play\" starts it, the base's\n// round button top-left returns to the lobby). A round submits its score to the title's leaderboard,\n// reports itself to quests that count rounds, and feeds the events and seasons that pay for a round;\n// the table itself is the leaderboard module's screen (\"Leaderboard\" opens it through ctx.features).\n\nexport function makeMiniGamePanel(features: FeatureRegistry): ComponentType {\n return function MiniGamePanel(): ReactNode {\n return (\n <div\n className=\"idos-scroll idos-screen idos-fit\"\n style={{\n position: \"absolute\",\n inset: 0,\n overflowY: \"auto\",\n background: screenBackground,\n fontFamily: v.font,\n color: v.text,\n // Room for the base's \"To the lobby\" button in the top-left corner.\n padding: \"64px 14px 24px\",\n pointerEvents: \"auto\",\n }}\n >\n {/* the field takes a PC's width too (the stars sit at percentages of it) */}\n <div style={{ maxWidth: 1120, margin: \"0 auto\" }}>\n <Round features={features} />\n </div>\n </div>\n );\n };\n}\n\nfunction Round({ features }: { features: FeatureRegistry }): ReactNode {\n const client = useIDosGamesClient();\n const toast = useToast();\n const kit = useUiKit();\n const layout = useGameLayout();\n const [playing, setPlaying] = useState(false);\n const [result, setResult] = useState<number | null>(null);\n const [bonus, setBonus] = useState<string[]>([]);\n const catalog = useCatalog();\n const board = pickLeaderboard(\n configSection<LeaderboardSection>(client, \"Leaderboard\"),\n );\n\n const finish = useCallback(\n async (score: number) => {\n setPlaying(false);\n setResult(score);\n kit.play(score > 0 ? \"levelUp\" : \"notify\");\n if (board && score > 0) {\n const res = await client.leaderboard.submitScore(\n board.LeaderboardID,\n score,\n );\n if (!res.ok) toast(errorText(res.error), \"error\");\n }\n // One round played — only if a quest counts rounds (otherwise the server refuses the metric).\n if (questsCountGames(configSection<QuestSection>(client, \"Quest\")))\n await client.quest.addQuestProgress(GAMES_PLAYED_METRIC, 1);\n if (score > 0) setBonus(await rewardRound(client, catalog.localize));\n },\n [client, board, toast, kit, catalog],\n );\n\n if (playing)\n return (\n <MiniGame\n height={layout === \"desktop\" ? 560 : layout === \"tablet\" ? 480 : 380}\n onFinish={(s) => void finish(s)}\n onCancel={() => setPlaying(false)}\n />\n );\n\n return (\n <div\n className=\"idos-shine\"\n style={{\n ...panel,\n padding: 20,\n display: \"grid\",\n gap: 12,\n justifyItems: \"center\",\n background: `linear-gradient(180deg, color-mix(in srgb, ${v.blue} 30%, ${v.panel}) 0%, ${v.panelDeep} 100%)`,\n maxWidth: 520,\n margin: \"0 auto\",\n }}\n >\n <div className=\"idos-bounce\">\n <Icon glyph=\"star\" size={72} />\n </div>\n <div\n style={{\n ...outlined,\n fontSize: 15,\n textAlign: \"center\",\n color: v.textDim,\n }}\n >\n {t(\"best\")}\n </div>\n {result !== null ? (\n <div\n style={{\n ...outlined,\n fontSize: 18,\n color: v.gold,\n animation: `idos-pop-in ${v.slow} ease-out both`,\n }}\n >\n {t(\"yourScore\")}: {formatAmount(result)}\n {result > 0 && board ? (\n <div\n style={{ fontSize: 13, color: v.textDim, textAlign: \"center\" }}\n >\n {t(\"submitted\")}\n </div>\n ) : null}\n {bonus.map((line) => (\n <div\n key={line}\n style={{ fontSize: 14, color: v.green, textAlign: \"center\" }}\n >\n {line}\n </div>\n ))}\n </div>\n ) : null}\n <Button\n attract\n size=\"lg\"\n style={{ minWidth: 180 }}\n onClick={() => {\n setResult(null);\n setBonus([]);\n setPlaying(true);\n }}\n data-tutorial-anchor=\"play:start\"\n >\n ▶ {result === null ? t(\"play\") : t(\"playAgain\")}\n </Button>\n {board && features.get(\"leaderboard\")?.available ? (\n <Button\n tone=\"blue\"\n size=\"sm\"\n onClick={() =>\n features.open(\"leaderboard\", {\n leaderboardID: board.LeaderboardID,\n })\n }\n >\n <Icon glyph=\"trophy\" size={18} /> {t(\"toTable\")}\n </Button>\n ) : null}\n </div>\n );\n}\n\n/**\n * A round's share of the title's live-ops: tokens of every active event that pays for a round (a\n * client-safe CustomAction, capped per day by the event) and status in every season chain open to\n * clients. Returns what was granted, for the result card. Best-effort: a refusal (a cap reached,\n * no active season) just grants nothing.\n */\nasync function rewardRound(\n client: IDosGamesClient,\n localize: (text: string | null | undefined) => string,\n): Promise<string[]> {\n const lines: string[] = [];\n const eventIDs = eventsCountingRounds(\n configSection<TimedEventSection>(client, \"TimedEvent\"),\n );\n if (eventIDs.length > 0) {\n const active = await client.timedEvent.getActiveEvents();\n for (const e of active.ok ? (active.data.ActiveEvents ?? []) : []) {\n if (\n !e.TimedEventID ||\n !eventIDs.includes(e.TimedEventID) ||\n e.CanEarn === false ||\n !e.Type\n )\n continue;\n const res = await client.timedEvent.grantTokens(\n e.Type,\n e.TimedEventID,\n \"CustomAction\",\n undefined,\n undefined,\n { ActionName: PLAY_ROUND_ACTION },\n );\n const op = res.ok\n ? (res.data.Grant?.Standard?.EventTokens?.[0] as\n { Applied?: number; Amount?: number } | undefined)\n : undefined;\n const amount = Number(op?.Applied ?? op?.Amount ?? 0);\n if (amount > 0)\n lines.push(\n `+${formatAmount(amount)} ${localize(e.Content?.Token?.DisplayName ?? e.Content?.DisplayName ?? e.TimedEventID)}`,\n );\n }\n }\n for (const chain of seasonChainsForRounds(\n configSection<SeasonSection>(client, \"Season\"),\n )) {\n const res = await client.season.grantStatusTokens(chain, ROUND_STATUS);\n if (res.ok && Number(res.data.AmountGranted ?? 0) > 0)\n lines.push(\n `+${formatAmount(Number(res.data.AmountGranted))} ${t(\"seasonStatus\")}`,\n );\n }\n return lines;\n}\n"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
"path": "model.ts",
|
|
58
|
-
"content": "// Pure helpers of the mini-game — covered by model.test.ts.\n\n/** Metric the mini-game reports once per finished round. A quest counts it only if it asks for it. */\nexport const GAMES_PLAYED_METRIC = \"games_played\";\n\n/** Leaderboard the mini-game submits to when the title has it; otherwise the first enabled one. */\nexport const PREFERRED_LEADERBOARD = \"weekly_score\";\n\nexport interface LeaderboardSection {\n Definitions?: Record<\n string,\n {\n LeaderboardID?: string;\n DisplayName?: string | null;\n IsEnabled?: boolean | null;\n ScoreSources?: Array<{ SourceType?: string | null } | null> | null;\n } | null\n > | null;\n}\n\n/**\n * The leaderboard the mini-game plays for: the preferred id if enabled, else the first enabled one.\n * Only a board that takes a score from the game — one with a `ClientScore` score source: the server\n * refuses a submitted score into any other board (one it fills itself, or one not set up yet).\n */\nexport function pickLeaderboard(\n section: LeaderboardSection | undefined,\n): { LeaderboardID: string; DisplayName?: string | null } | null {\n const defs = Object.entries(section?.Definitions ?? {})\n .map(([id, def]) =>\n def ? { ...def, LeaderboardID: def.LeaderboardID ?? id } : null,\n )\n .filter(\n (\n d,\n ): d is {\n LeaderboardID: string;\n DisplayName?: string | null;\n IsEnabled?: boolean | null;\n } => d !== null && d.IsEnabled !== false,\n )\n .filter((d) =>\n (\n (\n d as {\n ScoreSources?: Array<{ SourceType?: string | null } | null> | null;\n }\n ).ScoreSources ?? []\n ).some((s) => s?.SourceType === \"ClientScore\"),\n );\n return (\n defs.find((d) => d.LeaderboardID === PREFERRED_LEADERBOARD) ??\n defs.sort((a, b) => a.LeaderboardID.localeCompare(b.LeaderboardID))[0] ??\n null\n );\n}\n\nexport interface QuestSection {\n Quests?: Record<\n string,\n {\n Objectives?: Record<\n string,\n { MetricID?: string | null; Source?: string } | null\n > | null;\n } | null\n > | null;\n}\n\n/** Whether some quest counts the mini-game's rounds — only then is the metric worth reporting. */\nexport function questsCountGames(section: QuestSection | undefined): boolean {\n return Object.values(section?.Quests ?? {}).some((q) =>\n Object.values(q?.Objectives ?? {}).some(\n (o) =>\n o?.MetricID === GAMES_PLAYED_METRIC &&\n (o.Source ?? \"ClientApi\") === \"ClientApi\",\n ),\n );\n}\n\n// ── A round feeds the title's events and seasons ─────────────────────────────────────────────\n// Found in the config, so the mini-game needs neither the events nor the season module installed.\n\n/** The CustomAction a round of the mini-game reports to timed events. */\nexport const PLAY_ROUND_ACTION = \"play_round\";\n/** Status a round grants to every season chain open to clients. */\nexport const ROUND_STATUS = 10;\n\nexport interface TimedEventSection {\n Definitions?: Record<\n string,\n {\n Content?: EventContentView | null;\n Events?: Array<{ Content?: EventContentView | null } | null> | null;\n } | null\n > | null;\n}\ntype EventContentView = {\n TokenSources?: Array<{\n SourceType?: string | null;\n Params?: Record<string, string> | null;\n } | null> | null;\n};\n\nfunction countsRounds(content: EventContentView | null | undefined): boolean {\n return (content?.TokenSources ?? []).some(\n (s) =>\n s?.SourceType === \"CustomAction\" &&\n s.Params?.ActionName === PLAY_ROUND_ACTION,\n );\n}\n\n/** Events (by id) whose token sources pay for a round — own content or any phase of a chain. */\nexport function eventsCountingRounds(\n section: TimedEventSection | undefined,\n): string[] {\n return Object.entries(section?.Definitions ?? {})\n .filter(\n ([, d]) =>\n d &&\n (countsRounds(d.Content) ||\n (d.Events ?? []).some((p) => countsRounds(p?.Content))),\n )\n .map(([id]) => id);\n}\n\nexport interface SeasonSection {\n Chains?: Record<\n string,\n { GrantTokensAccessMode?: string | null } | null\n > | null;\n}\n\n/**\n * Season chains the client may grant status to. Only an explicit \"Both\" / \"ClientOnly\" opens a\n * chain: the engine's default is ServerOnly, so a missing field means \"server only\" too.\n */\nexport function seasonChainsForRounds(\n section: SeasonSection | undefined,\n): string[] {\n return Object.entries(section?.Chains ?? {})\n .filter(\n ([, c]) =>\n c?.GrantTokensAccessMode === \"Both\" ||\n c?.GrantTokensAccessMode === \"ClientOnly\",\n )\n .map(([id]) => id);\n}\n"
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"path": "module.ts",
|
|
62
|
-
"content": "import { defineModule, type Module } from \"@idosgames/module-sdk\";\nimport { makeMiniGamePanel } from \"./MiniGamePanel\";\nimport { t } from \"./i18n\";\n\n// The smallest complete game module: a mode of its own (a route + one full-screen panel, no scene)\n// that the base's lobby offers as \"Play\". It is what a new title gets when no other game fits the idea\n// — the placeholder the real game replaces, with the same contract: register a route, draw the mode,\n// and leave the lobby, balances and wallet to the base.\nexport const miniGameModule: Module = defineModule({\n id: \"mini-game\",\n meta: {\n name: \"Mini-game\",\n type: \"game\",\n genre: \"casual\",\n engine: \"dom\",\n },\n setup(ctx) {\n ctx.registerRoute({ id: \"mini-game\", label: t(\"title\"), icon: \"⭐\" });\n ctx.registerPanel({\n id: \"round\",\n slot: \"overlay\",\n component: makeMiniGamePanel(ctx.features),\n });\n\n // The round is ordinary React in the DOM: an agent reads it and presses its buttons like a person.\n ctx.exposeToAgent({\n state: () => ({ ui: \"dom\", mode: ctx.modes.current() }),\n describeActions: {},\n });\n },\n});\n"
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
"path": "module.meta.json",
|
|
66
|
-
"content": "{\n \"id\": \"mini-game\",\n \"type\": \"template\",\n \"summary\": \"The smallest complete game: a 15-second 'catch the stars' round launched from the lobby's Play button, with a leaderboard score.\",\n \"description\": \"A tiny game of its own mode (React/DOM, no engine): tap as many stars as you can in 15 seconds, gold stars are worth 3. The score goes to the title's leaderboard, and a round counts for quests that ask for games_played, for timed events that pay for the 'play_round' action and for season chains open to clients. The platform gives it to a new title when no other game fits the idea — it shows where a game plugs into the base (sign-in → lobby → Play) and is meant to be replaced by the real game. No demo settings of its own: it plays on the base's world.\",\n \"provides\": [\n \"a small tap game launched from the lobby\",\n \"score submitted to the leaderboard\",\n \"rounds counted by quests, events and the season\",\n \"placeholder game to replace with the real one\"\n ],\n \"tags\": [\"starter\", \"mini-game\", \"casual\", \"tap\", \"leaderboard\"],\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.1.4\"\n}\n"
|
|
67
|
-
}
|
|
68
|
-
]
|
|
69
|
-
}
|