@genex-ai/cli-demo 0.90.0-dev.227 → 0.91.0-dev.228
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/README.md +4 -1
- package/dist/index.js +296 -66
- package/package.json +1 -1
- package/templates/controllers/NOTICE.md +6 -4
- package/templates/controllers/character/player-character.ts +217 -0
- package/templates/skills/genex-ai-character/SKILL.md +54 -8
- package/templates/skills/genex-ai-image/SKILL.md +5 -1
- package/templates/skills/genex-ai-menu/SKILL.md +14 -6
- package/templates/skills/genex-ai-skybox/SKILL.md +19 -6
- package/templates/skills/genex-game-director/SKILL.md +29 -11
- package/templates/skills/genex-game-director/references/routing-map.md +3 -3
- package/templates/skills/genex-threejs-character-controller/SKILL.md +103 -51
- package/templates/skills/genex-threejs-character-controller/references/animations.md +62 -50
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +5 -3
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +10 -8
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +8 -7
package/README.md
CHANGED
|
@@ -183,7 +183,10 @@ already-installed actions are not charged again. The controller command writes
|
|
|
183
183
|
`public/assets/meshy-character.json`, whose permanent R2 URLs point at the
|
|
184
184
|
current rigged model and compact same-signature animation GLBs. Re-run it after
|
|
185
185
|
adding actions to refresh the manifest without overwriting existing controller
|
|
186
|
-
source. The
|
|
186
|
+
source. The game's own generated character is the player's body wherever a human
|
|
187
|
+
body appears on screen; the profile VRM avatar is the fallback, and the vendored
|
|
188
|
+
`character/player-character.ts` resolves both behind one call, so installing the
|
|
189
|
+
manifest needs no code change in the game.
|
|
187
190
|
|
|
188
191
|
## Install / run
|
|
189
192
|
|
package/dist/index.js
CHANGED
|
@@ -265,13 +265,16 @@ Your capabilities (all via \`npx genex \u2026\`): generate \`model\` \xB7 \`skyb
|
|
|
265
265
|
9. 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\`.
|
|
266
266
|
10. Fonts: the brief's display + body pair comes from the menu skill's genre table (or carries a one-line stated reason) and is LOADED for real in \`index.html\`.
|
|
267
267
|
11. 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.
|
|
268
|
-
12. Wire-in floors before any publish and before ending a session: sprite HUD wired (or the recorded CSS lane finished), menu video playing (or its recorded still fallback), logotype placed, fonts loaded, Escape pause, branded loader. Run \`npx genex wait\` on every generation you enqueued and wire what landed \u2014 never park landed assets.
|
|
269
|
-
13. 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.
|
|
270
|
-
14. 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.
|
|
268
|
+
12. Wire-in floors before any publish and before ending a session: sprite HUD wired (or the recorded CSS lane finished), menu video playing (or its recorded still fallback), logotype placed, fonts loaded, Escape pause, branded loader, the player wearing the game's own generated character (or DESIGN.md recording why it isn't). Run \`npx genex wait\` on every generation you enqueued and wire what landed \u2014 never park landed assets.
|
|
269
|
+
13. 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. The profile VRM avatar is the FALLBACK: a temporary body while the character renders (say in one plain line that it's temporary), or the stand-in when generation genuinely could not happen \u2014 out of credits, failed, unverified; record which in DESIGN.md as \`Player character: VRM \u2014 <reason>\`. Games whose player is not a person (car, ship, RTS cursor, board) generate that object with \`npx genex model\` instead. 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.
|
|
270
|
+
14. 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.
|
|
271
271
|
15. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
|
|
272
272
|
16. Every finished Build-plan milestone ends with \`npx genex preview\` and the player's page link (\`<dashboard>/draft/<slug>\` with \`<dashboard>\` from \`.genex/project.json\`, \`/world/<slug>\` once published) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. After every round of player feedback, end with a preview push.
|
|
273
273
|
17. Parallel work: this line is your standing authorization and request to use sub-agents / parallel agent work whenever your platform provides them. While drafting the Build plan, decide per module what runs in parallel and what stays serial for THIS game \u2014 dependencies decide, there is no fixed list \u2014 and record each call in the Modules table with a one-line reason. Independent modules default to parallel; building everything serially needs a stated reason. You keep integration, previews, and the player conversation; each sub-agent owns only its module's files.
|
|
274
274
|
18. Talk to the player in plain game language \u2014 what changed in the game and what to try; never code, file names, build output, or tool internals unless they ask. Short status lines while you work; long silent stretches are a failure.
|
|
275
|
+
19. Never add debug-only code to the game to check your own work \u2014 no hidden test modes, no special URL parameters, no forced-visible flags, no auth mocks, no pixel-sampling hooks. \`?genex_local_test=1\` is the platform's own supported mode and is fine; your own bypass is not. (The multiplayer skill's small build identifier, token-free status line, and connected-quorum watchdog are production supportability, not a bypass \u2014 keep those.)
|
|
276
|
+
20. Input directions match their labels: A/\u2190 moves or turns the player screen-LEFT, D/\u2192 screen-RIGHT, mouse-up looks up, and drag-pan axes share ONE convention. The cursor is either the gameplay tool (RTS, card, builder) or locked away during play \u2014 keyboard-only games included. Check it in every milestone's smoke pass.
|
|
277
|
+
21. v0 is a milestone, not the destination. When the ask was bigger than one loop, every milestone after v0 grows back toward the FULL ask with DESIGN.md's content lines as the checklist \u2014 a slice that previewed well never quietly becomes the game. Cosmetics never jump the queue past promised content.
|
|
275
278
|
${CONTRACT_END}
|
|
276
279
|
`;
|
|
277
280
|
var CLAUDE_IMPORT_LINE = "@AGENTS.md";
|
|
@@ -2113,6 +2116,7 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
|
|
|
2113
2116
|
white halo over dark scenes). [--expect N: hard-error
|
|
2114
2117
|
unless exactly N components] [--min-pixels 2000]
|
|
2115
2118
|
[--padding 8] [--chroma 240] [--dilate 0] [--no-defringe]
|
|
2119
|
+
[--allow-speckle]
|
|
2116
2120
|
ui masks --out-dir <dir> --pairs "name:cX,cY,w,h:aX,aY,w,h[:n];\u2026"
|
|
2117
2121
|
OR --auto (detect clean/annotated cell pairs itself) \u2014
|
|
2118
2122
|
clean frame + green-annotated duplicate \u2192 frame + fill mask
|
|
@@ -2128,6 +2132,9 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
|
|
|
2128
2132
|
[--min-frac 0.004] [--out <json>] [--crop <png>]
|
|
2129
2133
|
ui trim [--out <png>] \u2014 crop to the alpha bbox (default: overwrite
|
|
2130
2134
|
--in), print real dims + refresh the .bbox.json sidecar.
|
|
2135
|
+
Hard-refuses a DAMAGED cutout (isolated colour speckle on
|
|
2136
|
+
the soft rim \u2014 what a background-removal pass does to an
|
|
2137
|
+
image that was already transparent). [--allow-speckle]
|
|
2131
2138
|
ui seams measure a tiling texture's wrap seam (opposite edges vs
|
|
2132
2139
|
interior detail) \u2192 ratio + verdict as JSON; nonzero exit on
|
|
2133
2140
|
a visible seam. [--seam-tolerance 3]
|
|
@@ -2139,7 +2146,9 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
|
|
|
2139
2146
|
ui audit mechanical HUD-wiring scan (no --in): mask\u2194frame pairing
|
|
2140
2147
|
mismatches (recorded crops disagree \u2014 PROVABLE, exit 1),
|
|
2141
2148
|
white-painted meter troughs (baked sheet-white inside a
|
|
2142
|
-
fill channel \u2014 PROVABLE, exit 1),
|
|
2149
|
+
fill channel \u2014 PROVABLE, exit 1), damaged cutouts (colour
|
|
2150
|
+
speckle on a wired sprite's rim \u2014 PROVABLE, exit 1),
|
|
2151
|
+
loosened-check stamps,
|
|
2143
2152
|
extracted-but-never-wired sprites, generated-but-
|
|
2144
2153
|
unreferenced masks, naive JS %-width fills, phones-
|
|
2145
2154
|
preload-video heuristic. Everything but the two provable
|
|
@@ -2422,6 +2431,15 @@ async function uiExtract(opts, log) {
|
|
|
2422
2431
|
if (inputMode === "chroma" && opts.defringe !== false) {
|
|
2423
2432
|
defringed = defringeAgainstMatte(out) > 0;
|
|
2424
2433
|
}
|
|
2434
|
+
const speckle = rimSpeckleRatio(out);
|
|
2435
|
+
if (speckle && speckle.ratio > SPECKLE_MAX_RATIO && !opts.allowSpeckle) {
|
|
2436
|
+
fail(speckleRefusal(name, speckle.ratio), {
|
|
2437
|
+
input,
|
|
2438
|
+
sprite: name,
|
|
2439
|
+
speckleRatio: Number(speckle.ratio.toFixed(4)),
|
|
2440
|
+
rimPixels: speckle.sampled
|
|
2441
|
+
});
|
|
2442
|
+
}
|
|
2425
2443
|
const outPath = path11.join(outDir, `${name}.png`);
|
|
2426
2444
|
await writePng(outPath, out);
|
|
2427
2445
|
const sidecar = {
|
|
@@ -3180,13 +3198,30 @@ async function uiTrim(opts, log) {
|
|
|
3180
3198
|
h: bbox.maxY - bbox.minY + 1
|
|
3181
3199
|
};
|
|
3182
3200
|
const trimmed = cropPng(png, cropBox);
|
|
3201
|
+
const speckle = rimSpeckleRatio(trimmed);
|
|
3202
|
+
if (speckle && speckle.ratio > SPECKLE_MAX_RATIO && !opts.allowSpeckle) {
|
|
3203
|
+
fail(speckleRefusal("this image", speckle.ratio), {
|
|
3204
|
+
input,
|
|
3205
|
+
speckleRatio: Number(speckle.ratio.toFixed(4)),
|
|
3206
|
+
rimPixels: speckle.sampled
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3183
3209
|
await writePng(outPath, trimmed);
|
|
3184
3210
|
const sidecar = computeBBoxes(trimmed);
|
|
3211
|
+
const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
|
|
3185
3212
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
3186
|
-
await fs11.writeFile(
|
|
3213
|
+
await fs11.writeFile(
|
|
3214
|
+
sidecarPath,
|
|
3215
|
+
JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
|
|
3216
|
+
);
|
|
3187
3217
|
log.success(
|
|
3188
3218
|
`Trimmed ${png.width}x${png.height} \u2192 ${trimmed.width}x${trimmed.height} (${outPath}).`
|
|
3189
3219
|
);
|
|
3220
|
+
if (speckleAllowed) {
|
|
3221
|
+
log.warn(
|
|
3222
|
+
"Shipped with --allow-speckle: this cutout is damaged and the override is stamped in its sidecar. Eyeball it over a bright background before it reaches the player."
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3190
3225
|
process.stdout.write(
|
|
3191
3226
|
`${JSON.stringify({
|
|
3192
3227
|
out: outPath,
|
|
@@ -3195,7 +3230,9 @@ async function uiTrim(opts, log) {
|
|
|
3195
3230
|
aspectRatio: sidecar.aspectRatio,
|
|
3196
3231
|
outerBBox: sidecar.outerBBox,
|
|
3197
3232
|
innerBBox: sidecar.innerBBox,
|
|
3198
|
-
sidecar: sidecarPath
|
|
3233
|
+
sidecar: sidecarPath,
|
|
3234
|
+
...speckle ? { speckleRatio: Number(speckle.ratio.toFixed(4)) } : {},
|
|
3235
|
+
...speckleAllowed ? { speckleAllowed: true } : {}
|
|
3199
3236
|
})}
|
|
3200
3237
|
`
|
|
3201
3238
|
);
|
|
@@ -3292,6 +3329,51 @@ async function uiPlate(opts, log) {
|
|
|
3292
3329
|
`
|
|
3293
3330
|
);
|
|
3294
3331
|
}
|
|
3332
|
+
var SPECKLE_RIM_ALPHA_MIN = 16;
|
|
3333
|
+
var SPECKLE_RIM_ALPHA_MAX = 220;
|
|
3334
|
+
var SPECKLE_CHROMA_EXCESS = 60;
|
|
3335
|
+
var SPECKLE_MIN_SAMPLE = 200;
|
|
3336
|
+
var SPECKLE_MAX_RATIO = 0.02;
|
|
3337
|
+
function rimSpeckleRatio(png) {
|
|
3338
|
+
const { width: W, height: H, data } = png;
|
|
3339
|
+
if (W < 3 || H < 3) return null;
|
|
3340
|
+
const chroma = new Uint8Array(W * H);
|
|
3341
|
+
for (let i = 0; i < W * H; i++) {
|
|
3342
|
+
const o = i * 4;
|
|
3343
|
+
const r = data[o];
|
|
3344
|
+
const g = data[o + 1];
|
|
3345
|
+
const b = data[o + 2];
|
|
3346
|
+
chroma[i] = Math.max(r, g, b) - Math.min(r, g, b);
|
|
3347
|
+
}
|
|
3348
|
+
const neighbourhood = [];
|
|
3349
|
+
let sampled = 0;
|
|
3350
|
+
let speckled = 0;
|
|
3351
|
+
for (let y = 1; y < H - 1; y++) {
|
|
3352
|
+
for (let x = 1; x < W - 1; x++) {
|
|
3353
|
+
const i = y * W + x;
|
|
3354
|
+
const a = data[i * 4 + 3];
|
|
3355
|
+
if (a <= SPECKLE_RIM_ALPHA_MIN || a >= SPECKLE_RIM_ALPHA_MAX) continue;
|
|
3356
|
+
sampled++;
|
|
3357
|
+
neighbourhood.length = 0;
|
|
3358
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
3359
|
+
for (let dx = -1; dx <= 1; dx++) neighbourhood.push(chroma[(y + dy) * W + (x + dx)]);
|
|
3360
|
+
}
|
|
3361
|
+
neighbourhood.sort((p, q) => p - q);
|
|
3362
|
+
if (chroma[i] - neighbourhood[4] > SPECKLE_CHROMA_EXCESS) speckled++;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
if (sampled < SPECKLE_MIN_SAMPLE) return null;
|
|
3366
|
+
return { ratio: speckled / sampled, sampled };
|
|
3367
|
+
}
|
|
3368
|
+
function speckleRefusal(label, ratio) {
|
|
3369
|
+
return [
|
|
3370
|
+
`Damaged cutout \u2014 ${(ratio * 100).toFixed(1)}% of ${label}'s soft edge is isolated colour speckle (want <= ${(SPECKLE_MAX_RATIO * 100).toFixed(0)}%).`,
|
|
3371
|
+
"A background-removal pass over an image that was ALREADY transparent re-keys every edge and sprays colour noise along it. Trimming or defringing cannot fix this.",
|
|
3372
|
+
" -> From `genex image --clean`? Ship the `--transparent` original instead.",
|
|
3373
|
+
" -> Source really had a background? Re-clean with `--bg-mode matte` (soft edges).",
|
|
3374
|
+
" Override (stamped in the output): --allow-speckle"
|
|
3375
|
+
].join("\n");
|
|
3376
|
+
}
|
|
3295
3377
|
var DEFRINGE_BAND_PX = 2;
|
|
3296
3378
|
function defringeAgainstMatte(png, matte = { r: 255, g: 255, b: 255 }) {
|
|
3297
3379
|
const { width: W, height: H, data } = png;
|
|
@@ -3374,7 +3456,8 @@ function defringeAgainstMatte(png, matte = { r: 255, g: 255, b: 255 }) {
|
|
|
3374
3456
|
}
|
|
3375
3457
|
var HARD_AUDIT_KINDS = /* @__PURE__ */ new Set([
|
|
3376
3458
|
"mask-frame-mismatch",
|
|
3377
|
-
"white-trough"
|
|
3459
|
+
"white-trough",
|
|
3460
|
+
"cutout-speckle"
|
|
3378
3461
|
]);
|
|
3379
3462
|
var TROUGH_OPAQUE_MIN_ALPHA = 200;
|
|
3380
3463
|
var TROUGH_WHITE_MIN = 235;
|
|
@@ -3535,6 +3618,22 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
3535
3618
|
}
|
|
3536
3619
|
}
|
|
3537
3620
|
}
|
|
3621
|
+
for (const [base, p] of pngByBase) {
|
|
3622
|
+
if (/-mask\.png$/.test(base)) continue;
|
|
3623
|
+
if (!referenced(base)) continue;
|
|
3624
|
+
let png;
|
|
3625
|
+
try {
|
|
3626
|
+
png = PNG2.sync.read(await fs11.readFile(p));
|
|
3627
|
+
} catch {
|
|
3628
|
+
continue;
|
|
3629
|
+
}
|
|
3630
|
+
const speckle = rimSpeckleRatio(png);
|
|
3631
|
+
if (!speckle || speckle.ratio <= SPECKLE_MAX_RATIO) continue;
|
|
3632
|
+
findings.push({
|
|
3633
|
+
kind: "cutout-speckle",
|
|
3634
|
+
message: `${base}: ${(speckle.ratio * 100).toFixed(1)}% of the soft edge is isolated colour speckle (want <= ${(SPECKLE_MAX_RATIO * 100).toFixed(0)}%) \u2014 a background-removal pass re-keyed edges that were already transparent and sprayed colour noise along them. Nothing downstream repairs it: ship the \`--transparent\` original, or re-clean the true source with \`--bg-mode matte\`. Do not ship this sprite.`
|
|
3635
|
+
});
|
|
3636
|
+
}
|
|
3538
3637
|
const loosenedNames = [...metaByName.entries()].filter(([, meta]) => meta.loosened).map(([name]) => name);
|
|
3539
3638
|
if (loosenedNames.length > 0) {
|
|
3540
3639
|
findings.push({
|
|
@@ -3866,13 +3965,109 @@ async function inlineLocalImage(filePath, flag) {
|
|
|
3866
3965
|
const mime = IMAGE_MIME_BY_EXT[path13.extname(filePath).toLowerCase()] ?? "image/png";
|
|
3867
3966
|
return { ok: true, dataUri: `data:${mime};base64,${bytes.toString("base64")}` };
|
|
3868
3967
|
}
|
|
3869
|
-
var SKYBOX_ENVIRONMENT_SUFFIX = ".
|
|
3968
|
+
var SKYBOX_ENVIRONMENT_SUFFIX = ". The image contains ONLY sky: cloud, atmosphere, light, weather and distant haze at the horizon. Every structure, object, plant and ground surface is outside the frame.";
|
|
3870
3969
|
var SKYBOX_PROMPT_MAX = 2e3;
|
|
3970
|
+
var SKYBOX_STRUCTURE_NOUNS = [
|
|
3971
|
+
"building",
|
|
3972
|
+
"buildings",
|
|
3973
|
+
"house",
|
|
3974
|
+
"houses",
|
|
3975
|
+
"hut",
|
|
3976
|
+
"huts",
|
|
3977
|
+
"cabin",
|
|
3978
|
+
"cabins",
|
|
3979
|
+
"tower",
|
|
3980
|
+
"towers",
|
|
3981
|
+
"castle",
|
|
3982
|
+
"castles",
|
|
3983
|
+
"cathedral",
|
|
3984
|
+
"cathedrals",
|
|
3985
|
+
"church",
|
|
3986
|
+
"churches",
|
|
3987
|
+
"temple",
|
|
3988
|
+
"temples",
|
|
3989
|
+
"shrine",
|
|
3990
|
+
"shrines",
|
|
3991
|
+
"monastery",
|
|
3992
|
+
"palace",
|
|
3993
|
+
"fortress",
|
|
3994
|
+
"citadel",
|
|
3995
|
+
"ruin",
|
|
3996
|
+
"ruins",
|
|
3997
|
+
"city",
|
|
3998
|
+
"cityscape",
|
|
3999
|
+
"skyline",
|
|
4000
|
+
"town",
|
|
4001
|
+
"village",
|
|
4002
|
+
"settlement",
|
|
4003
|
+
"street",
|
|
4004
|
+
"streets",
|
|
4005
|
+
"road",
|
|
4006
|
+
"roads",
|
|
4007
|
+
"courtyard",
|
|
4008
|
+
"plaza",
|
|
4009
|
+
"bridge",
|
|
4010
|
+
"bridges",
|
|
4011
|
+
"wall",
|
|
4012
|
+
"walls",
|
|
4013
|
+
"gate",
|
|
4014
|
+
"gates",
|
|
4015
|
+
"fence",
|
|
4016
|
+
"fences",
|
|
4017
|
+
"pillar",
|
|
4018
|
+
"pillars",
|
|
4019
|
+
"column",
|
|
4020
|
+
"columns",
|
|
4021
|
+
"statue",
|
|
4022
|
+
"statues",
|
|
4023
|
+
"monument",
|
|
4024
|
+
"obelisk",
|
|
4025
|
+
"spire",
|
|
4026
|
+
"spires",
|
|
4027
|
+
"rooftop",
|
|
4028
|
+
"rooftops",
|
|
4029
|
+
"tree",
|
|
4030
|
+
"trees",
|
|
4031
|
+
"forest",
|
|
4032
|
+
"jungle",
|
|
4033
|
+
"ship",
|
|
4034
|
+
"ships",
|
|
4035
|
+
"boat",
|
|
4036
|
+
"boats",
|
|
4037
|
+
"car",
|
|
4038
|
+
"cars",
|
|
4039
|
+
"vehicle",
|
|
4040
|
+
"vehicles",
|
|
4041
|
+
"person",
|
|
4042
|
+
"people",
|
|
4043
|
+
"figure",
|
|
4044
|
+
"figures",
|
|
4045
|
+
"character",
|
|
4046
|
+
"characters",
|
|
4047
|
+
"creature",
|
|
4048
|
+
"creatures",
|
|
4049
|
+
"dragon",
|
|
4050
|
+
"banner",
|
|
4051
|
+
"banners",
|
|
4052
|
+
"flag",
|
|
4053
|
+
"flags",
|
|
4054
|
+
"torch",
|
|
4055
|
+
"torches",
|
|
4056
|
+
"lamp",
|
|
4057
|
+
"lamps"
|
|
4058
|
+
];
|
|
4059
|
+
var SKYBOX_NOUN_RE = new RegExp(`\\b(${SKYBOX_STRUCTURE_NOUNS.join("|")})\\b`, "gi");
|
|
4060
|
+
function skyboxStructureNouns(prompt) {
|
|
4061
|
+
const found = /* @__PURE__ */ new Set();
|
|
4062
|
+
for (const m of prompt.matchAll(SKYBOX_NOUN_RE)) found.add(m[1].toLowerCase());
|
|
4063
|
+
return [...found];
|
|
4064
|
+
}
|
|
3871
4065
|
function applySkyboxGuard(prompt, raw) {
|
|
3872
|
-
if (raw) return { prompt, guarded: false };
|
|
4066
|
+
if (raw) return { prompt, guarded: false, nouns: [] };
|
|
4067
|
+
const nouns = skyboxStructureNouns(prompt);
|
|
3873
4068
|
const guarded = `${prompt}${SKYBOX_ENVIRONMENT_SUFFIX}`;
|
|
3874
|
-
if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false };
|
|
3875
|
-
return { prompt: guarded, guarded: true };
|
|
4069
|
+
if (guarded.length > SKYBOX_PROMPT_MAX) return { prompt, guarded: false, nouns };
|
|
4070
|
+
return { prompt: guarded, guarded: true, nouns };
|
|
3876
4071
|
}
|
|
3877
4072
|
function buildGenOptions(kind, opts) {
|
|
3878
4073
|
const options = { ...opts.generationOptions };
|
|
@@ -3927,6 +4122,23 @@ async function runGenerate(kind, opts) {
|
|
|
3927
4122
|
return;
|
|
3928
4123
|
}
|
|
3929
4124
|
const skyboxGuard = kind === "skybox" ? applySkyboxGuard(typedPrompt, opts.raw) : void 0;
|
|
4125
|
+
if (skyboxGuard && skyboxGuard.nouns.length > 0) {
|
|
4126
|
+
log.error(
|
|
4127
|
+
`A skybox is sky only \u2014 this prompt names ${skyboxGuard.nouns.join(", ")}.`
|
|
4128
|
+
);
|
|
4129
|
+
log.plain(
|
|
4130
|
+
[
|
|
4131
|
+
" Structures painted into a sky render at infinite distance: they never get closer",
|
|
4132
|
+
" as the player moves, and they read as broken the moment the camera strafes.",
|
|
4133
|
+
` ${c.cyan("\u2192")} Build them as scene geometry (${c.cyan("npx genex model")}); describe only light, colour,`,
|
|
4134
|
+
" cloud, weather and time of day here.",
|
|
4135
|
+
` ${c.cyan("\u2192")} Genuinely want it painted into the sky (a space-station panorama for a scene`,
|
|
4136
|
+
` with no world geometry)? Re-run with ${c.cyan("--raw")}.`
|
|
4137
|
+
].join("\n")
|
|
4138
|
+
);
|
|
4139
|
+
process.exitCode = 1;
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
3930
4142
|
let prompt = skyboxGuard?.prompt ?? typedPrompt;
|
|
3931
4143
|
if (kind === "image" && opts.glass) {
|
|
3932
4144
|
const conflict = opts.transparent ? "--transparent" : opts.removeBackground ? "--remove-bg" : opts.editUrl ? "--edit" : opts.cleanUrl ? "--clean" : opts.upscaleUrl ? "--upscale" : opts.noWait ? "--no-wait" : void 0;
|
|
@@ -4660,6 +4872,14 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4660
4872
|
);
|
|
4661
4873
|
}
|
|
4662
4874
|
}
|
|
4875
|
+
if (!/player character:/i.test(design)) {
|
|
4876
|
+
const hasCharacter = await fs14.access(path14.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
|
|
4877
|
+
if (!hasCharacter && await loadsPlayerBody(cwd)) {
|
|
4878
|
+
warnings.push(
|
|
4879
|
+
`Player character is the stock avatar \u2014 no generated character is wired. The game's own generated character is the player's body wherever a human body appears on screen, first-person included (genex-ai-character). Generate it, or record "Player character: VRM \u2014 <reason>" in DESIGN.md (no human body in this game / out of credits / player declined).`
|
|
4880
|
+
);
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4663
4883
|
for (const w of warnings) log.warn(w);
|
|
4664
4884
|
} catch {
|
|
4665
4885
|
}
|
|
@@ -4687,6 +4907,21 @@ async function hasHudLikeDom(cwd) {
|
|
|
4687
4907
|
}
|
|
4688
4908
|
return false;
|
|
4689
4909
|
}
|
|
4910
|
+
async function loadsPlayerBody(cwd) {
|
|
4911
|
+
const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
|
|
4912
|
+
try {
|
|
4913
|
+
const entries = await fs14.readdir(path14.join(cwd, "src"), { recursive: true });
|
|
4914
|
+
for (const rel of entries) {
|
|
4915
|
+
if (rel.includes("node_modules")) continue;
|
|
4916
|
+
if (rel.split(path14.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
|
|
4917
|
+
if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
|
|
4918
|
+
const text = await fs14.readFile(path14.join(cwd, "src", rel), "utf8").catch(() => "");
|
|
4919
|
+
if (BODY_LOADERS.test(text)) return true;
|
|
4920
|
+
}
|
|
4921
|
+
} catch {
|
|
4922
|
+
}
|
|
4923
|
+
return false;
|
|
4924
|
+
}
|
|
4690
4925
|
async function hasExtractionSidecars(cwd) {
|
|
4691
4926
|
try {
|
|
4692
4927
|
const entries = await fs14.readdir(path14.join(cwd, "public"), { recursive: true });
|
|
@@ -15372,9 +15607,14 @@ var CONTROLLER_FILE_SETS = {
|
|
|
15372
15607
|
"character/animation-packs.ts",
|
|
15373
15608
|
"character/motion-actions.ts",
|
|
15374
15609
|
"character/meshy/meshy-loader.ts",
|
|
15610
|
+
// The player's body, resolved once: this game's own generated character
|
|
15611
|
+
// when it has one, the visiting player's avatar when it doesn't. ONE
|
|
15612
|
+
// shape for both lanes, so a character that lands at hour 2 is a file
|
|
15613
|
+
// drop and not a rewrite of the boot path written at hour 0.
|
|
15614
|
+
"character/player-character.ts",
|
|
15375
15615
|
"character/presets.ts",
|
|
15376
15616
|
// VRM avatar support (three-vrm): load + retarget the UAL clips + auto-fit
|
|
15377
|
-
// the capsule + optional foot IK
|
|
15617
|
+
// the capsule + optional foot IK — the FALLBACK body's lane.
|
|
15378
15618
|
"character/vrm/vrm-loader.ts",
|
|
15379
15619
|
"character/vrm/vrm-retarget.ts",
|
|
15380
15620
|
"character/vrm/capsule-fit.ts",
|
|
@@ -15383,22 +15623,27 @@ var CONTROLLER_FILE_SETS = {
|
|
|
15383
15623
|
NOTICE
|
|
15384
15624
|
],
|
|
15385
15625
|
// The FALLBACK VRM is written to public/assets/avatar.vrm at install time by
|
|
15386
|
-
//
|
|
15387
|
-
//
|
|
15388
|
-
//
|
|
15389
|
-
//
|
|
15390
|
-
//
|
|
15391
|
-
//
|
|
15626
|
+
// installFallbackAvatar (the bundled CC0 default) — so it is NOT a static
|
|
15627
|
+
// manifest asset; at runtime the game loads the playing user's own avatar
|
|
15628
|
+
// (user.avatarUrl from the embed identity, AG-804) and this file covers
|
|
15629
|
+
// local dev + load failures. It is only ever the FALLBACK body: the player
|
|
15630
|
+
// is this game's own generated character whenever it has one.
|
|
15631
|
+
// animation-library.glb (the 12-clip core) still is a manifest asset; extra
|
|
15632
|
+
// clips arrive via `genex controller anims` into public/assets/anims/.
|
|
15392
15633
|
assets: ["assets/animation-library.glb"],
|
|
15393
15634
|
skill: "genex-threejs-character-controller",
|
|
15635
|
+
// ONE sketch, with or without --character. The lane is a runtime decision
|
|
15636
|
+
// inside loadPlayerCharacter, never a code edit the agent has to remember
|
|
15637
|
+
// to make once the generated character lands.
|
|
15394
15638
|
sketch: [
|
|
15395
15639
|
`const physics = await PhysicsWorld.create();`,
|
|
15396
|
-
`const { user } = await waitForPlayer(); // @genex-ai/embed-sdk
|
|
15397
|
-
`const
|
|
15398
|
-
`const
|
|
15399
|
-
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...
|
|
15400
|
-
`character.root.add(scene);
|
|
15401
|
-
`
|
|
15640
|
+
`const { user } = await waitForPlayer(); // @genex-ai/embed-sdk`,
|
|
15641
|
+
`const player = await loadPlayerCharacter({ avatarUrl: user.avatarUrl }); // this game's character, else the avatar fallback`,
|
|
15642
|
+
`const fit = capsuleFromModel(player.scene);`,
|
|
15643
|
+
`const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
|
|
15644
|
+
`character.root.add(player.scene); player.scene.position.y = fit.modelOffsetY;`,
|
|
15645
|
+
`const anims = new CharacterAnimations(player.scene, player.clips, { locomotionProfile: player.locomotionProfile });`,
|
|
15646
|
+
`addEventListener("pointerdown", () => anims.playOneShot("Punch_Jab")); // per frame: anims.update(character, dt); player.update(dt);`
|
|
15402
15647
|
]
|
|
15403
15648
|
},
|
|
15404
15649
|
car: {
|
|
@@ -15557,8 +15802,7 @@ async function runController(opts) {
|
|
|
15557
15802
|
return;
|
|
15558
15803
|
}
|
|
15559
15804
|
} else {
|
|
15560
|
-
|
|
15561
|
-
await installOwnerAvatar({ root, srcDir, apiUrl: getApiUrl(opts.apiUrl), token, log });
|
|
15805
|
+
await installFallbackAvatar({ root, srcDir, log });
|
|
15562
15806
|
}
|
|
15563
15807
|
log.plain("");
|
|
15564
15808
|
}
|
|
@@ -15577,16 +15821,18 @@ async function runController(opts) {
|
|
|
15577
15821
|
log.plain(
|
|
15578
15822
|
kind === "touch" ? ` ${stepOffset + 2}. Wiring sketch (create behind a touch check; read per frame):` : ` ${stepOffset + 2}. Wiring sketch (controllers update BEFORE the physics step):`
|
|
15579
15823
|
);
|
|
15580
|
-
const
|
|
15581
|
-
`const physics = await PhysicsWorld.create();`,
|
|
15582
|
-
`const native = await loadMeshyCharacter("./assets/meshy-character.json");`,
|
|
15583
|
-
`const fit = capsuleFromModel(native.scene); const character = new CharacterController(physics.world, camera, { ...characterPresets["default"].options, ...fit, position: { x: 0, y: 2, z: 0 } });`,
|
|
15584
|
-
`character.root.add(native.scene); const anims = new CharacterAnimations(native.scene, native.clips, { locomotionProfile: native.locomotionProfile });`,
|
|
15585
|
-
`physics.onBeforeStep(() => character.update(dt, input)); // Rapier owns movement; then anims.update(character, dt)`
|
|
15586
|
-
] : set.sketch;
|
|
15587
|
-
for (const line of sketch) {
|
|
15824
|
+
for (const line of set.sketch) {
|
|
15588
15825
|
log.dim(` ${line}`);
|
|
15589
15826
|
}
|
|
15827
|
+
if (kind === "character" && !await exists3(path17.join(root, ASSETS_DEST, "meshy-character.json"))) {
|
|
15828
|
+
log.plain("");
|
|
15829
|
+
log.plain(
|
|
15830
|
+
` ${stepOffset + 3}. This game has no generated character yet \u2014 ${c.cyan(
|
|
15831
|
+
'npx genex character "<look>"'
|
|
15832
|
+
)} makes the player's real body;`
|
|
15833
|
+
);
|
|
15834
|
+
log.plain(" the avatar above is only the fallback until it lands. No code change when it does.");
|
|
15835
|
+
}
|
|
15590
15836
|
}
|
|
15591
15837
|
async function installMeshyCharacterManifest(args) {
|
|
15592
15838
|
const response = await apiFetch(
|
|
@@ -15618,6 +15864,7 @@ async function installMeshyCharacterManifest(args) {
|
|
|
15618
15864
|
`
|
|
15619
15865
|
);
|
|
15620
15866
|
args.log.dim(` public/assets/meshy-character.json (${args.characterId}, current revision)`);
|
|
15867
|
+
args.log.success("This game's generated character is now the player's body \u2014 picked up on the next reload, no code change needed.");
|
|
15621
15868
|
const pack2 = manifest.controllerPack;
|
|
15622
15869
|
if (typeof pack2?.key === "string" && typeof pack2.version === "number") {
|
|
15623
15870
|
args.log.success(`Meshy controller pack ${pack2.key} v${pack2.version}`);
|
|
@@ -15747,36 +15994,12 @@ function assertCompleteMeshyControllerPack(manifest) {
|
|
|
15747
15994
|
incompletePack(`${pack2.key} v${pack2.version} fingerprint does not match its action bindings.`);
|
|
15748
15995
|
}
|
|
15749
15996
|
}
|
|
15750
|
-
async function
|
|
15751
|
-
const { root, srcDir,
|
|
15997
|
+
async function installFallbackAvatar(args) {
|
|
15998
|
+
const { root, srcDir, log } = args;
|
|
15752
15999
|
const dest = path17.join(root, ASSETS_DEST, "avatar.vrm");
|
|
15753
16000
|
await fs17.mkdir(path17.dirname(dest), { recursive: true });
|
|
15754
|
-
if (token) {
|
|
15755
|
-
try {
|
|
15756
|
-
const res = await apiFetch(`${apiUrl}/api/avatars/me`, {
|
|
15757
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
15758
|
-
});
|
|
15759
|
-
if (res.ok) {
|
|
15760
|
-
const me = await res.json();
|
|
15761
|
-
if (me.vrmUrl) {
|
|
15762
|
-
const vrmRes = await fetch(me.vrmUrl);
|
|
15763
|
-
if (vrmRes.ok) {
|
|
15764
|
-
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
15765
|
-
await fs17.writeFile(dest, buf);
|
|
15766
|
-
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
15767
|
-
return;
|
|
15768
|
-
}
|
|
15769
|
-
}
|
|
15770
|
-
}
|
|
15771
|
-
log.dim(" couldn't fetch your avatar; using the bundled default.");
|
|
15772
|
-
} catch {
|
|
15773
|
-
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
15774
|
-
}
|
|
15775
|
-
}
|
|
15776
16001
|
await fs17.copyFile(path17.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
15777
|
-
log.dim(
|
|
15778
|
-
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
15779
|
-
);
|
|
16002
|
+
log.dim(" public/assets/avatar.vrm (fallback avatar \u2014 bundled CC0 default)");
|
|
15780
16003
|
}
|
|
15781
16004
|
async function exists3(p) {
|
|
15782
16005
|
try {
|
|
@@ -18006,9 +18229,11 @@ ${c.bold("Usage")}
|
|
|
18006
18229
|
without a physics controller. "quality" = the
|
|
18007
18230
|
adaptive-quality kit (device tier, runtime governor,
|
|
18008
18231
|
per-tier asset rungs) every game wires at boot.
|
|
18009
|
-
Character
|
|
18232
|
+
Character's player body is the game's own generated
|
|
18233
|
+
character, with a VRM avatar as the fallback.
|
|
18010
18234
|
genex controller character --character <id>
|
|
18011
|
-
|
|
18235
|
+
Point the game at its generated character (writes the
|
|
18236
|
+
manifest; no code change \u2014 the loader picks it up).
|
|
18012
18237
|
genex controller anims <sel \u2026> Install legacy VRM + UAL animation clips by tag/name
|
|
18013
18238
|
into public/assets/anims/. --list shows the catalog;
|
|
18014
18239
|
--reset forgets the previous selection first.
|
|
@@ -18020,7 +18245,9 @@ ${c.bold("Usage")}
|
|
|
18020
18245
|
${c.bold("Options for the generators (`model` `skybox` `sfx` `music` `voice` `texture` `image` `video`)")}
|
|
18021
18246
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
18022
18247
|
--raw (skybox) send the prompt exactly as written \u2014 skip the
|
|
18023
|
-
appended environment-only
|
|
18248
|
+
appended environment-only clause AND the refusal on
|
|
18249
|
+
prompts that name structures/objects (a sky is sky only;
|
|
18250
|
+
landforms and weather always pass).
|
|
18024
18251
|
--duration <sec> (sfx, video, music) target clip length in seconds
|
|
18025
18252
|
(music: 10-300, default 90 \u2014 a looping gameplay track).
|
|
18026
18253
|
--transparent (image) transparent background (alpha) \u2014 for decals/stickers.
|
|
@@ -18136,8 +18363,8 @@ ${c.bold("Options for `character` / `animations search`")}
|
|
|
18136
18363
|
controller slots, requirements, review status, and estimated cost.
|
|
18137
18364
|
|
|
18138
18365
|
${c.bold("Options for `controller character` / legacy `controller anims`")}
|
|
18139
|
-
--character <id> (character) install the owned Meshy character manifest
|
|
18140
|
-
|
|
18366
|
+
--character <id> (character) install the owned Meshy character manifest \u2014 the
|
|
18367
|
+
game's own generated character becomes the player's body.
|
|
18141
18368
|
--list [selectors] (anims) show the bundled VRM + UAL tag/name catalog.
|
|
18142
18369
|
--reset (anims) forget the previous game-local UAL selection.
|
|
18143
18370
|
|
|
@@ -18323,6 +18550,9 @@ function parseArgs(argv) {
|
|
|
18323
18550
|
case "--no-defringe":
|
|
18324
18551
|
parsed.options.defringe = false;
|
|
18325
18552
|
break;
|
|
18553
|
+
case "--allow-speckle":
|
|
18554
|
+
parsed.options.allowSpeckle = true;
|
|
18555
|
+
break;
|
|
18326
18556
|
case "--loop":
|
|
18327
18557
|
parsed.options.loop = true;
|
|
18328
18558
|
break;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.91.0-dev.228",
|
|
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": {
|
|
@@ -46,11 +46,13 @@ license (SPDX: `CC0-1.0`). No attribution is required by the license; this
|
|
|
46
46
|
notice is provided as a courtesy — supporting Quaternius on Patreon is a nice
|
|
47
47
|
way to say thanks.
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Fallback avatar — CC0 1.0 Universal
|
|
50
50
|
|
|
51
|
-
The
|
|
52
|
-
|
|
53
|
-
`public/assets/avatar.vrm
|
|
51
|
+
The player's body is the game's own generated character; a VRM avatar is the
|
|
52
|
+
fallback lane. `genex controller character` copies the bundled default
|
|
53
|
+
`assets/default-avatar.vrm` to `public/assets/avatar.vrm`, which covers local
|
|
54
|
+
dev and load failures when that lane runs. It is **Wizzir** from the
|
|
55
|
+
**100 Avatars** project by
|
|
54
56
|
**Polygonal Mind** (polygonalmind.com), dedicated to the public domain under
|
|
55
57
|
CC0 1.0 Universal (SPDX: `CC0-1.0`). Attribution is a courtesy, not required.
|
|
56
58
|
|