@genex-ai/cli-demo 0.80.0-dev.211 → 0.81.0-dev.214

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) {
@@ -2999,11 +3006,29 @@ async function walkFiles(dir) {
2999
3006
  return out;
3000
3007
  }
3001
3008
  async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3009
+ const viewportFindings = [];
3010
+ try {
3011
+ const indexHtml = await fs10.readFile(path10.join(cwd, "index.html"), "utf8");
3012
+ if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
3013
+ viewportFindings.push({
3014
+ kind: "viewport-meta",
3015
+ message: `index.html has no viewport meta \u2014 phones will render a zoomed-out desktop canvas. Add: <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">`
3016
+ });
3017
+ } else if (!/viewport-fit\s*=/i.test(indexHtml)) {
3018
+ viewportFindings.push({
3019
+ kind: "viewport-meta",
3020
+ message: `index.html's viewport meta lacks viewport-fit=cover \u2014 env(safe-area-inset-*) (touch-kit widget offsets) is 0 without it. Append ", viewport-fit=cover" to the meta's content.`
3021
+ });
3022
+ }
3023
+ } catch {
3024
+ }
3002
3025
  const absAssets = path10.resolve(cwd, assetDir);
3003
3026
  try {
3004
- if (!(await fs10.stat(absAssets)).isDirectory()) return null;
3027
+ if (!(await fs10.stat(absAssets)).isDirectory()) {
3028
+ return viewportFindings.length > 0 ? viewportFindings : null;
3029
+ }
3005
3030
  } catch {
3006
- return null;
3031
+ return viewportFindings.length > 0 ? viewportFindings : null;
3007
3032
  }
3008
3033
  const srcFiles = (await walkFiles(path10.resolve(cwd, srcDir))).filter(
3009
3034
  (p) => AUDIT_SRC_EXTS.has(path10.extname(p).toLowerCase())
@@ -3023,7 +3048,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
3023
3048
  }
3024
3049
  }
3025
3050
  const referenced = (basename) => sources.some((s) => s.text.includes(basename));
3026
- const findings = [];
3051
+ const findings = [...viewportFindings];
3027
3052
  const assetFiles = await walkFiles(absAssets);
3028
3053
  const maskReported = /* @__PURE__ */ new Set();
3029
3054
  for (const p of assetFiles) {
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.81.0-dev.214",
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
+ }
@@ -615,7 +615,8 @@ after a bad single costs the whole chain).
615
615
  Use the returned hex verbatim for the DOM text over that region.
616
616
  - `npx genex ui trim --in <png>` — crop to alpha content + report real dims.
617
617
  - `npx genex ui audit [--dir public/assets/hud] [--src src]` — the mechanical
618
- wiring scan (unreferenced sprites/masks, naive %-fill patterns); warn-only,
618
+ wiring scan (unreferenced sprites/masks, naive %-fill patterns, and a
619
+ missing/incomplete mobile viewport meta in the root `index.html`); warn-only,
619
620
  `--strict` exits 1 on findings.
620
621
 
621
622
  ## Troubleshooting
@@ -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
@@ -149,6 +149,13 @@ z-order stays: drag zone (5) under joystick/buttons (10) under pause menus.
149
149
  - **Safe-area insets are built in** — default positions clear notches and the
150
150
  home indicator. If you position widgets yourself near screen edges, keep
151
151
  `env(safe-area-inset-*)` in the calc.
152
+ - **Viewport meta is a prerequisite** — every `env(safe-area-inset-*)` value is
153
+ silently **0** unless the page's viewport meta carries `viewport-fit=cover`.
154
+ Ship `<meta name="viewport" content="width=device-width, initial-scale=1,
155
+ viewport-fit=cover">` in the game's own `index.html`: the platform edge-injects
156
+ a default for published games, but the local dev server doesn't go through
157
+ that edge — without your own meta, widgets hug the notch in local testing
158
+ and look different from production.
152
159
  - **The drag zone swallows its touches** — taps inside it don't reach the
153
160
  canvas. If the game also needs taps there (tap to shoot), read them from the
154
161
  zone (`onChange` + a small-movement threshold) or shrink the zone.