@genex-ai/cli-demo 1.5.2-dev.398 → 1.6.0

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 (26) hide show
  1. package/dist/index.js +288 -373
  2. package/package.json +1 -1
  3. package/templates/asset-viewer/asset.config.json +25 -0
  4. package/templates/asset-viewer/genex-asset.example.json +222 -0
  5. package/templates/asset-viewer/index.html +200 -0
  6. package/templates/asset-viewer/package.json +24 -0
  7. package/templates/asset-viewer/public/fonts/Geist-variable.woff2 +0 -0
  8. package/templates/asset-viewer/public/fonts/GeistMono-variable.woff2 +0 -0
  9. package/templates/asset-viewer/shared-files.sha256.json +14 -0
  10. package/templates/asset-viewer/src/asset/PLACEHOLDER.ts +237 -0
  11. package/templates/asset-viewer/src/main.js +149 -0
  12. package/templates/asset-viewer/src/viewer/gates-overlay.js +521 -0
  13. package/templates/asset-viewer/src/viewer/hud.js +73 -0
  14. package/templates/asset-viewer/src/viewer/stage.js +760 -0
  15. package/templates/asset-viewer/tools/emit-manifest.mjs +653 -0
  16. package/templates/asset-viewer/tools/gates.mjs +682 -0
  17. package/templates/asset-viewer/tools/stamp-manifest.mjs +134 -0
  18. package/templates/asset-viewer/vite.config.js +24 -0
  19. package/templates/skills/genex-ai-texture/SKILL.md +1 -1
  20. package/templates/skills/genex-ai-video/SKILL.md +1 -1
  21. package/templates/skills/genex-asset-author/SKILL.md +101 -0
  22. package/templates/skills/genex-game-director/references/routing-map.md +1 -1
  23. package/templates/skills/genex-getting-started/SKILL.md +2 -2
  24. package/templates/skills/genex-threejs-visual-validation/SKILL.md +3 -7
  25. package/templates/skills/genex-updates/SKILL.md +1 -1
  26. package/templates/skills/genex-monetization/SKILL.md +0 -261
@@ -0,0 +1,134 @@
1
+ // Phase 2 of the manifest: rewrite dist/genex-asset.json in place with the
2
+ // measured geometry, the measured space and the gate block, so what ships is
3
+ // what was actually measured rather than what the author claimed.
4
+ //
5
+ // Refuses to stamp a stale report (one taken against different source), and
6
+ // re-validates the finished bytes - a build whose manifest cannot offer the two
7
+ // dashboard buttons must not reach a deploy.
8
+ //
9
+ // exit 0 stamped
10
+ // exit 1 refused (stale, failing, or invalid)
11
+ //
12
+ // Copied verbatim into every asset project and hashed by gate G13.
13
+ //
14
+ // Usage: node tools/stamp-manifest.mjs [--dist dist] [--report .gates/report.json]
15
+ // [--play-url <url>] [--allow-failed]
16
+
17
+ import fs from "node:fs";
18
+ import fsp from "node:fs/promises";
19
+ import path from "node:path";
20
+ import { fileURLToPath } from "node:url";
21
+
22
+ import {
23
+ MANIFEST_FILENAME,
24
+ MANIFEST_MAX_BYTES,
25
+ defaultPlayUrl,
26
+ readAssetConfig,
27
+ validateManifest,
28
+ } from "./emit-manifest.mjs";
29
+
30
+ const PROJECT_DIR = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
31
+
32
+ function parseArgs(argv) {
33
+ const args = { dist: "dist", report: path.join(".gates", "report.json"), playUrl: null, allowFailed: false };
34
+ for (let i = 0; i < argv.length; i += 1) {
35
+ const a = argv[i];
36
+ if (a === "--dist") args.dist = argv[++i];
37
+ else if (a === "--report") args.report = argv[++i];
38
+ else if (a === "--play-url") args.playUrl = argv[++i];
39
+ else if (a === "--allow-failed") args.allowFailed = true;
40
+ else if (a === "--help" || a === "-h") args.help = true;
41
+ else throw new Error(`unknown flag: ${a}`);
42
+ }
43
+ return args;
44
+ }
45
+
46
+ async function main() {
47
+ const args = parseArgs(process.argv.slice(2));
48
+ if (args.help) {
49
+ console.log("usage: node tools/stamp-manifest.mjs [--dist dist] [--report .gates/report.json] [--play-url <url>] [--allow-failed]");
50
+ return 0;
51
+ }
52
+
53
+ const distDir = path.resolve(PROJECT_DIR, args.dist);
54
+ const manifestPath = path.join(distDir, MANIFEST_FILENAME);
55
+ const reportPath = path.resolve(PROJECT_DIR, args.report);
56
+
57
+ if (!fs.existsSync(manifestPath)) throw new Error(`no ${MANIFEST_FILENAME} in ${args.dist} - run \`npm run build\` first`);
58
+ if (!fs.existsSync(reportPath)) throw new Error(`no gate report at ${args.report} - run \`npm run gates\` first`);
59
+
60
+ const config = readAssetConfig(PROJECT_DIR);
61
+ const manifest = JSON.parse(await fsp.readFile(manifestPath, "utf8"));
62
+ const report = JSON.parse(await fsp.readFile(reportPath, "utf8"));
63
+
64
+ // A report taken against different source text describes a different asset.
65
+ if (report.sourceSha256 !== manifest.source.sha256) {
66
+ throw new Error(
67
+ `gate report is stale: it was run against source ${String(report.sourceSha256).slice(0, 12)}… but dist holds ${manifest.source.sha256.slice(0, 12)}… - rebuild and re-run the gates`,
68
+ );
69
+ }
70
+ if (!report.allPassed && !args.allowFailed) {
71
+ const failed = (report.results ?? []).filter((r) => !r.passed).map((r) => r.id);
72
+ throw new Error(`gates failed (${failed.join(", ")}) - fix them, or pass --allow-failed to stamp a build that says so`);
73
+ }
74
+
75
+ const stamped = {
76
+ ...manifest,
77
+ geometry: report.geometry,
78
+ space: {
79
+ ...manifest.space,
80
+ boundingBox: report.space.boundingBox,
81
+ sizeMeters: report.space.sizeMeters,
82
+ statedSizeMeters: config.statedSizeMeters,
83
+ sizeDeltaPct: report.space.sizeDeltaPct,
84
+ },
85
+ gates: {
86
+ runAt: report.runAt,
87
+ runner: report.runner,
88
+ allPassed: Boolean(report.allPassed),
89
+ results: (report.results ?? []).map(({ id, name, threshold, measured, passed }) => ({
90
+ id,
91
+ name,
92
+ threshold,
93
+ measured,
94
+ passed,
95
+ })),
96
+ },
97
+ preview: {
98
+ ...manifest.preview,
99
+ playUrl: args.playUrl ?? manifest.preview?.playUrl ?? defaultPlayUrl(config.slug),
100
+ },
101
+ };
102
+
103
+ const body = JSON.stringify(stamped, null, 2) + "\n";
104
+ const bytes = Buffer.byteLength(body, "utf8");
105
+ const shape = validateManifest(stamped, { bytes });
106
+ if (!shape.ok) throw new Error(`refusing to stamp an invalid manifest:\n - ${shape.errors.join("\n - ")}`);
107
+ if (bytes > MANIFEST_MAX_BYTES) {
108
+ throw new Error(`refusing to stamp: ${bytes} bytes exceeds the ${MANIFEST_MAX_BYTES} byte ceiling`);
109
+ }
110
+
111
+ await fsp.writeFile(manifestPath, body, "utf8");
112
+
113
+ console.log(
114
+ [
115
+ "",
116
+ ` stamped ${path.relative(PROJECT_DIR, manifestPath)} (${(bytes / 1024).toFixed(1)} KB)`,
117
+ ` ${stamped.geometry.triangles.toLocaleString("en-US")} tris · ${stamped.geometry.drawCalls} draws · ${stamped.geometry.materials} materials`,
118
+ ` ${stamped.space.sizeMeters.map((n) => n.toFixed(2)).join(" × ")} m`,
119
+ ` gates: ${stamped.gates.allPassed ? `all ${stamped.gates.results.length} passed` : `${stamped.gates.results.filter((r) => !r.passed).length} FAILED`}`,
120
+ ` preview: ${stamped.preview.playUrl}`,
121
+ "",
122
+ ].join("\n"),
123
+ );
124
+
125
+ return 0;
126
+ }
127
+
128
+ main().then(
129
+ (code) => process.exit(code),
130
+ (error) => {
131
+ console.error(`\n[stamp] ${error?.message ?? error}\n`);
132
+ process.exit(1);
133
+ },
134
+ );
@@ -0,0 +1,24 @@
1
+ // Shared viewer build config. Copied verbatim into every asset project and
2
+ // hashed by gate G13 - edit it in packages/asset-viewer-template/template.
3
+ import { defineConfig } from "vite";
4
+ import { emitManifest } from "./tools/emit-manifest.mjs";
5
+
6
+ export default defineConfig({
7
+ // Games serve at the domain root, but relative keeps the build origin-agnostic.
8
+ base: "./",
9
+ build: {
10
+ // `dist` is the ONLY outDir name the Genex CLI's deployGame recognises;
11
+ // anything else uploads the whole source folder instead of the build.
12
+ outDir: "dist",
13
+ // Fingerprinted chunks opt into the serve worker's immutable cache dir.
14
+ // dist/genex-asset.json deliberately stays outside it, so a republish is
15
+ // always picked up.
16
+ assetsDir: "_immutable",
17
+ emptyOutDir: true,
18
+ target: "es2022",
19
+ },
20
+ server: {
21
+ port: Number(process.env.PORT) || 5190,
22
+ },
23
+ plugins: [emitManifest()],
24
+ });
@@ -225,7 +225,7 @@ first one is the one a screenshot of the whole arena will not show you.
225
225
 
226
226
  ## Troubleshooting
227
227
 
228
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
228
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
229
229
  - **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
230
230
  this texture generation. Tell the user the facts the CLI printed: their balance,
231
231
  this generation's cost, and when their credits refill. Then offer to continue the
@@ -151,7 +151,7 @@ set belongs to `$genex-ai-hud` — both build on `npx genex image`/`video`.
151
151
 
152
152
  ## Troubleshooting
153
153
 
154
- - **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
154
+ - **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
155
155
  - **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
156
156
  This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
157
157
  - **Nothing plays / black surface** — the first `video.play()` must run inside a user
@@ -0,0 +1,101 @@
1
+ ---
2
+ name: genex-asset-author
3
+ description: Author and publish ONE asset to the free Genex asset library via `genex asset new` — STRICTLY user-initiated. Use ONLY when the user explicitly asks to publish something from their game as an asset, or to create a standalone asset for the library. Never suggest, offer, or trigger this lane on your own — building a game never requires it.
4
+ ---
5
+
6
+ # Genex Asset Author
7
+
8
+ Ship one procedural Three.js asset to the free, MIT-licensed Genex asset
9
+ library: a standalone project whose build is a uniform orbitable viewer, whose
10
+ only varying file is ONE self-contained TypeScript factory module, and whose
11
+ quality bar is 17 machine-checked gates.
12
+
13
+ ## When this lane runs — and when it must not
14
+
15
+ Run it ONLY when the user explicitly asks — the two real shapes:
16
+
17
+ - **"Publish this <thing> from my game as an asset"** — extract an object out
18
+ of the game they're building.
19
+ - **"I want to make an asset / assets"** — asset-first authoring.
20
+
21
+ NEVER offer this unprompted. Finishing a game, noticing a nice prop, or
22
+ wanting to showcase work is NOT a trigger; the user's request is. If they ask
23
+ what publishing an asset means, explain (free, MIT, copyable by anyone from the
24
+ dashboard, listed under the gallery's Assets tab) and let them decide.
25
+
26
+ ## The one non-negotiable before publish: MIT with THEIR name
27
+
28
+ A published asset gets a **Copy code** button on its page — anyone can take the
29
+ file. Publishing = releasing it under MIT. Before scaffolding, ask the user for
30
+ their attribution name and pass it as `--holder "<name>"`. If it was skipped,
31
+ set it before publish in BOTH places: `asset.config.json` → `license.holder`,
32
+ and line 2 of the module (the MIT header). Never publish with the placeholder.
33
+
34
+ ## Scaffold
35
+
36
+ ```bash
37
+ npx genex asset new <kebab-slug> --title "<Display Name>" \
38
+ --dims WxDxH --tri-band LOW-HIGH --holder "<user's name>"
39
+ cd <kebab-slug>
40
+ npm install
41
+ npx playwright-core install chromium # once per machine
42
+ ```
43
+
44
+ One asset = one project = one turntable page. Several assets → run the command
45
+ once per asset. `--dims` is width x depth x height in metres as a human states
46
+ it (gate G4 measures the built model against it ±5%); tune it and the config
47
+ honestly to the real object before gating, not to whatever the code produced.
48
+
49
+ The viewer is hash-locked: never edit `index.html`, `src/viewer/*`, `src/main.js`
50
+ or `tools/*` — gate G13 fails the build if you do. Your entire surface is
51
+ `src/asset/<name>.ts` and `asset.config.json`.
52
+
53
+ ## The module contract
54
+
55
+ `export function create<Name>Model(options): THREE.Group` — one self-contained
56
+ file:
57
+
58
+ - Core `three` ONLY. No `three/examples/jsm`, no `ShaderMaterial`, no
59
+ `CanvasTexture` (needs a DOM), no `fetch`, no external URL anywhere in the
60
+ file (gate G11 scans for `http` — comments included).
61
+ - Real metres, Y-up, +Z forward, origin at base centre (y = 0).
62
+ - Seeded determinism: same seed + options → identical vertex data (G10).
63
+ - `userData.dispose()` returns the renderer to baseline (G12) — every
64
+ geometry, material and texture you create is released.
65
+ - Textures are in-code `DataTexture`s; `MeshStandardMaterial` for surfaces.
66
+ - Keep the 4-line header (name / MIT + holder / units / requires) — it is what
67
+ makes a copied file self-describing.
68
+
69
+ Extracting from a game: COPY the code out (the game keeps its own), then strip
70
+ every game dependency, re-origin to base centre, convert to real metres, seed
71
+ the randomness, and add the dispose contract. The result must run in a project
72
+ holding nothing but `three` — that is literally gate G8 (paste-and-run).
73
+
74
+ ## Gate loop
75
+
76
+ ```bash
77
+ npm run verify # vite build + 17 gates + manifest stamp, in one command
78
+ ```
79
+
80
+ Iterate until 17/17. Each failure names its gate and what it measured. The
81
+ gates are the design bar of the Assets tab — never loosen `asset.config.json`
82
+ ceilings to sneak a failing model through; fix the model.
83
+
84
+ Two measured traps: metalness 1.0 needs the viewer's environment map to read
85
+ correctly — it renders BLACK in a game with no envmap, so prefer 0.2-0.9 for
86
+ believable metals; and a mirrored basis can pass every numeric gate while
87
+ being silently inside-out — LOOK at the turntable from several angles before
88
+ calling it done.
89
+
90
+ ## Ship
91
+
92
+ ```bash
93
+ npx genex init # create the project (sign-in if needed)
94
+ npx genex preview --no-build # staging turntable — show the user this link
95
+ npx genex publish --no-build --categories assets # only after the user says ship it
96
+ ```
97
+
98
+ `--no-build` matters: `npm run verify` already built AND stamped the manifest
99
+ into `dist/` — a plain preview/publish would rebuild and ship an unstamped
100
+ manifest, and the dashboard's Copy-code button reads that manifest. Publish
101
+ ONLY on the user's explicit go, with the licence holder set to their real name.
@@ -59,7 +59,6 @@ copy demo architecture.
59
59
  | in-world motion art or another requested video | `$genex-ai-video` |
60
60
  | sound effect, one looping music bed, or a short spoken line | `$genex-ai-sfx`, `$genex-ai-music`, or `$genex-ai-voice` |
61
61
  | requested UI/HUD/menu/interface work, a visible UI problem, or an interface you decided this game wants built with generated art | `$genex-threejs-game-ui` |
62
- | selling anything for platform coin: a shop, an item catalog, boosts, cosmetics, "make it earn"; also any request for a loot box, gacha, wager, casino mechanic or donation prompt, which that skill refuses and replaces | `$genex-monetization` |
63
62
  | cinematic menu/title/pause/victory/defeat/lobby/credits video treatment | `$genex-ai-menu` |
64
63
  | drawn HUD chrome the game's style wants—one element or a matched set of frames, masks, and icons | `$genex-ai-hud` |
65
64
  | the game works but feels flat, floaty, or unresponsive: input response, camera, impacts, cooldowns, difficulty, fail/retry | `$genex-threejs-game-feel` |
@@ -67,6 +66,7 @@ copy demo architecture.
67
66
  | players talking to each other by TEXT — chat, room chat, "let them type to each other" (`room.chat` + `npx genex controller chat`) | `$genex-threejs-multiplayer` |
68
67
  | players TALKING — voice chat, mic, "hear each other", proximity/positional voice (`room.voice` + `npx genex controller voice`; party-sized, max 6 per room — say that number out loud before building the feature) | `$genex-threejs-multiplayer` |
69
68
  | player identity, sign-in, guests, saves/progress, per-player state, shared persistent world, leaderboards—mandatory for every game | `$genex-threejs-embed-auth` |
69
+ | the player EXPLICITLY asks to make an asset or publish something from their game as an asset for the free library | `$genex-asset-author` (`genex asset new`) — never offer this unprompted; no game duty routes here |
70
70
 
71
71
  ## Request-sized execution
72
72
 
@@ -161,7 +161,7 @@ and re-link the clone to the same live game:
161
161
  ```bash
162
162
  git clone <the game's repo url> my-game && cd my-game
163
163
  npm install
164
- npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
164
+ npx @genex-ai/cli-demo@latest link <slug> # slug = the name in the play URL
165
165
  ```
166
166
 
167
167
  Don't know the slug? **`npx genex list`** prints every game on your account —
@@ -188,7 +188,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
188
188
  and your own files are never touched:
189
189
 
190
190
  ```bash
191
- npx @genex-ai/cli-demo@dev init
191
+ npx @genex-ai/cli-demo@latest init
192
192
  ```
193
193
 
194
194
  Use `--force` only if you intentionally want your own existing files overwritten
@@ -134,13 +134,9 @@ everything twice.
134
134
  visible; the lock may only ever engage from the Play/Resume click or a
135
135
  gameplay canvas click (the phase binding `setPaused(phase !== "playing")` is
136
136
  what guarantees this — check it rides `setPhase`, not the render loop).
137
- Headless caveat, measured on Chromium 151: `requestPointerLock` does NOT
138
- throw it locks, with or without a user gesture, so the lock and the
139
- unlocked cue ARE yours to assert headless. What does not survive is the
140
- both-axes look check: synthesised mouse movement cancels to a net zero
141
- delta, so turning right then left proves nothing about direction. Assert the
142
- wiring and the cue in a screenshot, and say plainly that confirming which way
143
- the view turns needs one manual pass with a real mouse.
137
+ Headless caveat: `requestPointerLock` throws in headless Chromium —
138
+ assert the wiring and the unlocked cue in a screenshot, and say plainly that
139
+ the lock itself needs one manual click (do the both-axes look check there).
144
140
  7. **Ask the scene the three things the screenshot cannot answer** (below). Run it
145
141
  once, in the same browser you already have open.
146
142
 
@@ -38,7 +38,7 @@ update, so update immediately.)
38
38
  Run exactly the command the nudge printed, from the game project root:
39
39
 
40
40
  ```bash
41
- npm i -D @genex-ai/cli-demo@dev # the genex CLI (a dev dependency)
41
+ npm i -D @genex-ai/cli-demo@latest # the genex CLI (a dev dependency)
42
42
  npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
43
43
  npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
44
44
  ```
@@ -1,261 +0,0 @@
1
- ---
2
- name: genex-monetization
3
- description: Build an in-game shop that sells for platform coin — item catalog, purchase flow, delivery, and the per-game soft-currency economy a purchase attaches to. Use when the player asks to sell things, add a shop, monetize, or make the game earn. Carries the hard rules: no paid randomness, no gambling in coin, no donation mechanics, and a real-money price beside every coin price.
4
- ---
5
-
6
- # Genex Monetization
7
-
8
- Games on Genex can sell things for **coin**, the platform currency. The player
9
- buys coin with real money once; spending it inside a game is a ledger movement
10
- the game never touches. You design what is for sale; the platform owns the
11
- wallet, the confirmation, and the money.
12
-
13
- Load this when the game should sell something. Ask first if it should — a game
14
- with no loop worth monetizing is better without a shop (see §1).
15
-
16
- ## The hard rules, and the test that generalizes them
17
-
18
- Before you build ANY purchasable thing, run this test:
19
-
20
- > **Does the player pay?** (with coin, or with anything bought with coin —
21
- > directly or indirectly, including a per-game token or key that coin bought.)
22
- > **Is the outcome uncertain when they pay?**
23
- > **Is there a prize** — an item, currency, or advantage they wanted?
24
- >
25
- > **All three yes = paid randomness. Build the deterministic version instead.**
26
-
27
- That triad is the test used by every app store to identify gambling, and it
28
- catches mechanics that do not exist yet — which a list of banned names cannot.
29
-
30
- Four things are never built, whatever the request:
31
-
32
- 1. **No paid randomness.** No loot boxes, gacha, mystery boxes, crates, packs,
33
- prize wheels, raffles, "spin for a bonus", "chance to double your coins".
34
- Directly or indirectly.
35
- 2. **No gambling in coin.** No wagering, staking, betting, coinflips, casino or
36
- slot mechanics denominated in coin or in anything coin buys.
37
- 3. **No donation or begging mechanics.** No "donate to me" prompts, tip jars, or
38
- player-to-player coin transfers. Coin buys goods; it never just moves.
39
- 4. **No pressure.** No countdown timers, "ends in", "limited time", "only N
40
- left", or stock counters anywhere in the shop.
41
-
42
- **Randomness the player EARNS by playing is gameplay, not commerce, and is
43
- completely fine**: an enemy dropping a random item, a chest you found in the
44
- level, a procedural layout, a critical-hit roll, a shuffled deck. The line is
45
- what triggered the roll — play, or payment. Build those freely.
46
-
47
- Genex refuses paid randomness outright rather than allowing it with disclosed
48
- odds. That is stricter than any app store, and it is why no Genex game needs an
49
- odds table, an age gate, or a per-country check.
50
-
51
- ### When a request crosses a line
52
-
53
- Answer in exactly three parts, then build:
54
-
55
- 1. **Name it.** "A loot crate is paid randomness — the player pays before
56
- knowing what they get."
57
- 2. **Why.** One sentence. "Genex doesn't sell chance; it's a purchase the player
58
- can't price, and it's what regulators fine studios over."
59
- 3. **Offer the alternative,** concretely enough to start on, and build that.
60
-
61
- Never build the banned version "as an option", never build a partial one, and
62
- never ask the user to confirm they want it. If they insist, restate the rule
63
- once and build the compliant version. There is no escalation path.
64
-
65
- **What to build instead:**
66
-
67
- | They asked for | Build |
68
- | --- | --- |
69
- | Loot box, crate, mystery box, card pack | A direct-purchase shop: every item listed at a fixed price, contents visible. For the collecting feel, add a **visible catalog with a completion track** — any purchase advances a meter to a stated milestone reward. |
70
- | Gacha, banner, pull, summon | A **deterministic unlock**: the character costs a fixed price, or unlocks at a stated number of runs. Coin may buy a stated, visible number of those points. |
71
- | Prize wheel, spin-to-win, slot machine | A **free spin earned by finishing a run** (never bought), or a **"pick one of three"** screen where all three are visible and the player chooses. Keeps the moment, drops the wager. |
72
- | Casino game, blackjack, poker, roulette | The same game with **chips that are granted free each session, reset on restart, cannot be bought and cannot become coin**. It becomes a card game. Sell cosmetics — table felt, card backs — for coin. |
73
- | Coinflip, double-or-nothing, wager my coins | A **skill-based risk/reward inside the run**: a harder route with a bigger payout, staking the run's own score, which was never purchasable. |
74
- | Betting on matches, PvP wagers | **Leaderboards with a fixed cosmetic reward for placement**, paid by the game. Nobody's balance goes down. |
75
- | Donate button, tip jar, "pls donate" | A **gift that is a purchase**: they buy a specific item at a stated price and give it. Or a **supporter cosmetic** — a badge or aura at a normal price, where what's delivered is visible. |
76
- | Pay to remove a wait / energy gate | **Delete the gate** and sell a permanent upgrade or a cosmetic. Pace with difficulty, not with a timer. |
77
- | Limited-time offer, flash sale | A **permanent tiered ladder** — the value comes from volume, not from a clock. |
78
- | Pay-to-win stat boost in a competitive game | **Cosmetics**, or a boost that only applies in single-player content. |
79
-
80
- ## Designing a shop worth buying from
81
-
82
- Nine checks. Each one is answerable about your actual design.
83
-
84
- 1. **The shop attaches to a progression that already exists.** Name the screen
85
- it opens from and the meter a purchase moves. Build the loop first; a shop in
86
- a game with nothing to want is furniture.
87
- 2. **A boost shortens a grind the player has already felt.** State it in one
88
- sentence: "this skips the ore-gathering they've done four times." If you
89
- can't, it isn't a boost, it's a number.
90
- 3. **Nothing sold invalidates the core loop.** If a paying and a non-paying
91
- player both reach the end, the payer must not have skipped the part that IS
92
- the game.
93
- 4. **No manufactured friction.** If the annoyance wouldn't exist without the
94
- shop, remove the annoyance instead of selling the cure.
95
- 5. **Everything sold is reachable free.** Spending is a shortcut or a
96
- decoration, never the only path.
97
- 6. **Prices land on the grid.** Item prices use 50 / 100 / 200 / 500 / 1000 coin
98
- so every coin pack divides evenly into them and nobody is left holding change
99
- they cannot spend.
100
- 7. **Every price shows real money next to it.** The server sends
101
- `priceDisplayUsdCents` with every item — render it. `250 coins ($2.49)`.
102
- 8. **One currency layer between money and goods.** Coin buys items. A per-game
103
- earned currency buys per-game upgrades. They never convert into each other.
104
- 9. **Purchases never expire and survive a reinstall.** Entitlements live on the
105
- server; the game re-reads them on every boot.
106
-
107
- For a per-game earned currency, the load-bearing number is **minutes of play per
108
- unit earned**. Set it, then price the cheapest meaningful item at one to three
109
- sessions of earning. Everything else follows. Spend sinks come in three kinds —
110
- permanent upgrades, refills, cosmetics — and cosmetics are what absorbs late-game
111
- currency without touching balance.
112
-
113
- ## Stocking the shop
114
-
115
- Items live on the platform, not in the game's code. You create them with the CLI,
116
- and the game names them by id — which is what stops a game inventing its own
117
- items or repricing them.
118
-
119
- ```bash
120
- npx genex shop add "Iron Key" --price 100 --type durable
121
- # → id: sku_a1b2c3 ← what the game passes to buy()
122
-
123
- npx genex shop list # what this game sells, and the valid prices
124
- npx genex shop set sku_a1b2c3 --price 200
125
- npx genex shop remove sku_a1b2c3 # retires it; players who bought it keep it
126
- ```
127
-
128
- `--type consumable` (default) is spent on use; `durable` is owned permanently.
129
-
130
- **Prices come off a fixed grid** — `genex shop list` prints it, and an off-grid
131
- price is refused. The grid exists so every coin pack divides evenly by the
132
- cheapest item, which is what stops a player being left holding change too small
133
- to spend. Pick the nearest grid price rather than working around it.
134
-
135
- Record the ids in `DESIGN.md` next to what each item does. They are the one
136
- thing the game's code cannot regenerate for itself.
137
-
138
- ## The API
139
-
140
- From `@genex-ai/embed-sdk`, already installed. `initEmbed()` must have run.
141
-
142
- ```ts
143
- import { getShop, buy, getEntitlements, consumeEntitlement } from '@genex-ai/embed-sdk';
144
-
145
- const items = await getShop();
146
- // [{ id, type, name, iconUrl, priceCoins, priceDisplayUsdCents }]
147
- ```
148
-
149
- Render `name`, `iconUrl`, `priceCoins` **and** `priceDisplayUsdCents`. Never
150
- hardcode a price: the server charges what its own catalog says, so a hardcoded
151
- number can silently disagree with what the player is charged.
152
-
153
- `getShop()` works for a **guest** and inside a **preview** build, so the shop
154
- window renders for everyone — that is the point of showing it to a signed-out
155
- player at all. Buying is what needs an account.
156
-
157
- **Your game cannot read the player's coin balance, and no HUD should show one.**
158
- The wallet spans every game on the platform, so an untrusted game is not told how
159
- much a player can spend. Show what you *can* know — what they own, from
160
- `getEntitlements()` — and let `buy()` report `insufficient_balance` if it comes
161
- to that.
162
-
163
- ### Buying
164
-
165
- ```ts
166
- buyButton.addEventListener('click', async () => { // must be a real click
167
- const result = await buy({ skuId: item.id });
168
- if (result.status === 'canceled') return; // normal — say nothing
169
- if (result.status !== 'succeeded') {
170
- showMessage(result.message ?? 'That did not go through.');
171
- return;
172
- }
173
- await deliverPending();
174
- });
175
- ```
176
-
177
- **Call `buy()` synchronously from the click handler.** On the game's own origin
178
- the confirmation is a popup, and browsers only allow one while a user gesture is
179
- live — an `await` before it loses the gesture and nothing opens.
180
-
181
- `buy()` resolves when the SERVER says what happened, not when a window closes.
182
- Statuses: `succeeded`, `canceled`, `expired`, `insufficient_balance`, `failed`.
183
-
184
- The player confirms on a Genex-drawn surface — your game does not render the
185
- price sheet, cannot skin it, and cannot complete a purchase itself. That is
186
- deliberate: it is what lets a player trust a purchase in a game they have never
187
- played before.
188
-
189
- ### Delivering
190
-
191
- ```ts
192
- async function deliverPending() {
193
- for (const e of await getEntitlements({ excludeConsumed: true })) {
194
- const { alreadyConsumed } = await consumeEntitlement(e.id);
195
- if (alreadyConsumed) continue; // someone got there first
196
- applyItem(e.skuId); // AFTER the consume
197
- await savePlayerState(currentSave());
198
- }
199
- }
200
- ```
201
-
202
- **Consume first, apply second, and run `deliverPending()` on every boot.**
203
-
204
- That order is not stylistic. If the game dies between consuming and applying,
205
- the player loses one item — a support ticket. If you apply first and die before
206
- consuming, every boot re-delivers it forever — an exploit. Re-listing on boot is
207
- what makes a purchase survive a crash, a refresh, or a closed tab.
208
-
209
- `consumable` items are used up. `durable` items are owned permanently — consume
210
- them once, then record ownership in the player's save.
211
-
212
- ## Checklist
213
-
214
- - [ ] Items exist (`npx genex shop list`) before the shop UI is written
215
- - [ ] The game has a loop and a progression before it has a shop
216
- - [ ] Every item price is on the 50/100/200/500/1000 grid
217
- - [ ] Every price renders `priceDisplayUsdCents` beside the coin figure
218
- - [ ] `buy()` is called synchronously inside a click/tap handler
219
- - [ ] `canceled` is silent; only real failures show a message
220
- - [ ] `deliverPending()` runs on every boot, before the player can act
221
- - [ ] `consumeEntitlement()` is awaited BEFORE the effect is applied
222
- - [ ] `alreadyConsumed: true` skips the effect
223
- - [ ] Durable purchases are written to the player's save
224
- - [ ] No timer, "limited", "ends in", or stock counter anywhere
225
- - [ ] Nothing sold is unreachable without paying
226
- - [ ] No paid randomness, no coin wagering, no donation prompt
227
-
228
- ## Troubleshooting
229
-
230
- **`buy()` returns `failed` with "the confirmation window was blocked"** — `buy()`
231
- was not called inside a user gesture, or an `await` ran before it. Move it to the
232
- first line of the click handler.
233
-
234
- **The purchase succeeded but the player got nothing** — the game applied the
235
- effect without consuming, or never ran `deliverPending()` on boot. The
236
- entitlement is still there; re-list it.
237
-
238
- **The player got the item twice** — the effect was applied before consuming, or
239
- `alreadyConsumed` was ignored. Both are the same bug.
240
-
241
- **`unauthorized` from `getShop()`** — no player identity yet. `initEmbed()` must
242
- have run and `waitForPlayer()` resolved. See `$genex-threejs-embed-auth`.
243
-
244
- **`guest_no_wallet`** — guests play but hold no wallet. Show the shop as
245
- sign-in-to-buy rather than hiding it.
246
-
247
- **`staging_no_purchase`** — a `genex preview` build cannot spend real coin. Test
248
- the shop's layout on staging; test a purchase after `genex promote`.
249
-
250
- **`getShop()` returns nothing** — the game has no items yet. `npx genex shop add
251
- "<name>" --price <coin>` and use the id it prints.
252
-
253
- **`price_off_grid`** — that price isn't on the platform's grid. `npx genex shop
254
- list` prints the valid ones; pick the nearest.
255
-
256
- **Everything coin-related 404s** — in-game purchases aren't enabled on this
257
- environment. Nothing to fix in the game; say so and build the rest.
258
-
259
- **Purchases do nothing in local testing** — local test mode has no wallet and no
260
- server. `buy()` returns `failed` immediately by design. Test purchases on a
261
- preview or published build.