@genex-ai/cli-demo 0.4.0 → 0.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Set up your ~/.claude workspace, authorize, create a game project, generate AI assets, and publish (genex CLI).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,132 @@
1
+ ---
2
+ name: genex-threejs-multiplayer
3
+ description: Implement realtime multiplayer for Genex Three.js games with `@genex-ai/multiplayer`. Use whenever 2+ players share a world — movement sync, shared scores/rounds, shots/emotes, presence, and persistent worlds. MANDATORY whenever a game has multiplayer: load this before writing any networking code.
4
+ ---
5
+
6
+ # Genex Three.js Multiplayer
7
+
8
+ `@genex-ai/multiplayer` is a **relay**: whatever you write to `me` or `shared` is
9
+ synced to everyone; everything else stays local. The server runs **no physics, no
10
+ prediction, and no interpolation** — you run your own game logic and add your own
11
+ smoothing. This skill covers the API, the smoothing you must add yourself
12
+ (interpolation + prediction), config wiring, and persistent worlds.
13
+
14
+ **This skill is mandatory for any multiplayer game.** Load it before you write a
15
+ single line of networking code. Naïvely snapping every remote player to their last
16
+ raw network position looks terrible — interpolation/prediction are not optional polish.
17
+
18
+ Read [references/realtime-patterns.md](references/realtime-patterns.md) for the
19
+ copy-paste `RemoteInterpolator` + local-prediction helpers and the persistence/config code.
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ npm i @genex-ai/multiplayer
25
+ ```
26
+
27
+ ## Connect
28
+
29
+ Pick your own per-player state shape (any JSON). `room` is the **project slug**
30
+ (printed by `genex init`) — same id = same room, different ids are fully isolated.
31
+
32
+ ```ts
33
+ import { connect } from "@genex-ai/multiplayer";
34
+
35
+ type State = { x: number; z: number; yaw: number }; // YOUR per-player state
36
+
37
+ const room = await connect<State>({
38
+ url: GENEX.colyseusUrl, // e.g. "wss://demo-colyseus.glotech.world" — see config wiring below
39
+ room: GENEX.slug, // the project slug — everyone with this id shares a room
40
+ name: "ada", // optional display name
41
+ });
42
+ ```
43
+
44
+ ## API surface (exact — do not invent methods)
45
+
46
+ - `room.id` — your own session id.
47
+ - `room.me.set(state)` — publish your state. **Replaces it wholesale** (send the full
48
+ object, not a partial). Call on a **fixed 10–20 Hz tick**, never per render frame —
49
+ one `set` = one network message.
50
+ - `room.players` — a **fresh `Map` each read**, and it **includes you**. Skip yourself
51
+ with `if (id === room.id) continue;`. Each value is `{ id, name, state }`.
52
+ - `room.shared.get(key)` / `room.shared.set(key, value)` / `room.shared.keys()` —
53
+ a key/value store (any JSON) synced to everyone. Use for world state, scores, round.
54
+ - `room.on(event, cb)` → returns an **unsubscribe** function. Events: `'join'` /
55
+ `'change'` `(id, state)` (both also fire for **you** on connect — filter `id === room.id`),
56
+ `'leave'` `(id)`, `'shared'` `(key, value)`, and any custom event name from `send`.
57
+ - `room.send(type, payload)` — fire-and-forget to all **other** clients, not stored in
58
+ state. Use for shots, emotes, chat, pings.
59
+ - `room.leave()` — leave the room.
60
+
61
+ ## The loop you must build (input → local → tick → render)
62
+
63
+ 1. **Input mutates a local object only** (`me.x += …`). Never network on keypress.
64
+ 2. **A fixed tick publishes it:** `setInterval(() => room.me.set(me), 66)` (~15 Hz).
65
+ 3. **Render at your own framerate**, drawing:
66
+ - **yourself** from your *local predicted* state (zero latency — never from the
67
+ echoed server copy of you), and
68
+ - **every other player** through an **interpolator** (render ~100 ms in the past and
69
+ lerp between their last two snapshots) — never snap to raw network state.
70
+ 4. **Create-or-reuse one mesh per id**; remove a player's mesh on `'leave'`.
71
+
72
+ See [references/realtime-patterns.md](references/realtime-patterns.md) for the ready-made
73
+ `RemoteInterpolator` helper and a complete movement example.
74
+
75
+ ## Config wiring (required — the browser can't read `.genex/project.json`)
76
+
77
+ The game runs in the browser; `.genex/project.json` (which holds `slug`, `colyseusUrl`,
78
+ `apiUrl`) is gitignored and not bundled. Surface those values to the client explicitly —
79
+ e.g. a tiny committed `src/genex.config.ts`:
80
+
81
+ ```ts
82
+ // src/genex.config.ts — values from `.genex/project.json` (printed by `genex init`)
83
+ export const GENEX = {
84
+ slug: "my-game-slug",
85
+ colyseusUrl: "wss://demo-colyseus.glotech.world",
86
+ apiUrl: "https://demo-api.glotech.world",
87
+ } as const;
88
+ ```
89
+
90
+ Read `.genex/project.json` once and fill these in. (A Vite `define` / `.env` with
91
+ `VITE_` vars works too — the point is the values must end up in the built JS.)
92
+
93
+ ## Persistent worlds (optional — survives restarts)
94
+
95
+ The relay is in-memory: room state is gone when everyone leaves or the server restarts.
96
+ For a world that persists, save/load one JSON blob keyed by the project slug:
97
+
98
+ ```ts
99
+ // load on boot
100
+ const { data } = await fetch(`${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`).then(r => r.json());
101
+ initWorld(data ?? defaultWorld());
102
+
103
+ // save from ONE authority (e.g. the host client) to avoid races; max 1 MB, last-write-wins
104
+ fetch(`${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`, {
105
+ method: "PUT",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify(world),
108
+ });
109
+ ```
110
+
111
+ `GET` returns `{ data }` (or `{ data: null }` if never saved). It's public (no auth) and
112
+ size-capped at 1 MB. Don't save every frame — debounce, and elect a single writer.
113
+
114
+ ## Checklist
115
+
116
+ - [ ] `npm i @genex-ai/multiplayer`, and config values wired into the client build.
117
+ - [ ] `room` is the **project slug**.
118
+ - [ ] `me.set` on a fixed **10–20 Hz** tick (not per frame); full object each time.
119
+ - [ ] Skip yourself in `room.players` (`id === room.id`).
120
+ - [ ] Remote players go through interpolation; **you** render from local predicted state.
121
+ - [ ] Create-or-reuse a mesh per id; remove it on `'leave'`.
122
+ - [ ] If the world should persist, save from one authority via the state API.
123
+
124
+ ## Troubleshooting
125
+
126
+ - **Other players stutter / teleport** — you're snapping to raw network state. Use the
127
+ `RemoteInterpolator` (render-delay + lerp).
128
+ - **My own movement feels laggy** — you're rendering yourself from the echoed server
129
+ state. Render yourself from your local predicted object instead.
130
+ - **Too much traffic / desync** — you're calling `me.set` per frame. Move it to the tick.
131
+ - **Players never appear** — `room.players` is empty until the first state patch lands;
132
+ read it in the render loop, and remember it includes you (filter your own id).
@@ -0,0 +1,202 @@
1
+ # Realtime patterns: interpolation, prediction, persistence
2
+
3
+ The `@genex-ai/multiplayer` relay gives you the **latest** state of each player and
4
+ nothing else — no timestamps, no smoothing. These are the pieces you add on top so the
5
+ game feels good. All code is plain TypeScript; nothing here is provided by the package.
6
+
7
+ ## Why you need this
8
+
9
+ - You publish `me.set` at ~15 Hz but render at 60 fps. If you snap each remote player to
10
+ their last received position, they move in visible 15 Hz steps and jump on packet
11
+ jitter. **Fix: interpolation** — render each remote player slightly in the past and
12
+ lerp between their two surrounding snapshots.
13
+ - Your *own* avatar would feel laggy if you waited for the server to echo your position
14
+ back. **Fix: prediction** — apply input locally and render yourself from that
15
+ immediately; the tick still publishes it for everyone else.
16
+
17
+ ## RemoteInterpolator (render-delay interpolation)
18
+
19
+ Buffers timestamped snapshots per remote player and samples a smoothed value ~100 ms in
20
+ the past. Stamp snapshots with the local clock on arrival (we don't get server time).
21
+
22
+ ```ts
23
+ // interpolation.ts
24
+ const RENDER_DELAY_MS = 100; // render remotes this far in the past; raise if jittery
25
+ const BUFFER_MS = 1000; // keep ~1s of history
26
+
27
+ type Vec = { x: number; y: number; z: number; yaw: number };
28
+ type Snap = { t: number; v: Vec };
29
+
30
+ function lerp(a: number, b: number, k: number) {
31
+ return a + (b - a) * k;
32
+ }
33
+ // shortest-arc angle lerp (radians) — avoids spinning the wrong way across ±π
34
+ function lerpAngle(a: number, b: number, k: number) {
35
+ let d = (b - a) % (Math.PI * 2);
36
+ if (d > Math.PI) d -= Math.PI * 2;
37
+ if (d < -Math.PI) d += Math.PI * 2;
38
+ return a + d * k;
39
+ }
40
+
41
+ export class RemoteInterpolator {
42
+ private buffers = new Map<string, Snap[]>();
43
+
44
+ /** Call whenever you read a remote player's latest state (e.g. each frame, or on 'change'). */
45
+ push(id: string, v: Vec, now = performance.now()) {
46
+ let buf = this.buffers.get(id);
47
+ if (!buf) this.buffers.set(id, (buf = []));
48
+ const last = buf[buf.length - 1];
49
+ // de-dupe identical repeats (players is re-read every frame)
50
+ if (last && last.v.x === v.x && last.v.z === v.z && last.v.yaw === v.yaw && last.v.y === v.y) return;
51
+ buf.push({ t: now, v });
52
+ const cutoff = now - BUFFER_MS;
53
+ while (buf.length > 2 && buf[0].t < cutoff) buf.shift();
54
+ }
55
+
56
+ /** Smoothed value to render this frame, or null if we have nothing yet. */
57
+ sample(id: string, now = performance.now()): Vec | null {
58
+ const buf = this.buffers.get(id);
59
+ if (!buf || buf.length === 0) return null;
60
+ const target = now - RENDER_DELAY_MS;
61
+ if (buf.length === 1 || target <= buf[0].t) return buf[0].v;
62
+ if (target >= buf[buf.length - 1].t) return buf[buf.length - 1].v; // clamp (no extrapolation)
63
+ for (let i = 0; i < buf.length - 1; i++) {
64
+ const a = buf[i], b = buf[i + 1];
65
+ if (target >= a.t && target <= b.t) {
66
+ const k = (target - a.t) / (b.t - a.t || 1);
67
+ return {
68
+ x: lerp(a.v.x, b.v.x, k),
69
+ y: lerp(a.v.y, b.v.y, k),
70
+ z: lerp(a.v.z, b.v.z, k),
71
+ yaw: lerpAngle(a.v.yaw, b.v.yaw, k),
72
+ };
73
+ }
74
+ }
75
+ return buf[buf.length - 1].v;
76
+ }
77
+
78
+ remove(id: string) {
79
+ this.buffers.delete(id);
80
+ }
81
+ }
82
+ ```
83
+
84
+ **Tuning:** `RENDER_DELAY_MS` ≈ one tick interval + jitter (100 ms is safe for a 15 Hz
85
+ tick). Lower = more responsive but more stutter on jitter. For sudden teleports (respawn,
86
+ warp) clear that player's buffer and snap, so you don't lerp across the whole map.
87
+
88
+ ## Complete movement example
89
+
90
+ ```ts
91
+ import * as THREE from "three";
92
+ import { connect } from "@genex-ai/multiplayer";
93
+ import { GENEX } from "./genex.config";
94
+ import { RemoteInterpolator } from "./interpolation";
95
+
96
+ type S = { x: number; z: number; yaw: number };
97
+
98
+ const room = await connect<S>({ url: GENEX.colyseusUrl, room: GENEX.slug });
99
+ const interp = new RemoteInterpolator();
100
+
101
+ // --- local player: prediction. Input mutates this; we render yourself from it. ---
102
+ const me = { x: 0, z: 0, yaw: 0 };
103
+ addEventListener("keydown", (e) => {
104
+ if (e.key === "ArrowLeft") me.yaw += 0.1;
105
+ if (e.key === "ArrowRight") me.yaw -= 0.1;
106
+ if (e.key === "ArrowUp") { me.x += Math.sin(me.yaw); me.z += Math.cos(me.yaw); }
107
+ });
108
+
109
+ // --- publish at ~15 Hz (NOT per frame) ---
110
+ const tick = setInterval(() => room.me.set(me), 66);
111
+
112
+ // --- meshes, one per id ---
113
+ const meshes = new Map<string, THREE.Object3D>();
114
+ function meshFor(id: string) {
115
+ let m = meshes.get(id);
116
+ if (!m) { m = new THREE.Mesh(boxGeo, boxMat); scene.add(m); meshes.set(id, m); }
117
+ return m;
118
+ }
119
+ room.on("leave", (id) => {
120
+ const m = meshes.get(id);
121
+ if (m) { scene.remove(m); meshes.delete(id); }
122
+ interp.remove(id);
123
+ });
124
+
125
+ function frame() {
126
+ // yourself: render from local predicted state (zero latency)
127
+ meshFor(room.id).position.set(me.x, 0, me.z);
128
+ meshFor(room.id).rotation.y = me.yaw;
129
+
130
+ // everyone else: feed the interpolator, render the smoothed sample
131
+ for (const [id, p] of room.players) {
132
+ if (id === room.id) continue;
133
+ interp.push(id, { x: p.state.x ?? 0, y: 0, z: p.state.z ?? 0, yaw: p.state.yaw ?? 0 });
134
+ const v = interp.sample(id);
135
+ if (!v) continue;
136
+ const m = meshFor(id);
137
+ m.position.set(v.x, v.y, v.z);
138
+ m.rotation.y = v.yaw;
139
+ }
140
+
141
+ renderer.render(scene, camera);
142
+ requestAnimationFrame(frame);
143
+ }
144
+ frame();
145
+
146
+ // cleanup if you ever tear down: clearInterval(tick); room.leave();
147
+ ```
148
+
149
+ ## Custom events (shots, emotes, chat)
150
+
151
+ For one-off actions that aren't part of continuous state, use `send` — it doesn't belong
152
+ in `me.set` (which is your *current* state, not events):
153
+
154
+ ```ts
155
+ // shooter:
156
+ room.send("shot", { from: room.id, x: me.x, z: me.z, yaw: me.yaw });
157
+
158
+ // everyone else:
159
+ room.on("shot", (msg: any) => spawnTracer(msg.x, msg.z, msg.yaw));
160
+ ```
161
+
162
+ ## Shared room state (scores, round, world)
163
+
164
+ `shared` is for state everyone agrees on, not per-player position:
165
+
166
+ ```ts
167
+ room.shared.set("round", (Number(room.shared.get("round")) || 0) + 1);
168
+ room.on("shared", (key, value) => { if (key === "score") updateScoreboard(value); });
169
+ ```
170
+
171
+ Any client can write any key (demo relay has no auth). For authority (who may change the
172
+ score / advance the round), pick one client — e.g. the first/lowest session id present —
173
+ and let only it write.
174
+
175
+ ## Persistence helper
176
+
177
+ ```ts
178
+ // persistence.ts
179
+ export async function loadWorld<T>(fallback: T): Promise<T> {
180
+ try {
181
+ const { data } = await fetch(`${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`).then(r => r.json());
182
+ return (data as T) ?? fallback;
183
+ } catch { return fallback; }
184
+ }
185
+
186
+ let saveTimer: ReturnType<typeof setTimeout> | null = null;
187
+ export function saveWorld(world: unknown) {
188
+ // debounce: at most one PUT per second, from ONE authority client
189
+ if (saveTimer) return;
190
+ saveTimer = setTimeout(() => {
191
+ saveTimer = null;
192
+ fetch(`${GENEX.apiUrl}/api/projects/${GENEX.slug}/state`, {
193
+ method: "PUT",
194
+ headers: { "Content-Type": "application/json" },
195
+ body: JSON.stringify(world),
196
+ }).catch(() => {});
197
+ }, 1000);
198
+ }
199
+ ```
200
+
201
+ State is one JSON blob per project, max 1 MB, last-write-wins. Save from a single
202
+ authority (host) so concurrent writers don't clobber each other.
@@ -36,6 +36,11 @@ map, execution order, and acceptance gate.
36
36
  | exposure, tone mapping, color grading, LUTs | `$genex-threejs-exposure-color-grading` |
37
37
  | render-target ownership, pass ordering, depth/normal/history signals | `$genex-threejs-image-pipeline` |
38
38
  | fixed-view captures, seed sweeps, browser and GPU evidence | `$genex-threejs-visual-validation` |
39
+ | realtime multiplayer: movement sync, shared state, presence, shots/emotes, persistence | `$genex-threejs-multiplayer` |
40
+
41
+ **Multiplayer is mandatory routing:** if the game has 2+ players sharing a world, loading
42
+ `$genex-threejs-multiplayer` is **required** before any networking code — the relay does no
43
+ interpolation/prediction, and that skill is how you add them.
39
44
 
40
45
  ## Real (AI-generated) assets — `npx genex` commands
41
46
 
@@ -58,6 +63,20 @@ Prefer the **procedural** skills above for abstract/parametric/animated systems
58
63
  **`npx genex` generators** for concrete, describable, photoreal assets. They complement
59
64
  each other.
60
65
 
66
+ **Generate a core asset set by default — don't wait to be asked.** For any game that
67
+ needs concrete objects or surfaces, decide a small core set from the concept and start
68
+ generating it **up front, in parallel** (each `npx genex` is an independent ~1-minute
69
+ job — launch them concurrently in the background, then scaffold the scene while they run
70
+ and wire each asset in as it lands, with a procedural placeholder as fallback until then):
71
+
72
+ - the **hero model** the player controls or chases (`npx genex model`),
73
+ - one key **texture** for the ground/main surface (`npx genex texture --terrain` for ground),
74
+ - a **skybox** when the scene is outdoors (`npx genex skybox`),
75
+ - a **sfx** or two for the core action/feedback (`npx genex sfx`).
76
+
77
+ Skip generation only for purely abstract/geometric games. Keep the set small and
78
+ concept-driven — a richer first build beats a grey-box one.
79
+
61
80
  ## Routing rules
62
81
 
63
82
  - Start from the playable game target: player verb, scene scale, camera distance,
@@ -51,3 +51,11 @@ publishing or multiplayer, inspect the project first. Prefer clean boundaries:
51
51
  - serializable player/session state;
52
52
  - explicit asset loading paths;
53
53
  - one clear start function for local preview and hosted launch.
54
+
55
+ **Multiplayer is mandatory routing.** If the game has 2+ players sharing a world,
56
+ load `$genex-threejs-multiplayer` **before writing any networking code** — it is not
57
+ optional. The `@genex-ai/multiplayer` relay syncs `me`/`shared` but does **no**
58
+ physics, prediction, or interpolation; that skill is how you add interpolation for
59
+ remote players, prediction for the local player, config wiring, and (for save-state
60
+ games) the persistent-world API. Use only the APIs that skill documents — do not
61
+ invent transport methods.