@genex-ai/cli-demo 1.16.0-dev.586 → 1.17.0-dev.588

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
@@ -730,7 +730,7 @@ Important note: put soul into your creations, with many details and love. Aim to
730
730
  6. Generated UI art is a tool you reach for, not a pipeline you owe. A restrained interface built in clean CSS is a finished, legitimate HUD \u2014 not a placeholder. Reach for the sprite lane (\`genex-ai-hud\`) when the game's own style genuinely wants drawn chrome \u2014 ornate, painterly, comic, hand-made \u2014 or when the player asks for HUD art. Generating ONE element you decided the game needs \u2014 a frame, a mask, an icon, a wordmark, a menu backdrop, a menu video \u2014 is a normal use of these tools, never a half-run pipeline. There is NO global game-concept image and no UI plan recited in chat: the art direction lives in the game's brief in words. A lane's own concept step survives only where the player is choosing a concrete thing (a character's candidates). Whatever you do generate, run its quality steps in full \u2014 extraction, masks, wiring, \`npx genex ui audit\`.
731
731
  7. Never draw a rectangular backing plate behind bars, digits, or icons \u2014 in sprites or CSS. Ornament lives on the widget's own silhouette; a truly needed shaped plate comes from \`npx genex ui plate\`.
732
732
  8. Never park ready work behind a question, and never stall on an unanswered one \u2014 decide, state the decision in chat, record it, keep building.
733
- 9. Before any publish and before ending a session: run \`npx genex wait\` on every generation you enqueued and wire in what landed \u2014 never park landed assets. Fonts the brief names are LOADED for real, Escape pauses, the loader shows something of the game rather than a black screen, and the player wears the game's own generated character (or DESIGN.md records why it doesn't).
733
+ 9. Before any publish and before ending a session: run \`npx genex wait\` on every generation you enqueued and wire in what landed \u2014 never park landed assets. Fonts the brief names are LOADED for real, Escape pauses, the loader shows something of the game rather than a black screen (the renderer draws before any \`await waitForPlayer()\` \u2014 identity takes seconds on a hosted page), and the player wears the game's own generated character (or DESIGN.md records why it doesn't).
734
734
  10. The player's body is the game's own generated character (\`npx genex character "<look>"\` \u2192 \`npx genex controller character --character <id>\`), enqueued with your first art actions, not after them. It applies wherever a human body appears on screen \u2014 first-person included, the moment remotes, a look-down body, a shadow, or a menu portrait shows one. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead, or build it procedurally \u2014 held to law 22's bar like everything else the player looks at. Characters: Meshy/Mixamo/VRM rigs rest facing +Z. Set yaw explicitly when placing a rig; never mirror a SkinnedMesh with negative scale. In any two-character scene, verify in a capture that they face each other, not the camera.
735
735
  11. Verify by looking: one smoke check per milestone, after that milestone's preview push, in local test mode (\`?genex_local_test=1\`) with a real gameplay screenshot. A claim without a capture is not verification. Local-test evidence proves visuals and controls ONLY \u2014 label it that way when you show the player, and never work around the draft sign-in gate any other way.
736
736
  12. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
@@ -5856,6 +5856,101 @@ function splitArgs(text) {
5856
5856
  return out;
5857
5857
  }
5858
5858
  var lineOf = (content, index) => content.slice(0, index).split("\n").length;
5859
+ function blankComments(raw) {
5860
+ return raw.replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m, p1) => p1 + " ".repeat(m.length - p1.length));
5861
+ }
5862
+ var IDENTITY_AWAIT = /\bawait\s+(waitForPlayer|waitForAuth)\s*\(/;
5863
+ var RENDERER_CTOR = /\bnew\s+(?:THREE\.)?(?:WebGLRenderer|WebGPURenderer)\s*\(/;
5864
+ var INIT_EMBED_CALL = /\binitEmbed\s*\(/;
5865
+ async function detectEmbedBoot(cwd = process.cwd()) {
5866
+ const found = { awaitsIdentity: [], callsInitEmbed: false, rendererAfterAwait: [] };
5867
+ const srcDir = path14.join(cwd, "src");
5868
+ let entries;
5869
+ try {
5870
+ entries = await fs15.readdir(srcDir, { recursive: true });
5871
+ } catch {
5872
+ return found;
5873
+ }
5874
+ for (const nativeRel of entries) {
5875
+ if (nativeRel.includes("node_modules")) continue;
5876
+ if (nativeRel.split(path14.sep)[0] === "controllers") continue;
5877
+ if (!/\.(ts|js|mts|mjs|tsx|jsx)$/.test(nativeRel)) continue;
5878
+ const raw = await fs15.readFile(path14.join(srcDir, nativeRel), "utf8").catch(() => "");
5879
+ if (!raw) continue;
5880
+ const rel = nativeRel.split(path14.sep).join("/");
5881
+ const content = blankComments(raw);
5882
+ if (INIT_EMBED_CALL.test(content)) found.callsInitEmbed = true;
5883
+ const awaits = [];
5884
+ const awaitRe = new RegExp(IDENTITY_AWAIT.source, "g");
5885
+ let m;
5886
+ while (m = awaitRe.exec(content)) {
5887
+ awaits.push({ index: m.index, name: m[1], flowEnd: enclosingFlowEnd(content, m.index) });
5888
+ found.awaitsIdentity.push({
5889
+ where: `${rel}:${lineOf(content, m.index)}`,
5890
+ detail: `${m[1]}()`
5891
+ });
5892
+ }
5893
+ if (awaits.length === 0) continue;
5894
+ const rendererRe = new RegExp(RENDERER_CTOR.source, "g");
5895
+ while (m = rendererRe.exec(content)) {
5896
+ const at = m.index;
5897
+ const blocking = awaits.find((a) => a.index < at && at < a.flowEnd);
5898
+ if (!blocking) continue;
5899
+ found.rendererAfterAwait.push({
5900
+ where: `${rel}:${lineOf(content, at)}`,
5901
+ detail: `\`await ${blocking.name}()\` (line ${lineOf(content, blocking.index)})`
5902
+ });
5903
+ }
5904
+ }
5905
+ return found;
5906
+ }
5907
+ var CONTROL_HEADS = /* @__PURE__ */ new Set(["if", "for", "while", "switch", "catch", "with", "await"]);
5908
+ function isFunctionBrace(tail) {
5909
+ const t = tail.trimEnd();
5910
+ if (t.endsWith("=>")) return true;
5911
+ if (!t.endsWith(")")) return false;
5912
+ let depth = 0;
5913
+ let i = t.length - 1;
5914
+ for (; i >= 0; i--) {
5915
+ const ch = t[i];
5916
+ if (ch === ")") depth++;
5917
+ else if (ch === "(" && --depth === 0) break;
5918
+ }
5919
+ if (i < 0) return false;
5920
+ const head = /([A-Za-z_$][\w$]*)\s*$/.exec(t.slice(0, i))?.[1];
5921
+ return head !== void 0 && !CONTROL_HEADS.has(head);
5922
+ }
5923
+ function enclosingFlowEnd(content, index) {
5924
+ const stack = [];
5925
+ let boundary = 0;
5926
+ for (let i = 0; i < index; i++) {
5927
+ const ch = content[i];
5928
+ if (ch === "{") {
5929
+ stack.push({ fn: isFunctionBrace(content.slice(boundary, i)) });
5930
+ boundary = i + 1;
5931
+ } else if (ch === "}") {
5932
+ stack.pop();
5933
+ boundary = i + 1;
5934
+ } else if (ch === ";") {
5935
+ boundary = i + 1;
5936
+ }
5937
+ }
5938
+ let fnAt = -1;
5939
+ for (let s = stack.length - 1; s >= 0; s--) {
5940
+ if (stack[s].fn) {
5941
+ fnAt = s;
5942
+ break;
5943
+ }
5944
+ }
5945
+ if (fnAt === -1) return content.length;
5946
+ let depth = stack.length - fnAt;
5947
+ for (let i = index; i < content.length; i++) {
5948
+ const ch = content[i];
5949
+ if (ch === "{") depth++;
5950
+ else if (ch === "}" && --depth === 0) return i;
5951
+ }
5952
+ return content.length;
5953
+ }
5859
5954
  var DEPTH_RATIO_LIMIT = 1e6;
5860
5955
  async function detectSurfaceScan(cwd = process.cwd()) {
5861
5956
  const found = {
@@ -5879,7 +5974,7 @@ async function detectSurfaceScan(cwd = process.cwd()) {
5879
5974
  const raw = await fs15.readFile(path14.join(srcDir, nativeRel), "utf8").catch(() => "");
5880
5975
  if (!raw) continue;
5881
5976
  const rel = nativeRel.split(path14.sep).join("/");
5882
- const content = raw.replace(/\/\*[\s\S]*?\*\//g, (m2) => m2.replace(/[^\n]/g, " ")).replace(/(^|[^:])\/\/[^\n]*/gm, (m2, p1) => p1 + " ".repeat(m2.length - p1.length));
5977
+ const content = blankComments(raw);
5883
5978
  const repeatRe = /\.repeat\.set\(\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\)/g;
5884
5979
  let m;
5885
5980
  while (m = repeatRe.exec(content)) {
@@ -6016,7 +6111,8 @@ async function detectFeatures(log, cwd = process.cwd()) {
6016
6111
  gameStateUsed: await detectGameStateUsage(cwd),
6017
6112
  mobileControls: await detectMobileControls(cwd),
6018
6113
  surfaces: await detectSurfaceScan(cwd),
6019
- generations: await detectGenerationAudit(cwd)
6114
+ generations: await detectGenerationAudit(cwd),
6115
+ embedBoot: await detectEmbedBoot(cwd)
6020
6116
  };
6021
6117
  }
6022
6118
  function advisoryNudges(log, d) {
@@ -6035,9 +6131,24 @@ function advisoryNudges(log, d) {
6035
6131
  "package.json declares genex.matchmaking but @genex-ai/multiplayer is not installed \u2014 matchmaking cannot work. Install the SDK and use matchmake() (see genex-threejs-multiplayer)."
6036
6132
  );
6037
6133
  }
6134
+ embedBootNudges(log, d);
6038
6135
  surfaceNudges(log, d.surfaces);
6039
6136
  generationNudges(log, d.generations);
6040
6137
  }
6138
+ function embedBootNudges(log, d) {
6139
+ const boot = d.embedBoot;
6140
+ if (d.embedSdkVersion && boot.awaitsIdentity.length > 0 && !boot.callsInitEmbed) {
6141
+ const list = boot.awaitsIdentity.map((a) => `\`${a.detail}\` at ${a.where}`).join(", ");
6142
+ log.warn(
6143
+ `This game awaits ${list} but never calls \`initEmbed(\u2026)\`, so identity never resolves and the page stays black for every player. \`initEmbed({ slug, apiUrl, dashboardOrigins })\` from \`src/genex.config.ts\` is the first statement of the boot (genex-threejs-embed-auth).`
6144
+ );
6145
+ }
6146
+ for (const r of boot.rendererAfterAwait) {
6147
+ log.warn(
6148
+ `${r.where} creates the renderer after ${r.detail}. Draw first, await identity where you need the name: on a hosted page identity takes seconds and the player sees black until then.`
6149
+ );
6150
+ }
6151
+ }
6041
6152
  function generationNudges(log, g) {
6042
6153
  const describe = (refs) => refs.map((r) => `${r.kind} ${r.id} ("${r.prompt.slice(0, 48)}")`).join("; ");
6043
6154
  if (g.unpicked.length) {
@@ -6694,6 +6805,36 @@ function solveMagentaGlass(source) {
6694
6805
  return { ...base, png: out, ok: true };
6695
6806
  }
6696
6807
 
6808
+ // src/lib/generation-failure.ts
6809
+ var BILLING_ERROR_MARKERS = [
6810
+ /"code"\s*:\s*2010\b/,
6811
+ /not enough credit/i,
6812
+ /returned HTTP 402\b/,
6813
+ /failed:\s*402\b/,
6814
+ /insufficient funds/i
6815
+ ];
6816
+ function isProviderBillingError(error) {
6817
+ if (!error) return false;
6818
+ return BILLING_ERROR_MARKERS.some((re) => re.test(error));
6819
+ }
6820
+ var RAW_ERROR_MAX_CHARS = 120;
6821
+ function oneLineError(error, max = RAW_ERROR_MAX_CHARS) {
6822
+ const flat = error.replace(/\s+/g, " ").trim();
6823
+ return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
6824
+ }
6825
+ function describeGenerationFailure(kind, error) {
6826
+ if (!isProviderBillingError(error)) {
6827
+ return { billing: false, lines: error ? [c.red(error)] : [] };
6828
+ }
6829
+ return {
6830
+ billing: true,
6831
+ lines: [
6832
+ `${c.red("\u2717")} The ${kind} provider is out of credit on this stand \u2014 not your prompt and not your Genex credits (auto-refunded). Do not re-generate: fall back to procedural/VRM for this asset and tell the player in one plain line.`,
6833
+ c.dim(oneLineError(error))
6834
+ ]
6835
+ };
6836
+ }
6837
+
6697
6838
  // src/lib/download-assets.ts
6698
6839
  import fs16 from "fs/promises";
6699
6840
  import path15 from "path";
@@ -6752,6 +6893,7 @@ function localDeliveryFor(mode, opts, prompt) {
6752
6893
  }
6753
6894
 
6754
6895
  // src/lib/lanes.ts
6896
+ var LANE_CREDITS = /* @__PURE__ */ new Set(["ok", "exhausted", "unknown"]);
6755
6897
  async function fetchLanes(apiUrl, token, timeoutMs = 4e3) {
6756
6898
  try {
6757
6899
  const res = await apiFetch(`${apiUrl}/api/generations/lanes`, {
@@ -6764,7 +6906,14 @@ async function fetchLanes(apiUrl, token, timeoutMs = 4e3) {
6764
6906
  return {
6765
6907
  lanes: body.lanes.filter(
6766
6908
  (l) => !!l && typeof l.kind === "string" && typeof l.provider === "string"
6767
- ),
6909
+ ).map((l) => ({
6910
+ kind: l.kind,
6911
+ provider: l.provider,
6912
+ mock: l.mock === true,
6913
+ // A value this CLI does not know is dropped, not passed through: the
6914
+ // doctor branches on the exact string.
6915
+ ...typeof l.credit === "string" && LANE_CREDITS.has(l.credit) ? { credit: l.credit } : {}
6916
+ })),
6768
6917
  paused: body.paused === true
6769
6918
  };
6770
6919
  } catch {
@@ -7367,14 +7516,16 @@ async function waitForTerminalView(apiUrl, token, id, kind, onProgress = () => {
7367
7516
  }
7368
7517
  async function reportTerminal(kind, view, log, open = false, json = false, local) {
7369
7518
  if (view.status !== "completed") {
7519
+ const failure = view.status === "failed" ? describeGenerationFailure(kind, view.error) : null;
7370
7520
  if (json) writeJson({ kind, id: view.id, status: view.status, error: view.error ?? null });
7521
+ else if (failure?.billing) for (const line of failure.lines) log.plain(line);
7371
7522
  else log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
7372
7523
  if (view.status === "failed") {
7373
7524
  await recordTerminal(view.id, "failed");
7374
- if (kind === "video") {
7525
+ if (!failure?.billing && kind === "video") {
7375
7526
  const failures = await countFailed("video");
7376
7527
  (failures >= 2 ? log.plain : log.dim)(videoFailureAdvice(failures));
7377
- } else {
7528
+ } else if (!failure?.billing) {
7378
7529
  const advice = laneFailureAdvice(kind, await countOutcomes(kind));
7379
7530
  if (advice) log.plain(advice);
7380
7531
  }
@@ -8015,14 +8166,24 @@ async function runWaitAll(opts) {
8015
8166
  const mark = r.status === "done" ? c.green("\u2713") : r.status === "failed" ? c.red("\u2717") : c.yellow("\u2026");
8016
8167
  const label = r.status === "running" && typeof r.progress === "number" ? `running ${r.progress}%` : r.status;
8017
8168
  log.plain(` ${mark} ${label.padEnd(12)} ${r.kind.padEnd(8)} ${r.id} ${c.dim(`"${r.prompt.slice(0, 56)}"`)}`);
8018
- if (r.status === "failed" && r.error) log.plain(` ${c.red(r.error)}`);
8169
+ if (r.status === "failed" && r.error) {
8170
+ for (const line of describeGenerationFailure(r.kind, r.error).lines) log.plain(` ${line}`);
8171
+ }
8019
8172
  }
8020
8173
  const count = (s) => rows.filter((r) => r.status === s).length;
8174
+ const billingFailed = rows.filter(
8175
+ (r) => r.status === "failed" && isProviderBillingError(r.error)
8176
+ ).length;
8021
8177
  log.plain("");
8022
8178
  log.plain(
8023
8179
  ` ${count("done")} done \xB7 ${count("running")} running \xB7 ${count("queued")} queued \xB7 ${count("failed")} failed`
8024
8180
  );
8025
- if (count("failed") > 0) {
8181
+ if (billingFailed > 0) {
8182
+ log.dim(
8183
+ ` ${count("failed")} failed \xB7 ${billingFailed} of them: provider out of credit \u2014 do not retry those lanes (every attempt fails the same way and refunds); fall back to procedural/VRM and tell the player in one plain line.`
8184
+ );
8185
+ }
8186
+ if (count("failed") > billingFailed) {
8026
8187
  log.dim(
8027
8188
  " A failed generation never produces its URL \u2014 anything wired to it silently won't appear. Re-plan or re-generate (that bills a new one)."
8028
8189
  );
@@ -17562,9 +17723,15 @@ var CONTROLLER_FILE_SETS = {
17562
17723
  // ONE sketch, with or without --character. The lane is a runtime decision
17563
17724
  // inside loadPlayerCharacter, never a code edit the agent has to remember
17564
17725
  // to make once the generated character lands.
17726
+ // The first two lines are the boot ORDER, not controller wiring: a hosted
17727
+ // build (2026-09-03) copied this sketch, opened main.ts with the identity
17728
+ // await, made the renderer after it and never called initEmbed — black
17729
+ // forever. Identity takes seconds on a hosted page; the frame comes first.
17565
17730
  sketch: [
17731
+ `initEmbed({ slug: GENEX.slug, apiUrl: GENEX.apiUrl, dashboardOrigins: GENEX.dashboardOrigins }); // @genex-ai/embed-sdk + ./genex.config \u2014 FIRST statement, before any await`,
17732
+ `const renderer = new THREE.WebGLRenderer({ antialias: tier.antialias, ...depthRendererOptions() }); // draw a frame before awaiting identity`,
17566
17733
  `const physics = await PhysicsWorld.create();`,
17567
- `const { user } = await waitForPlayer(); // @genex-ai/embed-sdk`,
17734
+ `const { user } = await waitForPlayer(); // @genex-ai/embed-sdk \u2014 only where the name/avatar is needed`,
17568
17735
  `const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl }); // this game's character, else the avatar fallback`,
17569
17736
  `const fit = capsuleFromModel(player.scene);`,
17570
17737
  `const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
@@ -21214,6 +21381,7 @@ async function runDoctor(opts = {}) {
21214
21381
  if (row.fix) log.dim(` ${row.fix}`);
21215
21382
  }
21216
21383
  let laneProblem = false;
21384
+ let creditProblem = false;
21217
21385
  if (authState === "ok") {
21218
21386
  log.plain("");
21219
21387
  if (!lanes) {
@@ -21228,9 +21396,10 @@ async function runDoctor(opts = {}) {
21228
21396
  log.dim(" The daily spend breaker is tripped \u2014 nothing generates until it resets.");
21229
21397
  } else {
21230
21398
  const mocks = lanes.lanes.filter((l) => l.mock);
21231
- const mark = mocks.length > 0 ? c.yellow("!") : c.green("\u2713");
21399
+ const exhausted = lanes.lanes.filter((l) => !l.mock && l.credit === "exhausted");
21400
+ const mark = exhausted.length > 0 ? c.red("\u2717") : mocks.length > 0 ? c.yellow("!") : c.green("\u2713");
21232
21401
  log.plain(
21233
- ` ${mark} Lanes ${lanes.lanes.length - mocks.length}/${lanes.lanes.length} live${mocks.length > 0 ? ` \xB7 ${mocks.length} in SAMPLE mode` : ""}`
21402
+ ` ${mark} Lanes ${lanes.lanes.length - mocks.length}/${lanes.lanes.length} live${mocks.length > 0 ? ` \xB7 ${mocks.length} in SAMPLE mode` : ""}${exhausted.length > 0 ? ` \xB7 ${exhausted.length} OUT OF CREDIT` : ""}`
21234
21403
  );
21235
21404
  for (const kind of laneOrder(lanes)) {
21236
21405
  const lane = lanes.lanes.find((l) => l.kind === kind);
@@ -21242,6 +21411,12 @@ async function runDoctor(opts = {}) {
21242
21411
  log.plain(
21243
21412
  ` ${c.yellow("!")} ${kind.padEnd(17)}${"SAMPLE (no provider key)".padEnd(26)}${cost}`
21244
21413
  );
21414
+ } else if (lane.credit === "exhausted") {
21415
+ creditProblem = true;
21416
+ log.plain(
21417
+ ` ${c.red("\u2717")} ${kind.padEnd(17)}${c.red(`live (${lane.provider}) \xB7 OUT OF CREDIT on this stand`)}${cost ? ` ${cost}` : ""}`
21418
+ );
21419
+ log.dim(" Generations on this lane fail and auto-refund; fall back to procedural/VRM.");
21245
21420
  } else {
21246
21421
  log.dim(` \xB7 ${kind.padEnd(17)}${`live (${lane.provider})`.padEnd(26)}${cost}`);
21247
21422
  }
@@ -21254,7 +21429,7 @@ async function runDoctor(opts = {}) {
21254
21429
  }
21255
21430
  }
21256
21431
  }
21257
- const failed = rows.some((r) => r.bad) || laneProblem && mode !== "game";
21432
+ const failed = rows.some((r) => r.bad) || laneProblem && mode !== "game" || creditProblem;
21258
21433
  log.plain("");
21259
21434
  if (failed) {
21260
21435
  log.plain(` ${c.red("Something above needs fixing.")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genex-ai/cli-demo",
3
- "version": "1.16.0-dev.586",
3
+ "version": "1.17.0-dev.588",
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": {
@@ -376,7 +376,22 @@ export class FollowCamera {
376
376
  if (this._aimState === "unlocked" && e.pointerType === "mouse" && e.button === 0) {
377
377
  this._requestLock();
378
378
  }
379
- this._domElement.setPointerCapture(e.pointerId);
379
+ // Capture may be REFUSED, and a refusal must not throw out of the
380
+ // handler. MEASURED 2026-09-04 across five graded games (`Uncaught
381
+ // InvalidStateError: Failed to execute 'setPointerCapture'`): the click
382
+ // above requests pointer lock, and once the lock lands Chromium retires
383
+ // the pointer, so the capture call on that same pointerdown throws — an
384
+ // uncaught exception on the first click of every game with this camera,
385
+ // which failed the eval prober's no-errors check while the drag path
386
+ // beneath it still worked. Same guard the touch kit uses (drag-zone.ts).
387
+ if (typeof this._domElement.setPointerCapture === "function") {
388
+ try {
389
+ this._domElement.setPointerCapture(e.pointerId);
390
+ } catch {
391
+ // the pointer is already gone (pointer lock took it, or it was released
392
+ // between the event and the call) — the drag works without capture
393
+ }
394
+ }
380
395
  this._pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
381
396
  if (this._pointers.size === 1) {
382
397
  this._orbiting = true;
@@ -305,6 +305,16 @@ game with a generated character, every remote wears it — one
305
305
  `Player character: VRM — out of credits` in DESIGN.md, and mark the spot with
306
306
  `// TODO(genex): regenerate when credits refill`. Do not stop the session over
307
307
  this, and do not hand-build a stand-in humanoid.
308
+ - **"The character provider is out of credit on this stand"** (the line `npx genex
309
+ wait` / `npx genex character` prints when the VENDOR refused for money — Meshy
310
+ "Insufficient funds", Tripo code 2010) — this is the platform's own provider
311
+ account, not your prompt and not the user's credits (the charge auto-refunds),
312
+ and no re-run changes it. Take the same fallback: keep the profile VRM avatar,
313
+ tell the user in one plain line that the game is wearing the platform avatar
314
+ because the character lane is unavailable on this stand, record
315
+ `Player character: VRM — provider out of credit` in DESIGN.md, and mark the spot
316
+ with `// TODO(genex): regenerate when credits refill`. `npx genex doctor` shows
317
+ the lane as OUT OF CREDIT while it lasts.
308
318
  - **"Email not verified" (`email_verification_required`)** — generation credits
309
319
  unlock after the account's email is verified. Give the user the verify link the
310
320
  CLI printed, wait for them to confirm, then re-run the command.
@@ -135,6 +135,9 @@ fork as a migration strategy. Install a fresh copy elsewhere and port only the n
135
135
  ## Minimal wiring
136
136
 
137
137
  ```ts
138
+ import { initEmbed, waitForPlayer } from "@genex-ai/embed-sdk";
139
+ import { GENEX } from "./genex.config";
140
+ import * as THREE from "three";
138
141
  import { PhysicsWorld } from "./controllers/shared/physics-world.ts";
139
142
  import { CharacterController } from "./controllers/character/character-controller.ts";
140
143
  import { CharacterAnimations } from "./controllers/character/character-animations.ts";
@@ -145,7 +148,15 @@ import { createAimCue } from "./controllers/character/aim-cue.ts";
145
148
  import { KeyboardInput } from "./controllers/character/keyboard-input.ts";
146
149
  import { loadPlayerCharacter } from "./controllers/character/player-character.ts";
147
150
  import { capsuleFromModel } from "./controllers/character/vrm/capsule-fit.ts";
148
- import { waitForPlayer } from "@genex-ai/embed-sdk";
151
+
152
+ initEmbed({ slug: GENEX.slug, apiUrl: GENEX.apiUrl, dashboardOrigins: GENEX.dashboardOrigins }); // FIRST — before any await
153
+
154
+ // draw first; identity takes seconds on a hosted page
155
+ const renderer = new THREE.WebGLRenderer({ antialias: tier.antialias, ...depthRendererOptions() }); // tier + depthRendererOptions: the quality kit
156
+ const scene = new THREE.Scene();
157
+ const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, near, far);
158
+ document.body.appendChild(renderer.domElement);
159
+ renderer.render(scene, camera); // a frame is on screen before anything below is awaited
149
160
 
150
161
  const physics = await PhysicsWorld.create(); // nothing RAPIER-related may run before this resolves
151
162
 
@@ -154,13 +165,13 @@ const physics = await PhysicsWorld.create(); // nothing RAPIER-related may run b
154
165
  // (with that character's exact-rig clips and locomotion profile), and falls
155
166
  // back to the visiting player's own profile avatar when it isn't — retargeting
156
167
  // the bundled core library plus any packs installed by `genex controller
157
- // anims`. `user.avatarUrl` comes from the embed identity
158
- // ($genex-threejs-embed-auth boots before this) and is used only in that
168
+ // anims`. `user.avatarUrl` comes from the embed identity (the `initEmbed`
169
+ // call above — $genex-threejs-embed-auth owns it) and is used only in that
159
170
  // fallback lane; the baked `./assets/avatar.vrm` covers local dev and load
160
171
  // failures. WRITE THIS ONCE: when the generated character lands mid-build,
161
172
  // `genex controller character --character <id>` drops the manifest in and the
162
173
  // next reload swaps the body. Nothing below changes.
163
- const { user } = await waitForPlayer(); // from "@genex-ai/embed-sdk"
174
+ const { user } = await waitForPlayer(); // identity, awaited HERE — where the avatar is needed, never as the first line of the boot
164
175
  const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl });
165
176
 
166
177
  const fit = capsuleFromModel(player.scene); // collider fits THIS body's bounds
@@ -54,6 +54,14 @@ initEmbed({
54
54
  });
55
55
  ```
56
56
 
57
+ **Create the renderer and draw a frame BEFORE you await identity; `await
58
+ waitForPlayer()` only where you need the name or the token.** On a hosted page
59
+ identity takes seconds (the dashboard handshake, or the standalone bounce), so
60
+ a `main.ts` whose first statement is `const { user } = await waitForPlayer()`
61
+ shows black for all of them — and forever when `initEmbed()` was never called,
62
+ because nothing else resolves that promise. Renderer, scene, camera, first
63
+ frame, then the await, down where the name or the avatar is actually used.
64
+
57
65
  Also give `<body>` a dark background in `index.html` (e.g.
58
66
  `<body style="margin:0;background:#080a14">`) — it makes the pre-boot frame
59
67
  (before any JS runs) match the SDK's own loading overlay instead of flashing