@genex-ai/cli-demo 1.2.4 → 1.3.0-dev.372
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 +345 -180
- package/package.json +1 -1
- package/templates/controllers/chat/chat-overlay.ts +305 -0
- package/templates/controllers/voice/voice-proximity.ts +247 -0
- package/templates/skills/genex-ai-skybox/SKILL.md +1 -1
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +1 -1
- package/templates/skills/genex-game-director/references/routing-map.md +2 -0
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +29 -4
- package/templates/skills/genex-threejs-multiplayer/references/realtime-patterns.md +44 -2
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +7 -3
- package/templates/skills/genex-updates/SKILL.md +1 -1
|
@@ -46,7 +46,8 @@ npm i @genex-ai/multiplayer@^0.12.0
|
|
|
46
46
|
> landed in 0.10; confirmed object controls, snaps, host-tick teardown, and reconnect rebasing
|
|
47
47
|
> in 0.9. An older resolve does not have those.
|
|
48
48
|
|
|
49
|
-
This skill targets `@genex-ai/multiplayer` **≥ 0.
|
|
49
|
+
This skill targets `@genex-ai/multiplayer` **≥ 0.15.0** (`objects`/`host` since 0.4; verified per-player `avatarUrl` since 0.12;
|
|
50
|
+
room text chat via `room.chat` since 0.14; mesh voice via `room.voice` since 0.15;
|
|
50
51
|
`matchmake()` since 0.5; private lobbies since 0.7; auto-reconnect + `inputs`/`onHostTick`
|
|
51
52
|
since 0.8; soft ownership handoff since 0.8.4; confirmed controls, snap epochs, and host-tick
|
|
52
53
|
lifecycle guarantees since 0.9; regional relay selection via `getColyseusUrls()` since 0.10; host-only `setRoomOpen()` since 0.13;
|
|
@@ -536,9 +537,11 @@ of five kinds — put each on its channel and the game just works:
|
|
|
536
537
|
| Your own avatar (position, rotation, anim) | `me.set` → others read `players` | you (each player their own) |
|
|
537
538
|
| A moving thing nobody owns (**ball**, puck, NPC) | `objects` (claim + set) | the one current **owner** |
|
|
538
539
|
| Slow agreed facts (score, round, wave, seed) | `shared` | the **host** (`isHost`) |
|
|
539
|
-
| One-off actions (shot, emote, hit
|
|
540
|
+
| One-off actions (shot, emote, hit) | `send` + `on` | whoever did it |
|
|
540
541
|
| Discrete per-player values (hp, ammo, flags) | in `me.set`, read via `stateRaw` | you |
|
|
541
542
|
| Which avatar MODEL a player is (VRM look) | already on `players` as `p.avatarUrl` — sync nothing | the **relay** (verified identity) |
|
|
543
|
+
| **Text chat** | `room.chat` — its own channel, NOT `send` | the **relay** (stamps the sender) |
|
|
544
|
+
| **Voice chat** | `room.voice` — peer-to-peer audio, max 6 | nobody (the relay carries no audio) |
|
|
542
545
|
|
|
543
546
|
Getting the channel right is the whole game. A ball on `shared` stutters (not smoothed) and
|
|
544
547
|
fights (many writers). A ball on `objects` glides and has one owner. That's the difference.
|
|
@@ -651,6 +654,28 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
|
|
|
651
654
|
you**, so apply your own action's local effect directly (draw your own tracer at fire time),
|
|
652
655
|
not inside `on(...)`. Relay-internal names (`state`, `shared`, `claim`, `obj`, `release`,
|
|
653
656
|
`destroy`, `match:*`, `__*`) are refused — pick your own event names. `room.leave()`.
|
|
657
|
+
- `room.chat` — room text chat, **its own channel, not `send`**: `chat.send(text)`,
|
|
658
|
+
`chat.on(m => …)`, `chat.history`, `chat.mute(id)` / `chat.unmute(id)` / `chat.muted`.
|
|
659
|
+
The relay stamps `m.from`/`m.name` from the verified session (nobody can post under
|
|
660
|
+
another player's name), sanitizes `m.text`, caps it at 500 chars, and replays the last
|
|
661
|
+
~50 lines so a late joiner sees the conversation. **Your own lines echo back to you** —
|
|
662
|
+
render only what arrives in `on`, or every message appears twice. UI:
|
|
663
|
+
`npx genex controller chat`, and you MUST pass its `onTypingChange` (or check
|
|
664
|
+
`chat.isTyping`) so the game stops reading the keyboard while someone types — otherwise
|
|
665
|
+
typing "was that a wall?" walks and jumps the player. Render `m.text` with `textContent`,
|
|
666
|
+
never `innerHTML`. See [references/realtime-patterns.md](references/realtime-patterns.md).
|
|
667
|
+
- `room.voice` — mesh voice chat: `voice.join(opts)` / `leave()` / `setMicEnabled(on)` /
|
|
668
|
+
`mute(id)` / `participants` / `active` / `speaking`, plus `room.on('voice', …)` and
|
|
669
|
+
`room.on('voice:rejected', …)`. **Party-sized: the relay caps it at 6** (audio is a full
|
|
670
|
+
mesh — everyone uploads to everyone, and reliability, not bandwidth, breaks first). The
|
|
671
|
+
room still holds 64 players; the 7th `join()` is refused, so handle `voice:rejected`
|
|
672
|
+
instead of retrying. `join()` MUST run inside a click/tap — browsers gate both the mic
|
|
673
|
+
prompt and audio playback on a user gesture — and it REJECTS when the player denies the
|
|
674
|
+
mic, which is an ordinary outcome to show, not an error to swallow. For a 3D game use
|
|
675
|
+
`npx genex controller voice` (proximity audio + push-to-talk); it joins with
|
|
676
|
+
`{ spatial: true }` and drives a `THREE.PositionalAudio` per speaker. **Configure TURN
|
|
677
|
+
for anything real**: ~10–20% of players are behind symmetric NAT and connect to nobody
|
|
678
|
+
without it, while everyone else works — a miserable bug to diagnose from a report.
|
|
654
679
|
- `room.inputs.send(payload)` / `room.inputs.on((fromId, payload) => …)` — the host-routed
|
|
655
680
|
input channel for host-authoritative physics: anyone sends, ONLY the current host receives.
|
|
656
681
|
See [references/host-physics.md](references/host-physics.md).
|
|
@@ -912,8 +937,8 @@ from any still capture whether movement feels smooth.** Don't try — it leads t
|
|
|
912
937
|
|
|
913
938
|
1. **Trust the SDK's smoothing.** Draw `state` directly; don't add your own.
|
|
914
939
|
2. **Verify it *runs*:** two clients, distinct meshes, both move, no console errors, each sees the
|
|
915
|
-
other (and the ball, if any). That's all a capture can prove. Local test mode (the
|
|
916
|
-
skill
|
|
940
|
+
other (and the ball, if any). That's all a capture can prove. Local test mode (what the dev
|
|
941
|
+
server boots into — the embed-auth skill) can NOT do this: it mints no relay credential, so `connect()`
|
|
917
942
|
fails there by design and two local-test tabs never see each other — run the two-client check
|
|
918
943
|
on the published game (or have the owner open their draft), and if multiplayer wasn't
|
|
919
944
|
exercised, say exactly that in your handoff instead of implying it was.
|
|
@@ -180,9 +180,10 @@ room.on("host", () => {/* host migrated — the new host takes over writing */})
|
|
|
180
180
|
de-duped) — but for a value you render every frame, read `room.shared.get(...)` in your loop (as
|
|
181
181
|
`updateHud` does) rather than wiring render state through the event.
|
|
182
182
|
|
|
183
|
-
## Custom events (shots, emotes
|
|
183
|
+
## Custom events (shots, emotes)
|
|
184
184
|
|
|
185
|
-
For one-off actions that aren't continuous state, use `send`.
|
|
185
|
+
For one-off actions that aren't continuous state, use `send`. (Text chat is NOT one of these —
|
|
186
|
+
it has its own channel, `room.chat`; see below.) The shooter judges the hit **locally**
|
|
186
187
|
against what it sees (favor-the-shooter) and announces it; the victim applies its own damage.
|
|
187
188
|
|
|
188
189
|
**`send` reaches only OTHER clients — it never echoes back to you.** So apply your own action's
|
|
@@ -210,6 +211,47 @@ if (!t) return;
|
|
|
210
211
|
const px = t.stateRaw.x, pz = t.stateRaw.z; // test against the raw latest
|
|
211
212
|
```
|
|
212
213
|
|
|
214
|
+
## Text chat
|
|
215
|
+
|
|
216
|
+
Chat has its own channel — **never build it on `send`**. The relay stamps the sender from the
|
|
217
|
+
verified session, sanitizes the text, and replays the recent history to late joiners; `send` gives
|
|
218
|
+
you none of that and lets a client post under any name it likes.
|
|
219
|
+
|
|
220
|
+
Install the overlay with `npx genex controller chat` (writes `src/controllers/chat/chat-overlay.ts`),
|
|
221
|
+
then wire it:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import { ChatOverlay } from "./controllers/chat/chat-overlay.ts";
|
|
225
|
+
|
|
226
|
+
const chat = new ChatOverlay({
|
|
227
|
+
room,
|
|
228
|
+
onTypingChange: (typing) => { keyboard.enabled = !typing; }, // ← REQUIRED, see below
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
**The one bug everybody ships: the game reads the keyboard while the player types.** Someone
|
|
233
|
+
types "was that a wall?" and their character walks, reverses and jumps. Either pass
|
|
234
|
+
`onTypingChange` (above) or check `chat.isTyping` in your input code. There is no third option.
|
|
235
|
+
|
|
236
|
+
Rolling your own UI instead? Three rules:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
room.chat.send("on my way"); // your OWN line comes back via on() — don't also append it
|
|
240
|
+
room.chat.on((m) => {
|
|
241
|
+
el.textContent = `${m.name}: ${m.text}`; // textContent, NEVER innerHTML — remote input
|
|
242
|
+
});
|
|
243
|
+
room.chat.history; // render on mount: a late joiner sees the conversation
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- **Your own lines echo back** (unlike `send`), in server order with the server's timestamp.
|
|
247
|
+
Appending locally on submit shows every message twice.
|
|
248
|
+
- **`m.name` is relay-verified** — safe to display, and a player cannot post under another name.
|
|
249
|
+
- **`m.text` is sanitized but not markup-escaped** — control and bidi characters are stripped by
|
|
250
|
+
the relay, but you must still render it as text.
|
|
251
|
+
|
|
252
|
+
Chat has its own rate budget (~2 lines/second per player), separate from gameplay traffic: a chat
|
|
253
|
+
flood cannot starve movement, and a busy game cannot starve chat.
|
|
254
|
+
|
|
213
255
|
## Persistence helper
|
|
214
256
|
|
|
215
257
|
```ts
|
|
@@ -129,9 +129,13 @@ everything twice.
|
|
|
129
129
|
visible; the lock may only ever engage from the Play/Resume click or a
|
|
130
130
|
gameplay canvas click (the phase binding `setPaused(phase !== "playing")` is
|
|
131
131
|
what guarantees this — check it rides `setPhase`, not the render loop).
|
|
132
|
-
Headless caveat: `requestPointerLock`
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
Headless caveat, measured on Chromium 151: `requestPointerLock` does NOT
|
|
133
|
+
throw — it locks, with or without a user gesture, so the lock and the
|
|
134
|
+
unlocked cue ARE yours to assert headless. What does not survive is the
|
|
135
|
+
both-axes look check: synthesised mouse movement cancels to a net zero
|
|
136
|
+
delta, so turning right then left proves nothing about direction. Assert the
|
|
137
|
+
wiring and the cue in a screenshot, and say plainly that confirming which way
|
|
138
|
+
the view turns needs one manual pass with a real mouse.
|
|
135
139
|
7. **Ask the scene the three things the screenshot cannot answer** (below). Run it
|
|
136
140
|
once, in the same browser you already have open.
|
|
137
141
|
|
|
@@ -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@
|
|
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
|
```
|