@genex-ai/cli-demo 1.36.5-dev.858 → 1.36.6-dev.861
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +890 -397
- package/package.json +1 -1
- package/templates/controllers/character/aim-cue.ts +2 -1
- package/templates/controllers/character/follow-camera.ts +4 -3
- package/templates/skills/genex-ai-video/SKILL.md +8 -3
- package/templates/skills/genex-game-director/SKILL.md +1 -0
- package/templates/skills/genex-llm-in-games/SKILL.md +9 -0
- package/templates/skills/genex-monetization/SKILL.md +26 -0
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +3 -3
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +2 -2
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +158 -36
- package/templates/skills/genex-threejs-embed-auth/references/shells.md +207 -0
- package/templates/skills/genex-threejs-game-ui/SKILL.md +22 -14
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +46 -8
- package/templates/skills/genex-threejs-touch-controls/SKILL.md +63 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "1.36.
|
|
3
|
+
"version": "1.36.6-dev.861",
|
|
4
4
|
"description": "Set up your project's agent workspace (.claude/.codex/.cursor in the game folder), authorize, create a game project, generate AI assets, and publish (genex CLI).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
// - "unlocked" → a bottom-center pill, "Click to aim — Esc pauses". Also shown on
|
|
13
13
|
// the "needs-gesture" re-emit (a re-lock that needs a fresh click).
|
|
14
14
|
// - "unavailable" → the same pill, "Drag to look" (lock was refused, e.g. a
|
|
15
|
-
//
|
|
15
|
+
// sandboxed embed without the allow-pointer-lock sandbox token —
|
|
16
|
+
// drag still works).
|
|
16
17
|
// - "off" / "paused" → nothing (aim disabled, or a menu owns the cursor).
|
|
17
18
|
//
|
|
18
19
|
// Games that want a custom HUD can skip this and read `onAimChange` directly.
|
|
@@ -116,7 +116,7 @@ export type FollowCameraAimState =
|
|
|
116
116
|
| "unlocked" // aim available, waiting for a click
|
|
117
117
|
| "locked" // pointer locked, mouse drives the view
|
|
118
118
|
| "paused" // suspended by the game (menu open / driving) — clicks don't re-lock
|
|
119
|
-
| "unavailable"; // lock permanently rejected (e.g.
|
|
119
|
+
| "unavailable"; // lock permanently rejected (e.g. a sandboxed embed without the allow-pointer-lock sandbox token) → drag-orbit fallback
|
|
120
120
|
|
|
121
121
|
/**
|
|
122
122
|
* Payload for {@link FollowCameraOptions.onAimChange}.
|
|
@@ -843,8 +843,9 @@ export class FollowCamera {
|
|
|
843
843
|
* - No usable user gesture (a resume outside a click, or Chrome's post-Esc
|
|
844
844
|
* cooldown): skip the doomed request, keep the "click to aim" cue up (a
|
|
845
845
|
* same-state "needs-gesture" re-emit), and arm a keydown retry.
|
|
846
|
-
* - SecurityError (permission denied — e.g.
|
|
847
|
-
* allow
|
|
846
|
+
* - SecurityError (permission denied — e.g. a sandboxed embed without the
|
|
847
|
+
* allow-pointer-lock sandbox token): permanent, fall back to drag-orbit
|
|
848
|
+
* ("unavailable").
|
|
848
849
|
* - NotSupportedError from `unadjustedMovement`: retry the plain request (still
|
|
849
850
|
* inside the transient-activation window, so no new gesture is needed).
|
|
850
851
|
* - Any other rejection: transient — treat like the missing-gesture case.
|
|
@@ -78,10 +78,15 @@ const screen = new THREE.Mesh(
|
|
|
78
78
|
);
|
|
79
79
|
scene.add(screen);
|
|
80
80
|
|
|
81
|
-
// start playback on
|
|
82
|
-
|
|
81
|
+
// start playback on the first input (never swallow the rejection). Both
|
|
82
|
+
// events: a click or tap is the first input a game usually gets, not a key.
|
|
83
|
+
const start = (): void => {
|
|
84
|
+
window.removeEventListener("pointerdown", start);
|
|
85
|
+
window.removeEventListener("keydown", start);
|
|
83
86
|
video.play().catch((e) => console.warn("video play", e));
|
|
84
|
-
}
|
|
87
|
+
};
|
|
88
|
+
window.addEventListener("pointerdown", start);
|
|
89
|
+
window.addEventListener("keydown", start);
|
|
85
90
|
```
|
|
86
91
|
|
|
87
92
|
One `VideoTexture` can feed **many** surfaces — a bank of monitors, N sprays. Cost is
|
|
@@ -165,6 +165,7 @@ vendored code from memory of another engine.
|
|
|
165
165
|
| 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` |
|
|
166
166
|
| 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` |
|
|
167
167
|
| player identity, sign-in, guests, saves/progress, per-player state, shared persistent world, leaderboards—mandatory for every game | `$genex-threejs-embed-auth` |
|
|
168
|
+
| anything the game asks the BROWSER or the DEVICE for: share, an invite link, copy to clipboard, open a link, save or download a file, fullscreen, tilt or gyro, vibration, microphone / camera / location, keep the screen on, its own `?params` or `#hash`, a browser dialog (`alert` / `confirm` / `prompt`). What works differs between the game's Genex page, the Genex app and the game's own link, usually with no error — find the row before the first call | `$genex-threejs-embed-auth` (`references/shells.md`) |
|
|
168
169
|
| 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 |
|
|
169
170
|
|
|
170
171
|
Request-shape rules, working modes and the protected-lane handoffs stay in the
|
|
@@ -140,6 +140,14 @@ handler, before any `await`.** The approval popup is reserved synchronously off
|
|
|
140
140
|
the gesture; an `await` in front of it loses the gesture and nothing opens.
|
|
141
141
|
`generate({ grantId })` needs no gesture at all — that is what a grant buys.
|
|
142
142
|
|
|
143
|
+
**Two shells have no approval surface, and say so before anything is asked or
|
|
144
|
+
charged.** Inside the Genex app every call on this lane resolves
|
|
145
|
+
`{ status: 'failed', error: 'native_unsupported' }`; on a dev server in local
|
|
146
|
+
test mode it is `local_test_unsupported`. Neither is a fault in the game. When
|
|
147
|
+
`getShell()` from `@genex-ai/embed-sdk` is `"app"`, leave the feature's entry
|
|
148
|
+
point out and play the authored fallback; never show it as an error. Which
|
|
149
|
+
shell draws what is in `$genex-threejs-embed-auth` (`references/shells.md`).
|
|
150
|
+
|
|
143
151
|
## `maxCredits` is a ceiling, not a price
|
|
144
152
|
|
|
145
153
|
The player is billed **as used**: the call's real provider cost plus the
|
|
@@ -389,6 +397,7 @@ These are source contracts, not a claim that every stand runs this lane —
|
|
|
389
397
|
- [ ] Disclosure numbers derive from the real loop and the bench's measured average, and are written in `DESIGN.md`
|
|
390
398
|
- [ ] Calls are batched and cached; nothing fires on an invisible timer
|
|
391
399
|
- [ ] Every grant-ending code has in-fiction copy and a playable fallback
|
|
400
|
+
- [ ] In the Genex app (`getShell() === "app"`) the feature's entry point is left out and the authored fallback plays
|
|
392
401
|
- [ ] The in-game readout comes from `getSpendGrant()`
|
|
393
402
|
- [ ] Exactly one `applyGeneration()` writer; boot re-attaches with `waitForGeneration()`
|
|
394
403
|
- [ ] `generationId` is saved BEFORE the await
|
|
@@ -234,6 +234,22 @@ price sheet, cannot skin it, and cannot complete a purchase itself. That is
|
|
|
234
234
|
deliberate: it is what lets a player trust a purchase in a game they have never
|
|
235
235
|
played before.
|
|
236
236
|
|
|
237
|
+
Where that surface is depends on the shell the game is in
|
|
238
|
+
(`$genex-threejs-embed-auth`, `references/shells.md`), and two of them change
|
|
239
|
+
what the game does:
|
|
240
|
+
|
|
241
|
+
- **On the game's Genex page the sheet is drawn by the page, above the game.**
|
|
242
|
+
To show it, the page leaves fullscreen first, and the SDK lets go of the
|
|
243
|
+
pointer lock before it asks (`@genex-ai/embed-sdk` 0.32.0+; a build on an
|
|
244
|
+
older SDK keeps the cursor captured until the player presses Escape). Treat
|
|
245
|
+
a purchase like a pause: re-enter fullscreen and re-lock the pointer from
|
|
246
|
+
the player's next Resume click, never by yourself.
|
|
247
|
+
- **The Genex app has no purchase surface yet.** There `buy()` resolves
|
|
248
|
+
`failed` without opening anything. That is the app, not an error: when
|
|
249
|
+
`getShell()` from `@genex-ai/embed-sdk` is `"app"`, show the shop as
|
|
250
|
+
browse-only or leave the buy buttons out, and never show a failure message
|
|
251
|
+
for it.
|
|
252
|
+
|
|
237
253
|
### Delivering
|
|
238
254
|
|
|
239
255
|
```ts
|
|
@@ -295,6 +311,8 @@ your own earnings. Use the test grant instead.
|
|
|
295
311
|
- [ ] Every item price is on the 50/100/200/500/1000 grid
|
|
296
312
|
- [ ] Every price renders `priceDisplayUsdCents` beside the coin figure
|
|
297
313
|
- [ ] `buy()` is called synchronously inside a click/tap handler
|
|
314
|
+
- [ ] In the Genex app (`getShell() === "app"`) the buy buttons are hidden or
|
|
315
|
+
the shop is browse-only — never a failure message
|
|
298
316
|
- [ ] `canceled` is silent; only real failures show a message
|
|
299
317
|
- [ ] `deliverPending()` runs on every boot, before the player can act
|
|
300
318
|
- [ ] `consumeEntitlement()` is awaited BEFORE the effect is applied
|
|
@@ -338,3 +356,11 @@ environment. Nothing to fix in the game; say so and build the rest.
|
|
|
338
356
|
**Purchases do nothing in local testing** — local test mode has no wallet and no
|
|
339
357
|
server. `buy()` returns `failed` immediately by design. Test purchases on a
|
|
340
358
|
preview or published build.
|
|
359
|
+
|
|
360
|
+
**`buy()` returns `failed` inside the Genex app** — the app has no purchase
|
|
361
|
+
surface yet, so nothing opens. Not a bug to chase: hide the buy buttons there
|
|
362
|
+
(`getShell() === "app"`).
|
|
363
|
+
|
|
364
|
+
**The game dropped out of fullscreen when the shop's confirmation opened** — by
|
|
365
|
+
design on the game's Genex page: the confirmation is the page's, and the page
|
|
366
|
+
has to be visible to show it. Re-enter from the player's next click.
|
|
@@ -114,9 +114,9 @@ grants the permission.
|
|
|
114
114
|
"click to resume" button.
|
|
115
115
|
5. Always-visible state: reticle while locked; real cursor + "click to
|
|
116
116
|
aim/resume" cue while unlocked. The cue is also the validation hook.
|
|
117
|
-
6. If the lock request is rejected (a
|
|
118
|
-
|
|
119
|
-
"Drag to look" instead. Never a dead game.
|
|
117
|
+
6. If the lock request is rejected (a sandboxed embed without the
|
|
118
|
+
`allow-pointer-lock` sandbox token), the mode falls back to drag-orbit —
|
|
119
|
+
show "Drag to look" instead. Never a dead game.
|
|
120
120
|
7. Gate firing/actions on `followCam.aimState === "locked"` — the click that
|
|
121
121
|
acquires the lock (and any drag in the fallback) must not also shoot.
|
|
122
122
|
8. Do NOT exit the lock between rounds, respawns, or cutscenes — only for real
|
|
@@ -178,8 +178,8 @@ Want a custom HUD? Read `onAimChange: ({ state }) => …` yourself instead of th
|
|
|
178
178
|
eye-height target fed to `moveTo`, `avatar.visible = false`, and
|
|
179
179
|
`controller.setLockForward(true)`.
|
|
180
180
|
- **Touch / rejected lock:** touch never locks (the mode reports `"off"`); a
|
|
181
|
-
|
|
182
|
-
drag-orbit stays as the fallback — never a dead camera.
|
|
181
|
+
sandboxed embed without the `allow-pointer-lock` sandbox token goes
|
|
182
|
+
`"unavailable"` and drag-orbit stays as the fallback — never a dead camera.
|
|
183
183
|
|
|
184
184
|
The bucket rule (mandatory / recommended / never) and the full lock lifecycle
|
|
185
185
|
contract live in `$genex-threejs-camera-direction`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-threejs-embed-auth
|
|
3
|
-
description: Wire up player identity AND durable game state for a Genex game via @genex-ai/embed-sdk. Load this UNCONDITIONALLY for every game, multiplayer or not, BEFORE writing any boot code — every player gets an identity (signed-in account or guest); per-player saves, shared world state, and leaderboards are one-line SDK calls; multiplayer requires the SDK's token either way.
|
|
3
|
+
description: Wire up player identity AND durable game state for a Genex game via @genex-ai/embed-sdk. Load this UNCONDITIONALLY for every game, multiplayer or not, BEFORE writing any boot code — every player gets an identity (signed-in account or guest); per-player saves, shared world state, and leaderboards are one-line SDK calls; multiplayer requires the SDK's token either way. Also holds the shells table: what a game may do on its Genex page, in the app and on its own link.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex Three.js Embed Auth
|
|
@@ -18,11 +18,13 @@ for both, using the SDK's token. The SDK handles every context with one
|
|
|
18
18
|
straight into guest play. No login screen either way.
|
|
19
19
|
- **Standalone (someone opens the game's link directly):** the visit bounces
|
|
20
20
|
once through Genex (at the CDN edge in production, so the game only loads
|
|
21
|
-
AFTER identity is resolved; the
|
|
22
|
-
|
|
23
|
-
zero clicks; everyone else arrives as a guest with a small
|
|
24
|
-
"sign in to save progress" popover (rendered by the SDK — don't
|
|
25
|
-
own). Nobody ever hits a login wall on a published game.
|
|
21
|
+
AFTER identity is resolved; where the edge did not, the SDK makes the same
|
|
22
|
+
bounce itself) — signed-in visitors arrive already authenticated on ANY
|
|
23
|
+
game with zero clicks; everyone else arrives as a guest with a small
|
|
24
|
+
dismissible "sign in to save progress" popover (rendered by the SDK — don't
|
|
25
|
+
build your own). Nobody ever hits a login wall on a published game.
|
|
26
|
+
- **Inside the Genex mobile app:** the app hands the game its player — the
|
|
27
|
+
signed-in account or a guest — with no bounce and no login screen.
|
|
26
28
|
- **Local test mode (your own self-testing):** `?genex_local_test=1` on the
|
|
27
29
|
local dev server boots a credential-less local session — see "Self-testing
|
|
28
30
|
a draft" below.
|
|
@@ -31,6 +33,32 @@ While identity is resolving (or blocked) the SDK shows its own full-screen
|
|
|
31
33
|
overlay over the game, so never build a separate "connecting" screen for auth.
|
|
32
34
|
Guest sessions have **no** overlay — the game just plays.
|
|
33
35
|
|
|
36
|
+
## The four shells — read before you call a browser or device API
|
|
37
|
+
|
|
38
|
+
Those four contexts are the **shells** a game runs in, and they differ in far
|
|
39
|
+
more than identity. What a game may open, save, share, read from its URL or
|
|
40
|
+
ask of the player's device is different in each one, and a call the shell
|
|
41
|
+
refuses usually fails with no error at all.
|
|
42
|
+
**Find the row in [references/shells.md](references/shells.md) before you
|
|
43
|
+
write `navigator.*`, `window.open`, a download link, a dialog, or anything
|
|
44
|
+
that reads the URL.** The digest:
|
|
45
|
+
|
|
46
|
+
- **Which shell:** `getShell()` → `"page"` (the frame on the game's Genex
|
|
47
|
+
page, where most players are, and any other frame), `"app"`,
|
|
48
|
+
`"standalone"` or `"local"`.
|
|
49
|
+
- **Share, copy, open a link, save a file, fullscreen, keep the screen on,
|
|
50
|
+
vibrate:** `share()`, `copyText()`, `openExternal()`, `saveFile()`,
|
|
51
|
+
`setFullscreen()`, `keepAwake()`, `haptic()`, from a click or tap handler,
|
|
52
|
+
before any `await`. They resolve with a reason and never throw.
|
|
53
|
+
- **Your own URL parameters, and where the visitor came from:** `getEntry()`,
|
|
54
|
+
never `location` or `document.referrer`.
|
|
55
|
+
- **Microphone, camera, location:** the build declares them at
|
|
56
|
+
`npx genex preview` / `npx genex publish`; the app refuses all three.
|
|
57
|
+
- **Never:** `alert` / `confirm` / `prompt`, `window.open`, a link or form
|
|
58
|
+
that leaves the game, a service worker, reading the clipboard.
|
|
59
|
+
- **Local test mode is a plain browser tab, not the frame.** A call that works
|
|
60
|
+
there can still be dead on the game's Genex page.
|
|
61
|
+
|
|
34
62
|
## Install
|
|
35
63
|
|
|
36
64
|
```bash
|
|
@@ -86,8 +114,9 @@ const { user, guest } = await waitForPlayer();
|
|
|
86
114
|
// ./assets/avatar.vrm behind it (absent on old APIs / local test mode).
|
|
87
115
|
// Peers resolve the same way via the multiplayer SDK's player.avatarUrl.
|
|
88
116
|
|
|
89
|
-
// ACCOUNT gate — resolves ONLY for signed-in players
|
|
90
|
-
//
|
|
117
|
+
// ACCOUNT gate — resolves ONLY for signed-in players. For a guest it stays
|
|
118
|
+
// pending for as long as this page is loaded: signing in loads the game
|
|
119
|
+
// again, and the gate resolves on that load. Use ONLY for /state
|
|
91
120
|
// saving/loading and other account-bound features.
|
|
92
121
|
const { user: account } = await waitForAuth();
|
|
93
122
|
```
|
|
@@ -104,9 +133,10 @@ boot-path gate; `waitForAuth()` guards saves only.
|
|
|
104
133
|
and player-name UI; resolves for guests and accounts alike, rejects only if
|
|
105
134
|
the session ends up blocked.
|
|
106
135
|
- `waitForAuth()` → `Promise<{ user }>` — the gate for `/state` calls and
|
|
107
|
-
account-bound features. Stays PENDING for
|
|
108
|
-
|
|
109
|
-
|
|
136
|
+
account-bound features. Stays PENDING for a guest for the whole page load
|
|
137
|
+
(signing in loads the game again, and it resolves on that next load);
|
|
138
|
+
rejects if blocked. Catch rejections and let the SDK's overlay handle the
|
|
139
|
+
UX (don't build your own sign-in UI).
|
|
110
140
|
- `isEmbedded()` → `boolean` — structural "is in an iframe" check; NOT the same
|
|
111
141
|
question as "is signed in".
|
|
112
142
|
- `getAuthState()` → `"pending" | "authenticated" | "guest" | "blocked"` —
|
|
@@ -123,8 +153,44 @@ boot-path gate; `waitForAuth()` guards saves only.
|
|
|
123
153
|
automatic re-seat reads a fresh token. Tokens rotate automatically (~every
|
|
124
154
|
10 minutes). Never cache the value across joins and never log it.
|
|
125
155
|
- `on(event, cb)` → unsubscribe fn. Events: `"authenticated"`, `"guest"`,
|
|
126
|
-
`"blocked"`, `"error"`. A
|
|
127
|
-
|
|
156
|
+
`"blocked"`, `"error"`. A page load ends in one of the first three. Signing
|
|
157
|
+
in loads the game again, so the account arrives as `"authenticated"` on
|
|
158
|
+
the next load, not as a second event on this one.
|
|
159
|
+
|
|
160
|
+
Where the game is running and what it was opened with (`@genex-ai/embed-sdk`
|
|
161
|
+
0.32.0+; all three are safe to call before `initEmbed()`):
|
|
162
|
+
|
|
163
|
+
- `getShell()` → `"page" | "app" | "standalone" | "local"` — which shell this
|
|
164
|
+
is. A fact about the page: not who is signed in, and not a permission. Any
|
|
165
|
+
frame is `"page"`, whoever frames it, and the whole page of any React
|
|
166
|
+
Native web view is `"app"`.
|
|
167
|
+
- `getEntry()` → `{ search, params, hash, referrer }` — the link the game was
|
|
168
|
+
opened with: its own query (`search` as text, `params` as
|
|
169
|
+
`URLSearchParams`), its own fragment, and the origin of the site the visitor
|
|
170
|
+
came from, or `null`.
|
|
171
|
+
- `onShellClosing(cb)` → unsubscribe fn — `cb` runs when the page is being
|
|
172
|
+
taken away: the tab closed or reloaded, the app closing the game. The last
|
|
173
|
+
chance for a small `savePlayerState()`, never the only one.
|
|
174
|
+
|
|
175
|
+
Platform calls (0.32.0+): one call each that does the right thing in
|
|
176
|
+
whichever shell the game is in. **Call them from a click or tap handler,
|
|
177
|
+
before any `await`.** They resolve and never throw. What each one does in
|
|
178
|
+
each shell is in [references/shells.md](references/shells.md).
|
|
179
|
+
|
|
180
|
+
- `share({ text?, params? })` → `Promise<{ status, url }>`; `status` is
|
|
181
|
+
`"shared" | "copied" | "dismissed" | "unavailable"`. The SDK builds the
|
|
182
|
+
link; the game supplies only its own `params`.
|
|
183
|
+
- `copyText(text)`, `keepAwake(on)`, `openExternal(url)`,
|
|
184
|
+
`saveFile(blob, name)`, `setFullscreen(on)` →
|
|
185
|
+
`Promise<{ ok: true } | { ok: false, error }>`; `error` is `unsupported` |
|
|
186
|
+
`needs_gesture` | `dismissed` | `busy` | `rate_limited` | `invalid_request`
|
|
187
|
+
| `too_large` | `timeout`. `unsupported` and `timeout` are ordinary answers,
|
|
188
|
+
not bugs.
|
|
189
|
+
- `haptic(kind)` → nothing; `kind` is `"selection" | "light" | "medium" |
|
|
190
|
+
"heavy" | "success" | "warning" | "error"`.
|
|
191
|
+
- `supports(name)` → `boolean`, synchronous: whether to draw the button at
|
|
192
|
+
all. `name` is `"share" | "copy" | "awake" | "haptic" | "open" | "save" |
|
|
193
|
+
"fullscreen"`.
|
|
128
194
|
|
|
129
195
|
Durable state + leaderboards (all six are safe to call from boot — they wait
|
|
130
196
|
for identity internally, never throw for guests, and reject only when the
|
|
@@ -135,8 +201,9 @@ session is blocked):
|
|
|
135
201
|
overwrite it). `{ data: null, version: 0 }` when they never saved.
|
|
136
202
|
- `savePlayerState(data, { ifVersion? }?)` → `Promise<{ saved, version?,
|
|
137
203
|
conflict?, guest?, queued?, staging? }>` — save any JSON ≤ 256KB to their
|
|
138
|
-
slot. For
|
|
139
|
-
|
|
204
|
+
slot. For a guest nothing is saved: the value is held in memory for this
|
|
205
|
+
page load only (`{ saved: false, guest: true, queued: true }`), and signing
|
|
206
|
+
in loads the game again. Small saves survive tab close automatically.
|
|
140
207
|
- `loadWorldState()` / `saveWorldState(data, { ifVersion? }?)` — the game's
|
|
141
208
|
ONE shared world slot (≤ 1MB, every player reads/writes the same blob) —
|
|
142
209
|
level layouts, persistent-world object positions. Multiplayer games: only
|
|
@@ -145,7 +212,8 @@ session is blocked):
|
|
|
145
212
|
`{ conflict: true, version }` — reload, merge, retry.
|
|
146
213
|
- `submitScore(score, { board?, mode? }?)` → `Promise<{ submitted, best?,
|
|
147
214
|
improved?, guest?, queued? }>` — keep-best leaderboard submit (`mode:
|
|
148
|
-
"min"` for lap-time boards).
|
|
215
|
+
"min"` for lap-time boards). A guest's score is not posted: the best value
|
|
216
|
+
is held in memory for this page load only.
|
|
149
217
|
- `getLeaderboard({ board?, limit?, order? }?)` → `Promise<{ items, me }>` —
|
|
150
218
|
top entries (verified display names — never trust client-side name input
|
|
151
219
|
for this) + the signed-in player's own `{ rank, score }`. Works for guests
|
|
@@ -162,10 +230,11 @@ and saves there by design (a staging build with a changed save schema must
|
|
|
162
230
|
never overwrite the save of the game people are playing). The helpers resolve
|
|
163
231
|
`{ staging: true }` — `loadPlayerState()` → `{ data: null, version: 0,
|
|
164
232
|
staging: true }`, `savePlayerState()` → `{ saved: false, staging: true }` —
|
|
165
|
-
and never throw or fire `"error"` for it. Keep a
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
233
|
+
and never throw or fire `"error"` for it. Keep a local copy of progress
|
|
234
|
+
(`localStorage`) so a preview stays playable — a cache for this device and
|
|
235
|
+
this shell, never the save — and if the game says anything about it, say
|
|
236
|
+
"Preview build — progress isn't saved to your account". Never "offline" or
|
|
237
|
+
"unavailable": the owner is testing their own game, and Genex is up.
|
|
169
238
|
|
|
170
239
|
|
|
171
240
|
## Saving progress (per-player — every player has their own slot)
|
|
@@ -186,11 +255,35 @@ applyProgress(data ?? defaultProgress());
|
|
|
186
255
|
void savePlayerState(progress);
|
|
187
256
|
```
|
|
188
257
|
|
|
189
|
-
Guests need
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
258
|
+
Guests need no gating: the helpers resolve for them instead of failing. But
|
|
259
|
+
**a guest's progress is not saved, and it does not follow them into an
|
|
260
|
+
account.** The SDK holds a guest's latest save in memory for this page load
|
|
261
|
+
only, and signing in on the game's page or its own address loads the game
|
|
262
|
+
again (the SDK's popover and the Genex page both leave for the sign-in and
|
|
263
|
+
come back to a fresh load), so that value is gone when the account arrives.
|
|
264
|
+
Signing in from another tab changes nothing until the game is reloaded. If a
|
|
265
|
+
guest's run should survive signing in, keep your own copy on the device and
|
|
266
|
+
start from it when the account has no save of its own:
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
// at every checkpoint: the device's copy, then the save
|
|
270
|
+
try { localStorage.setItem("progress", JSON.stringify(progress)); } catch {}
|
|
271
|
+
void savePlayerState(progress);
|
|
272
|
+
|
|
273
|
+
// at boot, in place of the plain load above. loadPlayerState() waits for the
|
|
274
|
+
// player, so on the load after a sign-in this is already the account.
|
|
275
|
+
const { data, guest, staging } = await loadPlayerState();
|
|
276
|
+
let kept: unknown = null;
|
|
277
|
+
try { kept = JSON.parse(localStorage.getItem("progress") ?? "null"); } catch {}
|
|
278
|
+
applyProgress(data ?? kept ?? defaultProgress());
|
|
279
|
+
// An account with no save of its own yet keeps what this device had.
|
|
280
|
+
if (data === null && kept !== null && !guest && !staging) void savePlayerState(kept);
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
That copy is a cache for this device and this shell, never the save: an
|
|
284
|
+
account that already has a save keeps it, and the copy does not exist on
|
|
285
|
+
another device or on the game's other address. The SDK already tells guests
|
|
286
|
+
to sign in (its popover / the dashboard's card) — don't add another prompt.
|
|
194
287
|
|
|
195
288
|
**Per-player vs world:** `savePlayerState` is each player's own progress;
|
|
196
289
|
`saveWorldState` is the game's ONE shared world (persistent-world layouts —
|
|
@@ -213,8 +306,9 @@ const { items, me } = await getLeaderboard({ limit: 10 });
|
|
|
213
306
|
|
|
214
307
|
Scores are per-account (one row per player per board, real display names from
|
|
215
308
|
their Genex account) and keep-best — submitting a worse score changes nothing
|
|
216
|
-
(`improved: false`). Guests can READ leaderboards;
|
|
217
|
-
|
|
309
|
+
(`improved: false`). Guests can READ leaderboards; a guest's submit is not
|
|
310
|
+
posted, and signing in loads the game again, so submit when a signed-in
|
|
311
|
+
player finishes a run. Send a consistent `mode` per board. Scores are
|
|
218
312
|
client-reported (arcade-style trust) — don't present them as anti-cheat.
|
|
219
313
|
|
|
220
314
|
## NEVER log the tokens
|
|
@@ -252,8 +346,19 @@ carries a one-time pass (or an inert guest marker) in the URL that the SDK
|
|
|
252
346
|
consumes and removes immediately. Unpublished drafts are the exception:
|
|
253
347
|
strangers can't play them, so a draft link shows the SDK's sign-in gate
|
|
254
348
|
instead (for self-testing, see local test mode below). Don't code around any
|
|
255
|
-
of this:
|
|
256
|
-
|
|
349
|
+
of this: `isEmbedded()` and the return-trip handling are internal SDK
|
|
350
|
+
concerns.
|
|
351
|
+
|
|
352
|
+
Your own `?query` comes through that round trip on every published game. Your
|
|
353
|
+
`#fragment` and the site the visitor came from need `@genex-ai/embed-sdk`
|
|
354
|
+
0.32.0+, written in `package.json` as a range that starts there (`^0.32.0`)
|
|
355
|
+
when the build was shipped.
|
|
356
|
+
Read all three with `getEntry()`, never from `location` or
|
|
357
|
+
`document.referrer`: until `initEmbed()` has run the address bar still holds
|
|
358
|
+
Genex's own fragment, and `document.referrer` is Genex's address or empty.
|
|
359
|
+
`embed` and every key starting `genex_` are reserved. Sizes, the other
|
|
360
|
+
reserved names and what each shell passes on are in
|
|
361
|
+
[references/shells.md](references/shells.md).
|
|
257
362
|
|
|
258
363
|
### Self-testing a draft: local test mode
|
|
259
364
|
|
|
@@ -284,7 +389,10 @@ is ever minted): real sign-in (`waitForAuth()` stays pending, exactly like a
|
|
|
284
389
|
guest), saves (they queue in memory), leaderboards (`getLeaderboard()`
|
|
285
390
|
resolves `{ items: [], me: null }` locally), score submits, and multiplayer
|
|
286
391
|
(`getColyseusAuth()` is `undefined`, so `connect()` fails at the relay —
|
|
287
|
-
expected in this mode, not a bug to chase).
|
|
392
|
+
expected in this mode, not a bug to chase). Nor anything the game's Genex
|
|
393
|
+
page or the app refuses: this is a plain browser tab, so `window.open`, a
|
|
394
|
+
download link and a browser dialog all work here and are dead there
|
|
395
|
+
([references/shells.md](references/shells.md)).
|
|
288
396
|
|
|
289
397
|
Rules:
|
|
290
398
|
|
|
@@ -299,8 +407,12 @@ Rules:
|
|
|
299
407
|
local test mode cannot open the hosted draft; hosted draft access stays
|
|
300
408
|
owner-only. Do NOT work around that gate: no undocumented URL fragments,
|
|
301
409
|
no auth mocks, and never drive the user's own signed-in browser.
|
|
302
|
-
- Opening localhost WITHOUT the marker
|
|
303
|
-
|
|
410
|
+
- Opening localhost WITHOUT the marker boots local test mode too on
|
|
411
|
+
`@genex-ai/embed-sdk` 0.14.0+ (an older SDK runs the identity bounce
|
|
412
|
+
there). Keep the marker in every URL you open: it says what you meant and
|
|
413
|
+
works on every SDK since 0.5.0. `?genex_auth=1` opts that dev-server tab
|
|
414
|
+
back into the real identity flow — that's for debugging real sign-in, not
|
|
415
|
+
for self-testing.
|
|
304
416
|
- The owner's draft run-through stays a required beat, not a fallback: push
|
|
305
417
|
`genex preview` at each playable milestone and hand it off plainly ("check
|
|
306
418
|
the draft — you can now X; ping me if something feels off"), then keep
|
|
@@ -329,12 +441,21 @@ Rules:
|
|
|
329
441
|
overlay and the dashboard own all of that UX.
|
|
330
442
|
- [ ] Self-test evidence captured in local test mode is labeled as such in the
|
|
331
443
|
handoff ("local test mode — auth, saves, and multiplayer not exercised").
|
|
444
|
+
- [ ] Every browser or device API the game calls has its row in
|
|
445
|
+
[references/shells.md](references/shells.md): sharing, copying, links
|
|
446
|
+
out, file saves and fullscreen go through the SDK calls, from a click
|
|
447
|
+
or tap handler; the game's own URL parameters come from `getEntry()`.
|
|
332
448
|
|
|
333
449
|
## Troubleshooting
|
|
334
450
|
|
|
335
|
-
- **Game loads then immediately navigates away (
|
|
451
|
+
- **Game loads then immediately navigates away (the game's own link)** — the
|
|
336
452
|
identity bounce, working as designed for EVERY standalone visit. It returns
|
|
337
|
-
to the game automatically (signed-in or as a guest) within a second.
|
|
453
|
+
to the game automatically (signed-in or as a guest) within a second. On a
|
|
454
|
+
dev server it only happens with `?genex_auth=1` or an SDK older than
|
|
455
|
+
0.14.0: open with `?genex_local_test=1`.
|
|
456
|
+
- **Works locally or on the game's own link, does nothing on its Genex page
|
|
457
|
+
or in the app** — that shell refuses the call, with no error. Find the row
|
|
458
|
+
in [references/shells.md](references/shells.md) and make the call it names.
|
|
338
459
|
- **`waitForAuth()` never resolves** — the player is a GUEST; that's the
|
|
339
460
|
designed behavior. Anything that must run for guests belongs behind
|
|
340
461
|
`waitForPlayer()` instead.
|
|
@@ -362,8 +483,9 @@ Rules:
|
|
|
362
483
|
switch to `savePlayerState`/`saveWorldState`, which handle every case.
|
|
363
484
|
- **Saves/loads resolve `{ staging: true }`** — the build is on the preview
|
|
364
485
|
channel (the draft workspace, `preview--<slug>`), where server saves are off
|
|
365
|
-
by design. Not an outage: keep the
|
|
366
|
-
|
|
486
|
+
by design. Not an outage: keep the local copy (a cache for this device and
|
|
487
|
+
this shell, never the save) and tell the player "Preview build — progress
|
|
488
|
+
isn't saved to your account", never "offline".
|
|
367
489
|
- **Saves or score submits return 429** — writing too often (per-player limits:
|
|
368
490
|
60 player-saves/min, 120 world-saves/min, 30 score submits/min). Debounce to
|
|
369
491
|
~1 write/sec at checkpoints and never save in the render loop; back off a
|