@idosgames/mcp 0.1.44 → 0.1.46

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.
Files changed (43) hide show
  1. package/dist/cli.js +1597 -1542
  2. package/package.json +1 -1
  3. package/registry/host.json +3 -3
  4. package/registry/index.json +172 -125
  5. package/registry/modules/ai.json +5 -5
  6. package/registry/modules/board-game.json +6 -6
  7. package/registry/modules/boosts.json +5 -5
  8. package/registry/modules/character.json +9 -9
  9. package/registry/modules/chat.json +3 -3
  10. package/registry/modules/collections.json +5 -5
  11. package/registry/modules/coop-events.json +6 -6
  12. package/registry/modules/craft.json +5 -5
  13. package/registry/modules/deals.json +6 -6
  14. package/registry/modules/events.json +3 -3
  15. package/registry/modules/friends.json +3 -3
  16. package/registry/modules/hero-run.json +15 -234
  17. package/registry/modules/idle-rpg.json +8 -8
  18. package/registry/modules/inventory.json +3 -3
  19. package/registry/modules/invite.json +3 -3
  20. package/registry/modules/kit-game.json +171 -0
  21. package/registry/modules/leaderboard.json +3 -3
  22. package/registry/modules/lootboxes.json +5 -5
  23. package/registry/modules/mailbox.json +3 -3
  24. package/registry/modules/marketplace.json +3 -3
  25. package/registry/modules/mini-game.json +3 -3
  26. package/registry/modules/multiplayer.json +3 -3
  27. package/registry/modules/premium.json +5 -5
  28. package/registry/modules/profile.json +3 -3
  29. package/registry/modules/pvp.json +3 -3
  30. package/registry/modules/quests.json +3 -3
  31. package/registry/modules/rewards.json +3 -3
  32. package/registry/modules/season.json +3 -3
  33. package/registry/modules/store.json +6 -6
  34. package/registry/modules/tutorials.json +3 -3
  35. package/registry/modules/video-studio.json +9 -9
  36. package/registry/modules/voxelcraft.json +6 -6
  37. package/registry/modules/workshop.json +6 -6
  38. package/registry/skills/currency-system.json +1 -1
  39. package/registry/skills/idosgames-compose-modules.json +1 -1
  40. package/registry/skills/idosgames-getting-started.json +1 -1
  41. package/registry/skills/idosgames-ui-kit.json +1 -1
  42. package/registry/skills/store-system.json +1 -1
  43. package/registry/video.json +1 -1
@@ -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- **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 keep a\n light inner edge (`panelShadow`, never a drop under them), 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\n## Fast on a cheap phone\n\n- **No SVG or CSS `filter` on anything that sits in a part of the screen that repaints** (a counter, a\n timer, a progress bar next to it). Chrome runs the filter again on the CPU at the screen's density\n on every repaint: the kit's icons once had an `feMorphology` outline and 24 of them cost a phone half\n its frames. Outlines are geometry (the kit's `Icon` draws its ink as a wider stroke under the shapes).\n- **Animate `transform` / `opacity` only**, never `background-position`, `width`, `box-shadow`, `filter` or\n colours in a loop: those repaint every frame (the drifting pattern behind kit screens is a `::before` moved\n by `transform`; `.idos-glow` is a shadow drawn once whose opacity pulses — use it, don't animate a glow yourself).\n- A Phaser game draws at the screen's density (`min(2, devicePixelRatio)`) with the camera zoomed by\n it, keeps `render.autoMobileTextures: false`, and sizes its canvas to what is visible (see the\n idle-rpg module's `phaser/density.ts` and controller). See `docs/feature-history/mobile-game-frame-perf.md`.\n\n## Size on a phone, and no shadows\n\n- **The kit's look is the ink outline only: no shadows.** No outer `box-shadow` / `text-shadow` drop\n (`0 3px 0 …`) and no `drop-shadow(0 Npx …)` filter under buttons, cards, icons or letters. Allowed:\n `inset` lips and highlights, a ring `0 0 0 Npx` (an outline), a glow `0 0 Npx`. Letters are outlined by\n `textStroke` alone; `theme.shadow` is transparent.\n- **A screen surface carries `className=\"idos-fit\"`** and is drawn with CSS `zoom` = `--idos-ui-zoom` (1 on a\n tablet or PC, 0.75…1 on a phone by its size against 430×860, `fit.ts`). The kit's popups, windows and toasts,\n the lobby, the frame around a game and the games' DOM panels already have it. A new full-screen DOM panel\n of a game puts it on its own root (or on the part over the canvas). ⚠ Never on a canvas host: a canvas\n inside a surface sits in `className=\"idos-fit-off\"`.\n- Inside a surface, `getBoundingClientRect()` is in screen pixels, `clientWidth` / `scrollTop` / `style.left`\n in the surface's own: convert with `fitScale(el)`. A canvas counts its pixels as\n `clientWidth × el.currentCSSZoom × dpr`. See `docs/feature-history/ui-kit-fit-no-shadows.md`.\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 keep a\n light inner edge (`panelShadow`, never a drop under them), 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## Prices in Aspis (the platform currency)\n\nA price option can be in Aspis (`PlatformCredits`, API name Credits) in every sale of the game's author: lootboxes,\npacks, craft, deals, boosts, VIP, upgrades, skins, coop spins, InApp AI, the Workshop publish fee. The player confirms\neach spend on idosgames.com; the game cannot charge it by itself.\n\n- Wrap the module's own call — it takes the confirmation ID as its last argument / `creditsConfirmationID` option:\n\n ```ts\n import { payWithCredits, errorText } from \"@idosgames/react/ui\";\n const res = await payWithCredits(client, (creditsConfirmationID) =>\n client.lootbox.open(boxID, 1, optionID, undefined, creditsConfirmationID),\n );\n if (!res.ok) toast(errorText(res.error), \"error\");\n ```\n\n A price without Aspis passes straight through (one request). With Aspis the site shows its window over the game on\n idosgames.com; anywhere else the kit shows its popup with the link (`CreditsConfirmLayer`, already in\n `renderKitOverlays`). Closing the popup cancels (`CREDITS_CONFIRMATION_CANCELLED`); `NOT_ENOUGH_CREDITS` also\n opens the top-up page. Not a hook: works from a game session or a Phaser scene too.\n- ⚠ **One confirmation, never two**: no game popup \"Buy for 50 Aspis?\" before it.\n- A method that waits for the confirmation itself (`client.workshop.publish({ credits })`): `const k =\n kitCreditsPrompt(client)`, pass `credits: k.options`, then `await k.done(res)`.\n- Showing the price: `costLines` gives a line of kind `credits` (icon and name \"Aspis\" come from `useCatalog` /\n `resourceName`), `canAfford` leaves it to the server, `creditsPriceOf(cost)` reads the amount, `CreditsPrice` draws\n \"[icon] 50 Aspis\". Batches (\"upgrade all\", \"×5\") do not take Aspis: leave such options out of them.\n- `errorText` names every `CREDITS_*` refusal; never a reward in Aspis.\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\n## Fast on a cheap phone\n\n- **No SVG or CSS `filter` on anything that sits in a part of the screen that repaints** (a counter, a\n timer, a progress bar next to it). Chrome runs the filter again on the CPU at the screen's density\n on every repaint: the kit's icons once had an `feMorphology` outline and 24 of them cost a phone half\n its frames. Outlines are geometry (the kit's `Icon` draws its ink as a wider stroke under the shapes).\n- **Animate `transform` / `opacity` only**, never `background-position`, `width`, `box-shadow`, `filter` or\n colours in a loop: those repaint every frame (the drifting pattern behind kit screens is a `::before` moved\n by `transform`; `.idos-glow` is a shadow drawn once whose opacity pulses — use it, don't animate a glow yourself).\n- A Phaser game draws at the screen's density (`min(2, devicePixelRatio)`) with the camera zoomed by\n it, keeps `render.autoMobileTextures: false`, and sizes its canvas to what is visible (see the\n idle-rpg module's `phaser/density.ts` and controller). See `docs/feature-history/mobile-game-frame-perf.md`.\n\n## Size on a phone, and no shadows\n\n- **The kit's look is the ink outline only: no shadows.** No outer `box-shadow` / `text-shadow` drop\n (`0 3px 0 …`) and no `drop-shadow(0 Npx …)` filter under buttons, cards, icons or letters. Allowed:\n `inset` lips and highlights, a ring `0 0 0 Npx` (an outline), a glow `0 0 Npx`. Letters are outlined by\n `textStroke` alone; `theme.shadow` is transparent.\n- **A screen surface carries `className=\"idos-fit\"`** and is drawn with CSS `zoom` = `--idos-ui-zoom` (1 on a\n tablet or PC, 0.75…1 on a phone by its size against 430×860, `fit.ts`). The kit's popups, windows and toasts,\n the lobby, the frame around a game and the games' DOM panels already have it. A new full-screen DOM panel\n of a game puts it on its own root (or on the part over the canvas). ⚠ Never on a canvas host: a canvas\n inside a surface sits in `className=\"idos-fit-off\"`.\n- Inside a surface, `getBoundingClientRect()` is in screen pixels, `clientWidth` / `scrollTop` / `style.left`\n in the surface's own: convert with `fitScale(el)`. A canvas counts its pixels as\n `clientWidth × el.currentCSSZoom × dpr`. See `docs/feature-history/ui-kit-fit-no-shadows.md`.\n",
5
5
  "references": []
6
6
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "store-system",
3
3
  "description": "Build a store / shop screen in a game on the iDosGames TypeScript SDK (@idosgames/core) via client.store (StoreService): load the RESOLVED storefront for the current player (rotating daily shops, sections, slots, badges, refresh timers, remaining limits), load the offer catalogue, and purchase one or many offers. Prices can be virtual currency, crypto, a real money store product, a rewarded-video ad credit, or free. Use this whenever the user is working in the iDosGames TS SDK or its game templates (board-game, idle-rpg) and wants a shop screen, a daily rotating shop, gem packs, free/ad-paid slots, first-purchase badges, purchase-limit UI, or otherwise touches client.store, StoreService, GetStorefrontResponse, StoreDefinitions or offer purchasing — even if they don't name the module.",
4
- "content": "---\nname: store-system\ndescription: >-\n Build a store / shop screen in a game on the iDosGames TypeScript SDK\n (@idosgames/core) via client.store (StoreService): load the RESOLVED\n storefront for the current player (rotating daily shops, sections, slots,\n badges, refresh timers, remaining limits), load the offer catalogue, and\n purchase one or many offers. Prices can be virtual currency, crypto, a real\n money store product, a rewarded-video ad credit, or free. Use this whenever\n the user is working in the iDosGames TS SDK or its game templates\n (board-game, idle-rpg) and wants a shop screen, a daily rotating shop, gem\n packs, free/ad-paid slots, first-purchase badges, purchase-limit UI, or\n otherwise touches client.store, StoreService, GetStorefrontResponse,\n StoreDefinitions or offer purchasing — even if they don't name the module.\n---\n\n# Store system (iDosGames TS SDK)\n\nA title sells **offers** (products) placed on **shelves**. The shop is a tree:\n\n```\nStore (storefront)\n└── Layout.Sections\n └── Slots ← a slot is a PLACE on the shelf\n └── the slot decides which offer stands in it\n```\n\n⚠⚠ **A slot is the only address a product can be bought through.** An offer\nlives in a flat catalogue (`StoreOffers`) and becomes buyable only by standing\nin a slot; a product placed in no slot **cannot be bought at all**. There is no\n\"list of stores\" on the product — that field was removed, because it was never\na right to buy: the purchase never checked it.\n\nEverything is **server-authoritative**. The client asks to purchase, the\nbackend validates price, windows, gates, limits **and the rotation roll**, then\nthe SDK mirrors the confirmed result into a local cache your UI reads.\n\n## ⚠ Draw `getStorefront()`, not `getDefinitions()`\n\nThis is the single most important rule of the module.\n\n`getDefinitions()` returns the **config** — identical for every player.\n`getStorefront()` returns the shop **resolved for this player**: rotation\nalready rolled, gates applied, badges and refresh timers computed, remaining\nlimits filled in. None of that can be derived on the client:\n\n- **Rotation** is recomputed by the server on every read and stored nowhere. A\n client rolling it itself would show two devices two different shops.\n- **Counters** (`PurchasedTotal`, remaining caps, cooldown) belong to the\n player and are not in the config.\n- **Gates** filter storefronts, sections, slots and pool entries per player.\n\nUse `getDefinitions()` only for catalogue-wide tooling (an admin view, a\nsearch). A shop screen never needs it.\n\n## Prices: six kinds, one shape\n\nPrice is always a `PriceOptions` dictionary (`Pricing.Options`), the\nplatform-wide standard. An option's cost can be:\n\n| Kind | What it is | On the storefront view |\n| --------------------------------------- | ------------------------------------------- | ------------------------------- |\n| Virtual currency / items / event tokens | ordinary `ResourceConsume` | — |\n| Crypto | a crypto currency entry | — |\n| Real money | a store product entry; needs a receipt | `IsStorePaid: true` |\n| Rewarded video | an ad-credit entry, spent like any resource | `IsAdPaid: true` |\n| **Credits** (platform currency) | a `PlatformCredits` entry, `Amount` = price | `IsCreditsPaid`, `CreditsPrice` |\n| **Free** | **no cost at all** | `IsFree: true` |\n\n⚠ **An unset cost means FREE, and that is a legal product** — not an unfilled\nprice. A free offer must be limited on some axis (`TotalCap`, `DailyCap`,\n`CooldownSeconds`, or the slot's `PerInstanceCap`); without a limit the server\nrefuses the purchase, because it would be a button that grants itself forever.\n\n⚠ Ad-paid offers spend a **credit the player already earned** by watching a\nvideo (the Advertising module grants it server-side). Read the balance from\n`storefront.AdCreditBalance` to decide whether a card shows \"Watch\" or \"Buy\".\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 auth.* method\n\nconst store = client.store; // the StoreService\n```\n\nEvery store method requires an authenticated session. Without one they return\n`{ ok: false, reason: \"unauthorized\" }` — they do not throw.\n\n## Methods\n\nAll methods return `Promise<OperationResult<T>>`: either `{ ok: true, data }`\nor `{ ok: false, reason, error }`. Always branch on `result.ok`.\n\n| Method | Purpose | `data` on success |\n| ------------------------------------- | ---------------------------------------- | ----------------------- |\n| `getStorefront()` | **The shop as this player sees it now.** | `GetStorefrontResponse` |\n| `getDefinitions()` | The raw catalogue (config). | `StoreDefinitions` |\n| `getUserState()` | Purchase counters (state). | `UserStoreState` |\n| `purchase(offerID, count?, options?)` | Buy `count` (default 1) of one offer. | `StorePurchaseResponse` |\n| `purchaseBatch(purchases)` | Buy several offers in one atomic call. | `PurchaseBatchResponse` |\n\n`options` = `{ selectedOptionID?, payment?, slot? }`.\n\n`purchase` clamps `count` server-side to **1–100**. `purchaseBatch` takes\n`StorePurchaseRef[]`, deduped by `OfferID`, clamped to **50 refs per call**.\n\n## Drawing a shop screen\n\n```ts\nconst res = await client.store.getStorefront();\nif (!res.ok) return showError(res.error);\nconst front = res.data;\n\nfor (const store of front.Stores ?? []) {\n for (const section of store.Sections ?? []) {\n for (const slot of section.Slots ?? []) {\n // ⚠ An EMPTY slot arrives with Offer === null and must still be drawn as\n // an empty frame. Skipping it makes the shelf jump around whenever a\n // window closes or a gate filters the pool — which is exactly why the\n // server sends the slot rather than omitting it.\n if (!slot.Offer) {\n drawEmptyCard(slot.RefreshesAtUtc); // \"opens in ...\"\n continue;\n }\n\n const offer = slot.Offer;\n const state = offer.State;\n\n drawCard({\n title: offer.Identity?.DisplayName, // localization key OR a literal\n ribbons: offer.Identity?.Tags, // static badges: best_value, x2 ...\n // Server-computed badges — do not recompute these:\n firstPurchase: state?.IsFirstPurchaseAvailable,\n soldOut: state?.SoldOut,\n readyAt: state?.AvailableAtUtc, // free-slot cooldown\n // Rotation timer: \"Refreshes in 15h 40m\"\n refreshesAt: slot.RefreshesAtUtc,\n remainingThisWindow: slot.RemainingThisRotation, // null = unlimited\n prices: offer.PriceOptions, // already filtered to this platform\n });\n }\n }\n}\n```\n\n⚠ **Count timers down from `front.ServerTimeUtc`, not from the device clock.**\nThat is what it is for; a player with a skewed clock otherwise sees a shop that\nrefreshes at the wrong moment or claims to have already refreshed.\n\n⚠ **`null` and `0` mean different things in every remaining-count field.**\n`null` = \"no limit on this axis\"; `0` = \"spent, sold out\". Treating `null` as\nzero grays out every unlimited product on the shelf.\n\n`client.store.nextRefreshAtUtc()` returns the nearest refresh across all\nvisible slots — use it to schedule one reload instead of a polling loop.\n\n## Purchasing\n\n```ts\nconst res = await client.store.purchase(offer.OfferID, 1, {\n selectedOptionID: option.OptionID,\n // Pass the slot the player tapped — the storefront gives you all three ids.\n slot: {\n storeID: store.StoreID,\n sectionID: section.SectionID,\n slotID: slot.SlotID,\n },\n});\nif (!res.ok) return showError(res.error);\n```\n\nAn option paid in a store needs the receipt as well:\n\n```ts\nawait client.store.purchase(\"gems_l\", 1, {\n selectedOptionID: option.OptionID,\n payment: { Store: \"GooglePlay\", Receipt: receiptJson, Signature: signature },\n});\n```\n\n⚠ **The slot address is optional as a whole** (all three or none): without it\nthe server finds the slot itself. The roll is re-checked either way, so a\nproduct that rolled for the player nowhere cannot be bought with or without an\naddress. Pass it anyway when you have it — for a pooled product it removes the\nambiguity of which slot's per-window cap applies.\n\n### Credits — the platform currency (`IsCreditsPaid`)\n\n⚠ **Players see this currency as \"Aspis\"** (owner's decision 26.10.2026) — write \"Aspis\" in any player-facing text\n(price \"100 Aspis\", \"Not enough Aspis\"). \"Credits\" is only the API name (`PlatformCredits`, `purchaseWithCredits`,\n`CreditsError`, `IsCreditsPaid`); do not name a game's own currency \"Coins\" next to it either — players confuse them.\n\nCredits live on the player's **iDos Games account**, not in the game; the game's author gets 70% of the price, the\nplatform 30%. ⚠ The game **cannot charge Credits by itself**: the first call only creates a purchase request, the\nplayer confirms it on idosgames.com with their own account, and only then the purchase completes. On idosgames.com's\nown game page the SITE shows its confirmation window right over the game — `purchaseWithCredits` handles that by itself\nand `onConfirmation` is not called; on the game's own address or `/embed` it falls back to the link. ⚠ **One\nconfirmation, never two** (owner, 04.11.2026): a tap on an Aspis price calls `purchaseWithCredits` AT ONCE — no game\npopup \"Confirm the purchase\" before it; the site's window is the confirmation. Show your own popup only from\n`onConfirmation` (the site could not ask), with the link. Reference: `modules/store` `StoreScreen.buyWithCredits`.\nUse the one call that does the whole flow:\n\n```ts\nimport { CreditsError } from \"@idosgames/core\";\n\nconst res = await client.store.purchaseWithCredits(offer.OfferID, 1, {\n selectedOptionID: option.OptionID,\n slot: { storeID, sectionID, slotID },\n // Show a LINK/button to view.Url (target=\"_blank\") — not window.open after an await: browsers block that.\n onConfirmation: (view) => showConfirmLink(view.Url),\n signal: abort.signal, // stop waiting when the popup closes\n});\nif (!res.ok && res.error === CreditsError.NotEnough) {\n const b = await client.store.getCreditsBalance(); // b.data.TopUpUrl — where to top up\n}\n```\n\n- Price the option in Credits in LiveOps → Store (resource picker → **Credits**). Only the Store accepts Credits; one Credits\n entry per option, never together with a store product, never as a reward.\n- Refusals (`CreditsError`): `CREDITS_LOGIN_REQUIRED` (guest — sign in with iDos Games), `NOT_ENOUGH_CREDITS`,\n `CREDITS_CONFIRMATION_DECLINED` / `_EXPIRED`, `CREDITS_BATCH_FORBIDDEN` (no Credits in `purchaseBatch`).\n- `getCreditsBalance()` — Credits the player can spend in games (bought + earned; bonus Credits are for platform services).\n- On the DEV copy of the title a Credits purchase is a free test: no request, no Credits moved.\n\n### ⚠ `OFFER_NOT_IN_ROTATION` means \"reload the shop\", not \"show an error\"\n\nThe rotation roll is recomputed, never stored, so editing a slot's pool\nreshuffles the shelf for everyone who has it open. A player who taps a card\nthat is no longer in their roll gets exactly this error:\n\n```ts\nimport { OFFER_NOT_IN_ROTATION } from \"@idosgames/core\";\n\nif (!res.ok && res.error === OFFER_NOT_IN_ROTATION) {\n await client.store.getStorefront(); // silently redraw; do not toast\n return;\n}\n```\n\nAlready-completed purchases are never undone by a reshuffle — counters are\naddressed by product and slot, not by the roll.\n\n## Events\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn:\n\n- `store:storefrontLoaded` → `GetStorefrontResponse`\n- `store:definitionsLoaded` → `StoreDefinitions`\n- `store:userStateLoaded` → `UserStoreState`\n- `store:offerPurchased` → `StorePurchaseResponse`\n- `store:offersPurchasedBatch` → `PurchaseBatchResponse`\n\nThe coarse `user:storeUpdated` (and `user:anyUpdated`) also fire on any store\ncache write.\n\n## Batch purchase\n\n```ts\nconst res = await client.store.purchaseBatch([\n { OfferID: \"pack1\", Count: 1 },\n { OfferID: \"starter_bundle\", Count: 1 },\n]);\nif (!res.ok) return showError(res.error);\nfor (const item of res.data) {\n if (item.Success) applyOk(item.Id);\n else showItemError(item.Id, item.Error);\n}\n```\n\nBatch results are **partial-aware**: the outer `res.ok` says the call ran, each\nelement's `Success`/`Error` says whether that item applied. Items rejected on\ntheir own merits (unknown id, outside window, gate failed, limit reached, not\nin rotation) are filtered out _before_ the merged charge is built. The\nsurvivors are then charged as **one merged, all-or-nothing transaction**.\n\n⚠ An option paid in a **store** cannot go in a batch — one receipt pays for one\npurchase. Ad credits can: a credit is an ordinary resource and two of them add\nup like two coins.\n\n## Gotchas\n\n- **Guard against double-submit.** Each `purchase()` mints a fresh idempotency\n key, so two calls are two real operations — a double-clicked \"Buy\" charges\n twice. Disable the control while a call is in flight.\n- **The struck-through price is display only.** `CompareAtCost` never reaches\n the charge. `DiscountPercent` is computed by the server and is `null` when it\n cannot be computed (a mixed bundle has no single percentage) — `null` is not\n zero, and printing \"0%\" there is a lie.\n- **Prices and rewards can be adjusted server-side.** Premium tiers apply\n discounts and bonuses automatically; read what was actually charged and\n granted off `res.data.Resources`, not off the displayed base values.\n- **`count` scales cost and rewards linearly, then premium is applied once** —\n it is not N independent purchases.\n- **Only one `Resources` apply per batch call**, but every item's own\n `OfferID`/`Count`/`ServerTimeUtc` is correct and drives its counter patch.\n- **`DailyPurchases` resets on UTC midnight.** Read `DailyResetUtc` from the\n cache after a call rather than computing it.\n- **Tags are for the client; player-dependent badges are not.** Anything whose\n truth depends on the player — first purchase, sold out, a timer — arrives in\n `State`, computed by the server. Do not encode those as tags.\n- **`DisplayName` is a localization key OR a literal.** Resolution is\n `t(x) = own table → fallback table → x itself`, so a title with literal\n names works with no migration. Run these through the localization module.\n- **Render from the cache, handle the error from the result.** Use `reason` to\n decide behaviour (retry on `\"connection\"`, re-auth on `\"unauthorized\"`, toast\n the `error` on `\"server\"`).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — every config and\nstorefront field, the rotation modes, the purchase-limit/reset rule matrix, and\nbatch semantics. Read it when building config-driven UI or when an error points\nat a rule you need to understand.\n",
4
+ "content": "---\nname: store-system\ndescription: >-\n Build a store / shop screen in a game on the iDosGames TypeScript SDK\n (@idosgames/core) via client.store (StoreService): load the RESOLVED\n storefront for the current player (rotating daily shops, sections, slots,\n badges, refresh timers, remaining limits), load the offer catalogue, and\n purchase one or many offers. Prices can be virtual currency, crypto, a real\n money store product, a rewarded-video ad credit, or free. Use this whenever\n the user is working in the iDosGames TS SDK or its game templates\n (board-game, idle-rpg) and wants a shop screen, a daily rotating shop, gem\n packs, free/ad-paid slots, first-purchase badges, purchase-limit UI, or\n otherwise touches client.store, StoreService, GetStorefrontResponse,\n StoreDefinitions or offer purchasing — even if they don't name the module.\n---\n\n# Store system (iDosGames TS SDK)\n\nA title sells **offers** (products) placed on **shelves**. The shop is a tree:\n\n```\nStore (storefront)\n└── Layout.Sections\n └── Slots ← a slot is a PLACE on the shelf\n └── the slot decides which offer stands in it\n```\n\n⚠⚠ **A slot is the only address a product can be bought through.** An offer\nlives in a flat catalogue (`StoreOffers`) and becomes buyable only by standing\nin a slot; a product placed in no slot **cannot be bought at all**. There is no\n\"list of stores\" on the product — that field was removed, because it was never\na right to buy: the purchase never checked it.\n\nEverything is **server-authoritative**. The client asks to purchase, the\nbackend validates price, windows, gates, limits **and the rotation roll**, then\nthe SDK mirrors the confirmed result into a local cache your UI reads.\n\n## ⚠ Draw `getStorefront()`, not `getDefinitions()`\n\nThis is the single most important rule of the module.\n\n`getDefinitions()` returns the **config** — identical for every player.\n`getStorefront()` returns the shop **resolved for this player**: rotation\nalready rolled, gates applied, badges and refresh timers computed, remaining\nlimits filled in. None of that can be derived on the client:\n\n- **Rotation** is recomputed by the server on every read and stored nowhere. A\n client rolling it itself would show two devices two different shops.\n- **Counters** (`PurchasedTotal`, remaining caps, cooldown) belong to the\n player and are not in the config.\n- **Gates** filter storefronts, sections, slots and pool entries per player.\n\nUse `getDefinitions()` only for catalogue-wide tooling (an admin view, a\nsearch). A shop screen never needs it.\n\n## Prices: six kinds, one shape\n\nPrice is always a `PriceOptions` dictionary (`Pricing.Options`), the\nplatform-wide standard. An option's cost can be:\n\n| Kind | What it is | On the storefront view |\n| --------------------------------------- | ------------------------------------------- | ------------------------------- |\n| Virtual currency / items / event tokens | ordinary `ResourceConsume` | — |\n| Crypto | a crypto currency entry | — |\n| Real money | a store product entry; needs a receipt | `IsStorePaid: true` |\n| Rewarded video | an ad-credit entry, spent like any resource | `IsAdPaid: true` |\n| **Credits** (platform currency) | a `PlatformCredits` entry, `Amount` = price | `IsCreditsPaid`, `CreditsPrice` |\n| **Free** | **no cost at all** | `IsFree: true` |\n\n⚠ **An unset cost means FREE, and that is a legal product** — not an unfilled\nprice. A free offer must be limited on some axis (`TotalCap`, `DailyCap`,\n`CooldownSeconds`, or the slot's `PerInstanceCap`); without a limit the server\nrefuses the purchase, because it would be a button that grants itself forever.\n\n⚠ Ad-paid offers spend a **credit the player already earned** by watching a\nvideo (the Advertising module grants it server-side). Read the balance from\n`storefront.AdCreditBalance` to decide whether a card shows \"Watch\" or \"Buy\".\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 auth.* method\n\nconst store = client.store; // the StoreService\n```\n\nEvery store method requires an authenticated session. Without one they return\n`{ ok: false, reason: \"unauthorized\" }` — they do not throw.\n\n## Methods\n\nAll methods return `Promise<OperationResult<T>>`: either `{ ok: true, data }`\nor `{ ok: false, reason, error }`. Always branch on `result.ok`.\n\n| Method | Purpose | `data` on success |\n| ------------------------------------- | ---------------------------------------- | ----------------------- |\n| `getStorefront()` | **The shop as this player sees it now.** | `GetStorefrontResponse` |\n| `getDefinitions()` | The raw catalogue (config). | `StoreDefinitions` |\n| `getUserState()` | Purchase counters (state). | `UserStoreState` |\n| `purchase(offerID, count?, options?)` | Buy `count` (default 1) of one offer. | `StorePurchaseResponse` |\n| `purchaseBatch(purchases)` | Buy several offers in one atomic call. | `PurchaseBatchResponse` |\n\n`options` = `{ selectedOptionID?, payment?, slot? }`.\n\n`purchase` clamps `count` server-side to **1–100**. `purchaseBatch` takes\n`StorePurchaseRef[]`, deduped by `OfferID`, clamped to **50 refs per call**.\n\n## Drawing a shop screen\n\n```ts\nconst res = await client.store.getStorefront();\nif (!res.ok) return showError(res.error);\nconst front = res.data;\n\nfor (const store of front.Stores ?? []) {\n for (const section of store.Sections ?? []) {\n for (const slot of section.Slots ?? []) {\n // ⚠ An EMPTY slot arrives with Offer === null and must still be drawn as\n // an empty frame. Skipping it makes the shelf jump around whenever a\n // window closes or a gate filters the pool — which is exactly why the\n // server sends the slot rather than omitting it.\n if (!slot.Offer) {\n drawEmptyCard(slot.RefreshesAtUtc); // \"opens in ...\"\n continue;\n }\n\n const offer = slot.Offer;\n const state = offer.State;\n\n drawCard({\n title: offer.Identity?.DisplayName, // localization key OR a literal\n ribbons: offer.Identity?.Tags, // static badges: best_value, x2 ...\n // Server-computed badges — do not recompute these:\n firstPurchase: state?.IsFirstPurchaseAvailable,\n soldOut: state?.SoldOut,\n readyAt: state?.AvailableAtUtc, // free-slot cooldown\n // Rotation timer: \"Refreshes in 15h 40m\"\n refreshesAt: slot.RefreshesAtUtc,\n remainingThisWindow: slot.RemainingThisRotation, // null = unlimited\n prices: offer.PriceOptions, // already filtered to this platform\n });\n }\n }\n}\n```\n\n⚠ **Count timers down from `front.ServerTimeUtc`, not from the device clock.**\nThat is what it is for; a player with a skewed clock otherwise sees a shop that\nrefreshes at the wrong moment or claims to have already refreshed.\n\n⚠ **`null` and `0` mean different things in every remaining-count field.**\n`null` = \"no limit on this axis\"; `0` = \"spent, sold out\". Treating `null` as\nzero grays out every unlimited product on the shelf.\n\n`client.store.nextRefreshAtUtc()` returns the nearest refresh across all\nvisible slots — use it to schedule one reload instead of a polling loop.\n\n## Purchasing\n\n```ts\nconst res = await client.store.purchase(offer.OfferID, 1, {\n selectedOptionID: option.OptionID,\n // Pass the slot the player tapped — the storefront gives you all three ids.\n slot: {\n storeID: store.StoreID,\n sectionID: section.SectionID,\n slotID: slot.SlotID,\n },\n});\nif (!res.ok) return showError(res.error);\n```\n\nAn option paid in a store needs the receipt as well:\n\n```ts\nawait client.store.purchase(\"gems_l\", 1, {\n selectedOptionID: option.OptionID,\n payment: { Store: \"GooglePlay\", Receipt: receiptJson, Signature: signature },\n});\n```\n\n⚠ **The slot address is optional as a whole** (all three or none): without it\nthe server finds the slot itself. The roll is re-checked either way, so a\nproduct that rolled for the player nowhere cannot be bought with or without an\naddress. Pass it anyway when you have it — for a pooled product it removes the\nambiguity of which slot's per-window cap applies.\n\n### Credits — the platform currency (`IsCreditsPaid`)\n\n⚠ **Players see this currency as \"Aspis\"** (owner's decision 26.10.2026) — write \"Aspis\" in any player-facing text\n(price \"100 Aspis\", \"Not enough Aspis\"). \"Credits\" is only the API name (`PlatformCredits`, `purchaseWithCredits`,\n`CreditsError`, `IsCreditsPaid`); do not name a game's own currency \"Coins\" next to it either — players confuse them.\n\nCredits live on the player's **iDos Games account**, not in the game; the game's author gets 70% of the price, the\nplatform 30%. ⚠ The game **cannot charge Credits by itself**: the first call only creates a purchase request, the\nplayer confirms it on idosgames.com with their own account, and only then the purchase completes. On idosgames.com's\nown game page the SITE shows its confirmation window right over the game — `purchaseWithCredits` handles that by itself\nand `onConfirmation` is not called; on the game's own address or `/embed` it falls back to the link. ⚠ **One\nconfirmation, never two** (owner, 04.11.2026): a tap on an Aspis price calls `purchaseWithCredits` AT ONCE — no game\npopup \"Confirm the purchase\" before it; the site's window is the confirmation. Show your own popup only from\n`onConfirmation` (the site could not ask), with the link. Reference: `modules/store` `StoreScreen.buyWithCredits`.\nUse the one call that does the whole flow:\n\n```ts\nimport { CreditsError } from \"@idosgames/core\";\n\nconst res = await client.store.purchaseWithCredits(offer.OfferID, 1, {\n selectedOptionID: option.OptionID,\n slot: { storeID, sectionID, slotID },\n // Show a LINK/button to view.Url (target=\"_blank\") — not window.open after an await: browsers block that.\n onConfirmation: (view) => showConfirmLink(view.Url),\n signal: abort.signal, // stop waiting when the popup closes\n});\nif (!res.ok && res.error === CreditsError.NotEnough) {\n const b = await client.store.getCreditsBalance(); // b.data.TopUpUrl — where to top up\n}\n```\n\n- Price the option in Credits in LiveOps (resource picker of a PRICE → **Aspis**). Every sale of the game's author\n accepts Credits (owner, 05.11.2026): store, lootboxes, collection packs, craft, deals, boosts, VIP, item and\n character upgrades, skins, coop spins, the board's special mode, the Workshop publish fee, InApp AI (charged only\n when the generation is ready). NOT in player-to-player prices or refundable fees (marketplace listing fee, PvP\n match) — `CREDITS_NOT_ALLOWED_HERE`. One Credits entry per option, never together with a store product, never as a\n reward.\n- Outside the store the same flow is `client.credits.pay` around the module's own call, which takes the confirmation\n ID as its last argument / `creditsConfirmationID` option:\n `client.credits.pay((id) => client.lootbox.open(boxID, 1, option.OptionID, undefined, id), { onConfirmation, signal })`.\n A direct call with an Aspis option only returns `CreditsConfirmation` and changes nothing — check it first.\n In a screen built on the UI kit use `payWithCredits(client, (id) => ...)` from `@idosgames/react/ui` instead: the\n same flow plus the kit's own popup with the link and the top-up page on `NOT_ENOUGH_CREDITS` (skill\n `idosgames-ui-kit`, \"Prices in Aspis\").\n- Refusals (`CreditsError`): `CREDITS_LOGIN_REQUIRED` (guest — sign in with iDos Games), `NOT_ENOUGH_CREDITS`,\n `CREDITS_CONFIRMATION_DECLINED` / `_EXPIRED`, `CREDITS_BATCH_FORBIDDEN` (no Credits in `purchaseBatch`).\n- `getCreditsBalance()` — Credits the player can spend in games (bought + earned; bonus Credits are for platform services).\n- On the DEV copy of the title a Credits purchase is a free test: no request, no Credits moved.\n\n### ⚠ `OFFER_NOT_IN_ROTATION` means \"reload the shop\", not \"show an error\"\n\nThe rotation roll is recomputed, never stored, so editing a slot's pool\nreshuffles the shelf for everyone who has it open. A player who taps a card\nthat is no longer in their roll gets exactly this error:\n\n```ts\nimport { OFFER_NOT_IN_ROTATION } from \"@idosgames/core\";\n\nif (!res.ok && res.error === OFFER_NOT_IN_ROTATION) {\n await client.store.getStorefront(); // silently redraw; do not toast\n return;\n}\n```\n\nAlready-completed purchases are never undone by a reshuffle — counters are\naddressed by product and slot, not by the roll.\n\n## Events\n\nSubscribe with `client.on(...)`; each returns an unsubscribe fn:\n\n- `store:storefrontLoaded` → `GetStorefrontResponse`\n- `store:definitionsLoaded` → `StoreDefinitions`\n- `store:userStateLoaded` → `UserStoreState`\n- `store:offerPurchased` → `StorePurchaseResponse`\n- `store:offersPurchasedBatch` → `PurchaseBatchResponse`\n\nThe coarse `user:storeUpdated` (and `user:anyUpdated`) also fire on any store\ncache write.\n\n## Batch purchase\n\n```ts\nconst res = await client.store.purchaseBatch([\n { OfferID: \"pack1\", Count: 1 },\n { OfferID: \"starter_bundle\", Count: 1 },\n]);\nif (!res.ok) return showError(res.error);\nfor (const item of res.data) {\n if (item.Success) applyOk(item.Id);\n else showItemError(item.Id, item.Error);\n}\n```\n\nBatch results are **partial-aware**: the outer `res.ok` says the call ran, each\nelement's `Success`/`Error` says whether that item applied. Items rejected on\ntheir own merits (unknown id, outside window, gate failed, limit reached, not\nin rotation) are filtered out _before_ the merged charge is built. The\nsurvivors are then charged as **one merged, all-or-nothing transaction**.\n\n⚠ An option paid in a **store** cannot go in a batch — one receipt pays for one\npurchase. Ad credits can: a credit is an ordinary resource and two of them add\nup like two coins.\n\n## Gotchas\n\n- **Guard against double-submit.** Each `purchase()` mints a fresh idempotency\n key, so two calls are two real operations — a double-clicked \"Buy\" charges\n twice. Disable the control while a call is in flight.\n- **The struck-through price is display only.** `CompareAtCost` never reaches\n the charge. `DiscountPercent` is computed by the server and is `null` when it\n cannot be computed (a mixed bundle has no single percentage) — `null` is not\n zero, and printing \"0%\" there is a lie.\n- **Prices and rewards can be adjusted server-side.** Premium tiers apply\n discounts and bonuses automatically; read what was actually charged and\n granted off `res.data.Resources`, not off the displayed base values.\n- **`count` scales cost and rewards linearly, then premium is applied once** —\n it is not N independent purchases.\n- **Only one `Resources` apply per batch call**, but every item's own\n `OfferID`/`Count`/`ServerTimeUtc` is correct and drives its counter patch.\n- **`DailyPurchases` resets on UTC midnight.** Read `DailyResetUtc` from the\n cache after a call rather than computing it.\n- **Tags are for the client; player-dependent badges are not.** Anything whose\n truth depends on the player — first purchase, sold out, a timer — arrives in\n `State`, computed by the server. Do not encode those as tags.\n- **`DisplayName` is a localization key OR a literal.** Resolution is\n `t(x) = own table → fallback table → x itself`, so a title with literal\n names works with no migration. Run these through the localization module.\n- **Render from the cache, handle the error from the result.** Use `reason` to\n decide behaviour (retry on `\"connection\"`, re-auth on `\"unauthorized\"`, toast\n the `error` on `\"server\"`).\n\n## Full reference\n\n[references/data-model.md](references/data-model.md) — every config and\nstorefront field, the rotation modes, the purchase-limit/reset rule matrix, and\nbatch semantics. Read it when building config-driven UI or when an error points\nat a rule you need to understand.\n",
5
5
  "references": [
6
6
  {
7
7
  "path": "data-model.md",
@@ -9660,7 +9660,7 @@
9660
9660
  "rig2d-character": {
9661
9661
  "feature": "rig2d-character",
9662
9662
  "modality": "text",
9663
- "system": "You design a 2D cartoon hero for a short video: a layered chibi character on a skeleton, seen from the side. Reply with ONE JSON object — a hero recipe — and nothing else: no markdown, no comments.\n\n{ \"name\": string (≤ 40 chars, in the player's language), \"description\": string (≤ 300 chars),\n \"look\": { \"parts\": { \"<part type>\": \"<part id>\" | null, … }, \"colors\": { \"<colour target>\": \"#rrggbb\", … } },\n \"art\": { \"<picture path>\": { \"svg\": \"<svg markup without the svg wrapper>\" }, … } (optional) }\n\n- Pick parts ONLY from the lists below; null takes a part off where it is allowed. Choose colours that make the hero instantly readable.\n- \"art\" redraws a picture of a part you picked with your own SVG of exactly that canvas (width × height in px, y down): only <path>, <circle>, <ellipse>, <rect>, <line>, <polygon>, <polyline>, <g>, gradients and clipPath — no text, no images, no links, no scripts; ≤ 12000 characters each, ≤ 16 pictures. Draw in the same style: a heavy dark outline (#1f1a21, width 3–4), flat fills with one darker shade. Use it only when no part fits the idea (a chef's apron, a superhero suit).\n\nParts and pictures of the rig:\nBack (or null): back/1, back/2, back/3, back/4, back/5, back/6, back/7, back/8, back/9, back/10, back/11, back/12, back/13, back/14, back/15, back/16, back/17, back/18, back/19, back/20, back/21, back/22, back/23, back/24, back/25, back/26, back/27, back/28, back/29, back/30, back/knight_cape, back/mage_cape\nBeard (or null): beard/1, beard/2, beard/3, beard/4, beard/5, beard/6, beard/7, beard/8, beard/9, beard/10\nBoots (or null): boots/1, boots/2, boots/3, boots/4, boots/5, boots/6, boots/7, boots/8, boots/9, boots/10, boots/11, boots/12, boots/13, boots/14, boots/15, boots/16, boots/17, boots/18, boots/19, boots/20, boots/21, boots/22, boots/23, boots/24, boots/25, boots/26, boots/27, boots/28, boots/29, boots/30, boots/plate_boots, boots/leather_boots\nPants (or null): pants/1, pants/2, pants/3, pants/4, pants/5, pants/6, pants/7, pants/8, pants/9, pants/10, pants/11, pants/12, pants/13, pants/14, pants/15, pants/16, pants/17, pants/18, pants/19, pants/20, pants/21, pants/22, pants/23, pants/24, pants/25, pants/26, pants/27, pants/28, pants/29, pants/30, pants/31, pants/32, pants/33, pants/34, pants/35, pants/36, pants/37, pants/38, pants/39, pants/40, pants/41, pants/42, pants/43, pants/44, pants/45, pants/46, pants/47, pants/48, pants/49, pants/50\nBrows: brows/1, brows/2, brows/3, brows/4, brows/5, brows/6, brows/7, brows/8, brows/9, brows/10\nEyes: eyes/1, eyes/2, eyes/3, eyes/4, eyes/5, eyes/6, eyes/7, eyes/8, eyes/9, eyes/10, eyes/11, eyes/12, eyes/13, eyes/14, eyes/15, eyes/16, eyes/17, eyes/18, eyes/19, eyes/20\nGloves (or null): gloves/1, gloves/2, gloves/3, gloves/4, gloves/5, gloves/6, gloves/7, gloves/8, gloves/9, gloves/10, gloves/11, gloves/12, gloves/13, gloves/14, gloves/15, gloves/16, gloves/17, gloves/18, gloves/19, gloves/20, gloves/21, gloves/22, gloves/23, gloves/24, gloves/25, gloves/26, gloves/27, gloves/28, gloves/29, gloves/30, gloves/gauntlets, gloves/bracers\nHair (or null): hair/1, hair/2, hair/3, hair/4, hair/5, hair/6, hair/7, hair/8, hair/9, hair/10, hair/11, hair/12, hair/13, hair/14, hair/15, hair/16, hair/17, hair/18, hair/19, hair/20, hair/21, hair/22, hair/23, hair/24, hair/25, hair/26, hair/27, hair/28, hair/29, hair/30\nHat (or null): hat/1, hat/2, hat/3, hat/4, hat/5, hat/6, hat/7, hat/8, hat/9, hat/10, hat/11, hat/12, hat/13, hat/14, hat/15, hat/16, hat/17, hat/18, hat/19, hat/20, hat/21, hat/22, hat/23, hat/24, hat/25, hat/26, hat/27, hat/28, hat/29, hat/30, hat/31, hat/32, hat/33, hat/34, hat/35, hat/36, hat/37, hat/38, hat/39, hat/40, hat/41, hat/42, hat/43, hat/44, hat/45, hat/46, hat/47, hat/48, hat/49, hat/50, hat/51, hat/52, hat/53, hat/54, hat/55, hat/56, hat/57, hat/58, hat/59, hat/60, hat/great_helm, hat/knight_helm, hat/winged_helm, hat/wizard_hat, hat/ranger_hood, hat/iron_helm\nMouth: mouth/1, mouth/2, mouth/3, mouth/4, mouth/5, mouth/6, mouth/7, mouth/8, mouth/9, mouth/10\nGlasses (or null): glasses/1, glasses/2, glasses/3, glasses/4, glasses/5, glasses/6, glasses/7, glasses/8, glasses/9, glasses/10, glasses/11, glasses/12, glasses/13, glasses/14, glasses/15, glasses/16, glasses/17, glasses/18, glasses/19, glasses/20, glasses/21, glasses/22, glasses/23, glasses/24, glasses/25, glasses/26, glasses/27, glasses/28, glasses/29, glasses/30\nOffhand (or null): offhand/1, offhand/2, offhand/3, offhand/4, offhand/5, offhand/6, offhand/7, offhand/8, offhand/9, offhand/10, offhand/11, offhand/12, offhand/13, offhand/14, offhand/15, offhand/16, offhand/17, offhand/18, offhand/19, offhand/20, offhand/21, offhand/22, offhand/23, offhand/24, offhand/25, offhand/26, offhand/27, offhand/28, offhand/29, offhand/30, offhand/31, offhand/32, offhand/33, offhand/34, offhand/35, offhand/36, offhand/37, offhand/38, offhand/39, offhand/40, offhand/kite_shield, offhand/heater_shield, offhand/round_shield, offhand/tower_shield, offhand/buckler\nWeapon (or null): weapon/1, weapon/2, weapon/3, weapon/4, weapon/5, weapon/6, weapon/7, weapon/10, weapon/11, weapon/12, weapon/13, weapon/14, weapon/15, weapon/16, weapon/17, weapon/18, weapon/19, weapon/20, weapon/21, weapon/22, weapon/23, weapon/24, weapon/25, weapon/26, weapon/27, weapon/28, weapon/29, weapon/30, weapon/31, weapon/32, weapon/33, weapon/34, weapon/35, weapon/36, weapon/37, weapon/38, weapon/39, weapon/40, weapon/knight_sword, weapon/dark_blade, weapon/broadsword, weapon/flame_sword, weapon/dagger, weapon/battle_axe, weapon/double_axe, weapon/mace, weapon/warhammer, weapon/spear, weapon/wizard_staff, weapon/druid_staff, weapon/hunting_bow, weapon/elven_bow, weapon/pistol, weapon/crossbow, weapon/musket, weapon/rifle\nTop: top/1, top/2, top/3, top/4, top/5, top/6, top/7, top/8, top/9, top/10, top/11, top/12, top/13, top/14, top/15, top/16, top/17, top/18, top/19, top/20, top/21, top/22, top/23, top/24, top/25, top/26, top/27, top/28, top/29, top/30, top/31, top/32, top/33, top/34, top/35, top/36, top/37, top/38, top/39, top/40, top/41, top/42, top/43, top/44, top/45, top/46, top/47, top/48, top/49, top/50, top/51, top/52, top/53, top/54, top/55, top/56, top/57, top/58, top/59, top/60, top/plate_armor, top/chainmail, top/leather_armor, top/mage_robe\nSkin: skin/1\ncolors: Skin, Hair, Beard, Brows, Eyes (#rrggbb)\npictures (path WxH, y down): back/1 84x76; back/2 75x74; back/3 71x71; back/4 74x83; back/5 81x95; back/6 76x88; back/7 79x78; back/8 55x109; back/9 85x100; back/10 63x94; back/11 105x99; back/12 73x98; back/13 92x93; back/14 76x75; back/15 95x124; back/16 79x86; back/17 76x83; back/18 96x90; back/19 102x97; back/20 65x87; back/21 84x76; back/22 71x71; back/23 75x74; back/24 95x124; back/25 96x90; back/26 65x87; back/27 73x98; back/28 76x83; back/29 55x109; back/30 79x86; beard/1 43x27; beard/2 42x39; beard/3 89x30; beard/4 85x46; beard/5 73x44; beard/6 39x42; beard/7 54x20; beard/8 80x18; beard/9 29x25; beard/10 88x44; boots/1_near 31x20; boots/1_far 31x20; boots/2_near 30x23; boots/2_far 30x23; boots/3_near 32x23; boots/3_far 32x23; boots/4_near 32x22; boots/4_far 32x22; boots/5_near 26x30; boots/5_far 26x30; boots/6_near 30x18; boots/6_far 30x18; boots/7_near 33x25; boots/7_far 33x25; boots/8_near 32x20; boots/8_far 32x20; boots/9_near 32x20; boots/9_far 32x20; boots/10_near 35x19; boots/10_far 35x19; boots/11_near 32x26; boots/11_far 32x26; boots/12_near 30x22; boots/12_far 30x22; boots/13_near 28x23; boots/13_far 28x23; boots/14_near 31x17; boots/14_far 31x17; boots/15_near 28x28; boots/15_far 28x28; boots/16_near 30x25; boots/16_far 30x25; boots/17_near 30x24; boots/17_far 30x24; boots/18_near 31x18; boots/18_far 31x18; boots/19_near 28x22; boots/19_far 28x22; boots/20_near 35x24; boots/20_far 35x24; boots/21_near 31x20; boots/21_far 31x20; boots/22_near 28x22; boots/22_far 28x22; boots/23_near 28x22; boots/23_far 28x22; boots/24_near 28x28; boots/24_far 28x28; boots/25_near 28x28; boots/25_far 28x28; boots/26_near 30x24; boots/26_far 30x24; boots/27_near 26x30; boots/27_far 26x30; boots/28_near 26x30; boots/28_far 26x30; boots/29_near 30x23; boots/29_far 30x23; boots/30_near 32x20; boots/30_far 32x20; pants/1 79x26; pants/1_near 25x28; pants/1_far 25x28; pants/2 79x26; pants/2_near 25x28; pants/2_far 25x28; pants/3 79x26; pants/3_near 25x41; pants/3_far 25x41; pants/4 79x26; pants/4_near 31x28; pants/4_far 31x28; pants/5 79x26; pants/5_near 36x42; pants/5_far 38x43; pants/6 92x40; pants/7 92x40; pants/8 93x46; pants/9 92x41; pants/10 92x40; pants/11 88x40; pants/12 79x26; pants/13 79x26; pants/13_near 25x41; pants/13_far 25x41; pants/14 79x26; pants/14_near 36x42; pants/14_far 38x43; pants/15 79x26; pants/15_near 25x41; pants/15_far 25x41; pants/16 79x26; pants/16_near 25x28; pants/16_far 25x40; pants/17 92x39; pants/17_near 25x41; pants/17_far 25x41; pants/18 96x36; pants/19 79x26; pants/19_near 25x41; pants/19_far 25x41; pants/20 79x26; pants/20_near 25x28; pants/20_far 25x28; pants/21 79x26; pants/21_near 25x41; pants/21_far 25x41; pants/22 79x26; pants/22_near 25x41; pants/22_far 25x41; pants/23 79x26; pants/23_near 25x41; pants/23_far 25x41; pants/24 79x26; pants/24_near 25x41; pants/24_far 25x41; pants/25 79x26; pants/25_near 25x41; pants/25_far 25x41; pants/26 82x37; pants/27 79x26; pants/27_near 25x41; pants/27_far 25x41; pants/28 79x26; pants/28_near 25x28; pants/28_far 25x28; pants/29 79x26; pants/29_near 25x28; pants/29_far 25x40; pants/30 79x26; pants/30_near 25x41; pants/30_far 25x41; pants/31 79x26; pants/31_near 25x41; pants/31_far 25x41; pants/32 79x26; pants/32_near 31x28; pants/32_far 31x28; pants/33 79x26; pants/33_near 31x28; pants/33_far 31x28; pants/34 79x26; pants/34_near 31x28; pants/34_far 31x28; pants/35 79x26; pants/35_near 31x28; pants/35_far 31x28; pants/36 92x39; pants/37 79x26; pants/37_near 25x41; pants/37_far 25x41; pants/38 79x26; pants/38_near 25x28; pants/38_far 25x28; pants/39 79x26; pants/39_near 25x28; pants/39_far 25x28; pants/40 79x26; pants/40_near 25x41; pants/40_far 25x41; pants/41 79x26; pants/41_near 25x41; pants/41_far 25x41; pants/42 82x37; pants/43 82x37; pants/44 88x40; pants/45 79x26; pants/45_near 36x42; pants/45_far 38x43; pants/46 79x26; pants/46_near 36x42; pants/46_far 38x43; pants/47 96x36; pants/47_near 25x41; pants/47_far 25x41; pants/48 96x38; pants/49 79x26; pants/50 79x26; pants/50_near 31x28; pants/50_far 31x28; brows/1 51x11; brows/2 65x18; brows/3 61x16; brows/4 66x19; brows/5 72x23; brows/6 44x10; brows/7 62x18; brows/8 57x18; brows/9 53x13; brows/10 17x23; eyes/1 61x20; eyes/2 63x22; eyes/3 66x30; eyes/4 64x31; eyes/5 64x24; eyes/6 63x28; eyes/7 69x29; eyes/8 44x22; eyes/9 48x20; eyes/10 61x23; eyes/11 60x21; eyes/12 65x27; eyes/13 59x21; eyes/14 62x22; eyes/15 63x23; eyes/16 59x26; eyes/17 55x22; eyes/18 57x6; eyes/19 59x23; eyes/20 59x26; glasses/1 85x48; glasses/2 71x30; glasses/3 90x38; glasses/4 96x40; glasses/5 80x44; glasses/6 91x41; glasses/7 80x35; glasses/8 58x27; glasses/9 84x47; glasses/10 81x58; glasses/11 72x21; glasses/12 83x36; glasses/13 81x24; glasses/14 72x37; glasses/15 83x44; glasses/16 86x39; glasses/17 89x58; glasses/18 74x39; glasses/19 89x37; glasses/20 90x54; glasses/21 79x40; glasses/22 71x32; glasses/23 82x38; glasses/24 77x34; glasses/25 73x34; glasses/26 70x33; glasses/27 74x64; glasses/28 86x77; glasses/29 72x41; glasses/30 71x51; offhand/1 101x83; offhand/2 79x87; offhand/3 87x87; offhand/4 96x81; offhand/5 109x87; offhand/6 73x73; offhand/7 91x70; offhand/8 89x89; offhand/9 95x70; offhand/10 85x82; offhand/11_low 56x56; offhand/12_low 56x56; offhand/13_low 65x64; offhand/14_low 61x46; offhand/15_low 78x36; offhand/16_low 65x30; offhand/17_low 55x44; offhand/18_low 37x37; offhand/19_low 46x43; offhand/20_low 43x41; offhand/21_low 56x44; offhand/22_low 56x66; offhand/23_low 49x72; offhand/24_low 56x55; offhand/25_low 77x51; offhand/26_low 94x50; offhand/27_low 78x59; offhand/28_low 63x35; offhand/29_low 78x35; offhand/30_low 46x59; offhand/31_low 53x44; offhand/32_low 57x54; offhand/33_low 65x29; offhand/34_low 92x34; offhand/35_low 54x48; offhand/36_low 83x31; offhand/37_low 51x42; offhand/38_low 65x32; offhand/39_low 53x37; offhand/40_low 53x37; weapon/1 111x77; weapon/2 112x92; weapon/3 115x60; weapon/4 96x78; weapon/5 144x71; weapon/6 135x43; weapon/7 141x40; weapon/10 100x66; weapon/11 101x87; weapon/12 124x58; weapon/13 144x33; weapon/14 131x45; weapon/15 143x60; weapon/16 142x44; weapon/17 133x35; weapon/18 103x65; weapon/19 132x39; weapon/20 133x33; weapon/21 112x29; weapon/22 116x45; weapon/23 88x58; weapon/24 137x28; weapon/25 153x43; weapon/26 160x50; weapon/27 156x51; weapon/28 174x47; weapon/29 160x65; weapon/30 147x40; weapon/31 112x92; weapon/32 115x59; weapon/33 157x51; weapon/34 143x44; weapon/35 143x44; weapon/36 175x47; weapon/37 124x58; weapon/38 135x43; weapon/39 143x60; weapon/40 87x58; gloves/1_near 28x25; gloves/1_far 27x25; gloves/2_near 34x35; gloves/2_far 32x35; gloves/3_near 28x25; gloves/3_far 27x25; gloves/4_near 31x34; gloves/4_far 32x33; gloves/5_near 36x36; gloves/5_far 35x36; gloves/6_near 31x32; gloves/6_far 32x33; gloves/7_near 35x34; gloves/7_far 34x34; gloves/8_near 30x14; gloves/8_far 28x14; gloves/9_near 38x18; gloves/9_far 38x18; gloves/10_near 34x35; gloves/10_far 32x35; gloves/11_near 30x23; gloves/11_far 28x23; gloves/12_near 33x35; gloves/12_far 33x36; gloves/13_near 36x36; gloves/13_far 32x37; gloves/14_near 32x27; gloves/14_far 29x24; gloves/15_near 36x40; gloves/15_far 35x40; gloves/16_near 31x36; gloves/16_far 30x37; gloves/17_near 31x36; gloves/17_far 30x37; gloves/18_near 32x31; gloves/18_far 30x30; gloves/19_near 32x34; gloves/19_far 29x34; gloves/20_near 34x38; gloves/20_far 31x38; gloves/21_near 28x25; gloves/21_far 27x25; gloves/22_near 28x25; gloves/22_far 27x25; gloves/23_near 36x40; gloves/23_far 35x40; gloves/24_near 36x40; gloves/24_far 35x40; gloves/25_near 36x36; gloves/25_far 32x37; gloves/26_near 30x14; gloves/26_far 28x14; gloves/27_near 38x18; gloves/27_far 38x18; gloves/28_near 38x18; gloves/28_far 38x18; gloves/29_near 31x34; gloves/29_far 32x33; gloves/30_near 32x27; gloves/30_far 29x24; hair_under/1 87x54; hair_under/2 111x60; hair_under/3 97x54; hair_under/4 103x55; hair_under/5 101x64; hair_under/6 96x54; hair_under/7 92x52; hair_under/8 123x75; hair_under/9 105x78; hair_under/10 130x72; hair_under/11 97x87; hair_under/12 126x64; hair_under/13 104x40; hair_under/14 96x55; hair_under/15 92x49; hair_under/16 95x55; hair_under/17 102x57; hair_under/18 118x69; hair_under/19 118x88; hair_under/20 96x52; hair_under/21 101x60; hair_under/22 90x53; hair_under/23 94x53; hair_under/24 122x66; hair_under/25 118x67; hair_under/26 98x55; hair_under/27 103x52; hair_under/28 122x79; hair_under/29 99x90; hair_under/30 116x96; hair/1 87x54; hair/2 113x61; hair/3 98x65; hair/4 119x94; hair/5 108x75; hair/6 115x70; hair/7 97x67; hair/8 147x120; hair/9 105x113; hair/10 136x84; hair/11 133x113; hair/12 134x74; hair/13 105x40; hair/14 114x80; hair/15 96x57; hair/16 97x86; hair/17 105x63; hair/18 128x83; hair/19 128x107; hair/20 97x59; hair/20_back 67x91; hair/21 101x65; hair/21_back 96x105; hair/22 114x71; hair/23 124x84; hair/24 127x74; hair/25 135x102; hair/26 103x68; hair/27 103x53; hair/28 125x86; hair/28_back 97x102; hair/29 99x111; hair/29_back 79x112; hair/30 119x114; hat/1 120x110; hat/1_back 95x59; hat/2 154x75; hat/3_low 126x82; hat/4 122x78; hat/5 118x120; hat/6 130x82; hat/7 121x105; hat/7_back 33x53; hat/8_low 136x75; hat/9_low 117x93; hat/10_low 140x94; hat/11_low 114x115; hat/12_low 145x69; hat/13_low 117x131; hat/14_low 140x120; hat/15 145x102; hat/16 129x97; hat/16_back 36x56; hat/17_low 126x84; hat/18 142x93; hat/19_low 122x106; hat/20_low 106x71; hat/20_back 68x44; hat/21_low 117x75; hat/22_low 123x94; hat/22_back 37x46; hat/23 137x110; hat/23_back 134x64; hat/24 133x81; hat/25 126x100; hat/25_back 43x60; hat/26 108x70; hat/26_back 46x52; hat/27_low 133x83; hat/28 126x107; hat/28_back 112x56; hat/29_low 124x117; hat/30_low 117x78; hat/31_low 136x75; hat/32 120x110; hat/32_back 95x59; hat/33_low 145x69; hat/34_low 106x71; hat/34_back 68x44; hat/35_low 106x71; hat/35_back 68x44; hat/36 154x75; hat/37 133x81; hat/38 133x81; hat/39 118x120; hat/40_low 117x75; hat/41_low 117x75; hat/42_low 117x93; hat/43_low 123x94; hat/43_back 37x46; hat/44_low 133x79; hat/45_low 114x115; hat/46_low 114x115; hat/47_low 140x94; hat/48 122x78; hat/49_low 107x70; hat/49_back 46x52; hat/50_low 124x117; hat/51_low 117x131; hat/52 126x107; hat/52_back 112x56; hat/53_low 140x120; hat/54_low 140x120; hat/55 137x110; hat/55_back 134x64; hat/56 137x110; hat/56_back 134x64; hat/57 130x82; hat/58 129x97; hat/58_back 36x56; hat/59 121x105; hat/59_back 33x53; hat/60 145x102; mouth/1 15x7; mouth/2 27x19; mouth/3 23x12; mouth/4 24x11; mouth/5 33x16; mouth/6 20x18; mouth/7 20x19; mouth/8 23x13; mouth/9 24x19; mouth/10 23x15; skin/1_arm_near 23x72; skin/1_arm_far 29x71; skin/1_torso 77x50; skin/1_head 103x90; skin/1_leg 27x55; top/1 87x71; top/2 91x88; top/2_sleeve_near 37x34; top/2_sleeve_far 34x35; top/3 90x88; top/3_sleeve_near 31x64; top/3_sleeve_far 32x63; top/4 87x88; top/4_sleeve_near 37x34; top/4_sleeve_far 34x35; top/5 87x81; top/5_sleeve_near 37x34; top/5_sleeve_far 34x35; top/6 88x82; top/6_sleeve_near 29x37; top/6_sleeve_far 29x38; top/7 87x83; top/7_sleeve_near 29x37; top/7_sleeve_far 29x38; top/8 87x80; top/9 87x81; top/9_sleeve_near 28x58; top/9_cap_near 37x34; top/9_sleeve_far 34x59; top/9_cap_far 34x36; top/10 56x59; top/10_coat 88x106; top/10_sleeve_near 28x58; top/10_sleeve_far 32x59; top/11 87x72; top/11_sleeve_near 37x34; top/11_sleeve_far 34x35; top/12 86x81; top/12_sleeve_near 28x58; top/12_sleeve_far 32x59; top/13 88x82; top/13_sleeve_near 28x58; top/13_sleeve_far 32x59; top/14 87x81; top/15 90x88; top/15_sleeve_near 31x64; top/15_sleeve_far 32x63; top/16 86x84; top/16_sleeve_near 31x59; top/16_sleeve_far 34x59; top/17 87x83; top/17_sleeve_near 34x37; top/17_sleeve_far 33x36; top/18 89x74; top/18_sleeve_near 37x34; top/18_sleeve_far 34x35; top/19 89x92; top/19_sleeve_near 37x34; top/19_sleeve_far 34x35; top/20 87x71; top/20_sleeve_near 28x58; top/20_sleeve_far 32x59; top/21 87x71; top/21_sleeve_near 28x58; top/21_sleeve_far 32x59; top/22 87x81; top/22_sleeve_near 28x58; top/22_sleeve_far 32x59; top/23 87x81; top/23_sleeve_near 28x58; top/23_sleeve_far 32x59; top/24 87x81; top/24_sleeve_near 28x58; top/24_sleeve_far 32x59; top/25 88x72; top/25_sleeve_near 28x58; top/25_sleeve_far 32x59; top/26 92x93; top/27 87x82; top/27_sleeve_near 28x58; top/27_sleeve_far 32x59; top/28 86x74; top/29 63x65; top/29_coat 88x106; top/29_sleeve_near 28x58; top/29_sleeve_far 32x59; top/30 87x80; top/30_sleeve_near 28x58; top/30_sleeve_far 32x59; top/31 90x88; top/31_sleeve_near 31x64; top/31_sleeve_far 32x63; top/32 88x82; top/32_sleeve_near 28x58; top/32_sleeve_far 32x59; top/33 87x81; top/33_sleeve_near 37x34; top/33_sleeve_far 34x35; top/34 87x81; top/34_sleeve_near 37x34; top/34_sleeve_far 34x35; top/35 87x81; top/36 87x71; top/36_sleeve_near 28x58; top/36_sleeve_far 32x59; top/37 87x81; top/37_sleeve_near 29x37; top/37_sleeve_far 29x38; top/38 87x82; top/38_sleeve_near 28x58; top/38_sleeve_far 32x59; top/39 87x83; top/39_sleeve_near 34x37; top/39_sleeve_far 33x36; top/40 90x88; top/40_sleeve_near 31x64; top/40_sleeve_far 32x63; top/41 91x87; top/41_sleeve_near 31x64; top/41_sleeve_far 32x63; top/42 87x81; top/42_sleeve_near 28x58; top/42_sleeve_far 32x59; top/43 87x79; top/43_sleeve_near 29x37; top/43_sleeve_far 29x38; top/44 87x72; top/44_sleeve_near 31x64; top/44_sleeve_far 32x63; top/45 87x88; top/45_sleeve_near 28x58; top/45_sleeve_far 32x59; top/46 87x72; top/46_sleeve_near 28x58; top/46_cap_near 37x34; top/46_sleeve_far 32x59; top/46_cap_far 34x35; top/47 88x82; top/47_sleeve_near 29x37; top/47_sleeve_far 29x38; top/48 87x71; top/48_sleeve_near 28x58; top/48_sleeve_far 32x59; top/49 86x81; top/49_sleeve_near 28x58; top/49_sleeve_far 32x59; top/50 89x74; top/50_sleeve_near 37x34; top/50_sleeve_far 34x35; top/51 87x72; top/51_sleeve_near 31x64; top/51_sleeve_far 32x63; top/52 87x72; top/52_sleeve_near 29x37; top/52_sleeve_far 29x38; top/53 87x72; top/53_sleeve_near 29x37; top/53_sleeve_far 29x38; top/54 89x92; top/54_sleeve_near 37x34; top/54_sleeve_far 34x35; top/55 87x81; top/56 86x84; top/56_sleeve_near 31x59; top/56_sleeve_far 34x59; top/57 87x80; top/57_sleeve_near 28x58; top/57_sleeve_far 32x59; top/58 92x93; top/59 90x88; top/59_sleeve_near 31x64; top/59_sleeve_far 32x63; top/60 87x80; weapon/knight_sword 156x51; weapon/dark_blade 160x50; weapon/broadsword 160x65; weapon/flame_sword 174x47; weapon/dagger 112x29; weapon/battle_axe 115x60; weapon/double_axe 112x92; weapon/mace 144x71; weapon/warhammer 103x65; weapon/spear 147x40; weapon/wizard_staff 135x43; weapon/druid_staff 144x33; offhand/kite_shield 85x82; offhand/heater_shield 96x81; offhand/round_shield 89x89; offhand/tower_shield 109x87; offhand/buckler 73x73; hat/great_helm 118x120; hat/knight_helm_low 114x115; hat/winged_helm_low 133x83; hat/wizard_hat 154x75; hat/ranger_hood_low 114x115; hat/iron_helm 130x82; top/plate_armor 87x88; top/plate_armor_sleeve_near 37x34; top/plate_armor_sleeve_far 34x35; top/chainmail 91x88; top/chainmail_sleeve_near 37x34; top/chainmail_sleeve_far 34x35; top/leather_armor 89x92; top/leather_armor_sleeve_near 37x34; top/leather_armor_sleeve_far 34x35; top/mage_robe 89x74; top/mage_robe_sleeve_near 37x34; top/mage_robe_sleeve_far 34x35; gloves/gauntlets_near 34x35; gloves/gauntlets_far 32x35; gloves/bracers_near 36x40; gloves/bracers_far 35x40; boots/plate_boots_near 30x23; boots/plate_boots_far 30x23; boots/leather_boots_near 26x30; boots/leather_boots_far 26x30; back/knight_cape 95x124; back/mage_cape 95x124; weapon/hunting_bow 70x112; weapon/hunting_bow_drawn 70x112; weapon/hunting_bow_shot 70x112; weapon/elven_bow 72x128; weapon/elven_bow_drawn 72x128; weapon/elven_bow_shot 72x128; weapon/pistol 96x40; weapon/pistol_shot 96x40; weapon/crossbow 104x56; weapon/crossbow_shot 104x56; weapon/musket 180x40; weapon/musket_shot 180x40; weapon/rifle 160x52; weapon/rifle_shot 160x52",
9663
+ "system": "You design a 2D cartoon hero for a short video: a layered chibi character on a skeleton, seen from the side. Reply with ONE JSON object — a hero recipe — and nothing else: no markdown, no comments.\n\n{ \"name\": string (≤ 40 chars, in the player's language), \"description\": string (≤ 300 chars),\n \"look\": { \"parts\": { \"<part type>\": \"<part id>\" | null, … }, \"colors\": { \"<colour target>\": \"#rrggbb\", … } },\n \"art\": { \"<picture path>\": { \"svg\": \"<svg markup without the svg wrapper>\" }, … } (optional) }\n\n- Pick parts ONLY from the lists below; null takes a part off where it is allowed. Choose colours that make the hero instantly readable.\n- \"art\" redraws a picture of a part you picked with your own SVG of exactly that canvas (width × height in px, y down): only <path>, <circle>, <ellipse>, <rect>, <line>, <polygon>, <polyline>, <g>, gradients and clipPath — no text, no images, no links, no scripts; ≤ 12000 characters each, ≤ 16 pictures. Draw in the same style: a heavy dark outline (#1f1a21, width 3–4), flat fills with one darker shade. Use it only when no part fits the idea (a chef's apron, a superhero suit).\n\nParts and pictures of the rig:\nBack (or null): back/1, back/2, back/3, back/4, back/5, back/6, back/7, back/8, back/9, back/10, back/11, back/12, back/13, back/14, back/15, back/16, back/17, back/18, back/19, back/20, back/21, back/22, back/23, back/24, back/25, back/26, back/27, back/28, back/29, back/30, back/knight_cape, back/mage_cape\nBeard (or null): beard/1, beard/2, beard/3, beard/4, beard/5, beard/6, beard/7, beard/8, beard/9, beard/10\nBoots (or null): boots/1, boots/2, boots/3, boots/4, boots/5, boots/6, boots/7, boots/8, boots/9, boots/10, boots/11, boots/12, boots/13, boots/14, boots/15, boots/16, boots/17, boots/18, boots/19, boots/20, boots/21, boots/22, boots/23, boots/24, boots/25, boots/26, boots/27, boots/28, boots/29, boots/30, boots/plate_boots, boots/leather_boots\nPants (or null): pants/1, pants/2, pants/3, pants/4, pants/5, pants/6, pants/7, pants/8, pants/9, pants/10, pants/11, pants/12, pants/13, pants/14, pants/15, pants/16, pants/17, pants/18, pants/19, pants/20, pants/21, pants/22, pants/23, pants/24, pants/25, pants/26, pants/27, pants/28, pants/29, pants/30, pants/31, pants/32, pants/33, pants/34, pants/35, pants/36, pants/37, pants/38, pants/39, pants/40, pants/41, pants/42, pants/43, pants/44, pants/45, pants/46, pants/47, pants/48, pants/49, pants/50\nBrows: brows/1, brows/2, brows/3, brows/4, brows/5, brows/6, brows/7, brows/8, brows/9, brows/10\nEyes: eyes/1, eyes/2, eyes/3, eyes/4, eyes/5, eyes/6, eyes/7, eyes/8, eyes/9, eyes/10, eyes/11, eyes/12, eyes/13, eyes/14, eyes/15, eyes/16, eyes/17, eyes/18, eyes/19, eyes/20\nGloves (or null): gloves/1, gloves/2, gloves/3, gloves/4, gloves/5, gloves/6, gloves/7, gloves/8, gloves/9, gloves/10, gloves/11, gloves/12, gloves/13, gloves/14, gloves/15, gloves/16, gloves/17, gloves/18, gloves/19, gloves/20, gloves/21, gloves/22, gloves/23, gloves/24, gloves/25, gloves/26, gloves/27, gloves/28, gloves/29, gloves/30, gloves/gauntlets, gloves/bracers\nHair (or null): hair/1, hair/2, hair/3, hair/4, hair/5, hair/6, hair/7, hair/8, hair/9, hair/10, hair/11, hair/12, hair/13, hair/14, hair/15, hair/16, hair/17, hair/18, hair/19, hair/20, hair/21, hair/22, hair/23, hair/24, hair/25, hair/26, hair/27, hair/28, hair/29, hair/30\nHat (or null): hat/1, hat/2, hat/3, hat/4, hat/5, hat/6, hat/7, hat/8, hat/9, hat/10, hat/11, hat/12, hat/13, hat/14, hat/15, hat/16, hat/17, hat/18, hat/19, hat/20, hat/21, hat/22, hat/23, hat/24, hat/25, hat/26, hat/27, hat/28, hat/29, hat/30, hat/31, hat/32, hat/33, hat/34, hat/35, hat/36, hat/37, hat/38, hat/39, hat/40, hat/41, hat/42, hat/43, hat/44, hat/45, hat/46, hat/47, hat/48, hat/49, hat/50, hat/51, hat/52, hat/53, hat/54, hat/55, hat/56, hat/57, hat/58, hat/59, hat/60, hat/great_helm, hat/knight_helm, hat/winged_helm, hat/wizard_hat, hat/ranger_hood, hat/iron_helm\nMouth: mouth/1, mouth/2, mouth/3, mouth/4, mouth/5, mouth/6, mouth/7, mouth/8, mouth/9, mouth/10\nGlasses (or null): glasses/1, glasses/2, glasses/3, glasses/4, glasses/5, glasses/6, glasses/7, glasses/8, glasses/9, glasses/10, glasses/11, glasses/12, glasses/13, glasses/14, glasses/15, glasses/16, glasses/17, glasses/18, glasses/19, glasses/20, glasses/21, glasses/22, glasses/23, glasses/24, glasses/25, glasses/26, glasses/27, glasses/28, glasses/29, glasses/30\nOffhand (or null): offhand/1, offhand/2, offhand/3, offhand/4, offhand/5, offhand/6, offhand/7, offhand/8, offhand/9, offhand/10, offhand/11, offhand/12, offhand/13, offhand/14, offhand/15, offhand/16, offhand/17, offhand/18, offhand/19, offhand/20, offhand/21, offhand/22, offhand/23, offhand/24, offhand/25, offhand/26, offhand/27, offhand/28, offhand/29, offhand/30, offhand/31, offhand/32, offhand/33, offhand/34, offhand/35, offhand/36, offhand/37, offhand/38, offhand/39, offhand/40, offhand/kite_shield, offhand/heater_shield, offhand/round_shield, offhand/tower_shield, offhand/buckler\nWeapon (or null): weapon/1, weapon/2, weapon/3, weapon/4, weapon/5, weapon/6, weapon/7, weapon/10, weapon/11, weapon/12, weapon/13, weapon/14, weapon/15, weapon/16, weapon/17, weapon/18, weapon/19, weapon/20, weapon/21, weapon/22, weapon/23, weapon/24, weapon/25, weapon/26, weapon/27, weapon/28, weapon/29, weapon/30, weapon/31, weapon/32, weapon/33, weapon/34, weapon/35, weapon/36, weapon/37, weapon/38, weapon/39, weapon/40, weapon/knight_sword, weapon/dark_blade, weapon/broadsword, weapon/flame_sword, weapon/dagger, weapon/battle_axe, weapon/double_axe, weapon/mace, weapon/warhammer, weapon/spear, weapon/wizard_staff, weapon/druid_staff, weapon/hunting_bow, weapon/elven_bow, weapon/pistol, weapon/crossbow, weapon/musket, weapon/rifle\nTop: top/1, top/2, top/3, top/4, top/5, top/6, top/7, top/8, top/9, top/10, top/11, top/12, top/13, top/14, top/15, top/16, top/17, top/18, top/19, top/20, top/21, top/22, top/23, top/24, top/25, top/26, top/27, top/28, top/29, top/30, top/31, top/32, top/33, top/34, top/35, top/36, top/37, top/38, top/39, top/40, top/41, top/42, top/43, top/44, top/45, top/46, top/47, top/48, top/49, top/50, top/51, top/52, top/53, top/54, top/55, top/56, top/57, top/58, top/59, top/60, top/plate_armor, top/chainmail, top/leather_armor, top/mage_robe\nSkin: skin/1\ncolors: Skin, Hair, Beard, Brows, Eyes (#rrggbb)\npictures (path WxH, y down): back/1 84x76; back/2 75x74; back/3 71x71; back/4 74x83; back/5 81x95; back/6 76x88; back/7 79x78; back/8 55x109; back/9 85x100; back/10 63x94; back/11 105x99; back/12 73x98; back/13 92x93; back/14 76x75; back/15 95x124; back/16 79x86; back/17 76x83; back/18 96x90; back/19 102x97; back/20 65x87; back/21 84x76; back/22 71x71; back/23 75x74; back/24 95x124; back/25 96x90; back/26 65x87; back/27 73x98; back/28 76x83; back/29 55x109; back/30 79x86; beard/1 43x27; beard/2 42x39; beard/3 89x30; beard/4 85x46; beard/5 73x44; beard/6 39x42; beard/7 54x20; beard/8 80x18; beard/9 29x25; beard/10 88x44; boots/1_near 31x20; boots/1_far 31x20; boots/2_near 30x23; boots/2_far 30x23; boots/3_near 32x23; boots/3_far 32x23; boots/4_near 32x22; boots/4_far 32x22; boots/5_near 26x30; boots/5_far 26x30; boots/6_near 30x18; boots/6_far 30x18; boots/7_near 33x25; boots/7_far 33x25; boots/8_near 32x20; boots/8_far 32x20; boots/9_near 32x20; boots/9_far 32x20; boots/10_near 35x19; boots/10_far 35x19; boots/11_near 32x26; boots/11_far 32x26; boots/12_near 30x22; boots/12_far 30x22; boots/13_near 28x23; boots/13_far 28x23; boots/14_near 31x17; boots/14_far 31x17; boots/15_near 28x28; boots/15_far 28x28; boots/16_near 30x25; boots/16_far 30x25; boots/17_near 30x24; boots/17_far 30x24; boots/18_near 31x18; boots/18_far 31x18; boots/19_near 28x22; boots/19_far 28x22; boots/20_near 35x24; boots/20_far 35x24; boots/21_near 31x20; boots/21_far 31x20; boots/22_near 28x22; boots/22_far 28x22; boots/23_near 28x22; boots/23_far 28x22; boots/24_near 28x28; boots/24_far 28x28; boots/25_near 28x28; boots/25_far 28x28; boots/26_near 30x24; boots/26_far 30x24; boots/27_near 26x30; boots/27_far 26x30; boots/28_near 26x30; boots/28_far 26x30; boots/29_near 30x23; boots/29_far 30x23; boots/30_near 32x20; boots/30_far 32x20; pants/1 79x26; pants/1_near 25x28; pants/1_far 25x28; pants/2 79x26; pants/2_near 25x28; pants/2_far 25x28; pants/3 79x26; pants/3_near 25x41; pants/3_far 25x41; pants/4 79x26; pants/4_near 31x28; pants/4_far 31x28; pants/5 79x26; pants/5_near 36x42; pants/5_far 38x43; pants/6 92x40; pants/7 92x40; pants/8 93x46; pants/9 92x41; pants/10 92x40; pants/11 88x40; pants/12 79x26; pants/13 79x26; pants/13_near 25x41; pants/13_far 25x41; pants/14 79x26; pants/14_near 36x42; pants/14_far 38x43; pants/15 79x26; pants/15_near 25x41; pants/15_far 25x41; pants/16 79x26; pants/16_near 25x28; pants/16_far 25x40; pants/17 92x39; pants/17_near 25x41; pants/17_far 25x41; pants/18 96x36; pants/19 79x26; pants/19_near 25x41; pants/19_far 25x41; pants/20 79x26; pants/20_near 25x28; pants/20_far 25x28; pants/21 79x26; pants/21_near 25x41; pants/21_far 25x41; pants/22 79x26; pants/22_near 25x41; pants/22_far 25x41; pants/23 79x26; pants/23_near 25x41; pants/23_far 25x41; pants/24 79x26; pants/24_near 25x41; pants/24_far 25x41; pants/25 79x26; pants/25_near 25x41; pants/25_far 25x41; pants/26 82x37; pants/27 79x26; pants/27_near 25x41; pants/27_far 25x41; pants/28 79x26; pants/28_near 25x28; pants/28_far 25x28; pants/29 79x26; pants/29_near 25x28; pants/29_far 25x40; pants/30 79x26; pants/30_near 25x41; pants/30_far 25x41; pants/31 79x26; pants/31_near 25x41; pants/31_far 25x41; pants/32 79x26; pants/32_near 31x28; pants/32_far 31x28; pants/33 79x26; pants/33_near 31x28; pants/33_far 31x28; pants/34 79x26; pants/34_near 31x28; pants/34_far 31x28; pants/35 79x26; pants/35_near 31x28; pants/35_far 31x28; pants/36 92x39; pants/37 79x26; pants/37_near 25x41; pants/37_far 25x41; pants/38 79x26; pants/38_near 25x28; pants/38_far 25x28; pants/39 79x26; pants/39_near 25x28; pants/39_far 25x28; pants/40 79x26; pants/40_near 25x41; pants/40_far 25x41; pants/41 79x26; pants/41_near 25x41; pants/41_far 25x41; pants/42 82x37; pants/43 82x37; pants/44 88x40; pants/45 79x26; pants/45_near 36x42; pants/45_far 38x43; pants/46 79x26; pants/46_near 36x42; pants/46_far 38x43; pants/47 96x36; pants/47_near 25x41; pants/47_far 25x41; pants/48 96x38; pants/49 79x26; pants/50 79x26; pants/50_near 31x28; pants/50_far 31x28; brows/1 51x11; brows/2 65x18; brows/3 61x16; brows/4 66x19; brows/5 72x23; brows/6 44x10; brows/7 62x18; brows/8 57x18; brows/9 53x13; brows/10 17x23; eyes/1 61x20; eyes/2 63x22; eyes/3 66x30; eyes/4 64x31; eyes/5 64x24; eyes/6 63x28; eyes/7 69x29; eyes/8 44x22; eyes/9 48x20; eyes/10 61x23; eyes/11 60x21; eyes/12 65x27; eyes/13 59x21; eyes/14 62x22; eyes/15 63x23; eyes/16 59x26; eyes/17 55x22; eyes/18 57x6; eyes/19 59x23; eyes/20 59x26; glasses/1 85x48; glasses/2 71x30; glasses/3 90x38; glasses/4 96x40; glasses/5 80x44; glasses/6 91x41; glasses/7 80x35; glasses/8 58x27; glasses/9 84x47; glasses/10 81x58; glasses/11 72x21; glasses/12 83x36; glasses/13 81x24; glasses/14 72x37; glasses/15 83x44; glasses/16 86x39; glasses/17 89x58; glasses/18 74x39; glasses/19 89x37; glasses/20 90x54; glasses/21 79x40; glasses/22 71x32; glasses/23 82x38; glasses/24 77x34; glasses/25 73x34; glasses/26 70x33; glasses/27 74x64; glasses/28 86x77; glasses/29 72x41; glasses/30 71x51; offhand/1 101x83; offhand/2 79x87; offhand/3 87x87; offhand/4 96x81; offhand/5 109x87; offhand/6 73x73; offhand/7 91x70; offhand/8 89x89; offhand/9 95x70; offhand/10 85x82; offhand/11_low 56x56; offhand/12_low 56x56; offhand/13_low 65x64; offhand/14_low 61x46; offhand/15_low 78x36; offhand/16_low 65x30; offhand/17_low 55x44; offhand/18_low 37x37; offhand/19_low 46x43; offhand/20_low 43x41; offhand/21_low 56x44; offhand/22_low 56x66; offhand/23_low 49x72; offhand/24_low 56x55; offhand/25_low 77x51; offhand/26_low 94x50; offhand/27_low 78x59; offhand/28_low 63x35; offhand/29_low 78x35; offhand/30_low 46x59; offhand/31_low 53x44; offhand/32_low 57x54; offhand/33_low 65x29; offhand/34_low 92x34; offhand/35_low 54x48; offhand/36_low 83x31; offhand/37_low 51x42; offhand/38_low 65x32; offhand/39_low 53x37; offhand/40_low 53x37; weapon/1 111x77; weapon/2 112x92; weapon/3 115x60; weapon/4 96x78; weapon/5 144x71; weapon/6 135x43; weapon/7 141x40; weapon/10 100x66; weapon/11 101x87; weapon/12 124x58; weapon/13 144x33; weapon/14 131x45; weapon/15 143x60; weapon/16 142x44; weapon/17 133x35; weapon/18 103x65; weapon/19 132x39; weapon/20 133x33; weapon/21 112x29; weapon/22 116x45; weapon/23 88x58; weapon/24 137x28; weapon/25 153x43; weapon/26 160x50; weapon/27 156x51; weapon/28 174x47; weapon/29 160x65; weapon/30 147x40; weapon/31 112x92; weapon/32 115x59; weapon/33 157x51; weapon/34 143x44; weapon/35 143x44; weapon/36 175x47; weapon/37 124x58; weapon/38 135x43; weapon/39 143x60; weapon/40 87x58; gloves/1_near 28x25; gloves/1_far 27x25; gloves/2_near 34x35; gloves/2_far 32x35; gloves/3_near 28x25; gloves/3_far 27x25; gloves/4_near 31x34; gloves/4_far 32x33; gloves/5_near 36x36; gloves/5_far 35x36; gloves/6_near 31x32; gloves/6_far 32x33; gloves/7_near 35x34; gloves/7_far 34x34; gloves/8_near 30x14; gloves/8_far 28x14; gloves/9_near 38x18; gloves/9_far 38x18; gloves/10_near 34x35; gloves/10_far 32x35; gloves/11_near 30x23; gloves/11_far 28x23; gloves/12_near 33x35; gloves/12_far 33x36; gloves/13_near 36x36; gloves/13_far 32x37; gloves/14_near 32x27; gloves/14_far 29x24; gloves/15_near 36x40; gloves/15_far 35x40; gloves/16_near 31x36; gloves/16_far 30x37; gloves/17_near 31x36; gloves/17_far 30x37; gloves/18_near 32x31; gloves/18_far 30x30; gloves/19_near 32x34; gloves/19_far 29x34; gloves/20_near 34x38; gloves/20_far 31x38; gloves/21_near 28x25; gloves/21_far 27x25; gloves/22_near 28x25; gloves/22_far 27x25; gloves/23_near 36x40; gloves/23_far 35x40; gloves/24_near 36x40; gloves/24_far 35x40; gloves/25_near 36x36; gloves/25_far 32x37; gloves/26_near 30x14; gloves/26_far 28x14; gloves/27_near 38x18; gloves/27_far 38x18; gloves/28_near 38x18; gloves/28_far 38x18; gloves/29_near 31x34; gloves/29_far 32x33; gloves/30_near 32x27; gloves/30_far 29x24; hair_under/1 87x54; hair_under/2 111x60; hair_under/3 97x54; hair_under/4 103x55; hair_under/5 101x64; hair_under/6 96x54; hair_under/7 92x52; hair_under/8 123x75; hair_under/9 105x78; hair_under/10 130x72; hair_under/11 97x87; hair_under/12 126x64; hair_under/13 104x40; hair_under/14 96x55; hair_under/15 92x49; hair_under/16 95x55; hair_under/17 102x57; hair_under/18 118x69; hair_under/19 118x88; hair_under/20 96x52; hair_under/21 101x60; hair_under/22 90x53; hair_under/23 94x53; hair_under/24 122x66; hair_under/25 118x67; hair_under/26 98x55; hair_under/27 103x52; hair_under/28 122x79; hair_under/29 99x90; hair_under/30 116x96; hair/1 87x54; hair/2 113x61; hair/3 98x65; hair/4 119x94; hair/5 108x75; hair/6 115x70; hair/7 97x67; hair/8 147x120; hair/9 105x113; hair/10 136x84; hair/11 133x113; hair/12 134x74; hair/13 105x40; hair/14 114x80; hair/15 96x57; hair/16 97x86; hair/17 105x63; hair/18 128x83; hair/19 128x107; hair/20 97x59; hair/20_back 67x91; hair/21 101x65; hair/21_back 96x105; hair/22 114x71; hair/23 124x84; hair/24 127x74; hair/25 135x102; hair/26 103x68; hair/27 103x53; hair/28 125x86; hair/28_back 97x102; hair/29 99x111; hair/29_back 79x112; hair/30 119x114; hat/1 120x110; hat/1_back 95x59; hat/2 154x75; hat/3_low 126x82; hat/4 122x78; hat/5 118x120; hat/6 130x82; hat/7 121x105; hat/7_back 33x53; hat/8_low 136x75; hat/9_low 117x93; hat/10_low 140x94; hat/11_low 114x115; hat/12_low 145x69; hat/13_low 117x131; hat/14_low 140x120; hat/15 145x102; hat/16 129x97; hat/16_back 36x56; hat/17_low 126x84; hat/18 142x93; hat/19_low 122x106; hat/20_low 106x71; hat/20_back 68x44; hat/21_low 117x75; hat/22_low 123x94; hat/22_back 37x46; hat/23 137x110; hat/23_back 134x64; hat/24 133x81; hat/25 126x100; hat/25_back 43x60; hat/26 108x70; hat/26_back 46x52; hat/27_low 133x83; hat/28 126x107; hat/28_back 112x56; hat/29_low 124x117; hat/30_low 117x78; hat/31_low 136x75; hat/32 120x110; hat/32_back 95x59; hat/33_low 145x69; hat/34_low 106x71; hat/34_back 68x44; hat/35_low 106x71; hat/35_back 68x44; hat/36 154x75; hat/37 133x81; hat/38 133x81; hat/39 118x120; hat/40_low 117x75; hat/41_low 117x75; hat/42_low 117x93; hat/43_low 123x94; hat/43_back 37x46; hat/44_low 133x79; hat/45_low 114x115; hat/46_low 114x115; hat/47_low 140x94; hat/48 122x78; hat/49_low 107x70; hat/49_back 46x52; hat/50_low 124x117; hat/51_low 117x131; hat/52 126x107; hat/52_back 112x56; hat/53_low 140x120; hat/54_low 140x120; hat/55 137x110; hat/55_back 134x64; hat/56 137x110; hat/56_back 134x64; hat/57 130x82; hat/58 129x97; hat/58_back 36x56; hat/59 121x105; hat/59_back 33x53; hat/60 145x102; mouth/1 15x7; mouth/2 27x19; mouth/3 23x12; mouth/4 24x11; mouth/5 33x16; mouth/6 20x18; mouth/7 20x19; mouth/8 23x13; mouth/9 24x19; mouth/10 23x15; skin/1_arm_near 23x72; skin/1_arm_far 29x71; skin/1_torso 77x50; skin/1_head 103x90; skin/1_leg 27x55; race_skin/elf_head 103x90; race_skin/dwarf_head 103x90; race_skin/orc_head 103x90; race_skin/undead_head 103x90; ears/elf 103x90; ears/orc 103x90; tusks/1 28x18; tusks/2 28x18; top/1 87x71; top/2 91x88; top/2_sleeve_near 37x34; top/2_sleeve_far 34x35; top/3 90x88; top/3_sleeve_near 31x64; top/3_sleeve_far 32x63; top/4 87x88; top/4_sleeve_near 37x34; top/4_sleeve_far 34x35; top/5 87x81; top/5_sleeve_near 37x34; top/5_sleeve_far 34x35; top/6 88x82; top/6_sleeve_near 29x37; top/6_sleeve_far 29x38; top/7 87x83; top/7_sleeve_near 29x37; top/7_sleeve_far 29x38; top/8 87x80; top/9 87x81; top/9_sleeve_near 28x58; top/9_cap_near 37x34; top/9_sleeve_far 34x59; top/9_cap_far 34x36; top/10 56x59; top/10_coat 88x106; top/10_sleeve_near 28x58; top/10_sleeve_far 32x59; top/11 87x72; top/11_sleeve_near 37x34; top/11_sleeve_far 34x35; top/12 86x81; top/12_sleeve_near 28x58; top/12_sleeve_far 32x59; top/13 88x82; top/13_sleeve_near 28x58; top/13_sleeve_far 32x59; top/14 87x81; top/15 90x88; top/15_sleeve_near 31x64; top/15_sleeve_far 32x63; top/16 86x84; top/16_sleeve_near 31x59; top/16_sleeve_far 34x59; top/17 87x83; top/17_sleeve_near 34x37; top/17_sleeve_far 33x36; top/18 89x74; top/18_sleeve_near 37x34; top/18_sleeve_far 34x35; top/19 89x92; top/19_sleeve_near 37x34; top/19_sleeve_far 34x35; top/20 87x71; top/20_sleeve_near 28x58; top/20_sleeve_far 32x59; top/21 87x71; top/21_sleeve_near 28x58; top/21_sleeve_far 32x59; top/22 87x81; top/22_sleeve_near 28x58; top/22_sleeve_far 32x59; top/23 87x81; top/23_sleeve_near 28x58; top/23_sleeve_far 32x59; top/24 87x81; top/24_sleeve_near 28x58; top/24_sleeve_far 32x59; top/25 88x72; top/25_sleeve_near 28x58; top/25_sleeve_far 32x59; top/26 92x93; top/27 87x82; top/27_sleeve_near 28x58; top/27_sleeve_far 32x59; top/28 86x74; top/29 63x65; top/29_coat 88x106; top/29_sleeve_near 28x58; top/29_sleeve_far 32x59; top/30 87x80; top/30_sleeve_near 28x58; top/30_sleeve_far 32x59; top/31 90x88; top/31_sleeve_near 31x64; top/31_sleeve_far 32x63; top/32 88x82; top/32_sleeve_near 28x58; top/32_sleeve_far 32x59; top/33 87x81; top/33_sleeve_near 37x34; top/33_sleeve_far 34x35; top/34 87x81; top/34_sleeve_near 37x34; top/34_sleeve_far 34x35; top/35 87x81; top/36 87x71; top/36_sleeve_near 28x58; top/36_sleeve_far 32x59; top/37 87x81; top/37_sleeve_near 29x37; top/37_sleeve_far 29x38; top/38 87x82; top/38_sleeve_near 28x58; top/38_sleeve_far 32x59; top/39 87x83; top/39_sleeve_near 34x37; top/39_sleeve_far 33x36; top/40 90x88; top/40_sleeve_near 31x64; top/40_sleeve_far 32x63; top/41 91x87; top/41_sleeve_near 31x64; top/41_sleeve_far 32x63; top/42 87x81; top/42_sleeve_near 28x58; top/42_sleeve_far 32x59; top/43 87x79; top/43_sleeve_near 29x37; top/43_sleeve_far 29x38; top/44 87x72; top/44_sleeve_near 31x64; top/44_sleeve_far 32x63; top/45 87x88; top/45_sleeve_near 28x58; top/45_sleeve_far 32x59; top/46 87x72; top/46_sleeve_near 28x58; top/46_cap_near 37x34; top/46_sleeve_far 32x59; top/46_cap_far 34x35; top/47 88x82; top/47_sleeve_near 29x37; top/47_sleeve_far 29x38; top/48 87x71; top/48_sleeve_near 28x58; top/48_sleeve_far 32x59; top/49 86x81; top/49_sleeve_near 28x58; top/49_sleeve_far 32x59; top/50 89x74; top/50_sleeve_near 37x34; top/50_sleeve_far 34x35; top/51 87x72; top/51_sleeve_near 31x64; top/51_sleeve_far 32x63; top/52 87x72; top/52_sleeve_near 29x37; top/52_sleeve_far 29x38; top/53 87x72; top/53_sleeve_near 29x37; top/53_sleeve_far 29x38; top/54 89x92; top/54_sleeve_near 37x34; top/54_sleeve_far 34x35; top/55 87x81; top/56 86x84; top/56_sleeve_near 31x59; top/56_sleeve_far 34x59; top/57 87x80; top/57_sleeve_near 28x58; top/57_sleeve_far 32x59; top/58 92x93; top/59 90x88; top/59_sleeve_near 31x64; top/59_sleeve_far 32x63; top/60 87x80; weapon/knight_sword 156x51; weapon/dark_blade 160x50; weapon/broadsword 160x65; weapon/flame_sword 174x47; weapon/dagger 112x29; weapon/battle_axe 115x60; weapon/double_axe 112x92; weapon/mace 144x71; weapon/warhammer 103x65; weapon/spear 147x40; weapon/wizard_staff 135x43; weapon/druid_staff 144x33; offhand/kite_shield 85x82; offhand/heater_shield 96x81; offhand/round_shield 89x89; offhand/tower_shield 109x87; offhand/buckler 73x73; hat/great_helm 118x120; hat/knight_helm_low 114x115; hat/winged_helm_low 133x83; hat/wizard_hat 154x75; hat/ranger_hood_low 114x115; hat/iron_helm 130x82; top/plate_armor 87x88; top/plate_armor_sleeve_near 37x34; top/plate_armor_sleeve_far 34x35; top/chainmail 91x88; top/chainmail_sleeve_near 37x34; top/chainmail_sleeve_far 34x35; top/leather_armor 89x92; top/leather_armor_sleeve_near 37x34; top/leather_armor_sleeve_far 34x35; top/mage_robe 89x74; top/mage_robe_sleeve_near 37x34; top/mage_robe_sleeve_far 34x35; gloves/gauntlets_near 34x35; gloves/gauntlets_far 32x35; gloves/bracers_near 36x40; gloves/bracers_far 35x40; boots/plate_boots_near 30x23; boots/plate_boots_far 30x23; boots/leather_boots_near 26x30; boots/leather_boots_far 26x30; back/knight_cape 95x124; back/mage_cape 95x124; weapon/hunting_bow 70x112; weapon/hunting_bow_drawn 70x112; weapon/hunting_bow_shot 70x112; weapon/elven_bow 72x128; weapon/elven_bow_drawn 72x128; weapon/elven_bow_shot 72x128; weapon/pistol 96x40; weapon/pistol_shot 96x40; weapon/crossbow 104x56; weapon/crossbow_shot 104x56; weapon/musket 180x40; weapon/musket_shot 180x40; weapon/rifle 160x52; weapon/rifle_shot 160x52",
9664
9664
  "style": "chibi-2d",
9665
9665
  "asset": {
9666
9666
  "kind": "actor",