@genex-ai/cli-demo 1.13.1-dev.558 → 1.13.2-dev.559
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 +285 -145
- package/package.json +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +11 -7
- package/templates/skills/genex-game-director/SKILL.md +76 -36
- package/templates/skills/genex-game-director/references/design-contract.md +13 -5
- package/templates/skills/genex-game-director/references/routing-map.md +12 -44
- package/templates/skills/genex-threejs-procedural-assets/SKILL.md +17 -10
package/dist/index.js
CHANGED
|
@@ -700,11 +700,11 @@ var CONTRACT_END = "<!-- genex:contract:end -->";
|
|
|
700
700
|
var GENEX_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
|
|
701
701
|
# Genex build contract (always in effect for this game)
|
|
702
702
|
|
|
703
|
-
Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` (\`--image\` from a reference image; \`model segment <id>\` into named parts; \`model rig <id>\` + \`model animate <rig-id> --preset walk\` for non-biped bodies \u2014 7 body plans) \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014
|
|
703
|
+
Your agentic capabilities that can boost your creation (all via \`npx genex \u2026\`): generate \`model\` (\`--image\` from a reference image; \`model segment <id>\` into named parts; \`model rig <id>\` + \`model animate <rig-id> --preset walk\` for non-biped bodies \u2014 7 body plans) \xB7 \`sfx\` \xB7 \`music\` \xB7 \`voice\` \xB7 \`texture\` \xB7 \`image\` (\`--edit\` \xB7 \`--inpaint\` \xB7 \`--glass\` \xB7 \`--clean\` \xB7 \`--upscale\`) \xB7 \`video\` \xB7 rigged \`character\` / \`creature\` \xB7 \`character animate <id> "<verb>"\` (also \`creature animate\`; \`--locomotion\` for the 8-way movement set, \`--video\` for your own footage) \xB7 the pixel toolbox \`ui extract|masks|plate|text-color|trim|audit\` \xB7 vendored \`controller character|car|drone|touch|quality|chat\` \xB7 \`animations search\` \xB7 \`wait <id>\` / \`wait --all\` \xB7 \`preview\` / \`publish\` \xB7 \`rename <name>\` (move the game to a new address \u2014 keeps its plays and likes, kills the old link, needs \`--yes\` once published, and the game must be rebuilt after). Full options: \`npx genex --help\`. There is NO sky generator: every sky, horizon, time of day and weather mood is built in code \u2014 your call how, from a background colour and fog to an atmospheric sky shader with a real sun. It costs nothing, weighs nothing on phones, and keeps the sun in sync with the scene's own lighting, which a painted panorama never can. Task\u2192lane routing lives in the \`genex-game-director\` skill's routing map \u2014 load the owning row before the first file of each subsystem, and re-load the map after any compaction.
|
|
704
704
|
|
|
705
|
-
Important note: put soul into your creations, with many details and love. Aim to make them realistic and feel real.
|
|
705
|
+
Important note: put soul into your creations, with many details and love. Aim to make them realistic and feel real. Reach for genex capabilities by default \u2014 they are how the world gets real assets (law 22) \u2014 but never limit your imagination \u2014 you are a powerful agent. Use your built-in sub-agents and iteration loops (\`/loop\` or your platform's equivalent) where you need them to reach outstanding results. Verify yourself. Anything you put in front of the player should already feel alive and impressive \u2014 never a bare scene waiting for "later". Build what's still missing before polishing what already works.
|
|
706
706
|
|
|
707
|
-
1. Load the \`genex-game-director\` skill before starting the requested work. Route from the player's latest clear request: a focused request starts directly without replaying discovery or commissioning unrelated lanes. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
|
|
707
|
+
1. Load the \`genex-game-director\` skill before starting the requested work. Route from the player's latest clear request: a focused request starts directly without replaying discovery or commissioning unrelated lanes. Before the first file of any subsystem, load its owning lane from the director's routing map and write it as \`Lane:\` beside that work in \`DESIGN.md\` (\`Lane: none \u2014 <why>\` when no row matches) \u2014 a matching row is not optional, and loading one lane never commissions another. After ANY context compaction or session resume, re-read this file and \`DESIGN.md\`, re-load the skill for the stage you are executing, and continue from the Build plan's \`Now:\` line \u2014 never from memory alone.
|
|
708
708
|
2. Ask only when one unresolved answer materially changes the work. If the request is clear, do not repeat an interview, confirm the pitch, force a concept round, or ask whole-game-vs-one-part again. For a genuinely broad new game, ask one decision: whole coordinated build or one request-relevant part first. If there is no clear request \u2014 a setup prompt pasted with nothing of their own \u2014 ask in PLAIN CHAT what they want to make, in their own words, and never fill the blank by pitching concepts. Once you know what you are building, ask one decision at a time with your built-in question tool with clickable answer options when you have one; use a short plain-chat question otherwise. If the player stays silent after a necessary question, proceed on stated assumptions where reasonable and record each as "assumed \u2014 player didn't answer" in DESIGN.md \u2192 Decisions.
|
|
709
709
|
3. \`DESIGN.md\` at the project root is the durable design contract AND build plan. Keep three truths distinct: the player's requested outcome, the current \`Now:\` focus, and open commitments. It must carry a \`## Build plan & status\` section with the working mode (\`whole coordinated build\`, \`step by step\`, or \`focused change\`), numbered milestones with status marks, and a \`Now:\` line naming the current one \u2014 a milestone is done only when its work reached a preview. The latest clear request may replace \`Now:\` immediately; it never silently shrinks the requested outcome or deletes unrelated commitments. Keep every decision, assumption, generation id/URL, local output, and wiring state current. When the plan first lands, tell the player in one plain line that you recorded what they asked for in \`DESIGN.md\` and will keep it current.
|
|
710
710
|
4. Use \`genex\` commands to generate art, audio, video, characters, and UI. A local reference image is not a reason to switch tools: pass its file path to genex (\`--edit\`, \`--inpaint\`, and model's \`--image\` accept local paths). Don't limit yourself: add details, and mix generated assets with procedural generation to make scenes and games detailed and lively.
|
|
@@ -713,7 +713,7 @@ Important note: put soul into your creations, with many details and love. Aim to
|
|
|
713
713
|
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\`.
|
|
714
714
|
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.
|
|
715
715
|
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).
|
|
716
|
-
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. 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.
|
|
716
|
+
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.
|
|
717
717
|
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.
|
|
718
718
|
12. Treat every \`genex\` warning line \u2014 preflight, \`ui audit\`, \`wait\` nudges \u2014 as work, not noise.
|
|
719
719
|
13. 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\`) \u2014 never a localhost link, a file path, or the bare play origin presented as their game. That page shows the build you just previewed; \`preview\` never disturbs the build players are on. When the player is happy with it, \`npx genex promote\` makes that exact build live for everyone \u2014 no rebuild. After every round of player feedback, end with a preview push. After the first release, the game is two versions and the player only ever hears these two words for them: the **draft** (their working copy, updated by \`preview\`) and the **public version** (what everyone plays). Command names are yours, not theirs \u2014 never say "promote" to the player; ask directly: "Want me to update the public version?", one line, once per round, and keep building while you wait \u2014 updating the public version is the ONE action that waits for an explicit yes, and more change requests instead of a yes mean "not yet". The player's "publish it" / "publish the update" / "update it" / "yes" after the first release ALL mean \`genex promote\` \u2014 never \`genex publish\` again, which would ship an untried rebuild. \`promote\` ships the last previewed build, so if anything was edited since the last \`preview\`, preview again before promoting.
|
|
@@ -724,6 +724,8 @@ Important note: put soul into your creations, with many details and love. Aim to
|
|
|
724
724
|
18. NEVER delete, empty, move, rename, or overwrite anything you did not create yourself. This folder may hold the player's own reference images, notes, sketches, or an earlier attempt \u2014 files that exist nowhere else and have no undo, no trash, no backup. A non-empty folder is normal and is NEVER something to clean up, and "start clean" is never a reason. That rules out \`rm\`/\`rm -rf\`, \`git clean\`, \`git checkout -- .\`, \`git reset --hard\` over their work, deleting to resolve a conflict or a stuck interactive prompt, and every setup tool's offer to empty a directory (\`--force\`, \`--overwrite\`, "Remove existing files") \u2014 scaffold into a fresh subfolder and copy in instead. You may add files and edit the ones you wrote. If a step genuinely cannot continue without removing something of theirs, STOP and ask, naming the exact files, and wait for a yes \u2014 "it looks like junk" is never that yes. This binds hardest during setup, where it runs fast and automatically before the player has asked for anything at all.
|
|
725
725
|
19. If the game sells anything, it sells it for coin at a fixed, visible price, and NEVER sells chance. Run this test on any purchasable thing before building it: does the player pay (with coin, or with anything coin bought, directly or indirectly), is the outcome uncertain when they pay, and is there a prize they wanted \u2014 all three yes means it is paid randomness, and you build the deterministic version instead. That rules out loot boxes, gacha, mystery boxes, crates, card packs, prize wheels and raffles; wagering, staking, betting, coinflips and casino or slot mechanics denominated in coin; and donation prompts, tip jars or any player-to-player coin transfer, because coin buys goods and never just moves. Randomness the player EARNS by playing is gameplay, not commerce \u2014 an enemy dropping a random item, a chest found in the level, a procedural layout, a crit roll \u2014 and is completely fine. Every coin price renders its real-money equivalent beside it (the server sends one with every item), item prices sit on the platform's price grid so no player is left holding change they cannot spend, and nothing in a shop carries a countdown, "limited time", or a stock counter. When a request crosses one of these lines, name the mechanic, give the one-sentence reason, propose a specific compliant alternative, and build that \u2014 never the banned version "as an option", never a partial one, and never after asking the player to confirm they want it. Load \`$genex-monetization\` before building a shop.
|
|
726
726
|
20. When the player names a specific thing to integrate \u2014 a library, a controller, an asset, a part of an existing game, or a tagged reference the platform placed in \`.genex/refs/<slug>/\` \u2014 integrate the real thing: read its source first and build from it, never a lighter look-alike written from scratch because it was faster. A tagged reference owns the subsystem it was tagged for, outranking the default lane and the vendored controllers for that subsystem. If you genuinely can't use it, say so in one plain line and ask how to proceed.
|
|
727
|
+
21. A turn ends in exactly one of two ways: on a question the player must answer before the next step can be chosen, or on a handoff \u2014 the draft link, what changed, what to try, and what is still open. Never close a turn on a promise. "I'll keep building", "while I keep working", "adding that now" are things you say and then DO before you stop; if you are stopping, say that you have stopped and what remains. While the Build plan's \`Now:\` line has open work and no answer is needed from the player, the turn is not over: preview, hand off, and start the next milestone in the same turn, until the requested outcome is reached or the platform's own budget ends the session. If a stop was forced on you anyway, the first line of your next turn is the promise you left and where it stands.
|
|
728
|
+
22. **What the player sees must read as the thing it is**, in this game's own style: a building as that building, a person as a person, a prop as that prop, a surface as its material \u2014 and a coloured box, a capsule, or a flat grey block standing in for one of them is never its finished version. Generation is the default route to that bar wherever code will not honestly reach it: the player's character, whatever the request names, whatever the player walks up to, enters, or interacts with, and the music and sound underneath \u2014 reach for the belt on your own judgment, without stopping to ask; a status line saying what you queued is enough, and \`--no-wait\` keeps you building while it lands. Procedural code is a first-class engine for what is structural, repeated, distant, or parametric \u2014 terrain, sky, fences, paving, modular kits, filler \u2014 and for anything else only when the result meets the same bar and you have looked at it in a capture before its row says \`landed\`. Decide per object, not per category, and write the route on each Assets row in \`DESIGN.md\` \u2014 the paid flow for generated pieces, the procedural flow with its local path for code-built ones. An Assets table with no generation in it is a decision, not a default: record it as \`Generation: none \u2014 <why code alone reaches the bar here>\` or generate.
|
|
727
729
|
${CONTRACT_END}
|
|
728
730
|
`;
|
|
729
731
|
var GENEX_TOOLS_CONTRACT_BLOCK = `${CONTRACT_BEGIN}
|
|
@@ -2466,10 +2468,222 @@ function relTime(iso) {
|
|
|
2466
2468
|
|
|
2467
2469
|
// src/lib/deploy.ts
|
|
2468
2470
|
import "child_process";
|
|
2471
|
+
|
|
2472
|
+
// src/lib/generation-ledger.ts
|
|
2473
|
+
import fs12 from "fs/promises";
|
|
2474
|
+
import path12 from "path";
|
|
2475
|
+
var ledgerPath = (cwd) => path12.join(cwd, ".genex", "generations.ndjson");
|
|
2476
|
+
async function append(cwd, event) {
|
|
2477
|
+
try {
|
|
2478
|
+
await fs12.access(path12.join(cwd, ".genex"));
|
|
2479
|
+
await fs12.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
2480
|
+
`, "utf8");
|
|
2481
|
+
} catch {
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
async function readLedger(cwd = process.cwd()) {
|
|
2485
|
+
let raw;
|
|
2486
|
+
try {
|
|
2487
|
+
raw = await fs12.readFile(ledgerPath(cwd), "utf8");
|
|
2488
|
+
} catch {
|
|
2489
|
+
return [];
|
|
2490
|
+
}
|
|
2491
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2492
|
+
for (const line of raw.split("\n")) {
|
|
2493
|
+
if (!line.trim()) continue;
|
|
2494
|
+
try {
|
|
2495
|
+
const e = JSON.parse(line);
|
|
2496
|
+
if (e.t === "q" && typeof e.id === "string" && typeof e.kind === "string") {
|
|
2497
|
+
if (!entries.has(e.id)) {
|
|
2498
|
+
entries.set(e.id, {
|
|
2499
|
+
id: e.id,
|
|
2500
|
+
kind: e.kind,
|
|
2501
|
+
prompt: typeof e.prompt === "string" ? e.prompt : "",
|
|
2502
|
+
...typeof e.tail === "string" ? { promptTail: e.tail } : {},
|
|
2503
|
+
queuedAt: typeof e.at === "string" ? e.at : "",
|
|
2504
|
+
status: "queued"
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
} else if (e.t === "done" && typeof e.id === "string") {
|
|
2508
|
+
const entry = entries.get(e.id);
|
|
2509
|
+
if (entry && (e.status === "completed" || e.status === "failed")) {
|
|
2510
|
+
entry.status = e.status;
|
|
2511
|
+
if (Array.isArray(e.urls)) entry.urls = e.urls.filter((u) => typeof u === "string");
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
} catch {
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
return [...entries.values()];
|
|
2518
|
+
}
|
|
2519
|
+
async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
|
|
2520
|
+
await append(cwd, {
|
|
2521
|
+
t: "q",
|
|
2522
|
+
id,
|
|
2523
|
+
kind,
|
|
2524
|
+
prompt: prompt.slice(0, 120),
|
|
2525
|
+
...prompt.length > 120 ? { tail: prompt.slice(-120) } : {},
|
|
2526
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2527
|
+
});
|
|
2528
|
+
}
|
|
2529
|
+
async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
2530
|
+
const existing = (await readLedger(cwd)).find((e) => e.id === id);
|
|
2531
|
+
if (existing && existing.status === status) return;
|
|
2532
|
+
await append(cwd, { t: "done", id, status, ...urls?.length ? { urls } : {} });
|
|
2533
|
+
}
|
|
2534
|
+
async function countFailed(kind, cwd = process.cwd()) {
|
|
2535
|
+
return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
|
|
2536
|
+
}
|
|
2537
|
+
async function countOutcomes(kind, cwd = process.cwd()) {
|
|
2538
|
+
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
2539
|
+
return {
|
|
2540
|
+
failed: rows.filter((e) => e.status === "failed").length,
|
|
2541
|
+
completed: rows.filter((e) => e.status === "completed").length
|
|
2542
|
+
};
|
|
2543
|
+
}
|
|
2544
|
+
|
|
2545
|
+
// src/lib/generation-plan.ts
|
|
2546
|
+
var LANE_OF_KIND = {
|
|
2547
|
+
image: "image",
|
|
2548
|
+
video: "video",
|
|
2549
|
+
sfx: "sfx",
|
|
2550
|
+
music: "music",
|
|
2551
|
+
voice: "voice",
|
|
2552
|
+
texture: "texture",
|
|
2553
|
+
skybox: "skybox",
|
|
2554
|
+
model: "model",
|
|
2555
|
+
model_segment: "model",
|
|
2556
|
+
model_rig: "model",
|
|
2557
|
+
model_animation: "model",
|
|
2558
|
+
character: "character",
|
|
2559
|
+
character_concept: "character",
|
|
2560
|
+
character_preview: "character",
|
|
2561
|
+
character_animation: "character",
|
|
2562
|
+
character_motion: "character",
|
|
2563
|
+
motion: "motion"
|
|
2564
|
+
};
|
|
2565
|
+
var LANE_OF_SKILL = {
|
|
2566
|
+
"genex-ai-model": "model",
|
|
2567
|
+
"genex-ai-texture": "texture",
|
|
2568
|
+
"genex-ai-image": "image",
|
|
2569
|
+
"genex-ai-video": "video",
|
|
2570
|
+
"genex-ai-sfx": "sfx",
|
|
2571
|
+
"genex-ai-music": "music",
|
|
2572
|
+
"genex-ai-voice": "voice",
|
|
2573
|
+
"genex-ai-character": "character",
|
|
2574
|
+
"genex-ai-hud": "image",
|
|
2575
|
+
"genex-ai-menu": "image"
|
|
2576
|
+
};
|
|
2577
|
+
var LANE_COMMAND = {
|
|
2578
|
+
model: "npx genex model",
|
|
2579
|
+
texture: "npx genex texture",
|
|
2580
|
+
image: "npx genex image",
|
|
2581
|
+
video: "npx genex video",
|
|
2582
|
+
sfx: "npx genex sfx",
|
|
2583
|
+
music: "npx genex music",
|
|
2584
|
+
voice: "npx genex voice",
|
|
2585
|
+
character: "npx genex character",
|
|
2586
|
+
motion: 'npx genex character animate <id> "<verb>"'
|
|
2587
|
+
};
|
|
2588
|
+
var LANE_TOKEN = /\$(genex-[a-z0-9-]+)/g;
|
|
2589
|
+
var PROCEDURAL_ROW = /procedural|code-built|hand-built|three\.?js|\.tsx?\b|building \(/i;
|
|
2590
|
+
var PAID_ROW = /\b(model|glb|mesh|texture|image|sprite|poster|decal|video|sfx|sound|music|track|voice|character|creature|motion)\b/i;
|
|
2591
|
+
var LANE_WORDS = [
|
|
2592
|
+
[/\b(model|glb|mesh)\b/i, "model"],
|
|
2593
|
+
[/\btexture\b/i, "texture"],
|
|
2594
|
+
[/\b(image|sprite|poster|decal)\b/i, "image"],
|
|
2595
|
+
[/\bvideo\b/i, "video"],
|
|
2596
|
+
[/\b(sfx|sound)\b/i, "sfx"],
|
|
2597
|
+
[/\b(music|track)\b/i, "music"],
|
|
2598
|
+
[/\bvoice\b/i, "voice"],
|
|
2599
|
+
[/\b(character|creature)\b/i, "character"],
|
|
2600
|
+
[/\bmotion\b/i, "motion"]
|
|
2601
|
+
];
|
|
2602
|
+
var GENERATION_NONE = /^\s*Generation:\s*none\s*—/im;
|
|
2603
|
+
var STATUS = /\b(proposed|planned|generating|landed|wired)\b/i;
|
|
2604
|
+
function sectionRows(design, heading) {
|
|
2605
|
+
const lines = design.split("\n");
|
|
2606
|
+
const start = lines.findIndex((l) => heading.test(l));
|
|
2607
|
+
if (start < 0) return [];
|
|
2608
|
+
const rows = [];
|
|
2609
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
2610
|
+
const line = lines[i] ?? "";
|
|
2611
|
+
if (/^##\s/.test(line)) break;
|
|
2612
|
+
if (!line.trim().startsWith("|")) continue;
|
|
2613
|
+
const cells = line.split("|").map((c2) => c2.trim()).filter((c2) => c2.length > 0);
|
|
2614
|
+
if (cells.length === 0 || cells.every((c2) => /^:?-+:?$/.test(c2))) continue;
|
|
2615
|
+
if (/^asset$/i.test(cells[0] ?? "") || /^module$/i.test(cells[0] ?? "")) continue;
|
|
2616
|
+
rows.push(cells);
|
|
2617
|
+
}
|
|
2618
|
+
return rows;
|
|
2619
|
+
}
|
|
2620
|
+
function classify(cells) {
|
|
2621
|
+
const name = cells[0] ?? "";
|
|
2622
|
+
if (/^<.*>$/.test(name)) return null;
|
|
2623
|
+
const rest = cells.slice(1).join(" ");
|
|
2624
|
+
const status = (STATUS.exec(rest) ?? [""])[0].toLowerCase();
|
|
2625
|
+
if (PROCEDURAL_ROW.test(rest)) return { name, lane: null, status, procedural: true };
|
|
2626
|
+
const token = new RegExp(LANE_TOKEN.source).exec(rest)?.[1];
|
|
2627
|
+
const fromSkill = token ? LANE_OF_SKILL[token] : void 0;
|
|
2628
|
+
if (fromSkill) return { name, lane: fromSkill, status, procedural: false };
|
|
2629
|
+
if (!PAID_ROW.test(rest) && !/https?:\/\//.test(rest) && !/generating\s*\(/i.test(rest)) return null;
|
|
2630
|
+
const lane = LANE_WORDS.find(([re]) => re.test(rest))?.[1] ?? null;
|
|
2631
|
+
return { name, lane, status, procedural: false };
|
|
2632
|
+
}
|
|
2633
|
+
function auditGenerationPlan(input) {
|
|
2634
|
+
const { design, ledger } = input;
|
|
2635
|
+
if (design.trim().length === 0) return [];
|
|
2636
|
+
const assetRows = sectionRows(design, /^##\s+Assets\b/i);
|
|
2637
|
+
const proceduralRows = sectionRows(design, /^##\s+Procedural assets\b/i);
|
|
2638
|
+
const rows = [
|
|
2639
|
+
...assetRows.map(classify),
|
|
2640
|
+
...proceduralRows.map((c2) => {
|
|
2641
|
+
const r = classify(c2);
|
|
2642
|
+
return r === null ? null : { ...r, lane: null, procedural: true };
|
|
2643
|
+
})
|
|
2644
|
+
].filter((r) => r !== null);
|
|
2645
|
+
const declaredLanes = /* @__PURE__ */ new Set();
|
|
2646
|
+
for (const m of design.matchAll(LANE_TOKEN)) {
|
|
2647
|
+
const lane = m[1] ? LANE_OF_SKILL[m[1]] : void 0;
|
|
2648
|
+
if (lane) declaredLanes.add(lane);
|
|
2649
|
+
}
|
|
2650
|
+
const lanesRun = new Set(ledger.map((e) => LANE_OF_KIND[e.kind] ?? e.kind));
|
|
2651
|
+
const warnings = [];
|
|
2652
|
+
const owed = /* @__PURE__ */ new Map();
|
|
2653
|
+
for (const row of rows) {
|
|
2654
|
+
if (row.procedural || row.lane === null) continue;
|
|
2655
|
+
if (row.status !== "planned" && row.status !== "generating") continue;
|
|
2656
|
+
if (lanesRun.has(row.lane)) continue;
|
|
2657
|
+
if (row.lane === "skybox") continue;
|
|
2658
|
+
const held = owed.get(row.lane) ?? [];
|
|
2659
|
+
held.push(row.status === "generating" ? `${row.name} (says generating, not in this project's ledger)` : row.name);
|
|
2660
|
+
owed.set(row.lane, held);
|
|
2661
|
+
}
|
|
2662
|
+
for (const lane of declaredLanes) {
|
|
2663
|
+
if (lanesRun.has(lane) || owed.has(lane) || lane === "skybox") continue;
|
|
2664
|
+
if (!rows.some((r) => r.lane === lane)) owed.set(lane, ["declared as this subsystem's lane"]);
|
|
2665
|
+
}
|
|
2666
|
+
for (const [lane, names] of owed) {
|
|
2667
|
+
const shown = names.slice(0, 3).join(", ") + (names.length > 3 ? `, +${names.length - 3} more` : "");
|
|
2668
|
+
warnings.push(
|
|
2669
|
+
`DESIGN.md plans generated ${lane} work (${shown}) and this project has never run a ${lane} generation \u2014 the plan says so and the generation ledger has nothing. Generate it (${LANE_COMMAND[lane] ?? `npx genex ${lane}`} "<prompt>" --no-wait), or change the row to say what happens instead and why.`
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2672
|
+
const paidRows = rows.filter((r) => !r.procedural);
|
|
2673
|
+
const codeRows = rows.filter((r) => r.procedural);
|
|
2674
|
+
if (paidRows.length === 0 && codeRows.length >= 1 && ledger.length === 0 && !GENERATION_NONE.test(design)) {
|
|
2675
|
+
warnings.push(
|
|
2676
|
+
`DESIGN.md's asset plan is all code-built rows and this project has never run a generation. What the player sees must read as the thing it is (build contract, law 22): a coloured box or a capsule is not a finished building, person, or prop. Generate what the request names and what the player looks at up close (npx genex model / texture / music \u2026 --no-wait), or record "Generation: none \u2014 <why code alone reaches the bar here>" beside the table.`
|
|
2677
|
+
);
|
|
2678
|
+
}
|
|
2679
|
+
return warnings;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// src/lib/deploy.ts
|
|
2469
2683
|
import crypto3 from "crypto";
|
|
2470
|
-
import
|
|
2684
|
+
import fs15 from "fs/promises";
|
|
2471
2685
|
import os8 from "os";
|
|
2472
|
-
import
|
|
2686
|
+
import path14 from "path";
|
|
2473
2687
|
|
|
2474
2688
|
// ../../packages/mobile-scan/src/image-dims.ts
|
|
2475
2689
|
function u32be(b, o) {
|
|
@@ -2733,12 +2947,12 @@ function tierFor(estVramMb) {
|
|
|
2733
2947
|
}
|
|
2734
2948
|
|
|
2735
2949
|
// src/commands/ui.ts
|
|
2736
|
-
import
|
|
2737
|
-
import
|
|
2950
|
+
import fs14 from "fs/promises";
|
|
2951
|
+
import path13 from "path";
|
|
2738
2952
|
import { PNG as PNG2 } from "pngjs";
|
|
2739
2953
|
|
|
2740
2954
|
// src/lib/png-tools.ts
|
|
2741
|
-
import
|
|
2955
|
+
import fs13 from "fs/promises";
|
|
2742
2956
|
import { PNG } from "pngjs";
|
|
2743
2957
|
var ALPHA_TRANSPARENT_MAX = 16;
|
|
2744
2958
|
var isHttpUrl = (s) => /^https?:\/\//i.test(s);
|
|
@@ -2749,12 +2963,12 @@ async function loadPng(input) {
|
|
|
2749
2963
|
if (!res.ok) throw new Error(`Couldn't fetch ${input} (HTTP ${res.status}).`);
|
|
2750
2964
|
buf = Buffer.from(await res.arrayBuffer());
|
|
2751
2965
|
} else {
|
|
2752
|
-
buf = await
|
|
2966
|
+
buf = await fs13.readFile(input);
|
|
2753
2967
|
}
|
|
2754
2968
|
return PNG.sync.read(buf);
|
|
2755
2969
|
}
|
|
2756
2970
|
async function writePng(file, png) {
|
|
2757
|
-
await
|
|
2971
|
+
await fs13.writeFile(file, PNG.sync.write(png));
|
|
2758
2972
|
}
|
|
2759
2973
|
function cropPng(image, box) {
|
|
2760
2974
|
const out = new PNG({ width: box.w, height: box.h });
|
|
@@ -3054,7 +3268,7 @@ async function uiExtract(opts, log) {
|
|
|
3054
3268
|
const dilatePx = opts.dilate ?? 0;
|
|
3055
3269
|
const sheet = await loadPng(input);
|
|
3056
3270
|
const { width: W, height: H, data } = sheet;
|
|
3057
|
-
await
|
|
3271
|
+
await fs14.mkdir(outDir, { recursive: true });
|
|
3058
3272
|
log.plain(c.bold("genex ui extract"));
|
|
3059
3273
|
log.dim(` ${input} (${W}x${H}) \u2192 ${outDir}, ${names.length} names`);
|
|
3060
3274
|
let hasTransparency = false;
|
|
@@ -3283,7 +3497,7 @@ async function uiExtract(opts, log) {
|
|
|
3283
3497
|
rimPixels: speckle.sampled
|
|
3284
3498
|
});
|
|
3285
3499
|
}
|
|
3286
|
-
const outPath =
|
|
3500
|
+
const outPath = path13.join(outDir, `${name}.png`);
|
|
3287
3501
|
await writePng(outPath, out);
|
|
3288
3502
|
const sidecar = {
|
|
3289
3503
|
name,
|
|
@@ -3302,7 +3516,7 @@ async function uiExtract(opts, log) {
|
|
|
3302
3516
|
defringed
|
|
3303
3517
|
};
|
|
3304
3518
|
const { name: _n, out: _o, ...sidecarBody } = sidecar;
|
|
3305
|
-
await
|
|
3519
|
+
await fs14.writeFile(
|
|
3306
3520
|
outPath.replace(/\.png$/i, "") + ".bbox.json",
|
|
3307
3521
|
JSON.stringify(sidecarBody, null, 2)
|
|
3308
3522
|
);
|
|
@@ -3311,8 +3525,8 @@ async function uiExtract(opts, log) {
|
|
|
3311
3525
|
`${name}.png ${cropW}x${cropH} (ar ${sidecar.aspectRatio}) at sheet ${padX0},${padY0}`
|
|
3312
3526
|
);
|
|
3313
3527
|
}
|
|
3314
|
-
const debugPath =
|
|
3315
|
-
await
|
|
3528
|
+
const debugPath = path13.join(outDir, "extract-debug.json");
|
|
3529
|
+
await fs14.writeFile(
|
|
3316
3530
|
debugPath,
|
|
3317
3531
|
JSON.stringify(
|
|
3318
3532
|
{
|
|
@@ -3774,7 +3988,7 @@ async function uiMasks(opts, log) {
|
|
|
3774
3988
|
const registrationTolerance = opts.registrationTolerance ?? 0.04;
|
|
3775
3989
|
const edgeFlushMax = opts.edgeFlushMax ?? 0.04;
|
|
3776
3990
|
const loosened = registrationTolerance > 0.04 || edgeFlushMax > 0.04 || minCoverage < 0.01 || maxCoverage > 0.85;
|
|
3777
|
-
await
|
|
3991
|
+
await fs14.mkdir(outDir, { recursive: true });
|
|
3778
3992
|
log.plain(c.bold("genex ui masks"));
|
|
3779
3993
|
log.dim(` ${input} (${image.width}x${image.height}), ${pairs.length} pair(s) \u2192 ${outDir}`);
|
|
3780
3994
|
const sheetComponents = detectSheetComponents(image, opts.minPixels ?? 2e3);
|
|
@@ -3827,11 +4041,11 @@ async function uiMasks(opts, log) {
|
|
|
3827
4041
|
});
|
|
3828
4042
|
}
|
|
3829
4043
|
const overlay = makeOverlay(clean2, converted.png);
|
|
3830
|
-
const framePath =
|
|
3831
|
-
const maskPath =
|
|
3832
|
-
const annotatedPath =
|
|
3833
|
-
const overlayPath =
|
|
3834
|
-
const metaPath =
|
|
4044
|
+
const framePath = path13.join(outDir, `${pair.name}-frame.png`);
|
|
4045
|
+
const maskPath = path13.join(outDir, `${pair.name}-mask.png`);
|
|
4046
|
+
const annotatedPath = path13.join(outDir, `${pair.name}-annotated-source.png`);
|
|
4047
|
+
const overlayPath = path13.join(outDir, `${pair.name}-overlay.png`);
|
|
4048
|
+
const metaPath = path13.join(outDir, `${pair.name}.annotated-progress.json`);
|
|
3835
4049
|
await writePng(framePath, clean2);
|
|
3836
4050
|
await writePng(maskPath, converted.png);
|
|
3837
4051
|
await writePng(annotatedPath, annotated);
|
|
@@ -3878,7 +4092,7 @@ async function uiMasks(opts, log) {
|
|
|
3878
4092
|
},
|
|
3879
4093
|
overlay: overlayPath
|
|
3880
4094
|
};
|
|
3881
|
-
await
|
|
4095
|
+
await fs14.writeFile(metaPath, `${JSON.stringify(meta, null, 2)}
|
|
3882
4096
|
`);
|
|
3883
4097
|
results.push(meta);
|
|
3884
4098
|
const fb = converted.bbox;
|
|
@@ -3894,8 +4108,8 @@ async function uiMasks(opts, log) {
|
|
|
3894
4108
|
);
|
|
3895
4109
|
}
|
|
3896
4110
|
}
|
|
3897
|
-
const indexPath =
|
|
3898
|
-
await
|
|
4111
|
+
const indexPath = path13.join(outDir, "annotated-progress.json");
|
|
4112
|
+
await fs14.writeFile(indexPath, `${JSON.stringify({ input, loosened, pairs: results }, null, 2)}
|
|
3899
4113
|
`);
|
|
3900
4114
|
log.plain("");
|
|
3901
4115
|
log.success(`Wrote ${pairs.length} frame+mask pair(s) \u2192 ${outDir} (index: ${indexPath}).`);
|
|
@@ -4021,7 +4235,7 @@ async function uiTextColor(opts, log) {
|
|
|
4021
4235
|
};
|
|
4022
4236
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
4023
4237
|
`);
|
|
4024
|
-
if (opts.out) await
|
|
4238
|
+
if (opts.out) await fs14.writeFile(opts.out, `${JSON.stringify(result, null, 2)}
|
|
4025
4239
|
`);
|
|
4026
4240
|
if (opts.cropPath) log.dim(` crop \u2192 ${opts.cropPath}`);
|
|
4027
4241
|
}
|
|
@@ -4053,7 +4267,7 @@ async function uiTrim(opts, log) {
|
|
|
4053
4267
|
const sidecar = computeBBoxes(trimmed);
|
|
4054
4268
|
const speckleAllowed = !!(speckle && speckle.ratio > SPECKLE_MAX_RATIO);
|
|
4055
4269
|
const sidecarPath = outPath.replace(/\.png$/i, "") + ".bbox.json";
|
|
4056
|
-
await
|
|
4270
|
+
await fs14.writeFile(
|
|
4057
4271
|
sidecarPath,
|
|
4058
4272
|
JSON.stringify(speckleAllowed ? { ...sidecar, speckleAllowed: true } : sidecar, null, 2)
|
|
4059
4273
|
);
|
|
@@ -4160,7 +4374,7 @@ async function uiPlate(opts, log) {
|
|
|
4160
4374
|
fail("No interior found \u2014 the image is fully transparent (or erode ate everything). Check --in / lower --erode.");
|
|
4161
4375
|
}
|
|
4162
4376
|
await writePng(outPath, out);
|
|
4163
|
-
const name =
|
|
4377
|
+
const name = path13.basename(outPath);
|
|
4164
4378
|
log.plain(c.bold("genex ui plate"));
|
|
4165
4379
|
log.success(`${outPath} ${W}x${H}, interior ${(count / (W * H) * 100).toFixed(1)}% (erode ${erode}px)`);
|
|
4166
4380
|
log.dim(" Wire it as the plate's silhouette (same box as the frame <img>, plate UNDER the art):");
|
|
@@ -4325,13 +4539,13 @@ async function walkFiles(dir) {
|
|
|
4325
4539
|
const out = [];
|
|
4326
4540
|
let entries;
|
|
4327
4541
|
try {
|
|
4328
|
-
entries = await
|
|
4542
|
+
entries = await fs14.readdir(dir, { withFileTypes: true });
|
|
4329
4543
|
} catch {
|
|
4330
4544
|
return out;
|
|
4331
4545
|
}
|
|
4332
4546
|
for (const entry of entries) {
|
|
4333
4547
|
if (entry.name === "node_modules" || entry.name.startsWith(".")) continue;
|
|
4334
|
-
const p =
|
|
4548
|
+
const p = path13.join(dir, entry.name);
|
|
4335
4549
|
if (entry.isDirectory()) out.push(...await walkFiles(p));
|
|
4336
4550
|
else out.push(p);
|
|
4337
4551
|
}
|
|
@@ -4340,7 +4554,7 @@ async function walkFiles(dir) {
|
|
|
4340
4554
|
async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
4341
4555
|
const viewportFindings = [];
|
|
4342
4556
|
try {
|
|
4343
|
-
const indexHtml = await
|
|
4557
|
+
const indexHtml = await fs14.readFile(path13.join(cwd, "index.html"), "utf8");
|
|
4344
4558
|
if (!/<meta[^>]+name=["']viewport["']/i.test(indexHtml)) {
|
|
4345
4559
|
viewportFindings.push({
|
|
4346
4560
|
kind: "viewport-meta",
|
|
@@ -4354,28 +4568,28 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4354
4568
|
}
|
|
4355
4569
|
} catch {
|
|
4356
4570
|
}
|
|
4357
|
-
const absAssets =
|
|
4571
|
+
const absAssets = path13.resolve(cwd, assetDir);
|
|
4358
4572
|
try {
|
|
4359
|
-
if (!(await
|
|
4573
|
+
if (!(await fs14.stat(absAssets)).isDirectory()) {
|
|
4360
4574
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
4361
4575
|
}
|
|
4362
4576
|
} catch {
|
|
4363
4577
|
return viewportFindings.length > 0 ? viewportFindings : null;
|
|
4364
4578
|
}
|
|
4365
|
-
const srcFiles = (await walkFiles(
|
|
4366
|
-
(p) => AUDIT_SRC_EXTS.has(
|
|
4579
|
+
const srcFiles = (await walkFiles(path13.resolve(cwd, srcDir))).filter(
|
|
4580
|
+
(p) => AUDIT_SRC_EXTS.has(path13.extname(p).toLowerCase())
|
|
4367
4581
|
);
|
|
4368
4582
|
try {
|
|
4369
|
-
for (const name of await
|
|
4370
|
-
const ext =
|
|
4371
|
-
if (ext === ".html" || ext === ".css") srcFiles.push(
|
|
4583
|
+
for (const name of await fs14.readdir(cwd)) {
|
|
4584
|
+
const ext = path13.extname(name).toLowerCase();
|
|
4585
|
+
if (ext === ".html" || ext === ".css") srcFiles.push(path13.join(cwd, name));
|
|
4372
4586
|
}
|
|
4373
4587
|
} catch {
|
|
4374
4588
|
}
|
|
4375
4589
|
const sources = [];
|
|
4376
4590
|
for (const p of srcFiles) {
|
|
4377
4591
|
try {
|
|
4378
|
-
sources.push({ rel:
|
|
4592
|
+
sources.push({ rel: path13.relative(cwd, p), text: await fs14.readFile(p, "utf8") });
|
|
4379
4593
|
} catch {
|
|
4380
4594
|
}
|
|
4381
4595
|
}
|
|
@@ -4385,11 +4599,11 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4385
4599
|
const metaByName = /* @__PURE__ */ new Map();
|
|
4386
4600
|
const bboxByPng = /* @__PURE__ */ new Map();
|
|
4387
4601
|
for (const p of assetFiles) {
|
|
4388
|
-
const base =
|
|
4602
|
+
const base = path13.basename(p);
|
|
4389
4603
|
const metaMatch = /^(.+)\.annotated-progress\.json$/.exec(base);
|
|
4390
4604
|
if (metaMatch) {
|
|
4391
4605
|
try {
|
|
4392
|
-
const meta = JSON.parse(await
|
|
4606
|
+
const meta = JSON.parse(await fs14.readFile(p, "utf8"));
|
|
4393
4607
|
metaByName.set(metaMatch[1], {
|
|
4394
4608
|
cleanCrop: meta.clean?.crop ?? null,
|
|
4395
4609
|
loosened: meta.loosened === true
|
|
@@ -4400,7 +4614,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4400
4614
|
}
|
|
4401
4615
|
if (base.endsWith(".bbox.json")) {
|
|
4402
4616
|
try {
|
|
4403
|
-
const sidecar = JSON.parse(await
|
|
4617
|
+
const sidecar = JSON.parse(await fs14.readFile(p, "utf8"));
|
|
4404
4618
|
if (sidecar.sheetBBox) bboxByPng.set(base.replace(/\.bbox\.json$/, ".png"), sidecar.sheetBBox);
|
|
4405
4619
|
} catch {
|
|
4406
4620
|
}
|
|
@@ -4409,7 +4623,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4409
4623
|
for (const [name, meta] of metaByName) {
|
|
4410
4624
|
if (!meta.cleanCrop || !referenced(`${name}-mask.png`)) continue;
|
|
4411
4625
|
for (const p of assetFiles) {
|
|
4412
|
-
const base =
|
|
4626
|
+
const base = path13.basename(p);
|
|
4413
4627
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4414
4628
|
if (base !== `${name}.png` && !base.startsWith(`${name}-`)) continue;
|
|
4415
4629
|
if (/-mask\.png$|-frame\.png$|-overlay\.png$|-annotated-source\.png$/.test(base)) continue;
|
|
@@ -4433,7 +4647,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4433
4647
|
}
|
|
4434
4648
|
const pngByBase = /* @__PURE__ */ new Map();
|
|
4435
4649
|
for (const p of assetFiles) {
|
|
4436
|
-
const base =
|
|
4650
|
+
const base = path13.basename(p);
|
|
4437
4651
|
if (base.toLowerCase().endsWith(".png")) pngByBase.set(base, p);
|
|
4438
4652
|
}
|
|
4439
4653
|
for (const [maskBase, maskPath] of pngByBase) {
|
|
@@ -4446,8 +4660,8 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4446
4660
|
let frame;
|
|
4447
4661
|
let mask;
|
|
4448
4662
|
try {
|
|
4449
|
-
frame = PNG2.sync.read(await
|
|
4450
|
-
mask = PNG2.sync.read(await
|
|
4663
|
+
frame = PNG2.sync.read(await fs14.readFile(framePath));
|
|
4664
|
+
mask = PNG2.sync.read(await fs14.readFile(maskPath));
|
|
4451
4665
|
} catch {
|
|
4452
4666
|
continue;
|
|
4453
4667
|
}
|
|
@@ -4466,7 +4680,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4466
4680
|
if (!referenced(base)) continue;
|
|
4467
4681
|
let png;
|
|
4468
4682
|
try {
|
|
4469
|
-
png = PNG2.sync.read(await
|
|
4683
|
+
png = PNG2.sync.read(await fs14.readFile(p));
|
|
4470
4684
|
} catch {
|
|
4471
4685
|
continue;
|
|
4472
4686
|
}
|
|
@@ -4486,7 +4700,7 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4486
4700
|
}
|
|
4487
4701
|
const maskReported = /* @__PURE__ */ new Set();
|
|
4488
4702
|
for (const p of assetFiles) {
|
|
4489
|
-
const m = /^(.+)\.annotated-progress\.json$/.exec(
|
|
4703
|
+
const m = /^(.+)\.annotated-progress\.json$/.exec(path13.basename(p));
|
|
4490
4704
|
if (!m) continue;
|
|
4491
4705
|
const maskBase = `${m[1]}-mask.png`;
|
|
4492
4706
|
if (!referenced(maskBase)) {
|
|
@@ -4498,14 +4712,14 @@ async function collectUiAuditFindings(assetDir, srcDir, cwd = process.cwd()) {
|
|
|
4498
4712
|
}
|
|
4499
4713
|
}
|
|
4500
4714
|
for (const p of assetFiles) {
|
|
4501
|
-
const base =
|
|
4715
|
+
const base = path13.basename(p);
|
|
4502
4716
|
if (!base.toLowerCase().endsWith(".png")) continue;
|
|
4503
4717
|
if (/-annotated-source\.png$|-overlay\.png$/.test(base)) continue;
|
|
4504
4718
|
if (maskReported.has(base)) continue;
|
|
4505
4719
|
if (!referenced(base)) {
|
|
4506
4720
|
findings.push({
|
|
4507
4721
|
kind: "unwired-sprite",
|
|
4508
|
-
message: `${
|
|
4722
|
+
message: `${path13.relative(cwd, p)} is on disk but never referenced in ${srcDir}/index.html/CSS \u2014 wire it, or record the one-line reason it was cut. (Computed-string references are invisible here \u2014 check.)`
|
|
4509
4723
|
});
|
|
4510
4724
|
}
|
|
4511
4725
|
}
|
|
@@ -4692,7 +4906,7 @@ async function printUiAuditPreflight(log) {
|
|
|
4692
4906
|
}
|
|
4693
4907
|
async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
4694
4908
|
try {
|
|
4695
|
-
const design = await
|
|
4909
|
+
const design = await fs15.readFile(path14.join(cwd, "DESIGN.md"), "utf8").catch(() => "");
|
|
4696
4910
|
const warnings = [];
|
|
4697
4911
|
if (design.length > 0 && !/##\s*build plan & status/i.test(design)) {
|
|
4698
4912
|
warnings.push(
|
|
@@ -4700,13 +4914,14 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4700
4914
|
);
|
|
4701
4915
|
}
|
|
4702
4916
|
if (!/player character:/i.test(design)) {
|
|
4703
|
-
const hasCharacter = await
|
|
4917
|
+
const hasCharacter = await fs15.access(path14.join(cwd, "public", "assets", "meshy-character.json")).then(() => true, () => false);
|
|
4704
4918
|
if (!hasCharacter && await loadsPlayerBody(cwd)) {
|
|
4705
4919
|
warnings.push(
|
|
4706
4920
|
`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).`
|
|
4707
4921
|
);
|
|
4708
4922
|
}
|
|
4709
4923
|
}
|
|
4924
|
+
warnings.push(...auditGenerationPlan({ design, ledger: await readLedger(cwd) }));
|
|
4710
4925
|
for (const w of warnings) log.warn(w);
|
|
4711
4926
|
} catch {
|
|
4712
4927
|
}
|
|
@@ -4714,12 +4929,12 @@ async function printPipelineStatePreflight(log, cwd = process.cwd()) {
|
|
|
4714
4929
|
async function loadsPlayerBody(cwd) {
|
|
4715
4930
|
const BODY_LOADERS = /\bloadPlayerCharacter\s*\(|\bloadVrm(?:Clone)?\s*\(/;
|
|
4716
4931
|
try {
|
|
4717
|
-
const entries = await
|
|
4932
|
+
const entries = await fs15.readdir(path14.join(cwd, "src"), { recursive: true });
|
|
4718
4933
|
for (const rel of entries) {
|
|
4719
4934
|
if (rel.includes("node_modules")) continue;
|
|
4720
|
-
if (rel.split(
|
|
4935
|
+
if (rel.split(path14.sep)[0] === "controllers" || rel.startsWith("controllers/")) continue;
|
|
4721
4936
|
if (!/\.(ts|tsx|js|jsx)$/.test(rel)) continue;
|
|
4722
|
-
const text = await
|
|
4937
|
+
const text = await fs15.readFile(path14.join(cwd, "src", rel), "utf8").catch(() => "");
|
|
4723
4938
|
if (BODY_LOADERS.test(text)) return true;
|
|
4724
4939
|
}
|
|
4725
4940
|
} catch {
|
|
@@ -4756,9 +4971,9 @@ async function deployGame(ctx, opts, log) {
|
|
|
4756
4971
|
}
|
|
4757
4972
|
log.success("Built.");
|
|
4758
4973
|
}
|
|
4759
|
-
const distDir =
|
|
4974
|
+
const distDir = path14.join(cwd, "dist");
|
|
4760
4975
|
const siteDir = await isDir(distDir) ? distDir : cwd;
|
|
4761
|
-
const rel =
|
|
4976
|
+
const rel = path14.relative(cwd, siteDir) || ".";
|
|
4762
4977
|
if (siteDir === cwd) await writeGitignore(cwd, log);
|
|
4763
4978
|
const files = await collectFiles(siteDir);
|
|
4764
4979
|
if (files.length === 0) {
|
|
@@ -4847,7 +5062,7 @@ async function deployGame(ctx, opts, log) {
|
|
|
4847
5062
|
}
|
|
4848
5063
|
async function hasBuildScript(cwd) {
|
|
4849
5064
|
try {
|
|
4850
|
-
const pkg = JSON.parse(await
|
|
5065
|
+
const pkg = JSON.parse(await fs15.readFile(path14.join(cwd, "package.json"), "utf8"));
|
|
4851
5066
|
return Boolean(pkg.scripts?.build);
|
|
4852
5067
|
} catch {
|
|
4853
5068
|
return false;
|
|
@@ -4856,12 +5071,12 @@ async function hasBuildScript(cwd) {
|
|
|
4856
5071
|
async function collectFiles(root) {
|
|
4857
5072
|
const out = [];
|
|
4858
5073
|
const walk2 = async (dir, prefix) => {
|
|
4859
|
-
for (const e of await
|
|
5074
|
+
for (const e of await fs15.readdir(dir, { withFileTypes: true })) {
|
|
4860
5075
|
const relPath = prefix ? `${prefix}/${e.name}` : e.name;
|
|
4861
5076
|
if (e.isDirectory()) {
|
|
4862
|
-
if (!EXCLUDE_DIRS.has(e.name)) await walk2(
|
|
5077
|
+
if (!EXCLUDE_DIRS.has(e.name)) await walk2(path14.join(dir, e.name), relPath);
|
|
4863
5078
|
} else if (e.isFile() && !isSecretEnvFile(e.name)) {
|
|
4864
|
-
out.push({ relPath, bytes: await
|
|
5079
|
+
out.push({ relPath, bytes: await fs15.readFile(path14.join(dir, e.name)) });
|
|
4865
5080
|
}
|
|
4866
5081
|
}
|
|
4867
5082
|
};
|
|
@@ -5103,7 +5318,7 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main", onCommi
|
|
|
5103
5318
|
log.error("Couldn't save your game's source \u2014 please try again.");
|
|
5104
5319
|
return false;
|
|
5105
5320
|
};
|
|
5106
|
-
const gitDir = await
|
|
5321
|
+
const gitDir = await fs15.mkdtemp(path14.join(os8.tmpdir(), "genex-source-"));
|
|
5107
5322
|
const base = { GIT_DIR: gitDir };
|
|
5108
5323
|
if (urlHasEmbeddedCredentials(pushUrl)) {
|
|
5109
5324
|
base.GIT_CONFIG_COUNT = "1";
|
|
@@ -5118,12 +5333,12 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main", onCommi
|
|
|
5118
5333
|
};
|
|
5119
5334
|
try {
|
|
5120
5335
|
if ((await run("git", ["init", "-q"], base)).code !== 0) return failed();
|
|
5121
|
-
await
|
|
5122
|
-
|
|
5336
|
+
await fs15.writeFile(
|
|
5337
|
+
path14.join(gitDir, "info", "exclude"),
|
|
5123
5338
|
// .env* are secrets — never publish them; `!` keeps the non-secret template.
|
|
5124
5339
|
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
5125
5340
|
);
|
|
5126
|
-
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE:
|
|
5341
|
+
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path14.join(gitDir, "index-source") };
|
|
5127
5342
|
let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
|
|
5128
5343
|
if (!lfs) {
|
|
5129
5344
|
log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
|
|
@@ -5175,7 +5390,7 @@ async function pushWorktree(cwd, pushUrl, managed, log, branch = "main", onCommi
|
|
|
5175
5390
|
} catch {
|
|
5176
5391
|
return failed();
|
|
5177
5392
|
} finally {
|
|
5178
|
-
await
|
|
5393
|
+
await fs15.rm(gitDir, { recursive: true, force: true }).catch(() => {
|
|
5179
5394
|
});
|
|
5180
5395
|
}
|
|
5181
5396
|
}
|
|
@@ -5227,7 +5442,7 @@ async function fetchPushUrl(ctx, log, expectStagingCommit) {
|
|
|
5227
5442
|
}
|
|
5228
5443
|
async function isDir(p) {
|
|
5229
5444
|
try {
|
|
5230
|
-
return (await
|
|
5445
|
+
return (await fs15.stat(p)).isDirectory();
|
|
5231
5446
|
} catch {
|
|
5232
5447
|
return false;
|
|
5233
5448
|
}
|
|
@@ -5711,81 +5926,6 @@ async function promoteBuild(apiUrl, projectId, token, log) {
|
|
|
5711
5926
|
// src/lib/detect-features.ts
|
|
5712
5927
|
import fs16 from "fs/promises";
|
|
5713
5928
|
import path15 from "path";
|
|
5714
|
-
|
|
5715
|
-
// src/lib/generation-ledger.ts
|
|
5716
|
-
import fs15 from "fs/promises";
|
|
5717
|
-
import path14 from "path";
|
|
5718
|
-
var ledgerPath = (cwd) => path14.join(cwd, ".genex", "generations.ndjson");
|
|
5719
|
-
async function append(cwd, event) {
|
|
5720
|
-
try {
|
|
5721
|
-
await fs15.access(path14.join(cwd, ".genex"));
|
|
5722
|
-
await fs15.appendFile(ledgerPath(cwd), `${JSON.stringify(event)}
|
|
5723
|
-
`, "utf8");
|
|
5724
|
-
} catch {
|
|
5725
|
-
}
|
|
5726
|
-
}
|
|
5727
|
-
async function readLedger(cwd = process.cwd()) {
|
|
5728
|
-
let raw;
|
|
5729
|
-
try {
|
|
5730
|
-
raw = await fs15.readFile(ledgerPath(cwd), "utf8");
|
|
5731
|
-
} catch {
|
|
5732
|
-
return [];
|
|
5733
|
-
}
|
|
5734
|
-
const entries = /* @__PURE__ */ new Map();
|
|
5735
|
-
for (const line of raw.split("\n")) {
|
|
5736
|
-
if (!line.trim()) continue;
|
|
5737
|
-
try {
|
|
5738
|
-
const e = JSON.parse(line);
|
|
5739
|
-
if (e.t === "q" && typeof e.id === "string" && typeof e.kind === "string") {
|
|
5740
|
-
if (!entries.has(e.id)) {
|
|
5741
|
-
entries.set(e.id, {
|
|
5742
|
-
id: e.id,
|
|
5743
|
-
kind: e.kind,
|
|
5744
|
-
prompt: typeof e.prompt === "string" ? e.prompt : "",
|
|
5745
|
-
...typeof e.tail === "string" ? { promptTail: e.tail } : {},
|
|
5746
|
-
queuedAt: typeof e.at === "string" ? e.at : "",
|
|
5747
|
-
status: "queued"
|
|
5748
|
-
});
|
|
5749
|
-
}
|
|
5750
|
-
} else if (e.t === "done" && typeof e.id === "string") {
|
|
5751
|
-
const entry = entries.get(e.id);
|
|
5752
|
-
if (entry && (e.status === "completed" || e.status === "failed")) {
|
|
5753
|
-
entry.status = e.status;
|
|
5754
|
-
if (Array.isArray(e.urls)) entry.urls = e.urls.filter((u) => typeof u === "string");
|
|
5755
|
-
}
|
|
5756
|
-
}
|
|
5757
|
-
} catch {
|
|
5758
|
-
}
|
|
5759
|
-
}
|
|
5760
|
-
return [...entries.values()];
|
|
5761
|
-
}
|
|
5762
|
-
async function recordQueued(id, kind, prompt, cwd = process.cwd()) {
|
|
5763
|
-
await append(cwd, {
|
|
5764
|
-
t: "q",
|
|
5765
|
-
id,
|
|
5766
|
-
kind,
|
|
5767
|
-
prompt: prompt.slice(0, 120),
|
|
5768
|
-
...prompt.length > 120 ? { tail: prompt.slice(-120) } : {},
|
|
5769
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
5770
|
-
});
|
|
5771
|
-
}
|
|
5772
|
-
async function recordTerminal(id, status, urls, cwd = process.cwd()) {
|
|
5773
|
-
const existing = (await readLedger(cwd)).find((e) => e.id === id);
|
|
5774
|
-
if (existing && existing.status === status) return;
|
|
5775
|
-
await append(cwd, { t: "done", id, status, ...urls?.length ? { urls } : {} });
|
|
5776
|
-
}
|
|
5777
|
-
async function countFailed(kind, cwd = process.cwd()) {
|
|
5778
|
-
return (await readLedger(cwd)).filter((e) => e.kind === kind && e.status === "failed").length;
|
|
5779
|
-
}
|
|
5780
|
-
async function countOutcomes(kind, cwd = process.cwd()) {
|
|
5781
|
-
const rows = (await readLedger(cwd)).filter((e) => e.kind === kind);
|
|
5782
|
-
return {
|
|
5783
|
-
failed: rows.filter((e) => e.status === "failed").length,
|
|
5784
|
-
completed: rows.filter((e) => e.status === "completed").length
|
|
5785
|
-
};
|
|
5786
|
-
}
|
|
5787
|
-
|
|
5788
|
-
// src/lib/detect-features.ts
|
|
5789
5929
|
async function detectEmbedSdkVersion(cwd = process.cwd()) {
|
|
5790
5930
|
try {
|
|
5791
5931
|
const raw = await fs16.readFile(path15.join(cwd, "package.json"), "utf8");
|
|
@@ -19391,7 +19531,7 @@ var EXPECT_DIR = {
|
|
|
19391
19531
|
bl: [D, -D],
|
|
19392
19532
|
br: [-D, -D]
|
|
19393
19533
|
};
|
|
19394
|
-
function
|
|
19534
|
+
function classify2(stemRaw) {
|
|
19395
19535
|
const stem = stemRaw.replace(/-s\d+$/, "");
|
|
19396
19536
|
if (stem.includes("jump") || stem.includes("ollie") || stem.includes("trick")) {
|
|
19397
19537
|
return { kind: "oneshot", expect: null };
|
|
@@ -19782,7 +19922,7 @@ function score(kind, m, gates = DEFAULT_GATES) {
|
|
|
19782
19922
|
}
|
|
19783
19923
|
function analyzeTake(stem, buf, gates = DEFAULT_GATES) {
|
|
19784
19924
|
const t = loadTake(buf);
|
|
19785
|
-
const { kind, expect } =
|
|
19925
|
+
const { kind, expect } = classify2(stem);
|
|
19786
19926
|
const F = t.F;
|
|
19787
19927
|
const lo = kind !== "oneshot" ? Math.min(RAMP, Math.max(4, Math.floor(F / 4))) : 8;
|
|
19788
19928
|
const w = [lo, F - Math.min(TAIL, Math.max(2, Math.floor(F / 8)))];
|
|
@@ -20047,7 +20187,7 @@ function compileSet(inputs, setName, cfg = DEFAULT_MOTION_CONFIG) {
|
|
|
20047
20187
|
const gaits = {};
|
|
20048
20188
|
const aimTakes = [];
|
|
20049
20189
|
for (const { stem, take } of inputs) {
|
|
20050
|
-
const { kind } =
|
|
20190
|
+
const { kind } = classify2(stem);
|
|
20051
20191
|
const aim = /aim|rifle|pistol/.test(stem);
|
|
20052
20192
|
if (aim && (kind === "idle" || kind === "freeform")) aimTakes.push({ stem, take });
|
|
20053
20193
|
if (kind === "loco") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.2-dev.559",
|
|
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": {
|
|
@@ -9,13 +9,17 @@ Turn a text prompt into a real, game-ready **GLB** and drop it into the project.
|
|
|
9
9
|
|
|
10
10
|
## When to use this vs. procedural geometry
|
|
11
11
|
|
|
12
|
-
- **Use `npx genex model`** for a specific, recognizable object
|
|
13
|
-
sword, a spaceship, an animal
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
- **Use `npx genex model`** for a specific, recognizable object the player will
|
|
13
|
+
look at — a barrel, a chair, a sword, a spaceship, an animal, a named
|
|
14
|
+
building, the thing the request asked for. You get a real textured mesh, and
|
|
15
|
+
this is the default route whenever code would not honestly make the object
|
|
16
|
+
read as itself.
|
|
17
|
+
- **Use `$genex-threejs-procedural-assets`** for what is structural, repeated,
|
|
18
|
+
distant, or parametric — modular kits, fences, walls, paving, terrain,
|
|
19
|
+
anything placed many times with variation (editable, seeded, no GLB file) —
|
|
20
|
+
and for a close-up object only when the code result meets the same bar and
|
|
21
|
+
you have checked it in a capture. Mixing both in one scene is the normal way
|
|
22
|
+
to build a detailed world.
|
|
19
23
|
|
|
20
24
|
**The output is a STATIC, unrigged mesh by default — no skeleton, no animation
|
|
21
25
|
clips.** A "wolf" or "guard" from this command can be posed and moved as one
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-game-director
|
|
3
|
-
description: The
|
|
3
|
+
description: The Genex request router. Read the latest game, tool, component, or focused-change request; ask only genuinely unresolved build forks; keep DESIGN.md current; and load each owning lane BEFORE the first file of the subsystem it owns — only the lanes the work touches, never the whole pack, and never fewer than the work needs. Use first for new work, resumed work, or whenever ownership is unclear.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex Game Director
|
|
7
7
|
|
|
8
8
|
The player chooses what to build and in what order. You route that request;
|
|
9
|
-
the owning Genex skills control how their lanes run.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
the owning Genex skills control how their lanes run.
|
|
10
|
+
|
|
11
|
+
Routing binds in BOTH directions: load only the lanes the requested work
|
|
12
|
+
touches — never the whole pack by default, and never unrelated work because an
|
|
13
|
+
old workflow listed it — and load a lane BEFORE the first file of the subsystem
|
|
14
|
+
it owns. A matching row in the routing map is not a suggestion: each lane
|
|
15
|
+
exists because writing that subsystem without it ships a known defect.
|
|
16
|
+
Loading one lane never commissions another.
|
|
12
17
|
|
|
13
18
|
**After any context compaction or session resume**, re-read the project's
|
|
14
19
|
`AGENTS.md`, `DESIGN.md` when present, and the skill for the work you are
|
|
@@ -103,32 +108,56 @@ Never invent a provider generation ID for local code. Sections for gameplay,
|
|
|
103
108
|
content, UI, world, assets, multiplayer, character/animation, tools, or
|
|
104
109
|
modules are conditional on the request and existing project.
|
|
105
110
|
|
|
106
|
-
## 4.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- `
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
-
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
-
|
|
122
|
-
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
111
|
+
## 4. Routing map — load the owning lane before the first file
|
|
112
|
+
|
|
113
|
+
For every subsystem the current milestone touches, find its row below, load
|
|
114
|
+
that skill, and write the lane beside the work in `DESIGN.md` — a `Lane` cell
|
|
115
|
+
on the module's row in the Modules table, or a `Lane:` line under `Now:` in
|
|
116
|
+
focused or step-by-step work. `Lane: none — <why>` is the only way to write a
|
|
117
|
+
subsystem with no lane. Then write the code. Never reconstruct a lane's API or
|
|
118
|
+
vendored code from memory of another engine.
|
|
119
|
+
|
|
120
|
+
| Work needed | Load |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| shot composition, chase/side/orbit rigs, camera handoffs, projection ownership, pointer look, mouse-aimed action, mouse-look, the screen-direction contract for hand-rolled steering/pan/look input signs, floating origins | `$genex-threejs-camera-direction` |
|
|
123
|
+
| on-foot player movement: walk/run/jump/crouch, third-person character, slopes, stairs, moving platforms, the player's body loader, directional locomotion, transitions, action motion | `$genex-threejs-character-controller` |
|
|
124
|
+
| **attacking, casting, aiming or reloading WHILE moving** — any action the legs must keep running under; a weapon carry stance over stock locomotion; a wind-up the character holds while walking | `$genex-threejs-character-controller` (`references/animations.md`, upper-body layering) |
|
|
125
|
+
| dash, dodge, roll, blink, backstep, a lunging attack — any burst that moves the character itself | `$genex-threejs-character-controller` (`references/tuning-and-presets.md`, dash recipe) |
|
|
126
|
+
| the game's own generated character—the player's body wherever a human body appears—or Meshy animation coverage beyond the stock pack: reference-informed A-pose concepts, exact action IDs, same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` |
|
|
127
|
+
| a character/enemy needs motion the catalog lacks—a signature move, boss telegraph, death, full 8-way set, or the player's footage; free plan before spend | `$genex-ai-character` motion section + `references/motion-generation.md` |
|
|
128
|
+
| remote player bodies in multiplayer—never hand-built primitives: the game's generated character when it has one, otherwise the player's `p.avatarUrl` VRM | `$genex-threejs-multiplayer` + `$genex-threejs-character-controller` |
|
|
129
|
+
| enemies, NPCs, or creatures: rigged bipeds via `npx genex creature`; non-biped body plans (quadruped, flier, serpent, aquatic, multi-leg) via `npx genex model rig` + `model animate`; static plus procedural motion only for shapes with no body plan; collider, facing, hit reaction, death | `$genex-threejs-creatures` |
|
|
130
|
+
| cars, drones, vehicle physics, gearbox, character↔vehicle enter/exit | `$genex-threejs-vehicle-controllers` |
|
|
131
|
+
| playable on phones: joystick, virtual buttons, drag zones, genre touch recipes, rotate-device overlay | `$genex-threejs-touch-controls` |
|
|
132
|
+
| phone-survivable rendering: device tiers, DPR/shadow/post budgets, runtime governor, generated-asset rungs, Quality picker, dispose-on-swap | `$genex-threejs-adaptive-quality` |
|
|
133
|
+
| anything falls, collides, gets pushed, or needs colliders/events | `$genex-threejs-physics-rapier` |
|
|
134
|
+
| launch/docking timelines, authored transform phases, springs, convergence, deterministic prop/debris motion | `$genex-threejs-procedural-animation` |
|
|
135
|
+
| rebuild a reference prop, hard-surface object, modular decoration, or structural environment piece (kits, fences, walls, paving) as editable parameterized Three.js code — held to the asset bar at the end of this section | `$genex-threejs-procedural-assets` |
|
|
136
|
+
| stylized/abstract shader-authored materials, the procedural/PBR material boundary | `$genex-threejs-procedural-assets` |
|
|
137
|
+
| particles, trails, plasma, shockwaves, pooled bursts, and event effects | `$genex-threejs-procedural-vfx` |
|
|
138
|
+
| stable large-world shadows, cascades, clipmaps, cached updates | `$genex-threejs-shadow-systems` |
|
|
139
|
+
| eye adaptation, tone mapping, output color, LUT grading, and proven static grain | `$genex-threejs-exposure-color-grading` |
|
|
140
|
+
| fixed-view screenshots, input direction, facing, temporal and budget evidence | `$genex-threejs-visual-validation` |
|
|
141
|
+
| the default for a concrete object the player looks at up close: a generated GLB for a prop, vehicle, building, or object — from text or a reference image (`--image`); split into named parts (`model segment`); rig + animate any mesh (`model rig` / `model animate`) | `$genex-ai-model` |
|
|
142
|
+
| generated surface or terrain texture with real-world UV scale | `$genex-ai-texture` |
|
|
143
|
+
| sky, skybox, horizon, time of day, weather mood, night or space backdrop | build it in code in the scene—there is no sky command and no owning skill, so pick the technique this game needs |
|
|
144
|
+
| poster, sign, sprite, decal, reference sheet, or other 2D art | `$genex-ai-image` |
|
|
145
|
+
| in-world motion art or another requested video | `$genex-ai-video` |
|
|
146
|
+
| sound effect, one looping music bed, or a short spoken line | `$genex-ai-sfx`, `$genex-ai-music`, or `$genex-ai-voice` |
|
|
147
|
+
| requested UI/HUD/menu/interface work, a visible UI problem, or an interface you decided this game wants built with generated art | `$genex-threejs-game-ui` |
|
|
148
|
+
| selling anything for platform coin: a shop, an item catalog, boosts, cosmetics, "make it earn"; also any request for a loot box, gacha, wager, casino mechanic or donation prompt, which that skill refuses and replaces | `$genex-monetization` |
|
|
149
|
+
| cinematic menu/title/pause/victory/defeat/lobby/credits video treatment | `$genex-ai-menu` |
|
|
150
|
+
| drawn HUD chrome the game's style wants—one element or a matched set of frames, masks, and icons | `$genex-ai-hud` |
|
|
151
|
+
| the game works but feels flat, floaty, or unresponsive: input response, camera, impacts, cooldowns, difficulty, fail/retry | `$genex-threejs-game-feel` |
|
|
152
|
+
| realtime multiplayer: movement sync, shared ball/NPC, host-run scores/enemies, shots/emotes, persistence | `$genex-threejs-multiplayer` |
|
|
153
|
+
| players talking to each other by TEXT — chat, room chat, "let them type to each other" (`room.chat` + `npx genex controller chat`) | `$genex-threejs-multiplayer` |
|
|
154
|
+
| players TALKING — voice chat, mic, "hear each other", proximity/positional voice (`room.voice` + `npx genex controller voice`; party-sized, max 6 per room — say that number out loud before building the feature) | `$genex-threejs-multiplayer` |
|
|
155
|
+
| player identity, sign-in, guests, saves/progress, per-player state, shared persistent world, leaderboards—mandatory for every game | `$genex-threejs-embed-auth` |
|
|
156
|
+
| the player EXPLICITLY asks to make an asset or publish something from their game as an asset for the free library | `$genex-asset-author` (`genex asset new`) — never offer this unprompted; no game duty routes here |
|
|
157
|
+
|
|
158
|
+
Request-shape rules, working modes and the protected-lane handoffs stay in the
|
|
159
|
+
[routing map](references/routing-map.md); read it when the request shape, not
|
|
160
|
+
the subsystem, is what is unclear.
|
|
132
161
|
|
|
133
162
|
For “make this image 3D,” ask **one** route question only when both results
|
|
134
163
|
honestly fit: a generated GLB built from the picture itself
|
|
@@ -145,12 +174,23 @@ supplied and creating one is part of the requested work, use the existing
|
|
|
145
174
|
Assets row.
|
|
146
175
|
|
|
147
176
|
The request sets the floor for the asset plan, never the ceiling: a world
|
|
148
|
-
should feel dressed and alive, so plan the detail its genre implies — and
|
|
149
|
-
the
|
|
150
|
-
player
|
|
151
|
-
|
|
152
|
-
or
|
|
153
|
-
|
|
177
|
+
should feel dressed and alive, so plan the detail its genre implies — and
|
|
178
|
+
choose the engine per object, never per category. The bar is the same for both
|
|
179
|
+
engines: whatever the player looks at must read as the thing it is, in this
|
|
180
|
+
game's own style — a building as that building, a person as a person, a prop as
|
|
181
|
+
that prop. A coloured box, a capsule or a flat grey block is not a finished
|
|
182
|
+
version of any of them. Generation is the default route to that bar wherever
|
|
183
|
+
code will not honestly reach it: the player's character, anything the request
|
|
184
|
+
names, anything the player walks up to, enters, or interacts with, and the
|
|
185
|
+
music and sound under it. Procedural code (`$genex-threejs-procedural-assets`)
|
|
186
|
+
is a first-class engine for what is structural, repeated, distant, or
|
|
187
|
+
parametric — terrain, sky (never generated), fences, paving, modular kits,
|
|
188
|
+
filler — and for anything else only when the result meets the same bar and you
|
|
189
|
+
have checked it in a capture before its row says `landed`. Write the route on
|
|
190
|
+
each Assets row: the paid flow for generated pieces, the procedural flow with
|
|
191
|
+
its local path for code-built ones — the table is where the per-object decision
|
|
192
|
+
lives, and a table with no generated row records
|
|
193
|
+
`Generation: none — <why code alone reaches the bar here>` beside it. Mixing
|
|
154
194
|
both in one scene is the normal way to build, never a fallback. In focused
|
|
155
195
|
work, stay inside the touched scope. Run independent planned generations with
|
|
156
196
|
`--no-wait`, scaffold while they land, and preserve their IDs, URLs, and
|
|
@@ -37,11 +37,15 @@ so it survives a long pause, context compaction, handoff, and remix.
|
|
|
37
37
|
status flow `proposed → planned → generating (id) → landed (URL) → wired`.
|
|
38
38
|
A landed asset is unfinished until wired or visibly cancelled. Preserve
|
|
39
39
|
generation IDs, permanent URLs, local outputs, and wiring state across
|
|
40
|
-
compaction.
|
|
40
|
+
compaction. Every row shows its route: generated rows carry the paid flow;
|
|
41
|
+
code-built rows carry the procedural flow and a local path. A table with no
|
|
42
|
+
generated row at all carries `Generation: none — <why code alone reaches the
|
|
43
|
+
bar here>` beside it (build contract, law 22).
|
|
41
44
|
- **Code-built procedural assets use their own truthful status flow:**
|
|
42
45
|
`proposed → planned → building (blockout | detail | material | runtime) →
|
|
43
46
|
landed (<local TypeScript path>) → wired`. They never invent a provider
|
|
44
|
-
generation ID.
|
|
47
|
+
generation ID. `landed` on a code-built row means a capture at gameplay
|
|
48
|
+
distance showed it reads as the thing it is. A generated reference image is a separate normal Assets row.
|
|
45
49
|
- Sections are request-driven. Gameplay, content, UI, assets, world,
|
|
46
50
|
multiplayer, character/animation, tools, modules, and procedural assets
|
|
47
51
|
appear only when the request or existing project uses them.
|
|
@@ -73,6 +77,7 @@ or explicitly re-scope it.>
|
|
|
73
77
|
Working mode: <whole coordinated build | step by step | focused change>
|
|
74
78
|
Now: ▶ <number/name of the current milestone or focused task>
|
|
75
79
|
Done when: <one plain, observable condition for the current work>
|
|
80
|
+
Lane: <$genex-… owning the current subsystem | none — why no row matches>
|
|
76
81
|
1. <milestone> — ✅ → previewed
|
|
77
82
|
2. <milestone> — ▶ in progress (<main agent | sub-agent>)
|
|
78
83
|
3. <milestone> — ⏸ paused (<reason>)
|
|
@@ -121,7 +126,10 @@ Menu video: <generation id/URL, or the still standing in while it renders>
|
|
|
121
126
|
| <asset> | <model/image/texture/…> | planned | — |
|
|
122
127
|
| <asset> | <kind> | generating (<id>) | — |
|
|
123
128
|
| <asset> | <kind> | landed → <URL> | yes/no |
|
|
129
|
+
| <asset> | procedural-code | building (blockout) | — |
|
|
124
130
|
Status flow: proposed → planned → generating (id) → landed (URL) → wired.
|
|
131
|
+
Code-built rows: proposed → planned → building (…) → landed (<local path>) → wired.
|
|
132
|
+
Generation: none — <why code alone reaches the bar in this game> (only when no row is generated)
|
|
125
133
|
|
|
126
134
|
**HUD pipeline state (only while the sprites lane is running) — keep this
|
|
127
135
|
current; it is how that pipeline survives a context compaction.**
|
|
@@ -152,9 +160,9 @@ Play-button rule: nothing connects before the click when a Play screen exists.
|
|
|
152
160
|
Verification still open: <two-identity/netcode evidence>
|
|
153
161
|
|
|
154
162
|
## Modules (whole coordinated mode or another useful split only)
|
|
155
|
-
| Module | Owns files | Built by | Done when |
|
|
156
|
-
|
|
157
|
-
| <request-derived stream> | <disjoint paths> | <parallel/serial + reason> | <observable result> |
|
|
163
|
+
| Module | Lane | Owns files | Built by | Done when |
|
|
164
|
+
|---|---|---|---|---|
|
|
165
|
+
| <request-derived stream> | <$genex-… or none — why> | <disjoint paths> | <parallel/serial + reason> | <observable result> |
|
|
158
166
|
|
|
159
167
|
## Verification evidence
|
|
160
168
|
- <preview URL/date and what it proves>
|
|
@@ -34,49 +34,17 @@ copy demo architecture.
|
|
|
34
34
|
|
|
35
35
|
## Route by system
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
| **attacking, casting, aiming or reloading WHILE moving** — any action the legs must keep running under; a weapon carry stance over stock locomotion; a wind-up the character holds while walking | `$genex-threejs-character-controller` (`references/animations.md`, upper-body layering) |
|
|
42
|
-
| dash, dodge, roll, blink, backstep, a lunging attack — any burst that moves the character itself | `$genex-threejs-character-controller` (`references/tuning-and-presets.md`, dash recipe) |
|
|
43
|
-
| the game's own generated character—the player's body wherever a human body appears—or Meshy animation coverage beyond the stock pack: reference-informed A-pose concepts, exact action IDs, same-rig adapter | `$genex-ai-character` + `$genex-threejs-character-controller` |
|
|
44
|
-
| a character/enemy needs motion the catalog lacks—a signature move, boss telegraph, death, full 8-way set, or the player's footage; free plan before spend | `$genex-ai-character` motion section + `references/motion-generation.md` |
|
|
45
|
-
| remote player bodies in multiplayer—never hand-built primitives: the game's generated character when it has one, otherwise the player's `p.avatarUrl` VRM | `$genex-threejs-multiplayer` + `$genex-threejs-character-controller` |
|
|
46
|
-
| enemies, NPCs, or creatures: rigged bipeds via `npx genex creature`; non-biped body plans (quadruped, flier, serpent, aquatic, multi-leg) via `npx genex model rig` + `model animate`; static plus procedural motion only for shapes with no body plan; collider, facing, hit reaction, death | `$genex-threejs-creatures` |
|
|
47
|
-
| cars, drones, vehicle physics, gearbox, character↔vehicle enter/exit | `$genex-threejs-vehicle-controllers` |
|
|
48
|
-
| playable on phones: joystick, virtual buttons, drag zones, genre touch recipes, rotate-device overlay | `$genex-threejs-touch-controls` |
|
|
49
|
-
| phone-survivable rendering: device tiers, DPR/shadow/post budgets, runtime governor, generated-asset rungs, Quality picker, dispose-on-swap | `$genex-threejs-adaptive-quality` |
|
|
50
|
-
| anything falls, collides, gets pushed, or needs colliders/events | `$genex-threejs-physics-rapier` |
|
|
51
|
-
| launch/docking timelines, authored transform phases, springs, convergence, deterministic prop/debris motion | `$genex-threejs-procedural-animation` |
|
|
52
|
-
| rebuild a reference prop, hard-surface object, modular decoration, or simple environment piece as editable parameterized Three.js code | `$genex-threejs-procedural-assets` |
|
|
53
|
-
| stylized/abstract shader-authored materials, the procedural/PBR material boundary | `$genex-threejs-procedural-assets` |
|
|
54
|
-
| particles, trails, plasma, shockwaves, pooled bursts, and event effects | `$genex-threejs-procedural-vfx` |
|
|
55
|
-
| stable large-world shadows, cascades, clipmaps, cached updates | `$genex-threejs-shadow-systems` |
|
|
56
|
-
| eye adaptation, tone mapping, output color, LUT grading, and proven static grain | `$genex-threejs-exposure-color-grading` |
|
|
57
|
-
| fixed-view screenshots, input direction, facing, temporal and budget evidence | `$genex-threejs-visual-validation` |
|
|
58
|
-
| a generated GLB for a concrete prop, vehicle, building, or object — from text or a reference image (`--image`); split into named parts (`model segment`); rig + animate any mesh (`model rig` / `model animate`) | `$genex-ai-model` |
|
|
59
|
-
| generated surface or terrain texture with real-world UV scale | `$genex-ai-texture` |
|
|
60
|
-
| sky, skybox, horizon, time of day, weather mood, night or space backdrop | build it in code in the scene—there is no sky command and no owning skill, so pick the technique this game needs |
|
|
61
|
-
| poster, sign, sprite, decal, reference sheet, or other 2D art | `$genex-ai-image` |
|
|
62
|
-
| in-world motion art or another requested video | `$genex-ai-video` |
|
|
63
|
-
| sound effect, one looping music bed, or a short spoken line | `$genex-ai-sfx`, `$genex-ai-music`, or `$genex-ai-voice` |
|
|
64
|
-
| requested UI/HUD/menu/interface work, a visible UI problem, or an interface you decided this game wants built with generated art | `$genex-threejs-game-ui` |
|
|
65
|
-
| selling anything for platform coin: a shop, an item catalog, boosts, cosmetics, "make it earn"; also any request for a loot box, gacha, wager, casino mechanic or donation prompt, which that skill refuses and replaces | `$genex-monetization` |
|
|
66
|
-
| cinematic menu/title/pause/victory/defeat/lobby/credits video treatment | `$genex-ai-menu` |
|
|
67
|
-
| drawn HUD chrome the game's style wants—one element or a matched set of frames, masks, and icons | `$genex-ai-hud` |
|
|
68
|
-
| the game works but feels flat, floaty, or unresponsive: input response, camera, impacts, cooldowns, difficulty, fail/retry | `$genex-threejs-game-feel` |
|
|
69
|
-
| realtime multiplayer: movement sync, shared ball/NPC, host-run scores/enemies, shots/emotes, persistence | `$genex-threejs-multiplayer` |
|
|
70
|
-
| players talking to each other by TEXT — chat, room chat, "let them type to each other" (`room.chat` + `npx genex controller chat`) | `$genex-threejs-multiplayer` |
|
|
71
|
-
| players TALKING — voice chat, mic, "hear each other", proximity/positional voice (`room.voice` + `npx genex controller voice`; party-sized, max 6 per room — say that number out loud before building the feature) | `$genex-threejs-multiplayer` |
|
|
72
|
-
| player identity, sign-in, guests, saves/progress, per-player state, shared persistent world, leaderboards—mandatory for every game | `$genex-threejs-embed-auth` |
|
|
73
|
-
| the player EXPLICITLY asks to make an asset or publish something from their game as an asset for the free library | `$genex-asset-author` (`genex asset new`) — never offer this unprompted; no game duty routes here |
|
|
37
|
+
The ownership table lives in **SKILL.md §4** so it is in front of you the
|
|
38
|
+
moment the director loads, rather than behind a link you have to decide to
|
|
39
|
+
open. This file keeps the request-shape and working-mode rules; read it when
|
|
40
|
+
the shape of the request, not the owning subsystem, is what is unclear.
|
|
74
41
|
|
|
75
42
|
## Request-sized execution
|
|
76
43
|
|
|
77
44
|
### Focused work
|
|
78
45
|
|
|
79
|
-
Load
|
|
46
|
+
Load the owning row or rows — before the first file of each subsystem, and
|
|
47
|
+
none beyond the work — preserve the current architecture, implement
|
|
80
48
|
the requested result, and run its relevant check. UI work routes directly to
|
|
81
49
|
UI; movement routes directly to movement; publishing routes directly to the
|
|
82
50
|
existing publish flow. Do not add a concept, unrelated asset batch, full-game
|
|
@@ -132,12 +100,12 @@ flow and state what remains.
|
|
|
132
100
|
|
|
133
101
|
## Procedural assets (code-built)
|
|
134
102
|
|
|
135
|
-
Use `$genex-threejs-procedural-assets` whenever code
|
|
136
|
-
|
|
137
|
-
props, parameterized objects — or when the player asks for procedural,
|
|
138
|
-
parametric, or code-built work. It is a first-class lane, not a fallback,
|
|
139
|
-
|
|
140
|
-
detailed world. An image alone still doesn't decide the route (ask the one
|
|
103
|
+
Use `$genex-threejs-procedural-assets` whenever code will honestly make the
|
|
104
|
+
object read as the thing it is — structural pieces, modular kits, repeated or
|
|
105
|
+
varied props, parameterized objects — or when the player asks for procedural,
|
|
106
|
+
parametric, or code-built work. It is a first-class lane, not a fallback, held
|
|
107
|
+
to the same bar as a generated GLB, and mixing it with generated GLBs in one
|
|
108
|
+
scene is the normal way to build a detailed world. An image alone still doesn't decide the route (ask the one
|
|
141
109
|
route question when both honestly fit), and never use it for player bodies,
|
|
142
110
|
rigged characters/creatures, or animation.
|
|
143
111
|
|
|
@@ -1,30 +1,34 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-threejs-procedural-assets
|
|
3
|
-
description: Build editable, parameterized Three.js objects in code —
|
|
3
|
+
description: Build editable, parameterized Three.js objects in code — modular kits, structural pieces, repeated or varied props, and any object code will honestly make read as the thing it is — or whenever the user asks for procedural/parametric work. A first-class lane alongside generated GLBs, held to the same bar; mix both freely.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex Three.js Procedural Assets
|
|
7
7
|
|
|
8
8
|
Build a recognizable asset as local, editable Three.js code. This lane is a
|
|
9
|
-
first-class engine, not a fallback
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
first-class engine, not a fallback — and it is held to the same bar as a
|
|
10
|
+
generated GLB: the result must read as the thing it is when the player looks at
|
|
11
|
+
it, in the game's own style. A coloured box or a flat grey block is a blockout,
|
|
12
|
+
never a landed asset. Mixing procedural pieces with generated pieces in the
|
|
13
|
+
same scene is the normal way to build a detailed, lively world.
|
|
13
14
|
|
|
14
15
|
## Choose the route
|
|
15
16
|
|
|
16
17
|
- Start directly when the user says procedural, parametric, code-built,
|
|
17
18
|
customizable, seeded, or asks for controlled variations.
|
|
18
|
-
- Also start directly
|
|
19
|
-
|
|
20
|
-
placed many times with variation.
|
|
19
|
+
- Also start directly for what is structural, repeated, distant, or
|
|
20
|
+
parametric: terrain, modular environment kits, fences, pipes, rails, walls,
|
|
21
|
+
paving, and anything placed many times with variation.
|
|
22
|
+
- For anything the player looks at up close, walks into, or interacts with — a
|
|
23
|
+
named building, a signature prop, a vehicle — code is a choice, not a
|
|
24
|
+
default: take it only when you will honestly reach the bar above, and confirm
|
|
25
|
+
it in a capture at gameplay distance before the row reads `landed`. Otherwise
|
|
26
|
+
`$genex-ai-model` owns it.
|
|
21
27
|
- An attached or available image does not activate this skill by itself.
|
|
22
28
|
- If “make this image 3D” could honestly mean either route, ask exactly one
|
|
23
29
|
question: **“Do you want a generated textured GLB, or editable procedural
|
|
24
30
|
Three.js code?”**
|
|
25
31
|
- Use `$genex-ai-model` when the user chooses a generated textured GLB.
|
|
26
|
-
- Use this skill for props, hard-surface objects, modular decorations, and
|
|
27
|
-
simple structures or environment pieces.
|
|
28
32
|
- Do not use it for characters, creatures, rigging, skeletal animation, or
|
|
29
33
|
character likeness. Keep those in their existing specialist lanes.
|
|
30
34
|
|
|
@@ -60,6 +64,9 @@ proposed → planned → building (blockout | detail | material | runtime)
|
|
|
60
64
|
→ landed (<local TypeScript path>) → wired
|
|
61
65
|
```
|
|
62
66
|
|
|
67
|
+
`landed` means a capture at gameplay distance showed it reads as the thing it
|
|
68
|
+
is — not that the file exists.
|
|
69
|
+
|
|
63
70
|
A Genex-generated reference keeps its separate image row and normal paid
|
|
64
71
|
generation status. Note that the procedural-code row is derived from that
|
|
65
72
|
image. A private user reference is not a paid generation row.
|