@genex-ai/cli-demo 0.26.0 → 0.28.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/dist/index.js
CHANGED
|
@@ -598,7 +598,7 @@ async function createDraftProject(opts) {
|
|
|
598
598
|
"Content-Type": "application/json",
|
|
599
599
|
Authorization: `Bearer ${token}`
|
|
600
600
|
},
|
|
601
|
-
body: JSON.stringify({ name })
|
|
601
|
+
body: JSON.stringify(opts.repoUrl ? { name, repoUrl: opts.repoUrl } : { name })
|
|
602
602
|
});
|
|
603
603
|
} catch (err) {
|
|
604
604
|
log.warn(`Couldn't reach the API at ${apiUrl} to create the project.`);
|
|
@@ -621,6 +621,9 @@ async function createDraftProject(opts) {
|
|
|
621
621
|
return null;
|
|
622
622
|
}
|
|
623
623
|
log.success(`Created project ${c.cyan(project.slug)}.`);
|
|
624
|
+
log.plain(
|
|
625
|
+
` your game's page (live now \u2014 share this link): ${c.cyan(`${new URL(dashboardUrl).origin}/draft/${project.slug}`)}`
|
|
626
|
+
);
|
|
624
627
|
if (project.playUrl) log.dim(` play (after preview/publish): ${project.playUrl}`);
|
|
625
628
|
log.dim(` dashboard: ${dashboardUrl}/dashboard`);
|
|
626
629
|
return {
|
|
@@ -901,21 +904,22 @@ async function runInit(opts) {
|
|
|
901
904
|
return;
|
|
902
905
|
}
|
|
903
906
|
const authBaseUrl = getAuthUrl(opts.authUrl);
|
|
904
|
-
let token;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
907
|
+
let token = await readUserToken(opts.envPath);
|
|
908
|
+
if (!token) {
|
|
909
|
+
try {
|
|
910
|
+
token = await authorize(authBaseUrl, {
|
|
911
|
+
log,
|
|
912
|
+
timeoutMs: opts.timeoutSec ? opts.timeoutSec * 1e3 : void 0
|
|
913
|
+
});
|
|
914
|
+
} catch (err) {
|
|
915
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
916
|
+
log.dim("Your workspace files were installed. Re-run `genex init` to finish authorizing.");
|
|
917
|
+
throw err;
|
|
918
|
+
}
|
|
919
|
+
const { path: tokenPath } = await writeUserToken(token, opts.envPath);
|
|
920
|
+
log.success(`Authorized. Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
|
|
921
|
+
log.plain("");
|
|
914
922
|
}
|
|
915
|
-
log.success("Authorized.");
|
|
916
|
-
const { path: tokenPath } = await writeUserToken(token, opts.envPath);
|
|
917
|
-
log.success(`Saved your token to ${c.cyan(tokenPath)} (${ENV_TOKEN_KEY}).`);
|
|
918
|
-
log.plain("");
|
|
919
923
|
await writeGitignore(process.cwd(), log);
|
|
920
924
|
const apiUrl = getApiUrl(opts.apiUrl);
|
|
921
925
|
const colyseusUrl = getColyseusUrl(opts.colyseusUrl);
|
|
@@ -924,15 +928,20 @@ async function runInit(opts) {
|
|
|
924
928
|
apiUrl,
|
|
925
929
|
token,
|
|
926
930
|
name: projectName,
|
|
931
|
+
repoUrl: opts.repo?.trim() || void 0,
|
|
927
932
|
colyseusUrl,
|
|
928
933
|
dashboardUrl: authBaseUrl,
|
|
929
934
|
log
|
|
930
935
|
});
|
|
931
|
-
if (meta) {
|
|
932
|
-
|
|
933
|
-
log.
|
|
934
|
-
|
|
936
|
+
if (!meta) {
|
|
937
|
+
log.plain("");
|
|
938
|
+
log.warn("Setup finished, but creating your game failed \u2014 fix the error above and re-run `genex init`.");
|
|
939
|
+
process.exitCode = 1;
|
|
940
|
+
return;
|
|
935
941
|
}
|
|
942
|
+
const { path: metaPath } = await writeProject(meta);
|
|
943
|
+
log.dim(` saved ${c.cyan(metaPath)}`);
|
|
944
|
+
await writeGameConfigFiles(meta, log);
|
|
936
945
|
log.plain("");
|
|
937
946
|
log.success("All set. \u{1F680}");
|
|
938
947
|
}
|
|
@@ -1259,12 +1268,13 @@ async function callPublish(ctx, commit, opts, log) {
|
|
|
1259
1268
|
}
|
|
1260
1269
|
async function pushSource(cwd, ctx, log) {
|
|
1261
1270
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
1271
|
+
const target = await fetchPushUrl(ctx, log);
|
|
1272
|
+
if (!target) return false;
|
|
1273
|
+
const { pushUrl, managed } = target;
|
|
1262
1274
|
const failed = () => {
|
|
1263
1275
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
1264
1276
|
return false;
|
|
1265
1277
|
};
|
|
1266
|
-
const pushUrl = await fetchPushUrl(ctx, log);
|
|
1267
|
-
if (!pushUrl) return false;
|
|
1268
1278
|
const gitDir = await fs9.mkdtemp(path10.join(os2.tmpdir(), "genex-source-"));
|
|
1269
1279
|
const base = { GIT_DIR: gitDir };
|
|
1270
1280
|
const ident = {
|
|
@@ -1277,7 +1287,7 @@ async function pushSource(cwd, ctx, log) {
|
|
|
1277
1287
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
1278
1288
|
await fs9.writeFile(
|
|
1279
1289
|
path10.join(gitDir, "info", "exclude"),
|
|
1280
|
-
// .env* are secrets —
|
|
1290
|
+
// .env* are secrets — never publish them; `!` keeps the non-secret template.
|
|
1281
1291
|
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
1282
1292
|
);
|
|
1283
1293
|
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path10.join(gitDir, "index-source") };
|
|
@@ -1290,7 +1300,14 @@ async function pushSource(cwd, ctx, log) {
|
|
|
1290
1300
|
const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
|
|
1291
1301
|
await run("git", ["update-ref", "refs/heads/main", commit], base);
|
|
1292
1302
|
const push = await run("git", ["push", "-q", pushUrl, "+refs/heads/main:main"], base);
|
|
1293
|
-
|
|
1303
|
+
if (push.code === 0) return true;
|
|
1304
|
+
if (!managed) {
|
|
1305
|
+
log.error(
|
|
1306
|
+
"Couldn't push to your repo. Check that you have push access (SSH key or git credentials) and that the repo exists."
|
|
1307
|
+
);
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
return failed();
|
|
1294
1311
|
} catch {
|
|
1295
1312
|
return failed();
|
|
1296
1313
|
} finally {
|
|
@@ -1322,7 +1339,7 @@ async function fetchPushUrl(ctx, log) {
|
|
|
1322
1339
|
log.error("The API didn't return a push URL.");
|
|
1323
1340
|
return null;
|
|
1324
1341
|
}
|
|
1325
|
-
return data.pushUrl;
|
|
1342
|
+
return { pushUrl: data.pushUrl, managed: data.managed !== false };
|
|
1326
1343
|
}
|
|
1327
1344
|
async function isDir2(p) {
|
|
1328
1345
|
try {
|
|
@@ -1516,6 +1533,8 @@ async function runPublish(opts) {
|
|
|
1516
1533
|
}
|
|
1517
1534
|
log.plain("");
|
|
1518
1535
|
log.success("Published. \u{1F389}");
|
|
1536
|
+
const dashboard = meta.dashboardOrigins?.[0];
|
|
1537
|
+
if (dashboard) log.plain(` world page (share this link): ${c.cyan(`${dashboard}/world/${meta.slug}`)}`);
|
|
1519
1538
|
if (meta.playUrl) log.dim(` play: ${meta.playUrl}`);
|
|
1520
1539
|
}
|
|
1521
1540
|
|
|
@@ -1559,6 +1578,11 @@ async function runPreview(opts) {
|
|
|
1559
1578
|
}
|
|
1560
1579
|
log.plain("");
|
|
1561
1580
|
log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
|
|
1581
|
+
const dashboard = meta.dashboardOrigins?.[0];
|
|
1582
|
+
if (dashboard) {
|
|
1583
|
+
const page = meta.status === "published" ? "world" : "draft";
|
|
1584
|
+
log.plain(` your game's page (share this link): ${c.cyan(`${dashboard}/${page}/${meta.slug}`)}`);
|
|
1585
|
+
}
|
|
1562
1586
|
}
|
|
1563
1587
|
|
|
1564
1588
|
// src/lib/sse.ts
|
|
@@ -1761,7 +1785,7 @@ async function poll(apiUrl, token, id, onProgress) {
|
|
|
1761
1785
|
function printHint(kind, files, log) {
|
|
1762
1786
|
const url = files[0]?.url ?? "";
|
|
1763
1787
|
const hint = {
|
|
1764
|
-
model: `
|
|
1788
|
+
model: `Standard GLB \u2014 load with GLTFLoader straight from the URL \u2014 see the genex-ai-model skill. url = "${url}"`,
|
|
1765
1789
|
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
|
|
1766
1790
|
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
|
|
1767
1791
|
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`
|
|
@@ -2055,6 +2079,8 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
|
2055
2079
|
${c.bold("Options for `init`")}
|
|
2056
2080
|
<name> Project name (positional; default: current directory name).
|
|
2057
2081
|
--name <name> Same as the positional name.
|
|
2082
|
+
--repo <url> Host the source in your own git repo (https/ssh) instead of a managed one;
|
|
2083
|
+
preview/publish push there with your git credentials.
|
|
2058
2084
|
--agents <list> Agents to install skills for (claude,codex,cursor; default: auto-detect).
|
|
2059
2085
|
--dir <path> Single destination workspace (overrides --agents).
|
|
2060
2086
|
--env <path> Token env file (default: ~/.genex/env).
|
|
@@ -2136,6 +2162,7 @@ function parseArgs(argv) {
|
|
|
2136
2162
|
"--colyseus-url",
|
|
2137
2163
|
"--agents",
|
|
2138
2164
|
"--name",
|
|
2165
|
+
"--repo",
|
|
2139
2166
|
"--title",
|
|
2140
2167
|
"--description",
|
|
2141
2168
|
"--categories",
|
|
@@ -2235,6 +2262,9 @@ function applyValueFlag(options, flag, value) {
|
|
|
2235
2262
|
case "--name":
|
|
2236
2263
|
options.name = value;
|
|
2237
2264
|
break;
|
|
2265
|
+
case "--repo":
|
|
2266
|
+
options.repo = value;
|
|
2267
|
+
break;
|
|
2238
2268
|
case "--title":
|
|
2239
2269
|
options.title = value;
|
|
2240
2270
|
break;
|
package/package.json
CHANGED
|
@@ -63,18 +63,13 @@ side — reads as broken at a glance.
|
|
|
63
63
|
|
|
64
64
|
## Load it into the scene
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
storage (same visuals, several times smaller — faster loads on every play).
|
|
68
|
-
Wire the decoder once — it's harmless for uncompressed files — then use
|
|
69
|
-
Three.js `GLTFLoader` with the URL the command printed (R2 sends the right
|
|
66
|
+
Use Three.js `GLTFLoader` with the URL the command printed (R2 sends the right
|
|
70
67
|
CORS headers, so cross-origin loading just works):
|
|
71
68
|
|
|
72
69
|
```ts
|
|
73
70
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
74
|
-
import { MeshoptDecoder } from "three/addons/libs/meshopt_decoder.module.js";
|
|
75
71
|
|
|
76
72
|
const loader = new GLTFLoader();
|
|
77
|
-
loader.setMeshoptDecoder(MeshoptDecoder); // required for genex model GLBs
|
|
78
73
|
// the URL `npx genex model` printed:
|
|
79
74
|
const MODEL_URL = "https://assets.genex.technology/generations/<id>/model-glb";
|
|
80
75
|
const gltf = await loader.loadAsync(MODEL_URL);
|
|
@@ -84,6 +79,10 @@ model.position.set(0, 0, 0);
|
|
|
84
79
|
scene.add(model);
|
|
85
80
|
```
|
|
86
81
|
|
|
82
|
+
Never swallow a failed load silently: if you wrap a generated-asset load in a
|
|
83
|
+
`catch`, `console.warn` the asset name in it — your own self-check reads the
|
|
84
|
+
console, and a bare `.catch(() => null)` hides a missing model from you too.
|
|
85
|
+
|
|
87
86
|
To place many copies, `model.clone()` per instance. For animated GLBs, drive
|
|
88
87
|
`gltf.animations` with a `THREE.AnimationMixer`.
|
|
89
88
|
|
|
@@ -34,9 +34,12 @@ example, the shared-object/ball code, rotation, and host usage. Read
|
|
|
34
34
|
## Install
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
|
-
npm i @genex-ai/multiplayer
|
|
37
|
+
npm i @genex-ai/multiplayer@^0.8.0
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
> Pin `@^0.8.0` (not a bare `npm i`): `inputs`/`onHostTick`, auto-reconnect, and the `reconnecting`
|
|
41
|
+
> events this skill relies on landed in 0.8. An older resolve would throw `room.onHostTick is not a function` at runtime.
|
|
42
|
+
|
|
40
43
|
This skill targets `@genex-ai/multiplayer` **≥ 0.8.0** (`objects`/`host` since 0.4; `matchmake()` since 0.5; presets + `score()`/`finish()` since 0.6; `createPrivate()`/`joinPrivate()` since 0.7; matchmake auto-retry + `retry()` since 0.7.1; auto-reconnect + `inputs`/`onHostTick` since 0.8).
|
|
41
44
|
|
|
42
45
|
## Trust model (say it plainly in your game's copy)
|
|
@@ -148,6 +151,7 @@ room.on("reconnecting", ({ attempt }) => showOverlay(`Reconnecting… (${attempt
|
|
|
148
151
|
room.on("reconnected", () => hideOverlay());
|
|
149
152
|
room.on("disconnect", (code) => {
|
|
150
153
|
// Terminal: server restart, revoked session, or the link never came back.
|
|
154
|
+
if (code === 4409) flushSaves(); // replaced by this player's OTHER tab/device — flush now
|
|
151
155
|
// To play again, read a FRESH token and connect() anew — never reuse the old auth object.
|
|
152
156
|
showMenu("Connection lost");
|
|
153
157
|
});
|
|
@@ -160,7 +164,9 @@ glide on; don't tear the scene down. A deliberate `room.leave()` never auto-reco
|
|
|
160
164
|
**One seat per player (enforced server-side):** joining the same game again — a second tab,
|
|
161
165
|
another device, or a page reload — instantly evicts the previous session (it gets
|
|
162
166
|
`disconnect`, code 4409). You never need to handle "the same player twice" and a reload
|
|
163
|
-
never leaves a ghost avatar behind.
|
|
167
|
+
never leaves a ghost avatar behind. If the evicted tab was the host and holds unsaved world
|
|
168
|
+
state, flush it in the `disconnect` handler (code 4409, above): that tab is still alive, so an
|
|
169
|
+
async `saveWorldState` completes — otherwise a debounced save in flight is lost.
|
|
164
170
|
|
|
165
171
|
## Which channel for which data
|
|
166
172
|
|
|
@@ -187,7 +193,10 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
|
|
|
187
193
|
`{ id, name, state, stateRaw }`: `state` is auto-smoothed (remotes) / live (you); `stateRaw` is
|
|
188
194
|
the raw latest (hit-tests, discrete values).
|
|
189
195
|
- `room.objects` — shared objects nobody owns until claimed (a ball, an NPC):
|
|
190
|
-
- `claim(id)` — take ownership (last claim wins; call on kick/contact).
|
|
196
|
+
- `claim(id)` — take ownership (last claim wins; call on kick/contact). Claiming is optimistic:
|
|
197
|
+
you own it locally the instant you call it, but if another player claimed the same tick the
|
|
198
|
+
server's last-claim-wins verdict can revoke you — a `set()` you sent before losing the race is
|
|
199
|
+
dropped. For contested objects, keep publishing while `isMine` stays true, not just once.
|
|
191
200
|
- `set(id, state)` — publish it (only lands while you own it; full flat object each call).
|
|
192
201
|
- `get(id)` → `{ id, owner, isMine, state, stateRaw }` or `undefined`. `state` is auto-smoothed
|
|
193
202
|
(or live if `isMine`); `stateRaw` is the raw latest.
|
|
@@ -212,6 +221,24 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
|
|
|
212
221
|
- `room.onHostTick(hz, cb)` — run a fixed simulation tick only while you are the host
|
|
213
222
|
(auto-starts/stops across host migration). Returns a disposer.
|
|
214
223
|
|
|
224
|
+
## Your message budget (every publish is one relay message)
|
|
225
|
+
|
|
226
|
+
Every `me.set`, `objects.set`, `objects.claim`, `send`, and `inputs.send` costs **one relay
|
|
227
|
+
message**, and the relay caps each connection at **~120 messages/second sustained** (drops
|
|
228
|
+
above that — you'll see a console warning "the relay dropped N of your messages"). The
|
|
229
|
+
budget math that matters:
|
|
230
|
+
|
|
231
|
+
- Your own state (`me.set`) at 15 Hz + ONE driven/owned moving object at 15 Hz = 30/s. Fine.
|
|
232
|
+
- The pattern that blows the budget: **republishing IDLE objects every tick.** A host that
|
|
233
|
+
owns several parked vehicles/props must NOT `objects.set` each of them at full tick rate —
|
|
234
|
+
publish an object **when it changed**, plus a low-rate keepalive (~1–2 Hz) so late joiners
|
|
235
|
+
converge. Unchanged pose ⇒ no message.
|
|
236
|
+
- If you see the drop warning, count your sends-per-tick: streams × tick-rate must stay well
|
|
237
|
+
under 120/s with headroom for claims and events.
|
|
238
|
+
- Over budget, the relay spreads the loss across ALL your streams (everything gets choppy at
|
|
239
|
+
once) rather than freezing one — so a single stuttering object is your cue to check the whole
|
|
240
|
+
budget, not just that object. The warning is the signal; don't design at the edge of the cap.
|
|
241
|
+
|
|
215
242
|
## The loop you must build (input → local → tick → render)
|
|
216
243
|
|
|
217
244
|
1. **Input mutates a local object only** (`me.x += …`). Never network on keypress.
|
|
@@ -259,7 +286,10 @@ details are in [references/realtime-patterns.md](references/realtime-patterns.md
|
|
|
259
286
|
## Host authority (scores, rounds, enemies)
|
|
260
287
|
|
|
261
288
|
One client is the `host`. Let *only* the host write agreed state and simulate shared enemies, so
|
|
262
|
-
there's a single source of truth:
|
|
289
|
+
there's a single source of truth. **But: a host is only the authority for objects it OWNS.**
|
|
290
|
+
If another player owns/drives an object (their claim landed), the host renders it from the
|
|
291
|
+
stream like everyone else — a host branch that pins "its" objects to a local pose without
|
|
292
|
+
checking `owner`/`isMine` shows every other player's driving as a frozen object:
|
|
263
293
|
|
|
264
294
|
```ts
|
|
265
295
|
if (room.isHost) room.shared.set("round", nextRound); // only the host advances the round
|
|
@@ -364,10 +394,12 @@ host-driven saving works as long as ANY account is in the room.
|
|
|
364
394
|
|
|
365
395
|
## Checklist
|
|
366
396
|
|
|
367
|
-
- [ ] `npm i @genex-ai/multiplayer
|
|
397
|
+
- [ ] `npm i @genex-ai/multiplayer@^0.8.0` (auto-reconnect, `inputs`, `onHostTick` — pin `^0.8.0`, a bare install can resolve older); config wired into the build.
|
|
368
398
|
- [ ] `reconnecting`/`reconnected`/`disconnect` render an overlay (don't tear the scene down).
|
|
369
399
|
- [ ] Numbers rounded (~2 decimals) before `me.set`/`objects.set`.
|
|
370
400
|
- [ ] Contested (sustained-contact) objects use the host-physics pattern, not claim-on-touch.
|
|
401
|
+
- [ ] Idle/unchanged objects republish at ≤2 Hz keepalive, never every tick (message budget).
|
|
402
|
+
- [ ] Host renders objects OWNED BY OTHERS from the stream (authority follows ownership).
|
|
371
403
|
- [ ] `connect()` runs AFTER `await waitForPlayer()` (never `waitForAuth()` — guests would
|
|
372
404
|
hang) and passes `auth: getColyseusAuth()!` (the relay rejects tokenless joins —
|
|
373
405
|
see `genex-threejs-embed-auth`).
|
|
@@ -81,29 +81,53 @@ The `genex controller` controllers are **local-only physics** — each player si
|
|
|
81
81
|
OWN rig (self-authoritative, zero latency). Networking them is publish-and-playback, never
|
|
82
82
|
remote simulation:
|
|
83
83
|
|
|
84
|
+
The vendored controllers expose their pose as `currPos` (a `THREE.Vector3`) and `currQuat` (a
|
|
85
|
+
`THREE.Quaternion`) — **not** `.position` / `.quaternion` — plus boolean state getters. Character
|
|
86
|
+
animation is driven by **five booleans**, not a single enum; publish the booleans and let remotes
|
|
87
|
+
reconstruct the animation. There is **no** `rig.animState`, no `remoteAnimator.play(...)`, and no
|
|
88
|
+
vehicle `wheelSpinPhase` getter.
|
|
89
|
+
|
|
84
90
|
```ts
|
|
85
|
-
// You: after your controller's update, on the fixed tick (~15Hz)
|
|
91
|
+
// You: after your controller's update(), on the fixed tick (~15Hz).
|
|
92
|
+
const p = character.currPos; // THREE.Vector3 (Vehicle/Drone: same getters)
|
|
93
|
+
const q = character.currQuat; // THREE.Quaternion
|
|
86
94
|
room.me.set({
|
|
87
|
-
x: r2(
|
|
88
|
-
q:
|
|
89
|
-
|
|
90
|
-
|
|
95
|
+
x: r2(p.x), y: r2(p.y), z: r2(p.z),
|
|
96
|
+
q: [r2(q.x), r2(q.y), r2(q.z), r2(q.w)], // quaternion array — never a scalar yaw
|
|
97
|
+
// character animation = 5 booleans (the CharacterController exposes each as a getter):
|
|
98
|
+
g: character.isOnGround, f: character.isFalling, m: character.isMoving,
|
|
99
|
+
r: character.runActive, j: character.jumpActive,
|
|
91
100
|
});
|
|
92
101
|
|
|
93
|
-
// Remote players:
|
|
94
|
-
//
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
102
|
+
// Remote players: a VISUAL-ONLY avatar — NO Rapier body, NO controller instance for remotes.
|
|
103
|
+
// Position/rotation from smoothed state; animation from the synced flags via the avatar's own
|
|
104
|
+
// update(flags, dt). The character-controller skill's animations reference owns the flag set.
|
|
105
|
+
const pl = room.players.get(id)!;
|
|
106
|
+
remoteAvatar.group.position.set(pl.state.x, pl.state.y, pl.state.z);
|
|
107
|
+
remoteAvatar.group.quaternion.fromArray(pl.state.q);
|
|
108
|
+
const raw = pl.stateRaw; // discrete flags: read RAW, never smoothed
|
|
109
|
+
remoteAvatar.update(
|
|
110
|
+
{ isOnGround: !!raw.g, isFalling: !!raw.f, isMoving: !!raw.m, runActive: !!raw.r, jumpActive: !!raw.j },
|
|
111
|
+
dt,
|
|
112
|
+
);
|
|
99
113
|
```
|
|
100
114
|
|
|
101
115
|
What to publish per controller:
|
|
102
116
|
|
|
103
|
-
- **character**: `x/y/z`, `
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
117
|
+
- **character**: `currPos` → `x/y/z`, `currQuat` → `q`, and the five booleans above
|
|
118
|
+
(`isOnGround`/`isFalling`/`isMoving`/`runActive`/`jumpActive`). Remotes rebuild the animation
|
|
119
|
+
with `avatar.update(flags, dt)` — see the `genex-threejs-character-controller` animations
|
|
120
|
+
reference for the flag set (single source of truth; don't invent a `play(anim)` call).
|
|
121
|
+
- **vehicle**: body `currPos` → `x/y/z` + `currQuat` → `q`. For visible steering, publish the
|
|
122
|
+
front wheel's `car.wheels.get(frontWheelId)?.steerAngle`; spin the wheels on remotes
|
|
123
|
+
procedurally from the body's speed (position delta) — there is no spin-phase getter, and you
|
|
124
|
+
never sync per-wheel transforms.
|
|
125
|
+
- **drone**: `currPos` → `x/y/z`, `currQuat` → `q`, and `drone.hoverThrottle` if the rotor visual
|
|
126
|
+
needs it. Remote rotor spin is procedural, like vehicle wheels.
|
|
127
|
+
|
|
128
|
+
> Remote **visuals** for vehicle wheels / drone rotors are author-built (a spinning mesh you drive
|
|
129
|
+
> from synced speed/throttle) — the controllers build those internally from a live Rapier body,
|
|
130
|
+
> which remotes don't have. Only the body pose + the flags/params above come over the wire.
|
|
107
131
|
|
|
108
132
|
Player-vs-player physical contact (bumping cars) stays approximate at this tier — each
|
|
109
133
|
client is authoritative over itself, so contacts are cosmetic. If a game's core loop IS
|