@genex-ai/cli-demo 1.16.0-dev.587 → 1.17.0-dev.589
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 +187 -12
- package/package.json +1 -1
- package/templates/controllers/character/follow-camera.ts +16 -1
- package/templates/skills/genex-ai-character/SKILL.md +10 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +15 -4
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +8 -0
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
|
|
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)
|
|
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 (
|
|
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
|
|
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.
|
|
3
|
+
"version": "1.17.0-dev.589",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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(); //
|
|
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
|