@genex-ai/cli-demo 0.20.0 → 0.22.0
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 +18 -18
- package/dist/index.js +138 -81
- package/package.json +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +42 -34
- package/templates/skills/genex-ai-sfx/SKILL.md +10 -6
- package/templates/skills/genex-ai-skybox/SKILL.md +11 -10
- package/templates/skills/genex-ai-texture/SKILL.md +10 -10
- package/templates/skills/genex-explore/SKILL.md +69 -0
- package/templates/skills/genex-getting-started/SKILL.md +3 -3
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +12 -0
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +18 -0
- package/templates/skills/genex-threejs-game-feel/SKILL.md +4 -0
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +9 -1
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ genex init <name> # authorize + create the draft project
|
|
|
8
8
|
genex link <slug> # re-link this folder to an existing game of yours (recovery)
|
|
9
9
|
genex preview # build + push to the hosted draft URL (unlisted)
|
|
10
10
|
genex publish # build + push, then list the game in the gallery
|
|
11
|
-
genex model "<prompt>" # generate a 3D model →
|
|
12
|
-
genex skybox "<prompt>" # generate a 360° sky →
|
|
13
|
-
genex sfx "<prompt>" # generate a sound fx →
|
|
14
|
-
genex texture "<prompt>" # generate a texture →
|
|
11
|
+
genex model "<prompt>" # generate a 3D model → prints an asset URL
|
|
12
|
+
genex skybox "<prompt>" # generate a 360° sky → prints an asset URL
|
|
13
|
+
genex sfx "<prompt>" # generate a sound fx → prints an asset URL
|
|
14
|
+
genex texture "<prompt>" # generate a texture → prints an asset URL
|
|
15
15
|
genex controller <type> # install a tuned character|car|drone controller → src/controllers/
|
|
16
16
|
```
|
|
17
17
|
|
|
@@ -70,22 +70,22 @@ Defaults: API `https://demo-api.glotech.world`, auth site
|
|
|
70
70
|
## Generating assets
|
|
71
71
|
|
|
72
72
|
`genex model | skybox | sfx | texture "<prompt>"` turn a prompt into a real,
|
|
73
|
-
game-ready asset
|
|
74
|
-
|
|
73
|
+
game-ready asset stored in R2. The command blocks until the asset is ready (live SSE
|
|
74
|
+
stream), then prints its public URL — the game loads it directly, nothing is
|
|
75
|
+
downloaded or committed:
|
|
75
76
|
|
|
76
|
-
| Command | Provider |
|
|
77
|
+
| Command | Provider | Prints (role) |
|
|
77
78
|
| --- | --- | --- |
|
|
78
|
-
| `genex model "<prompt>"` | Tripo |
|
|
79
|
-
| `genex skybox "<prompt>"` | Blockade Labs |
|
|
80
|
-
| `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs |
|
|
81
|
-
| `genex texture "<prompt>" [--terrain]` | Gemini |
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
`genex-ai-<kind>` skill with the exact Three.js loader code.
|
|
79
|
+
| `genex model "<prompt>"` | Tripo | `…/generations/<id>/model-glb` |
|
|
80
|
+
| `genex skybox "<prompt>"` | Blockade Labs | `…/generations/<id>/skybox-equirect` |
|
|
81
|
+
| `genex sfx "<prompt>" [--duration <s>]` | ElevenLabs | `…/generations/<id>/audio-sfx` |
|
|
82
|
+
| `genex texture "<prompt>" [--terrain]` | Gemini | `…/generations/<id>/texture-basecolor` |
|
|
83
|
+
|
|
84
|
+
Each URL is a permanent `https://assets.genex.technology/...` address served straight
|
|
85
|
+
from R2 (with CORS, so three.js loads it cross-origin without tainting). It resolves
|
|
86
|
+
the same in local dev, the published game, and remixes, and the bytes never pass back
|
|
87
|
+
through this API. Each kind has a scaffolded `genex-ai-<kind>` skill with the exact
|
|
88
|
+
Three.js loader code.
|
|
89
89
|
|
|
90
90
|
Auth reuses the existing `GENEX_TOKEN` (run `genex init` first). Server-side, each
|
|
91
91
|
provider is keyed by an env var (`TRIPO_API_KEY`, `BLOCKADE_LABS_API_KEY`,
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { readFileSync } from "fs";
|
|
5
|
-
import
|
|
5
|
+
import path12 from "path";
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/commands/init.ts
|
|
@@ -1399,6 +1399,9 @@ async function runPublish(opts) {
|
|
|
1399
1399
|
if (opts.description) body.description = opts.description;
|
|
1400
1400
|
if (opts.regenerateCover) body.regenerateCover = true;
|
|
1401
1401
|
if (opts.categories?.length) body.categories = opts.categories;
|
|
1402
|
+
if (opts.sourceRepoUrl) body.sourceRepoUrl = opts.sourceRepoUrl;
|
|
1403
|
+
if (opts.sourceAuthor) body.sourceAuthor = opts.sourceAuthor;
|
|
1404
|
+
if (opts.license) body.license = opts.license;
|
|
1402
1405
|
if (detections.embedSdkVersion) body.embedSdkVersion = detections.embedSdkVersion;
|
|
1403
1406
|
body.multiplayer = detections.multiplayer;
|
|
1404
1407
|
body.matchmaking = detections.matchmaking ?? null;
|
|
@@ -1465,25 +1468,6 @@ async function runPreview(opts) {
|
|
|
1465
1468
|
log.success("Preview deployed \u2014 unlisted draft (run `genex publish` to list it).");
|
|
1466
1469
|
}
|
|
1467
1470
|
|
|
1468
|
-
// src/commands/generate.ts
|
|
1469
|
-
import path12 from "path";
|
|
1470
|
-
|
|
1471
|
-
// src/lib/assets.ts
|
|
1472
|
-
import fs10 from "fs/promises";
|
|
1473
|
-
import path11 from "path";
|
|
1474
|
-
function slugify(input) {
|
|
1475
|
-
const s = input.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50).replace(/-+$/g, "");
|
|
1476
|
-
return s || "asset";
|
|
1477
|
-
}
|
|
1478
|
-
async function downloadToFile(url, dest, headers) {
|
|
1479
|
-
const res = await fetch(url, { headers });
|
|
1480
|
-
if (!res.ok) throw new Error(`download failed (HTTP ${res.status}) for ${url}`);
|
|
1481
|
-
const buf = Buffer.from(await res.arrayBuffer());
|
|
1482
|
-
await fs10.mkdir(path11.dirname(dest), { recursive: true });
|
|
1483
|
-
await fs10.writeFile(dest, buf);
|
|
1484
|
-
return buf.byteLength;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
1471
|
// src/lib/sse.ts
|
|
1488
1472
|
async function* readSSE(body) {
|
|
1489
1473
|
const decoder = new TextDecoder();
|
|
@@ -1524,24 +1508,7 @@ async function* readSSE(body) {
|
|
|
1524
1508
|
}
|
|
1525
1509
|
|
|
1526
1510
|
// src/commands/generate.ts
|
|
1527
|
-
var PUBLIC_PREFIX = "public/";
|
|
1528
|
-
var KIND_DIR = {
|
|
1529
|
-
model: "public/assets/models",
|
|
1530
|
-
skybox: "public/assets/skybox",
|
|
1531
|
-
sfx: "public/assets/sfx",
|
|
1532
|
-
texture: "public/assets/textures"
|
|
1533
|
-
};
|
|
1534
|
-
function runtimePath(rel) {
|
|
1535
|
-
return `./${rel.replace(PUBLIC_PREFIX, "")}`;
|
|
1536
|
-
}
|
|
1537
1511
|
var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1538
|
-
function fileDest(kind, slug, f) {
|
|
1539
|
-
if (kind === "texture") {
|
|
1540
|
-
const name = f.role.replace(/^texture-/, "");
|
|
1541
|
-
return `public/assets/textures/${slug}/${name}.${f.ext}`;
|
|
1542
|
-
}
|
|
1543
|
-
return `${KIND_DIR[kind]}/${slug}.${f.ext}`;
|
|
1544
|
-
}
|
|
1545
1512
|
async function runGenerate(kind, opts) {
|
|
1546
1513
|
const log = createLogger({ quiet: opts.quiet });
|
|
1547
1514
|
const prompt = opts.prompt?.trim();
|
|
@@ -1588,7 +1555,7 @@ async function runGenerate(kind, opts) {
|
|
|
1588
1555
|
return;
|
|
1589
1556
|
}
|
|
1590
1557
|
if (opts.noWait) {
|
|
1591
|
-
log.success(`Queued (${id}). Re-run without --no-wait to
|
|
1558
|
+
log.success(`Queued (${id}). Re-run without --no-wait to get the finished asset URL.`);
|
|
1592
1559
|
return;
|
|
1593
1560
|
}
|
|
1594
1561
|
log.step("Generating\u2026 (this can take up to a minute)");
|
|
@@ -1606,25 +1573,20 @@ async function runGenerate(kind, opts) {
|
|
|
1606
1573
|
process.exitCode = 1;
|
|
1607
1574
|
return;
|
|
1608
1575
|
}
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
for (const f of view.files ?? []) {
|
|
1613
|
-
const rel = fileDest(kind, slug, f);
|
|
1614
|
-
const url = `${apiUrl}/api/generations/${id}/asset/${f.role}`;
|
|
1615
|
-
const bytes = await downloadToFile(url, path12.join(process.cwd(), rel), {
|
|
1616
|
-
Authorization: `Bearer ${token}`
|
|
1617
|
-
});
|
|
1618
|
-
written.push(rel);
|
|
1619
|
-
log.success(`${rel} ${c.dim(`(${(bytes / 1024).toFixed(0)} KB)`)}`);
|
|
1620
|
-
}
|
|
1621
|
-
} catch (err) {
|
|
1622
|
-
log.error(`Download failed: ${String(err)}`);
|
|
1576
|
+
const files = view.files ?? [];
|
|
1577
|
+
if (files.length === 0) {
|
|
1578
|
+
log.error("Generation completed but produced no files.");
|
|
1623
1579
|
process.exitCode = 1;
|
|
1624
1580
|
return;
|
|
1625
1581
|
}
|
|
1626
1582
|
log.plain("");
|
|
1627
|
-
|
|
1583
|
+
log.success("Done \u2014 live on R2 (nothing downloaded or committed):");
|
|
1584
|
+
for (const f of files) {
|
|
1585
|
+
const label = files.length > 1 ? `${c.dim(`${f.role.replace(/^texture-/, "")}: `)}` : "";
|
|
1586
|
+
log.plain(` ${label}${f.url}`);
|
|
1587
|
+
}
|
|
1588
|
+
log.plain("");
|
|
1589
|
+
printHint(kind, files, log);
|
|
1628
1590
|
}
|
|
1629
1591
|
var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
1630
1592
|
var TERMINAL = /* @__PURE__ */ new Set(["completed", "failed"]);
|
|
@@ -1703,23 +1665,21 @@ async function poll(apiUrl, token, id, onProgress) {
|
|
|
1703
1665
|
}
|
|
1704
1666
|
return null;
|
|
1705
1667
|
}
|
|
1706
|
-
function printHint(kind,
|
|
1707
|
-
const
|
|
1668
|
+
function printHint(kind, files, log) {
|
|
1669
|
+
const url = files[0]?.url ?? "";
|
|
1708
1670
|
const hint = {
|
|
1709
|
-
model: `Load with GLTFLoader and add to the scene \u2014 see the genex-ai-model skill.
|
|
1710
|
-
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox.
|
|
1711
|
-
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx.
|
|
1712
|
-
texture: `Load with TextureLoader
|
|
1671
|
+
model: `Load with GLTFLoader from the URL and add it to the scene \u2014 see the genex-ai-model skill. url = "${url}"`,
|
|
1672
|
+
skybox: `Load as an equirectangular texture \u2192 scene.background + scene.environment \u2014 see genex-ai-skybox. url = "${url}"`,
|
|
1673
|
+
sfx: `Load with AudioLoader into a THREE.PositionalAudio (camera needs an AudioListener) \u2014 see genex-ai-sfx. url = "${url}"`,
|
|
1674
|
+
texture: `Load each map with TextureLoader (RepeatWrapping) into a MeshStandardMaterial \u2014 see genex-ai-texture. Use the URLs above by role.`
|
|
1713
1675
|
};
|
|
1714
|
-
log.success(
|
|
1715
|
-
"Done. Asset saved into public/assets (so Vite ships it; load it as ./assets/\u2026 \u2014 `genex publish` commits and ships it with the game)."
|
|
1716
|
-
);
|
|
1717
1676
|
log.dim(` ${hint[kind]}`);
|
|
1677
|
+
log.dim(" Reference the URL directly in your code \u2014 don't download it into the repo.");
|
|
1718
1678
|
}
|
|
1719
1679
|
|
|
1720
1680
|
// src/commands/controller.ts
|
|
1721
|
-
import
|
|
1722
|
-
import
|
|
1681
|
+
import fs10 from "fs/promises";
|
|
1682
|
+
import path11 from "path";
|
|
1723
1683
|
var CONTROLLER_KINDS = ["character", "car", "drone"];
|
|
1724
1684
|
var SHARED = [
|
|
1725
1685
|
"shared/math.ts",
|
|
@@ -1798,8 +1758,8 @@ var CONTROLLER_FILE_SETS = {
|
|
|
1798
1758
|
]
|
|
1799
1759
|
}
|
|
1800
1760
|
};
|
|
1801
|
-
var CODE_DEST =
|
|
1802
|
-
var ASSETS_DEST =
|
|
1761
|
+
var CODE_DEST = path11.join("src", "controllers");
|
|
1762
|
+
var ASSETS_DEST = path11.join("public", "assets");
|
|
1803
1763
|
async function runController(opts) {
|
|
1804
1764
|
const log = createLogger({ quiet: opts.quiet });
|
|
1805
1765
|
const kind = opts.kind?.trim();
|
|
@@ -1812,31 +1772,31 @@ async function runController(opts) {
|
|
|
1812
1772
|
process.exitCode = 1;
|
|
1813
1773
|
return;
|
|
1814
1774
|
}
|
|
1815
|
-
const srcDir =
|
|
1775
|
+
const srcDir = path11.join(getTemplatesDir(), "controllers");
|
|
1816
1776
|
const root = opts.cwd ?? process.cwd();
|
|
1817
1777
|
const set = CONTROLLER_FILE_SETS[kind];
|
|
1818
1778
|
log.plain(c.bold(`genex controller ${kind}`));
|
|
1819
1779
|
log.plain("");
|
|
1820
|
-
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST +
|
|
1780
|
+
log.step(`Installing the ${kind} controller into ${c.cyan(CODE_DEST + path11.sep)}`);
|
|
1821
1781
|
const plan = [
|
|
1822
|
-
...set.code.map((rel) => ({ from: rel, rel:
|
|
1782
|
+
...set.code.map((rel) => ({ from: rel, rel: path11.join(CODE_DEST, rel) })),
|
|
1823
1783
|
...set.assets.map((rel) => ({
|
|
1824
1784
|
from: rel,
|
|
1825
|
-
rel:
|
|
1785
|
+
rel: path11.join(ASSETS_DEST, path11.basename(rel))
|
|
1826
1786
|
}))
|
|
1827
1787
|
];
|
|
1828
1788
|
let copied = 0;
|
|
1829
1789
|
let skipped = 0;
|
|
1830
1790
|
try {
|
|
1831
1791
|
for (const file of plan) {
|
|
1832
|
-
const dest =
|
|
1792
|
+
const dest = path11.join(root, file.rel);
|
|
1833
1793
|
if (!opts.force && await exists2(dest)) {
|
|
1834
1794
|
skipped++;
|
|
1835
1795
|
log.dim(` skipped ${file.rel} (exists \u2014 use --force to overwrite)`);
|
|
1836
1796
|
continue;
|
|
1837
1797
|
}
|
|
1838
|
-
await
|
|
1839
|
-
await
|
|
1798
|
+
await fs10.mkdir(path11.dirname(dest), { recursive: true });
|
|
1799
|
+
await fs10.copyFile(path11.join(srcDir, file.from), dest);
|
|
1840
1800
|
copied++;
|
|
1841
1801
|
log.dim(` ${file.rel}`);
|
|
1842
1802
|
}
|
|
@@ -1868,8 +1828,8 @@ async function runController(opts) {
|
|
|
1868
1828
|
}
|
|
1869
1829
|
async function installOwnerAvatar(args) {
|
|
1870
1830
|
const { root, srcDir, apiUrl, token, log } = args;
|
|
1871
|
-
const dest =
|
|
1872
|
-
await
|
|
1831
|
+
const dest = path11.join(root, ASSETS_DEST, "avatar.vrm");
|
|
1832
|
+
await fs10.mkdir(path11.dirname(dest), { recursive: true });
|
|
1873
1833
|
if (token) {
|
|
1874
1834
|
try {
|
|
1875
1835
|
const res = await fetch(`${apiUrl}/api/avatars/me`, {
|
|
@@ -1881,7 +1841,7 @@ async function installOwnerAvatar(args) {
|
|
|
1881
1841
|
const vrmRes = await fetch(me.vrmUrl);
|
|
1882
1842
|
if (vrmRes.ok) {
|
|
1883
1843
|
const buf = Buffer.from(await vrmRes.arrayBuffer());
|
|
1884
|
-
await
|
|
1844
|
+
await fs10.writeFile(dest, buf);
|
|
1885
1845
|
log.dim(` public/assets/avatar.vrm (your avatar \u2014 ${(buf.length / 1e6).toFixed(1)} MB)`);
|
|
1886
1846
|
return;
|
|
1887
1847
|
}
|
|
@@ -1892,27 +1852,93 @@ async function installOwnerAvatar(args) {
|
|
|
1892
1852
|
log.dim(" avatar fetch failed (offline?); using the bundled default.");
|
|
1893
1853
|
}
|
|
1894
1854
|
}
|
|
1895
|
-
await
|
|
1855
|
+
await fs10.copyFile(path11.join(srcDir, "assets", "default-avatar.vrm"), dest);
|
|
1896
1856
|
log.dim(
|
|
1897
1857
|
token ? " public/assets/avatar.vrm (bundled default)" : " public/assets/avatar.vrm (bundled default \u2014 sign in and re-run for your own)"
|
|
1898
1858
|
);
|
|
1899
1859
|
}
|
|
1900
1860
|
async function exists2(p) {
|
|
1901
1861
|
try {
|
|
1902
|
-
await
|
|
1862
|
+
await fs10.access(p);
|
|
1903
1863
|
return true;
|
|
1904
1864
|
} catch {
|
|
1905
1865
|
return false;
|
|
1906
1866
|
}
|
|
1907
1867
|
}
|
|
1908
1868
|
|
|
1869
|
+
// src/commands/explore.ts
|
|
1870
|
+
async function runExplore(opts) {
|
|
1871
|
+
const log = createLogger({ quiet: opts.quiet });
|
|
1872
|
+
const query = opts.query?.trim();
|
|
1873
|
+
if (!query) {
|
|
1874
|
+
log.error('Missing query. Usage: genex explore "<what you need>"');
|
|
1875
|
+
process.exitCode = 1;
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
const apiUrl = getApiUrl(opts.apiUrl);
|
|
1879
|
+
let res;
|
|
1880
|
+
try {
|
|
1881
|
+
res = await fetch(`${apiUrl}/api/gallery/search-corpus?curated=1`);
|
|
1882
|
+
} catch {
|
|
1883
|
+
log.error("Couldn't reach Genex \u2014 please try again.");
|
|
1884
|
+
process.exitCode = 1;
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (!res.ok) {
|
|
1888
|
+
log.error(`explore: API returned ${res.status} \u2014 please try again.`);
|
|
1889
|
+
process.exitCode = 1;
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
const { items } = await res.json();
|
|
1893
|
+
const ranked = rank(items, query).slice(0, 5);
|
|
1894
|
+
if (opts.json) {
|
|
1895
|
+
console.log(JSON.stringify(ranked, null, 2));
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
if (ranked.length === 0) {
|
|
1899
|
+
log.plain(
|
|
1900
|
+
`No curated projects matched "${query}". Try a broader term (terrain, grass, vehicle, water, building, dungeon, fish, shader).`
|
|
1901
|
+
);
|
|
1902
|
+
return;
|
|
1903
|
+
}
|
|
1904
|
+
for (const [i, p] of ranked.entries()) {
|
|
1905
|
+
const meta = [
|
|
1906
|
+
p.categories.join(", "),
|
|
1907
|
+
p.license ?? "",
|
|
1908
|
+
p.sourceAuthor ? `originally by ${p.sourceAuthor}` : ""
|
|
1909
|
+
].filter(Boolean).join(" \xB7 ");
|
|
1910
|
+
log.plain(`${c.bold(`${i + 1}. ${p.title}`)} \u2014 ${meta}`);
|
|
1911
|
+
if (p.description) log.plain(` ${p.description.slice(0, 160)}`);
|
|
1912
|
+
if (p.playUrl) log.plain(` play: ${p.playUrl}`);
|
|
1913
|
+
if (p.cloneUrl) log.plain(` clone: ${p.cloneUrl} (editable source on main)`);
|
|
1914
|
+
if (p.sourceRepoUrl) log.plain(` upstream: ${p.sourceRepoUrl}`);
|
|
1915
|
+
}
|
|
1916
|
+
log.plain("");
|
|
1917
|
+
log.plain(
|
|
1918
|
+
`To use one: clone it as a starting template, or borrow parts into your current game \u2014 the genex-explore skill has the exact steps. Keep the credits.`
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
function rank(items, query) {
|
|
1922
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
1923
|
+
return items.map((p) => {
|
|
1924
|
+
const hay = `${p.title} ${p.description} ${p.categories.join(" ")}`.toLowerCase();
|
|
1925
|
+
let score = 0;
|
|
1926
|
+
for (const t of terms) {
|
|
1927
|
+
if (p.title.toLowerCase().includes(t)) score += 3;
|
|
1928
|
+
if (p.categories.some((cat) => cat.includes(t))) score += 2;
|
|
1929
|
+
if (hay.includes(t)) score += 1;
|
|
1930
|
+
}
|
|
1931
|
+
return { p, score };
|
|
1932
|
+
}).filter((x) => x.score > 0).sort((a, b) => b.score - a.score || b.p.playsCount - a.p.playsCount).map((x) => x.p);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1909
1935
|
// src/index.ts
|
|
1910
1936
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture"]);
|
|
1911
1937
|
function getVersion() {
|
|
1912
1938
|
try {
|
|
1913
|
-
const here =
|
|
1939
|
+
const here = path12.dirname(fileURLToPath2(import.meta.url));
|
|
1914
1940
|
const pkg = JSON.parse(
|
|
1915
|
-
readFileSync(
|
|
1941
|
+
readFileSync(path12.resolve(here, "..", "package.json"), "utf8")
|
|
1916
1942
|
);
|
|
1917
1943
|
return pkg.version ?? "0.0.0";
|
|
1918
1944
|
} catch {
|
|
@@ -1934,6 +1960,8 @@ ${c.bold("Usage")}
|
|
|
1934
1960
|
genex texture "<prompt>" [options] Generate a PBR texture into public/assets/textures.
|
|
1935
1961
|
genex controller <type> [--force] Install a physics controller (character|car|drone)
|
|
1936
1962
|
into src/controllers (+ assets into public/assets).
|
|
1963
|
+
genex explore "<query>" [options] Search the curated community gallery \u2014 proven
|
|
1964
|
+
Three.js systems you can clone or borrow parts from.
|
|
1937
1965
|
|
|
1938
1966
|
${c.bold("Options for the generators (`model` `skybox` `sfx` `texture`)")}
|
|
1939
1967
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
@@ -1972,9 +2000,17 @@ ${c.bold("Options for `preview` / `publish`")}
|
|
|
1972
2000
|
--categories <list> (publish) 1-3 gallery categories, comma-separated.
|
|
1973
2001
|
Valid: games, assets, physics, terrain, lighting, vfx.
|
|
1974
2002
|
--regenerate-cover (publish) Re-mint the disc cover (concept changed).
|
|
2003
|
+
--source-repo-url <url> (publish) Upstream repo this game is a port of \u2014
|
|
2004
|
+
shown as "Ported from \u2026" credit on the game page.
|
|
2005
|
+
--source-author <name> (publish) Upstream author to credit.
|
|
2006
|
+
--license <label> (publish) Upstream license label (e.g. MIT).
|
|
1975
2007
|
--api-url <url> (publish) Override the API base URL.
|
|
1976
2008
|
--env <path> Token env file (default: ~/.genex/env).
|
|
1977
2009
|
|
|
2010
|
+
${c.bold("Options for `explore`")}
|
|
2011
|
+
--json Print the top matches as JSON (for tools/agents).
|
|
2012
|
+
--api-url <url> Override the API base URL.
|
|
2013
|
+
|
|
1978
2014
|
${c.bold("Global")}
|
|
1979
2015
|
--quiet Reduce output.
|
|
1980
2016
|
-h, --help Show this help.
|
|
@@ -1999,6 +2035,7 @@ ${c.bold("Examples")}
|
|
|
1999
2035
|
genex sfx "punchy laser zap" --duration 2
|
|
2000
2036
|
genex texture "mossy cracked cobblestone" --terrain
|
|
2001
2037
|
genex controller character
|
|
2038
|
+
genex explore "grass"
|
|
2002
2039
|
`;
|
|
2003
2040
|
function parseArgs(argv) {
|
|
2004
2041
|
const parsed = {
|
|
@@ -2019,7 +2056,10 @@ function parseArgs(argv) {
|
|
|
2019
2056
|
"--description",
|
|
2020
2057
|
"--categories",
|
|
2021
2058
|
"--timeout",
|
|
2022
|
-
"--duration"
|
|
2059
|
+
"--duration",
|
|
2060
|
+
"--source-repo-url",
|
|
2061
|
+
"--source-author",
|
|
2062
|
+
"--license"
|
|
2023
2063
|
]);
|
|
2024
2064
|
let i = 0;
|
|
2025
2065
|
while (i < argv.length) {
|
|
@@ -2058,6 +2098,9 @@ function parseArgs(argv) {
|
|
|
2058
2098
|
case "--quiet":
|
|
2059
2099
|
parsed.options.quiet = true;
|
|
2060
2100
|
break;
|
|
2101
|
+
case "--json":
|
|
2102
|
+
parsed.options.json = true;
|
|
2103
|
+
break;
|
|
2061
2104
|
default: {
|
|
2062
2105
|
if (needsValue.has(arg)) {
|
|
2063
2106
|
const value = argv[i++];
|
|
@@ -2073,6 +2116,8 @@ function parseArgs(argv) {
|
|
|
2073
2116
|
parsed.command = arg;
|
|
2074
2117
|
} else if (parsed.options.name === void 0) {
|
|
2075
2118
|
parsed.options.name = arg;
|
|
2119
|
+
} else if (parsed.command === "explore") {
|
|
2120
|
+
parsed.options.name = `${parsed.options.name} ${arg}`;
|
|
2076
2121
|
} else {
|
|
2077
2122
|
parsed.error = `Unexpected argument: ${arg}`;
|
|
2078
2123
|
return parsed;
|
|
@@ -2115,6 +2160,15 @@ function applyValueFlag(options, flag, value) {
|
|
|
2115
2160
|
case "--categories":
|
|
2116
2161
|
options.categories = value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
2117
2162
|
break;
|
|
2163
|
+
case "--source-repo-url":
|
|
2164
|
+
options.sourceRepoUrl = value;
|
|
2165
|
+
break;
|
|
2166
|
+
case "--source-author":
|
|
2167
|
+
options.sourceAuthor = value;
|
|
2168
|
+
break;
|
|
2169
|
+
case "--license":
|
|
2170
|
+
options.license = value;
|
|
2171
|
+
break;
|
|
2118
2172
|
case "--timeout": {
|
|
2119
2173
|
const n = Number(value);
|
|
2120
2174
|
if (!Number.isFinite(n) || n <= 0) {
|
|
@@ -2180,6 +2234,9 @@ async function main() {
|
|
|
2180
2234
|
case "publish":
|
|
2181
2235
|
await runPublish(parsed.options);
|
|
2182
2236
|
break;
|
|
2237
|
+
case "explore":
|
|
2238
|
+
await runExplore({ ...parsed.options, query: parsed.options.name });
|
|
2239
|
+
break;
|
|
2183
2240
|
default:
|
|
2184
2241
|
log.error(`Unknown command: ${parsed.command}`);
|
|
2185
2242
|
log.plain(`Run ${c.cyan("genex --help")} for usage.`);
|
package/package.json
CHANGED
|
@@ -21,47 +21,58 @@ npx genex model "<prompt>"
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Write a specific prompt — "weathered wooden barrel with rusted iron bands" beats
|
|
24
|
-
"barrel". The command blocks until the mesh is ready (up to ~a minute), then
|
|
24
|
+
"barrel". The command blocks until the mesh is ready (up to ~a minute), then prints
|
|
25
|
+
its public URL:
|
|
25
26
|
|
|
26
27
|
```
|
|
27
|
-
|
|
28
|
+
https://assets.genex.technology/generations/<id>/model-glb
|
|
28
29
|
```
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
##
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
- **
|
|
44
|
-
|
|
45
|
-
|
|
31
|
+
The GLB lives in Genex storage (R2) and loads straight from that URL. You **don't
|
|
32
|
+
download it** and **nothing is committed to your repo** — copy the URL the command
|
|
33
|
+
prints into your loader. The URL is permanent, so it works the same in local dev, your
|
|
34
|
+
published game, and remixes.
|
|
35
|
+
|
|
36
|
+
## Models that point somewhere must face forward
|
|
37
|
+
|
|
38
|
+
Any model with a "front" the game aims — the hero the player controls, an **NPC that
|
|
39
|
+
walks toward or looks at players**, a turret, a vehicle — should face **forward**: its
|
|
40
|
+
front toward `+Z` (glTF's "looking forward"), so yaw/`lookAt` code points it correctly.
|
|
41
|
+
A hero whose nose points sideways — or an enemy that chases you while staring off to the
|
|
42
|
+
side — reads as broken at a glance.
|
|
43
|
+
|
|
44
|
+
- **In the prompt:** ask for it — e.g. `"...game-ready, facing forward, front toward +Z"`.
|
|
45
|
+
Treat this as a hint, not a guarantee: generated meshes regularly come back rotated
|
|
46
|
+
anyway, so the check below is never optional.
|
|
47
|
+
- **On load (always check — hero AND NPC):** if the GLB isn't `+Z`-forward, rotate it
|
|
48
|
+
once. Wrap the model in a parent `Object3D` and put the correction on the child, so
|
|
49
|
+
game code rotates the parent cleanly:
|
|
46
50
|
```ts
|
|
47
|
-
const
|
|
51
|
+
const rig = new THREE.Object3D();
|
|
48
52
|
gltf.scene.rotation.y = Math.PI / 2; // one-time facing correction — tune per model
|
|
49
|
-
|
|
50
|
-
scene.add(
|
|
53
|
+
rig.add(gltf.scene);
|
|
54
|
+
scene.add(rig); // move/rotate/lookAt `rig`, not gltf.scene
|
|
51
55
|
```
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
Clones inherit the fix only if you clone the corrected child (or the whole rig) —
|
|
57
|
+
never the raw `gltf.scene`.
|
|
58
|
+
- **Verify it, don't assume it.** Orientation **is** visible in a still — in your
|
|
59
|
+
self-check screenshot confirm the hero faces its travel direction AND that NPCs driven
|
|
60
|
+
by chase/aim code face their target (an enemy rotated 90° from its victim is this
|
|
61
|
+
pipeline's most common visible bug). If you can't capture real gameplay (a draft's
|
|
62
|
+
sign-in gate is up), say so plainly instead of skipping the check silently.
|
|
54
63
|
|
|
55
64
|
## Load it into the scene
|
|
56
65
|
|
|
57
|
-
Use Three.js `GLTFLoader`
|
|
58
|
-
|
|
66
|
+
Use Three.js `GLTFLoader` with the URL the command printed (R2 sends the right CORS
|
|
67
|
+
headers, so cross-origin loading just works):
|
|
59
68
|
|
|
60
69
|
```ts
|
|
61
70
|
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
62
71
|
|
|
63
72
|
const loader = new GLTFLoader();
|
|
64
|
-
|
|
73
|
+
// the URL `npx genex model` printed:
|
|
74
|
+
const MODEL_URL = "https://assets.genex.technology/generations/<id>/model-glb";
|
|
75
|
+
const gltf = await loader.loadAsync(MODEL_URL);
|
|
65
76
|
const model = gltf.scene;
|
|
66
77
|
model.scale.setScalar(1); // tune to taste
|
|
67
78
|
model.position.set(0, 0, 0);
|
|
@@ -78,17 +89,14 @@ scene is a ghost: players and objects pass straight through it.
|
|
|
78
89
|
|
|
79
90
|
## Publish checklist (so players see the model)
|
|
80
91
|
|
|
81
|
-
-
|
|
82
|
-
|
|
83
|
-
-
|
|
84
|
-
prompt's publish step).
|
|
85
|
-
- Keep generated assets under `public/assets/` — Vite only ships files under `public/`
|
|
86
|
-
(or files you `import`); `npx genex publish` ships that folder with the game.
|
|
92
|
+
- Load it from the **URL** the command printed — it's absolute and permanent, so it
|
|
93
|
+
resolves the same in local dev, the published game, and remixes. Nothing to commit.
|
|
94
|
+
- Don't copy the GLB into `public/assets/` — generated assets live in R2, not the repo.
|
|
87
95
|
|
|
88
96
|
## Options
|
|
89
97
|
|
|
90
|
-
- `--no-wait` — enqueue and return immediately (
|
|
91
|
-
|
|
98
|
+
- `--no-wait` — enqueue and return immediately (won't print the URL; re-run without
|
|
99
|
+
`--no-wait` to get it).
|
|
92
100
|
- `--api-url <url>` — override the API base (local dev).
|
|
93
101
|
|
|
94
102
|
## Troubleshooting
|
|
@@ -15,13 +15,15 @@ npx genex sfx "punchy laser zap, short and dry" --duration 2
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
`--duration <sec>` (0.5–22) sets a target length; omit it to let the model pick.
|
|
18
|
-
Blocks until ready, then
|
|
18
|
+
Blocks until ready, then prints its public URL:
|
|
19
19
|
|
|
20
20
|
```
|
|
21
|
-
|
|
21
|
+
https://assets.genex.technology/generations/<id>/audio-sfx
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
The mp3 lives in Genex storage (R2) and loads straight from that URL — you don't download
|
|
25
|
+
it and nothing is committed to your repo. The URL is permanent (local dev, published game,
|
|
26
|
+
and remixes alike).
|
|
25
27
|
|
|
26
28
|
## Play it in Three.js
|
|
27
29
|
|
|
@@ -36,7 +38,9 @@ import * as THREE from "three";
|
|
|
36
38
|
const listener = new THREE.AudioListener();
|
|
37
39
|
camera.add(listener);
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
// the URL `npx genex sfx` printed (R2 sends CORS headers, so cross-origin works):
|
|
42
|
+
const SFX_URL = "https://assets.genex.technology/generations/<id>/audio-sfx";
|
|
43
|
+
const buffer = await new THREE.AudioLoader().loadAsync(SFX_URL);
|
|
40
44
|
|
|
41
45
|
const sound = new THREE.PositionalAudio(listener);
|
|
42
46
|
sound.setBuffer(buffer);
|
|
@@ -55,8 +59,8 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
|
|
|
55
59
|
|
|
56
60
|
## Publish checklist
|
|
57
61
|
|
|
58
|
-
-
|
|
59
|
-
|
|
62
|
+
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
63
|
+
the same in local dev, the published game, and remixes. Nothing to commit.
|
|
60
64
|
|
|
61
65
|
## Options
|
|
62
66
|
|
|
@@ -23,13 +23,15 @@ also lights it (image-based lighting).
|
|
|
23
23
|
npx genex skybox "<prompt>"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
Blocks until ready, then
|
|
26
|
+
Blocks until ready, then prints its public URL:
|
|
27
27
|
|
|
28
28
|
```
|
|
29
|
-
|
|
29
|
+
https://assets.genex.technology/generations/<id>/skybox-equirect
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
The
|
|
32
|
+
The image lives in Genex storage (R2) and loads straight from that URL — you don't
|
|
33
|
+
download it and nothing is committed to your repo. The URL is permanent (works the same
|
|
34
|
+
in local dev, the published game, and remixes).
|
|
33
35
|
|
|
34
36
|
## Load it as background + environment
|
|
35
37
|
|
|
@@ -39,9 +41,9 @@ background and the lighting:
|
|
|
39
41
|
```ts
|
|
40
42
|
import * as THREE from "three";
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
44
|
+
// the URL `npx genex skybox` printed (R2 sends CORS headers, so cross-origin works):
|
|
45
|
+
const SKYBOX_URL = "https://assets.genex.technology/generations/<id>/skybox-equirect";
|
|
46
|
+
const texture = await new THREE.TextureLoader().loadAsync(SKYBOX_URL);
|
|
45
47
|
texture.mapping = THREE.EquirectangularReflectionMapping;
|
|
46
48
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
47
49
|
|
|
@@ -62,10 +64,9 @@ scene.background = texture; // keep the raw texture for the visible sky
|
|
|
62
64
|
|
|
63
65
|
## Publish checklist
|
|
64
66
|
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
ships that folder with the game.
|
|
67
|
+
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
68
|
+
the same in local dev, the published game, and remixes. Nothing to commit.
|
|
69
|
+
- Don't copy the image into `public/assets/` — generated assets live in R2, not the repo.
|
|
69
70
|
|
|
70
71
|
## Options
|
|
71
72
|
|
|
@@ -24,13 +24,15 @@ npx genex texture "<prompt>"
|
|
|
24
24
|
npx genex texture "lush green grass" --terrain # seamless tiling for ground/terrain
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Blocks until ready, then
|
|
27
|
+
Blocks until ready, then prints its public URL:
|
|
28
28
|
|
|
29
29
|
```
|
|
30
|
-
|
|
30
|
+
https://assets.genex.technology/generations/<id>/texture-basecolor
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
The image lives in Genex storage (R2) and loads straight from that URL — you don't
|
|
34
|
+
download it and nothing is committed to your repo. The URL is permanent (local dev,
|
|
35
|
+
published game, and remixes alike).
|
|
34
36
|
|
|
35
37
|
> **Scope:** v1 generates the **base-color (albedo)** map only. Normal / roughness
|
|
36
38
|
> / AO are a planned follow-up — for now set sensible `roughness`/`metalness`
|
|
@@ -41,11 +43,9 @@ Committed by `npx genex publish`, so it ships with your game.
|
|
|
41
43
|
```ts
|
|
42
44
|
import * as THREE from "three";
|
|
43
45
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
const map = await new THREE.TextureLoader().loadAsync(
|
|
47
|
-
"./assets/textures/lush-green-grass/basecolor.png",
|
|
48
|
-
);
|
|
46
|
+
// the URL `npx genex texture` printed (R2 sends CORS headers, so cross-origin works):
|
|
47
|
+
const TEXTURE_URL = "https://assets.genex.technology/generations/<id>/texture-basecolor";
|
|
48
|
+
const map = await new THREE.TextureLoader().loadAsync(TEXTURE_URL);
|
|
49
49
|
map.colorSpace = THREE.SRGBColorSpace;
|
|
50
50
|
map.wrapS = map.wrapT = THREE.RepeatWrapping;
|
|
51
51
|
map.repeat.set(8, 8); // tile count — raise for large surfaces
|
|
@@ -69,8 +69,8 @@ scene.add(ground);
|
|
|
69
69
|
|
|
70
70
|
## Publish checklist
|
|
71
71
|
|
|
72
|
-
-
|
|
73
|
-
|
|
72
|
+
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
73
|
+
the same in local dev, the published game, and remixes. Nothing to commit.
|
|
74
74
|
|
|
75
75
|
## Options
|
|
76
76
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-explore
|
|
3
|
+
description: Search the curated Genex community gallery with `npx genex explore` before hand-writing hard visual or physics systems. Use when the game needs terrain, grass, water, vehicles, buildings, dungeons, flocking/boids, or advanced shader work — proven open-source Three.js implementations you can clone as a starting point or borrow parts from, credits included.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Explore — proven building blocks from the community gallery
|
|
7
|
+
|
|
8
|
+
Some systems are notoriously hard to get right from scratch: believable grass,
|
|
9
|
+
vehicle physics, procedural buildings and dungeons, fish/boid flocking, water,
|
|
10
|
+
advanced shader effects. The curated community gallery holds **faithful ports of
|
|
11
|
+
proven open-source Three.js projects** — playable, cloneable, and licensed for
|
|
12
|
+
reuse. Search it before hand-writing one of those systems.
|
|
13
|
+
|
|
14
|
+
## When to use
|
|
15
|
+
|
|
16
|
+
Before building a hard visual/physics system by hand, run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx genex explore "<what you need>"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Examples: `npx genex explore "grass"`, `npx genex explore "vehicle physics"`,
|
|
23
|
+
`npx genex explore "procedural building"`. No sign-in needed — this works even
|
|
24
|
+
before `genex init`.
|
|
25
|
+
|
|
26
|
+
Each result prints everything you need to act on it:
|
|
27
|
+
|
|
28
|
+
- `play:` — open it in the browser to judge whether it fits.
|
|
29
|
+
- `clone:` — the public repo with **editable source on `main`**.
|
|
30
|
+
- `upstream:` — the original source repo, with its author and license.
|
|
31
|
+
|
|
32
|
+
Add `--json` for machine-readable output.
|
|
33
|
+
|
|
34
|
+
## How to integrate a result
|
|
35
|
+
|
|
36
|
+
**As a NEW game (start from the whole project):**
|
|
37
|
+
|
|
38
|
+
1. `git clone <clone URL from the output> <name>` — pick a short one-word name.
|
|
39
|
+
2. `cd <name>`, then `npx @genex-ai/cli-demo@latest init <name>` — choose
|
|
40
|
+
"Ignore files and continue" if it warns the folder isn't empty; never use
|
|
41
|
+
`--force`. This creates your own project; the original is untouched.
|
|
42
|
+
3. `npm install`, keep `base: './'` in `vite.config`, then build your changes
|
|
43
|
+
and ship with `npx genex preview`.
|
|
44
|
+
|
|
45
|
+
**To BORROW parts into the current project:**
|
|
46
|
+
|
|
47
|
+
1. Clone the result somewhere temporary, separate from your project
|
|
48
|
+
(e.g. `/tmp/genex-explore-src`).
|
|
49
|
+
2. Study how it implements the part you need, then bring **just that** over —
|
|
50
|
+
real code and assets, adapted to your file paths. Don't wholesale-overwrite
|
|
51
|
+
your game.
|
|
52
|
+
3. Reused assets must live under `public/assets/` to ship. New assets are
|
|
53
|
+
generated as usual with `$genex-ai-model`, `$genex-ai-skybox`,
|
|
54
|
+
`$genex-ai-sfx`, or `$genex-ai-texture`.
|
|
55
|
+
|
|
56
|
+
## Credits rule (non-negotiable)
|
|
57
|
+
|
|
58
|
+
Every curated result carries an upstream link, author, and license — that
|
|
59
|
+
credit is part of the deal that makes these projects reusable. Whatever you
|
|
60
|
+
build from one:
|
|
61
|
+
|
|
62
|
+
- keep the attribution block in the README (upstream source repo URL, author,
|
|
63
|
+
license) exactly as the port carries it;
|
|
64
|
+
- keep any in-game credits screens or notices in place;
|
|
65
|
+
- carry the credit into anything you publish or remix from it.
|
|
66
|
+
|
|
67
|
+
If nothing matches your query, try a broader term (terrain, grass, vehicle,
|
|
68
|
+
water, building, dungeon, fish, shader) — or build it with the regular
|
|
69
|
+
`genex-threejs-*` skills instead.
|
|
@@ -28,9 +28,9 @@ validation.
|
|
|
28
28
|
|
|
29
29
|
## Generating real assets
|
|
30
30
|
|
|
31
|
-
Beyond procedural code, Genex can generate **real, AI-made assets** from a prompt
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
Beyond procedural code, Genex can generate **real, AI-made assets** from a prompt.
|
|
32
|
+
Each command prints a permanent `assets.genex.technology` URL you load directly —
|
|
33
|
+
the asset lives in Genex storage (R2), not your repo, so there's nothing to commit:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
npx genex model "weathered wooden barrel" # a 3D mesh (GLB)
|
|
@@ -33,6 +33,18 @@ Read [references/camera-rigs.md](references/camera-rigs.md)
|
|
|
33
33
|
for exact chase/side/orbit rigs, projection values, transition
|
|
34
34
|
rules, floating-origin shot, pointer controls, and implementation limits.
|
|
35
35
|
|
|
36
|
+
## Aiming and pointer lock
|
|
37
|
+
|
|
38
|
+
Camera-aimed action — a third-person shooter reticle, first-person look —
|
|
39
|
+
wants **pointer lock**, not drag-orbit: request it on canvas click
|
|
40
|
+
(`canvas.requestPointerLock()`), drive yaw/pitch from `mousemove` deltas while
|
|
41
|
+
locked, and treat lock loss (Esc) as aim-paused — show a small "click to aim"
|
|
42
|
+
hint whenever unlocked. Keep drag-orbit for non-combat cameras (exploration,
|
|
43
|
+
building, spectating). Pointer lock works everywhere a Genex game runs:
|
|
44
|
+
standalone the game is the top-level page, and the platform's game frame
|
|
45
|
+
already grants the pointer-lock permission — no setup needed. Re-sync
|
|
46
|
+
yaw/pitch from the camera whenever lock is acquired (rule below).
|
|
47
|
+
|
|
36
48
|
## Non-negotiable rules
|
|
37
49
|
|
|
38
50
|
- Use subject dimensions to derive offsets; do not tune one fixed distance for
|
|
@@ -260,6 +260,24 @@ instead. Don't code around any of this: no `?`/`#` URL params of yours will
|
|
|
260
260
|
be affected, and `isEmbedded()` / the return-trip handling are internal SDK
|
|
261
261
|
concerns.
|
|
262
262
|
|
|
263
|
+
### Validating a draft (read before self-testing)
|
|
264
|
+
|
|
265
|
+
An unpublished draft shows the sign-in gate to any browser that isn't signed
|
|
266
|
+
in as the owner — **including your own test browser** (Playwright, headless
|
|
267
|
+
Chrome). The game still boots behind the overlay: console logs, DOM snapshots,
|
|
268
|
+
and key events all work — but every screenshot shows the gate, not the game,
|
|
269
|
+
and a gate capture is NOT visual evidence.
|
|
270
|
+
|
|
271
|
+
- Validate what the gate can't hide: a clean console, the canvas booting, the
|
|
272
|
+
HUD present in a DOM snapshot, controls registering.
|
|
273
|
+
- Do NOT work around the gate: don't dig through the SDK's internals for
|
|
274
|
+
undocumented URL fragments, and don't drive the user's own signed-in
|
|
275
|
+
browser.
|
|
276
|
+
- For the visual pass on a draft, hand it off plainly: "open your draft page
|
|
277
|
+
and tell me what you see" — the owner's view is the real check. Once the
|
|
278
|
+
game is **published**, any fresh browser gets in as a guest, so your own
|
|
279
|
+
test browser works again for full visual validation.
|
|
280
|
+
|
|
263
281
|
## Checklist
|
|
264
282
|
|
|
265
283
|
- [ ] `initGameSentry({ slug: GENEX.slug })` is the very first call in `main.ts`.
|
|
@@ -33,6 +33,10 @@ no matter how good it looks.
|
|
|
33
33
|
itself, or apply them only to hand-rolled movement.)
|
|
34
34
|
- If an action can't fire (cooldown, no ammo), say so instantly — a click, a
|
|
35
35
|
dimmed icon — silence reads as broken input.
|
|
36
|
+
- Camera-aimed shooting wants **pointer lock** (click to aim, Esc releases —
|
|
37
|
+
`$genex-threejs-camera-direction` has the rig rules); firing at a reticle
|
|
38
|
+
with an unlocked drag-to-turn camera feels imprecise no matter how tight
|
|
39
|
+
the numbers are.
|
|
36
40
|
|
|
37
41
|
## Movement: snappy beats realistic
|
|
38
42
|
|
|
@@ -32,7 +32,15 @@ this is the whole acceptance gate, and it is also the minimum for every game:
|
|
|
32
32
|
2. Press each documented control once (keys, pointer); assert a **visible
|
|
33
33
|
response** to every one — the player moves, the camera turns, the button
|
|
34
34
|
fires.
|
|
35
|
-
3. Capture one screenshot of live gameplay
|
|
35
|
+
3. Capture one screenshot of live gameplay — of the **game**, not a sign-in
|
|
36
|
+
gate or loading screen. A capture of the SDK's "Sign in to play" overlay is
|
|
37
|
+
NOT gameplay evidence; if a draft's gate blocks the view, say so plainly
|
|
38
|
+
(see the embed-auth skill's "Validating a draft" note) instead of passing
|
|
39
|
+
the capture off as validation.
|
|
40
|
+
4. In that screenshot, check oriented models: the hero faces its travel
|
|
41
|
+
direction, and NPCs driven by chase/aim code face their target. A model
|
|
42
|
+
rotated 90° reads as broken — `$genex-ai-model` has the one-time facing
|
|
43
|
+
fix.
|
|
36
44
|
|
|
37
45
|
Everything deeper (baselines, seed sweeps, mosaics, budgets) belongs to
|
|
38
46
|
visual-system work — the sequence above.
|