@genex-ai/cli-demo 0.80.0-dev.211 → 0.80.2-dev.213

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
@@ -1109,7 +1109,7 @@ async function writeProject(meta, cwd = process.cwd()) {
1109
1109
  import fs7 from "fs/promises";
1110
1110
  import path7 from "path";
1111
1111
  var DEFAULT_DASHBOARD_ORIGIN = new URL(DEFAULT_AUTH_URL).origin;
1112
- function renderGenexConfig() {
1112
+ function renderGenexConfig(slug) {
1113
1113
  return `// src/genex.config.ts \u2014 written by \`genex init\`. DO NOT hardcode URLs here.
1114
1114
  //
1115
1115
  // Build once, run anywhere. The games host injects the serving environment into
@@ -1117,14 +1117,21 @@ function renderGenexConfig() {
1117
1117
  // bundle), so ONE build is correct on every stand \u2014 promoting a game is a copy,
1118
1118
  // never a rebuild, and an immutable bundle can never pin the wrong environment.
1119
1119
  // Vite env still overrides, but is loaded ONLY by \`npm run dev\`:
1120
- // .env -> VITE_GENEX_SLUG (this game's identity; committed)
1120
+ // .env -> VITE_GENEX_SLUG (local override for \`npm run dev\`)
1121
1121
  // .env.development.local -> local-stack URL overrides (dev mode ONLY; gitignored)
1122
+ //
1123
+ // The slug is ALSO baked as a literal below, on purpose: \`genex preview/publish\`
1124
+ // excludes .env from the source push (it's treated as a secret file), so a clone of
1125
+ // the published repo has no .env. Reading the slug from env ALONE would then build
1126
+ // \`slug: undefined\` -> the guest-session call posts {} -> 400 -> guests can't play a
1127
+ // remixed game. The literal keeps a bare clone rebuilding into a working game; the
1128
+ // env var still wins locally.
1122
1129
  const injected: { apiUrl?: string; dashboardOrigins?: string[] } =
1123
1130
  (typeof window !== "undefined" && (window as { __GENEX__?: unknown }).__GENEX__) as
1124
1131
  | { apiUrl?: string; dashboardOrigins?: string[] }
1125
1132
  | undefined ?? {};
1126
1133
  export const GENEX = {
1127
- slug: import.meta.env.VITE_GENEX_SLUG as string,
1134
+ slug: (import.meta.env.VITE_GENEX_SLUG as string | undefined) ?? ${JSON.stringify(slug)},
1128
1135
  apiUrl:
1129
1136
  (import.meta.env.VITE_GENEX_API_URL as string | undefined) ??
1130
1137
  injected.apiUrl ??
@@ -1170,7 +1177,7 @@ async function writeIfAbsent(file, content, log) {
1170
1177
  }
1171
1178
  async function writeGameConfigFiles(meta, log, cwd = process.cwd()) {
1172
1179
  await fs7.mkdir(path7.join(cwd, "src"), { recursive: true });
1173
- await writeIfAbsent(path7.join(cwd, "src", "genex.config.ts"), renderGenexConfig(), log);
1180
+ await writeIfAbsent(path7.join(cwd, "src", "genex.config.ts"), renderGenexConfig(meta.slug), log);
1174
1181
  await writeIfAbsent(path7.join(cwd, ".env"), renderSlugEnv(meta.slug), log);
1175
1182
  const overrides = renderDevOverrides(meta);
1176
1183
  if (overrides) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "0.80.0-dev.211",
3
+ "version": "0.80.2-dev.213",
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": {
@@ -88,6 +88,36 @@ type ResolvedMovementInput = {
88
88
  /** Crouch input interpretation — see {@link CharacterControllerOptions.crouchMode}. */
89
89
  export type CrouchMode = "toggle" | "hold";
90
90
 
91
+ /**
92
+ * The over-the-network snapshot of a character. Publish it every fixed tick with
93
+ * `room.me.set(character.netState())`; apply a remote's smoothed copy with
94
+ * `applyNetState(remoteObject, players.get(id).state)`.
95
+ *
96
+ * `y` is the SETTLED body height while grounded: the float-spring's idle up/down
97
+ * oscillation is removed, so a standing remote player does NOT bob in place.
98
+ * Publish this — never `currPos`, which is the raw physics capsule and bobs on
99
+ * its suspension spring. Numbers are rounded to ~cm; rotation is a 4-number
100
+ * quaternion (never a scalar yaw — that lerps the long way across ±π). The six
101
+ * booleans drive a remote's CharacterAnimations.
102
+ */
103
+ export interface NetState {
104
+ x: number;
105
+ y: number;
106
+ z: number;
107
+ q: [number, number, number, number];
108
+ onGround: boolean;
109
+ falling: boolean;
110
+ moving: boolean;
111
+ running: boolean;
112
+ jumping: boolean;
113
+ crouching: boolean;
114
+ }
115
+
116
+ /** Round a networked number to ~cm precision (raw floats serialize as 17-digit JSON). */
117
+ function roundNet(v: number): number {
118
+ return Math.round(v * 100) / 100;
119
+ }
120
+
91
121
  export interface CharacterRecoveryEvent {
92
122
  reason: "non-finite" | "out-of-bounds";
93
123
  position: { x: number; y: number; z: number };
@@ -484,6 +514,8 @@ export class CharacterController {
484
514
  private slideFrictionCoef = 0;
485
515
  private standingPointFriction = 0;
486
516
  private readonly standingPoint = new THREE.Vector3();
517
+ /** Scratch for {@link netPos} — the float-oscillation-free networked position. */
518
+ private readonly _netPos = new THREE.Vector3();
487
519
  private readonly characterMassImpulse = new THREE.Vector3();
488
520
  private readonly movingObjectPosition = new THREE.Vector3();
489
521
  private readonly movingObjectVelocity = new THREE.Vector3();
@@ -648,6 +680,29 @@ export class CharacterController {
648
680
  get currPos(): THREE.Vector3 {
649
681
  return this.currentPos;
650
682
  }
683
+ /**
684
+ * Body position to PUBLISH over the network. Identical to {@link currPos} while
685
+ * airborne (jump/fall arcs must survive), but while grounded the up-axis
686
+ * component is snapped to the SETTLED float height above the ground — removing
687
+ * the suspension-spring idle oscillation that otherwise makes a standing remote
688
+ * player bob up and down. Publish this (via {@link netState}), never `currPos`.
689
+ * Live vector — copy it if you keep it. Follows custom gravity (up = bodyYAxis).
690
+ */
691
+ get netPos(): THREE.Vector3 {
692
+ this._netPos.copy(this.currentPos);
693
+ if (this._isOnGround) {
694
+ // currPos bobs because the float spring holds the capsule a VARYING gap
695
+ // above the ground each step. standingPoint is the STATIC ground contact;
696
+ // the settled center→ground gap (along up) is groundFloatingDistance −
697
+ // rayOriginOffset. Shift the up component by (settled − current) so the
698
+ // published height is the settled one, not the oscillating one.
699
+ const gapNow =
700
+ this._netPos.dot(this.characterYAxis) - this.standingPoint.dot(this.characterYAxis);
701
+ const gapSettled = this.groundFloatingDistance - this.rayOriginOffset;
702
+ this._netPos.addScaledVector(this.characterYAxis, gapSettled - gapNow);
703
+ }
704
+ return this._netPos;
705
+ }
651
706
  /** Body rotation (this step). Live quaternion. */
652
707
  get currQuat(): THREE.Quaternion {
653
708
  return this.currentQuat;
@@ -799,6 +854,32 @@ export class CharacterController {
799
854
  // Public methods
800
855
  // ────────────────────────────────────────────────────────────────────────
801
856
 
857
+ /**
858
+ * The snapshot to PUBLISH for multiplayer, in one call:
859
+ * `room.me.set(character.netState())` — on the fixed 10–20 Hz tick, never per
860
+ * frame. Position is {@link netPos} (float-oscillation-free, so remotes never
861
+ * bob), rotation is a 4-number quaternion, plus the six animation booleans a
862
+ * remote feeds to its own CharacterAnimations. Numbers are rounded to ~cm to
863
+ * keep the wire small. This is the ONLY position you should network — reaching
864
+ * for `currPos` here is the classic "standing remote player bobs" bug.
865
+ */
866
+ netState(): NetState {
867
+ const p = this.netPos;
868
+ const q = this.currentQuat;
869
+ return {
870
+ x: roundNet(p.x),
871
+ y: roundNet(p.y),
872
+ z: roundNet(p.z),
873
+ q: [roundNet(q.x), roundNet(q.y), roundNet(q.z), roundNet(q.w)],
874
+ onGround: this.isOnGround,
875
+ falling: this.isFalling,
876
+ moving: this.isMoving,
877
+ running: this.runActive,
878
+ jumping: this.jumpActive,
879
+ crouching: this.crouchActive,
880
+ };
881
+ }
882
+
802
883
  /**
803
884
  * Merge movement intents into the input state. Only fields you pass are
804
885
  * changed, so different input sources (keyboard, joystick, buttons) can each
@@ -1927,3 +2008,19 @@ export class CharacterController {
1927
2008
  d.velocityArrow.setLength(this._relativeVel.length() / this.targetMoveSpeed(this._runActive));
1928
2009
  }
1929
2010
  }
2011
+
2012
+ /**
2013
+ * Apply a remote player's networked snapshot to their VISUAL object (a plain
2014
+ * Object3D — never a CharacterController; remotes are interpolated visuals, not
2015
+ * simulated). Read the SMOOTHED copy the SDK exposes so motion glides:
2016
+ * `applyNetState(remoteObject, players.get(id).state)`. The six booleans
2017
+ * (`s.onGround`, `s.falling`, …) feed that remote's CharacterAnimations
2018
+ * separately — see the character-controller skill's multiplayer rule.
2019
+ */
2020
+ export function applyNetState(target: THREE.Object3D, s: NetState): void {
2021
+ target.position.set(s.x, s.y, s.z);
2022
+ // normalize: netState() rounds each quaternion component to ~cm precision, so the 4-tuple is
2023
+ // almost never exactly unit — Three.js bakes a non-unit quaternion straight into the matrix as a
2024
+ // small (~0.4%) scale that shifts as the remote turns. One call keeps remotes at true scale.
2025
+ target.quaternion.set(s.q[0], s.q[1], s.q[2], s.q[3]).normalize();
2026
+ }
@@ -318,11 +318,16 @@ it. Simulating remote players' physics locally guarantees divergence — every
318
318
  client would compute a different world. Never render every remote with your
319
319
  own avatar file: players picked their looks, show them.
320
320
 
321
- - Publish your own `currPos` + `currQuat` as a four-number quaternion on the fixed 10–20 Hz tick,
322
- not per frame. Never reduce multiplayer rotation to scalar yaw.
323
- - To animate remotes, sync the six animation booleans (`isOnGround`,
324
- `isFalling`, `isMoving`, `runActive`, `jumpActive`, `crouchActive`) and feed
325
- them to a per-remote `CharacterAnimations` see the animations reference.
321
+ - **Publish `character.netState()`** on the fixed 10–20 Hz tick (never per frame):
322
+ `room.me.set(character.netState())`. It bundles the network-safe position, a four-number
323
+ quaternion, and the six animation booleans in one call. It uses `netPos`, **not** `currPos` —
324
+ `currPos` is the raw physics capsule whose Y bobs on the float-suspension spring, so publishing
325
+ it makes a *standing* remote player visibly bob up and down. `netState()` snaps the grounded Y to
326
+ the settled height (raw while airborne, so jumps still arc). Never reduce rotation to a scalar yaw.
327
+ - On each remote, apply the SDK's **smoothed** copy with
328
+ `applyNetState(remoteObject, players.get(id).state)` (sets position + rotation), and feed the same
329
+ six booleans (`onGround`, `falling`, `moving`, `running`, `jumping`, `crouching`) to that remote's
330
+ `CharacterAnimations` — see the animations reference. Never publish `currPos` directly.
326
331
  - Only the owning client runs `MotionActionDriver`. Remotes play the same
327
332
  one-shot event while following smoothed owner-authored position/rotation;
328
333
  their animation mixer never moves them through the world.
@@ -620,6 +620,12 @@ than silently disappearing. The budget math that matters:
620
620
  `players.get(id).state` directly (already smoothed); every object from `objects.get(id).state`.
621
621
  4. **Create-or-reuse one mesh per id**; remove a player's mesh on `'leave'`.
622
622
 
623
+ > **Physics character?** If your player is the `genex controller character` capsule, do NOT
624
+ > hand-build the state from the body position — publish `room.me.set(character.netState())` and
625
+ > apply remotes with `applyNetState(mesh, players.get(id).state)`. The controller's raw `currPos.y`
626
+ > bobs on its float-suspension spring; `netState()` publishes a settled ground Y so a standing remote
627
+ > doesn't bob. See `$genex-threejs-character-controller` → "Multiplayer rule".
628
+
623
629
  ## Rotation: sync a quaternion, not an angle
624
630
 
625
631
  Send rotation as a 4-number quaternion `q: mesh.quaternion.toArray()`; on the remote do