@idosgames/mcp 0.1.32 → 0.1.34
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/README.md +12 -1
- package/dist/cli.js +40 -3
- package/package.json +5 -5
- package/registry/host.json +1 -1
- package/registry/index.json +177 -152
- package/registry/modules/ai.json +7 -7
- package/registry/modules/board-game.json +3 -3
- package/registry/modules/boosts.json +7 -7
- package/registry/modules/character.json +7 -7
- package/registry/modules/chat.json +7 -7
- package/registry/modules/collections.json +7 -7
- package/registry/modules/coop-events.json +7 -7
- package/registry/modules/craft.json +7 -7
- package/registry/modules/deals.json +7 -7
- package/registry/modules/events.json +7 -7
- package/registry/modules/friends.json +7 -7
- package/registry/modules/idle-rpg.json +3 -3
- package/registry/modules/inventory.json +7 -7
- package/registry/modules/invite.json +7 -7
- package/registry/modules/leaderboard.json +7 -7
- package/registry/modules/lootboxes.json +7 -7
- package/registry/modules/mailbox.json +7 -7
- package/registry/modules/marketplace.json +7 -7
- package/registry/modules/mini-game.json +3 -3
- package/registry/modules/multiplayer.json +7 -7
- package/registry/modules/premium.json +7 -7
- package/registry/modules/profile.json +9 -9
- package/registry/modules/pvp.json +7 -7
- package/registry/modules/quests.json +7 -7
- package/registry/modules/rewards.json +7 -7
- package/registry/modules/season.json +7 -7
- package/registry/modules/store.json +7 -7
- package/registry/modules/tutorials.json +7 -7
- package/registry/modules/voxelcraft.json +478 -97
- package/registry/modules/workshop.json +7 -7
- package/registry/skills/idosgames-compose-modules.json +1 -1
- package/registry/skills/idosgames-module-contract.json +2 -2
- package/registry/skills/idosgames-ui-kit.json +1 -1
- package/registry/skills/voxelcraft-characters.json +6 -0
- package/registry/skills/voxelcraft-quests.json +6 -0
- package/registry/skills/voxelcraft-worlds.json +2 -2
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
"name": "iDos Games",
|
|
37
37
|
"url": "https://idosgames.com"
|
|
38
38
|
},
|
|
39
|
-
"version": "0.1.
|
|
39
|
+
"version": "0.1.3"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@idosgames/core": "0.21.
|
|
43
|
-
"@idosgames/module-sdk": "0.
|
|
44
|
-
"@idosgames/react": "0.
|
|
42
|
+
"@idosgames/core": "0.21.6",
|
|
43
|
+
"@idosgames/module-sdk": "0.11.0",
|
|
44
|
+
"@idosgames/react": "0.5.0",
|
|
45
45
|
"react": "19.2.7"
|
|
46
46
|
},
|
|
47
|
-
"contentHash": "
|
|
47
|
+
"contentHash": "ba3020258426f2c8b890d16cd169bffb4e96a4fc61d8a312c24d706ec9eac315",
|
|
48
48
|
"files": [
|
|
49
49
|
{
|
|
50
50
|
"path": "components/Catalog.tsx",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"path": "module.ts",
|
|
95
|
-
"content": "import { defineModule, type Module } from \"@idosgames/module-sdk\";\nimport { makeWorkshopScreen } from \"./WorkshopApp\";\nimport { t } from \"./i18n\";\n\n// The Workshop system: a tab of the lobby (ctx.features) with the catalog of player & publisher\n// content. It works for any game — what a content type IS (how to list, capture and open it) comes\n// from the handlers games register in `ctx.content`; the Workshop never imports a game. With no game\n// that registers a content type there is nothing to share, and the tab stays hidden.\nexport const workshopModule: Module = defineModule({\n id: \"workshop\",\n meta: {\n name: \"Workshop\",\n type: \"app\",\n engine: \"dom\",\n },\n setup(ctx) {\n ctx.features.register({\n id: \"workshop\",\n label: t(\"title\"),\n icon: \"globe\",\n group: \"social\",\n order: 88,\n available: false,\n Screen: makeWorkshopScreen({\n client: ctx.client,\n content: ctx.content,\n navigate: (modeId) => ctx.navigate(modeId),\n }),\n });\n\n // Games register their content types in their own setup() — before or after this one — so the\n // tab follows the registry instead of looking once.\n const sync = (): void =>\n ctx.features.setAvailable(\n \"workshop\",\n ctx.content.listTypes().length > 0,\n \"no installed game registers a content type (ctx.content.registerType)\",\n );\n sync();\n ctx.content.subscribe(sync);\n\n ctx.exposeToAgent({\n state: () => ({\n contentTypes: ctx.content.listTypes().map((type) => ({\n type: type.type,\n canPublish: Boolean(type.listLocal && type.capture),\n canOpen: Boolean(type.open),\n })),\n }),\n describeActions: {},\n });\n },\n});\n"
|
|
95
|
+
"content": "import { defineModule, type Module } from \"@idosgames/module-sdk\";\nimport { makeWorkshopScreen } from \"./WorkshopApp\";\nimport { t } from \"./i18n\";\n\n// The Workshop system: a tab of the lobby (ctx.features) with the catalog of player & publisher\n// content. It works for any game — what a content type IS (how to list, capture and open it) comes\n// from the handlers games register in `ctx.content`; the Workshop never imports a game. With no game\n// that registers a content type there is nothing to share, and the tab stays hidden.\nexport const workshopModule: Module = defineModule({\n id: \"workshop\",\n meta: {\n name: \"Workshop\",\n type: \"app\",\n engine: \"dom\",\n },\n setup(ctx) {\n ctx.features.register({\n id: \"workshop\",\n label: () => t(\"title\"),\n icon: \"globe\",\n group: \"social\",\n order: 88,\n available: false,\n Screen: makeWorkshopScreen({\n client: ctx.client,\n content: ctx.content,\n navigate: (modeId) => ctx.navigate(modeId),\n }),\n });\n\n // Games register their content types in their own setup() — before or after this one — so the\n // tab follows the registry instead of looking once.\n const sync = (): void =>\n ctx.features.setAvailable(\n \"workshop\",\n ctx.content.listTypes().length > 0,\n \"no installed game registers a content type (ctx.content.registerType)\",\n );\n sync();\n ctx.content.subscribe(sync);\n\n ctx.exposeToAgent({\n state: () => ({\n contentTypes: ctx.content.listTypes().map((type) => ({\n type: type.type,\n canPublish: Boolean(type.listLocal && type.capture),\n canOpen: Boolean(type.open),\n })),\n }),\n describeActions: {},\n });\n },\n});\n"
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
"path": "styles.ts",
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
"path": "module.meta.json",
|
|
111
|
-
"content": "{\n \"id\": \"workshop\",\n \"type\": \"feature\",\n \"summary\": \"Workshop: players and the publisher share content (maps, levels, skins, models) — free, for in-game resources, or unlocked by holding items.\",\n \"description\": \"A ready catalog of user-generated content for ANY game. Players publish what they made (a world, a level, a skin, any file the title allows), set how others get it — free, a price in in-game resources (several options at once, the author receives it minus commission), or 'hold N of this item / currency to unlock' (nothing is charged; while held or once) — and others browse, filter, acquire, open, like, favorite, follow authors and report. The publisher configures content types, formats, limits, commission and moderation in the Workshop section of the title config, curates featured collections and publishes official content from the dashboard. A game plugs its own content in by registering a handler with ctx.content.registerType({ type, label, listLocal, capture, open, modeId }) — the Workshop never imports the game. VoxelCraft registers 'voxelcraft.world'.\",\n \"provides\": [\n \"player-made content catalog (maps, levels, skins, models, any file type the title allows)\",\n \"publish a map or level made in the game\",\n \"sell content for in-game resources with author royalties minus commission\",\n \"free content sharing\",\n \"unlock content by holding an item or currency without spending it\",\n \"likes, favorites and following content creators\",\n \"report content and auto-hide after reports\",\n \"publisher featured collections and official content\",\n \"download and open shared content in the game\"\n ],\n \"tags\": [\n \"workshop\",\n \"ugc\",\n \"user-generated-content\",\n \"maps\",\n \"levels\",\n \"mods\",\n \"sharing\",\n \"marketplace\",\n \"creators\",\n \"social\"\n ],\n \"author\": { \"name\": \"iDos Games\", \"url\": \"https://idosgames.com\" },\n \"version\": \"0.1.
|
|
111
|
+
"content": "{\n \"id\": \"workshop\",\n \"type\": \"feature\",\n \"summary\": \"Workshop: players and the publisher share content (maps, levels, skins, models) — free, for in-game resources, or unlocked by holding items.\",\n \"description\": \"A ready catalog of user-generated content for ANY game. Players publish what they made (a world, a level, a skin, any file the title allows), set how others get it — free, a price in in-game resources (several options at once, the author receives it minus commission), or 'hold N of this item / currency to unlock' (nothing is charged; while held or once) — and others browse, filter, acquire, open, like, favorite, follow authors and report. The publisher configures content types, formats, limits, commission and moderation in the Workshop section of the title config, curates featured collections and publishes official content from the dashboard. A game plugs its own content in by registering a handler with ctx.content.registerType({ type, label, listLocal, capture, open, modeId }) — the Workshop never imports the game. VoxelCraft registers 'voxelcraft.world'.\",\n \"provides\": [\n \"player-made content catalog (maps, levels, skins, models, any file type the title allows)\",\n \"publish a map or level made in the game\",\n \"sell content for in-game resources with author royalties minus commission\",\n \"free content sharing\",\n \"unlock content by holding an item or currency without spending it\",\n \"likes, favorites and following content creators\",\n \"report content and auto-hide after reports\",\n \"publisher featured collections and official content\",\n \"download and open shared content in the game\"\n ],\n \"tags\": [\n \"workshop\",\n \"ugc\",\n \"user-generated-content\",\n \"maps\",\n \"levels\",\n \"mods\",\n \"sharing\",\n \"marketplace\",\n \"creators\",\n \"social\"\n ],\n \"author\": { \"name\": \"iDos Games\", \"url\": \"https://idosgames.com\" },\n \"version\": \"0.1.3\"\n}\n"
|
|
112
112
|
}
|
|
113
113
|
]
|
|
114
114
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idosgames-compose-modules",
|
|
3
3
|
"description": "Put an iDosGames app together from the base and modules — the systems (shop, heroes, leaderboards, quests, chests, multiplayer…) as lobby tabs, one or several games behind \"Play\", progress shared across them. Use this whenever a developer wants to COMBINE or MERGE modules into a single title, add or remove a system, add a second game, switch between the lobby and game modes, share currency/inventory across modules, or asks how the lobby, \"Play\", ctx.modes, activeOnly panels, the shared-UI roles (sharedUi, shouldDraw, the \"modes\" role), typed cross-module events (defineTopic/shape) or host-level shared state work. Builds on idosgames-getting-started (scaffolding) and idosgames-module-contract (a single module).",
|
|
4
|
-
"content": "---\nname: idosgames-compose-modules\ndescription: >-\n Put an iDosGames app together from the base and modules — the systems (shop, heroes, leaderboards,\n quests, chests, multiplayer…) as lobby tabs, one or several games behind \"Play\", progress shared across them. Use this\n whenever a developer wants to COMBINE or MERGE modules into a single title, add or remove a system,\n add a second game, switch between the lobby and game modes, share currency/inventory across modules,\n or asks how the lobby, \"Play\", ctx.modes, activeOnly panels, the shared-UI roles (sharedUi,\n shouldDraw, the \"modes\" role), typed cross-module events (defineTopic/shape) or host-level shared\n state work. Builds on idosgames-getting-started (scaffolding) and idosgames-module-contract (a single\n module).\n---\n\n# Putting an app together: the base + modules\n\nEvery project is **the base + modules**. The base is sign-in and the lobby and comes from the SDK\n(`@idosgames/app-shell`: `createBaseModule`, `DefaultLoginScreen`, `DefaultPlayAccessScreen`); the project\nconfigures it in `src/app.config.ts` (where the player lands, the tab order, guest sign-in), `src/ui.config.ts`\n(the look) and `src/main.tsx`. Everything else is a module in `src/modules/<id>/`:\n\n- **systems** (`type: feature`) — one per player-facing module of the engine: store, premium (VIP),\n character, inventory, lootboxes, boosts, leaderboard, rewards (daily), quests, collections, craft,\n marketplace, deals, events, coop-events, season, friends, chat, invite, pvp, multiplayer, profile,\n tutorials, workshop, ai, mailbox. Each registers its screen as a **feature** → a tab of the lobby.\n The base installs only six of them into every new project (`inBase: true`: profile, inventory,\n invite, friends, quests, mailbox); every other system is installed only when the app needs it — a game\n brings the ones it lists in `uses`, and you install any other one as soon as the task needs what\n it `provides` (InstallModule / `get_module`) — the user does not have to name the module; reuse it\n instead of writing that feature again. Never install one the task does not need.\n- **games** (`type: template`) — board-game, idle-rpg, voxelcraft, mini-game, or the game's own. Each\n registers a **route** → a \"Play\" button in the lobby. A game that plugs into a system lists it in\n `uses` in its module.meta.json (`ctx.multiplayer` → `\"multiplayer\"`, `ctx.content` → `\"workshop\"`;\n `npm run check` enforces those two) — install those together with the game.\n\n## Register modules\n\n```ts\n// src/modules.ts — the platform regenerates it; by hand it is imports + the array\nexport const modules: Module[] = [\n storeModule, // systems: lobby tabs\n characterModule,\n leaderboardModule,\n boardGameModule, // games: \"Play\"\n idleRpgModule,\n];\n```\n\n`src/main.tsx` mounts `[createBaseModule(appConfig, …), ...modules]`. After sign-in the player lands in the game that\nlives inside the lobby, or in the lobby when the games take the whole screen (`src/app.config.ts`\n`start`: `\"game\"` — the default, `\"lobby\"`, or a game's mode id). \"Play\" switches the host to the\ngame's mode: only the active mode's scene is mounted and ticking; the rest are suspended (their RAF\nstops). This is why two different engines (Three + Phaser) live in one project — they never render\nat the same time. Two kinds of game (owner's decision 03.10.2026 — chosen per game, never for all):\n\n- `ctx.registerRoute({ …, inLobby: true })` — the game sits INSIDE the lobby (idle-rpg): the base\n keeps the balances on top and the tabs at the bottom (host panel slots `header`/`footer` — the\n game is laid out between them, it does nothing for it); \"Play\" is the game, any other tab opens\n that screen, the app starts in it. A game with its own bar to the systems hides it when\n `ctx.sharedUi.shouldDraw(\"menu\")` is false (idle-rpg does).\n- without it (the default) — the game takes the whole screen (voxelcraft, board-game, mini-game):\n \"Play\" in the lobby launches it, over it the base draws only a small \"To the lobby\" button.\n\n- A system tab is hidden while the title has nothing for it (no store offers, no heroes…): the module\n registers with `available: false` and calls `ctx.features.setAvailable(id, ok, reason)`.\n- `ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type);\n the lobby builds \"Play\" from the `game` ones. Switch with `ctx.navigate(id)` (`\"lobby\"` is the base).\n- Open another system's screen by id: `ctx.features.open(\"store\")` — from a game too (the host shows\n it in a window while the lobby is off screen). Never import another module.\n\n## Share progress across modes (the merge)\n\nShared state lives in the ONE SDK client, not in any module. Every module reads/writes the same\n`client` (currency, inventory, characters), so progress carries across modes automatically:\n\n- Gold earned idle in the RPG mode is spendable in the Board mode and in the shop — same\n `client.currency`, same cache. No cross-module plumbing needed for durable state.\n- For live cross-module signals (not durable state), use `ctx.events` with a **typed topic** — see\n \"Signals between modules\" below.\n- Modules never import each other's files. The client and the event bus are the seams\n between them — which is what keeps a module reusable in another game. Code several of THIS game's\n modules need (shared types, the game's UI kit, helpers) goes to `src/shared/`, which never imports a\n module (see idosgames-project-structure).\n\n## Shared chrome: roles, and the base takes them\n\nBalances, the crypto wallet, the status line, \"Log out\" / player ID, the menu of features and the\nswitch between modes are **shared-UI roles** (`currency-bar | wallet | status | account | menu |\nmodes`). A module that draws one for EVERY mode declares it statically on the module object —\nthe base does:\n\n```ts\ndefineModule({\n id: \"base\",\n meta: { name: \"Base\", type: \"app\", engine: \"dom\" },\n sharedUi: {\n provides: [\"currency-bar\", \"wallet\", \"account\", \"menu\", \"modes\"],\n },\n setup(ctx) {\n /* the lobby: balances, wallet, profile, tabs, \"Play\" */\n },\n});\n```\n\nThe host resolves role owners from these declarations BEFORE any `setup()` — the first provider wins,\na conflict is warned — and every module asks `ctx.sharedUi.shouldDraw(role)`. A game draws its own\ncopy only while nobody took the role. So:\n\n- Next to the base a game hides its own balances and wallet by itself. Do NOT edit a catalog game to\n remove them: it does that itself, and an edit marks it customized (no more catalog updates).\n- `status` — the line of a game's own actions (\"Rolling…\") — stays with each game.\n- A game installed without a base stays complete — nobody owns a role, it draws everything, and the\n host draws its own nav of routes, ☰ and \"Log out\" (those are the host-drawn roles `modes`, `menu`,\n `account`).\n- A new module that needs, say, the balance bar but does not draw it declares\n `sharedUi: { requires: [\"currency-bar\"] }`; with no provider installed the host warns.\n\n**Layout is the host's job.** A game's overlay fills the screen under the base's small button; a\nscene's own DOM HUD can keep clear of host chrome with the CSS variables on the host root —\n`top: calc(var(--idos-safe-top, 0px) + 8px)`, `bottom: calc(var(--idos-safe-bottom, 0px) + 8px)`\n(voxelcraft's hotbar does this). Both are 0 when nothing is pinned. `hud`-slot panels are wrapped in\n`display: contents`, so a panel can stretch (`flex: 1`) and decide its own pointer-events: let clicks\nthrough to the game, take them only on controls.\n\n## Signals between modules (typed topics)\n\nA topic is a token passed by value: the name carries the major version, the payload is a flat JSON\nshape — one artifact that gives the TS type, the runtime check and the catalog entry.\n\n```ts\n// idle-rpg/events.ts — the EMITTER owns the topic\nimport { defineTopic, shape } from \"@idosgames/module-sdk\";\nexport const characterUpgraded = defineTopic(\n \"idle-rpg:character-upgraded@1\",\n shape({ characterId: \"string\", level: \"number\" }),\n);\n// setup(): hand the panel a callback that does\n// ctx.events.emit(characterUpgraded, { characterId, level });\n```\n\n```ts\n// board-game/events.ts — the LISTENER keeps its OWN copy (copied from the catalog), never an import.\n// Optional: idle-rpg may not be installed. Only the fields this module reads.\nexport const idleCharacterUpgraded = defineTopic(\n \"idle-rpg:character-upgraded@1\",\n shape({ level: \"number\" }),\n);\n// setup() — NOT a panel effect (activeOnly panels unmount with their mode and miss events):\nctx.events.on(idleCharacterUpgraded, ({ level }) =>\n news.push(`Idle RPG hero reached Lv ${level}`),\n);\n```\n\nDeclare both sides in `module.meta.json` — the catalog and the agent read this, not your code:\n\n```json\n\"events\": {\n \"emits\": [{ \"topic\": \"idle-rpg:character-upgraded@1\", \"when\": \"a hero levelled up\",\n \"payload\": { \"characterId\": \"string\", \"level\": \"number\" } }],\n \"listens\": [{ \"topic\": \"idle-rpg:character-upgraded@1\", \"why\": \"news chip in the board HUD\" }]\n}\n```\n\nRules:\n\n- Name `<your-module-id>:<event>@<major>`. Emit only into your own namespace — the host drops an\n emit into another module's namespace with `console.error`; `host:` is reserved. The only topics\n you import are `hostTopics` from `@idosgames/module-sdk` (`hostTopics.modeChanged` =\n `host:mode-changed@1 { from?: string; to: string }`, sent on every mode switch).\n- Shape leaves: `\"string\" | \"number\" | \"boolean\"`, with `\"[]\"` and/or a trailing `\"?\"`; nested\n objects allowed. Payloads are plain JSON — no functions, class instances or engine objects.\n- An incompatible change is a NEW topic `@2` (send both during the transition); within a major, only\n add fields. A listener whose fields are missing is skipped (one `console.warn`), never crashed.\n- An event is a signal, not state, and is never replayed — \"what exists now\" is SDK data or\n `ctx.sharedUi`. No request/response between modules: if you need an answer, it is data.\n- A throwing handler does not stop other listeners or reach the emitter; the host removes every\n subscription on logout, so re-login does not double handlers.\n- In the AI editor's preview, `gameState` returns the last 50 events, who listens to what, and hints\n like \"board-game listens to x@1 but only x@2 is emitted\".\n- A game's OWN modules may keep shared topic tokens in `src/shared/events/` and import them on both\n sides — still declare them in each `module.meta.json`.\n\n## Reference app\n\nThe base + the systems + `board-game` + `idle-rpg`:\n\n- sign-in → the lobby: balances, wallet and profile on top, the systems as tabs, \"Play\" with two games;\n- \"Play\" → one engine scene mounted at a time; the lobby's tabs stay around an `inLobby` game, a\n full-screen game gets the button back;\n- the games hide their own balances and wallet (the lobby has them); their status lines stay theirs;\n- currency changed in one mode is immediately visible in the others and in the lobby; levelling a hero\n in Idle RPG shows a news chip on the board (`idle-rpg:character-upgraded@1`).\n\nTo pull the modules, use `get_module {id}` for each (MCP) and register them as above. Adjust layouts\nper module (a full-bleed overlay UI vs a docked side panel) — see each module's RootPanel.\n",
|
|
4
|
+
"content": "---\nname: idosgames-compose-modules\ndescription: >-\n Put an iDosGames app together from the base and modules — the systems (shop, heroes, leaderboards,\n quests, chests, multiplayer…) as lobby tabs, one or several games behind \"Play\", progress shared across them. Use this\n whenever a developer wants to COMBINE or MERGE modules into a single title, add or remove a system,\n add a second game, switch between the lobby and game modes, share currency/inventory across modules,\n or asks how the lobby, \"Play\", ctx.modes, activeOnly panels, the shared-UI roles (sharedUi,\n shouldDraw, the \"modes\" role), typed cross-module events (defineTopic/shape) or host-level shared\n state work. Builds on idosgames-getting-started (scaffolding) and idosgames-module-contract (a single\n module).\n---\n\n# Putting an app together: the base + modules\n\nEvery project is **the base + modules**. The base is sign-in and the lobby and comes from the SDK\n(`@idosgames/app-shell`: `createBaseModule`, `DefaultLoginScreen`, `DefaultPlayAccessScreen`); the project\nconfigures it in `src/app.config.ts` (where the player lands, the tab order, guest sign-in), `src/ui.config.ts`\n(the look) and `src/main.tsx`. Everything else is a module in `src/modules/<id>/`:\n\n- **systems** (`type: feature`) — one per player-facing module of the engine: store, premium (VIP),\n character, inventory, lootboxes, boosts, leaderboard, rewards (daily), quests, collections, craft,\n marketplace, deals, events, coop-events, season, friends, chat, invite, pvp, multiplayer, profile,\n tutorials, workshop, ai, mailbox. Each registers its screen as a **feature** → a tab of the lobby.\n The base installs only six of them into every new project (`inBase: true`: profile, inventory,\n invite, friends, quests, mailbox); every other system is installed only when the app needs it — a game\n brings the ones it lists in `uses`, and you install any other one as soon as the task needs what\n it `provides` (InstallModule / `get_module`) — the user does not have to name the module; reuse it\n instead of writing that feature again. Never install one the task does not need.\n- **games** (`type: template`) — board-game, idle-rpg, voxelcraft, mini-game, or the game's own. Each\n registers a **route** → a \"Play\" button in the lobby. A game that plugs into a system lists it in\n `uses` in its module.meta.json (`ctx.multiplayer` → `\"multiplayer\"`, `ctx.content` → `\"workshop\"`;\n `npm run check` enforces those two) — install those together with the game.\n\n## Register modules\n\n```ts\n// src/modules.ts — the platform regenerates it; by hand it is imports + the array\nexport const modules: Module[] = [\n storeModule, // systems: lobby tabs\n characterModule,\n leaderboardModule,\n boardGameModule, // games: \"Play\"\n idleRpgModule,\n];\n```\n\n`src/main.tsx` mounts `[createBaseModule(appConfig, …), ...modules]`. After sign-in the player lands in the game that\nlives inside the lobby, or in the lobby when the games take the whole screen (`src/app.config.ts`\n`start`: `\"game\"` — the default, `\"lobby\"`, or a game's mode id). \"Play\" switches the host to the\ngame's mode: only the active mode's scene is mounted and ticking; the rest are suspended (their RAF\nstops). This is why two different engines (Three + Phaser) live in one project — they never render\nat the same time. Two kinds of game (owner's decision 03.10.2026 — chosen per game, never for all):\n\n- `ctx.registerRoute({ …, inLobby: true })` — the game sits INSIDE the lobby (idle-rpg): the base\n keeps the balances on top and the tabs at the bottom (host panel slots `header`/`footer` — the\n game is laid out between them, it does nothing for it); \"Play\" is the game, any other tab opens\n that screen, the app starts in it. A game with its own bar to the systems hides it when\n `ctx.sharedUi.shouldDraw(\"menu\")` is false (idle-rpg does).\n- without it (the default) — the game takes the whole screen (voxelcraft, board-game, mini-game):\n \"Play\" in the lobby launches it, over it the base draws only a small \"To the lobby\" button.\n A game with its own way back (a pause menu with \"To the lobby\" → `ctx.lobby.open()`, and a pause\n button on touch screens) says `ownExit: true` in its route — then the base draws nothing over it\n (voxelcraft).\n\n- A system tab is hidden while the title has nothing for it (no store offers, no heroes…): the module\n registers with `available: false` and calls `ctx.features.setAvailable(id, ok, reason)`.\n- `ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type);\n the lobby builds \"Play\" from the `game` ones. Switch with `ctx.navigate(id)` (`\"lobby\"` is the base).\n- Open another system's screen by id: `ctx.features.open(\"store\")` — from a game too (the host shows\n it in a window while the lobby is off screen). Never import another module.\n\n## Share progress across modes (the merge)\n\nShared state lives in the ONE SDK client, not in any module. Every module reads/writes the same\n`client` (currency, inventory, characters), so progress carries across modes automatically:\n\n- Gold earned idle in the RPG mode is spendable in the Board mode and in the shop — same\n `client.currency`, same cache. No cross-module plumbing needed for durable state.\n- For live cross-module signals (not durable state), use `ctx.events` with a **typed topic** — see\n \"Signals between modules\" below.\n- Modules never import each other's files. The client and the event bus are the seams\n between them — which is what keeps a module reusable in another game. Code several of THIS game's\n modules need (shared types, the game's UI kit, helpers) goes to `src/shared/`, which never imports a\n module (see idosgames-project-structure).\n\n## Shared chrome: roles, and the base takes them\n\nBalances, the crypto wallet, the status line, \"Log out\" / player ID, the menu of features and the\nswitch between modes are **shared-UI roles** (`currency-bar | wallet | status | account | menu |\nmodes`). A module that draws one for EVERY mode declares it statically on the module object —\nthe base does:\n\n```ts\ndefineModule({\n id: \"base\",\n meta: { name: \"Base\", type: \"app\", engine: \"dom\" },\n sharedUi: {\n provides: [\"currency-bar\", \"wallet\", \"account\", \"menu\", \"modes\"],\n },\n setup(ctx) {\n /* the lobby: balances, wallet, profile, tabs, \"Play\" */\n },\n});\n```\n\nThe host resolves role owners from these declarations BEFORE any `setup()` — the first provider wins,\na conflict is warned — and every module asks `ctx.sharedUi.shouldDraw(role)`. A game draws its own\ncopy only while nobody took the role. So:\n\n- Next to the base a game hides its own balances and wallet by itself. Do NOT edit a catalog game to\n remove them: it does that itself, and an edit marks it customized (no more catalog updates).\n- `status` — the line of a game's own actions (\"Rolling…\") — stays with each game.\n- A game installed without a base stays complete — nobody owns a role, it draws everything, and the\n host draws its own nav of routes, ☰ and \"Log out\" (those are the host-drawn roles `modes`, `menu`,\n `account`).\n- A new module that needs, say, the balance bar but does not draw it declares\n `sharedUi: { requires: [\"currency-bar\"] }`; with no provider installed the host warns.\n\n**Layout is the host's job.** A game's overlay fills the screen under the base's small button; a\nscene's own DOM HUD can keep clear of host chrome with the CSS variables on the host root —\n`top: calc(var(--idos-safe-top, 0px) + 8px)`, `bottom: calc(var(--idos-safe-bottom, 0px) + 8px)`\n(voxelcraft's hotbar does this). Both are 0 when nothing is pinned. `hud`-slot panels are wrapped in\n`display: contents`, so a panel can stretch (`flex: 1`) and decide its own pointer-events: let clicks\nthrough to the game, take them only on controls.\n\n## Signals between modules (typed topics)\n\nA topic is a token passed by value: the name carries the major version, the payload is a flat JSON\nshape — one artifact that gives the TS type, the runtime check and the catalog entry.\n\n```ts\n// idle-rpg/events.ts — the EMITTER owns the topic\nimport { defineTopic, shape } from \"@idosgames/module-sdk\";\nexport const characterUpgraded = defineTopic(\n \"idle-rpg:character-upgraded@1\",\n shape({ characterId: \"string\", level: \"number\" }),\n);\n// setup(): hand the panel a callback that does\n// ctx.events.emit(characterUpgraded, { characterId, level });\n```\n\n```ts\n// board-game/events.ts — the LISTENER keeps its OWN copy (copied from the catalog), never an import.\n// Optional: idle-rpg may not be installed. Only the fields this module reads.\nexport const idleCharacterUpgraded = defineTopic(\n \"idle-rpg:character-upgraded@1\",\n shape({ level: \"number\" }),\n);\n// setup() — NOT a panel effect (activeOnly panels unmount with their mode and miss events):\nctx.events.on(idleCharacterUpgraded, ({ level }) =>\n news.push(`Idle RPG hero reached Lv ${level}`),\n);\n```\n\nDeclare both sides in `module.meta.json` — the catalog and the agent read this, not your code:\n\n```json\n\"events\": {\n \"emits\": [{ \"topic\": \"idle-rpg:character-upgraded@1\", \"when\": \"a hero levelled up\",\n \"payload\": { \"characterId\": \"string\", \"level\": \"number\" } }],\n \"listens\": [{ \"topic\": \"idle-rpg:character-upgraded@1\", \"why\": \"news chip in the board HUD\" }]\n}\n```\n\nRules:\n\n- Name `<your-module-id>:<event>@<major>`. Emit only into your own namespace — the host drops an\n emit into another module's namespace with `console.error`; `host:` is reserved. The only topics\n you import are `hostTopics` from `@idosgames/module-sdk` (`hostTopics.modeChanged` =\n `host:mode-changed@1 { from?: string; to: string }`, sent on every mode switch).\n- Shape leaves: `\"string\" | \"number\" | \"boolean\"`, with `\"[]\"` and/or a trailing `\"?\"`; nested\n objects allowed. Payloads are plain JSON — no functions, class instances or engine objects.\n- An incompatible change is a NEW topic `@2` (send both during the transition); within a major, only\n add fields. A listener whose fields are missing is skipped (one `console.warn`), never crashed.\n- An event is a signal, not state, and is never replayed — \"what exists now\" is SDK data or\n `ctx.sharedUi`. No request/response between modules: if you need an answer, it is data.\n- A throwing handler does not stop other listeners or reach the emitter; the host removes every\n subscription on logout, so re-login does not double handlers.\n- In the AI editor's preview, `gameState` returns the last 50 events, who listens to what, and hints\n like \"board-game listens to x@1 but only x@2 is emitted\".\n- A game's OWN modules may keep shared topic tokens in `src/shared/events/` and import them on both\n sides — still declare them in each `module.meta.json`.\n\n## Reference app\n\nThe base + the systems + `board-game` + `idle-rpg`:\n\n- sign-in → the lobby: balances, wallet and profile on top, the systems as tabs, \"Play\" with two games;\n- \"Play\" → one engine scene mounted at a time; the lobby's tabs stay around an `inLobby` game, a\n full-screen game gets the button back;\n- the games hide their own balances and wallet (the lobby has them); their status lines stay theirs;\n- currency changed in one mode is immediately visible in the others and in the lobby; levelling a hero\n in Idle RPG shows a news chip on the board (`idle-rpg:character-upgraded@1`).\n\nTo pull the modules, use `get_module {id}` for each (MCP) and register them as above. Adjust layouts\nper module (a full-bleed overlay UI vs a docked side panel) — see each module's RootPanel.\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, mini-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.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, 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.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; 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?, Screen })`. 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\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.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### `capture()` — the frame for \"Take a shot and share\"\n\nidosgames.com has a **Shot** button under the game: it asks the game for one frame, puts the\nplatform's branding and a QR code on it and offers to post it. A scene answers 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, mini-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.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, 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.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?, Screen })`. 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.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### `capture()` — the frame for \"Take a shot and share\"\n\nidosgames.com has a **Shot** button under the game: it asks the game for one frame, puts the\nplatform's branding and a QR code on it and offers to post it. A scene answers 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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idosgames-ui-kit",
|
|
3
3
|
"description": "Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui) and the ready system modules: change colours, animations and sounds in ONE file (src/ui.config.ts), install a ready system (shop, VIP, heroes, inventory, chests, quests, leaderboard, daily rewards, season, friends, chat, profile, workshop, AI studio …) instead of writing it, open or register a screen (ctx.features.open / register, setAvailable), split a dense screen into popups, play UI sounds, celebrate a reward, mark tutorial anchors and read sound / motion prefs. Use this whenever the user asks to change how the game's interface looks, sounds or animates, or to add a screen, menu, popup, reward animation, bottom navigation or settings screen, or touches @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features, useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.",
|
|
4
|
-
"content": "---\nname: idosgames-ui-kit\ndescription: >-\n Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui)\n and the ready system modules: change colours, animations and sounds in ONE\n file (src/ui.config.ts), install a ready system (shop, VIP, heroes, inventory,\n chests, quests, leaderboard, daily rewards, season, friends, chat, profile,\n workshop, AI studio …) instead of writing it, open or register a screen\n (ctx.features.open / register, setAvailable), split a dense screen into\n popups, play UI sounds, celebrate a reward, mark tutorial anchors and read\n sound / motion prefs. Use this whenever the user asks to change how the\n game's interface looks, sounds or animates, or to add a screen, menu, popup,\n reward animation, bottom navigation or settings screen, or touches\n @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features,\n useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.\n---\n\n# The UI kit and the system modules\n\nThe app's interface is the base's lobby (from `@idosgames/app-shell`) plus **system modules**, all on one **UI kit**:\n\n- `@idosgames/react/ui` — the kit: components (buttons, tabs, popups, resource lists, item tiles,\n progress bars, timers), the reward popup, toasts, UI sounds (synthesized, no files), animations,\n and the player's own sound / motion prefs.\n- system modules — the platform's systems, each a whole `feature` module (its screen = a lobby\n tab, its client logic, its demo settings). The base installs six of them into every new project\n (`inBase`: profile, inventory, invite, friends, quests, mailbox); the rest are installed only when the app\n needs them (a game's `uses`, or a task that needs what they `provide`). ONE system per player-facing module of the engine\n (`API/Client/v2/<Module>`) — the owner's rule since 29.09.2026. The module id is the feature id:\n\n| Module | Engine module | Screen |\n| ------------- | -------------- | ------------------------------------------------------------- |\n| `store` | Store+Purchase | shop: stores as tabs, offers, purchase popup |\n| `premium` | Premium | VIP passes: benefits, timer, free trial, buy / extend |\n| `character` | Character | heroes: gear slots, \"Equip best\", stats, ranks, skins |\n| `inventory` | Item | items by rarity, item card (always shown, even with no items) |\n| `lootboxes` | Lootbox | chests: every way to open, ×1 / ×10, opening animation |\n| `boosts` | TimedBoost | running boosts + global happy hours, activate a boost |\n| `leaderboard` | Leaderboard | tables, my place, rank rewards, last cycle's prize |\n| `rewards` | Reward | daily login calendar, greeting popup |\n| `quests` | Quest | quests by cycle, details, claim all, points track |\n| `collections` | Collection | album of sets, packs, chests for points, card trades |\n| `craft` | Craft | recipes, contract popup |\n| `marketplace` | Marketplace | lots, auctions, selling, trades |\n| `deals` | DealOffer | special offers, auto-popup of a new deal |\n| `events` | TimedEvent | limited-time events, milestones, VIP prize |\n| `coop-events` | CoopEvent | team event: join a group, spin to build, team prize |\n| `season` | Season | season tiers, VIP prize |\n| `friends` | Social | friends, requests, suggestions, news |\n| `chat` | Chat | channels, private chats |\n| `invite` | Referral | own code and link, friend's code, rewards (always shown) |\n| `pvp` | Match | duels, strategy, fight replay |\n| `multiplayer` | Multiplayer | rooms: list, create / join by code, ready, chat, host \"Play\" |\n| `profile` | User | name, sound, language, consent, delete account |\n| `tutorials` | Tutorial | hint overlay + replay list |\n| `workshop` | Workshop | players' and the publisher's content: catalog, publish |\n| `ai` | AI | AI studio: the title's AI features for players, history |\n\nNot a system on purpose: Currency, Authentication, Blockchain (the base — balances, sign-in,\nwallet); GameLoop (the `board-game` game); CommunityMarketing (idosgames.com only); Mailbox and\nAdvertising (no client handler / no web ad network); Analytics, Experiment, Push, Localization,\nCloudCode, DataCollections, TitleCustomData, UserCustomData (services the systems and games call).\n\nHand-offs between systems go through `ctx.features.open(id, args)`: inventory → `character`\n(`{ itemInstanceID }`) and → `lootboxes` (`{ itemID }` of a chest or key); events / season →\n`premium` (`{ premiumID }`). Multiplayer: the `multiplayer` system owns the rooms; a game registers\nin `ctx.multiplayer` and takes the room in `start(room)` (skill **multiplayer-realtime**); the lobby\nshows \"With friends\" next to such a game's \"Play\".\n\n**Need one of these? Install the module — don't write the screen.** Every screen follows the\nserver's rules and ships demo title settings (`title-seed.json`). The app does not need one → remove it\nfrom `src/modules.ts`, its tab is gone. The lobby itself (tabs, balances, wallet, profile, \"Play\") is\nthe base, `@idosgames/app-shell` (configured by `src/app.config.ts`, styled by `src/ui.config.ts`).\n\n## Change the look, animations and sounds — only in `src/ui.config.ts`\n\n```ts\nimport { defineUiConfig } from \"@idosgames/react/ui\";\n\nexport default defineUiConfig({\n theme: {\n bgTop: \"#10263f\",\n panel: \"#1b3a5c\",\n gold: \"#ffcc33\",\n radius: 12,\n rarity: { Mythic: \"#ff3df5\" },\n },\n motion: { speed: 1.5, intensity: 0.5, particles: false },\n sound: {\n volume: 0.4,\n events: {\n click: \"tab\",\n countTick: false,\n reward: { url: \"https://…/win.mp3\" },\n },\n },\n});\n```\n\n- Every field is optional; what you leave out keeps its default. The host passes the file to\n `mountHost({ ui })`, and every module on the kit follows it at once.\n- ⚠ **Never restyle a catalog module by editing it.** An edited module is marked \"customized\" and\n stops getting catalog updates. Colours, radii, fonts, animation speed and sounds are all here.\n- **One theme for the WHOLE app:** sign-in (the SDK's `DefaultLoginScreen`, mounted by\n `src/main.tsx`), the lobby, every system and the host's own screens all draw from\n these tokens. A new look — even a light theme — is an edit of this file ONLY: never recolour the\n sign-in, the logo, icons or `index.html` by hand (the logo and the sign-in icons are painted with the\n theme's colours, the page background follows `bgBottom`).\n- Theme tokens: `bgTop`, `bgBottom`, `panel`, `panelDeep`, `panelEdge`, `text`, `textDim`, `gold`,\n `goldDeep`, `green`, `greenDeep`, `blue`, `blueDeep`, `red`, `redDeep`, `shadow`, `backdrop`,\n `radius`, `buttonRadius`, `font`, `rarity` (rarity id → frame colour).\n- **Derived tokens — usually leave them out.** They are computed from the colours above: set a light\n `panel` and dark `text`, and the dark text outline disappears (`textOutline`), lines and recesses\n turn faint-dark instead of white/black (`line`, `well`, `wellSoft`, `wellShadow`, `edge`), cards get a\n soft shadow (`panelShadow`), and buttons of a pastel tone get dark text (`onGreen`, `onGold`,\n `onBlue`, `onRed`). Set one only to override the computed value.\n- A light theme is just colours:\n `theme: { bgTop: \"#f4fcf8\", bgBottom: \"#d9f0e6\", panel: \"#ffffff\", panelDeep: \"#f1faf5\",\npanelEdge: \"#bfe6d3\", text: \"#1f3b30\", textDim: \"#5f7d71\", green: \"#7ee0b5\", greenDeep: \"#4cc493\" }`.\n- Motion: `enabled`, `speed` (2 = twice as fast), `intensity` (0 = no bounce), `rewardFlight`,\n `particles`, `stagger`, `respectReducedMotion`.\n- Sound events: `click`, `tab`, `open`, `close`, `reward`, `coin`, `purchase`, `claim`, `error`,\n `notify`, `lootbox`, `levelUp`, `countTick`. Each: another event's name, `{ url }`, `{ synth: {\nnotes: [{ freq, dur, wave }] } }` or `false`.\n- The PLAYER's own sound / volume / \"fewer animations\" live on the device (the profile module draws\n them):\n `const [prefs, setPrefs] = usePlayerPrefs()` → `{ muted, volume, reducedMotion }`.\n\n## Features: screens that find each other\n\nA module registers a **feature**; whoever draws the menu shows it — the base's lobby as tabs (phone:\n4 + \"More\", tablet: more, desktop: a side rail; the base takes the shared-UI role `\"menu\"`), or the\nhost's floating ☰ when nobody takes the role. Tab order and main tabs can be overridden in\n`src/app.config.ts` (`lobby.order`, `lobby.primary`).\n\n```ts\n// module.ts\nsetup(ctx) {\n ctx.features.register({\n id: \"forge\", label: \"Forge\", icon: \"hammer\", group: \"economy\", order: 60,\n primary: false, // true = a main tab of the lobby\n Screen: ForgeScreen, // (props: FeatureScreenProps) => ReactNode; props.args, props.close()\n available: false, // hidden until the title has something for it — see below\n });\n // Badge (\"something to claim\"): ctx.features.setBadge(\"forge\", 2)\n // The title has forge recipes → show the tab; the reason is for the AI Coder's agent only:\n ctx.features.setAvailable(\"forge\", hasRecipes(configSection(ctx.client, \"Craft\")), \"no forge recipes\");\n}\n```\n\n- Open another module's screen without importing it: `ctx.features.open(\"store\")`,\n `ctx.features.open(\"chat\", { userID })`. It returns `false` when the module is not installed or its\n tab is hidden (`ctx.features.get(\"store\")?.available`). From a game (the lobby is off screen) the\n host shows the screen in a window.\n- A screen that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows — from the client config\n (`configSection(client, \"Store\")`) or a read it makes anyway (a badge watcher); no extra request\n at start-up: every module's first reads arrive together and brush the per-player rate limit.\n- Groups: `economy`, `events`, `social`, `profile`. Icons are kit glyph names (`shop`, `hero`,\n `bag`, `trophy`, `gift`, `hammer`, `market`, `offer`, `flame`, `friends`, `chat`, `link`,\n `swords`, `user`, `bulb`, …) or an image URL.\n- Something that must be on screen all the time (a daily auto-popup, an overlay) is a panel:\n `ctx.registerPanel({ id, slot: \"modal\", activeOnly: false, component })`. Background work with\n NOTHING to draw - keeping a badge in step, a poll - is NOT a panel: use `ctx.effect(() => { …; return\ncleanup })` / `ctx.every(ms, run)` in `setup()` (cleaned up on logout).\n- **A badge must go out the moment the reward is taken.** Count it in a `ctx.effect` watcher from the\n CURRENT cache on every change — `client.on(\"user:anyUpdated\", update)` reading\n `client.data.user.state` — not in an effect keyed on a state object (the cache patches it in place,\n so such an effect does not re-run and the badge stays lit). A badge counted from data the screen\n re-reads (a storefront, active events) also listens to that read's event\n (`store:storefrontLoaded`…), and a store that several places reload runs one read at a time and keeps\n what it has when a read fails — see `modules/events` / `modules/deals`.\n- **Never re-read at start-up what the login already brought** — read through the central cache\n (`client.cache`, core ≥ 0.21). State containers: `client.cache.ensureState([\"Quest\"])` (no request\n while the container's `Rev` is confirmed; the server only when it must compute something — a new\n quest cycle, a hero given by default); a screen that wants changes made elsewhere passes\n `{ maxAgeMs: 60_000 }`. Definitions: `client.x.getDefinitions()` answers from the title config cache.\n Server views (storefront, active events/deals/season, unread counters, leaderboard progress) are\n cached by their service — call them freely, and take the data FROM THE ANSWER: the login usually\n brings them before a watcher subscribes, and a read the cache answers emits no `*Loaded` event. A game's own server data: `defineView` + `client.cache.read`.\n- A section the engine does not put into the client config (`TimedBoost`, `Marketplace`, `Match`,\n `AI`) is read with the service's own `getDefinitions()` — it lands in the same config section and is\n kept under the config version, so the next login does not ask again.\n- Auto-popups (`<Popup queued>`) wait while a game mode is on screen and show in the lobby.\n\n## Building a screen on the kit\n\nKeep the screen a short list; put every detail in a **popup**. Form controls: `TextInput` / `TextArea` (the kit's text field in the theme's colours - pass `style` only for width/size) and `Field` (a label above a control); do not hand-style an `<input>`.\n\n```tsx\nimport {\n Button,\n Popup,\n ResourceList,\n Tabs,\n useCelebrate,\n useToast,\n useUiKit,\n errorText,\n grantedBy,\n panel,\n outlined,\n v,\n} from \"@idosgames/react/ui\";\n\nfunction ForgeScreen(): ReactNode {\n const [open, setOpen] = useState<string | null>(null);\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n }}\n >\n {/* rows… onClick={() => setOpen(id)} */}\n {open ? (\n <Popup title=\"Forge\" onClose={() => setOpen(null)} width={420}>\n …\n </Popup>\n ) : null}\n </div>\n );\n}\n```\n\n- `Popup` is a bottom sheet on a phone and a centred window elsewhere (`variant=\"center\"` forces\n centred); it animates in and out and plays open/close sounds. Close from inside with\n `usePopupClose()`. `ConfirmPopup` for yes/no. A popup that opens BY ITSELF (not on a tap) passes\n `queued` — auto-popups then show one at a time instead of stacking.\n- `useCelebrate()(lines)` shows \"You got\" and flies the icons to any element marked\n `data-resource-counter=\"{currencyID}\"` (the lobby's balances are). Build the lines\n from a response with `grantedBy(data)`; a reward with premium (VIP) bundles needs them added.\n- `useUiKit().play(\"claim\")`, `useUiKit().fx.burst(element)` — a sound and a particle burst.\n- **Notifications — `useNotify()`** (the same system as a module's `ctx.notifications`):\n `notify.toast({ level: \"success\", title: \"Saved\" })` — a toast at the top that goes by itself;\n `await notify.alert({ level: \"warning\", title, message, actions })` — a window the player must\n answer, queued one at a time. Levels `info | success | warning | error | reward` pick the colour,\n icon, sound and how long a toast stays (errors longest). `icon` (a glyph, emoji or URL), `image` (a\n thumbnail / a big picture in the window), `source` (a feature id — its name on the card, a tap opens\n it), `key` (the same key replaces instead of stacking; identical toasts merge into \"×2\"),\n `duration` (`0` — until closed), `actions`. Refusals: `notify.toast({ level: \"error\", message:\nerrorText(res.error) })`; never re-check server rules. `useToast()(text, kind)` is the old short\n form of the same thing.\n- Lines of resources: `grantLines(grant)`, `costLines(cost)`, `bundleLines(bundle)`,\n `canAfford(lines, balances)`, `<ResourceList lines={…} />`. Names and icons: `useCatalog()`.\n- A crypto balance is ONE number — the backed crypto plus its IOU (`{ID}_IOU`, the unbacked part of\n rewards; it spends like the crypto): `cryptoBalance(state, id, catalog.cryptoCurrencies)` →\n `{ backed, unbacked, total }`, `<CryptoBalanceChip currencyID=\"Main\" />` (the total, the split in a\n hover/tap popover; the lobby's top row shows it). Never draw `{ID}_IOU` as a currency of its own\n (`iouCurrencies(...)` lists them), never round a crypto amount to whole numbers (`formatCryptoAmount`).\n- Words: `makeT(EN, RU)` for your strings, `plural(n, \"лот|лота|лотов\")` for counts.\n- ⚠ A root `display: grid` with a wide child (a row of tabs) grows past a phone screen — give it\n `gridTemplateColumns: \"minmax(0, 1fr)\"`.\n- ⚠ **No colour literals in a screen** (`\"#fff\"`, `\"rgba(0,0,0,.3)\"`, `\"#5a3fd0\"`): they break the\n moment the theme changes (white text on a light panel, a black stripe under a pastel button). Use\n the tokens: text `v.text` / `v.textDim` (or `...outlined`), surfaces `panel` / `well` / `v.wellSoft`,\n lines `v.line`, dimming `v.backdrop`. A coloured banner, \"your row\", a highlighted day →\n `...toneSurface(\"blue\" | \"green\" | \"gold\" | \"red\")`: it fills with that tone AND switches the text\n inside it to the tone's own readable colour. Text on a filled button — `ON_TONE[tone]`. A blend:\n `` `color-mix(in srgb, ${v.gold} 20%, transparent)` ``.\n\n## Tutorial hints\n\nThe `tutorials` module highlights any element marked `data-tutorial-anchor=\"…\"` — the lobby's tabs\nare `tab:{featureID}`, its profile button `lobby:profile`, its wallet `lobby:wallet`, a game's \"Play\"\nbutton `play:{modeID}`. Mark the controls of your own screen the same\nway; the publisher points tutorial steps at them in LiveOps (Tutorial → step → AnchorID).\n",
|
|
4
|
+
"content": "---\nname: idosgames-ui-kit\ndescription: >-\n Build and restyle game screens with the iDosGames UI kit (@idosgames/react/ui)\n and the ready system modules: change colours, animations and sounds in ONE\n file (src/ui.config.ts), install a ready system (shop, VIP, heroes, inventory,\n chests, quests, leaderboard, daily rewards, season, friends, chat, profile,\n workshop, AI studio …) instead of writing it, open or register a screen\n (ctx.features.open / register, setAvailable), split a dense screen into\n popups, play UI sounds, celebrate a reward, mark tutorial anchors and read\n sound / motion prefs. Use this whenever the user asks to change how the\n game's interface looks, sounds or animates, or to add a screen, menu, popup,\n reward animation, bottom navigation or settings screen, or touches\n @idosgames/react/ui, defineUiConfig, ui.config.ts, ctx.features,\n useCelebrate, usePlayerPrefs, Popup, Tabs or ResourceList.\n---\n\n# The UI kit and the system modules\n\nThe app's interface is the base's lobby (from `@idosgames/app-shell`) plus **system modules**, all on one **UI kit**:\n\n- `@idosgames/react/ui` — the kit: components (buttons, tabs, popups, resource lists, item tiles,\n progress bars, timers), the reward popup, toasts, UI sounds (synthesized, no files), animations,\n and the player's own sound / motion prefs.\n- system modules — the platform's systems, each a whole `feature` module (its screen = a lobby\n tab, its client logic, its demo settings). The base installs six of them into every new project\n (`inBase`: profile, inventory, invite, friends, quests, mailbox); the rest are installed only when the app\n needs them (a game's `uses`, or a task that needs what they `provide`). ONE system per player-facing module of the engine\n (`API/Client/v2/<Module>`) — the owner's rule since 29.09.2026. The module id is the feature id:\n\n| Module | Engine module | Screen |\n| ------------- | -------------- | ------------------------------------------------------------- |\n| `store` | Store+Purchase | shop: stores as tabs, offers, purchase popup |\n| `premium` | Premium | VIP passes: benefits, timer, free trial, buy / extend |\n| `character` | Character | heroes: gear slots, \"Equip best\", stats, ranks, skins |\n| `inventory` | Item | items by rarity, item card (always shown, even with no items) |\n| `lootboxes` | Lootbox | chests: every way to open, ×1 / ×10, opening animation |\n| `boosts` | TimedBoost | running boosts + global happy hours, activate a boost |\n| `leaderboard` | Leaderboard | tables, my place, rank rewards, last cycle's prize |\n| `rewards` | Reward | daily login calendar, greeting popup |\n| `quests` | Quest | quests by cycle, details, claim all, points track |\n| `collections` | Collection | album of sets, packs, chests for points, card trades |\n| `craft` | Craft | recipes, contract popup |\n| `marketplace` | Marketplace | lots, auctions, selling, trades |\n| `deals` | DealOffer | special offers, auto-popup of a new deal |\n| `events` | TimedEvent | limited-time events, milestones, VIP prize |\n| `coop-events` | CoopEvent | team event: join a group, spin to build, team prize |\n| `season` | Season | season tiers, VIP prize |\n| `friends` | Social | friends, requests, suggestions, news |\n| `chat` | Chat | channels, private chats |\n| `invite` | Referral | own code and link, friend's code, rewards (always shown) |\n| `pvp` | Match | duels, strategy, fight replay |\n| `multiplayer` | Multiplayer | rooms: list, create / join by code, ready, chat, host \"Play\" |\n| `profile` | User | name, sound, language, consent, delete account |\n| `tutorials` | Tutorial | hint overlay + replay list |\n| `workshop` | Workshop | players' and the publisher's content: catalog, publish |\n| `ai` | AI | AI studio: the title's AI features for players, history |\n\nNot a system on purpose: Currency, Authentication, Blockchain (the base — balances, sign-in,\nwallet); GameLoop (the `board-game` game); CommunityMarketing (idosgames.com only); Mailbox and\nAdvertising (no client handler / no web ad network); Analytics, Experiment, Push, Localization,\nCloudCode, DataCollections, TitleCustomData, UserCustomData (services the systems and games call).\n\nHand-offs between systems go through `ctx.features.open(id, args)`: inventory → `character`\n(`{ itemInstanceID }`) and → `lootboxes` (`{ itemID }` of a chest or key); events / season →\n`premium` (`{ premiumID }`). Multiplayer: the `multiplayer` system owns the rooms; a game registers\nin `ctx.multiplayer` and takes the room in `start(room)` (skill **multiplayer-realtime**); the lobby\nshows \"With friends\" next to such a game's \"Play\".\n\n**Need one of these? Install the module — don't write the screen.** Every screen follows the\nserver's rules and ships demo title settings (`title-seed.json`). The app does not need one → remove it\nfrom `src/modules.ts`, its tab is gone. The lobby itself (tabs, balances, wallet, profile, \"Play\") is\nthe base, `@idosgames/app-shell` (configured by `src/app.config.ts`, styled by `src/ui.config.ts`).\n\n## Change the look, animations and sounds — only in `src/ui.config.ts`\n\n```ts\nimport { defineUiConfig } from \"@idosgames/react/ui\";\n\nexport default defineUiConfig({\n theme: {\n bgTop: \"#10263f\",\n panel: \"#1b3a5c\",\n gold: \"#ffcc33\",\n radius: 12,\n rarity: { Mythic: \"#ff3df5\" },\n },\n motion: { speed: 1.5, intensity: 0.5, particles: false },\n sound: {\n volume: 0.4,\n events: {\n click: \"tab\",\n countTick: false,\n reward: { url: \"https://…/win.mp3\" },\n },\n },\n});\n```\n\n- Every field is optional; what you leave out keeps its default. The host passes the file to\n `mountHost({ ui })`, and every module on the kit follows it at once.\n- ⚠ **Never restyle a catalog module by editing it.** An edited module is marked \"customized\" and\n stops getting catalog updates. Colours, radii, fonts, animation speed and sounds are all here.\n- **One theme for the WHOLE app:** sign-in (the SDK's `DefaultLoginScreen`, mounted by\n `src/main.tsx`), the lobby, every system and the host's own screens all draw from\n these tokens. A new look — even a light theme — is an edit of this file ONLY: never recolour the\n sign-in, the logo, icons or `index.html` by hand (the logo and the sign-in icons are painted with the\n theme's colours, the page background follows `bgBottom`).\n- Theme tokens: `bgTop`, `bgBottom`, `panel`, `panelDeep`, `panelEdge`, `text`, `textDim`, `gold`,\n `goldDeep`, `green`, `greenDeep`, `blue`, `blueDeep`, `red`, `redDeep`, `shadow`, `backdrop`,\n `radius`, `buttonRadius`, `font`, `rarity` (rarity id → frame colour).\n- **Derived tokens — usually leave them out.** They are computed from the colours above: set a light\n `panel` and dark `text`, and the dark text outline disappears (`textOutline`), lines and recesses\n turn faint-dark instead of white/black (`line`, `well`, `wellSoft`, `wellShadow`, `edge`), cards get a\n soft shadow (`panelShadow`), and buttons of a pastel tone get dark text (`onGreen`, `onGold`,\n `onBlue`, `onRed`). Set one only to override the computed value.\n- A light theme is just colours:\n `theme: { bgTop: \"#f4fcf8\", bgBottom: \"#d9f0e6\", panel: \"#ffffff\", panelDeep: \"#f1faf5\",\npanelEdge: \"#bfe6d3\", text: \"#1f3b30\", textDim: \"#5f7d71\", green: \"#7ee0b5\", greenDeep: \"#4cc493\" }`.\n- Motion: `enabled`, `speed` (2 = twice as fast), `intensity` (0 = no bounce), `rewardFlight`,\n `particles`, `stagger`, `respectReducedMotion`.\n- Sound events: `click`, `tab`, `open`, `close`, `reward`, `coin`, `purchase`, `claim`, `error`,\n `notify`, `lootbox`, `levelUp`, `countTick`. Each: another event's name, `{ url }`, `{ synth: {\nnotes: [{ freq, dur, wave }] } }` or `false`.\n- The PLAYER's own sound / volume / \"fewer animations\" live on the device (the profile module draws\n them):\n `const [prefs, setPrefs] = usePlayerPrefs()` → `{ muted, volume, reducedMotion }`.\n\n## Features: screens that find each other\n\nA module registers a **feature**; whoever draws the menu shows it — the base's lobby as tabs (phone:\n4 + \"More\", tablet: more, desktop: a side rail; the base takes the shared-UI role `\"menu\"`), or the\nhost's floating ☰ when nobody takes the role. Tab order and main tabs can be overridden in\n`src/app.config.ts` (`lobby.order`, `lobby.primary`).\n\n```ts\n// module.ts\nsetup(ctx) {\n ctx.features.register({\n id: \"forge\", label: \"Forge\", icon: \"hammer\", group: \"economy\", order: 60,\n primary: false, // true = a main tab of the lobby\n Screen: ForgeScreen, // (props: FeatureScreenProps) => ReactNode; props.args, props.close()\n available: false, // hidden until the title has something for it — see below\n });\n // Badge (\"something to claim\"): ctx.features.setBadge(\"forge\", 2)\n // The title has forge recipes → show the tab; the reason is for the AI Coder's agent only:\n ctx.features.setAvailable(\"forge\", hasRecipes(configSection(ctx.client, \"Craft\")), \"no forge recipes\");\n}\n```\n\n- Open another module's screen without importing it: `ctx.features.open(\"store\")`,\n `ctx.features.open(\"chat\", { userID })`. It returns `false` when the module is not installed or its\n tab is hidden (`ctx.features.get(\"store\")?.available`). From a game (the lobby is off screen) the\n host shows the screen in a window.\n- A screen that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows — from the client config\n (`configSection(client, \"Store\")`) or a read it makes anyway (a badge watcher); no extra request\n at start-up: every module's first reads arrive together and brush the per-player rate limit.\n- Groups: `economy`, `events`, `social`, `profile`. Icons are kit glyph names (`shop`, `hero`,\n `bag`, `trophy`, `gift`, `hammer`, `market`, `offer`, `flame`, `friends`, `chat`, `link`,\n `swords`, `user`, `bulb`, …) or an image URL.\n- Something that must be on screen all the time (a daily auto-popup, an overlay) is a panel:\n `ctx.registerPanel({ id, slot: \"modal\", activeOnly: false, component })`. Background work with\n NOTHING to draw - keeping a badge in step, a poll - is NOT a panel: use `ctx.effect(() => { …; return\ncleanup })` / `ctx.every(ms, run)` in `setup()` (cleaned up on logout).\n- **A badge must go out the moment the reward is taken.** Count it in a `ctx.effect` watcher from the\n CURRENT cache on every change — `client.on(\"user:anyUpdated\", update)` reading\n `client.data.user.state` — not in an effect keyed on a state object (the cache patches it in place,\n so such an effect does not re-run and the badge stays lit). A badge counted from data the screen\n re-reads (a storefront, active events) also listens to that read's event\n (`store:storefrontLoaded`…), and a store that several places reload runs one read at a time and keeps\n what it has when a read fails — see `modules/events` / `modules/deals`.\n- **Never re-read at start-up what the login already brought** — read through the central cache\n (`client.cache`, core ≥ 0.21). State containers: `client.cache.ensureState([\"Quest\"])` (no request\n while the container's `Rev` is confirmed; the server only when it must compute something — a new\n quest cycle, a hero given by default); a screen that wants changes made elsewhere passes\n `{ maxAgeMs: 60_000 }`. Definitions: `client.x.getDefinitions()` answers from the title config cache.\n Server views (storefront, active events/deals/season, unread counters, leaderboard progress) are\n cached by their service — call them freely, and take the data FROM THE ANSWER: the login usually\n brings them before a watcher subscribes, and a read the cache answers emits no `*Loaded` event. A game's own server data: `defineView` + `client.cache.read`.\n- A section the engine does not put into the client config (`TimedBoost`, `Marketplace`, `Match`,\n `AI`) is read with the service's own `getDefinitions()` — it lands in the same config section and is\n kept under the config version, so the next login does not ask again.\n- Auto-popups (`<Popup queued>`) wait while a game mode is on screen and show in the lobby.\n\n## Building a screen on the kit\n\nKeep the screen a short list; put every detail in a **popup**. Form controls: `TextInput` / `TextArea` (the kit's text field in the theme's colours - pass `style` only for width/size), `Select` (a drop-down in the same look, `<option>`s as children; react ≥ 0.5) and `Field` (a label above a control); do not hand-style an `<input>` or a `<select>`. A choice of a few options is `Tabs` (pass `style={{ flexWrap: \"wrap\" }}` in a narrow column).\n\n```tsx\nimport {\n Button,\n Popup,\n ResourceList,\n Tabs,\n useCelebrate,\n useToast,\n useUiKit,\n errorText,\n grantedBy,\n panel,\n outlined,\n v,\n} from \"@idosgames/react/ui\";\n\nfunction ForgeScreen(): ReactNode {\n const [open, setOpen] = useState<string | null>(null);\n return (\n <div\n style={{\n display: \"grid\",\n gridTemplateColumns: \"minmax(0, 1fr)\",\n gap: 12,\n }}\n >\n {/* rows… onClick={() => setOpen(id)} */}\n {open ? (\n <Popup title=\"Forge\" onClose={() => setOpen(null)} width={420}>\n …\n </Popup>\n ) : null}\n </div>\n );\n}\n```\n\n- `Popup` is a bottom sheet on a phone and a centred window elsewhere (`variant=\"center\"` forces\n centred); it animates in and out and plays open/close sounds. Close from inside with\n `usePopupClose()`. `ConfirmPopup` for yes/no. A popup that opens BY ITSELF (not on a tap) passes\n `queued` — auto-popups then show one at a time instead of stacking.\n- `useCelebrate()(lines)` shows \"You got\" and flies the icons to any element marked\n `data-resource-counter=\"{currencyID}\"` (the lobby's balances are). Build the lines\n from a response with `grantedBy(data)`; a reward with premium (VIP) bundles needs them added.\n- `useUiKit().play(\"claim\")`, `useUiKit().fx.burst(element)` — a sound and a particle burst.\n- **Notifications — `useNotify()`** (the same system as a module's `ctx.notifications`):\n `notify.toast({ level: \"success\", title: \"Saved\" })` — a toast at the top that goes by itself;\n `await notify.alert({ level: \"warning\", title, message, actions })` — a window the player must\n answer, queued one at a time. Levels `info | success | warning | error | reward` pick the colour,\n icon, sound and how long a toast stays (errors longest). `icon` (a glyph, emoji or URL), `image` (a\n thumbnail / a big picture in the window), `source` (a feature id — its name on the card, a tap opens\n it), `key` (the same key replaces instead of stacking; identical toasts merge into \"×2\"),\n `duration` (`0` — until closed), `actions`. Refusals: `notify.toast({ level: \"error\", message:\nerrorText(res.error) })`; never re-check server rules. `useToast()(text, kind)` is the old short\n form of the same thing.\n- Lines of resources: `grantLines(grant)`, `costLines(cost)`, `bundleLines(bundle)`,\n `canAfford(lines, balances)`, `<ResourceList lines={…} />`. Names and icons: `useCatalog()`.\n- A crypto balance is ONE number — the backed crypto plus its IOU (`{ID}_IOU`, the unbacked part of\n rewards; it spends like the crypto): `cryptoBalance(state, id, catalog.cryptoCurrencies)` →\n `{ backed, unbacked, total }`, `<CryptoBalanceChip currencyID=\"Main\" />` (the total, the split in a\n hover/tap popover; the lobby's top row shows it). Never draw `{ID}_IOU` as a currency of its own\n (`iouCurrencies(...)` lists them), never round a crypto amount to whole numbers (`formatCryptoAmount`).\n- Words: `makeT(EN, RU)` for your strings, `plural(n, \"лот|лота|лотов\")` for counts. A table answers\n in the CURRENT language at every call, so a module-level `t` follows the language switch.\n- Interface language: the player's choice — `setLanguage(\"en\" | \"ru\")`, `LanguageSwitch` (English /\n Русский, already in the profile screen and the base's account popup), `useLanguage()` re-renders a\n component on change, `onLanguageChange(fn)`. Saved in localStorage `idos:language`; the window\n event `idos:language` tells games without the kit (VoxelCraft `src/i18n.ts` listens and fires it\n from its pause menu). Tab labels follow only as functions: `label: () => t(\"title\")`.\n- ⚠ A root `display: grid` with a wide child (a row of tabs) grows past a phone screen — give it\n `gridTemplateColumns: \"minmax(0, 1fr)\"`.\n- ⚠ **No colour literals in a screen** (`\"#fff\"`, `\"rgba(0,0,0,.3)\"`, `\"#5a3fd0\"`): they break the\n moment the theme changes (white text on a light panel, a black stripe under a pastel button). Use\n the tokens: text `v.text` / `v.textDim` (or `...outlined`), surfaces `panel` / `well` / `v.wellSoft`,\n lines `v.line`, dimming `v.backdrop`. A coloured banner, \"your row\", a highlighted day →\n `...toneSurface(\"blue\" | \"green\" | \"gold\" | \"red\")`: it fills with that tone AND switches the text\n inside it to the tone's own readable colour. Text on a filled button — `ON_TONE[tone]`. A blend:\n `` `color-mix(in srgb, ${v.gold} 20%, transparent)` ``.\n\n## Tutorial hints\n\nThe `tutorials` module highlights any element marked `data-tutorial-anchor=\"…\"` — the lobby's tabs\nare `tab:{featureID}`, its profile button `lobby:profile`, its wallet `lobby:wallet`, a game's \"Play\"\nbutton `play:{modeID}`. Mark the controls of your own screen the same\nway; the publisher points tutorial steps at them in LiveOps (Tutorial → step → AnchorID).\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voxelcraft-characters",
|
|
3
|
+
"description": "VoxelCraft characters and item looks from a recipe: the player's own blocky character (CharacterSpec — skeleton template, boxes on bones, palette, pixel-art faces with expressions), its animation (state graph, actions, emotes, emotions), item looks (ItemSkinSpec — a new look of an existing item or cosmetics worn on the body: hats, capes, wings), NPCs and custom mobs made from a character. Built from reference pictures (front/side/back) and a description with InApp AI (client.ai, features \"voxel-character\" and \"voxel-item\") or without AI. Covers the recipe formats, the Character studio, 'My characters / My items', the third-person camera, the emote wheel, network sync of looks, the Workshop types voxelcraft.character / voxelcraft.item and the agent actions. Use it whenever the user wants a player skin, avatar, character, NPC, creature, outfit, weapon look, animation, emote or emotion in the voxelcraft module — even without naming the module.",
|
|
4
|
+
"content": "---\nname: voxelcraft-characters\ndescription: >-\n VoxelCraft characters and item looks from a recipe: the player's own blocky\n character (CharacterSpec — skeleton template, boxes on bones, palette,\n pixel-art faces with expressions), its animation (state graph, actions,\n emotes, emotions), item looks (ItemSkinSpec — a new look of an existing item\n or cosmetics worn on the body: hats, capes, wings), NPCs and custom mobs made\n from a character. Built from reference pictures (front/side/back) and a\n description with InApp AI (client.ai, features \"voxel-character\" and\n \"voxel-item\") or without AI. Covers the recipe formats, the Character studio,\n 'My characters / My items', the third-person camera, the emote wheel, network\n sync of looks, the Workshop types voxelcraft.character / voxelcraft.item and\n the agent actions. Use it whenever the user wants a player skin, avatar,\n character, NPC, creature, outfit, weapon look, animation, emote or emotion in\n the voxelcraft module — even without naming the module.\n---\n\n# VoxelCraft characters (CharacterSpec, ItemSkinSpec)\n\nLike a world (see `voxelcraft-worlds`), a character is a **recipe**: the AI, the\nno-AI path, the studio and the agent all write the same JSON, and the game builds\nthe model itself. One format, many consumers:\n\n```\npictures + description ─► AI (Text) ─► CharacterSpec / ItemSkinSpec (JSON)\nno AI: template + colours from a picture ─┘ │\n parseCharacterSpec / parseItemSkin (the only doors)\n ▼\n layoutCharacter (pure data, golden-tested) ─► CharacterAsset (GPU, cached by hash)\n ▼\n CharacterView (bones, sockets, face decal) ◄── Animator (layers, graph, emotions)\n ▲ ▲ ▲ ▲\n own player other players NPC / mob AnimInput (one struct for all)\n```\n\nSource: `modules/voxelcraft/src/character/`.\n\n## CharacterSpec\n\nCoordinates are model **pixels** (16 px = 1 block): `x` to the character's right\nhand, `y` up (0 = soles), `z` backwards — the face looks toward **−z**. A person\nis 32 px tall.\n\n| Field | What |\n| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `rig` | skeleton template: `humanoid`, `quadruped`, `flyer`, `blob` (`character/rigs/*.ts`, one file each) |\n| `scale` | 0.25..3 (players use 0.8..1.15) |\n| `palette` | one character → `#rrggbb` (≤ 32; `.` and space are transparent) |\n| `textures` | id → `{ w, h, rows }` pixel art from palette characters (≤ 32×32, ≤ 16k px total) |\n| `parts` | ≤ 64 boxes: `{ bone, size, pos (centre), color, faces?, rot?, mirror? }` |\n| `bones` | ≤ 16 extra bones `{ name, parent, pivot, role }` — roles `tail`, `wingL`, `wingR`, `ear`, `hair`, `cape`, `antenna`, `static` move by themselves |\n| `face` | `{ bone, rect: [x,y,w,h] on the head's front, expressions: { neutral, blink, happy, sad, angry, surprised, scared, tired, hurt, talk } }` — a decal; changing the expression only moves its UV |\n| `style` | `walk` (`normal`, `bouncy`, `heavy`, `sneaky`, `robotic`), `idle` (`calm`, `fidgety`, `proud`), `energy` 0..1 |\n| `voice`, `tags` | reserved for NPC speech and behaviour |\n| `finish` | `{ noise, outline, shade, density }` — fabric noise, a dark outline on every box face, top-to-bottom shading, texels per model pixel (defaults 0.5 / 0.4 / 0.25 / 2); 0 = a clean flat look |\n\n`mirror: true` adds the mirrored copy on the paired bone (`armL` → `armR`).\nFaces of a box: `front` (−z), `back`, `left` (character's left, −x), `right`,\n`top`, `bottom`; a texture is stretched over the face, first row on top.\n\n`parseCharacterSpec` never crashes the game: it fills defaults, clamps numbers,\nmoves boxes on unknown bones to the body, gives colours outside the palette their\nown character, drops unknown texture characters, and reports a cut-off answer as\n`truncated`. The default character \"Путник\" (`defaultCharacter.ts`) is the\nreference recipe.\n\n**Golden test.** `layoutCharacter` output is pinned in `character.test.ts`:\ncustom mobs carry their recipe in world saves, so a change there changes the look\nof saved creatures — understand why before re-pinning.\n\n## The detailed style\n\nThe game's look (owner's reference, 11.10.2026): blocky people with fine pixel-art\nclothes. Textures are drawn at 2 texels per model pixel (an 8×8 face is 16×16),\nvolume comes from layered boxes 0.2–0.6 px bigger than what they cover (hair cap,\nnose, rolled sleeves with cuffs, belt, boots), and `finish` adds fabric noise,\noutlines and shading to every face. `humanSource(look)` in `defaultCharacter.ts`\nbuilds such a person from parts (`top`: shirt / tee / sweater, `bottom`: pants /\nshorts, `shoes`: boots / sneakers, `hair`: short / long / curly / none, `hat`,\n`colors`); NPCs and the studio's \"Outfit\" row use it, `recolor()` keeps shades in\nstep with a base colour.\n\n## Animation\n\n`character/anim/` — everything is data, addressed by bone **role**, so any\nskeleton gets every animation it has bones for:\n\n- `LOCOMOTION` (Animator.ts) — the state graph by priority: dead, sleep, ride,\n sit, swim, glide, fly, fall, sneak, run, walk, idle; transitions blend.\n- `clips.ts` — procedural clips (`RIG_CLIPS` swaps them per template: a blob\n hops); actions `swing`, `place`, `throw`, `hurt`, `land`; hold-RMB `eat`,\n `drink`, `draw` (bow), `block` (shield).\n- `emotes.ts` — keyframe emotes as data: `wave`, `cheer`, `dance`, `point`,\n `laugh`, `cry`, `bow`, `shrug`, `facepalm`, `sit`.\n- `emotions.ts` — `neutral`, `happy`, `sad`, `angry`, `surprised`, `scared`,\n `tired`, `hurt`: face via a fallback chain, posture, tempo; `EMOTION_EVENTS`\n maps game events (damage, achievement, tamed, boss killed…) to emotions;\n chat smileys set an emotion.\n- `AnimInput` (types.ts) is the one struct every owner fills (player, remote\n player, NPC, mob). A new game state = a new field there.\n\nNew state → a line in `LOCOMOTION`; new clip → `clips.ts`; new emote →\n`emotes.ts`; new emotion → `emotions.ts`. The animator never changes.\n\n## Item looks (ItemSkinSpec)\n\n`character/items/`. Shapes like world models (`box`, `cylinder`, `cone`,\n`sphere`, `line`, `mirror`) with a palette colour instead of a block, in a pixel\ngrid ≤ 32³.\n\n- `kind: \"held\"` + `base` (an existing item): the look lives in the stack's\n `tag.skin` (hash); damage, durability, crafting and network stay the base\n item's. Built standing up: handle at `grip.at`, tip up (+y), flat side to −z;\n `grip.pose`: `one_hand`, `two_hand`, `bow`, `shield`, `staff`.\n- `kind: \"wearable\"` + `slot` (`head`, `face`, `back`, `shoulders`, `waist` →\n sockets of the skeleton): cosmetics, no effect on play; wide back pieces are\n wings and open in flight.\n\nLooks used in a world travel in its save (`save.skins`); the registry is\n`skins.ts` (`registerSkin`, `stackSkin`, `stackIconURL`).\n\n## NPCs and custom mobs\n\n`entities/characterMobs.ts`: type `c:<hash>:<behaviour>` with behaviours `npc`,\n`passive`, `neutral`, `hostile`, `pet`. Stats come from the preset table — never\nfrom the AI. The recipe rides with the mob in saves and network snapshots\n(`look`), and a recipe whose hash does not match its type is refused. NPCs from\na world recipe (`WorldSpec.npcs`) get a deterministic look from their name\n(`npcLook.ts`) and stand up once their place is loaded (`game/npcs.ts`).\n\n## AI features of the title\n\nTwo Text features with images, configured like `\"voxel-world\"`:\n`\"voxel-character\"` and `\"voxel-item\"`. Copy the system prompts from\n`ai/characterPrompt.ts` (`VOXEL_CHARACTER_SYSTEM_PROMPT`,\n`VOXEL_ITEM_SYSTEM_PROMPT`) into the title config\n(`AI.Features[...].Behavior.SystemInstructions`); `AllowImages: true`,\n`MaxInputImages: 4`; a reasoning model needs `MaxTokens` ≈ 32000 (thinking\ncounts against it; a cut-off answer is charged and reported as `truncated`). The\nmechanics (one key per press, retry, polling, resume after reload) are shared\nwith worlds in `ai/specAi.ts`. \"Edit with AI\" sends the previous recipe when it\nfits the feature's `MaxPromptChars`.\n\n## In the game\n\n- The studio lives in the **SDK lobby** (`src/lobby/`, UI kit), not in the game:\n the lobby's home shows the player's character on a pedestal (`lobby/hero.ts`,\n `ctx.lobby.setHero`) with \"Edit\" / \"Emote\"; tabs **Character**\n (`CharacterScreen.tsx`: pictures with angles, description, skeleton, AI or\n colours from the picture, outfit, palette and height, turntable with states,\n emotes and emotions, \"Play as this character\", summon NPC/mob while the game is\n open in creative), **Items** (`ItemsScreen.tsx`: look of an item / wearable,\n wear, apply to the item in hand) and **Locker** (`LockerScreen.tsx`). The look\n is shared by the lobby and the game through `character/Looks.ts` (`hub.looks`).\n- **F5** — first person → back → front. **G** — emote wheel. `/emote <id>`,\n `/feel <emotion> [sec]`.\n- \"My characters / My items\" — IndexedDB `voxelcraft-characters`; the chosen\n character and the outfit are in localStorage and follow the player across\n worlds.\n- Network: `PlayerMeta.look/outfit/heldSkin` (hashes), message `LOOK` (recipes,\n resent when a player joins), `EMOTE`. A look that has not arrived yet shows\n the default character.\n- Workshop: types `voxelcraft.character` and `voxelcraft.item` (main JSON =\n the recipe) — they must be declared in the title's `Workshop.ContentTypes`.\n\n## Agent actions\n\n`loadCharacterSpec { spec, play? }`, `loadItemSkin { spec, wear?, give? }`,\n`emote { id }`, `setEmotion { emotion, seconds? }`, `setCamera { mode }`,\n`spawnCharacter { behavior, spec? }`, `openStudio { tab? }`; `state().character`\nshows the current look, camera, state, emote, emotion and outfit.\n",
|
|
5
|
+
"references": []
|
|
6
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "voxelcraft-quests",
|
|
3
|
+
"description": "VoxelCraft quest book — quests that double as the survival tutorial for a player who has never played a Minecraft-like game. Chapters and quests are data (parents, tasks, hints, rewards), task kinds are a registry, quest sources are pluggable (the built-in \"Survival\" line today, server quests from client.quest later). Hints get more precise when the player is stuck (text → gold «!» marker on the nearest block/mob → recipe as pictures), situational tips (dusk, hunger, low health, a Hisser nearby), the on-screen tracker, the book (J / 📜), English and Russian texts. Use it whenever the user wants quests, missions, a tutorial, onboarding, hints, goals, achievements-like progress or a new language string in the voxelcraft module — even without naming the module.",
|
|
4
|
+
"content": "---\nname: voxelcraft-quests\ndescription: >-\n VoxelCraft quest book — quests that double as the survival tutorial for a\n player who has never played a Minecraft-like game. Chapters and quests are\n data (parents, tasks, hints, rewards), task kinds are a registry, quest\n sources are pluggable (the built-in \"Survival\" line today, server quests from\n client.quest later). Hints get more precise when the player is stuck (text →\n gold «!» marker on the nearest block/mob → recipe as pictures), situational tips (dusk,\n hunger, low health, a Hisser nearby), the on-screen tracker, the book (J /\n 📜), English and Russian texts. Use it whenever the user wants quests,\n missions, a tutorial, onboarding, hints, goals, achievements-like progress or\n a new language string in the voxelcraft module — even without naming the\n module.\n---\n\n# VoxelCraft quest book\n\nFiles (module `modules/voxelcraft/src`):\n\n| File | What |\n| --------------------------- | --------------------------------------------------------------------------------------------------- |\n| `quests/types.ts` | The format: `ChapterDef`, `QuestDef`, `TaskDef`, `HintDef`, `RewardDef`, `QuestSource`, `QuestSave` |\n| `quests/tasks.ts` | `TASK_KINDS` — the task-kind registry, `matches()` for patterns |\n| `quests/survival.ts` | The built-in line \"Survival\" (5 chapters, 28 quests) — `survivalSource` |\n| `quests/tips.ts` | Situational tips `TIPS` |\n| `quests/QuestBook.ts` | The engine: progress, unlocking, rewards, hint timer, tips, save |\n| `ui/quests/QuestTracker.ts` | On-screen tracker (one quest), the quest target `goal()`, \"Quest complete!\" toast |\n| `ui/quests/QuestBookUI.ts` | The book (J, touch 📜) |\n| `ui/quests/strings.ts` | Screen strings (EN/RU), `itemName`, `itemIcon`, `recipeView` |\n| `i18n.ts` | `Loc = { en, ru }`, `tr`, `makeT`, `plural`, `lang`, `setLang` |\n\n## Add a quest\n\nOne entry in `SURVIVAL_QUESTS` (or in your own source):\n\n```ts\n{\n id: \"furnace\", // unique within the source\n chapter: \"iron\", // a ChapterDef id of the same source\n icon: \"furnace\", // item id\n after: [\"stone\"], // parents: opens when all are done\n title: { en: \"Furnace\", ru: \"Печь\" },\n desc: { en: \"...\", ru: \"...\" },\n descTouch: { en: \"...\", ru: \"...\" }, // optional: touch buttons ⚒ ✋ ▦ ⤒ 📜\n tasks: [\n { kind: \"craft\", of: [\"furnace\"], label: { en: \"Craft a furnace\", ru: \"Сделай печь\" } },\n { kind: \"place\", of: [\"furnace\"], label: { en: \"Place it\", ru: \"Поставь её\" } },\n ],\n hints: [\n { text: {...}, touch: {...}, recipe: \"furnace\" }, // shown at once\n { after: 60, text: {...}, find: { blocks: [\"coal_ore\"] } }, // after 60 s without progress, with the «!» target\n ],\n rewards: [{ item: \"apple\", count: 2 }],\n}\n```\n\n- Every text is `{ en, ru }`, both non-empty. The test checks this.\n- `quests/quests.test.ts` checks that every item, block, mob and recipe id exists, that parents exist and that there are no cycles. Run it after adding a quest.\n- Use the game's own names: the creeper is the **Hisser** (`bomber`), the Nether is **Inferno** (`registry/names.ts`, English — `registry/namesEn.ts`; `labelOf` picks by the game language).\n\n### Patterns in `of` and `find`\n\n- `oak_log` — exact id;\n- `*_log`, `cooked_*` — mask;\n- `#logs` — recipe tag (`registry/tags.ts`);\n- `@hostile` — mob category;\n- empty — anything.\n\n## Task kinds (`TASK_KINDS`)\n\n| kind | counts | fields |\n| ---------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- |\n| `have` | items OBTAINED: picked up / crafted / taken out of a furnace (`itemPickup`, `craft`, `smelt`) since the quest opened, plus what lies in the inventory now — may be spent at once (cook-and-eat, smelt the ore) | `of`, `count` |\n| `craft` | crafted items (`craft` event) | `of`, `count` |\n| `break` | blocks broken by the player | `of`, `count` |\n| `place` | blocks placed | `of`, `count` |\n| `kill` | mobs that died after a hit by the player | `of`, `count` |\n| `eat` | food eaten | `of`, `count` |\n| `walk` | steps | `count` |\n| `wear` | armour worn (state) | `of` |\n| `depth` | reached height `y` or below (state) | `y` |\n| `fullFood` | a full hunger bar (state — already full when the quest opens counts) | — |\n| `event` | any bus event `on` whose payload has the fields `where` | `on`, `where`, `count` |\n\n- **A new kind** is one entry: the events it listens to plus `update(t, prev, ctx, ev, payload)`. Set `state: true` when it depends on the game state rather than on events. Nothing else changes.\n- **Progress never goes down,** and events count only while the quest is open. State kinds are checked when a quest opens, so the order is free.\n- **`nightSurvived`** is an event emitted by the book itself: dawn after a night without death since dusk, sleeping counts.\n\n⚠ **Simple-things rule (owner, 12.10.2026):** a task must count whatever order a beginner does things in.\nNever count \"how many are in the inventory at once\" for things that get spent (food, ore, logs, coal,\nwool) — `have` counts obtained items for that reason. An event-only task must not hang when the state\nit waits for is already true (`fullFood`). `quests.test.ts` keeps both cases.\n\n## A new quest source (server quests, quests from a world recipe, AI)\n\n```ts\nconst server: QuestSource = {\n id: \"server\", // full quest ids become \"server:<id>\"\n chapters: () => [...],\n quests: () => [...], // the same QuestDef format\n claim: async (q) => { /* client.quest… */ return true; }, // the source gives the reward; the book shows \"Claim\"\n progress: (q, values, done) => { /* report progress */ },\n subscribe: (onChange) => { /* call onChange when the list changes */ return () => {}; },\n};\ngame.quests.addSource(server);\n```\n\nA source without `claim` gets its rewards from the book at once. States of quests whose source is not loaded survive the save.\n\n## The player's side\n\n- **Tracker:** ONE quest (owner's rule 12.10.2026: two at once confused players) — the tracked one (`book.track(id)`, the \"Track\" button), else the first open one in order.\n- **Hints:** the main hint is always shown; a more precise one (\"dig down\", a `find` target) appears under it after `after` seconds without progress.\n- **Quest target:** a hint with `find` makes the tracker look for the nearest matching block / mob (`QuestTracker.goal()`); the gold «!» marks it on the compass, the minimap, the big map and in the world (`ui/map`, see `docs/feature-history/voxelcraft-map.md`). Hint texts call it \"the gold marker «!»\", not \"the arrow\".\n- **Hints switch** in the book: it hides hints, the quest target and tips; quests still count. This is a per-player setting for all worlds.\n- **Creative mode:** nothing counts and the tracker is hidden.\n- **Save:** `save.quests` — every world has its own book. A new world and a network guest start from scratch.\n- **Commands:** `/quests`, `/quests done <id>`, `/quests reset`.\n- **Agent:** `state().quests`; actions `completeQuest { id }` and `openQuests`.\n\n## Language\n\n- **Data:** `Loc` = `{ en, ru }`; `tr(loc, vars)` picks the current language.\n- **Screen strings:** `makeT(EN, RU)` — the Russian table must have every key of the English one.\n- **Choice:** by the browser, the same rule as `pickLanguage` of the UI kit (`@idosgames/react/ui`). Change it in both places.\n- **Item names** in the registry are Russian only. English falls back to the prettified id (`itemName`).\n",
|
|
5
|
+
"references": []
|
|
6
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "voxelcraft-worlds",
|
|
3
|
-
"description": "Build VoxelCraft worlds from a recipe (WorldSpec): from up to 4 reference pictures and a description, with InApp AI (client.ai, feature \"voxel-world\") or without it, or by hand. The AI recreates whatever the pictures show — objects built of coloured blocks (planes, cars, statues), landscapes (terrain sketches, rivers, biomes) and whole scenes. Covers the WorldSpec format, a project's start world (src/worlds/startWorld.ts), the \"Create world\" and \"My worlds\" screens, configuring the \"voxel-world\" AI feature, the agent actions generateWorld / loadWorldSpec, and sharing worlds through the Workshop. Use this whenever the user works on the voxelcraft module and wants a world or object from a picture, a generated or themed voxel map, a start world, or touches WorldSpec, SpecGenerator, VoxelModel, parseWorldSpec, buildSpecWithoutAI or
|
|
4
|
-
"content": "---\nname: voxelcraft-worlds\ndescription: >-\n Build VoxelCraft worlds from a recipe (WorldSpec): from up to 4 reference\n pictures and a description, with InApp AI (client.ai, feature \"voxel-world\")\n or without it, or by hand. The AI recreates whatever the pictures show —\n objects built of coloured blocks (planes, cars, statues), landscapes (terrain\n sketches, rivers, biomes) and whole scenes. Covers the WorldSpec format, a\n project's start world (src/worlds/startWorld.ts), the \"Create world\" and\n \"My worlds\" screens, configuring the \"voxel-world\" AI feature, the agent\n actions generateWorld / loadWorldSpec, and sharing worlds through the\n Workshop. Use this whenever the user works on the voxelcraft module and wants\n a world or object from a picture, a generated or themed voxel map, a start\n world, or touches WorldSpec, SpecGenerator, VoxelModel, parseWorldSpec,\n buildSpecWithoutAI or CreateWorldUI — even without naming the module.\n---\n\n# VoxelCraft worlds (WorldSpec)\n\nA VoxelCraft world is built by a **deterministic generator** (`SpecGenerator`)\nfrom a compact JSON **recipe** — the WorldSpec. The same recipe always gives the\nsame world, block for block. Nothing writes the world's blocks directly;\neverything that makes a world writes a recipe:\n\n| Path | Where | What it can do |\n| ------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |\n| **AI** — pictures + description | `createClientWorldAI(client)` → `client.ai.generateText(\"voxel-world\", …)` | anything the pictures show: objects built from blocks, landscapes, scenes |\n| **Without AI** | `buildSpecWithoutAI` — first picture read in the browser (`ImageAnalyzer`), text by keywords (ru/en) | terrain from a top-down map, style from a mood picture, prefab structures by keyword — **no objects from pictures** |\n| **By hand / by an agent** | write the JSON yourself | everything the format allows |\n\nAll paths meet in `parseWorldSpec()` — the only door into the game. It fills\ndefaults, clamps numbers, drops unknown structures, shapes and blocks. A recipe\nnever crashes the generator, wherever it came from.\n\n## The WorldSpec format\n\nWorld coordinates `x`, `z`, `x2`, `z2`, `radius`, river points are **fractions\nof the world (0..1)**: `x=0` west, `x=1` east, `z=0` north, `z=1` south. Every\nfield is optional.\n\n```jsonc\n{\n \"version\": 1,\n \"name\": \"Frost Keep\", // <= 60 chars\n \"description\": \"…\",\n \"seed\": 2026, // number or string; omitted = hash of name\n \"size\": { \"mode\": \"island\", \"widthChunks\": 16, \"depthChunks\": 16 },\n // mode: island | bounded (land to the edges, sea outside) | infinite\n // 4..48 chunks per side (16 blocks each); default island 16×16 = 256×256 blocks\n \"terrain\": {\n \"shape\": \"mountains\", // island | archipelago | continent | valley | mountains | plateau | flat\n \"baseHeight\": 30, // 6..48 — average land height (the world is 64 blocks high)\n \"amplitude\": 20,\n \"roughness\": 0.6,\n \"waterLevel\": 25,\n \"sketch\": { \"rows\": [\"0012345\", \"…\"] },\n // optional TOP-DOWN elevation map drawn by the AI: 4..32 rows (north→south) of 4..32 digits\n // (west→east): 0 deep water, 1 shallow, 2 shore, 3 lowland, 4 plain, 5 hills, 6 high hills,\n // 7 mountains, 8 high mountains, 9 peaks. Smoothed + natural detail; with a sketch the recipe\n // draws its own coastline (no island mask). A top-down PICTURE's heightmap wins over it.\n // heightmap / surfaceMap — filled by the game from a top-down picture; don't write by hand\n },\n \"zones\": [\n // <= 12; each point belongs to the nearest zone (weighted by radius)\n { \"x\": 0.5, \"z\": 0.5, \"radius\": 0.4, \"biome\": \"snow\" },\n // biome: meadow | forest | birch_forest | taiga | snow | desert | badlands | beach |\n // swamp | rocky | savanna | jungle\n // overrides: surface, subsurface (any solid block — even wool), trees 0..1,\n // treeType oak|birch|spruce|palm|dead|none, grass 0..1, flowers 0..1, height -20..20\n ],\n \"structures\": [\n // <= 40\n { \"type\": \"castle\", \"x\": 0.5, \"z\": 0.5, \"size\": \"large\", \"rotation\": 180 },\n // house | tower | castle | village | wall | road | bridge | ruins | pond | rock |\n // tree_cluster | pyramid | well | farm | model | river\n // size small|medium|large; rotation 0|90|180|270 (buildings: door north|east|south|west)\n { \"type\": \"road\", \"x\": 0.25, \"z\": 0.72, \"x2\": 0.5, \"z2\": 0.5 }, // wall/road/bridge: x2,z2\n {\n \"type\": \"river\",\n \"path\": [\n [0.8, 0.2],\n [0.5, 0.5],\n [0.1, 0.95],\n ],\n }, // 2..16 points, source → mouth\n {\n \"type\": \"model\",\n \"model\": \"airplane\",\n \"x\": 0.5,\n \"z\": 0.55,\n \"rotation\": 90,\n \"elevation\": 0,\n },\n ],\n \"models\": { \"airplane\": {/* see below */} }, // <= 4 models, each can be placed many times\n \"npcs\": [\n { \"name\": \"Björn\", \"role\": \"smith\", \"x\": 0.26, \"z\": 0.7, \"persona\": \"…\" },\n ],\n \"palette\": {\n \"wall\": \"planks\",\n \"roof\": \"dark_planks\",\n \"floor\": \"planks\",\n \"path\": \"dirt_path\",\n \"accent\": \"stone_bricks\",\n },\n \"ambience\": { \"timeOfDay\": \"dawn\" }, // dawn | day | sunset | night\n}\n```\n\n### Models — objects built from blocks\n\nThe AI builds an object (a plane, a car, a ship, a statue, a creature) from\n**shapes**, not block by block — a language model reasons well about\n\"fuselage = cylinder, wings = flat boxes, symmetric\", and a shape list stays\nshort. `rasterizeModel()` turns it into blocks.\n\n```jsonc\n\"airplane\": {\n \"size\": [40, 12, 33], // [length x, height y, width z], <= 64 × 40 × 64\n // model coords: x from the FRONT (x=0, the nose) back, y UP (y=0 on the ground), z left→right;\n // the middle plane is z = (width - 1) / 2. Later shapes overwrite earlier; \"air\" carves.\n \"parts\": [ // <= 200\n { \"shape\": \"cylinder\", \"axis\": \"x\", \"center\": [4, 4, 16], \"radius\": 2.5, \"length\": 30, \"block\": \"white_wool\" },\n // center = middle of the START cap; runs `length` blocks along +axis (negative = −axis)\n { \"shape\": \"cone\", \"axis\": \"x\", \"center\": [3, 4, 16], \"radius\": 2.5, \"radius2\": 0.6, \"length\": -4, \"block\": \"white_wool\" },\n { \"shape\": \"box\", \"from\": [14, 3, 1], \"to\": [19, 3, 13], \"block\": \"white_wool\", \"mirror\": \"z\" },\n // \"mirror\": \"x\" | \"z\" also draws the mirror copy across the middle — wings, wheels, lights\n { \"shape\": \"sphere\", \"center\": [x, y, z], \"radii\": [rx, ry, rz], \"block\": \"…\", \"hollow\": true },\n { \"shape\": \"line\", \"from\": [x, y, z], \"to\": [x, y, z], \"radius\": 0, \"block\": \"…\" },\n { \"shape\": \"box\", \"from\": [33, 9, 16], \"to\": [34, 11, 16], \"block\": \"air\" }\n ],\n \"voxels\": [[9, 5, 14, \"light_blue_wool\"]] // <= 400 single blocks for details\n}\n```\n\nPlaced by a `model` structure: centred at `x, z`, turned by `rotation`, its\nbottom on the ground under the centre (on water — on the surface), `elevation`\nlifts it (a plane in flight). The footprint is cleared (trees and bumps don't\ngrow through it). Model blocks written with Minecraft names are forgiven:\n`red_concrete`, `cyan_terracotta`, `minecraft:lime_wool` → our wool of that colour;\n`*_stained_glass` → glass. Anything unknown drops that shape.\n\n### Blocks\n\ngrass, dirt, stone, cobblestone, mossy_cobblestone, sand, red_sand, gravel,\nclay, snow, ice, sandstone, terracotta, stone_bricks, bricks, planks,\ndark_planks, oak_log, spruce_log, birch_log, glass, dirt_path, leaves,\nspruce_leaves, birch_leaves, and wool in 12 colours (white, red, orange,\nyellow, green, blue, light_blue, purple, pink, brown, gray, black — `red_wool`…).\n\nGood to know: a **village** already has houses facing a well, paths and (large)\na farm; a **bridge** needs water under it (`valley` shape, a river, a pond);\na **river** always flows downhill from its first point and cuts its bed through\nhills; keep structures inside `0.2..0.8` on an island; the player spawns on dry\nland nearest the centre, away from structures.\n\n## The project's start world\n\n`src/worlds/startWorld.ts` (in a project: `src/modules/voxelcraft/src/worlds/startWorld.ts`):\n\n```ts\nexport const START_WORLD: unknown = { name: \"Frost Keep\", terrain: { shape: \"mountains\" }, … };\n```\n\n`null` (default) = the classic seed-1337 noise world. A player with a saved\nworld continues it (\"My worlds\"). Easiest way to get a recipe: in the game,\n**Create world → Download recipe**, then paste the JSON as an object.\n\n## In the game\n\n- **Create world** (pause overlay): up to **4 pictures** (drop, click, Ctrl+V —\n e.g. one car from several angles), description, size (island 256 / large 512\n / infinite / custom), how to read the pictures:\n - **Образец для ИИ** (default) — the AI decides from the description what the\n pictures are (an object to build, a landscape to recreate, a style);\n - **Карта сверху** — the first picture is a top-down map: blue = water, land\n rises from the shore, pixel colour → nearest block (works without AI too);\n - **Настроение** — only climate, colours, materials.\n Then with or without AI, a **mini-map preview** (red dot = spawn), play /\n download / load recipe.\n- **My worlds**: every world autosaves to IndexedDB (every 30 s of play, on\n pause, before switching worlds); the last one reopens on the next launch.\n\n## AI: the \"voxel-world\" feature of the title\n\nThe AI option appears only when the title has a Text feature with key\n`voxel-world` (`client.ai.getDefinitions()`). Configure it in the dashboard\n(InApp AI page) or via the title-config MCP `save_ai`:\n\n- `Modality: \"Text\"` (every generation is a queued job the game polls — a world\n with models is a long answer, minutes; there is no Async switch any more).\n- `Model`: a Text **mode** id from `GetInAppAIModels` (e.g. `standard`) whose `AcceptsImages` is true — never a model name\n (otherwise image requests are refused before any charge). Model quality =\n object quality: a strong vision model builds far better planes and cars.\n- `Behavior.SystemInstructions` = **`VOXEL_WORLD_SYSTEM_PROMPT`** from\n `src/ai/worldSpecPrompt.ts`, verbatim (a test keeps it in sync with the\n format). `HistoryMessages: 0`. **`MaxTokens` ≈ 48000** — models are the long part, and a\n reasoning model (GLM 5.3 Flash) spends part of the limit on thinking: at 16000 the\n first live run was cut off, the second took ~18000 output tokens (~1.6 credits).\n- `AllowImages: true`, **`MaxInputImages: 4`** or more — the game sends as many\n JPEGs (≤ 1568 px) as the feature allows; there is no platform cap, only what\n the model accepts.\n- `Safety.LockBehavior: true`, `MaxPromptChars: 1000` (the game trims the description).\n- `Limits` (`DailyCap`, `CooldownSeconds`) and `PriceOptions`.\n\n⚠ No JSON mode in the engine, and an answer cut off by `MaxTokens` comes back\n`completed` **and is charged**. The game detects a cut answer and builds the\nworld without AI instead — keep `MaxTokens` generous.\n\n`createClientWorldAI(client)` (`src/ai/worldAi.ts`): status from definitions,\none `relatedEntityID` per click (a dropped connection is retried with the same\nkey — no double charge), polling of the job, a job abandoned by closing\nthe screen is resumed next time, refusals mapped to player-facing messages, the\nplayer's chosen size enforced over the model's.\n\n## Sharing worlds in the Workshop\n\nVoxelCraft registers the content type **`voxelcraft.world`** (`src/workshop.ts`,\n`ctx.content.registerType` in `module.ts`). With the `workshop` module installed, players publish\nworlds from \"My worlds\" and open other players' worlds; the Workshop switches to the `voxelcraft`\nmode after opening.\n\n- `capture` publishes the world's **SaveData JSON** (role `main`, `application/json`) — the source\n plus the player's diffs, not the blocks — and a **webp preview** for recipe worlds (noise worlds\n have none). Metadata: `kind`, `seed`.\n- `open` parses the save, opens it in the running game (`openSharedWorld`) or stores it as a new\n local world.\n- Title config (dashboard → LiveOps → Workshop → Content types, or the title-config MCP):\n `ContentTypes[\"voxelcraft.world\"] = { DisplayName, Files: { main: { AllowedMimeTypes:\n[\"application/json\"], MaxBytes: … } } }`. Don't set `ThumbnailRequired` — noise worlds can't\n capture a preview. How to charge / gate access: **workshop-system**.\n\n## Agent debug surface\n\n`state().world`: `id`, `name`, `source` (`noise` | `spec`), `seed`, `size`,\n`shape`, `biomes`, `structures`, `fromPicture`, `spawnHint`; `createWorldOpen`.\nActions: `generateWorld { text, size?, seed? }` (without AI),\n`loadWorldSpec { spec }` (from a recipe object; returns `warnings`). The screen\nitself is plain DOM — read it and click it like a player.\n\n## Don't break\n\n- **Generators are part of the save format.** A save is `source + diffs`; the\n world is regenerated and the diffs land on it. Changing the output of\n `TerrainGenerator`, `SpecGenerator`, the structure builders or\n `rasterizeModel` corrupts every saved world — and every world **published in\n the Workshop**, which is the same SaveData. Both generators have golden\n fingerprint tests — a red one means \"you broke saves\", not \"update the numbers\".\n- **Block ids are append-only** (`registry/Blocks.ts`); new tiles go to the END\n of `TILE_ORDER` (`gfx/TextureAtlas.ts`).\n- The module takes **only types** from `@idosgames/core`; the client comes from\n the host (`ctx.client`).\n",
|
|
3
|
+
"description": "Build VoxelCraft worlds from a recipe (WorldSpec): from up to 4 reference pictures and a description, with InApp AI (client.ai, feature \"voxel-world\") or without it, or by hand. The AI recreates whatever the pictures show — objects built of coloured blocks (planes, cars, statues), landscapes (terrain sketches, rivers, biomes) and whole scenes. Covers the WorldSpec format, a project's start world (src/worlds/startWorld.ts), the \"Create world\" and \"My worlds\" screens, configuring the \"voxel-world\" AI feature, the agent actions generateWorld / loadWorldSpec, and sharing worlds through the Workshop. Use this whenever the user works on the voxelcraft module and wants a world or object from a picture, a generated or themed voxel map, a start world, or touches WorldSpec, SpecGenerator, VoxelModel, parseWorldSpec, buildSpecWithoutAI or the lobby's Worlds tab (src/lobby/WorldsScreen.tsx) — even without naming the module.",
|
|
4
|
+
"content": "---\nname: voxelcraft-worlds\ndescription: >-\n Build VoxelCraft worlds from a recipe (WorldSpec): from up to 4 reference\n pictures and a description, with InApp AI (client.ai, feature \"voxel-world\")\n or without it, or by hand. The AI recreates whatever the pictures show —\n objects built of coloured blocks (planes, cars, statues), landscapes (terrain\n sketches, rivers, biomes) and whole scenes. Covers the WorldSpec format, a\n project's start world (src/worlds/startWorld.ts), the \"Create world\" and\n \"My worlds\" screens, configuring the \"voxel-world\" AI feature, the agent\n actions generateWorld / loadWorldSpec, and sharing worlds through the\n Workshop. Use this whenever the user works on the voxelcraft module and wants\n a world or object from a picture, a generated or themed voxel map, a start\n world, or touches WorldSpec, SpecGenerator, VoxelModel, parseWorldSpec,\n buildSpecWithoutAI or the lobby's Worlds tab (src/lobby/WorldsScreen.tsx) —\n even without naming the module.\n---\n\n# VoxelCraft worlds (WorldSpec)\n\nA VoxelCraft world is built by a **deterministic generator** (`SpecGenerator`)\nfrom a compact JSON **recipe** — the WorldSpec. The same recipe always gives the\nsame world, block for block. Nothing writes the world's blocks directly;\neverything that makes a world writes a recipe:\n\n| Path | Where | What it can do |\n| ------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |\n| **AI** — pictures + description | `createClientWorldAI(client)` → `client.ai.generateText(\"voxel-world\", …)` | anything the pictures show: objects built from blocks, landscapes, scenes |\n| **Without AI** | `buildSpecWithoutAI` — first picture read in the browser (`ImageAnalyzer`), text by keywords (ru/en) | terrain from a top-down map, style from a mood picture, prefab structures by keyword — **no objects from pictures** |\n| **By hand / by an agent** | write the JSON yourself | everything the format allows |\n\nAll paths meet in `parseWorldSpec()` — the only door into the game. It fills\ndefaults, clamps numbers, drops unknown structures, shapes and blocks. A recipe\nnever crashes the generator, wherever it came from.\n\n## The WorldSpec format\n\nWorld coordinates `x`, `z`, `x2`, `z2`, `radius`, river points are **fractions\nof the world (0..1)**: `x=0` west, `x=1` east, `z=0` north, `z=1` south. Every\nfield is optional.\n\n```jsonc\n{\n \"version\": 1,\n \"name\": \"Frost Keep\", // <= 60 chars\n \"description\": \"…\",\n \"seed\": 2026, // number or string; omitted = hash of name\n \"size\": { \"mode\": \"island\", \"widthChunks\": 16, \"depthChunks\": 16 },\n // mode: island | bounded (land to the edges, sea outside) | infinite\n // 4..48 chunks per side (16 blocks each); default island 16×16 = 256×256 blocks\n \"terrain\": {\n \"shape\": \"mountains\", // island | archipelago | continent | valley | mountains | plateau | flat\n \"baseHeight\": 30, // 6..48 — average land height (the world is 64 blocks high)\n \"amplitude\": 20,\n \"roughness\": 0.6,\n \"waterLevel\": 25,\n \"sketch\": { \"rows\": [\"0012345\", \"…\"] },\n // optional TOP-DOWN elevation map drawn by the AI: 4..32 rows (north→south) of 4..32 digits\n // (west→east): 0 deep water, 1 shallow, 2 shore, 3 lowland, 4 plain, 5 hills, 6 high hills,\n // 7 mountains, 8 high mountains, 9 peaks. Smoothed + natural detail; with a sketch the recipe\n // draws its own coastline (no island mask). A top-down PICTURE's heightmap wins over it.\n // heightmap / surfaceMap — filled by the game from a top-down picture; don't write by hand\n },\n \"zones\": [\n // <= 12; each point belongs to the nearest zone (weighted by radius)\n { \"x\": 0.5, \"z\": 0.5, \"radius\": 0.4, \"biome\": \"snow\" },\n // biome: meadow | forest | birch_forest | taiga | snow | desert | badlands | beach |\n // swamp | rocky | savanna | jungle\n // overrides: surface, subsurface (any solid block — even wool), trees 0..1,\n // treeType oak|birch|spruce|palm|dead|none, grass 0..1, flowers 0..1, height -20..20\n ],\n \"structures\": [\n // <= 40\n { \"type\": \"castle\", \"x\": 0.5, \"z\": 0.5, \"size\": \"large\", \"rotation\": 180 },\n // house | tower | castle | village | wall | road | bridge | ruins | pond | rock |\n // tree_cluster | pyramid | well | farm | model | river\n // size small|medium|large; rotation 0|90|180|270 (buildings: door north|east|south|west)\n { \"type\": \"road\", \"x\": 0.25, \"z\": 0.72, \"x2\": 0.5, \"z2\": 0.5 }, // wall/road/bridge: x2,z2\n {\n \"type\": \"river\",\n \"path\": [\n [0.8, 0.2],\n [0.5, 0.5],\n [0.1, 0.95],\n ],\n }, // 2..16 points, source → mouth\n {\n \"type\": \"model\",\n \"model\": \"airplane\",\n \"x\": 0.5,\n \"z\": 0.55,\n \"rotation\": 90,\n \"elevation\": 0,\n },\n ],\n \"models\": { \"airplane\": {/* see below */} }, // <= 4 models, each can be placed many times\n \"npcs\": [\n { \"name\": \"Björn\", \"role\": \"smith\", \"x\": 0.26, \"z\": 0.7, \"persona\": \"…\" },\n ],\n \"palette\": {\n \"wall\": \"planks\",\n \"roof\": \"dark_planks\",\n \"floor\": \"planks\",\n \"path\": \"dirt_path\",\n \"accent\": \"stone_bricks\",\n },\n \"ambience\": { \"timeOfDay\": \"dawn\" }, // dawn | day | sunset | night\n}\n```\n\n### Models — objects built from blocks\n\nThe AI builds an object (a plane, a car, a ship, a statue, a creature) from\n**shapes**, not block by block — a language model reasons well about\n\"fuselage = cylinder, wings = flat boxes, symmetric\", and a shape list stays\nshort. `rasterizeModel()` turns it into blocks.\n\n```jsonc\n\"airplane\": {\n \"size\": [40, 12, 33], // [length x, height y, width z], <= 64 × 40 × 64\n // model coords: x from the FRONT (x=0, the nose) back, y UP (y=0 on the ground), z left→right;\n // the middle plane is z = (width - 1) / 2. Later shapes overwrite earlier; \"air\" carves.\n \"parts\": [ // <= 200\n { \"shape\": \"cylinder\", \"axis\": \"x\", \"center\": [4, 4, 16], \"radius\": 2.5, \"length\": 30, \"block\": \"white_wool\" },\n // center = middle of the START cap; runs `length` blocks along +axis (negative = −axis)\n { \"shape\": \"cone\", \"axis\": \"x\", \"center\": [3, 4, 16], \"radius\": 2.5, \"radius2\": 0.6, \"length\": -4, \"block\": \"white_wool\" },\n { \"shape\": \"box\", \"from\": [14, 3, 1], \"to\": [19, 3, 13], \"block\": \"white_wool\", \"mirror\": \"z\" },\n // \"mirror\": \"x\" | \"z\" also draws the mirror copy across the middle — wings, wheels, lights\n { \"shape\": \"sphere\", \"center\": [x, y, z], \"radii\": [rx, ry, rz], \"block\": \"…\", \"hollow\": true },\n { \"shape\": \"line\", \"from\": [x, y, z], \"to\": [x, y, z], \"radius\": 0, \"block\": \"…\" },\n { \"shape\": \"box\", \"from\": [33, 9, 16], \"to\": [34, 11, 16], \"block\": \"air\" }\n ],\n \"voxels\": [[9, 5, 14, \"light_blue_wool\"]] // <= 400 single blocks for details\n}\n```\n\nPlaced by a `model` structure: centred at `x, z`, turned by `rotation`, its\nbottom on the ground under the centre (on water — on the surface), `elevation`\nlifts it (a plane in flight). The footprint is cleared (trees and bumps don't\ngrow through it). Model blocks written with Minecraft names are forgiven:\n`red_concrete`, `cyan_terracotta`, `minecraft:lime_wool` → our wool of that colour;\n`*_stained_glass` → glass. Anything unknown drops that shape.\n\n### Blocks\n\ngrass, dirt, stone, cobblestone, mossy_cobblestone, sand, red_sand, gravel,\nclay, snow, ice, sandstone, terracotta, stone_bricks, bricks, planks,\ndark_planks, oak_log, spruce_log, birch_log, glass, dirt_path, leaves,\nspruce_leaves, birch_leaves, and wool in 12 colours (white, red, orange,\nyellow, green, blue, light_blue, purple, pink, brown, gray, black — `red_wool`…).\n\nGood to know: a **village** already has houses facing a well, paths and (large)\na farm; a **bridge** needs water under it (`valley` shape, a river, a pond);\na **river** always flows downhill from its first point and cuts its bed through\nhills; keep structures inside `0.2..0.8` on an island; the player spawns on dry\nland nearest the centre, away from structures.\n\n## The project's start world\n\n`src/worlds/startWorld.ts` (in a project: `src/modules/voxelcraft/src/worlds/startWorld.ts`):\n\n```ts\nexport const START_WORLD: unknown = { name: \"Frost Keep\", terrain: { shape: \"mountains\" }, … };\n```\n\n`null` (default) = the classic seed-1337 noise world. A player with a saved\nworld continues it (\"My worlds\"). Easiest way to get a recipe: in the game,\nthe lobby's **Worlds → New world → Download recipe**, then paste the JSON as an object.\n\n## In the game\n\n- **Worlds** tab of the SDK lobby (`src/lobby/WorldsScreen.tsx`, UI kit; the\n home's world card opens it with \"Change\" / \"New\"): up to **4 pictures** (drop, click, Ctrl+V —\n e.g. one car from several angles), description, size (island 256 / large 512\n / infinite / custom), how to read the pictures:\n - **Образец для ИИ** (default) — the AI decides from the description what the\n pictures are (an object to build, a landscape to recreate, a style);\n - **Карта сверху** — the first picture is a top-down map: blue = water, land\n rises from the shore, pixel colour → nearest block (works without AI too);\n - **Настроение** — only climate, colours, materials.\n Then with or without AI, a **mini-map preview** (red dot = spawn), play /\n download / load recipe. \"Play this world\" hands the recipe to the scene\n through `hub.play()` (`src/hub.ts`) and switches to the game.\n- **My worlds** (same tab): play, download the save, delete. Every world autosaves to IndexedDB (every 30 s of play, on\n pause, before switching worlds); the last one reopens on the next launch.\n\n## AI: the \"voxel-world\" feature of the title\n\nThe AI option appears only when the title has a Text feature with key\n`voxel-world` (`client.ai.getDefinitions()`). Configure it in the dashboard\n(InApp AI page) or via the title-config MCP `save_ai`:\n\n- `Modality: \"Text\"` (every generation is a queued job the game polls — a world\n with models is a long answer, minutes; there is no Async switch any more).\n- `Model`: a Text **mode** id from `GetInAppAIModels` (e.g. `standard`) whose `AcceptsImages` is true — never a model name\n (otherwise image requests are refused before any charge). Model quality =\n object quality: a strong vision model builds far better planes and cars.\n- `Behavior.SystemInstructions` = **`VOXEL_WORLD_SYSTEM_PROMPT`** from\n `src/ai/worldSpecPrompt.ts`, verbatim (a test keeps it in sync with the\n format). `HistoryMessages: 0`. **`MaxTokens` ≈ 48000** — models are the long part, and a\n reasoning model (GLM 5.3 Flash) spends part of the limit on thinking: at 16000 the\n first live run was cut off, the second took ~18000 output tokens (~1.6 credits).\n- `AllowImages: true`, **`MaxInputImages: 4`** or more — the game sends as many\n JPEGs (≤ 1568 px) as the feature allows; there is no platform cap, only what\n the model accepts.\n- `Safety.LockBehavior: true`, `MaxPromptChars: 1000` (the game trims the description).\n- `Limits` (`DailyCap`, `CooldownSeconds`) and `PriceOptions`.\n\n⚠ No JSON mode in the engine, and an answer cut off by `MaxTokens` comes back\n`completed` **and is charged**. The game detects a cut answer and builds the\nworld without AI instead — keep `MaxTokens` generous.\n\n`createClientWorldAI(client)` (`src/ai/worldAi.ts`): status from definitions,\none `relatedEntityID` per click (a dropped connection is retried with the same\nkey — no double charge), polling of the job, a job abandoned by closing\nthe screen is resumed next time, refusals mapped to player-facing messages, the\nplayer's chosen size enforced over the model's.\n\n## Sharing worlds in the Workshop\n\nVoxelCraft registers the content type **`voxelcraft.world`** (`src/workshop.ts`,\n`ctx.content.registerType` in `module.ts`). With the `workshop` module installed, players publish\nworlds from \"My worlds\" and open other players' worlds; the Workshop switches to the `voxelcraft`\nmode after opening.\n\n- `capture` publishes the world's **SaveData JSON** (role `main`, `application/json`) — the source\n plus the player's diffs, not the blocks — and a **webp preview** for recipe worlds (noise worlds\n have none). Metadata: `kind`, `seed`.\n- `open` parses the save, opens it in the running game (`openSharedWorld`) or stores it as a new\n local world.\n- Title config (dashboard → LiveOps → Workshop → Content types, or the title-config MCP):\n `ContentTypes[\"voxelcraft.world\"] = { DisplayName, Files: { main: { AllowedMimeTypes:\n[\"application/json\"], MaxBytes: … } } }`. Don't set `ThumbnailRequired` — noise worlds can't\n capture a preview. How to charge / gate access: **workshop-system**.\n\n## Agent debug surface\n\n`state().world`: `id`, `name`, `source` (`noise` | `spec`), `seed`, `size`,\n`shape`, `biomes`, `structures`, `fromPicture`, `spawnHint`; `createWorldOpen`.\nActions: `generateWorld { text, size?, seed? }` (without AI),\n`loadWorldSpec { spec }` (from a recipe object; returns `warnings`). The screen\nitself is plain DOM — read it and click it like a player.\n\n## Don't break\n\n- **Generators are part of the save format.** A save is `source + diffs`; the\n world is regenerated and the diffs land on it. Changing the output of\n `TerrainGenerator`, `SpecGenerator`, the structure builders or\n `rasterizeModel` corrupts every saved world — and every world **published in\n the Workshop**, which is the same SaveData. Both generators have golden\n fingerprint tests — a red one means \"you broke saves\", not \"update the numbers\".\n- **Block ids are append-only** (`registry/Blocks.ts`); new tiles go to the END\n of `TILE_ORDER` (`gfx/TextureAtlas.ts`).\n- The module takes **only types** from `@idosgames/core`; the client comes from\n the host (`ctx.client`).\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|