@idosgames/mcp 0.1.36 → 0.1.38
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 +6 -0
- package/dist/cli.js +2204 -27
- package/package.json +4 -1
- package/registry/host.json +7 -7
- package/registry/index.json +310 -156
- package/registry/modules/ai.json +7 -7
- package/registry/modules/board-game.json +14 -14
- package/registry/modules/boosts.json +3 -3
- package/registry/modules/character.json +32 -15
- package/registry/modules/chat.json +3 -3
- package/registry/modules/collections.json +7 -7
- package/registry/modules/coop-events.json +7 -7
- package/registry/modules/craft.json +3 -3
- package/registry/modules/deals.json +7 -7
- package/registry/modules/events.json +8 -8
- package/registry/modules/friends.json +9 -9
- package/registry/modules/hero-run.json +498 -0
- package/registry/modules/idle-rpg.json +69 -17
- package/registry/modules/inventory.json +3 -3
- package/registry/modules/invite.json +7 -7
- package/registry/modules/leaderboard.json +10 -10
- package/registry/modules/lootboxes.json +7 -7
- package/registry/modules/mailbox.json +8 -8
- package/registry/modules/marketplace.json +8 -8
- package/registry/modules/mini-game.json +8 -8
- package/registry/modules/multiplayer.json +7 -7
- package/registry/modules/premium.json +7 -7
- package/registry/modules/profile.json +7 -7
- package/registry/modules/pvp.json +7 -7
- package/registry/modules/quests.json +8 -8
- package/registry/modules/rewards.json +8 -8
- package/registry/modules/season.json +7 -7
- package/registry/modules/store.json +7 -7
- package/registry/modules/tutorials.json +7 -7
- package/registry/modules/video-studio.json +228 -0
- package/registry/modules/voxelcraft.json +42 -125
- package/registry/modules/workshop.json +3 -3
- package/registry/skills/authentication.json +1 -1
- package/registry/skills/hero-run.json +6 -0
- package/registry/skills/idosgames-module-contract.json +2 -2
- package/registry/skills/idosgames-project-structure.json +1 -1
- package/registry/skills/idosgames-ui-kit.json +1 -1
- package/registry/skills/video-studio.json +6 -0
- package/registry/video.json +9800 -0
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"version": "0.1.3"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@idosgames/core": "0.
|
|
43
|
-
"@idosgames/module-sdk": "0.
|
|
44
|
-
"@idosgames/react": "0.
|
|
42
|
+
"@idosgames/core": "0.23.0",
|
|
43
|
+
"@idosgames/module-sdk": "0.13.0",
|
|
44
|
+
"@idosgames/react": "0.6.0",
|
|
45
45
|
"react": "19.2.7"
|
|
46
46
|
},
|
|
47
47
|
"contentHash": "ba3020258426f2c8b890d16cd169bffb4e96a4fc61d8a312c24d706ec9eac315",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "authentication",
|
|
3
3
|
"description": "Log players into a game on the iDosGames TypeScript SDK (@idosgames/core) via client.auth (AuthenticationService): guest/device-id login, two-step email registration with a confirmation code, email login, Google/Telegram login, SSO-code login from idosgames.com, wallet login, password reset, auto login on relaunch, session refresh, logout, and client-side email/password validation. Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and asks about logging a player in, sessions, registration, email confirmation codes, resending a code, guest accounts, device-id login, Telegram login, Google login, SSO login, wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise touches client.auth, AuthenticationService, or AuthContext — even if they don't name the module explicitly.",
|
|
4
|
-
"content": "---\nname: authentication\ndescription: >-\n Log players into a game on the iDosGames TypeScript SDK (@idosgames/core)\n via client.auth (AuthenticationService): guest/device-id login, two-step\n email registration with a confirmation code, email login, Google/Telegram\n login, SSO-code login from idosgames.com, wallet login, password reset, auto\n login on relaunch, session refresh, logout, and client-side email/password\n validation. Use this whenever the user is working in the iDosGames TS SDK or\n its game templates (board-game, idle-rpg) and asks about logging a player\n in, sessions, registration, email confirmation codes, resending a code,\n guest accounts, device-id login, Telegram login, Google login, SSO login,\n wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise\n touches client.auth, AuthenticationService, or AuthContext — even if they\n don't name the module explicitly.\n---\n\n# Authentication (iDosGames TS SDK)\n\nThe Authentication module is how a player gets a session. It's the one module\nevery other feature depends on: nothing else on `client` works until a login\nmethod has succeeded. Unlike feature modules, most of its methods don't return\na narrow per-feature payload — they return the **entire post-login bootstrap\nstate** (`ClientState`: title config + the player's full `User` state),\nbecause a successful login is also \"give me everything the client needs to\nrender.\"\n\nThis skill is for **using** production login flows, not for porting or\nextending the service. If a login is rejected, that's the backend enforcing a\nrule (bad credentials, duplicate email, banned account) — surface the error,\ndon't try to reproduce the check client-side.\n\n## Remember me\n\n`setRememberSession(remember)` decides whether a successful login is written to storage. Call it\n**before** a `login*` method — it is read when that login completes, and it applies to whichever\nprovider runs next.\n\n```ts\nclient.auth.setRememberSession(rememberCheckbox); // default: true\nconst result = await client.auth.loginWithDeviceID();\n```\n\n- **On** (the default, and how every release before core 0.1.3 behaved) — the session is persisted,\n so `autoLogin()` signs the player back in on the next launch.\n- **Off** — nothing is persisted and any previously remembered session is dropped, so the next\n launch opens on the login screen. The current session is NOT weakened: the credentials stay in\n memory for this tab, so the transport's automatic 401 re-login still works.\n\n`logout()` ends the session and clears the persisted one, so a reload does not walk back into the\naccount the player just left. (Before core 0.1.3 it left storage untouched and a reload silently\nsigned them back in.)\n\nSessions are extended by the **refresh token** (`Authentication/RefreshSession`, core ≥ 0.18): every\nlogin returns one, it is kept in memory and — when remembering — in storage, and rotated on every\nrefresh. A 401 and a cold-start `autoLogin()` use it first, so ANY method comes back, wallet and\nGoogle included; replaying the login method is only the fallback for a missing/expired token. On a\ntitle whose login mode needs a token balance the server re-checks it on that refresh; a balance\nthat dropped closes the game (`client.auth.playAccess.Granted === false`), it does not end the session.\n\n## Mental model\n\n- **Guest login** (`loginWithDeviceID`) — anonymous account keyed off a\n per-device ID the platform adapter generates/stores. Zero-friction first\n launch; no credentials to lose, but also nothing to recover if the device ID\n is gone (reinstall, new device).\n- **Linked login methods** — `loginWithEmail` / `registerWithEmail`,\n `loginWithGoogle`, `loginWithTelegram`, `loginWithSsoCode`. These tie\n the account to a real identity so the player can resume it elsewhere.\n- **`autoLogin()`** picks up where the player left off. It first extends the\n remembered session by its **refresh token** (works for every method). Only\n without a usable token does it fall back to replaying — and that fallback\n covers just the methods that don't need a fresh externally-issued token: it replays `loginWithEmail`\n if the last successful login was Email (saved password), `loginWithDeviceID`\n if it was Device/None, and `loginWithTelegram` if it was Telegram (its\n `initData` is re-read live from the Telegram WebApp bridge each time, not\n stored). For Google/Facebook/GooglePlay/platform-token, there's no stored\n credential to replay — `autoLogin()` returns `reason: \"client\"` instead of\n guessing, because DeviceID and email/platform logins hash to **different\n backend UserIDs** (see Gotchas), so silently falling back to a guest login\n would strand the player on an unrelated empty account. What `autoLogin()`\n has to replay is controlled by **\"remember me\"** — see below. Call this on app\n start instead of hand-rolling \"which method did they use last,\" but be ready\n to handle its failure by re-running the platform's sign-in flow.\n- **Session refresh is automatic.** The HTTP transport calls\n `refreshSession()` itself when a request comes back 401 — it calls the\n server's `RefreshSession` with the refresh token (falling back to replaying\n the login method only without a usable token) and retries once. You almost never call\n `refreshSession()` directly; it's documented here for completeness, not as a\n method you wire up in app code.\n- **Static validators** (`AuthenticationService.isValidEmail`,\n `AuthenticationService.isValidPasswordLength`) are plain synchronous\n functions, not calls to the backend — use them to validate a form before\n spending a network round-trip on a login/register call that will fail\n server-side anyway. They mirror, but do not replace, the server's own\n checks — the server always re-validates.\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any other auth.* login method\n\nclient.auth.isLoggedIn; // true once a login call has succeeded\n```\n\nEvery other module's methods require this to have succeeded first — without a\nsession they return `{ ok: false, reason: \"unauthorized\" }` rather than\nthrowing. There is one `client` per player; don't share it across sessions.\nAuthentication requests carry no Bearer ticket at all — login/register/reset\nare the one family of calls that work before you have a session.\n\n## Methods\n\nAll login/register/reset methods return `Promise<OperationResult<T>>`: either\n`{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data`. `reason` is one of `\"client\"` (bad\nlocal args, e.g. missing Telegram initData or empty platform token),\n`\"unauthorized\"`, `\"throttled\"` (fired the same endpoint again inside the\nthrottle window), `\"connection\"` (transient, offer Retry), `\"validation\"`\n(response/schema drift), or `\"server\"` (backend rejected it — `error` carries\nthe reason code the backend returned, e.g. `\"INCORRECT_EMAIL_OR_PASSWORD\"`,\n`\"INVALID_VERIFICATION_CODE\"` — see Gotchas for the verbatim set).\n\n| Method | Purpose | `data` on success |\n| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |\n| `loginWithDeviceID()` | Anonymous/guest login keyed off the device ID. | `ClientState` |\n| `loginWithTelegram()` | Login using the Telegram Mini App's init data. | `ClientState` |\n| `loginWithEmail(email, password)` | Login with email + password. | `ClientState` |\n| `registerWithEmail(email, password)` | **Start** a registration. Only mails a code — never returns a session, see below. | `EmailRegistrationOutcome` |\n| `confirmEmailRegistration(email, code)` | Finish the registration with the code from the email, creating the player. | `ClientState` |\n| `resendVerificationCode(email)` | Mail a fresh code. Resolves successfully whatever happened — except when there is no usable sender at all (`EMAIL_SENDER_NOT_CONFIGURED`). | `void` |\n| `requestWalletChallenge(networkID, address)` | Ask for the message the player's wallet must sign. Step one of wallet login. | `WalletChallengeResponse` |\n| `loginWithWallet(networkID, address, signature)` | Exchange the signed challenge for a title session. | `ClientState` |\n| `loginWithGoogle(googleIDToken)` | Login with a Google ID token. | `ClientState` |\n| `loginWithSsoCode(code)` | Exchange a one-time code from idosgames.com for a title session. **Web only** — see below. | `ClientState` |\n| `forgotPassword(email)` | Trigger a password-reset email (a 6-digit code). | `SuccessResponse` |\n| `resetPassword(email, resetToken, password)` | Complete a password reset using the emailed code. | `SuccessResponse` |\n| `autoLogin()` | Replay the last-used auth method (Email/Device/None/Telegram only — fails with `reason: \"client\"` for Google/Facebook/GooglePlay/platform-token). | `ClientState` |\n| `refreshSession()` | Re-authenticate and return a fresh session ticket; used internally by the transport on 401. | `string \\| null` (not an `OperationResult`) |\n| `logout()` | Clear the session, forget the saved login method, and reset the local cache. Synchronous, no network call. | `void` |\n| `setRememberSession(remember)` | \"Remember me\" — whether the NEXT login is persisted for `autoLogin()`. Default `true`. Call before a `login*` method. | `void` |\n| `AuthenticationService.isValidEmail(email)` | Static, synchronous client-side format check. | `boolean` |\n| `AuthenticationService.isValidPasswordLength(pw)` | Static, synchronous length check (8–100 chars). | `boolean` |\n\n⚠ **Each title has its OWN password.** The same address in two games means two independent\npasswords: resetting in one game does not change the login for the other. The reason is that the\nreset e-mail goes out through the PUBLISHER's SMTP — with one shared password, one publisher's mail\nserver would control access to every other publisher's game. So don't tell the player \"the same\npassword as in your other game\", and don't reuse stored credentials across titles.\nThe IDENTITY is still shared: Google, Apple and the idosgames.com sign-in recognise the same person\nin every game. Identity answers \"who is this\", not \"what proves they may sign in\".\n\n⚠ **Signing in through idosgames.com from a NATIVE build is not in this SDK.** `beginSsoRedirect` /\n`loginWithSsoCode` are the web half: the browser comes back to the same page with the code in the\nURL fragment. In a packaged Android/iOS build the browser is a separate program that tells the app\nnothing, so that flow uses a request the game polls for instead (`BeginNativeSsoRequest` /\n`PollNativeSsoRequest`), and it is implemented in the Unity SDK. Don't reach for `loginWithSsoCode`\nfrom a native wrapper — it has no way to receive the code.\n\n⚠ **That native flow now has a SHORT CODE, and a native client should use it.** `BeginNativeSsoRequest`\naccepts `UseUserCode: true` and then answers with `UserCode` (`WDJB-MJHT`) plus `VerificationUri`.\nThe game displays the code; the player opens the plain URL and types it there. The older shape — the\ngame opening `…/sso?title=X&request=<handle>` and the page confirming on its own — still works for\nalready-shipped builds, but **a request created with a code refuses that path**, so a build must do\none or the other, never a mix.\n\nThe reason is not convenience. In the older shape the handle travels IN THE ADDRESS, so the link can\nbe sent to somebody else: the attacker creates the request on their own device, the victim opens the\nlink and confirms, and the attacker's game gets a session as the victim. PKCE does not help (the\nverifier belongs to whoever created the request). With a code there is nothing in the URL to forward\n— the request is named only by what is on the device's own screen. `GetAuthOptions` reports\n`NativeSso.UserCodeSupported` and `NativeSso.PollIntervalSeconds`; ask before assuming, and follow\nthe interval or the poll quota will refuse you.\n\n⚠ **The code is shown, the secret is not.** Players read codes off screens, over shoulders and out\nof screenshots — so the code alone must never buy a session. Polling keeps requiring the request\nsecret, which never leaves the game.\n\nNon-Promise instance getters (read directly, no `await`):\n\n| Getter | Returns |\n| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `client.auth.context` | `AuthContext \\| null` — `{ userID, clientSessionTicket, clientSessionTicketExpiration, platformUserID?, platformAuthToken?, platformAuthTokenExpiration? }`, or `null` if never logged in. |\n| `client.auth.isLoggedIn` | `true` once `context` has a non-empty `userID` and `clientSessionTicket`. |\n| `client.auth.lastAuthType` | The persisted `AuthType` (`\"None\" \\| \"Device\" \\| \"Email\" \\| \"iDosGames\" \\| \"Facebook\" \\| \"Google\" \\| \"GooglePlay\" \\| \"Telegram\"`) from the previous successful login, read from local storage. |\n\nOn a successful login/register call, the SDK mirrors the full `ClientState`\ninto `client.data` (title config + user state) — same cache every other\nmodule reads from — and emits events. `email`/`password` are persisted to\nlocal storage **only** after a successful `loginWithEmail` or\n`confirmEmailRegistration` call, so `autoLogin()` has something to replay.\n⚠ Note it is the CONFIRMATION, not `registerWithEmail`, that persists them:\nregistration alone creates no account, so there is nothing to replay yet (the\npassword is carried over from the first step internally). A leftover\nusername you pass in is only a fallback — Google logins use the Google\nprofile name, platform-token logins use the platform profile name, and if\nnone is available anywhere the backend assigns a generated one (see Gotchas).\n\n## Events\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn.\n\n- `auth:requestSent` → `void` — fired at the start of every login/reset call (before the network round-trip). One exception: `loginWithSsoCode` validates its argument first, so an empty-code `reason: \"client\"` failure doesn't fire it.\n- `auth:loggedIn` → `void` — fired after a login/register call succeeds and `ClientState` has been applied to the cache.\n- `auth:loggedOut` → `void` — fired synchronously by `logout()`.\n- `auth:unauthorized` → `void` — transport-level, fired by the HTTP layer only when a 401 could **not** be transparently recovered (the automatic refresh was unavailable, already attempted, or failed). A 401 that the refresh-and-retry rescues never emits it. Not emitted by `AuthenticationService` itself.\n- `user:clientStateReceived` → `ClientState` — fired whenever a full client state is applied (login, or any other flow that re-fetches it).\n- `user:stateUpdated` → `void` — fired whenever the cached `User` half of `ClientState` is replaced, which includes every successful login.\n- `user:anyUpdated` → `void` — coarsest \"something in the user cache changed\" signal; also fires on login.\n\n```ts\nconst off = client.on(\"auth:loggedIn\", () => {\n console.log(\"logged in as\", client.auth.context?.userID);\n});\n// later: off();\n```\n\n## Recipes\n\n### Guest login on first launch\n\nIn the base template the \"Play as guest\" button is OFF by default; a project turns it on with\n`auth: { guest: true }` in `src/app.config.ts`. The Google button shows only when the title has its\nOWN Google client (`getAuthOptions()` → `Google.Enabled` and not `Google.PlatformFallback`).\n\n```ts\nconst result = await client.auth.loginWithDeviceID();\nif (!result.ok) return showError(result.error ?? result.reason);\n// client.data now has the full ClientState; render the game.\n```\n\n### Try auto-login first, fall back appropriately\n\n```ts\nasync function bootstrap() {\n const result = await client.auth.autoLogin();\n if (result.ok) return; // resumed the last session\n if (result.reason === \"client\" && client.auth.lastAuthType !== \"None\") {\n // Last session used Google/Facebook/GooglePlay/a platform token — autoLogin\n // can't replay that silently. Re-run that platform's sign-in flow and call\n // the matching login method (e.g. loginWithGoogle) with a fresh token.\n return reauthenticateViaPlatformSDK(client.auth.lastAuthType);\n }\n showError(result.error ?? result.reason);\n}\n```\n\n`autoLogin()` safely self-replays Email, Device/None, and Telegram — call it\nonce on app start for those. For anything else it deliberately does **not**\nfall back to a guest login (see Gotchas) — treat its `reason: \"client\"`\nfailure as \"go get a fresh token,\" not as a generic error to toast.\n\n### Register a new account with email\n\n```ts\nif (!AuthenticationService.isValidEmail(email)) {\n return showFieldError(\"email\", \"Enter a valid email address.\");\n}\nif (!AuthenticationService.isValidPasswordLength(password)) {\n return showFieldError(\"password\", \"Password must be 8–100 characters.\");\n}\n\nconst started = await client.auth.registerWithEmail(email, password);\nif (!started.ok) return showError(started.error ?? started.reason);\n\n// No account yet — a code was mailed. Show a code screen; the session arrives from confirm.\nconst done = await client.auth.confirmEmailRegistration(email, codeFromPlayer);\nif (!done.ok) return showError(done.error ?? done.reason);\n```\n\n⚠ **`registerWithEmail` never creates an account, and success does not mean the address was\nfree.**\n\nRegistration always stores the attempt and mails a 6-digit code; the player is created by\n`confirmEmailRegistration`. Building a screen that assumes an immediate session will leave players\nstaring at a spinner that never resolves.\n\nConfirmation is **platform behaviour, not a title setting** — there is no flag to switch it off,\nand no `verificationRequired` branch to write. Only the numbers are configurable (code lifetime,\nattempt cap, resend cooldown).\n\nThe answer is also deliberately **identical for a free and for a taken address** — there is no\n`EMAIL_ALREADY_EXISTS` on this path any more. Telling them apart would turn the registration form\ninto a way of checking whether a given person has an account here, which is exactly what the\npassword-recovery form goes out of its way to avoid. The owner of a taken address gets a \"you\nalready have an account\" e-mail instead, without a code.\n\nOne failure on this path **is** reported: `EMAIL_SENDER_NOT_CONFIGURED`, when the server has no\nusable mail sender at all. It says nothing about the address — the same answer comes back for a\nfree one, a taken one and a typo — and it exists because confirmation cannot be switched off: with\nno sender, e-mail registration does not work at all, and answering \"code sent\" would hide a broken\ndeployment behind a normal-looking screen. Show it as a server-side problem, not as the player's\nmistake, and do not offer a \"resend\" — the resend refuses the same way. A letter that merely fails\nto leave is a different case and still answers success.\n\nEvery way of getting the code wrong — no such registration, expired, mismatched — answers with the\nsame `INVALID_VERIFICATION_CODE`, for the same reason. Do not branch on it.\n\n`resendVerificationCode` mails a **new** code: only its hash is stored, so the previous one cannot\nbe re-sent and stops working. Rate-limit the button in the UI (the server's cooldown is reported as\n`resendCooldownSeconds`) — the server answers successfully even when it sent nothing.\n\nEmail is matched case-insensitively — the backend trims and lowercases it\nbefore every lookup and write, so `Player@Mail.com` and `player@mail.com`\ncollide on the same account.\n\n### Upgrade a guest account to an email account\n\nThe SDK has no dedicated \"link email to this guest\" call — `registerWithEmail`\nalways mints its **own** account (a fresh UserID derived from the email, via a\ndifferent hash than the device-id account uses). To move a guest forward\nwithout losing their progress, do it through your own game-side flow (e.g. a\ncloud script that copies/merges state) rather than assuming registration\ncarries the guest's inventory along; don't build a UI that implies it will.\n\n### Forgot / reset password\n\n```ts\nconst sent = await client.auth.forgotPassword(email);\nif (!sent.ok) return showError(sent.error ?? sent.reason);\n// If an account exists for this email, a 6-digit reset code was emailed,\n// valid for 5 minutes. The response looks identical whether or not the\n// email is registered (see Gotchas) — don't tell the player \"no such account.\"\n\nconst reset = await client.auth.resetPassword(email, resetToken, newPassword);\nif (!reset.ok) return showError(reset.error ?? reset.reason);\n// password changed; call loginWithEmail with the new password next\n```\n\n### Handle a rejected login\n\n```ts\nconst result = await client.auth.loginWithEmail(email, password);\nif (!result.ok) {\n switch (result.reason) {\n case \"connection\":\n return offerRetry();\n case \"throttled\":\n return; // ignore — same call already in flight/just ran\n case \"client\":\n case \"server\":\n default:\n return showError(result.error ?? \"Login failed.\");\n }\n}\n```\n\n### Log out\n\n```ts\nclient.auth.logout(); // synchronous — clears context + resets client.data cache\n// route to login screen; no network call is made\n```\n\n## Gotchas\n\n- **A title's login mode can refuse methods** (`cfg.Blockchain.PlayAccess`). `WalletOnly` accepts\n only wallet login (others fail with `PLAY_ACCESS_WALLET_LOGIN_ONLY`); `AnyWithLinkedWallet` refuses\n guests (`PLAY_ACCESS_GUEST_DISABLED`) and lets other logins in with `client.auth.playAccess.Granted\n=== false` until a wallet is linked and holds enough tokens. Read the mode BEFORE login with\n `client.auth.getAuthOptions()` (`PlayAccessMode`) to hide dead buttons. The host template does all\n of this already — see the `blockchain-system` skill, \"Login mode and play access\".\n- **`autoLogin()` deliberately refuses to guess for federated/platform\n logins.** The backend derives DeviceID UserIDs from\n `SHA256(platform + device + deviceID)` and email/platform-linked UserIDs\n from `SHA256(email + titleID)` — two different hashes with no server-side\n link between them. Falling back to `loginWithDeviceID()` for a\n Google/Facebook/GooglePlay/platform-token session would silently resolve to\n a different, empty account, not \"safely resume as a guest.\" If\n `lastAuthType` is one of those, `autoLogin()` returns\n `{ ok: false, reason: \"client\" }` instead; the app must obtain a fresh token\n from that platform's SDK and call the matching `login*` method itself.\n- **The exact `error` strings on `reason: \"server\"` are backend-defined\n codes/messages**, not prose meant for direct display — surface them through\n your own copy/localization layer rather than showing them raw. Verified\n values from the backend for the flows above: `\"INVALID_INPUT_DATA\"`\n (missing/blank required field) and `\"INCORRECT_EMAIL_OR_PASSWORD\"` (bad\n credentials **or** unknown email — the backend intentionally doesn't\n distinguish the two, so don't tell the player \"no such account\") and\n `\"INVALID_VERIFICATION_CODE\"` (every way of getting the confirmation code\n wrong, collapsed into one) are shared\n `MessageCode` enum values also used elsewhere in the backend;\n `\"EMAIL_ALREADY_EXISTS\"` is **no longer returned by registration** — the form\n answers identically for a free and for a taken address, on purpose;\n `\"RATE_LIMIT_EXCEEDED\"` / `\"OPERATION_IN_PROGRESS\"` (per-account login lock\n — see below) are the same enum family. Google/platform-token logins instead\n return ad-hoc string literals specific to that flow, e.g. `\"BANNED_GLOBAL\"`\n (platform account is banned), `\"GOOGLE_ACCOUNT_CONFLICT\"` (email already\n linked to a _different_ Google account), `\"INVALID_GOOGLE_TOKEN\"`\n (bad/expired Google ID token) — see\n [references/data-model.md](references/data-model.md) for the full list.\n Telegram login instead surfaces plain sentences like `\"Invalid Telegram\ndata\"` / an internal `\"Telegram auth_date is stale\"` condition (initData\n older than 24h or timestamped implausibly in the future) — treat any\n non-uppercase-code string as an opaque message, not something to\n pattern-match on.\n- **Per-account login is rate-limited server-side, independent of the SDK's\n own 600 ms throttle.** Repeated `loginWithEmail` attempts for the same\n account inside a ~1-second window come back `\"RATE_LIMIT_EXCEEDED\"`; a login\n already being processed for that account comes back\n `\"OPERATION_IN_PROGRESS\"`. This is a brute-force guard, not a bug — don't\n retry-loop past it.\n- **Telegram login needs the platform adapter's init data.** `loginWithTelegram()`\n calls `platform.getTelegramInitDataRaw()` first; if that returns falsy (not\n running inside Telegram, or the adapter doesn't support it), the call fails\n client-side with `reason: \"client\"` before any network request — no point\n retrying without fixing the environment. Server-side, the init data is\n rejected if its `auth_date` is missing, more than 24 hours old, or more than\n 5 minutes in the future — a Telegram Mini App that's been idle a long time\n may need a fresh WebApp launch, not just a retry. On success it's recorded\n under its own `AuthType.Telegram` (not `Device`) precisely so `autoLogin()`\n replays `loginWithTelegram()` again rather than a device-id login.\n- **`loginWithPlatformToken` no longer exists.** The site used to hand games the\n platform auth token — a 24-hour string that also authorizes the whole\n management API. It was replaced by the one-time SSO code\n (`loginWithSsoCode`), and the endpoint has since been removed as unreachable.\n A session opened by that code is still recorded under `AuthType.iDosGames`,\n which is what `lastAuthType` reports afterward.\n- **A username you pass in is a last resort, not a guarantee.** For Google and\n platform-token logins, the backend prefers the identity provider's own\n profile name over any client-supplied username on every login (not just the\n first) — so a locally cached display name can be silently overwritten by the\n linked account's name. If no name is available from any source at\n registration time, the backend assigns a generated one\n (`\"BraveTiger482\"`-style: adjective + noun + 2–4 digit number) rather than\n leaving it blank.\n- **`forgotPassword` never reveals whether an email is registered.** Unknown\n emails and known emails both return `{ ok: true }` with no email actually\n sent for the unknown case — this is a deliberate anti-enumeration measure,\n not a bug to work around. The emailed reset code is a 6-digit number valid\n for 5 minutes; requesting again inside a 60-second cooldown silently\n no-ops (still returns `ok: true`, no second email). `resetPassword` allows\n at most 5 wrong-code attempts before the code is invalidated server-side and\n a fresh `forgotPassword` call is required.\n ⚠ **The remaining-attempt count is NOT in the response any more**, and neither\n is \"no such account\": every outcome of `resetPassword` — unknown address, no\n code requested, expired code, wrong code — answers `\"INVALID_RESET_CODE\"`, and\n an exhausted code answers `\"RESET_CODE_ATTEMPTS_EXCEEDED\"`. Reporting the\n remaining count both confirmed the account existed and told an attacker how\n much room was left. Don't parse a number out of `error`.\n- **Email credentials are only saved on success.** `loginWithEmail` and\n `confirmEmailRegistration` persist email+password to local storage (scoped by\n `titleID`) only after `fetchAndApplyClientState` succeeds — a failed login\n doesn't overwrite previously saved credentials, and a fresh install has\n nothing saved until the first successful email login. `registerWithEmail` on\n its own saves nothing: at that point the account does not exist yet.\n- **`refreshSession()` has a re-entrancy guard.** If it's already running\n (e.g. two requests 401 at nearly the same time), a second call returns `null`\n immediately instead of recursing — this exists to avoid deadlocking the\n retry, so don't assume a `null` result means the session is actually dead.\n- **`logout()` forgets the saved login method too (changed in core 0.1.3).** It\n nulls the auth context, wipes the cached user state and title-config bundle,\n clears `lastAuthType` and any saved email/password, and emits\n `auth:loggedOut` — so a later `autoLogin()` has nothing to resume. Before\n 0.1.3 storage survived a logout and the next launch silently signed the\n player back into the account they had just left. If you want a sign-out the\n player is remembered through, don't call `logout()`.\n- **Never reload the page after `logout()`** (or after deleting the account). The host shell\n (`AuthGate`) returns to the sign-in screen by itself on `auth:loggedOut`. A `location.reload()`\n restarts the whole app for nothing and, in the AI Coder's live preview, left a dark empty frame:\n the preview's files arrive by message and do not survive a reload.\n- **`refreshSession()` and `logout()` don't return `OperationResult`.**\n `refreshSession()` resolves to `string | null` (the fresh session ticket, or\n `null` on failure/no saved method); `logout()` is synchronous `void`. Don't\n branch on `.ok` for these two.\n- **`GameLoop` is excluded from the login `ClientState`.** Board/stage state\n and its definitions are fetched separately per-stage by the GameLoop\n feature, not bundled into every login response.\n- **Guard against double-submit.** Each call fires a real request; a\n double-tapped \"Log in\" button can fire twice. Firing the same auth endpoint\n again inside the SDK's own throttle window (default 600 ms) comes back\n `reason: \"throttled\"` rather than duplicating the call, but disable the\n control while a call is in flight rather than relying on that — and note the\n server has its own, stricter per-account lock on top (see above).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — request/response wire\nshapes, the exact backend validation and error-code rules for each login/reset\npath, UserID derivation, and the default-username generator. Read it when you\nneed to reason about _why_ a specific `error` string came back, or when\nbuilding a password-reset or registration UI that needs to match the backend's\ntiming/attempt rules exactly.\n",
|
|
4
|
+
"content": "---\nname: authentication\ndescription: >-\n Log players into a game on the iDosGames TypeScript SDK (@idosgames/core)\n via client.auth (AuthenticationService): guest/device-id login, two-step\n email registration with a confirmation code, email login, Google/Telegram\n login, SSO-code login from idosgames.com, wallet login, password reset, auto\n login on relaunch, session refresh, logout, and client-side email/password\n validation. Use this whenever the user is working in the iDosGames TS SDK or\n its game templates (board-game, idle-rpg) and asks about logging a player\n in, sessions, registration, email confirmation codes, resending a code,\n guest accounts, device-id login, Telegram login, Google login, SSO login,\n wallet login, forgot/reset password, auto-login, isLoggedIn, or otherwise\n touches client.auth, AuthenticationService, or AuthContext — even if they\n don't name the module explicitly.\n---\n\n# Authentication (iDosGames TS SDK)\n\nThe Authentication module is how a player gets a session. It's the one module\nevery other feature depends on: nothing else on `client` works until a login\nmethod has succeeded. Unlike feature modules, most of its methods don't return\na narrow per-feature payload — they return the **entire post-login bootstrap\nstate** (`ClientState`: title config + the player's full `User` state),\nbecause a successful login is also \"give me everything the client needs to\nrender.\"\n\nThis skill is for **using** production login flows, not for porting or\nextending the service. If a login is rejected, that's the backend enforcing a\nrule (bad credentials, duplicate email, banned account) — surface the error,\ndon't try to reproduce the check client-side.\n\n## Remember me\n\n`setRememberSession(remember)` decides whether a successful login is written to storage. Call it\n**before** a `login*` method — it is read when that login completes, and it applies to whichever\nprovider runs next.\n\n```ts\nclient.auth.setRememberSession(rememberCheckbox); // default: true\nconst result = await client.auth.loginWithDeviceID();\n```\n\n- **On** (the default, and how every release before core 0.1.3 behaved) — the session is persisted,\n so `autoLogin()` signs the player back in on the next launch.\n- **Off** — nothing is persisted and any previously remembered session is dropped, so the next\n launch opens on the login screen. The current session is NOT weakened: the credentials stay in\n memory for this tab, so the transport's automatic 401 re-login still works.\n\n`logout()` ends the session and clears the persisted one, so a reload does not walk back into the\naccount the player just left. (Before core 0.1.3 it left storage untouched and a reload silently\nsigned them back in.)\n\nSessions are extended by the **refresh token** (`Authentication/RefreshSession`, core ≥ 0.18): every\nlogin returns one, it is kept in memory and — when remembering — in storage, and rotated on every\nrefresh. A 401 and a cold-start `autoLogin()` use it first, so ANY method comes back, wallet and\nGoogle included; replaying the login method is only the fallback for a missing/expired token. On a\ntitle whose login mode needs a token balance the server re-checks it on that refresh; a balance\nthat dropped closes the game (`client.auth.playAccess.Granted === false`), it does not end the session.\n\n## Mental model\n\n- **Guest login** (`loginWithDeviceID`) — anonymous account keyed off a\n per-device ID the platform adapter generates/stores. Zero-friction first\n launch; no credentials to lose, but also nothing to recover if the device ID\n is gone (reinstall, new device).\n- **Linked login methods** — `loginWithEmail` / `registerWithEmail`,\n `loginWithGoogle`, `loginWithTelegram`, `loginWithSsoCode`. These tie\n the account to a real identity so the player can resume it elsewhere.\n- **`autoLogin()`** picks up where the player left off. It first extends the\n remembered session by its **refresh token** (works for every method). Only\n without a usable token does it fall back to replaying — and that fallback\n covers just the methods that don't need a fresh externally-issued token: it replays `loginWithEmail`\n if the last successful login was Email (saved password), `loginWithDeviceID`\n if it was Device/None, and `loginWithTelegram` if it was Telegram (its\n `initData` is re-read live from the Telegram WebApp bridge each time, not\n stored). For Google/Facebook/GooglePlay/platform-token, there's no stored\n credential to replay — `autoLogin()` returns `reason: \"client\"` instead of\n guessing, because DeviceID and email/platform logins hash to **different\n backend UserIDs** (see Gotchas), so silently falling back to a guest login\n would strand the player on an unrelated empty account. What `autoLogin()`\n has to replay is controlled by **\"remember me\"** — see below. Call this on app\n start instead of hand-rolling \"which method did they use last,\" but be ready\n to handle its failure by re-running the platform's sign-in flow.\n- **Session refresh is automatic.** The HTTP transport calls\n `refreshSession()` itself when a request comes back 401 — it calls the\n server's `RefreshSession` with the refresh token (falling back to replaying\n the login method only without a usable token) and retries once. You almost never call\n `refreshSession()` directly; it's documented here for completeness, not as a\n method you wire up in app code.\n- **Static validators** (`AuthenticationService.isValidEmail`,\n `AuthenticationService.isValidPasswordLength`) are plain synchronous\n functions, not calls to the backend — use them to validate a form before\n spending a network round-trip on a login/register call that will fail\n server-side anyway. They mirror, but do not replace, the server's own\n checks — the server always re-validates.\n\n## Setup\n\n```ts\nimport { createIDosGamesClient } from \"@idosgames/core\";\n\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\nawait client.auth.loginWithDeviceID(); // or any other auth.* login method\n\nclient.auth.isLoggedIn; // true once a login call has succeeded\n```\n\nEvery other module's methods require this to have succeeded first — without a\nsession they return `{ ok: false, reason: \"unauthorized\" }` rather than\nthrowing. There is one `client` per player; don't share it across sessions.\nAuthentication requests carry no Bearer ticket at all — login/register/reset\nare the one family of calls that work before you have a session.\n\n## Methods\n\nAll login/register/reset methods return `Promise<OperationResult<T>>`: either\n`{ ok: true, data }` or `{ ok: false, reason, error }`. Always branch on\n`result.ok` before touching `result.data`. `reason` is one of `\"client\"` (bad\nlocal args, e.g. missing Telegram initData or empty platform token),\n`\"unauthorized\"`, `\"throttled\"` (fired the same endpoint again inside the\nthrottle window), `\"connection\"` (transient, offer Retry), `\"validation\"`\n(response/schema drift), or `\"server\"` (backend rejected it — `error` carries\nthe reason code the backend returned, e.g. `\"INCORRECT_EMAIL_OR_PASSWORD\"`,\n`\"INVALID_VERIFICATION_CODE\"` — see Gotchas for the verbatim set).\n\n| Method | Purpose | `data` on success |\n| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- |\n| `loginWithDeviceID()` | Anonymous/guest login keyed off the device ID. | `ClientState` |\n| `loginWithTelegram()` | Login using the Telegram Mini App's init data. | `ClientState` |\n| `loginWithEmail(email, password)` | Login with email + password. | `ClientState` |\n| `registerWithEmail(email, password)` | **Start** a registration. Only mails a code — never returns a session, see below. | `EmailRegistrationOutcome` |\n| `confirmEmailRegistration(email, code)` | Finish the registration with the code from the email, creating the player. | `ClientState` |\n| `resendVerificationCode(email)` | Mail a fresh code. Resolves successfully whatever happened — except when there is no usable sender at all (`EMAIL_SENDER_NOT_CONFIGURED`). | `void` |\n| `requestWalletChallenge(networkID, address)` | Ask for the message the player's wallet must sign. Step one of wallet login. | `WalletChallengeResponse` |\n| `loginWithWallet(networkID, address, signature)` | Exchange the signed challenge for a title session. | `ClientState` |\n| `loginWithGoogle(googleIDToken)` | Login with a Google ID token. | `ClientState` |\n| `loginWithSsoCode(code)` | Exchange a one-time code from idosgames.com for a title session. **Web only** — see below. | `ClientState` |\n| `forgotPassword(email)` | Trigger a password-reset email (a 6-digit code). | `SuccessResponse` |\n| `resetPassword(email, resetToken, password)` | Complete a password reset using the emailed code. | `SuccessResponse` |\n| `autoLogin()` | Replay the last-used auth method (Email/Device/None/Telegram only — fails with `reason: \"client\"` for Google/Facebook/GooglePlay/platform-token). | `ClientState` |\n| `refreshSession()` | Re-authenticate and return a fresh session ticket; used internally by the transport on 401. | `string \\| null` (not an `OperationResult`) |\n| `logout()` | Clear the session, forget the saved login method, and reset the local cache. Synchronous, no network call. | `void` |\n| `setRememberSession(remember)` | \"Remember me\" — whether the NEXT login is persisted for `autoLogin()`. Default `true`. Call before a `login*` method. | `void` |\n| `AuthenticationService.isValidEmail(email)` | Static, synchronous client-side format check. | `boolean` |\n| `AuthenticationService.isValidPasswordLength(pw)` | Static, synchronous length check (8–100 chars). | `boolean` |\n\n⚠ **Each title has its OWN password.** The same address in two games means two independent\npasswords: resetting in one game does not change the login for the other. The reason is that the\nreset e-mail goes out through the PUBLISHER's SMTP — with one shared password, one publisher's mail\nserver would control access to every other publisher's game. So don't tell the player \"the same\npassword as in your other game\", and don't reuse stored credentials across titles.\nThe IDENTITY is still shared: Google, Apple and the idosgames.com sign-in recognise the same person\nin every game. Identity answers \"who is this\", not \"what proves they may sign in\".\n\n⚠ **Signing in through idosgames.com from a NATIVE build is not in this SDK.** `beginSsoRedirect` /\n`loginWithSsoCode` are the web half: the browser comes back to the same page with the code in the\nURL fragment. In a packaged Android/iOS build the browser is a separate program that tells the app\nnothing, so that flow uses a request the game polls for instead (`BeginNativeSsoRequest` /\n`PollNativeSsoRequest`), and it is implemented in the Unity SDK. Don't reach for `loginWithSsoCode`\nfrom a native wrapper — it has no way to receive the code.\n\n⚠ **That native flow now has a SHORT CODE, and a native client should use it.** `BeginNativeSsoRequest`\naccepts `UseUserCode: true` and then answers with `UserCode` (`WDJB-MJHT`) plus `VerificationUri`.\nThe game displays the code; the player opens the plain URL and types it there. The older shape — the\ngame opening `…/sso?title=X&request=<handle>` and the page confirming on its own — still works for\nalready-shipped builds, but **a request created with a code refuses that path**, so a build must do\none or the other, never a mix.\n\nThe reason is not convenience. In the older shape the handle travels IN THE ADDRESS, so the link can\nbe sent to somebody else: the attacker creates the request on their own device, the victim opens the\nlink and confirms, and the attacker's game gets a session as the victim. PKCE does not help (the\nverifier belongs to whoever created the request). With a code there is nothing in the URL to forward\n— the request is named only by what is on the device's own screen. `GetAuthOptions` reports\n`NativeSso.UserCodeSupported` and `NativeSso.PollIntervalSeconds`; ask before assuming, and follow\nthe interval or the poll quota will refuse you.\n\n⚠ **The code is shown, the secret is not.** Players read codes off screens, over shoulders and out\nof screenshots — so the code alone must never buy a session. Polling keeps requiring the request\nsecret, which never leaves the game.\n\nNon-Promise instance getters (read directly, no `await`):\n\n| Getter | Returns |\n| -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `client.auth.context` | `AuthContext \\| null` — `{ userID, clientSessionTicket, clientSessionTicketExpiration, platformUserID?, platformAuthToken?, platformAuthTokenExpiration? }`, or `null` if never logged in. |\n| `client.auth.isLoggedIn` | `true` once `context` has a non-empty `userID` and `clientSessionTicket`. |\n| `client.auth.lastAuthType` | The persisted `AuthType` (`\"None\" \\| \"Device\" \\| \"Email\" \\| \"iDosGames\" \\| \"Facebook\" \\| \"Google\" \\| \"GooglePlay\" \\| \"Telegram\"`) from the previous successful login, read from local storage. |\n\nOn a successful login/register call, the SDK mirrors the full `ClientState`\ninto `client.data` (title config + user state) — same cache every other\nmodule reads from — and emits events. `email`/`password` are persisted to\nlocal storage **only** after a successful `loginWithEmail` or\n`confirmEmailRegistration` call, so `autoLogin()` has something to replay.\n⚠ Note it is the CONFIRMATION, not `registerWithEmail`, that persists them:\nregistration alone creates no account, so there is nothing to replay yet (the\npassword is carried over from the first step internally). A leftover\nusername you pass in is only a fallback — Google logins use the Google\nprofile name, platform-token logins use the platform profile name, and if\nnone is available anywhere the backend assigns a generated one (see Gotchas).\n\n## Events\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn.\n\n- `auth:requestSent` → `void` — fired at the start of every login/reset call (before the network round-trip). One exception: `loginWithSsoCode` validates its argument first, so an empty-code `reason: \"client\"` failure doesn't fire it.\n- `auth:loggedIn` → `void` — fired after a login/register call succeeds and `ClientState` has been applied to the cache.\n- `auth:loggedOut` → `void` — fired synchronously by `logout()`.\n- `auth:unauthorized` → `void` — transport-level, fired by the HTTP layer only when a 401 could **not** be transparently recovered (the automatic refresh was unavailable, already attempted, or failed). A 401 that the refresh-and-retry rescues never emits it. Not emitted by `AuthenticationService` itself.\n- `user:clientStateReceived` → `ClientState` — fired whenever a full client state is applied (login, or any other flow that re-fetches it).\n- `user:stateUpdated` → `void` — fired whenever the cached `User` half of `ClientState` is replaced, which includes every successful login.\n- `user:anyUpdated` → `void` — coarsest \"something in the user cache changed\" signal; also fires on login.\n\n```ts\nconst off = client.on(\"auth:loggedIn\", () => {\n console.log(\"logged in as\", client.auth.context?.userID);\n});\n// later: off();\n```\n\n## Recipes\n\n### Guest login on first launch\n\nIn the base template the \"Play as guest\" button is OFF by default; a project turns it on with\n`auth: { guest: true }` in `src/app.config.ts`. The Google button shows only when the title has its\nOWN Google client (`getAuthOptions()` → `Google.Enabled` and not `Google.PlatformFallback`).\n\n```ts\nconst result = await client.auth.loginWithDeviceID();\nif (!result.ok) return showError(result.error ?? result.reason);\n// client.data now has the full ClientState; render the game.\n```\n\n### Try auto-login first, fall back appropriately\n\n```ts\nasync function bootstrap() {\n const result = await client.auth.autoLogin();\n if (result.ok) return; // resumed the last session\n if (result.reason === \"client\" && client.auth.lastAuthType !== \"None\") {\n // Last session used Google/Facebook/GooglePlay/a platform token — autoLogin\n // can't replay that silently. Re-run that platform's sign-in flow and call\n // the matching login method (e.g. loginWithGoogle) with a fresh token.\n return reauthenticateViaPlatformSDK(client.auth.lastAuthType);\n }\n showError(result.error ?? result.reason);\n}\n```\n\n`autoLogin()` safely self-replays Email, Device/None, and Telegram — call it\nonce on app start for those. For anything else it deliberately does **not**\nfall back to a guest login (see Gotchas) — treat its `reason: \"client\"`\nfailure as \"go get a fresh token,\" not as a generic error to toast.\n\n### Register a new account with email\n\n```ts\nif (!AuthenticationService.isValidEmail(email)) {\n return showFieldError(\"email\", \"Enter a valid email address.\");\n}\nif (!AuthenticationService.isValidPasswordLength(password)) {\n return showFieldError(\"password\", \"Password must be 8–100 characters.\");\n}\n\nconst started = await client.auth.registerWithEmail(email, password);\nif (!started.ok) return showError(started.error ?? started.reason);\n\n// No account yet — a code was mailed. Show a code screen; the session arrives from confirm.\nconst done = await client.auth.confirmEmailRegistration(email, codeFromPlayer);\nif (!done.ok) return showError(done.error ?? done.reason);\n```\n\n⚠ **`registerWithEmail` never creates an account, and success does not mean the address was\nfree.**\n\nRegistration always stores the attempt and mails a 6-digit code; the player is created by\n`confirmEmailRegistration`. Building a screen that assumes an immediate session will leave players\nstaring at a spinner that never resolves.\n\nConfirmation is **platform behaviour, not a title setting** — there is no flag to switch it off,\nand no `verificationRequired` branch to write. Only the numbers are configurable (code lifetime,\nattempt cap, resend cooldown).\n\nThe answer is also deliberately **identical for a free and for a taken address** — there is no\n`EMAIL_ALREADY_EXISTS` on this path any more. Telling them apart would turn the registration form\ninto a way of checking whether a given person has an account here, which is exactly what the\npassword-recovery form goes out of its way to avoid. The owner of a taken address gets a \"you\nalready have an account\" e-mail instead, without a code.\n\nOne failure on this path **is** reported: `EMAIL_SENDER_NOT_CONFIGURED`, when the server has no\nusable mail sender at all. It says nothing about the address — the same answer comes back for a\nfree one, a taken one and a typo — and it exists because confirmation cannot be switched off: with\nno sender, e-mail registration does not work at all, and answering \"code sent\" would hide a broken\ndeployment behind a normal-looking screen. Show it as a server-side problem, not as the player's\nmistake, and do not offer a \"resend\" — the resend refuses the same way. A letter that merely fails\nto leave is a different case and still answers success.\n\nEvery way of getting the code wrong — no such registration, expired, mismatched — answers with the\nsame `INVALID_VERIFICATION_CODE`, for the same reason. Do not branch on it.\n\n`resendVerificationCode` mails a **new** code: only its hash is stored, so the previous one cannot\nbe re-sent and stops working. Rate-limit the button in the UI (the server's cooldown is reported as\n`resendCooldownSeconds`) — the server answers successfully even when it sent nothing.\n\nEmail is matched case-insensitively — the backend trims and lowercases it\nbefore every lookup and write, so `Player@Mail.com` and `player@mail.com`\ncollide on the same account.\n\n### Upgrade a guest account to an email account\n\nThe SDK has no dedicated \"link email to this guest\" call — `registerWithEmail`\nalways mints its **own** account (a fresh UserID derived from the email, via a\ndifferent hash than the device-id account uses). To move a guest forward\nwithout losing their progress, do it through your own game-side flow (e.g. a\ncloud script that copies/merges state) rather than assuming registration\ncarries the guest's inventory along; don't build a UI that implies it will.\n\n### Forgot / reset password\n\n```ts\nconst sent = await client.auth.forgotPassword(email);\nif (!sent.ok) return showError(sent.error ?? sent.reason);\n// If an account exists for this email, a 6-digit reset code was emailed,\n// valid for 5 minutes. The response looks identical whether or not the\n// email is registered (see Gotchas) — don't tell the player \"no such account.\"\n\nconst reset = await client.auth.resetPassword(email, resetToken, newPassword);\nif (!reset.ok) return showError(reset.error ?? reset.reason);\n// password changed; call loginWithEmail with the new password next\n```\n\n### Handle a rejected login\n\n```ts\nconst result = await client.auth.loginWithEmail(email, password);\nif (!result.ok) {\n switch (result.reason) {\n case \"connection\":\n return offerRetry();\n case \"throttled\":\n return; // ignore — same call already in flight/just ran\n case \"client\":\n case \"server\":\n default:\n return showError(result.error ?? \"Login failed.\");\n }\n}\n```\n\n### Log out\n\n```ts\nclient.auth.logout(); // synchronous — clears context + resets client.data cache\n// route to login screen; no network call is made\n```\n\n## Gotchas\n\n- **A title's login mode can refuse methods** (`cfg.Blockchain.PlayAccess`). `WalletOnly` accepts\n only wallet login (others fail with `PLAY_ACCESS_WALLET_LOGIN_ONLY`); `AnyWithLinkedWallet` refuses\n guests (`PLAY_ACCESS_GUEST_DISABLED`) and lets other logins in with `client.auth.playAccess.Granted\n=== false` until a wallet is linked and holds enough tokens. Read the mode BEFORE login with\n `client.auth.getAuthOptions()` (`PlayAccessMode`) to hide dead buttons. The host template does all\n of this already — see the `blockchain-system` skill, \"Login mode and play access\".\n- **The wallet button is not offered on an open game.** `getAuthOptions().ShowWalletLogin` is true\n only when wallet login is on AND the mode is token-gated or the publisher set\n `Blockchain.WalletLogin.ShowOnLoginScreen` (`save_blockchain`). `DefaultLoginScreen` follows it — to\n show the button on an open game, set that flag on the title; do not hard-code the button.\n- **`autoLogin()` deliberately refuses to guess for federated/platform\n logins.** The backend derives DeviceID UserIDs from\n `SHA256(platform + device + deviceID)` and email/platform-linked UserIDs\n from `SHA256(email + titleID)` — two different hashes with no server-side\n link between them. Falling back to `loginWithDeviceID()` for a\n Google/Facebook/GooglePlay/platform-token session would silently resolve to\n a different, empty account, not \"safely resume as a guest.\" If\n `lastAuthType` is one of those, `autoLogin()` returns\n `{ ok: false, reason: \"client\" }` instead; the app must obtain a fresh token\n from that platform's SDK and call the matching `login*` method itself.\n- **The exact `error` strings on `reason: \"server\"` are backend-defined\n codes/messages**, not prose meant for direct display — surface them through\n your own copy/localization layer rather than showing them raw. Verified\n values from the backend for the flows above: `\"INVALID_INPUT_DATA\"`\n (missing/blank required field) and `\"INCORRECT_EMAIL_OR_PASSWORD\"` (bad\n credentials **or** unknown email — the backend intentionally doesn't\n distinguish the two, so don't tell the player \"no such account\") and\n `\"INVALID_VERIFICATION_CODE\"` (every way of getting the confirmation code\n wrong, collapsed into one) are shared\n `MessageCode` enum values also used elsewhere in the backend;\n `\"EMAIL_ALREADY_EXISTS\"` is **no longer returned by registration** — the form\n answers identically for a free and for a taken address, on purpose;\n `\"RATE_LIMIT_EXCEEDED\"` / `\"OPERATION_IN_PROGRESS\"` (per-account login lock\n — see below) are the same enum family. Google/platform-token logins instead\n return ad-hoc string literals specific to that flow, e.g. `\"BANNED_GLOBAL\"`\n (platform account is banned), `\"GOOGLE_ACCOUNT_CONFLICT\"` (email already\n linked to a _different_ Google account), `\"INVALID_GOOGLE_TOKEN\"`\n (bad/expired Google ID token) — see\n [references/data-model.md](references/data-model.md) for the full list.\n Telegram login instead surfaces plain sentences like `\"Invalid Telegram\ndata\"` / an internal `\"Telegram auth_date is stale\"` condition (initData\n older than 24h or timestamped implausibly in the future) — treat any\n non-uppercase-code string as an opaque message, not something to\n pattern-match on.\n- **Per-account login is rate-limited server-side, independent of the SDK's\n own 600 ms throttle.** Repeated `loginWithEmail` attempts for the same\n account inside a ~1-second window come back `\"RATE_LIMIT_EXCEEDED\"`; a login\n already being processed for that account comes back\n `\"OPERATION_IN_PROGRESS\"`. This is a brute-force guard, not a bug — don't\n retry-loop past it.\n- **Telegram login needs the platform adapter's init data.** `loginWithTelegram()`\n calls `platform.getTelegramInitDataRaw()` first; if that returns falsy (not\n running inside Telegram, or the adapter doesn't support it), the call fails\n client-side with `reason: \"client\"` before any network request — no point\n retrying without fixing the environment. Server-side, the init data is\n rejected if its `auth_date` is missing, more than 24 hours old, or more than\n 5 minutes in the future — a Telegram Mini App that's been idle a long time\n may need a fresh WebApp launch, not just a retry. On success it's recorded\n under its own `AuthType.Telegram` (not `Device`) precisely so `autoLogin()`\n replays `loginWithTelegram()` again rather than a device-id login.\n- **`loginWithPlatformToken` no longer exists.** The site used to hand games the\n platform auth token — a 24-hour string that also authorizes the whole\n management API. It was replaced by the one-time SSO code\n (`loginWithSsoCode`), and the endpoint has since been removed as unreachable.\n A session opened by that code is still recorded under `AuthType.iDosGames`,\n which is what `lastAuthType` reports afterward.\n- **A username you pass in is a last resort, not a guarantee.** For Google and\n platform-token logins, the backend prefers the identity provider's own\n profile name over any client-supplied username on every login (not just the\n first) — so a locally cached display name can be silently overwritten by the\n linked account's name. If no name is available from any source at\n registration time, the backend assigns a generated one\n (`\"BraveTiger482\"`-style: adjective + noun + 2–4 digit number) rather than\n leaving it blank.\n- **`forgotPassword` never reveals whether an email is registered.** Unknown\n emails and known emails both return `{ ok: true }` with no email actually\n sent for the unknown case — this is a deliberate anti-enumeration measure,\n not a bug to work around. The emailed reset code is a 6-digit number valid\n for 5 minutes; requesting again inside a 60-second cooldown silently\n no-ops (still returns `ok: true`, no second email). `resetPassword` allows\n at most 5 wrong-code attempts before the code is invalidated server-side and\n a fresh `forgotPassword` call is required.\n ⚠ **The remaining-attempt count is NOT in the response any more**, and neither\n is \"no such account\": every outcome of `resetPassword` — unknown address, no\n code requested, expired code, wrong code — answers `\"INVALID_RESET_CODE\"`, and\n an exhausted code answers `\"RESET_CODE_ATTEMPTS_EXCEEDED\"`. Reporting the\n remaining count both confirmed the account existed and told an attacker how\n much room was left. Don't parse a number out of `error`.\n- **Email credentials are only saved on success.** `loginWithEmail` and\n `confirmEmailRegistration` persist email+password to local storage (scoped by\n `titleID`) only after `fetchAndApplyClientState` succeeds — a failed login\n doesn't overwrite previously saved credentials, and a fresh install has\n nothing saved until the first successful email login. `registerWithEmail` on\n its own saves nothing: at that point the account does not exist yet.\n- **`refreshSession()` has a re-entrancy guard.** If it's already running\n (e.g. two requests 401 at nearly the same time), a second call returns `null`\n immediately instead of recursing — this exists to avoid deadlocking the\n retry, so don't assume a `null` result means the session is actually dead.\n- **`logout()` forgets the saved login method too (changed in core 0.1.3).** It\n nulls the auth context, wipes the cached user state and title-config bundle,\n clears `lastAuthType` and any saved email/password, and emits\n `auth:loggedOut` — so a later `autoLogin()` has nothing to resume. Before\n 0.1.3 storage survived a logout and the next launch silently signed the\n player back into the account they had just left. If you want a sign-out the\n player is remembered through, don't call `logout()`.\n- **Never reload the page after `logout()`** (or after deleting the account). The host shell\n (`AuthGate`) returns to the sign-in screen by itself on `auth:loggedOut`. A `location.reload()`\n restarts the whole app for nothing and, in the AI Coder's live preview, left a dark empty frame:\n the preview's files arrive by message and do not survive a reload.\n- **`refreshSession()` and `logout()` don't return `OperationResult`.**\n `refreshSession()` resolves to `string | null` (the fresh session ticket, or\n `null` on failure/no saved method); `logout()` is synchronous `void`. Don't\n branch on `.ok` for these two.\n- **`GameLoop` is excluded from the login `ClientState`.** Board/stage state\n and its definitions are fetched separately per-stage by the GameLoop\n feature, not bundled into every login response.\n- **Guard against double-submit.** Each call fires a real request; a\n double-tapped \"Log in\" button can fire twice. Firing the same auth endpoint\n again inside the SDK's own throttle window (default 600 ms) comes back\n `reason: \"throttled\"` rather than duplicating the call, but disable the\n control while a call is in flight rather than relying on that — and note the\n server has its own, stricter per-account lock on top (see above).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — request/response wire\nshapes, the exact backend validation and error-code rules for each login/reset\npath, UserID derivation, and the default-username generator. Read it when you\nneed to reason about _why_ a specific `error` string came back, or when\nbuilding a password-reset or registration UI that needs to match the backend's\ntiming/attempt rules exactly.\n",
|
|
5
5
|
"references": [
|
|
6
6
|
{
|
|
7
7
|
"path": "data-model.md",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hero-run",
|
|
3
|
+
"description": "Hero Run — the roguelike runner game module (Obsidian Knight's runs with Warcraft III's damage): the campaign of islands and levels, the rage and the choice of one of three cards, the 58 upgrade cards (a trigger and an effect, Obsidian Knight's form), the attack types × armor types table and the WC3 armor formula, troops dressed in the gear that gives their types, the hero's types from its gear (item CustomData AttackType / ArmorType), shields as a blue pool, one ultimate, potions, the camp, and the run economy (ENERGY → RUN_TICKET → reward chest through client.currency and client.lootbox). Use it whenever the user wants to add or tune a card, a troop, an island, a boss, the damage table, the energy or the rewards of hero-run — or a roguelike / auto-runner / card-draft / rock-paper-scissors damage game on the iDosGames SDK — even without naming the module.",
|
|
4
|
+
"content": "---\nname: hero-run\ndescription: >-\n Hero Run — the roguelike runner game module (Obsidian Knight's runs with\n Warcraft III's damage): the campaign of islands and levels, the rage and the\n choice of one of three cards, the 58 upgrade cards (a trigger and an effect, Obsidian Knight's form), the\n attack types × armor types table and the WC3 armor formula, troops dressed\n in the gear that gives their types, the hero's types from its gear (item\n CustomData AttackType / ArmorType), shields as a blue pool, one ultimate,\n potions, the camp, and the run economy (ENERGY → RUN_TICKET → reward chest\n through client.currency and client.lootbox). Use it whenever the user wants\n to add or tune a card, a troop, an island, a boss, the damage table, the\n energy or the rewards of hero-run — or a roguelike / auto-runner / card-draft\n / rock-paper-scissors damage game on the iDosGames SDK — even without naming\n the module.\n---\n\n# Hero Run\n\nFiles (module `modules/hero-run/src`):\n\n| File | What |\n| ----------------------- | ----------------------------------------------------------------------------------------------------- |\n| `game/damage.ts` | `DAMAGE_TABLE` (7 attack × 6 armor), `armorReduction` / `armorFactor` (WC3), `dealt`, types from gear |\n| `game/units.ts` | `TROOPS`: what each troop wears and holds (→ its types), its numbers; bosses |\n| `game/campaign.ts` | Islands (`troops`, `bosses`, `introduces`, `scouting`), `levelOf(n)` — a level's road, `scout` |\n| `game/cards.ts` | `CARDS` (58): passives (`apply` → `RunMods`) and triggers (`proc`: trigger, chance / every, effect) |\n| `game/battle.ts` | One run: road, segments, rage thresholds, cards pause, camp/fountain/chest, ultimate, potions, end |\n| `game/economy.ts` | Run economy from the title's config: energy → ticket, chests by tier, XP level |\n| `game/session.ts` | `RunSession`: hero, gear types, progress, start (convert), claim (lootbox open) |\n| `game/heroGear.ts` | The hero's attack / armor type and shield from its look and its items' CustomData |\n| `game/enemyLooks.ts` | A troop's look = its gear + a face from its id |\n| `phaser/BattleScene.ts` | Draws the run (no rules); number colour by the table (green — strong, grey — weak) |\n| `ui/*` | Map + scouting (`MapPanel`), run bar, card pick / camp / end popups, HUD with the rage bar |\n\n## Rules that are easy to break\n\n- **The ultimate goes once a run.** Ready from the start, kills do not charge it; only a card brings\n it back, by a chance at an action (`ultOnKill`, `ultOnLevel` → `restoreUlt`, event `ultBack`). Auto\n keeps it for the boss, a crowd or low health (`ultWorthIt`).\n- **No health regen.** Health comes back only from potions, the camp, fountains and the life cards.\n The sheet's eight numbers are Obsidian Knight's: damage, attack speed, crit chance, crit multiplier |\n defense (armor's share off a blow), counter (chance), dodge, lifesteal (chance, `LIFESTEAL_SHARE`).\n A new card effect that is a number goes into `HeroNumber`; any other effect goes into `effectLines`\n (the \"Info\" popup).\n- **The hero's stats are not sold.** Between runs they grow from gear (item levels) and rank on the\n Heroes screen, in a run from the cards. Do not bring back a gold Enhance ladder — it outgrows the\n cards. The demo world's stats have no `PriceOptions`, and the `character` system shows no upgrade for\n a priceless stat. The run's sheet (`Battle.heroNumbers`, `ui/RunStats.tsx`) shows each number as the\n hero came and with the cards.\n\n- **Damage goes through `dealt()` only.** Table first, then the armor value. `spells` ignore the armor\n value. Do not subtract armor flatly.\n- **A troop's types come from its gear** (`troopAttack` / `troopArmor`). Change the gear and the picture\n changes with the types. Name `attackType` / `armorType` on the troop only when the gear would lie\n (bosses — hero / hero, the warlock — chaos).\n- **New types come in island by island** (`introduces`). The first island stays casual: unarmored and\n light troops, normal and pierce attacks, no scouting. A test checks every introduced type is first met\n on its island. Every new type needs a tip text `tip.{kind}.{type}` in `i18n.ts` (EN and RU).\n- **A card is data, in Obsidian Knight's form:** a trigger and an effect, or a passive number.\n - Give it a rarity (common / rare / legendary), `island`, `stacks` and an `icon` (`ui/cardIcons.tsx`).\n - Write the RU and EN text with markup: `**trigger**`, `[g]number[/g]`, `[o]effect[/o]`.\n - A passive number is `apply(m)` changing `RunMods`; a triggered effect is\n `proc: { trigger, chance | every, effect }`.\n - A new effect kind goes into `Effect` in `cards.ts`, `Battle.apply` and, if it flies, `boltFx` in the\n scene.\n - A card with nothing to build on lists it in `needs` (`counter` / `dodge` / `shield`).\n - Keep 40–60 cards — a test checks it, and that every trigger of the reference is there.\n- **The run economy is the server's.** Energy and tickets go through `client.currency.convert`, the reward\n through `client.lootbox.open` with the ticket.\n - Never grant rewards from the client.\n - Lootbox ids: `run_chest_defeat` / `run_chest_victory` / `run_chest_treasure`, priced 1 `RUN_TICKET`.\n - The player's first `FIRST_CHESTS` (3) chests are the starter copies `run_chest_*_first`: the same\n contents plus a sure potion. The engine has no \"first N openings\" rule, so the client counts the\n opened chests in `hero_run_progress.chests`. A title without the `_first` chests opens the usual ones.\n - Conversions: `ENERGY → RUN_TICKET` (Manual rate = 1 / energy per ticket, MinAmount = MaxAmount),\n `POTION → XP`, `GEMS → XP` (camp feast).\n - Without the ENERGY → RUN_TICKET conversion the game plays free.\n- **The demo world is the engine's** (`iDos_Games_Engine/Tests/.../HeroRunStarterFixtures.cs` →\n `IDOS_UPDATE_SEEDS=1 dotnet test --filter StarterSeedExport`). Never edit `title-seed.json` by hand.\n\n## Recipes\n\n- **A new troop.** Add an entry to `TROOPS`, using fantasy-pack parts (`top/plate_armor`,\n `weapon/warhammer`, `offhand/tower_shield` …), then list it in an island's `troops`.\n- **A new island loop step.** Add an entry to `ISLANDS` in `campaign.ts` and a theme in `themes.ts`\n (the backdrop paints it). Add the tips of its `introduces`.\n- **Tuning.** `BASE_HP`, `BASE_ATTACK`, `POWER_GROWTH` (the level curve), `RAGE_THRESHOLDS`,\n `RARITY_WEIGHT`, `SHIELD_SHARE`, `CAMP_*` / `FOUNTAIN_*`, `ULTS`.\n- **Checking the balance.** `game/sim.ts` plays the real `Battle` headless on a title config (a\n `title-seed.json` or the title's sections).\n - `simulateRun` — one run.\n - `simulateCampaign` — a player who runs, opens chests as the server does (`RewardMultiplier`,\n `PityRules`), farms when stuck and upgrades with gold.\n - In the SDK `npm run balance -w @idosgames/mod-hero-run` prints the full report.\n - Rerun it after changing a number above, the chests, the gear prices or the cards. A level that\n takes dozens of tries, or a card far above the others, shows there first.\n- **Testing a run.** Use `new Battle({ level, stats, seed })` in `game.test.ts`. For a hand-made road use\n the test helper `road([...segments])`. Take cards in the loop (`takeFirst`). The stand pane runs at\n about 1 fps — fast-forward with `scene.update(0, 250)` from the console.\n",
|
|
5
|
+
"references": []
|
|
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.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### Videos on idosgames.com — `focus()`, `audio()`, `ctx.clips.mark()`\n\nOn idosgames.com the player records the game: **Clip** (the last 30/60 s, always kept in the\nbackground), **Record** (start/stop), **Best moments** (made automatically), then trims, picks\nYouTube 16:9 / TikTok 9:16 / square and shares. The host (`@idosgames/app-shell`) records the active\nscene's `<canvas>` by itself — no code is needed for a basic video. Three optional touches make the\nvideos good; add them to every canvas game:\n\n```ts\n// in the scene object\nfocus() { // where the action is, 0–1 on the canvas — the vertical cut follows it\n const p = worldToScreen(player.position); // your own projection\n return { x: p.x / canvas.width, y: p.y / canvas.height };\n},\naudio() { // the game's sound, so videos are not silent\n return musicAndSfxDestination.stream; // a MediaStreamAudioDestinationNode your\n}, // master gain ALSO feeds (keep the speakers)\n\n// in setup() / gameplay — bright moments become the player's \"Best moments\" clips\nctx.clips.mark(\"boss_down\", 8); // weight 1–10: a boss 8, a record 6, a level-up 3, a silly death 4\n```\n\n- A first-person game needs no `focus` (the action is in the centre). Do not call it per frame —\n the host samples it a few times a second.\n- `audio()` returns `null` until your AudioContext exists (it is created on the first click) — fine,\n the host asks again. Route every sound through ONE master node and tap that (VoxelCraft:\n `systems/Audio.ts` `output()` / `recordStream()`).\n- Mark real moments only — the session keeps the 3 heaviest; close marks merge and add up.\n- An analytics event the publisher flagged as a highlight (`Analytics.Events[name].Highlight`) and\n loud voice chat are marks too, without any code.\n- DOM HUD panels are NOT in the video (only the canvas) — a clean video reads better in a feed.\n- Reference: `modules/voxelcraft` (`scene.ts` `audio`, `game/wire.ts` marks).\n\n### `capture()` — one still frame\n\nA scene may also answer the platform's still-frame request through the optional\n`capture?(): Promise<Blob | null>`. The host (`@idosgames/app-shell`) wires the request itself —\na module only has to return a picture of its active scene.\n\n**Implement it in any scene that renders to a canvas.** Without it the host falls back to reading\nthe `<canvas>` from outside, and for WebGL that is a trap: a renderer created WITHOUT\n`preserveDrawingBuffer` (the default, and the right default — the flag costs memory and a copy every\nframe) has already cleared its buffer by the time anyone reads it, so `toBlob()` returns a **fully\nblack** image and reports nothing. The host rejects single-colour frames, so the player gets the\ngame's cover instead of their moment.\n\nRender and read in the SAME tick:\n\n```ts\n// three.js\ncapture() {\n return new Promise((resolve) => {\n renderer.render(scene, camera); // draw now…\n renderer.domElement.toBlob(resolve, \"image/png\"); // …and read before the buffer is cleared\n });\n},\n\n// Phaser — use its own snapshot, it handles the timing\ncapture() {\n return new Promise((resolve) => {\n setTimeout(() => resolve(null), 1500); // no frame came (context lost) → answer \"no\", don't hang\n game.loop.wake(); // a sleeping loop never draws again → snapshot never fires\n game.renderer.snapshot((image) => {\n if (!(image instanceof HTMLImageElement)) return resolve(null);\n const c = document.createElement(\"canvas\");\n c.width = image.naturalWidth || image.width;\n c.height = image.naturalHeight || image.height;\n c.getContext(\"2d\")?.drawImage(image, 0, 0);\n c.toBlob(resolve, \"image/png\");\n });\n });\n},\n```\n\nAlways answer — with a Blob or `null`. A `capture()` that never settles makes the site wait out its\ntimeout before it falls back, and the player stares at a spinner.\n\nReturn `null` when there is nothing worth showing (loading, no scene yet) — the site then uses the\ncover. Only the scene is captured, not the React panels over it: a shot for a feed reads better\nwithout the HUD. DOM-only modules (no canvas) simply omit `capture`. Working references:\n`modules/board-game` and `modules/voxelcraft` (three), `modules/idle-rpg` (Phaser).\n\n## UiPanel (the React part)\n\nPanels are React components the host renders inside its provider stack (client + status already\nprovided). Use `@idosgames/react` hooks (`useIDosGamesClient`, `useUserState`) — do NOT re-create\nproviders.\n\n- `slot`: `hud | sidebar | overlay | modal`.\n- `activeOnly` (default true): show only while this module's mode is active. Set `false` for shared\n chrome that stays across every mode (e.g. a persistent HUD).\n- `overlay`/`sidebar` layers sit between the shared HUD and the nav (the host measures both), so a\n panel positioned `absolute` inside its layer never ends up under them. Things drawn outside the\n layers use `var(--idos-safe-top, 0px)` / `var(--idos-safe-bottom, 0px)`.\n- A `hud` panel's wrapper is `display: contents`: the panel is a direct flex child of the HUD row\n (can `flex: 1`) and must set `pointer-events: auto` only on its controls.\n\n## Shared chrome (`sharedUi`)\n\nRoles: `currency-bar` (virtual-currency balances), `wallet` (crypto wallet entry — `LazyWalletPanel`),\n`status` (the `useStatus()` line), `account` (Log out + player ID), `menu` (the menu of features),\n`modes` (the switch between modes). The base takes `currency-bar`, `wallet`, `account`, `menu` and\n`modes` — its lobby draws them; `status` stays with each game. A module that draws one for every\nmode declares `sharedUi: { provides: [...] }` on the module object — statically, so the host resolves\nowners before any `setup()` and the answer never depends on module order.\n\n**Rule for templates: yield every role you draw yourself.** Read it once in `setup()` and close over\nthe answer (it is fixed for the session):\n\n```ts\nsetup(ctx) {\n const chrome = {\n wallet: ctx.sharedUi.shouldDraw(\"wallet\"),\n balances: ctx.sharedUi.shouldDraw(\"currency-bar\"),\n status: ctx.sharedUi.shouldDraw(\"status\"),\n };\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: makeRootPanel(box, chrome) });\n}\n```\n\nHide only the SHARED pieces — genre UI (the board's stage/tile/cycle chips) always stays. Never\nwrap your UI in your own `<StatusProvider>`: the host provides ONE, and a nested one would swallow\nyour messages so the shared status line never sees them. A module that needs a role but does not\ndraw it declares `sharedUi: { requires: [\"currency-bar\"] }`. `ctx.sharedUi.ownerOf(role)` names the\nprovider (or `null`). A full-screen mode (its own bottom tab bar) that shows the player ID and\n\"Log out\" in its own header declares `sharedUi: { drawsOwn: [\"account\"] }` and draws them only while\n`shouldDraw(\"account\")`: the host then hides its floating bottom-left copy while that mode is on\nscreen (it would cover the tab bar) and keeps it in the other modes. `drawsOwn` takes no role —\nwith the base installed the lobby draws the account and the mode hides its copy.\n\n## Events (`ctx.events`)\n\nTyped topic tokens — `defineTopic(\"<your-id>:<event>@1\", shape({ … }))` from\n`@idosgames/module-sdk`; emit only your own namespace; to listen to another module, copy its topic\nand payload descriptor from the catalog into your own `defineTopic` (never import it); declare both\nin `module.meta.json` (`events.emits` / `events.listens`). **Subscribe in `setup()`**, not in a panel\neffect — `activeOnly` panels unmount with their mode and miss events; store what arrives in a small\nstore the panel reads. Full rules and examples: **idosgames-compose-modules** (\"Signals between\nmodules\"). The string overloads (`emit(\"x\", …)`) are deprecated.\n\n## Content types (`ctx.content`) and switching modes (`ctx.navigate`)\n\nA game whose players make things (worlds, levels, skins) registers a **content type handler** so the\nshared Workshop module can publish and open them — the Workshop never imports the game:\n\n```ts\nsetup(ctx) {\n ctx.content.registerType({\n type: \"voxelcraft.world\", // = a key of Workshop.ContentTypes in the title config\n label: \"VoxelCraft world\",\n icon: \"⛏️\",\n modeId: \"voxelcraft\", // route to switch to after open()\n async listLocal() { return saves.map((s) => ({ id: s.id, name: s.name, updatedAt: s.at })); },\n async capture(localId) { // roles/MIME types must fit the title's content type config\n return { files: [{ role: \"main\", contentType: \"application/json\", data: json }],\n thumbnail: { contentType: \"image/webp\", data: webp }, suggestedTitle: name };\n },\n async open(content) { importSave(content.files[0].data); },\n });\n}\n```\n\nHandlers registered in `setup()` are removed with the module; `registerType` returns an unregister\nfunction for anything dynamic. `ctx.navigate(modeId)` switches the host to another module's route\n(the Workshop uses it to jump into the game after `open`); an unknown id is ignored. Details:\n**workshop-system**.\n\n## Bridging scene ↔ panel\n\nThe scene creates its controller at `mount` (needs the canvas), but panels render before that. Share\nit with a one-slot observable and read it with `useSyncExternalStore`:\n\n```ts\nexport function createControllerBox<T>() {\n /* get/set/subscribe */\n}\n// panel: const controller = useSyncExternalStore(box.subscribe, box.get); if (!controller) return <Loading/>;\n```\n\nFor the module's controller React context use the shared factory instead of hand-writing it:\n\n```ts\nexport const [BoardControllerProvider, useBoardController] =\n createControllerContext<BoardController>(\"BoardController\"); // from @idosgames/react\n```\n\n## Layout and boundaries\n\nA module is one folder, `src/modules/<id>/`. It imports only its own files, the game's shared code\nin `src/shared/` and npm packages — never another module's files or host files. Talk to other\nmodules through the shared `ctx.client` (durable state) and `ctx.events` (live signals, typed\ntopics `<module-id>:<event>@<major>` declared in `module.meta.json`). A module shipped in the catalog is fully self-contained — it never uses\n`src/shared/`, so it installs into any game; a game's own module may use it, and its shared code is\ncopied into it when it is published for other creators.\n\n```\nsrc/modules/<id>/\n index.ts export { <camelCaseId>Module } from \"./module\";\n module.ts defineModule({ … })\n module.meta.json manifest: type (template|feature), summary, provides, tags, version, author\n components/ game/ data/ react/ — as needed\n```\n\nEvery module carries its own `module.meta.json` (the `ModuleManifest` shape), including a game's own\nmodules. The full standard — where a new feature goes, file size, documentation — is\n**idosgames-project-structure**.\n\n## Dependencies\n\nDeclare only the module's UNIQUE deps (e.g. `phaser` for idle-rpg, `three` for board/voxel) plus the\nshared baseline (react, @idosgames/*). The platform pins shared libs identically across modules; two\nmodules must not request different versions of one package (the build allowlist rejects it).\n\nStudy a real module via `get_module {id}` (MCP) before writing a new one.\n",
|
|
3
|
+
"description": "Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk: the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route registration, and the shared controller-box bridge between an imperative scene and React panels. Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg, voxelcraft, 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.characterArt (the game's own hero drawn on the Heroes screen and other modules' screens), ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.",
|
|
4
|
+
"content": "---\nname: idosgames-module-contract\ndescription: >-\n Write or modify an iDosGames feature module against the module contract in @idosgames/module-sdk:\n the Module manifest, ModuleContext, EngineScene (Three/Phaser/vanilla), UiPanel (React), route\n registration, and the shared controller-box bridge between an imperative scene and React panels.\n Use this whenever a developer authors a NEW module, edits an existing one (board-game, idle-rpg,\n voxelcraft, mini-game, store, character, workshop…), or asks about defineModule,\n registerScene/registerPanel/registerRoute, ctx.effect / ctx.every (background work: badges, polling),\n ctx.features (register / open / setBadge / setAvailable),\n ctx.modes,\n activate/suspend, SceneMountContext, sharedUi / ctx.sharedUi.shouldDraw, ctx.events / defineTopic,\n ctx.content / ContentTypeHandler (letting the Workshop publish and open the game's content),\n ctx.multiplayer / registerGame (playing online through the lobby's rooms system),\n ctx.lobby / setHero (a game's showcase on the lobby's home — its character on a pedestal + \"Play\"),\n ctx.characterArt (the game's own hero drawn on the Heroes screen and other modules' screens),\n ctx.navigate, the --idos-safe-* layout variables, or the {camelCase(id)}Module export convention.\n---\n\n# The module contract (@idosgames/module-sdk)\n\nA module is a manifest the host installs once. It exports `{camelCase(id)}Module` from its\n`index.ts` (e.g. `board-game` → `boardGameModule`, `idle-rpg` → `idleRpgModule`) — the host seeder\nderives the import name from the id, so this convention is required.\n\n```ts\nimport { defineModule } from \"@idosgames/module-sdk\";\n\nexport const boardGameModule = defineModule({\n id: \"board-game\",\n meta: { name: \"Board Game\", type: \"game\", genre: \"board\", engine: \"three\" }, // engine: three|phaser|dom\n setup(ctx) {\n // ctx.client (shared, authed) · ctx.events (typed cross-module bus) · ctx.sharedUi · ctx.surface\n ctx.registerScene(createScene(box)); // rendered engine scene (optional)\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: RootPanel }); // React UI (optional)\n ctx.registerRoute({ id: \"board-game\", label: \"Board\", icon: \"🎲\" }); // a mode → the lobby's \"Play\"\n },\n});\n```\n\nTwo kinds of module plug into the base (sign-in and the lobby, from `@idosgames/app-shell`):\n\n- a **game** (`meta.type: \"game\"`) registers a route: the lobby's \"Play\" launches it on the whole\n screen with a small \"To the lobby\" button over it (none with `ownExit: true` — the game leads back\n by itself, voxelcraft's pause menu); with `inLobby: true` on the route (idle-rpg) it\n instead lives INSIDE the lobby — the base keeps its balances and tabs around the game through the\n host's `header`/`footer` panel slots (the canvas and every other slot are laid out between them,\n so a game needs nothing for it) and the app starts in it;\n- a **system** (`meta.type: \"app\"`) registers a feature — its screen becomes a lobby tab:\n `ctx.features.register({ id, label, icon, group, order, primary?, available?, fullBleed?, Screen })`\n (`fullBleed: true` — the screen paints edge to edge, the lobby gives it the whole area). A tab\n that depends on title data registers with `available: false` and calls\n `ctx.features.setAvailable(id, ok, reason)` once it knows (details: **idosgames-ui-kit**).\n Give `label` as a function — `label: () => t(\"title\")` — so the tab follows the player's language\n switch without a reload (a plain string stays as registered).\n\n`ctx.modes.list()` / `current()` / `subscribe()` — every mode (id, label, icon, module, module type),\nfilled once all modules are installed (read it at render time, not inside `setup()`).\n`ctx.navigate(id)` switches (`\"lobby\"` is the base); called inside `setup()` it picks the mode the app\nstarts in. `ctx.features.open(id, args)` from a game (the lobby not on screen) switches to the lobby\nand opens that tab there.\n\n`ctx.lobby` — the base's lobby as seen by a game. A game NEVER draws a lobby of its own (no own\n\"Play\" screen, no own menu of creation screens): sign-in, balances, profile and every system are the\nbase's, and the game plugs into it —\n\n- `ctx.lobby.setHero({ mode, mount(host, stage), view(), subscribe(fn), beforePlay? })` puts its\n showcase on the lobby's home (\"Play\" tab), Fortnite-style: `mount` draws into an element filling\n the home behind the lobby's interface (any engine; stand the figure in `stage.spot()` — CSS px of\n the host, read it every frame), returns the cleanup; `view()` describes what the lobby lays out\n with the UI kit around it — `title` / `subtitle` under the figure, `actions` there (\"Edit\",\n \"Emote\" with a `menu`), and the `play` card next to the big \"Play\" (`caption`, `title`, `actions`,\n `more` for its \"⋯\"). The lobby's \"Play\" switches to `mode`. Reference: voxelcraft `src/lobby/hero.ts`;\n- the game's own sections (worlds, character editor, items…) are ordinary features\n (`ctx.features.register`, screens on the UI kit) — tabs next to the shop and the quests;\n- `ctx.lobby.available()` / `open()` — \"To the lobby\" in the game's pause menu (hide it when false).\n Works before the game's scene is mounted: keep what the tabs and the hero need outside the scene\n (voxelcraft: `src/hub.ts`) and hand the choice made there to the scene when it starts.\n\n`ctx.characterArt` — the game's hero on OTHER modules' screens (the `character` system's Heroes\nscreen: the hub, the hero card, the hero list). A game with a character of its own sets it once in\nsetup — `ctx.characterArt.set({ mount(host, subject, { mode }) })`: draw the hero described by\n`subject` (`characterId`, `classId`, `equipment` slot → `ItemID`, `skinId`, `locked`) filling `host`,\nstanding on its bottom edge; `mode` is `\"live\"` (a big view: it may move) or `\"still\"` (one frame, a\nlist of many); return `{ update(subject), destroy() }` — `update` comes on every change (a piece\nequipped). A screen reads `ctx.characterArt.get()` and falls back to its own picture when it is null.\nReference: idle-rpg `src/heroArt.ts` (its layered hero on a WebGL canvas, `CharacterCanvas`).\n\n`ctx.multiplayer` — a game that plays online registers itself (`registerGame({ mode, topology,\nchatMode, maxPlayers, joinInProgress, settings, start(room) })`) and takes the room the lobby's\nrooms system hands it on \"Play\"; it never creates or joins rooms (skill **multiplayer-realtime**).\n\nOptional static field: `sharedUi: { provides?: SharedUiRole[]; requires?: SharedUiRole[] }` — see\n\"Shared chrome\" below.\n\n`meta.type` is `game | app | ai-app`; `meta.engine` is `three | phaser | dom` (`dom` = no renderer —\na pure React/DOM app module). `setup` is called ONCE with `ctx: ModuleContext`.\n\n## EngineScene (the rendered part)\n\nA scene mounts into a bare `HTMLElement` (framework-free — this is why a vanilla Three game like\nvoxelcraft fits). The host's Mode Router drives it; only the active mode runs.\n\n```ts\nconst scene: EngineScene = {\n surface: \"fullbleed-canvas\",\n mount(ctx: SceneMountContext) {\n controller = new Controller(ctx.host);\n box.set(controller);\n },\n activate() {\n controller?.setRunning(true);\n }, // became the active mode → resume RAF\n suspend() {\n controller?.setRunning(false);\n }, // hidden → stop RAF (invariant: only active ticks)\n destroy() {\n controller?.destroy();\n }, // permanent teardown\n};\n```\n\n`mount` takes a context object (not a bare element) so it can grow — e.g. a host-shared renderer in\nthe composition era — without breaking the contract. A scene MUST stop its RAF on `suspend`.\n\n### Loading — never a still screen\n\nA scene's FIRST start goes behind the app's loading screen by itself: the host shows it (themed,\n\"Loading «<mode>»…\"), lets it paint, THEN calls `mount` — so heavy synchronous work in `mount`\n(generating terrain, building meshes) never looks like a frozen app. The screen goes away after the\nfirst frame, or — when the scene has more to do — when its optional `ready(): Promise<void>` resolves\n(voxelcraft: the last played world restored from storage). Never draw your own start-up spinner.\n\nLong work AFTER the start goes through `ctx.loading` — the same screen:\n\n- `await ctx.loading.track(fetchWorld(), \"Loading the world…\")` — async work;\n- `await ctx.loading.run(() => buildWorld(seed), \"Building the world…\")` — work that BLOCKS the page:\n the screen is shown and painted first (calling heavy code right after `begin()` freezes the page\n before the screen appears);\n- `const task = ctx.loading.begin(\"…\"); task.progress(0.4, \"Textures…\"); task.done();` — with progress.\n\n### Notifications — `ctx.notifications`\n\nThe app's one system of toasts and alert windows (the UI kit draws them, themed): never draw a banner\nof your own. `ctx.notifications.toast({ level, title, message, icon, image, source, key, actions })`\n— `level`: `info | success | warning | error | reward`; `source` defaults to the module's id (the\ntoast shows that screen's name and a tap opens it). `await ctx.notifications.alert({ level, title,\nmessage, actions: [{ id: \"go\", label: \"Open\", primary: true, openFeature: \"season\" }, { label: \"Later\" }] })`\nresolves with the pressed action's id (`null` — closed). React screens: `useNotify()` from the kit.\n\nA module that registers a scene MUST also publish its debug surface — `ctx.exposeToAgent({ state,\nactions, describeActions })` — and must NOT gate controls on Pointer Lock (unavailable in the\npreview's cross-origin iframe). Nothing inside a `<canvas>` is observable from the DOM, so without\nthe surface neither the AI Coder nor a human reviewer can tell what the game is doing. See the\n`idosgames-agent-debug-surface` skill.\n\n### Videos on idosgames.com — `focus()`, `audio()`, `ctx.clips.mark()`\n\nOn idosgames.com the player records the game: **Clip** (the last 30/60 s, always kept in the\nbackground), **Record** (start/stop), **Best moments** (made automatically), then trims, picks\nYouTube 16:9 / TikTok 9:16 / square and shares. The host (`@idosgames/app-shell`) records the active\nscene's `<canvas>` by itself — no code is needed for a basic video. Three optional touches make the\nvideos good; add them to every canvas game:\n\n```ts\n// in the scene object\nfocus() { // where the action is, 0–1 on the canvas — the vertical cut follows it\n const p = worldToScreen(player.position); // your own projection\n return { x: p.x / canvas.width, y: p.y / canvas.height };\n},\naudio() { // the game's sound, so videos are not silent\n return musicAndSfxDestination.stream; // a MediaStreamAudioDestinationNode your\n}, // master gain ALSO feeds (keep the speakers)\n\n// in setup() / gameplay — bright moments become the player's \"Best moments\" clips\nctx.clips.mark(\"boss_down\", 8); // weight 1–10: a boss 8, a record 6, a level-up 3, a silly death 4\n```\n\n- A first-person game needs no `focus` (the action is in the centre). Do not call it per frame —\n the host samples it a few times a second.\n- `audio()` returns `null` until your AudioContext exists (it is created on the first click) — fine,\n the host asks again. Route every sound through ONE master node and tap that (VoxelCraft:\n `systems/Audio.ts` `output()` / `recordStream()`).\n- Mark real moments only — the session keeps the 3 heaviest; close marks merge and add up.\n- An analytics event the publisher flagged as a highlight (`Analytics.Events[name].Highlight`) and\n loud voice chat are marks too, without any code.\n- DOM HUD panels are NOT in the video (only the canvas) — a clean video reads better in a feed.\n- Reference: `modules/voxelcraft` (`scene.ts` `audio`, `game/wire.ts` marks).\n\n### `capture()` — one still frame\n\nA scene may also answer the platform's still-frame request through the optional\n`capture?(): Promise<Blob | null>`. The host (`@idosgames/app-shell`) wires the request itself —\na module only has to return a picture of its active scene.\n\n**Implement it in any scene that renders to a canvas.** Without it the host falls back to reading\nthe `<canvas>` from outside, and for WebGL that is a trap: a renderer created WITHOUT\n`preserveDrawingBuffer` (the default, and the right default — the flag costs memory and a copy every\nframe) has already cleared its buffer by the time anyone reads it, so `toBlob()` returns a **fully\nblack** image and reports nothing. The host rejects single-colour frames, so the player gets the\ngame's cover instead of their moment.\n\nRender and read in the SAME tick:\n\n```ts\n// three.js\ncapture() {\n return new Promise((resolve) => {\n renderer.render(scene, camera); // draw now…\n renderer.domElement.toBlob(resolve, \"image/png\"); // …and read before the buffer is cleared\n });\n},\n\n// Phaser — use its own snapshot, it handles the timing\ncapture() {\n return new Promise((resolve) => {\n setTimeout(() => resolve(null), 1500); // no frame came (context lost) → answer \"no\", don't hang\n game.loop.wake(); // a sleeping loop never draws again → snapshot never fires\n game.renderer.snapshot((image) => {\n if (!(image instanceof HTMLImageElement)) return resolve(null);\n const c = document.createElement(\"canvas\");\n c.width = image.naturalWidth || image.width;\n c.height = image.naturalHeight || image.height;\n c.getContext(\"2d\")?.drawImage(image, 0, 0);\n c.toBlob(resolve, \"image/png\");\n });\n });\n},\n```\n\nAlways answer — with a Blob or `null`. A `capture()` that never settles makes the site wait out its\ntimeout before it falls back, and the player stares at a spinner.\n\nReturn `null` when there is nothing worth showing (loading, no scene yet) — the site then uses the\ncover. Only the scene is captured, not the React panels over it: a shot for a feed reads better\nwithout the HUD. DOM-only modules (no canvas) simply omit `capture`. Working references:\n`modules/board-game` and `modules/voxelcraft` (three), `modules/idle-rpg` (Phaser).\n\n## UiPanel (the React part)\n\nPanels are React components the host renders inside its provider stack (client + status already\nprovided). Use `@idosgames/react` hooks (`useIDosGamesClient`, `useUserState`) — do NOT re-create\nproviders.\n\n- `slot`: `hud | sidebar | overlay | modal`.\n- `activeOnly` (default true): show only while this module's mode is active. Set `false` for shared\n chrome that stays across every mode (e.g. a persistent HUD).\n- `overlay`/`sidebar` layers sit between the shared HUD and the nav (the host measures both), so a\n panel positioned `absolute` inside its layer never ends up under them. Things drawn outside the\n layers use `var(--idos-safe-top, 0px)` / `var(--idos-safe-bottom, 0px)`.\n- A `hud` panel's wrapper is `display: contents`: the panel is a direct flex child of the HUD row\n (can `flex: 1`) and must set `pointer-events: auto` only on its controls.\n\n## Shared chrome (`sharedUi`)\n\nRoles: `currency-bar` (virtual-currency balances), `wallet` (crypto wallet entry — `LazyWalletPanel`),\n`status` (the `useStatus()` line), `account` (Log out + player ID), `menu` (the menu of features),\n`modes` (the switch between modes). The base takes `currency-bar`, `wallet`, `account`, `menu` and\n`modes` — its lobby draws them; `status` stays with each game. A module that draws one for every\nmode declares `sharedUi: { provides: [...] }` on the module object — statically, so the host resolves\nowners before any `setup()` and the answer never depends on module order.\n\n**Rule for templates: yield every role you draw yourself.** Read it once in `setup()` and close over\nthe answer (it is fixed for the session):\n\n```ts\nsetup(ctx) {\n const chrome = {\n wallet: ctx.sharedUi.shouldDraw(\"wallet\"),\n balances: ctx.sharedUi.shouldDraw(\"currency-bar\"),\n status: ctx.sharedUi.shouldDraw(\"status\"),\n };\n ctx.registerPanel({ id: \"root\", slot: \"overlay\", component: makeRootPanel(box, chrome) });\n}\n```\n\nHide only the SHARED pieces — genre UI (the board's stage/tile/cycle chips) always stays. Never\nwrap your UI in your own `<StatusProvider>`: the host provides ONE, and a nested one would swallow\nyour messages so the shared status line never sees them. A module that needs a role but does not\ndraw it declares `sharedUi: { requires: [\"currency-bar\"] }`. `ctx.sharedUi.ownerOf(role)` names the\nprovider (or `null`). A full-screen mode (its own bottom tab bar) that shows the player ID and\n\"Log out\" in its own header declares `sharedUi: { drawsOwn: [\"account\"] }` and draws them only while\n`shouldDraw(\"account\")`: the host then hides its floating bottom-left copy while that mode is on\nscreen (it would cover the tab bar) and keeps it in the other modes. `drawsOwn` takes no role —\nwith the base installed the lobby draws the account and the mode hides its copy.\n\n## Events (`ctx.events`)\n\nTyped topic tokens — `defineTopic(\"<your-id>:<event>@1\", shape({ … }))` from\n`@idosgames/module-sdk`; emit only your own namespace; to listen to another module, copy its topic\nand payload descriptor from the catalog into your own `defineTopic` (never import it); declare both\nin `module.meta.json` (`events.emits` / `events.listens`). **Subscribe in `setup()`**, not in a panel\neffect — `activeOnly` panels unmount with their mode and miss events; store what arrives in a small\nstore the panel reads. Full rules and examples: **idosgames-compose-modules** (\"Signals between\nmodules\"). The string overloads (`emit(\"x\", …)`) are deprecated.\n\n## Content types (`ctx.content`) and switching modes (`ctx.navigate`)\n\nA game whose players make things (worlds, levels, skins) registers a **content type handler** so the\nshared Workshop module can publish and open them — the Workshop never imports the game:\n\n```ts\nsetup(ctx) {\n ctx.content.registerType({\n type: \"voxelcraft.world\", // = a key of Workshop.ContentTypes in the title config\n label: \"VoxelCraft world\",\n icon: \"⛏️\",\n modeId: \"voxelcraft\", // route to switch to after open()\n async listLocal() { return saves.map((s) => ({ id: s.id, name: s.name, updatedAt: s.at })); },\n async capture(localId) { // roles/MIME types must fit the title's content type config\n return { files: [{ role: \"main\", contentType: \"application/json\", data: json }],\n thumbnail: { contentType: \"image/webp\", data: webp }, suggestedTitle: name };\n },\n async open(content) { importSave(content.files[0].data); },\n });\n}\n```\n\nHandlers registered in `setup()` are removed with the module; `registerType` returns an unregister\nfunction for anything dynamic. `ctx.navigate(modeId)` switches the host to another module's route\n(the Workshop uses it to jump into the game after `open`); an unknown id is ignored. Details:\n**workshop-system**.\n\n## Bridging scene ↔ panel\n\nThe scene creates its controller at `mount` (needs the canvas), but panels render before that. Share\nit with a one-slot observable and read it with `useSyncExternalStore`:\n\n```ts\nexport function createControllerBox<T>() {\n /* get/set/subscribe */\n}\n// panel: const controller = useSyncExternalStore(box.subscribe, box.get); if (!controller) return <Loading/>;\n```\n\nFor the module's controller React context use the shared factory instead of hand-writing it:\n\n```ts\nexport const [BoardControllerProvider, useBoardController] =\n createControllerContext<BoardController>(\"BoardController\"); // from @idosgames/react\n```\n\n## Layout and boundaries\n\nA module is one folder, `src/modules/<id>/`. It imports only its own files, the game's shared code\nin `src/shared/` and npm packages — never another module's files or host files. Talk to other\nmodules through the shared `ctx.client` (durable state) and `ctx.events` (live signals, typed\ntopics `<module-id>:<event>@<major>` declared in `module.meta.json`). A module shipped in the catalog is fully self-contained — it never uses\n`src/shared/`, so it installs into any game; a game's own module may use it, and its shared code is\ncopied into it when it is published for other creators.\n\n```\nsrc/modules/<id>/\n index.ts export { <camelCaseId>Module } from \"./module\";\n module.ts defineModule({ … })\n module.meta.json manifest: type (template|feature), summary, provides, tags, version, author\n components/ game/ data/ react/ — as needed\n```\n\nEvery module carries its own `module.meta.json` (the `ModuleManifest` shape), including a game's own\nmodules. The full standard — where a new feature goes, file size, documentation — is\n**idosgames-project-structure**.\n\n## Dependencies\n\nDeclare only the module's UNIQUE deps (e.g. `phaser` for idle-rpg, `three` for board/voxel) plus the\nshared baseline (react, @idosgames/*). The platform pins shared libs identically across modules; two\nmodules must not request different versions of one package (the build allowlist rejects it).\n\nStudy a real module via `get_module {id}` (MCP) before writing a new one.\n",
|
|
5
5
|
"references": []
|
|
6
6
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "idosgames-project-structure",
|
|
3
3
|
"description": "The layout standard for an iDosGames game project (host shell + composable modules) and how it grows without turning into a mess: where a new feature goes (a catalog module, an existing module, or a new module), the folder layout inside a module, module boundaries (no cross-module imports), the game's shared code in src/shared/, module.meta.json, src/modules.ts being platform-generated, idos.modules.lock.json and customized catalog modules, file size, and project documentation (IDOS.md vs docs/feature-history/). Use this BEFORE creating a module, creating or changing src/shared/, moving code between files or folders, adding a system that spans several modules, or writing project documentation.",
|
|
4
|
-
"content": "---\nname: idosgames-project-structure\ndescription: >-\n The layout standard for an iDosGames game project (host shell + composable modules) and how it\n grows without turning into a mess: where a new feature goes (a catalog module, an existing module,\n or a new module), the folder layout inside a module, module boundaries (no cross-module imports),\n the game's shared code in src/shared/, module.meta.json, src/modules.ts being platform-generated,\n idos.modules.lock.json and customized catalog modules, file size, and project documentation\n (IDOS.md vs docs/feature-history/). Use this BEFORE creating a module, creating or changing\n src/shared/, moving code between files or folders, adding a system that spans several modules, or\n writing project documentation.\n---\n\n# Project structure (iDosGames game projects)\n\n## The project at a glance\n\n```\nIDOS.md project guide every agent reads first — short, evergreen\nAGENTS.md, CLAUDE.md pointers to IDOS.md for external tools (Cursor, Codex, Claude Code…)\nidos.modules.lock.json platform-owned: catalog modules, their versions and file fingerprints\ndocs/feature-history/ one file per game system + README.md index\npackage.json · vite.config.ts · tsconfig.json · index.html\nsrc/\n main.tsx the host: ONE SDK client + mountHost([createBaseModule(…), ...modules])\n app.config.ts what happens after sign-in (start) and the lobby's tab order\n ui.config.ts the look, animations and sounds of every screen\n walletLogin.tsx, the wallet stack of the project: sign-in / \"link wallet\" buttons and the lobby's\n walletButton.tsx wallet button (the base itself is in @idosgames/app-shell, not in the project)\n modules.ts the composition list — generated by the platform\n idos.title.ts the project's identity — generated, never edit\n config.ts title / environment resolution — platform-owned, never edit\n env.ts build-time values (.env.local)\n shared/ optional: code two or more of THIS game's modules need\n ui/ types/ utils/\n modules/\n <id>/ one folder per module: the catalog's systems and games, the game's own\n```\n\nSign-in and the lobby are the base; everything else the game does lives in a module — a **system**\n(its screen is a lobby tab: shop, heroes, leaderboards…) or a **game** (its mode gets a \"Play\"\nbutton). Ready-made catalog modules and the game's own modules\nshare the one `src/modules/` folder — where a module came from is recorded in\n`idos.modules.lock.json`, not in its path, because a catalog template becomes the creator's code the\nmoment they start changing it. `src/shared/` is the one place for code several of the game's own\nmodules use; there is no project-level `utils/` or `components/` besides it.\n\n## Where does a new feature go?\n\nDecide in this order:\n\n1. **The catalog already has it** → install that module (the AI editor's InstallModule, or\n `get_module` over MCP) and adapt it. Don't rebuild what exists.\n2. **It extends an existing module's gameplay** (a new tile type in the board game, a new enemy in\n the idle RPG) → change that module, inside its folder.\n3. **It is its own mode, screen or system** (a shop, a clan screen, a mini-game, a quest board) → a\n **new module** `src/modules/<feature-id>/`.\n4. **It is the lobby, sign-in or chrome everybody shares.** Balances, the wallet, the profile, the\n tabs and \"Play\" are the base, part of `@idosgames/app-shell` (not project code): its settings are\n `src/app.config.ts` and the look is `src/ui.config.ts`; games hide their own copies of the\n balances and wallet by themselves. Other UI shown over every mode → a panel with\n `activeOnly: false`. See **idosgames-compose-modules**.\n5. **It is code two or more of the game's modules need** (a shared type, the game's UI kit, a\n formatting helper) → `src/shared/` (see below).\n\nBetween 2 and 3: if it would get its own lobby tab or \"Play\" button, or could be switched off on its\nown, it is its own module.\n\n## Inside a module\n\n```\nsrc/modules/<id>/\n index.ts export { <camelCaseId>Module } from \"./module\";\n module.ts defineModule({ id, meta, setup(ctx) { … } })\n module.meta.json manifest: type, summary, provides, tags, version, author\n components/ React panels and UI pieces\n game/ engine and simulation (engine subfolders are fine: game/phaser/, game/three/)\n data/ static tables and tuning (levels, tiles, item lists)\n react/ hooks, contexts, the scene↔panel controller bridge\n```\n\n- The folder id is kebab-case `[a-z0-9-]`, at most 64 characters. The export name is derived from it\n — `daily-quests` → `dailyQuestsModule` — and the platform registers the module by that exact name.\n- Create only the folders you need; a small module can be `index.ts` + `module.ts` + one component.\n- `voxelcraft` is a ported vanilla game with its own layout — leave it as it is. New modules follow\n the layout above.\n\n### module.meta.json\n\n```json\n{\n \"id\": \"daily-quests\",\n \"type\": \"feature\",\n \"summary\": \"One-line pitch shown in the Modules dialog.\",\n \"description\": \"What it does, in a few sentences.\",\n \"provides\": [\"daily quest board\", \"streak rewards\"],\n \"tags\": [\"quests\", \"retention\"],\n \"version\": \"0.1.0\",\n \"author\": { \"name\": \"…\" },\n \"events\": {\n \"emits\": [\n {\n \"topic\": \"daily-quests:quest-completed@1\",\n \"when\": \"a quest's reward was claimed\",\n \"payload\": { \"questId\": \"string\" }\n }\n ],\n \"listens\": [\n {\n \"topic\": \"idle-rpg:character-upgraded@1\",\n \"why\": \"progress 'level a hero' quests\"\n }\n ]\n }\n}\n```\n\n`type` is `template` (a complete game to start from) or `feature` (a capability added to a game).\n`events` declares every `defineTopic(...)` the module uses — its own topics under `emits` (with the\nsame payload descriptor it passes to `shape()`), other modules' under `listens`; omit it when the\nmodule has none.\n`provides` is what an agent matches a request against — keep it accurate. Every module carries this\nfile, the game's own included: it is how a module shows up in the Modules dialog, and what lets it be\npublished to the shared catalog for other creators later.\n\n### Register it\n\n`src/modules.ts` has exactly this shape — imports plus the array, nothing else:\n\n```ts\nimport type { Module } from \"@idosgames/module-sdk\";\nimport { boardGameModule } from \"./modules/board-game\";\nimport { dailyQuestsModule } from \"./modules/daily-quests\";\n\nexport const modules: Module[] = [boardGameModule, dailyQuestsModule];\n```\n\nThe platform regenerates this file whenever modules are installed or removed; any other code in it is\nlost. Array order is mount order (and nav order).\n\n## Module boundaries\n\nA module imports only:\n\n- files inside its own folder;\n- `src/shared/` — the game's shared code;\n- npm packages (`@idosgames/*`, `react`, `three`, `phaser`, …).\n\nNever another module's files (`../board-game/…`) and never host files (`../../main`, `../../config`).\nModules are mixed into different games — a module that reaches into its neighbours breaks the moment\none of them is removed or replaced. Cooperate instead through:\n\n- **Durable shared state** (currency, inventory, characters, progress) → the ONE SDK client every\n module receives as `ctx.client`. Gold earned in one mode is spendable in another with no plumbing.\n- **Live signals** → `ctx.events` with typed topics `<module-id>:<event>@<major>`\n (`defineTopic(\"idle-rpg:character-upgraded@1\", shape({ … }))`), declared in `module.meta.json`.\n The emitter doesn't know who listens; a listener keeps its own copy of the topic. Topics shared by\n several of THIS game's own modules can live in `src/shared/events/` and be imported on both sides.\n- **Code several modules need** → `src/shared/`.\n- **Logic that must be shared and trusted** → it belongs on the server (SDK services, CloudCode),\n not in a client file.\n\n## src/shared/ — the game's shared code\n\nFor code that two or more of THIS game's own modules need.\n\n- **Created on demand.** Code only one module needs stays inside that module; move it to\n `src/shared/` when a second module needs it, not in advance.\n- **One-way dependency.** Modules import from `src/shared/`; `src/shared/` NEVER imports a module.\n Otherwise every module using the shared code silently drags another module in with it.\n- **No game state, no game logic.** Types, constants, the game's UI components and theme, pure\n helpers. Progress and data go through the SDK client, signals through `ctx.events`.\n- **Organised by purpose:** `src/shared/ui/`, `src/shared/types/`, `src/shared/utils/` — not one pile.\n- **Catalog modules never depend on it.** A module that ships in the catalog must install into any\n game, so it is fully self-contained. A game's own module that imports `src/shared/` is tied to this\n game — fine for your own game; when you publish such a module, its shared code is copied into it\n (the Modules dialog marks these modules \"uses shared code\").\n\n## Catalog modules you change\n\nA module installed from the catalog is source in your project — change it freely. At install the\nplatform records its file fingerprints in `idos.modules.lock.json`; once any file differs, the module\ncounts as **customized**, and updating it from the catalog is refused until the user confirms\noverwriting their changes in the Modules dialog. An agent never forces that overwrite. Write down\nnon-obvious customizations in `docs/feature-history/` so the next person knows why the module differs\nfrom the catalog.\n\n## Files\n\n- Keep files focused. When a change adds a new responsibility to a file past ~400 lines, move that\n part into its own file — as part of that change, not as a separate drive-by refactor.\n- `src/idos.title.ts`, `src/config.ts` and `idos.modules.lock.json` are platform-owned: never edit\n them. The platform's build packs its own `idos.title.ts` and `config.ts` over the project's, so\n an edit there never reaches the game anyway.\n- Never read the title or the environment from the URL: a link is editable by anyone. Use\n `client.titleID` (or `ctx.titleId` in a module's `setup()`).\n\n## Documentation\n\n- **IDOS.md** — the always-loaded guide: what the game is (\"This game\"), layout, conventions,\n \"never do X\" constraints, lasting user preferences. Short and evergreen; change a line when a fact\n changes. The \"Installed modules\" block is maintained by the platform.\n- **docs/feature-history/<slug>.md** — one file per game system: ONLY what a later session must take\n into account and cannot see in the code — why it is designed this way, a non-obvious constraint, a\n decision the user made, a rule for how the system must be changed. Test: \"would a new session\n working on this system go wrong without knowing this?\" If not, write nothing. Ordinary edits (a\n restyle, a bug fix, a text or balance tweak, a routine new screen) get no entry, and it is never a\n log of what was done. Update the system's file when such a fact changes, and give every file ONE\n line in `docs/feature-history/README.md`: `- [Title](slug.md) — one-line gist`.\n- Never append a feature write-up to IDOS.md. It is loaded on every run, so every paragraph there is\n paid for by all future work — that is exactly how instruction files bloat.\n- Feature-history entries are not loaded automatically: open the relevant one before changing that\n system.\n",
|
|
4
|
+
"content": "---\nname: idosgames-project-structure\ndescription: >-\n The layout standard for an iDosGames game project (host shell + composable modules) and how it\n grows without turning into a mess: where a new feature goes (a catalog module, an existing module,\n or a new module), the folder layout inside a module, module boundaries (no cross-module imports),\n the game's shared code in src/shared/, module.meta.json, src/modules.ts being platform-generated,\n idos.modules.lock.json and customized catalog modules, file size, and project documentation\n (IDOS.md vs docs/feature-history/). Use this BEFORE creating a module, creating or changing\n src/shared/, moving code between files or folders, adding a system that spans several modules, or\n writing project documentation.\n---\n\n# Project structure (iDosGames game projects)\n\n## The project at a glance\n\n```\nIDOS.md project guide every agent reads first — short, evergreen\nAGENTS.md, CLAUDE.md pointers to IDOS.md for external tools (Cursor, Codex, Claude Code…)\nidos.modules.lock.json platform-owned: catalog modules, their versions and file fingerprints\ndocs/feature-history/ one file per game system + README.md index\npackage.json · vite.config.ts · tsconfig.json · index.html\nsrc/\n main.tsx the host: ONE SDK client + mountHost([createBaseModule(…), ...modules])\n app.config.ts what happens after sign-in (start) and the lobby's tab order\n ui.config.ts the look, animations and sounds of every screen\n walletLogin.tsx, the wallet stack of the project: sign-in / \"link wallet\" buttons and the lobby's\n walletButton.tsx wallet button (the base itself is in @idosgames/app-shell, not in the project)\n modules.ts the composition list — generated by the platform\n idos.title.ts the project's identity — generated, never edit\n config.ts title / environment resolution — platform-owned, never edit\n env.ts build-time values (.env.local)\n shared/ optional: code two or more of THIS game's modules need\n ui/ types/ utils/\n modules/\n <id>/ one folder per module: the catalog's systems and games, the game's own\n```\n\nSign-in and the lobby are the base; everything else the game does lives in a module — a **system**\n(its screen is a lobby tab: shop, heroes, leaderboards…) or a **game** (its mode gets a \"Play\"\nbutton). Ready-made catalog modules and the game's own modules\nshare the one `src/modules/` folder — where a module came from is recorded in\n`idos.modules.lock.json`, not in its path, because a catalog template becomes the creator's code the\nmoment they start changing it. `src/shared/` is the one place for code several of the game's own\nmodules use; there is no project-level `utils/` or `components/` besides it.\n\n## Where does a new feature go?\n\nDecide in this order:\n\n1. **The catalog already has it** → install that module (the AI editor's InstallModule, or\n `get_module` over MCP) and adapt it. Don't rebuild what exists.\n2. **It extends an existing module's gameplay** (a new tile type in the board game, a new enemy in\n the idle RPG) → change that module, inside its folder.\n3. **It is its own mode, screen or system** (a shop, a clan screen, a mini-game, a quest board) → a\n **new module** `src/modules/<feature-id>/`.\n4. **It is the lobby, sign-in or chrome everybody shares.** Balances, the wallet, the profile, the\n tabs and \"Play\" are the base, part of `@idosgames/app-shell` (not project code): its settings are\n `src/app.config.ts` and the look is `src/ui.config.ts`; games hide their own copies of the\n balances and wallet by themselves. Other UI shown over every mode → a panel with\n `activeOnly: false`. See **idosgames-compose-modules**.\n5. **It is code two or more of the game's modules need** (a shared type, the game's UI kit, a\n formatting helper) → `src/shared/` (see below).\n\nBetween 2 and 3: if it would get its own lobby tab or \"Play\" button, or could be switched off on its\nown, it is its own module.\n\n## Inside a module\n\n```\nsrc/modules/<id>/\n index.ts export { <camelCaseId>Module } from \"./module\";\n module.ts defineModule({ id, meta, setup(ctx) { … } })\n module.meta.json manifest: type, summary, provides, tags, version, author\n components/ React panels and UI pieces\n game/ engine and simulation (engine subfolders are fine: game/phaser/, game/three/)\n data/ static tables and tuning (levels, tiles, item lists)\n react/ hooks, contexts, the scene↔panel controller bridge\n```\n\n- The folder id is kebab-case `[a-z0-9-]`, at most 64 characters. The export name is derived from it\n — `daily-quests` → `dailyQuestsModule` — and the platform registers the module by that exact name.\n- Create only the folders you need; a small module can be `index.ts` + `module.ts` + one component.\n- `voxelcraft` is a ported vanilla game with its own layout — leave it as it is. New modules follow\n the layout above.\n\n### module.meta.json\n\n```json\n{\n \"id\": \"daily-quests\",\n \"type\": \"feature\",\n \"summary\": \"One-line pitch shown in the Modules dialog.\",\n \"description\": \"What it does, in a few sentences.\",\n \"provides\": [\"daily quest board\", \"streak rewards\"],\n \"tags\": [\"quests\", \"retention\"],\n \"version\": \"0.1.0\",\n \"author\": { \"name\": \"…\" },\n \"events\": {\n \"emits\": [\n {\n \"topic\": \"daily-quests:quest-completed@1\",\n \"when\": \"a quest's reward was claimed\",\n \"payload\": { \"questId\": \"string\" }\n }\n ],\n \"listens\": [\n {\n \"topic\": \"idle-rpg:character-upgraded@1\",\n \"why\": \"progress 'level a hero' quests\"\n }\n ]\n }\n}\n```\n\n`type` is `template` (a complete game to start from) or `feature` (a capability added to a game).\n`events` declares every `defineTopic(...)` the module uses — its own topics under `emits` (with the\nsame payload descriptor it passes to `shape()`), other modules' under `listens`; omit it when the\nmodule has none.\n`provides` is what an agent matches a request against — keep it accurate. Every module carries this\nfile, the game's own included: it is how a module shows up in the Modules dialog, and what lets it be\npublished to the shared catalog for other creators later.\n\n### Register it\n\n`src/modules.ts` has exactly this shape — imports plus the array, nothing else:\n\n```ts\nimport type { Module } from \"@idosgames/module-sdk\";\nimport { boardGameModule } from \"./modules/board-game\";\nimport { dailyQuestsModule } from \"./modules/daily-quests\";\n\nexport const modules: Module[] = [boardGameModule, dailyQuestsModule];\n```\n\nThe platform regenerates this file whenever modules are installed or removed; any other code in it is\nlost. Array order is mount order (and nav order).\n\n## Module boundaries\n\nA module imports only:\n\n- files inside its own folder;\n- `src/shared/` — the game's shared code;\n- npm packages (`@idosgames/*`, `react`, `three`, `phaser`, …).\n\nNever another module's files (`../board-game/…`) and never host files (`../../main`, `../../config`).\nModules are mixed into different games — a module that reaches into its neighbours breaks the moment\none of them is removed or replaced. Cooperate instead through:\n\n- **Durable shared state** (currency, inventory, characters, progress) → the ONE SDK client every\n module receives as `ctx.client`. Gold earned in one mode is spendable in another with no plumbing.\n- **Live signals** → `ctx.events` with typed topics `<module-id>:<event>@<major>`\n (`defineTopic(\"idle-rpg:character-upgraded@1\", shape({ … }))`), declared in `module.meta.json`.\n The emitter doesn't know who listens; a listener keeps its own copy of the topic. Topics shared by\n several of THIS game's own modules can live in `src/shared/events/` and be imported on both sides.\n- **Code several modules need** → `src/shared/`.\n- **A data format several CATALOG modules share** (characters, video timelines) → an `@idosgames/*`\n package: `@idosgames/characters` (voxel, 2D-rig, GLB characters), `@idosgames/timeline` (videos,\n cutscenes). Use it; extend it with DATA (your own recipes and part packs registered at startup),\n never by copying its source into a module.\n- **Logic that must be shared and trusted** → it belongs on the server (SDK services, CloudCode),\n not in a client file.\n\n## src/shared/ — the game's shared code\n\nFor code that two or more of THIS game's own modules need.\n\n- **Created on demand.** Code only one module needs stays inside that module; move it to\n `src/shared/` when a second module needs it, not in advance.\n- **One-way dependency.** Modules import from `src/shared/`; `src/shared/` NEVER imports a module.\n Otherwise every module using the shared code silently drags another module in with it.\n- **No game state, no game logic.** Types, constants, the game's UI components and theme, pure\n helpers. Progress and data go through the SDK client, signals through `ctx.events`.\n- **Organised by purpose:** `src/shared/ui/`, `src/shared/types/`, `src/shared/utils/` — not one pile.\n- **Catalog modules never depend on it.** A module that ships in the catalog must install into any\n game, so it is fully self-contained. A game's own module that imports `src/shared/` is tied to this\n game — fine for your own game; when you publish such a module, its shared code is copied into it\n (the Modules dialog marks these modules \"uses shared code\").\n\n## Catalog modules you change\n\nA module installed from the catalog is source in your project — change it freely. At install the\nplatform records its file fingerprints in `idos.modules.lock.json`; once any file differs, the module\ncounts as **customized**, and updating it from the catalog is refused until the user confirms\noverwriting their changes in the Modules dialog. An agent never forces that overwrite. Write down\nnon-obvious customizations in `docs/feature-history/` so the next person knows why the module differs\nfrom the catalog.\n\n## Files\n\n- Keep files focused. When a change adds a new responsibility to a file past ~400 lines, move that\n part into its own file — as part of that change, not as a separate drive-by refactor.\n- `src/idos.title.ts`, `src/config.ts` and `idos.modules.lock.json` are platform-owned: never edit\n them. The platform's build packs its own `idos.title.ts` and `config.ts` over the project's, so\n an edit there never reaches the game anyway.\n- Never read the title or the environment from the URL: a link is editable by anyone. Use\n `client.titleID` (or `ctx.titleId` in a module's `setup()`).\n\n## Documentation\n\n- **IDOS.md** — the always-loaded guide: what the game is (\"This game\"), layout, conventions,\n \"never do X\" constraints, lasting user preferences. Short and evergreen; change a line when a fact\n changes. The \"Installed modules\" block is maintained by the platform.\n- **docs/feature-history/<slug>.md** — one file per game system: ONLY what a later session must take\n into account and cannot see in the code — why it is designed this way, a non-obvious constraint, a\n decision the user made, a rule for how the system must be changed. Test: \"would a new session\n working on this system go wrong without knowing this?\" If not, write nothing. Ordinary edits (a\n restyle, a bug fix, a text or balance tweak, a routine new screen) get no entry, and it is never a\n log of what was done. Update the system's file when such a fact changes, and give every file ONE\n line in `docs/feature-history/README.md`: `- [Title](slug.md) — one-line gist`.\n- Never append a feature write-up to IDOS.md. It is loaded on every run, so every paragraph there is\n paid for by all future work — that is exactly how instruction files bloat.\n- Feature-history entries are not loaded automatically: open the relevant one before changing that\n system.\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), `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",
|
|
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- **The default look is Layer Lab \"Vertical Casual\"** (react ≥ 0.6): bright blue-violet screens,\n blue cards, a heavy dark **ink** outline around buttons, panels and letters, glossy plates with a\n darker bottom lip, white outlined letters, the Rubik font (shipped inside the kit, EN + RU — no CDN).\n- Theme tokens: `bgTop`, `bgBottom`, `panel`, `panelDeep`, `panelEdge`, `text`, `textDim`, `gold`,\n `goldDeep`, `green`, `greenDeep`, `blue`, `blueDeep`, `red`, `redDeep`, `purple`, `purpleDeep`,\n `orange`, `orangeDeep`, `teal`, `tealDeep`, `ink` (the outline colour), `outline` (its thickness,\n px), `nav` (the bottom menu bar), `shadow`, `backdrop`, `radius`, `buttonRadius`, `font`, `rarity`\n (rarity id → plate colour; Common blue, Uncommon teal, Rare green, Epic purple, Legendary orange,\n Mythic red).\n- **Derived tokens — usually leave them out.** They are computed from the colours above: set a light\n `panel` and dark `text`, and the ink outline of the letters disappears (`textOutline`,\n `textStroke`), panels get a soft frame instead of the ink one (`panelEdge`), 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 and desktop: more, gathered in the middle of the bar — a full-width bar at the bottom on every width;\nthe 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 fullBleed: false, // true = the screen paints edge to edge (its own background, a stage):\n // the lobby gives it the whole area, no padding, no width limit\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- **Width.** A plain screen is a page: the lobby pads it and gives it the full width — from a 1440px\n window on, a centred 1120px column (the `UScreen` pages then get ink sides). Build it to fit any\n width from 320px, never fixed widths; keep a small block (a week calendar, a form) to its own\n `maxWidth` and centre it. A screen with its own backdrop (a hero stage, a map) registers\n `fullBleed: true` and fills the area itself (its root `flex: \"1 0 auto\"`; it gets the full height,\n under the lobby's top row too — keep its content below `var(--idos-lobby-top)`) — never negative\n margins to escape the padding. The lobby's own background — white see-through gear icons over\n the theme — moves (`screenBackground` + `className=\"idos-screen\"`): a page leaves it see-through\n rather than painting its own. The menu is a full-width bar at the bottom on every width, \"Play\"\n in its middle on a raised gold plate.\n- **Phone, tablet and PC at once.** A list never stretches one row across a wide window: put it in\n `Columns` — `kind=\"cards\"` (cards in as many columns of `min` px as fit), `kind=\"strips\"`\n (edge-to-edge rows on a phone, framed cards side by side from 700px), `kind=\"ranked\"` (an\n ordered list running DOWN one column, then the next — a leaderboard, a level road). It reacts to\n its own width (a container query), so it fits a page, a window or a side panel alike. A heading\n or a button inside it spans the row with `gridColumn: \"1 / -1\"`. A screen of two kinds of\n content (a profile, a referral code + its rewards) is two blocks in one `Columns`.\n- **Decorations never size a row.** Rays, glows and floating icons sit `position: absolute` over\n the line they belong to; only the line's content sets its height.\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 centred window on every screen — the blue body, a lighter title bar, the slate ✕\n sticking out of the top-right corner (`variant=\"sheet\"` makes it a bottom sheet); it animates in\n 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- **The pieces of the casual sheet** — use them instead of drawing your own:\n - buttons: `Button tone=` `green | gold | blue | red | grey | purple | orange | teal | white |\nsilver | slate` (`gold` = the big \"Play\"/\"STAGE\", `grey` = \"Cancel\", `white` = blue letters);\n `IconButton` (a square slate plate — close, menu), `InfoButton` (the round \"i\"), `BackButton`\n (the slanted \"◀\" tab at the screen's left edge), `MenuTile` (an icon over its name on a dark-blue\n plate — \"Ranking\", \"Mission\");\n - frames: `ScreenHeader` (back + title + whatever goes right), `SectionTitle tone=` (a ribbon\n banner with a swallowtail — shop sections in turn red / green / purple / gold), `Ribbon` (a\n centred ribbon with folded tails and stars), `listRow` (a strip a shade deeper than the panel —\n letters, friends), `IconTile` (a square light tile holding an icon or a face), `rarityPlate(colour)`\n / `tonePlate(tone)` (a glossy plate in the ink frame);\n - values: `ProgressBar tone=` (a dark track, a glossy fill, \"275/100\" on it), `LevelShield`,\n `LevelBar` (the shield over the bar's left end), `HexBadge`, `StepperCounter` (\"29 / 100 +\"),\n `ResourceCounter` (the icon overlapping a dark plate), `RarityTag`, `SoldOutStamp`,\n `Toggle` (ON / OFF), `SpeechBubble`, `LevelRoadRow` (one step of a level road: `done | current |\nnext | locked`, its reward in a hexagon);\n - text: `outlined` (the theme's text with its ink outline), `inked` (white letters with the ink\n outline on ANY plate). `Tabs` are the white-active segmented strip (\"Daily | Challenge\").\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`, the outline `v.ink` / `v.outline`, dimming `v.backdrop`. A coloured banner, \"your\n row\", a highlighted day → `...toneSurface(tone)` (any button tone): 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": "video-studio",
|
|
3
|
+
"description": "The Video studio template (module video-studio) and the two packages it stands on — @idosgames/timeline (a video is data: VideoSpec, beats, formats, captions, sound, a three.js stage, MP4 export on the device) and @idosgames/characters (voxel, 2D rig, GLB and cut-out characters with emotions and lip-sync). Players make short viral videos (dialogue, story, top-5, quiz, what-if) from ready characters and scenes or AI assets, edit them in slots and on tracks, voice them, export MP4, publish and remix in the Workshop. Use this whenever the user works on the video studio, a video/cutscene/animation made of characters, a VideoSpec, beats or formats, captions, the AI director (\"video-director\"), voice-over features, MP4 export, remixes of videos, or adds a character, a scene, a beat or a format — even without naming the module.",
|
|
4
|
+
"content": "---\nname: video-studio\ndescription: >-\n The Video studio template (module video-studio) and the two packages it stands\n on — @idosgames/timeline (a video is data: VideoSpec, beats, formats, captions,\n sound, a three.js stage, MP4 export on the device) and @idosgames/characters\n (voxel, 2D rig, GLB and cut-out characters with emotions and lip-sync). Players\n make short viral videos (dialogue, story, top-5, quiz, what-if) from ready\n characters and scenes or AI assets, edit them in slots and on tracks, voice\n them, export MP4, publish and remix in the Workshop. Use this whenever the user\n works on the video studio, a video/cutscene/animation made of characters, a\n VideoSpec, beats or formats, captions, the AI director (\"video-director\"),\n voice-over features, MP4 export, remixes of videos, or adds a character, a\n scene, a beat or a format — even without naming the module.\n---\n\n# Video studio\n\nA video is **data** — a `VideoSpec` of a few kilobytes. The timeline engine plays it on the device,\nthe same frame at the same moment everywhere (preview, MP4). There is NO server render (owner's\ndecision 24.10.2026): the MP4 is made on the device. Money is spent only on AI steps; everything from\nthe library, recombined or the player's own media is free.\n\n```\nidea ─► AI director (title feature \"video-director\", JSON by schema) ─► VideoSpec + plan by the ladder\n ─► estimate in the studio currency (PriceOptions of the title's features) ─► free preview (draft voice)\n ─► edits (slots, tracks → overrides) ─► final voice ─► MP4 / live link / publish ─► Remix\n```\n\n## Where things live\n\n| What | Where |\n| --- | --- |\n| VideoSpec, parse/validate, `remixOf`, `voiceKey`, `linesNeedingVoice` | `@idosgames/timeline` (`spec/`) |\n| Beats (69, data), formats (`dialogue@1`, `story@1`, `top5@1`, `quiz@1`, `whatif@1`), camera moves, captions, SFX, sound plan | `@idosgames/timeline` |\n| `compileVideo(spec, catalog)` → timeline; `frameAt(tl, set, t)` | `@idosgames/timeline` |\n| `Stage` (deterministic frame), `Player` (live, audio clock) | `@idosgames/timeline/three` |\n| `exportMp4(stage)` (WebCodecs + mediabunny), `canExportVideo` | `@idosgames/timeline/export` |\n| Characters: `voxel` (CharacterSpec), `rig2d` (the idle-rpg hero with the whole wardrobe), `gltf` (humanoid-v1), contract (emotions, visemes, poses) | `@idosgames/characters` |\n| Library (actors, scenes, voices), templates, director, voice, AI assets, pricing, store, catalog, screens | `modules/video-studio/src/` |\n| Demo title settings (Frames, AI features with prices, Workshop types, store, quests, VIP, weekly board) | `modules/video-studio/title-seed.json` — GENERATED by the engine's `StarterSeedExportTests`, never edited |\n| AI prompts | `src/ai/prompts.ts` → `ai-prompts.json` (test `prompts.test.ts`; the engine fixture reads it) |\n\nPackages, not module code: the \"package or module\" rule — two catalog modules need the same data\nformat with its runtime (voxelcraft + idle-rpg + video-studio share characters; games may use the\ntimeline for cutscenes). Extend them with DATA (a beat, an asset, a recipe), not by copying.\n\n## Names the studio and the title agree on (`src/ids.ts`)\n\n- AI features (all tagged `hidden` — the AI studio tab does not show them): `video-director` (Text,\n JSON), `video-voice-draft` (free, `audio-cheap`, English voices), `video-voice` (`audio-balanced`,\n Russian too), `video-image`, `video-background`, `voxel-character`, `rig2d-character`, `video-music`,\n `video-clip`. Both voice features have `ShareIdenticalResults` — the same line in the same voice from\n any player of the title is free from the title's cache.\n- Currency `FRAMES`; VIP `studio_vip` (benefits `no_watermark`, `hd_export`, `premium_voices`);\n premium packs = items `pack_fantasy` / `pack_space` (catalog `studio_packs`).\n- Workshop types `video.spec` (a video), `video.asset` (one asset), `video.pack` (a set). Files: `main`\n (JSON) + `media` (what was inline: the player's photo, own voice). Remixes go through\n `ParentContentID` (`Stats.Remixes`, browse `parentContentID`, sort `MostRemixed`).\n- Quest metrics `video_published`, `video_remixed` (client), `hero_created` (server, on completion of\n the character features); weekly leaderboard `weekly_creators` (+1 per published video).\n\n## The ladder of requests (plan items)\n\n`library` (0) → `combine` (recolour, outfit, prop — 0) → `own` (photo, voice — 0) → `ai-asset`\n(voxel-character, rig2d-character, image-actor, background, prop, music) → `ai-clip` (needs its own\nconfirmation). The director always puts the CLOSEST catalog ref into the spec; the studio repairs any\nunknown or locked ref itself (`settle` → `closestAsset`) — never a second paid call for a typo.\n\"Quick\" mode drops the AI items of the plan; \"Unique\" shows them in the estimate.\n\n## Recipes\n\n**A new library character.** Voxel: `voxelHuman(...)` / `voxelCreature(...)` in\n`src/library/actors.ts` (height in scene units next to a 1.7 human). 2D: a `HERO_PRESETS` look of\n`@idosgames/characters/rig2d`. Give `tags` in both languages and `affordances` — the director finds\nassets by them. Premium → `tier: \"premium\"` (opened by its pack item, see `model/access.ts`).\n\n**A new scene.** `src/library/sets.ts`: background gradient/picture, ground, props as boxes; anchors\nand shots are shared (`ANCHORS`, `SHOTS`). Shots are framed for a 1.7 hero; the stage refits a\nsingle-actor shot to the actor's height and follows it off its anchor.\n\n**A new beat.** Data in `@idosgames/timeline/src/beats/library.ts`\n(`beat(id, title, duration, roles, tags, tracks)`), ids are `beat:<name>` (specs may omit the prefix).\nThe director prompt lists beats from this registry — regenerate `ai-prompts.json`.\n\n**A new format.** `@idosgames/timeline/src/formats/<name>.ts` with an `@1` id, a params JSON schema and\na `directorHint`; register it in `formats/index.ts`. Never change a published `@1` — add `@2`.\n\n**Ready videos without AI.** `src/model/templates.ts` — every format has one, in en/ru; they compile in\ntests (`studio.test.ts`). They are the free one-tap start and the agent's demo.\n\n## Agent actions (`ctx.exposeToAgent`)\n\n`openTrends()`, `openCreate({ format? })`, `newFromTemplate({ id })` (free, no AI), `watch({ contentID })`.\nState: the current view and the template ids. The DOM of the studio is readable directly.\n\n## Traps\n\n- MP4 needs the final voice when the title has the voice feature; without the feature a video exports\n with captions only. Russian drafts are read by the browser (Web Speech) and never reach the file.\n- `captureFrame` / export create their own WebGL context — release it (`forceContextLoss`), a tab has ~16.\n- A video saved from someone else's publication keeps its media as `data:` URLs (not `blob:`) — it must\n survive a reload in \"My videos\".\n- Live link: `#video=<contentID>` opens the video for watching; the title is never taken from a link.\n- A video PACKAGE travels in a link too: `#videodata=v1.<base64url(deflate-raw(JSON {v, spec, assets}))>`\n (`encodeVideoPackage` / `decodeVideoPackage` / `readVideoPackage` in `@idosgames/timeline`). The studio\n opens it as a new video in the editor (its assets go to \"My assets\", origin `link`) and clears the hash.\n ⚠ The site backend encodes the same format (`McpVideoTools.EncodePackage`) — both sides pin each\n other's string in their tests; change one, change both.\n\n## Agents (MCP)\n\n- Platform MCP (`site.idosgames.com/api/v2/mcp`, publisher's credits, right `ai:generate`):\n `plan_video` (the director: the prompt, template and schema come from `video.json` of the registry —\n `src/catalog.test.ts` exports them; the server repairs refs like `settle`) and `generate_asset` (the\n table `src/ai/generators.ts`: prompt, model settings and the asset shape with `$url` / `$out` /\n `$out.<field>` — the studio's `generateAsset` reads the same table). Both return the package and the\n `#videodata=` link of the title. `publish_asset` (per title, `config:write`) puts assets into the app's\n Workshop as official content (`video.asset` / `video.pack`, the `packAssets` file, the cover from the\n publisher's own generation): the site forwards the AGENT'S token to the engine, which lets it in only on\n the official Workshop actions (`AdminActionPolicy.AgentScope`). Free reads: `list_video_formats`,\n `get_video_format`, `search_assets`, `list_video_templates`.\n- SDK MCP (`npx @idosgames/mcp`, free, offline): `validate_asset`, `validate_video` — the timeline code\n bundled into the server.\n- A new AI asset kind = a row in `ASSET_GENERATORS` + its branch in `generateAsset`; the server picks it\n up from the registry without a deploy (if its answer fits the placeholders).\n",
|
|
5
|
+
"references": []
|
|
6
|
+
}
|