@idosgames/mcp 0.1.38 → 0.1.40

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 (39) hide show
  1. package/dist/cli.js +40 -9
  2. package/package.json +1 -1
  3. package/registry/host.json +2 -2
  4. package/registry/index.json +52 -53
  5. package/registry/modules/ai.json +1 -1
  6. package/registry/modules/board-game.json +1 -1
  7. package/registry/modules/boosts.json +1 -1
  8. package/registry/modules/character.json +1 -1
  9. package/registry/modules/chat.json +1 -1
  10. package/registry/modules/collections.json +1 -1
  11. package/registry/modules/coop-events.json +1 -1
  12. package/registry/modules/craft.json +1 -1
  13. package/registry/modules/deals.json +1 -1
  14. package/registry/modules/events.json +1 -1
  15. package/registry/modules/friends.json +1 -1
  16. package/registry/modules/hero-run.json +19 -20
  17. package/registry/modules/idle-rpg.json +6 -6
  18. package/registry/modules/inventory.json +1 -1
  19. package/registry/modules/invite.json +1 -1
  20. package/registry/modules/leaderboard.json +1 -1
  21. package/registry/modules/lootboxes.json +1 -1
  22. package/registry/modules/mailbox.json +1 -1
  23. package/registry/modules/marketplace.json +1 -1
  24. package/registry/modules/mini-game.json +1 -1
  25. package/registry/modules/multiplayer.json +1 -1
  26. package/registry/modules/premium.json +1 -1
  27. package/registry/modules/profile.json +1 -1
  28. package/registry/modules/pvp.json +1 -1
  29. package/registry/modules/quests.json +1 -1
  30. package/registry/modules/rewards.json +1 -1
  31. package/registry/modules/season.json +1 -1
  32. package/registry/modules/store.json +9 -5
  33. package/registry/modules/tutorials.json +1 -1
  34. package/registry/modules/video-studio.json +41 -41
  35. package/registry/modules/voxelcraft.json +5 -5
  36. package/registry/modules/workshop.json +1 -1
  37. package/registry/skills/hero-run.json +2 -2
  38. package/registry/skills/store-system.json +1 -1
  39. package/registry/skills/video-studio.json +1 -1
@@ -81,17 +81,17 @@
81
81
  "name": "iDos Games",
82
82
  "url": "https://idosgames.com"
83
83
  },
84
- "version": "0.7.2"
84
+ "version": "0.7.3"
85
85
  },
86
86
  "dependencies": {
87
- "@idosgames/characters": "0.1.0",
88
- "@idosgames/core": "0.23.0",
87
+ "@idosgames/characters": "0.1.1",
88
+ "@idosgames/core": "0.24.0",
89
89
  "@idosgames/module-sdk": "0.13.0",
90
90
  "@idosgames/react": "0.6.0",
91
91
  "react": "19.2.7",
92
92
  "three": "0.185.1"
93
93
  },
94
- "contentHash": "42da445bf001d19dd0e96483de896c87ea9069478e87bbd8f8434a81b1cc8f9a",
94
+ "contentHash": "3384674f472d849d5d14fb106332a299ada5b589f1ceb347293a5c3772121d2f",
95
95
  "files": [
96
96
  {
97
97
  "path": "agent.ts",
@@ -2327,7 +2327,7 @@
2327
2327
  },
2328
2328
  {
2329
2329
  "path": "module.meta.json",
2330
- "content": "{\n \"id\": \"voxelcraft\",\n \"type\": \"template\",\n \"uses\": [\"multiplayer\", \"workshop\"],\n \"summary\": \"Minecraft-like voxel survival game: mine, craft, build, farm and fight from the first night to the dragon — with worlds generated from a picture + description (with or without AI).\",\n \"description\": \"A first-person voxel game rendered in Three.js that follows Minecraft's mechanics 1:1 under its own names (every player-facing name lives in one table, so a commercial game can re-skin it; Minecraft names are accepted as aliases). Survival and creative modes: ores and tool tiers, crafting, smelting, armor, hunger, air and drowning, XP, enchanting, anvils, smithing, potions and beacons; farming, animals, taming and riding, fishing, boats and minecarts; villages with trading villagers, raids, and dozens of hostile and neutral mobs. Three dimensions — the overworld (biomes from deserts and jungles to the deep dark, caves, ocean temples, mansions, trial chambers, archaeology), the Inferno (Nether-like: fortresses, its own mobs and biomes) and the Beyond (End-like: citadel portal, the dragon boss, outer islands and cities, wings). Redstone (wire, torches, repeaters, comparators, pistons, observers, hoppers, dispensers, rails), weather, day/night, block light, fluids, achievements, generated music, a chat console with commands, and touch controls. The lobby's 'Worlds' tab turns a reference picture and a text description into a world — locally in the browser (the picture read as a top-down map or as a mood, the text by keywords) or with InApp AI (client.ai, title feature \\\"voxel-world\\\") — via a compact WorldSpec recipe. Worlds autosave to 'My worlds'. The agent sees and drives the game through its debug surface (state, lookingAt, console commands). Covers any 'Minecraft clone', survival / crafting / mining game, block-building sandbox or 'generate a world from a picture' request; by far the largest feature module. Co-op multiplayer for up to 8 players through the lobby's rooms (the multiplayer module, ctx.multiplayer): the lobby's home shows 'With friends' next to the game's 'Play' and a 'Multiplayer' tab, and the pause menu's 'Play with friends' opens 'Create a room' there (public or by invite, room settings 'guest game mode' and 'commands for everyone'); the host's 'Play' takes the whole room into the host's world, late joiners included; in game the host grants operator, kicks and bans. Peer-to-peer over WebRTC (the SDK transport @idosgames/core/realtime, rooms and signaling on the engine's Multiplayer module; TURN relay from the engine when configured): each live dimension is simulated by one player and players may be in different dimensions at once; everyone's own inventory and progress stay theirs; when the host leaves or its tab dies the world moves to another player without stopping. Needs the title's Multiplayer section enabled (Host topology). Characters: the player's own blocky character (third-person view on F5, the first-person arm and an inventory doll from the same recipe) is a CharacterSpec recipe — skeleton template (humanoid, quadruped, winged, blob), boxes on bones, palette and pixel-art faces with expressions — animated by a layered animator (locomotion state graph, actions, emotes, emotions with faces and posture, secondary motion of tails and capes). The lobby's 'Character' and 'Items' tabs turn reference pictures (front, side, back) and a description into a character or an item look — with InApp AI (title features \\\"voxel-character\\\" and \\\"voxel-item\\\") or without it (colours from the picture); item looks re-skin an existing item (stats stay) or are cosmetics worn on the body (hats, capes, wings). Characters also become NPCs and custom mobs with preset behaviours; looks, emotes and emotions sync over the network; characters and items publish to the Workshop. Not included yet: AI dialogue for NPCs and a dedicated server.\",\n \"provides\": [\n \"first-person voxel world: mine, place and break blocks\",\n \"survival and creative modes (health, hunger, air, XP, death and respawn)\",\n \"crafting, smelting, tool and armor tiers\",\n \"enchanting, anvil, smithing, potions, beacon\",\n \"farming, animals, taming and riding, fishing, boats\",\n \"villages, trading villagers, raids\",\n \"dozens of hostile and neutral mobs, bosses (dragon, ash lord)\",\n \"three dimensions: overworld, Inferno (Nether-like), Beyond (End-like)\",\n \"biomes, caves and structures (temples, mansions, fortresses, trial chambers, ruins)\",\n \"redstone: wire, repeaters, comparators, pistons, observers, hoppers, rails and minecarts\",\n \"weather, day/night cycle, block light and fluid simulation\",\n \"achievements and procedurally generated music\",\n \"chat console with commands (/give, /tp, /locate, /summon, …)\",\n \"touch controls for mobile\",\n \"world generation from a picture + description (WorldSpec recipe)\",\n \"AI world generation via client.ai (feature \\\"voxel-world\\\")\",\n \"AI builds objects from pictures out of coloured blocks (a plane, a car… — models of shapes)\",\n \"AI recreates landscapes from pictures (terrain sketch, rivers, biomes)\",\n \"co-op multiplayer up to 8 players (P2P over WebRTC) through the lobby's rooms (ctx.multiplayer), late joiners included\",\n \"open the world to the network: invite code or public list, guest game mode, operators, kick and ban\",\n \"players in different dimensions at once; host migration without stopping the world\",\n \"autosave and 'My worlds' (IndexedDB)\",\n \"player character from a recipe: third-person view (F5), first-person arm and inventory doll\",\n \"character animation: state graph (walk, run, sneak, swim, fly, glide, ride, sleep, death), actions, emotes, emotions with faces\",\n \"AI character from pictures (front/side/back) + description via client.ai (feature \\\"voxel-character\\\"), or colours from a picture without AI\",\n \"AI item looks via client.ai (feature \\\"voxel-item\\\"): new look of an existing item, or cosmetics (hats, capes, wings)\",\n \"NPCs and custom mobs from a character with preset behaviours (npc, passive, neutral, hostile, pet)\",\n \"emote wheel (G), /emote, /feel, chat smileys; synced over the network\",\n \"characters and item looks in the Workshop (voxelcraft.character, voxelcraft.item)\",\n \"quest book (J): a survival line that doubles as the tutorial — chapters, quests with parents, hints that get more precise when the player is stuck, an arrow to the nearest target, recipes as pictures, situational tips; English and Russian\",\n \"pluggable quest sources (QuestSource) — the base for server quests; new task kinds as registry entries\",\n \"lives inside the SDK lobby, Fortnite-style: the player's character on a pedestal in a block diorama on the lobby's home (ctx.lobby hero) with Edit / Emote, the world card and Play\",\n \"lobby tabs on the UI kit next to every system: Worlds (new world from pictures + description, My worlds), Character (studio), Items (looks and wearables), Locker\",\n \"\\\"To the lobby\\\" in the pause menu returns to the SDK lobby; the world chosen there opens on Play\",\n \"own player-facing names for a re-skinnable commercial game (Minecraft names as aliases)\",\n \"3D block rendering (Three.js)\"\n ],\n \"tags\": [\n \"voxel\",\n \"sandbox\",\n \"survival\",\n \"crafting\",\n \"minecraft\",\n \"3d\",\n \"first-person\",\n \"open-world\",\n \"redstone\",\n \"mobile\",\n \"three\",\n \"ai\",\n \"world-generation\",\n \"character\",\n \"avatar\",\n \"animation\",\n \"multiplayer\",\n \"co-op\"\n ],\n \"media\": {\n \"image\": \"https://cloud.idosgames.com/drive/modules/voxelcraft/cover.png\",\n \"video\": \"https://cloud.idosgames.com/drive/modules/voxelcraft/demo.mp4\"\n },\n \"demoUrl\": \"https://cloud.idosgames.com/drive/modules/voxelcraft/demo/\",\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.7.2\"\n}\n"
2330
+ "content": "{\n \"id\": \"voxelcraft\",\n \"type\": \"template\",\n \"uses\": [\"multiplayer\", \"workshop\"],\n \"summary\": \"Minecraft-like voxel survival game: mine, craft, build, farm and fight from the first night to the dragon — with worlds generated from a picture + description (with or without AI).\",\n \"description\": \"A first-person voxel game rendered in Three.js that follows Minecraft's mechanics 1:1 under its own names (every player-facing name lives in one table, so a commercial game can re-skin it; Minecraft names are accepted as aliases). Survival and creative modes: ores and tool tiers, crafting, smelting, armor, hunger, air and drowning, XP, enchanting, anvils, smithing, potions and beacons; farming, animals, taming and riding, fishing, boats and minecarts; villages with trading villagers, raids, and dozens of hostile and neutral mobs. Three dimensions — the overworld (biomes from deserts and jungles to the deep dark, caves, ocean temples, mansions, trial chambers, archaeology), the Inferno (Nether-like: fortresses, its own mobs and biomes) and the Beyond (End-like: citadel portal, the dragon boss, outer islands and cities, wings). Redstone (wire, torches, repeaters, comparators, pistons, observers, hoppers, dispensers, rails), weather, day/night, block light, fluids, achievements, generated music, a chat console with commands, and touch controls. The lobby's 'Worlds' tab turns a reference picture and a text description into a world — locally in the browser (the picture read as a top-down map or as a mood, the text by keywords) or with InApp AI (client.ai, title feature \\\"voxel-world\\\") — via a compact WorldSpec recipe. Worlds autosave to 'My worlds'. The agent sees and drives the game through its debug surface (state, lookingAt, console commands). Covers any 'Minecraft clone', survival / crafting / mining game, block-building sandbox or 'generate a world from a picture' request; by far the largest feature module. Co-op multiplayer for up to 8 players through the lobby's rooms (the multiplayer module, ctx.multiplayer): the lobby's home shows 'With friends' next to the game's 'Play' and a 'Multiplayer' tab, and the pause menu's 'Play with friends' opens 'Create a room' there (public or by invite, room settings 'guest game mode' and 'commands for everyone'); the host's 'Play' takes the whole room into the host's world, late joiners included; in game the host grants operator, kicks and bans. Peer-to-peer over WebRTC (the SDK transport @idosgames/core/realtime, rooms and signaling on the engine's Multiplayer module; TURN relay from the engine when configured): each live dimension is simulated by one player and players may be in different dimensions at once; everyone's own inventory and progress stay theirs; when the host leaves or its tab dies the world moves to another player without stopping. Needs the title's Multiplayer section enabled (Host topology). Characters: the player's own blocky character (third-person view on F5, the first-person arm and an inventory doll from the same recipe) is a CharacterSpec recipe — skeleton template (humanoid, quadruped, winged, blob), boxes on bones, palette and pixel-art faces with expressions — animated by a layered animator (locomotion state graph, actions, emotes, emotions with faces and posture, secondary motion of tails and capes). The lobby's 'Character' and 'Items' tabs turn reference pictures (front, side, back) and a description into a character or an item look — with InApp AI (title features \\\"voxel-character\\\" and \\\"voxel-item\\\") or without it (colours from the picture); item looks re-skin an existing item (stats stay) or are cosmetics worn on the body (hats, capes, wings). Characters also become NPCs and custom mobs with preset behaviours; looks, emotes and emotions sync over the network; characters and items publish to the Workshop. Not included yet: AI dialogue for NPCs and a dedicated server.\",\n \"provides\": [\n \"first-person voxel world: mine, place and break blocks\",\n \"survival and creative modes (health, hunger, air, XP, death and respawn)\",\n \"crafting, smelting, tool and armor tiers\",\n \"enchanting, anvil, smithing, potions, beacon\",\n \"farming, animals, taming and riding, fishing, boats\",\n \"villages, trading villagers, raids\",\n \"dozens of hostile and neutral mobs, bosses (dragon, ash lord)\",\n \"three dimensions: overworld, Inferno (Nether-like), Beyond (End-like)\",\n \"biomes, caves and structures (temples, mansions, fortresses, trial chambers, ruins)\",\n \"redstone: wire, repeaters, comparators, pistons, observers, hoppers, rails and minecarts\",\n \"weather, day/night cycle, block light and fluid simulation\",\n \"achievements and procedurally generated music\",\n \"chat console with commands (/give, /tp, /locate, /summon, …)\",\n \"touch controls for mobile\",\n \"world generation from a picture + description (WorldSpec recipe)\",\n \"AI world generation via client.ai (feature \\\"voxel-world\\\")\",\n \"AI builds objects from pictures out of coloured blocks (a plane, a car… — models of shapes)\",\n \"AI recreates landscapes from pictures (terrain sketch, rivers, biomes)\",\n \"co-op multiplayer up to 8 players (P2P over WebRTC) through the lobby's rooms (ctx.multiplayer), late joiners included\",\n \"open the world to the network: invite code or public list, guest game mode, operators, kick and ban\",\n \"players in different dimensions at once; host migration without stopping the world\",\n \"autosave and 'My worlds' (IndexedDB)\",\n \"player character from a recipe: third-person view (F5), first-person arm and inventory doll\",\n \"character animation: state graph (walk, run, sneak, swim, fly, glide, ride, sleep, death), actions, emotes, emotions with faces\",\n \"AI character from pictures (front/side/back) + description via client.ai (feature \\\"voxel-character\\\"), or colours from a picture without AI\",\n \"AI item looks via client.ai (feature \\\"voxel-item\\\"): new look of an existing item, or cosmetics (hats, capes, wings)\",\n \"NPCs and custom mobs from a character with preset behaviours (npc, passive, neutral, hostile, pet)\",\n \"emote wheel (G), /emote, /feel, chat smileys; synced over the network\",\n \"characters and item looks in the Workshop (voxelcraft.character, voxelcraft.item)\",\n \"quest book (J): a survival line that doubles as the tutorial — chapters, quests with parents, hints that get more precise when the player is stuck, an arrow to the nearest target, recipes as pictures, situational tips; English and Russian\",\n \"pluggable quest sources (QuestSource) — the base for server quests; new task kinds as registry entries\",\n \"lives inside the SDK lobby, Fortnite-style: the player's character on a pedestal in a block diorama on the lobby's home (ctx.lobby hero) with Edit / Emote, the world card and Play\",\n \"lobby tabs on the UI kit next to every system: Worlds (new world from pictures + description, My worlds), Character (studio), Items (looks and wearables), Locker\",\n \"\\\"To the lobby\\\" in the pause menu returns to the SDK lobby; the world chosen there opens on Play\",\n \"own player-facing names for a re-skinnable commercial game (Minecraft names as aliases)\",\n \"3D block rendering (Three.js)\"\n ],\n \"tags\": [\n \"voxel\",\n \"sandbox\",\n \"survival\",\n \"crafting\",\n \"minecraft\",\n \"3d\",\n \"first-person\",\n \"open-world\",\n \"redstone\",\n \"mobile\",\n \"three\",\n \"ai\",\n \"world-generation\",\n \"character\",\n \"avatar\",\n \"animation\",\n \"multiplayer\",\n \"co-op\"\n ],\n \"media\": {\n \"image\": \"https://cloud.idosgames.com/drive/modules/voxelcraft/cover.png\",\n \"video\": \"https://cloud.idosgames.com/drive/modules/voxelcraft/demo.mp4\"\n },\n \"demoUrl\": \"https://cloud.idosgames.com/drive/modules/voxelcraft/demo/\",\n \"author\": {\n \"name\": \"iDos Games\",\n \"url\": \"https://idosgames.com\"\n },\n \"version\": \"0.7.3\"\n}\n"
2331
2331
  }
2332
2332
  ]
2333
2333
  }
@@ -39,7 +39,7 @@
39
39
  "version": "0.1.3"
40
40
  },
41
41
  "dependencies": {
42
- "@idosgames/core": "0.23.0",
42
+ "@idosgames/core": "0.24.0",
43
43
  "@idosgames/module-sdk": "0.13.0",
44
44
  "@idosgames/react": "0.6.0",
45
45
  "react": "19.2.7"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hero-run",
3
- "description": "Hero Run — the roguelike runner game module (Obsidian Knight's runs with Warcraft III's damage): the campaign of islands and levels, the rage and the choice of one of three cards, the 58 upgrade cards (a trigger and an effect, Obsidian Knight's form), the attack types × armor types table and the WC3 armor formula, troops dressed in the gear that gives their types, the hero's types from its gear (item CustomData AttackType / ArmorType), shields as a blue pool, one ultimate, potions, the camp, and the run economy (ENERGY → RUN_TICKET → reward chest through client.currency and client.lootbox). Use it whenever the user wants to add or tune a card, a troop, an island, a boss, the damage table, the energy or the rewards of hero-run — or a roguelike / auto-runner / card-draft / rock-paper-scissors damage game on the iDosGames SDK — even without naming the module.",
4
- "content": "---\nname: hero-run\ndescription: >-\n Hero Run — the roguelike runner game module (Obsidian Knight's runs with\n Warcraft III's damage): the campaign of islands and levels, the rage and the\n choice of one of three cards, the 58 upgrade cards (a trigger and an effect, Obsidian Knight's form), the\n attack types × armor types table and the WC3 armor formula, troops dressed\n in the gear that gives their types, the hero's types from its gear (item\n CustomData AttackType / ArmorType), shields as a blue pool, one ultimate,\n potions, the camp, and the run economy (ENERGY → RUN_TICKET → reward chest\n through client.currency and client.lootbox). Use it whenever the user wants\n to add or tune a card, a troop, an island, a boss, the damage table, the\n energy or the rewards of hero-run — or a roguelike / auto-runner / card-draft\n / rock-paper-scissors damage game on the iDosGames SDK — even without naming\n the module.\n---\n\n# Hero Run\n\nFiles (module `modules/hero-run/src`):\n\n| File | What |\n| ----------------------- | ----------------------------------------------------------------------------------------------------- |\n| `game/damage.ts` | `DAMAGE_TABLE` (7 attack × 6 armor), `armorReduction` / `armorFactor` (WC3), `dealt`, types from gear |\n| `game/units.ts` | `TROOPS`: what each troop wears and holds (→ its types), its numbers; bosses |\n| `game/campaign.ts` | Islands (`troops`, `bosses`, `introduces`, `scouting`), `levelOf(n)` — a level's road, `scout` |\n| `game/cards.ts` | `CARDS` (58): passives (`apply` → `RunMods`) and triggers (`proc`: trigger, chance / every, effect) |\n| `game/battle.ts` | One run: road, segments, rage thresholds, cards pause, camp/fountain/chest, ultimate, potions, end |\n| `game/economy.ts` | Run economy from the title's config: energy → ticket, chests by tier, XP level |\n| `game/session.ts` | `RunSession`: hero, gear types, progress, start (convert), claim (lootbox open) |\n| `game/heroGear.ts` | The hero's attack / armor type and shield from its look and its items' CustomData |\n| `game/enemyLooks.ts` | A troop's look = its gear + a face from its id |\n| `phaser/BattleScene.ts` | Draws the run (no rules); number colour by the table (green — strong, grey — weak) |\n| `ui/*` | Map + scouting (`MapPanel`), run bar, card pick / camp / end popups, HUD with the rage bar |\n\n## Rules that are easy to break\n\n- **The ultimate goes once a run.** Ready from the start, kills do not charge it; only a card brings\n it back, by a chance at an action (`ultOnKill`, `ultOnLevel` → `restoreUlt`, event `ultBack`). Auto\n keeps it for the boss, a crowd or low health (`ultWorthIt`).\n- **No health regen.** Health comes back only from potions, the camp, fountains and the life cards.\n The sheet's eight numbers are Obsidian Knight's: damage, attack speed, crit chance, crit multiplier |\n defense (armor's share off a blow), counter (chance), dodge, lifesteal (chance, `LIFESTEAL_SHARE`).\n A new card effect that is a number goes into `HeroNumber`; any other effect goes into `effectLines`\n (the \"Info\" popup).\n- **The hero's stats are not sold.** Between runs they grow from gear (item levels) and rank on the\n Heroes screen, in a run from the cards. Do not bring back a gold Enhance ladder — it outgrows the\n cards. The demo world's stats have no `PriceOptions`, and the `character` system shows no upgrade for\n a priceless stat. The run's sheet (`Battle.heroNumbers`, `ui/RunStats.tsx`) shows each number as the\n hero came and with the cards.\n\n- **Damage goes through `dealt()` only.** Table first, then the armor value. `spells` ignore the armor\n value. Do not subtract armor flatly.\n- **A troop's types come from its gear** (`troopAttack` / `troopArmor`). Change the gear and the picture\n changes with the types. Name `attackType` / `armorType` on the troop only when the gear would lie\n (bosses — hero / hero, the warlock — chaos).\n- **New types come in island by island** (`introduces`). The first island stays casual: unarmored and\n light troops, normal and pierce attacks, no scouting. A test checks every introduced type is first met\n on its island. Every new type needs a tip text `tip.{kind}.{type}` in `i18n.ts` (EN and RU).\n- **A card is data, in Obsidian Knight's form:** a trigger and an effect, or a passive number.\n - Give it a rarity (common / rare / legendary), `island`, `stacks` and an `icon` (`ui/cardIcons.tsx`).\n - Write the RU and EN text with markup: `**trigger**`, `[g]number[/g]`, `[o]effect[/o]`.\n - A passive number is `apply(m)` changing `RunMods`; a triggered effect is\n `proc: { trigger, chance | every, effect }`.\n - A new effect kind goes into `Effect` in `cards.ts`, `Battle.apply` and, if it flies, `boltFx` in the\n scene.\n - A card with nothing to build on lists it in `needs` (`counter` / `dodge` / `shield`).\n - Keep 40–60 cards — a test checks it, and that every trigger of the reference is there.\n- **The run economy is the server's.** Energy and tickets go through `client.currency.convert`, the reward\n through `client.lootbox.open` with the ticket.\n - Never grant rewards from the client.\n - Lootbox ids: `run_chest_defeat` / `run_chest_victory` / `run_chest_treasure`, priced 1 `RUN_TICKET`.\n - The player's first `FIRST_CHESTS` (3) chests are the starter copies `run_chest_*_first`: the same\n contents plus a sure potion. The engine has no \"first N openings\" rule, so the client counts the\n opened chests in `hero_run_progress.chests`. A title without the `_first` chests opens the usual ones.\n - Conversions: `ENERGY → RUN_TICKET` (Manual rate = 1 / energy per ticket, MinAmount = MaxAmount),\n `POTION → XP`, `GEMS → XP` (camp feast).\n - Without the ENERGY → RUN_TICKET conversion the game plays free.\n- **The demo world is the engine's** (`iDos_Games_Engine/Tests/.../HeroRunStarterFixtures.cs` →\n `IDOS_UPDATE_SEEDS=1 dotnet test --filter StarterSeedExport`). Never edit `title-seed.json` by hand.\n\n## Recipes\n\n- **A new troop.** Add an entry to `TROOPS`, using fantasy-pack parts (`top/plate_armor`,\n `weapon/warhammer`, `offhand/tower_shield` …), then list it in an island's `troops`.\n- **A new island loop step.** Add an entry to `ISLANDS` in `campaign.ts` and a theme in `themes.ts`\n (the backdrop paints it). Add the tips of its `introduces`.\n- **Tuning.** `BASE_HP`, `BASE_ATTACK`, `POWER_GROWTH` (the level curve), `RAGE_THRESHOLDS`,\n `RARITY_WEIGHT`, `SHIELD_SHARE`, `CAMP_*` / `FOUNTAIN_*`, `ULTS`.\n- **Checking the balance.** `game/sim.ts` plays the real `Battle` headless on a title config (a\n `title-seed.json` or the title's sections).\n - `simulateRun` — one run.\n - `simulateCampaign` — a player who runs, opens chests as the server does (`RewardMultiplier`,\n `PityRules`), farms when stuck and upgrades with gold.\n - In the SDK `npm run balance -w @idosgames/mod-hero-run` prints the full report.\n - Rerun it after changing a number above, the chests, the gear prices or the cards. A level that\n takes dozens of tries, or a card far above the others, shows there first.\n- **Testing a run.** Use `new Battle({ level, stats, seed })` in `game.test.ts`. For a hand-made road use\n the test helper `road([...segments])`. Take cards in the loop (`takeFirst`). The stand pane runs at\n about 1 fps — fast-forward with `scene.update(0, 250)` from the console.\n",
3
+ "description": "Hero Run — the roguelike runner game module (Obsidian Knight's runs with attack and armor types): the campaign of islands and levels, the rage and the choice of one of three cards, the 58 upgrade cards (a trigger and an effect, Obsidian Knight's form), the attack types × armor types table and the armor formula, troops dressed in the gear that gives their types, the hero's types from its gear (item CustomData AttackType / ArmorType), shields as a blue pool, one ultimate, potions, the camp, and the run economy (ENERGY → RUN_TICKET → reward chest through client.currency and client.lootbox). Use it whenever the user wants to add or tune a card, a troop, an island, a boss, the damage table, the energy or the rewards of hero-run — or a roguelike / auto-runner / card-draft / rock-paper-scissors damage game on the iDosGames SDK — even without naming the module.",
4
+ "content": "---\nname: hero-run\ndescription: >-\n Hero Run — the roguelike runner game module (Obsidian Knight's runs with\n attack and armor types): the campaign of islands and levels, the rage and the\n choice of one of three cards, the 58 upgrade cards (a trigger and an effect, Obsidian Knight's form), the\n attack types × armor types table and the armor formula, troops dressed\n in the gear that gives their types, the hero's types from its gear (item\n CustomData AttackType / ArmorType), shields as a blue pool, one ultimate,\n potions, the camp, and the run economy (ENERGY → RUN_TICKET → reward chest\n through client.currency and client.lootbox). Use it whenever the user wants\n to add or tune a card, a troop, an island, a boss, the damage table, the\n energy or the rewards of hero-run — or a roguelike / auto-runner / card-draft\n / rock-paper-scissors damage game on the iDosGames SDK — even without naming\n the module.\n---\n\n# Hero Run\n\nFiles (module `modules/hero-run/src`):\n\n| File | What |\n| ----------------------- | ----------------------------------------------------------------------------------------------------- |\n| `game/damage.ts` | `DAMAGE_TABLE` (7 attack × 6 armor), `armorReduction` / `armorFactor`, `dealt`, types from gear |\n| `game/units.ts` | `TROOPS`: what each troop wears and holds (→ its types), its numbers; bosses |\n| `game/campaign.ts` | Islands (`troops`, `bosses`, `introduces`, `scouting`), `levelOf(n)` — a level's road, `scout` |\n| `game/cards.ts` | `CARDS` (58): passives (`apply` → `RunMods`) and triggers (`proc`: trigger, chance / every, effect) |\n| `game/battle.ts` | One run: road, segments, rage thresholds, cards pause, camp/fountain/chest, ultimate, potions, end |\n| `game/economy.ts` | Run economy from the title's config: energy → ticket, chests by tier, XP level |\n| `game/session.ts` | `RunSession`: hero, gear types, progress, start (convert), claim (lootbox open) |\n| `game/heroGear.ts` | The hero's attack / armor type and shield from its look and its items' CustomData |\n| `game/enemyLooks.ts` | A troop's look = its gear + a face from its id |\n| `phaser/BattleScene.ts` | Draws the run (no rules); number colour by the table (green — strong, grey — weak) |\n| `ui/*` | Map + scouting (`MapPanel`), run bar, card pick / camp / end popups, HUD with the rage bar |\n\n## Rules that are easy to break\n\n- **The ultimate goes once a run.** Ready from the start, kills do not charge it; only a card brings\n it back, by a chance at an action (`ultOnKill`, `ultOnLevel` → `restoreUlt`, event `ultBack`). Auto\n keeps it for the boss, a crowd or low health (`ultWorthIt`).\n- **No health regen.** Health comes back only from potions, the camp, fountains and the life cards.\n The sheet's eight numbers are Obsidian Knight's: damage, attack speed, crit chance, crit multiplier |\n defense (armor's share off a blow), counter (chance), dodge, lifesteal (chance, `LIFESTEAL_SHARE`).\n A new card effect that is a number goes into `HeroNumber`; any other effect goes into `effectLines`\n (the \"Info\" popup).\n- **The hero's stats are not sold.** Between runs they grow from gear (item levels) and rank on the\n Heroes screen, in a run from the cards. Do not bring back a gold Enhance ladder — it outgrows the\n cards. The demo world's stats have no `PriceOptions`, and the `character` system shows no upgrade for\n a priceless stat. The run's sheet (`Battle.heroNumbers`, `ui/RunStats.tsx`) shows each number as the\n hero came and with the cards.\n\n- **Damage goes through `dealt()` only.** Table first, then the armor value. `spells` ignore the armor\n value. Do not subtract armor flatly.\n- **A troop's types come from its gear** (`troopAttack` / `troopArmor`). Change the gear and the picture\n changes with the types. Name `attackType` / `armorType` on the troop only when the gear would lie\n (bosses — hero / hero, the warlock — chaos).\n- **New types come in island by island** (`introduces`). The first island stays casual: unarmored and\n light troops, normal and pierce attacks, no scouting. A test checks every introduced type is first met\n on its island. Every new type needs a tip text `tip.{kind}.{type}` in `i18n.ts` (EN and RU).\n- **A card is data, in Obsidian Knight's form:** a trigger and an effect, or a passive number.\n - Give it a rarity (common / rare / legendary), `island`, `stacks` and an `icon` (`ui/cardIcons.tsx`).\n - Write the RU and EN text with markup: `**trigger**`, `[g]number[/g]`, `[o]effect[/o]`.\n - A passive number is `apply(m)` changing `RunMods`; a triggered effect is\n `proc: { trigger, chance | every, effect }`.\n - A new effect kind goes into `Effect` in `cards.ts`, `Battle.apply` and, if it flies, `boltFx` in the\n scene.\n - A card with nothing to build on lists it in `needs` (`counter` / `dodge` / `shield`).\n - Keep 40–60 cards — a test checks it, and that every trigger of the reference is there.\n- **The run economy is the server's.** Energy and tickets go through `client.currency.convert`, the reward\n through `client.lootbox.open` with the ticket.\n - Never grant rewards from the client.\n - Lootbox ids: `run_chest_defeat` / `run_chest_victory` / `run_chest_treasure`, priced 1 `RUN_TICKET`.\n - The player's first `FIRST_CHESTS` (3) chests are the starter copies `run_chest_*_first`: the same\n contents plus a sure potion. The engine has no \"first N openings\" rule, so the client counts the\n opened chests in `hero_run_progress.chests`. A title without the `_first` chests opens the usual ones.\n - Conversions: `ENERGY → RUN_TICKET` (Manual rate = 1 / energy per ticket, MinAmount = MaxAmount),\n `POTION → XP`, `GEMS → XP` (camp feast).\n - Without the ENERGY → RUN_TICKET conversion the game plays free.\n- **The demo world is the engine's** (`iDos_Games_Engine/Tests/.../HeroRunStarterFixtures.cs` →\n `IDOS_UPDATE_SEEDS=1 dotnet test --filter StarterSeedExport`). Never edit `title-seed.json` by hand.\n\n## Recipes\n\n- **A new troop.** Add an entry to `TROOPS`, using fantasy-pack parts (`top/plate_armor`,\n `weapon/warhammer`, `offhand/tower_shield` …), then list it in an island's `troops`.\n- **A new island loop step.** Add an entry to `ISLANDS` in `campaign.ts` and a theme in `themes.ts`\n (the backdrop paints it). Add the tips of its `introduces`.\n- **Tuning.** `BASE_HP`, `BASE_ATTACK`, `POWER_GROWTH` (the level curve), `RAGE_THRESHOLDS`,\n `RARITY_WEIGHT`, `SHIELD_SHARE`, `CAMP_*` / `FOUNTAIN_*`, `ULTS`.\n- **Checking the balance.** `game/sim.ts` plays the real `Battle` headless on a title config (a\n `title-seed.json` or the title's sections).\n - `simulateRun` — one run.\n - `simulateCampaign` — a player who runs, opens chests as the server does (`RewardMultiplier`,\n `PityRules`), farms when stuck and upgrades with gold.\n - In the SDK `npm run balance -w @idosgames/mod-hero-run` prints the full report.\n - Rerun it after changing a number above, the chests, the gear prices or the cards. A level that\n takes dozens of tries, or a card far above the others, shows there first.\n- **Testing a run.** Use `new Battle({ level, stats, seed })` in `game.test.ts`. For a hand-made road use\n the test helper `road([...segments])`. Take cards in the loop (`takeFirst`). The stand pane runs at\n about 1 fps — fast-forward with `scene.update(0, 250)` from the console.\n",
5
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: five 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| **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### ⚠ `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": "---\r\nname: store-system\r\ndescription: >-\r\n Build a store / shop screen in a game on the iDosGames TypeScript SDK\r\n (@idosgames/core) via client.store (StoreService): load the RESOLVED\r\n storefront for the current player (rotating daily shops, sections, slots,\r\n badges, refresh timers, remaining limits), load the offer catalogue, and\r\n purchase one or many offers. Prices can be virtual currency, crypto, a real\r\n money store product, a rewarded-video ad credit, or free. Use this whenever\r\n the user is working in the iDosGames TS SDK or its game templates\r\n (board-game, idle-rpg) and wants a shop screen, a daily rotating shop, gem\r\n packs, free/ad-paid slots, first-purchase badges, purchase-limit UI, or\r\n otherwise touches client.store, StoreService, GetStorefrontResponse,\r\n StoreDefinitions or offer purchasing — even if they don't name the module.\r\n---\r\n\r\n# Store system (iDosGames TS SDK)\r\n\r\nA title sells **offers** (products) placed on **shelves**. The shop is a tree:\r\n\r\n```\r\nStore (storefront)\r\n└── Layout.Sections\r\n └── Slots ← a slot is a PLACE on the shelf\r\n └── the slot decides which offer stands in it\r\n```\r\n\r\n⚠⚠ **A slot is the only address a product can be bought through.** An offer\r\nlives in a flat catalogue (`StoreOffers`) and becomes buyable only by standing\r\nin a slot; a product placed in no slot **cannot be bought at all**. There is no\r\n\"list of stores\" on the product — that field was removed, because it was never\r\na right to buy: the purchase never checked it.\r\n\r\nEverything is **server-authoritative**. The client asks to purchase, the\r\nbackend validates price, windows, gates, limits **and the rotation roll**, then\r\nthe SDK mirrors the confirmed result into a local cache your UI reads.\r\n\r\n## ⚠ Draw `getStorefront()`, not `getDefinitions()`\r\n\r\nThis is the single most important rule of the module.\r\n\r\n`getDefinitions()` returns the **config** — identical for every player.\r\n`getStorefront()` returns the shop **resolved for this player**: rotation\r\nalready rolled, gates applied, badges and refresh timers computed, remaining\r\nlimits filled in. None of that can be derived on the client:\r\n\r\n- **Rotation** is recomputed by the server on every read and stored nowhere. A\r\n client rolling it itself would show two devices two different shops.\r\n- **Counters** (`PurchasedTotal`, remaining caps, cooldown) belong to the\r\n player and are not in the config.\r\n- **Gates** filter storefronts, sections, slots and pool entries per player.\r\n\r\nUse `getDefinitions()` only for catalogue-wide tooling (an admin view, a\r\nsearch). A shop screen never needs it.\r\n\r\n## Prices: six kinds, one shape\r\n\r\nPrice is always a `PriceOptions` dictionary (`Pricing.Options`), the\r\nplatform-wide standard. An option's cost can be:\r\n\r\n| Kind | What it is | On the storefront view |\r\n| --------------------------------------- | ------------------------------------------- | ---------------------- |\r\n| Virtual currency / items / event tokens | ordinary `ResourceConsume` | — |\r\n| Crypto | a crypto currency entry | — |\r\n| Real money | a store product entry; needs a receipt | `IsStorePaid: true` |\r\n| Rewarded video | an ad-credit entry, spent like any resource | `IsAdPaid: true` |\r\n| **Credits** (platform currency) | a `PlatformCredits` entry, `Amount` = price | `IsCreditsPaid`, `CreditsPrice` |\r\n| **Free** | **no cost at all** | `IsFree: true` |\r\n\r\n⚠ **An unset cost means FREE, and that is a legal product** — not an unfilled\r\nprice. A free offer must be limited on some axis (`TotalCap`, `DailyCap`,\r\n`CooldownSeconds`, or the slot's `PerInstanceCap`); without a limit the server\r\nrefuses the purchase, because it would be a button that grants itself forever.\r\n\r\n⚠ Ad-paid offers spend a **credit the player already earned** by watching a\r\nvideo (the Advertising module grants it server-side). Read the balance from\r\n`storefront.AdCreditBalance` to decide whether a card shows \"Watch\" or \"Buy\".\r\n\r\n## Setup\r\n\r\n```ts\r\nimport { createIDosGamesClient } from \"@idosgames/core\";\r\n\r\nconst client = createIDosGamesClient({ titleID: \"your-title-id\" });\r\nawait client.auth.loginWithDeviceID(); // or any auth.* method\r\n\r\nconst store = client.store; // the StoreService\r\n```\r\n\r\nEvery store method requires an authenticated session. Without one they return\r\n`{ ok: false, reason: \"unauthorized\" }` — they do not throw.\r\n\r\n## Methods\r\n\r\nAll methods return `Promise<OperationResult<T>>`: either `{ ok: true, data }`\r\nor `{ ok: false, reason, error }`. Always branch on `result.ok`.\r\n\r\n| Method | Purpose | `data` on success |\r\n| ------------------------------------- | ---------------------------------------- | ----------------------- |\r\n| `getStorefront()` | **The shop as this player sees it now.** | `GetStorefrontResponse` |\r\n| `getDefinitions()` | The raw catalogue (config). | `StoreDefinitions` |\r\n| `getUserState()` | Purchase counters (state). | `UserStoreState` |\r\n| `purchase(offerID, count?, options?)` | Buy `count` (default 1) of one offer. | `StorePurchaseResponse` |\r\n| `purchaseBatch(purchases)` | Buy several offers in one atomic call. | `PurchaseBatchResponse` |\r\n\r\n`options` = `{ selectedOptionID?, payment?, slot? }`.\r\n\r\n`purchase` clamps `count` server-side to **1–100**. `purchaseBatch` takes\r\n`StorePurchaseRef[]`, deduped by `OfferID`, clamped to **50 refs per call**.\r\n\r\n## Drawing a shop screen\r\n\r\n```ts\r\nconst res = await client.store.getStorefront();\r\nif (!res.ok) return showError(res.error);\r\nconst front = res.data;\r\n\r\nfor (const store of front.Stores ?? []) {\r\n for (const section of store.Sections ?? []) {\r\n for (const slot of section.Slots ?? []) {\r\n // ⚠ An EMPTY slot arrives with Offer === null and must still be drawn as\r\n // an empty frame. Skipping it makes the shelf jump around whenever a\r\n // window closes or a gate filters the pool — which is exactly why the\r\n // server sends the slot rather than omitting it.\r\n if (!slot.Offer) {\r\n drawEmptyCard(slot.RefreshesAtUtc); // \"opens in ...\"\r\n continue;\r\n }\r\n\r\n const offer = slot.Offer;\r\n const state = offer.State;\r\n\r\n drawCard({\r\n title: offer.Identity?.DisplayName, // localization key OR a literal\r\n ribbons: offer.Identity?.Tags, // static badges: best_value, x2 ...\r\n // Server-computed badges — do not recompute these:\r\n firstPurchase: state?.IsFirstPurchaseAvailable,\r\n soldOut: state?.SoldOut,\r\n readyAt: state?.AvailableAtUtc, // free-slot cooldown\r\n // Rotation timer: \"Refreshes in 15h 40m\"\r\n refreshesAt: slot.RefreshesAtUtc,\r\n remainingThisWindow: slot.RemainingThisRotation, // null = unlimited\r\n prices: offer.PriceOptions, // already filtered to this platform\r\n });\r\n }\r\n }\r\n}\r\n```\r\n\r\n⚠ **Count timers down from `front.ServerTimeUtc`, not from the device clock.**\r\nThat is what it is for; a player with a skewed clock otherwise sees a shop that\r\nrefreshes at the wrong moment or claims to have already refreshed.\r\n\r\n⚠ **`null` and `0` mean different things in every remaining-count field.**\r\n`null` = \"no limit on this axis\"; `0` = \"spent, sold out\". Treating `null` as\r\nzero grays out every unlimited product on the shelf.\r\n\r\n`client.store.nextRefreshAtUtc()` returns the nearest refresh across all\r\nvisible slots — use it to schedule one reload instead of a polling loop.\r\n\r\n## Purchasing\r\n\r\n```ts\r\nconst res = await client.store.purchase(offer.OfferID, 1, {\r\n selectedOptionID: option.OptionID,\r\n // Pass the slot the player tapped — the storefront gives you all three ids.\r\n slot: {\r\n storeID: store.StoreID,\r\n sectionID: section.SectionID,\r\n slotID: slot.SlotID,\r\n },\r\n});\r\nif (!res.ok) return showError(res.error);\r\n```\r\n\r\nAn option paid in a store needs the receipt as well:\r\n\r\n```ts\r\nawait client.store.purchase(\"gems_l\", 1, {\r\n selectedOptionID: option.OptionID,\r\n payment: { Store: \"GooglePlay\", Receipt: receiptJson, Signature: signature },\r\n});\r\n```\r\n\r\n⚠ **The slot address is optional as a whole** (all three or none): without it\r\nthe server finds the slot itself. The roll is re-checked either way, so a\r\nproduct that rolled for the player nowhere cannot be bought with or without an\r\naddress. Pass it anyway when you have it — for a pooled product it removes the\r\nambiguity of which slot's per-window cap applies.\r\n\r\n### Credits — the platform currency (`IsCreditsPaid`)\r\n\r\n⚠ **Players see this currency as \"Aspis\"** (owner's decision 26.10.2026) — write \"Aspis\" in any player-facing text\r\n(price \"100 Aspis\", \"Not enough Aspis\"). \"Credits\" is only the API name (`PlatformCredits`, `purchaseWithCredits`,\r\n`CreditsError`, `IsCreditsPaid`); do not name a game's own currency \"Coins\" next to it either — players confuse them.\r\n\r\nCredits live on the player's **iDos Games account**, not in the game; the game's author gets 70% of the price, the\r\nplatform 30%. ⚠ The game **cannot charge Credits by itself**: the first call only creates a purchase request, the\r\nplayer confirms it on idosgames.com with their own account, and only then the purchase completes. On idosgames.com's\r\nown game page the SITE shows its confirmation window right over the game — `purchaseWithCredits` handles that by itself\r\nand `onConfirmation` is not called; on the game's own address or `/embed` it falls back to the link. Use the one call\r\nthat does the whole flow:\r\n\r\n```ts\r\nimport { CreditsError } from \"@idosgames/core\";\r\n\r\nconst res = await client.store.purchaseWithCredits(offer.OfferID, 1, {\r\n selectedOptionID: option.OptionID,\r\n slot: { storeID, sectionID, slotID },\r\n // Show a LINK/button to view.Url (target=\"_blank\") — not window.open after an await: browsers block that.\r\n onConfirmation: (view) => showConfirmLink(view.Url),\r\n signal: abort.signal, // stop waiting when the popup closes\r\n});\r\nif (!res.ok && res.error === CreditsError.NotEnough) {\r\n const b = await client.store.getCreditsBalance(); // b.data.TopUpUrl — where to top up\r\n}\r\n```\r\n\r\n- Price the option in Credits in LiveOps → Store (resource picker → **Credits**). Only the Store accepts Credits; one Credits\r\n entry per option, never together with a store product, never as a reward.\r\n- Refusals (`CreditsError`): `CREDITS_LOGIN_REQUIRED` (guest — sign in with iDos Games), `NOT_ENOUGH_CREDITS`,\r\n `CREDITS_CONFIRMATION_DECLINED` / `_EXPIRED`, `CREDITS_BATCH_FORBIDDEN` (no Credits in `purchaseBatch`).\r\n- `getCreditsBalance()` — Credits the player can spend in games (bought + earned; bonus Credits are for platform services).\r\n- On the DEV copy of the title a Credits purchase is a free test: no request, no Credits moved.\r\n\r\n### ⚠ `OFFER_NOT_IN_ROTATION` means \"reload the shop\", not \"show an error\"\r\n\r\nThe rotation roll is recomputed, never stored, so editing a slot's pool\r\nreshuffles the shelf for everyone who has it open. A player who taps a card\r\nthat is no longer in their roll gets exactly this error:\r\n\r\n```ts\r\nimport { OFFER_NOT_IN_ROTATION } from \"@idosgames/core\";\r\n\r\nif (!res.ok && res.error === OFFER_NOT_IN_ROTATION) {\r\n await client.store.getStorefront(); // silently redraw; do not toast\r\n return;\r\n}\r\n```\r\n\r\nAlready-completed purchases are never undone by a reshuffle — counters are\r\naddressed by product and slot, not by the roll.\r\n\r\n## Events\r\n\r\nSubscribe with `client.on(...)`; each returns an unsubscribe fn:\r\n\r\n- `store:storefrontLoaded` → `GetStorefrontResponse`\r\n- `store:definitionsLoaded` → `StoreDefinitions`\r\n- `store:userStateLoaded` → `UserStoreState`\r\n- `store:offerPurchased` → `StorePurchaseResponse`\r\n- `store:offersPurchasedBatch` → `PurchaseBatchResponse`\r\n\r\nThe coarse `user:storeUpdated` (and `user:anyUpdated`) also fire on any store\r\ncache write.\r\n\r\n## Batch purchase\r\n\r\n```ts\r\nconst res = await client.store.purchaseBatch([\r\n { OfferID: \"pack1\", Count: 1 },\r\n { OfferID: \"starter_bundle\", Count: 1 },\r\n]);\r\nif (!res.ok) return showError(res.error);\r\nfor (const item of res.data) {\r\n if (item.Success) applyOk(item.Id);\r\n else showItemError(item.Id, item.Error);\r\n}\r\n```\r\n\r\nBatch results are **partial-aware**: the outer `res.ok` says the call ran, each\r\nelement's `Success`/`Error` says whether that item applied. Items rejected on\r\ntheir own merits (unknown id, outside window, gate failed, limit reached, not\r\nin rotation) are filtered out _before_ the merged charge is built. The\r\nsurvivors are then charged as **one merged, all-or-nothing transaction**.\r\n\r\n⚠ An option paid in a **store** cannot go in a batch — one receipt pays for one\r\npurchase. Ad credits can: a credit is an ordinary resource and two of them add\r\nup like two coins.\r\n\r\n## Gotchas\r\n\r\n- **Guard against double-submit.** Each `purchase()` mints a fresh idempotency\r\n key, so two calls are two real operations — a double-clicked \"Buy\" charges\r\n twice. Disable the control while a call is in flight.\r\n- **The struck-through price is display only.** `CompareAtCost` never reaches\r\n the charge. `DiscountPercent` is computed by the server and is `null` when it\r\n cannot be computed (a mixed bundle has no single percentage) — `null` is not\r\n zero, and printing \"0%\" there is a lie.\r\n- **Prices and rewards can be adjusted server-side.** Premium tiers apply\r\n discounts and bonuses automatically; read what was actually charged and\r\n granted off `res.data.Resources`, not off the displayed base values.\r\n- **`count` scales cost and rewards linearly, then premium is applied once** —\r\n it is not N independent purchases.\r\n- **Only one `Resources` apply per batch call**, but every item's own\r\n `OfferID`/`Count`/`ServerTimeUtc` is correct and drives its counter patch.\r\n- **`DailyPurchases` resets on UTC midnight.** Read `DailyResetUtc` from the\r\n cache after a call rather than computing it.\r\n- **Tags are for the client; player-dependent badges are not.** Anything whose\r\n truth depends on the player — first purchase, sold out, a timer — arrives in\r\n `State`, computed by the server. Do not encode those as tags.\r\n- **`DisplayName` is a localization key OR a literal.** Resolution is\r\n `t(x) = own table → fallback table → x itself`, so a title with literal\r\n names works with no migration. Run these through the localization module.\r\n- **Render from the cache, handle the error from the result.** Use `reason` to\r\n decide behaviour (retry on `\"connection\"`, re-auth on `\"unauthorized\"`, toast\r\n the `error` on `\"server\"`).\r\n\r\n## Full reference\r\n\r\n[references/data-model.md](references/data-model.md) — every config and\r\nstorefront field, the rotation modes, the purchase-limit/reset rule matrix, and\r\nbatch semantics. Read it when building config-driven UI or when an error points\r\nat a rule you need to understand.\r\n",
5
5
  "references": [
6
6
  {
7
7
  "path": "data-model.md",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "video-studio",
3
3
  "description": "The Video studio template (module video-studio) and the two packages it stands on — @idosgames/timeline (a video is data: VideoSpec, beats, formats, captions, sound, a three.js stage, MP4 export on the device) and @idosgames/characters (voxel, 2D rig, GLB and cut-out characters with emotions and lip-sync). Players make short viral videos (dialogue, story, top-5, quiz, what-if) from ready characters and scenes or AI assets, edit them in slots and on tracks, voice them, export MP4, publish and remix in the Workshop. Use this whenever the user works on the video studio, a video/cutscene/animation made of characters, a VideoSpec, beats or formats, captions, the AI director (\"video-director\"), voice-over features, MP4 export, remixes of videos, or adds a character, a scene, a beat or a format — even without naming the module.",
4
- "content": "---\nname: video-studio\ndescription: >-\n The Video studio template (module video-studio) and the two packages it stands\n on — @idosgames/timeline (a video is data: VideoSpec, beats, formats, captions,\n sound, a three.js stage, MP4 export on the device) and @idosgames/characters\n (voxel, 2D rig, GLB and cut-out characters with emotions and lip-sync). Players\n make short viral videos (dialogue, story, top-5, quiz, what-if) from ready\n characters and scenes or AI assets, edit them in slots and on tracks, voice\n them, export MP4, publish and remix in the Workshop. Use this whenever the user\n works on the video studio, a video/cutscene/animation made of characters, a\n VideoSpec, beats or formats, captions, the AI director (\"video-director\"),\n voice-over features, MP4 export, remixes of videos, or adds a character, a\n scene, a beat or a format — even without naming the module.\n---\n\n# Video studio\n\nA video is **data** — a `VideoSpec` of a few kilobytes. The timeline engine plays it on the device,\nthe same frame at the same moment everywhere (preview, MP4). There is NO server render (owner's\ndecision 24.10.2026): the MP4 is made on the device. Money is spent only on AI steps; everything from\nthe library, recombined or the player's own media is free.\n\n```\nidea ─► AI director (title feature \"video-director\", JSON by schema) ─► VideoSpec + plan by the ladder\n ─► estimate in the studio currency (PriceOptions of the title's features) ─► free preview (draft voice)\n ─► edits (slots, tracks → overrides) ─► final voice ─► MP4 / live link / publish ─► Remix\n```\n\n## Where things live\n\n| What | Where |\n| --- | --- |\n| VideoSpec, parse/validate, `remixOf`, `voiceKey`, `linesNeedingVoice` | `@idosgames/timeline` (`spec/`) |\n| Beats (69, data), formats (`dialogue@1`, `story@1`, `top5@1`, `quiz@1`, `whatif@1`), camera moves, captions, SFX, sound plan | `@idosgames/timeline` |\n| `compileVideo(spec, catalog)` → timeline; `frameAt(tl, set, t)` | `@idosgames/timeline` |\n| `Stage` (deterministic frame), `Player` (live, audio clock) | `@idosgames/timeline/three` |\n| `exportMp4(stage)` (WebCodecs + mediabunny), `canExportVideo` | `@idosgames/timeline/export` |\n| Characters: `voxel` (CharacterSpec), `rig2d` (the idle-rpg hero with the whole wardrobe), `gltf` (humanoid-v1), contract (emotions, visemes, poses) | `@idosgames/characters` |\n| Library (actors, scenes, voices), templates, director, voice, AI assets, pricing, store, catalog, screens | `modules/video-studio/src/` |\n| Demo title settings (Frames, AI features with prices, Workshop types, store, quests, VIP, weekly board) | `modules/video-studio/title-seed.json` — GENERATED by the engine's `StarterSeedExportTests`, never edited |\n| AI prompts | `src/ai/prompts.ts` → `ai-prompts.json` (test `prompts.test.ts`; the engine fixture reads it) |\n\nPackages, not module code: the \"package or module\" rule — two catalog modules need the same data\nformat with its runtime (voxelcraft + idle-rpg + video-studio share characters; games may use the\ntimeline for cutscenes). Extend them with DATA (a beat, an asset, a recipe), not by copying.\n\n## Names the studio and the title agree on (`src/ids.ts`)\n\n- AI features (all tagged `hidden` — the AI studio tab does not show them): `video-director` (Text,\n JSON), `video-voice-draft` (free, `audio-cheap`, English voices), `video-voice` (`audio-balanced`,\n Russian too), `video-image`, `video-background`, `voxel-character`, `rig2d-character`, `video-music`,\n `video-clip`. Both voice features have `ShareIdenticalResults` — the same line in the same voice from\n any player of the title is free from the title's cache.\n- Currency `FRAMES`; VIP `studio_vip` (benefits `no_watermark`, `hd_export`, `premium_voices`);\n premium packs = items `pack_fantasy` / `pack_space` (catalog `studio_packs`).\n- Workshop types `video.spec` (a video), `video.asset` (one asset), `video.pack` (a set). Files: `main`\n (JSON) + `media` (what was inline: the player's photo, own voice). Remixes go through\n `ParentContentID` (`Stats.Remixes`, browse `parentContentID`, sort `MostRemixed`).\n- Quest metrics `video_published`, `video_remixed` (client), `hero_created` (server, on completion of\n the character features); weekly leaderboard `weekly_creators` (+1 per published video).\n\n## The ladder of requests (plan items)\n\n`library` (0) → `combine` (recolour, outfit, prop — 0) → `own` (photo, voice — 0) → `ai-asset`\n(voxel-character, rig2d-character, image-actor, background, prop, music) → `ai-clip` (needs its own\nconfirmation). The director always puts the CLOSEST catalog ref into the spec; the studio repairs any\nunknown or locked ref itself (`settle` → `closestAsset`) — never a second paid call for a typo.\n\"Quick\" mode drops the AI items of the plan; \"Unique\" shows them in the estimate.\n\n## Recipes\n\n**A new library character.** Voxel: `voxelHuman(...)` / `voxelCreature(...)` in\n`src/library/actors.ts` (height in scene units next to a 1.7 human). 2D: a `HERO_PRESETS` look of\n`@idosgames/characters/rig2d`. Give `tags` in both languages and `affordances` — the director finds\nassets by them. Premium → `tier: \"premium\"` (opened by its pack item, see `model/access.ts`).\n\n**A new scene.** `src/library/sets.ts`: background gradient/picture, ground, props as boxes; anchors\nand shots are shared (`ANCHORS`, `SHOTS`). Shots are framed for a 1.7 hero; the stage refits a\nsingle-actor shot to the actor's height and follows it off its anchor.\n\n**A new beat.** Data in `@idosgames/timeline/src/beats/library.ts`\n(`beat(id, title, duration, roles, tags, tracks)`), ids are `beat:<name>` (specs may omit the prefix).\nThe director prompt lists beats from this registry — regenerate `ai-prompts.json`.\n\n**A new format.** `@idosgames/timeline/src/formats/<name>.ts` with an `@1` id, a params JSON schema and\na `directorHint`; register it in `formats/index.ts`. Never change a published `@1` — add `@2`.\n\n**Ready videos without AI.** `src/model/templates.ts` — every format has one, in en/ru; they compile in\ntests (`studio.test.ts`). They are the free one-tap start and the agent's demo.\n\n## Agent actions (`ctx.exposeToAgent`)\n\n`openTrends()`, `openCreate({ format? })`, `newFromTemplate({ id })` (free, no AI), `watch({ contentID })`.\nState: the current view and the template ids. The DOM of the studio is readable directly.\n\n## Traps\n\n- MP4 needs the final voice when the title has the voice feature; without the feature a video exports\n with captions only. Russian drafts are read by the browser (Web Speech) and never reach the file.\n- `captureFrame` / export create their own WebGL context — release it (`forceContextLoss`), a tab has ~16.\n- A video saved from someone else's publication keeps its media as `data:` URLs (not `blob:`) — it must\n survive a reload in \"My videos\".\n- Live link: `#video=<contentID>` opens the video for watching; the title is never taken from a link.\n- A video PACKAGE travels in a link too: `#videodata=v1.<base64url(deflate-raw(JSON {v, spec, assets}))>`\n (`encodeVideoPackage` / `decodeVideoPackage` / `readVideoPackage` in `@idosgames/timeline`). The studio\n opens it as a new video in the editor (its assets go to \"My assets\", origin `link`) and clears the hash.\n ⚠ The site backend encodes the same format (`McpVideoTools.EncodePackage`) — both sides pin each\n other's string in their tests; change one, change both.\n\n## Agents (MCP)\n\n- Platform MCP (`site.idosgames.com/api/v2/mcp`, publisher's credits, right `ai:generate`):\n `plan_video` (the director: the prompt, template and schema come from `video.json` of the registry —\n `src/catalog.test.ts` exports them; the server repairs refs like `settle`) and `generate_asset` (the\n table `src/ai/generators.ts`: prompt, model settings and the asset shape with `$url` / `$out` /\n `$out.<field>` — the studio's `generateAsset` reads the same table). Both return the package and the\n `#videodata=` link of the title. `publish_asset` (per title, `config:write`) puts assets into the app's\n Workshop as official content (`video.asset` / `video.pack`, the `packAssets` file, the cover from the\n publisher's own generation): the site forwards the AGENT'S token to the engine, which lets it in only on\n the official Workshop actions (`AdminActionPolicy.AgentScope`). Free reads: `list_video_formats`,\n `get_video_format`, `search_assets`, `list_video_templates`.\n- SDK MCP (`npx @idosgames/mcp`, free, offline): `validate_asset`, `validate_video` — the timeline code\n bundled into the server.\n- A new AI asset kind = a row in `ASSET_GENERATORS` + its branch in `generateAsset`; the server picks it\n up from the registry without a deploy (if its answer fits the placeholders).\n",
4
+ "content": "---\nname: video-studio\ndescription: >-\n The Video studio template (module video-studio) and the two packages it stands\n on — @idosgames/timeline (a video is data: VideoSpec, beats, formats, captions,\n sound, a three.js stage, MP4 export on the device) and @idosgames/characters\n (voxel, 2D rig, GLB and cut-out characters with emotions and lip-sync). Players\n make short viral videos (dialogue, story, top-5, quiz, what-if) from ready\n characters and scenes or AI assets, edit them in slots and on tracks, voice\n them, export MP4, publish and remix in the Workshop. Use this whenever the user\n works on the video studio, a video/cutscene/animation made of characters, a\n VideoSpec, beats or formats, captions, the AI director (\"video-director\"),\n voice-over features, MP4 export, remixes of videos, or adds a character, a\n scene, a beat or a format — even without naming the module.\n---\n\n# Video studio\n\nA video is **data** — a `VideoSpec` of a few kilobytes. The timeline engine plays it on the device,\nthe same frame at the same moment everywhere (preview, MP4). There is NO server render (owner's\ndecision 24.10.2026): the MP4 is made on the device. Money is spent only on AI steps; everything from\nthe library, recombined or the player's own media is free.\n\n```\nidea ─► AI director (title feature \"video-director\", JSON by schema) ─► VideoSpec + plan by the ladder\n ─► estimate in the studio currency (PriceOptions of the title's features) ─► free preview (draft voice)\n ─► edits (slots, tracks → overrides) ─► final voice ─► MP4 / live link / publish ─► Remix\n```\n\n## Where things live\n\n| What | Where |\n| --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |\n| VideoSpec, parse/validate, `remixOf`, `voiceKey`, `linesNeedingVoice` | `@idosgames/timeline` (`spec/`) |\n| Beats (69, data), formats (`dialogue@1`, `story@1`, `top5@1`, `quiz@1`, `whatif@1`), camera moves, captions, SFX, sound plan | `@idosgames/timeline` |\n| `compileVideo(spec, catalog)` → timeline; `frameAt(tl, set, t)` | `@idosgames/timeline` |\n| `Stage` (deterministic frame), `Player` (live, audio clock) | `@idosgames/timeline/three` |\n| `exportMp4(stage)` (WebCodecs + mediabunny), `canExportVideo` | `@idosgames/timeline/export` |\n| Characters: `voxel` (CharacterSpec), `rig2d` (the idle-rpg hero with the whole wardrobe), `gltf` (humanoid-v1), contract (emotions, visemes, poses) | `@idosgames/characters` |\n| Library (actors, scenes, voices), templates, director, voice, AI assets, pricing, store, catalog, screens | `modules/video-studio/src/` |\n| Demo title settings (Frames, AI features with prices, Workshop types, store, quests, VIP, weekly board) | `modules/video-studio/title-seed.json` — GENERATED by the engine's `StarterSeedExportTests`, never edited |\n| AI prompts | `src/ai/prompts.ts` → `ai-prompts.json` (test `prompts.test.ts`; the engine fixture reads it) |\n\nPackages, not module code: the \"package or module\" rule — two catalog modules need the same data\nformat with its runtime (voxelcraft + idle-rpg + video-studio share characters; games may use the\ntimeline for cutscenes). Extend them with DATA (a beat, an asset, a recipe), not by copying.\n\n## Names the studio and the title agree on (`src/ids.ts`)\n\n- AI features (all tagged `hidden` — the AI studio tab does not show them): `video-director` (Text,\n JSON), `video-voice-draft` (free, `audio-cheap`, English voices), `video-voice` (`audio-balanced`,\n Russian too), `video-image`, `video-background`, `voxel-character`, `rig2d-character`, `video-music`,\n `video-clip`. Both voice features have `ShareIdenticalResults` — the same line in the same voice from\n any player of the title is free from the title's cache.\n- Currency `FRAMES`; VIP `studio_vip` (benefits `no_watermark`, `hd_export`, `premium_voices`);\n premium packs = items `pack_fantasy` / `pack_space` (catalog `studio_packs`).\n- Workshop types `video.spec` (a video), `video.asset` (one asset), `video.pack` (a set). Files: `main`\n (JSON) + `media` (what was inline: the player's photo, own voice). Remixes go through\n `ParentContentID` (`Stats.Remixes`, browse `parentContentID`, sort `MostRemixed`).\n- Quest metrics `video_published`, `video_remixed` (client), `hero_created` (server, on completion of\n the character features); weekly leaderboard `weekly_creators` (+1 per published video).\n\n## The ladder of requests (plan items)\n\n`library` (0) → `combine` (recolour, outfit, prop — 0) → `own` (photo, voice — 0) → `ai-asset`\n(voxel-character, rig2d-character, image-actor, background, prop, music) → `ai-clip` (needs its own\nconfirmation). The director always puts the CLOSEST catalog ref into the spec; the studio repairs any\nunknown or locked ref itself (`settle` → `closestAsset`) — never a second paid call for a typo.\n\"Quick\" mode drops the AI items of the plan; \"Unique\" shows them in the estimate.\n\n## Recipes\n\n**A new library character.** Voxel: `voxelHuman(...)` / `voxelCreature(...)` in\n`src/library/actors.ts` (height in scene units next to a 1.7 human). 2D: a `HERO_PRESETS` look of\n`@idosgames/characters/rig2d`. Give `tags` in both languages and `affordances` — the director finds\nassets by them. Premium → `tier: \"premium\"` (opened by its pack item, see `model/access.ts`).\n\n**A new scene.** `src/library/sets.ts`: background gradient/picture, ground, props as boxes; anchors\nand shots are shared (`ANCHORS`, `SHOTS`). Shots are framed for a 1.7 hero; the stage refits a\nsingle-actor shot to the actor's height and follows it off its anchor.\n\n**A new beat.** Data in `@idosgames/timeline/src/beats/library.ts`\n(`beat(id, title, duration, roles, tags, tracks)`), ids are `beat:<name>` (specs may omit the prefix).\nThe director prompt lists beats from this registry — regenerate `ai-prompts.json`.\n\n**A new format.** `@idosgames/timeline/src/formats/<name>.ts` with an `@1` id, a params JSON schema and\na `directorHint`; register it in `formats/index.ts`. Never change a published `@1` — add `@2`.\n\n**Ready videos without AI.** `src/model/templates.ts` — every format has one, in en/ru; they compile in\ntests (`studio.test.ts`). They are the free one-tap start and the agent's demo.\n\n## Agent actions (`ctx.exposeToAgent`)\n\n`openTrends()`, `openCreate({ format? })`, `newFromTemplate({ id })` (free, no AI), `watch({ contentID })`.\nState: the current view and the template ids. The DOM of the studio is readable directly.\n\n## Traps\n\n- MP4 needs the final voice when the title has the voice feature; without the feature a video exports\n with captions only. Russian drafts are read by the browser (Web Speech) and never reach the file.\n- `captureFrame` / export create their own WebGL context — release it (`forceContextLoss`), a tab has ~16.\n- A video saved from someone else's publication keeps its media as `data:` URLs (not `blob:`) — it must\n survive a reload in \"My videos\".\n- Live link: `#video=<contentID>` opens the video for watching; the title is never taken from a link.\n- A video PACKAGE travels in a link too: `#videodata=v1.<base64url(deflate-raw(JSON {v, spec, assets}))>`\n (`encodeVideoPackage` / `decodeVideoPackage` / `readVideoPackage` in `@idosgames/timeline`). The studio\n opens it as a new video in the editor (its assets go to \"My assets\", origin `link`) and clears the hash.\n ⚠ The site backend encodes the same format (`McpVideoTools.EncodePackage`) — both sides pin each\n other's string in their tests; change one, change both.\n\n## Agents (MCP)\n\n- Platform MCP (`site.idosgames.com/api/v2/mcp`, publisher's credits, right `ai:generate`):\n `plan_video` (the director: the prompt, template and schema come from `video.json` of the registry —\n `src/catalog.test.ts` exports them; the server repairs refs like `settle`) and `generate_asset` (the\n table `src/ai/generators.ts`: prompt, model settings and the asset shape with `$url` / `$out` /\n `$out.<field>` — the studio's `generateAsset` reads the same table). Both return the package and the\n `#videodata=` link of the title. `publish_asset` (per title, `config:write`) puts assets into the app's\n Workshop as official content (`video.asset` / `video.pack`, the `packAssets` file, the cover from the\n publisher's own generation): the site forwards the AGENT'S token to the engine, which lets it in only on\n the official Workshop actions (`AdminActionPolicy.AgentScope`). Free reads: `list_video_formats`,\n `get_video_format`, `search_assets`, `list_video_templates`.\n- SDK MCP (`npx @idosgames/mcp`, free, offline): `validate_asset`, `validate_video` — the timeline code\n bundled into the server.\n- A new AI asset kind = a row in `ASSET_GENERATORS` + its branch in `generateAsset`; the server picks it\n up from the registry without a deploy (if its answer fits the placeholders).\n",
5
5
  "references": []
6
6
  }