@genex-ai/cli-demo 1.36.5 → 1.36.6-dev.862

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.
@@ -401,13 +401,39 @@ and friends join it; the lobby is persistent (rounds replay, nobody is evicted):
401
401
 
402
402
  ```ts
403
403
  import { createPrivate, joinPrivate } from "@genex-ai/multiplayer";
404
- const lobby = await createPrivate<MyState>({ urls, room: slug, auth: () => getColyseusAuth() }); // live NOW
405
- showCode(lobby.code); // share this
406
- // a friend, elsewhere:
407
- const lobby = await joinPrivate<MyState>(code, { urls, room: slug, auth: () => getColyseusAuth() });
404
+ import { share, getEntry } from "@genex-ai/embed-sdk"; // 0.32.0+
405
+ const opts = { urls, room: slug, auth: () => getColyseusAuth() };
406
+
407
+ // host
408
+ const lobby = await createPrivate<MyState>(opts); // live NOW
409
+ showCode(lobby.code); // always show the code too: the way in when a link will not do
410
+ inviteBtn.onclick = async () => { // share() starts inside the click, before any await
411
+ const { status, url } = await share({ text: "Join my lobby", params: { lobby: lobby.code } });
412
+ if (status === "copied") toast("Invite link copied");
413
+ if (status === "unavailable") showLink(url); // no share sheet, no clipboard: show the link
414
+ };
415
+
416
+ // friend: ONE way in, for the code from a link and for a code typed by hand
417
+ async function enter(code: string) {
418
+ if (!/^[\w-]{1,32}$/.test(code)) return null; // a link is text anyone can write: take a code, nothing else
419
+ try { return await joinPrivate<MyState>(code, opts); }
420
+ catch { showNotice("That lobby is no longer open"); return null; }
421
+ }
422
+ // at boot, after waitForPlayer(): did they arrive on an invite link?
423
+ const invited = getEntry().params.get("lobby");
424
+ const joined = invited ? await enter(invited) : null; // null → the menu, with its "Enter code" field
408
425
  // same handle API as matchmake(): lobby.session, lobby.matchmaking, eliminated()/score()/finish(), cancel()
409
426
  ```
410
427
 
428
+ `share()` builds the link itself — the game's page on Genex plus `?lobby=<code>`, or the
429
+ address the game is on where that is a preview address or a creator's own domain — and
430
+ `getEntry()` hands the parameter back to whoever opens it. **Never build the invite from
431
+ `location.href`**: on the game's page that is the frame's bare address, not the page a friend
432
+ should land on. `lobby` is this example's name; any name works except the ones Genex keeps
433
+ (`$genex-threejs-embed-auth`, `references/shells.md`). `joinPrivate` REJECTS when the code leads
434
+ to no lobby this player can reach — it closed, or its host is on another region's relay — so
435
+ catch it and show the menu, and keep the typed code as the second way in.
436
+
411
437
  ## Connect
412
438
 
413
439
  Pick your own per-player state shape (any JSON). `room` is the **project slug**
@@ -674,10 +700,22 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
674
700
  mesh — everyone uploads to everyone, and reliability, not bandwidth, breaks first). The
675
701
  room still holds 64 players; the 7th `join()` is refused, so handle `voice:rejected`
676
702
  instead of retrying. `join()` MUST run inside a click/tap — browsers gate both the mic
677
- prompt and audio playback on a user gesture — and it REJECTS when the player denies the
678
- mic, which is an ordinary outcome to show, not an error to swallow. For a 3D game use
679
- `npx genex controller voice` (proximity audio + push-to-talk); it joins with
680
- `{ spatial: true }` and drives a `THREE.PositionalAudio` per speaker. **TURN is automatic
703
+ prompt and audio playback on a user gesture — and it REJECTS when the microphone is
704
+ refused, which is an ordinary outcome to show, not an error to swallow. **Two refusals are
705
+ not the player's.** On its Genex page a game gets the microphone only when its BUILD
706
+ declared it: `npx genex preview` / `npx genex publish` declare it when the game's own
707
+ source calls `.voice.join()` with nothing or with options written at the call, and print
708
+ what the build is given — read that line. `voice.join(opts)` with the options in a
709
+ variable is not seen: declare the microphone in `package.json` then. When the
710
+ page withheld it, `join()` rejects with an error whose `name` is `VoiceNotDelegatedError`
711
+ (SDK 0.16.2+, Chromium browsers; elsewhere it arrives as the browser's own error): show
712
+ "Voice chat is not available here", never "allow your microphone" — the player was never
713
+ asked. And the Genex app refuses the microphone for every game: hide the voice button when
714
+ `getShell()` from `@genex-ai/embed-sdk` is `"app"`. The declaration rule, and the
715
+ `package.json` line for a call the CLI cannot see, are in `$genex-threejs-embed-auth`
716
+ (`references/shells.md`).
717
+ For a 3D game use `npx genex controller voice` (proximity audio + push-to-talk); it joins
718
+ with `{ spatial: true }` and drives a `THREE.PositionalAudio` per speaker. **TURN is automatic
681
719
  — do not pass `iceServers`**: `join()` fetches short-lived relay credentials from the
682
720
  platform before opening any peer connection, which is what lets the ~15% of players behind
683
721
  symmetric NAT or carrier CGNAT be heard at all (in a mesh they fail every leg, so they
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: genex-threejs-touch-controls
3
- description: Make a Genex Three.js game playable on phones — vendored touch primitives via `npx genex controller touch` (floating/static joystick, virtual buttons, drag zone, rotate-device overlay) plus per-genre wiring recipes (movement, camera look, taps, paddles, action buttons, touch pause). Use when wiring default touch controls into a new game, whenever someone asks to make a game playable on mobile/phone/tablet, or before shipping a game whose input is keyboard/mouse-only.
3
+ description: Make a Genex Three.js game playable on phones — vendored touch primitives via `npx genex controller touch` (floating/static joystick, virtual buttons, drag zone, rotate-device overlay) plus per-genre wiring recipes (movement, camera look, taps, paddles, action buttons, touch pause, tilt). Use when wiring default touch controls into a new game, whenever someone asks to make a game playable on mobile/phone/tablet, or before shipping a game whose input is keyboard/mouse-only.
4
4
  ---
5
5
 
6
6
  # Genex Three.js Touch Controls
@@ -116,6 +116,68 @@ If the game is merely suboptimal sideways, skip the overlay and just resize.
116
116
  The web cannot force an orientation (fullscreen-only lock, none on iOS Safari)
117
117
  — the overlay asks; it never traps.
118
118
 
119
+ ## Tilt controls
120
+
121
+ Steering by tilting the phone is an extra on top of the stick, never a
122
+ replacement for it: the sensor is the one input a phone can silently not have.
123
+ Four rules, all in the block below.
124
+
125
+ 1. **Ask inside the Start tap.** An iPhone hands a page no motion events until
126
+ the player allows them, and it asks only when `requestPermission()` is
127
+ called from a tap inside the game. Called at boot it is refused without a
128
+ prompt.
129
+ 2. **A refusal, or silence, is "no sensor".** A browser can answer "granted"
130
+ and then send nothing, and a desktop can send events full of `null`. Trust
131
+ tilt only after a real reading has arrived.
132
+ 3. **Keep the joystick.** It stays on screen and wins whenever it is touched.
133
+ 4. **Hold the screen awake.** Tilt play has no touches, so the phone dims and
134
+ locks mid-game unless the game asks it not to.
135
+
136
+ ```ts
137
+ import { keepAwake } from "@genex-ai/embed-sdk"; // 0.32.0+
138
+
139
+ const tilt = { x: 0, y: 0, live: false }; // x/y in [-1, 1], same sense as joy.x / joy.y
140
+ const TILT_FULL = 25; // degrees of lean for full deflection
141
+ let rest: { x: number; y: number } | null = null; // how the phone was held at the first reading
142
+
143
+ type MotionGate = { requestPermission?: () => Promise<"granted" | "denied"> };
144
+
145
+ // Call from the Start/Play tap handler, before any await in that handler.
146
+ async function enableTilt(): Promise<void> {
147
+ void keepAwake(true); // released with keepAwake(false) on the menu / pause
148
+ const gate = (window as unknown as { DeviceOrientationEvent?: MotionGate }).DeviceOrientationEvent;
149
+ if (!gate) return; // no sensor API: the joystick is the control
150
+ try {
151
+ if (typeof gate.requestPermission === "function" && (await gate.requestPermission()) !== "granted") return;
152
+ } catch { return; } // refused, or not inside a tap
153
+ window.addEventListener("deviceorientation", (e) => {
154
+ if (e.beta == null || e.gamma == null) return; // a desktop fires events with nulls
155
+ // The reading is in the DEVICE's axes; turn it into the screen's.
156
+ const a = screen.orientation?.angle ?? 0;
157
+ const x = a === 90 ? e.beta : a === 270 ? -e.beta : a === 180 ? -e.gamma : e.gamma;
158
+ const y = a === 90 ? -e.gamma : a === 270 ? e.gamma : a === 180 ? -e.beta : e.beta;
159
+ rest ??= { x, y }; // "level" is however the player was holding it
160
+ const unit = (v: number) => Math.max(-1, Math.min(1, v / TILT_FULL));
161
+ tilt.x = unit(x - rest.x);
162
+ tilt.y = -unit(y - rest.y); // leaning the top edge away from you = up, like the stick
163
+ tilt.live = true;
164
+ });
165
+ }
166
+
167
+ // each frame, where the stick is read:
168
+ const steer = joy.x !== 0 ? joy.x : tilt.live ? tilt.x : 0;
169
+ ```
170
+
171
+ Clear `rest` when the device rotates or the player taps a "recentre" button,
172
+ so level is re-taken from the next reading. **Check the directions on a real
173
+ phone, in the orientation the game is played in** — a desktop browser has no
174
+ sensor, so nothing in a local test exercises this. Lean-right must steer
175
+ screen-right (the screen-direction contract in
176
+ `$genex-threejs-camera-direction`); a flipped feel is a sign to fix in the
177
+ mapping above, never a device quirk. What each Genex shell does with the
178
+ sensors and the wake lock is in `$genex-threejs-embed-auth`
179
+ (`references/shells.md`).
180
+
119
181
  ## Match the game's style
120
182
 
121
183
  The touch controls are UI like any other — restyle them from the same style
@@ -13,11 +13,12 @@ model has to run for the player.
13
13
  ## What the platform does
14
14
 
15
15
  A game hosted on Genex can call a language model from inside the running game
16
- and get back text or JSON. **The player pays and the player approves**: coins
17
- from their Genex wallet, or their own Claude / ChatGPT subscription, chosen on
18
- an approval sheet Genex draws that the game cannot render, skin or bypass.
19
- Either one call at a time, or one standing budget the player approves once and
20
- the game then spends against without another popup. The game holds no provider
16
+ and get back text or JSON. **The player pays and the player approves**: credits
17
+ from their Genex account, billed as each call is used and never past the
18
+ per-call ceiling the game declares, or their own Claude / ChatGPT subscription,
19
+ chosen on an approval sheet Genex draws that the game cannot render, skin or
20
+ bypass. Either one call at a time, or one standing budget the player approves
21
+ once and the game then spends against without another popup. The game holds no provider
21
22
  key, sees no credential, and never talks to a model vendor.
22
23
 
23
24
  That is the whole reason this is a platform feature and not something you wire
@@ -40,9 +41,9 @@ Any of these means a model at PLAY time, however it is phrased:
40
41
  Say this and stop:
41
42
 
42
43
  > A model running while people play is built into the Genex platform — the
43
- > player pays, with Genex coins or their own Claude/ChatGPT subscription, and
44
- > approves it on a Genex sheet; your game just calls `generate()`. Want it
45
- > that way?
44
+ > player pays, with their Genex credits or their own Claude/ChatGPT
45
+ > subscription, and approves it on a Genex sheet; your game just calls
46
+ > `generate()`. Want it that way?
46
47
 
47
48
  Wait for the answer. Do not start building either version first, and do not
48
49
  expand the offer into a pitch — one line, one question.
@@ -64,7 +65,8 @@ expand the offer into a pitch — one line, one question.
64
65
  ask before you do if you have not already.
65
66
  3. Load `$genex-llm-in-games` — it arrives with the conversion and owns the
66
67
  build: the SDK surface, one-time calls versus a standing budget, measuring
67
- the price before the game declares it, and honest handling of every refusal.
68
+ the per-call ceiling before the game declares it, and honest handling of
69
+ every refusal.
68
70
 
69
71
  **The game has to be a static browser build.** A hosted game is files served
70
72
  from the edge; there is no server of yours inside it. So the familiar pattern —
@@ -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@latest # the genex CLI (a dev dependency)
41
+ npm i -D @genex-ai/cli-demo@dev # 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
  ```