@genex-ai/cli-demo 0.47.0 → 0.48.0-dev.75
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 +111 -6
- package/package.json +1 -1
- package/templates/controllers/shared/physics-world.ts +2 -1
- package/templates/skills/genex-ai-hud/SKILL.md +1 -1
- package/templates/skills/genex-ai-image/SKILL.md +1 -1
- package/templates/skills/genex-ai-menu/SKILL.md +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +1 -1
- package/templates/skills/genex-ai-sfx/SKILL.md +1 -1
- package/templates/skills/genex-ai-skybox/SKILL.md +1 -1
- package/templates/skills/genex-ai-texture/SKILL.md +1 -1
- package/templates/skills/genex-ai-video/SKILL.md +1 -1
- package/templates/skills/genex-explore/SKILL.md +1 -1
- package/templates/skills/genex-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-threejs-character-controller/SKILL.md +6 -1
- package/templates/skills/genex-threejs-character-controller/references/wiring.md +5 -1
- package/templates/skills/genex-threejs-physics-rapier/SKILL.md +4 -2
- package/templates/skills/genex-threejs-vehicle-controllers/SKILL.md +4 -1
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +10 -5
- package/templates/skills/genex-updates/SKILL.md +1 -1
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs from "fs";
|
|
|
8
8
|
import os from "os";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { fileURLToPath } from "url";
|
|
11
|
-
var RAW_CHANNEL = "
|
|
11
|
+
var RAW_CHANNEL = "dev";
|
|
12
12
|
var CLI_CHANNEL = RAW_CHANNEL === "dev" ? "dev" : "latest";
|
|
13
13
|
var STANDS = {
|
|
14
14
|
prod: { api: "https://api.genex.games", dashboard: "https://genex.games" },
|
|
@@ -868,6 +868,49 @@ async function writeGitignore(dir, log) {
|
|
|
868
868
|
await fs4.writeFile(file, next);
|
|
869
869
|
log.dim(`Updated .gitignore (${toAdd.join(", ")}).`);
|
|
870
870
|
}
|
|
871
|
+
var LFS_PATTERNS = [
|
|
872
|
+
// models
|
|
873
|
+
"*.glb",
|
|
874
|
+
"*.gltf",
|
|
875
|
+
"*.bin",
|
|
876
|
+
"*.vrm",
|
|
877
|
+
"*.fbx",
|
|
878
|
+
"*.obj",
|
|
879
|
+
// textures / images
|
|
880
|
+
"*.png",
|
|
881
|
+
"*.jpg",
|
|
882
|
+
"*.jpeg",
|
|
883
|
+
"*.webp",
|
|
884
|
+
"*.ktx2",
|
|
885
|
+
"*.basis",
|
|
886
|
+
"*.hdr",
|
|
887
|
+
"*.exr",
|
|
888
|
+
"*.tga",
|
|
889
|
+
// audio / video
|
|
890
|
+
"*.mp3",
|
|
891
|
+
"*.ogg",
|
|
892
|
+
"*.wav",
|
|
893
|
+
"*.m4a",
|
|
894
|
+
"*.mp4",
|
|
895
|
+
"*.webm"
|
|
896
|
+
];
|
|
897
|
+
async function writeGitattributes(dir, log) {
|
|
898
|
+
const file = path4.join(dir, ".gitattributes");
|
|
899
|
+
let content = "";
|
|
900
|
+
try {
|
|
901
|
+
content = await fs4.readFile(file, "utf8");
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
const present = new Set(content.split("\n").map((l) => l.trim().split(/\s+/)[0]));
|
|
905
|
+
const toAdd = LFS_PATTERNS.filter((p) => !present.has(p));
|
|
906
|
+
if (toAdd.length === 0) return;
|
|
907
|
+
let next = content;
|
|
908
|
+
if (next.length > 0 && !next.endsWith("\n")) next += "\n";
|
|
909
|
+
if (!content.trim()) next += "# Binary game assets go to Git LFS (R2) \u2014 keeps the source push small\n";
|
|
910
|
+
next += toAdd.map((p) => `${p} filter=lfs diff=lfs merge=lfs -text`).join("\n") + "\n";
|
|
911
|
+
await fs4.writeFile(file, next);
|
|
912
|
+
log.dim(`Updated .gitattributes (${toAdd.length} binary globs \u2192 Git LFS).`);
|
|
913
|
+
}
|
|
871
914
|
|
|
872
915
|
// src/lib/store.ts
|
|
873
916
|
import fs6 from "fs/promises";
|
|
@@ -1512,6 +1555,28 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
|
|
|
1512
1555
|
["node_modules/", "dist/", ".git/", ".genex/", ".env", ".env.*", "!.env.example", ""].join("\n")
|
|
1513
1556
|
);
|
|
1514
1557
|
const env = { ...base, GIT_WORK_TREE: cwd, GIT_INDEX_FILE: path10.join(gitDir, "index-source") };
|
|
1558
|
+
let lfs = (await run("git", ["lfs", "version"], base)).code !== 0 ? false : true;
|
|
1559
|
+
if (!lfs) {
|
|
1560
|
+
log.step("Installing git-lfs (keeps large binary assets out of the source push)\u2026");
|
|
1561
|
+
const [bin, args] = process.platform === "darwin" ? ["brew", ["install", "git-lfs"]] : ["sudo", ["-n", "apt-get", "install", "-y", "git-lfs"]];
|
|
1562
|
+
await run(bin, args, base);
|
|
1563
|
+
lfs = (await run("git", ["lfs", "version"], base)).code === 0;
|
|
1564
|
+
if (!lfs) {
|
|
1565
|
+
log.warn(
|
|
1566
|
+
"Couldn't install git-lfs \u2014 large binary assets won't be saved to your source (the game still publishes)."
|
|
1567
|
+
);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (lfs) {
|
|
1571
|
+
await writeGitattributes(cwd, log);
|
|
1572
|
+
const filters = [
|
|
1573
|
+
["filter.lfs.clean", "git-lfs clean -- %f"],
|
|
1574
|
+
["filter.lfs.smudge", "git-lfs smudge -- %f"],
|
|
1575
|
+
["filter.lfs.process", "git-lfs filter-process"],
|
|
1576
|
+
["filter.lfs.required", "true"]
|
|
1577
|
+
];
|
|
1578
|
+
for (const [key, value] of filters) await run("git", ["config", key, value], base);
|
|
1579
|
+
}
|
|
1515
1580
|
await run("git", ["add", "-A"], env);
|
|
1516
1581
|
const tree = (await run("git", ["ls-files"], env)).out.trim() ? (await run("git", ["write-tree"], env)).out.trim() : "";
|
|
1517
1582
|
if (!tree || tree === EMPTY_TREE) {
|
|
@@ -1520,6 +1585,10 @@ async function pushWorktree(cwd, pushUrl, managed, log) {
|
|
|
1520
1585
|
}
|
|
1521
1586
|
const commit = (await run("git", ["commit-tree", tree, "-m", "source"], { ...base, ...ident })).out.trim();
|
|
1522
1587
|
await run("git", ["update-ref", "refs/heads/main", commit], base);
|
|
1588
|
+
if (lfs && (await run("git", ["lfs", "push", "--all", pushUrl, "main"], base)).code !== 0) {
|
|
1589
|
+
log.error("Couldn't upload your game's binary assets to the source store.");
|
|
1590
|
+
return false;
|
|
1591
|
+
}
|
|
1523
1592
|
const push = await run(
|
|
1524
1593
|
"git",
|
|
1525
1594
|
["-c", "http.postBuffer=536870912", "push", "-q", pushUrl, "+refs/heads/main:main"],
|
|
@@ -1731,12 +1800,31 @@ async function detectGameStateUsage(cwd = process.cwd()) {
|
|
|
1731
1800
|
}
|
|
1732
1801
|
return false;
|
|
1733
1802
|
}
|
|
1803
|
+
var UI_PHASE_MARKERS = /data-phase|setPhase/;
|
|
1804
|
+
async function detectUiPhases(cwd = process.cwd()) {
|
|
1805
|
+
try {
|
|
1806
|
+
const html = await fs10.readFile(path11.join(cwd, "index.html"), "utf8").catch(() => "");
|
|
1807
|
+
if (UI_PHASE_MARKERS.test(html)) return true;
|
|
1808
|
+
const srcDir = path11.join(cwd, "src");
|
|
1809
|
+
const entries = await fs10.readdir(srcDir, { recursive: true });
|
|
1810
|
+
for (const rel of entries) {
|
|
1811
|
+
if (rel.includes("node_modules")) continue;
|
|
1812
|
+
if (!/\.(ts|js|mts|mjs|tsx|jsx|html|css)$/.test(rel)) continue;
|
|
1813
|
+
const content = await fs10.readFile(path11.join(srcDir, rel), "utf8").catch(() => "");
|
|
1814
|
+
if (UI_PHASE_MARKERS.test(content)) return true;
|
|
1815
|
+
}
|
|
1816
|
+
return false;
|
|
1817
|
+
} catch {
|
|
1818
|
+
return true;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1734
1821
|
async function detectFeatures(log, cwd = process.cwd()) {
|
|
1735
1822
|
return {
|
|
1736
1823
|
embedSdkVersion: await detectEmbedSdkVersion(cwd),
|
|
1737
1824
|
multiplayer: await detectMultiplayer(cwd),
|
|
1738
1825
|
matchmaking: await detectMatchmaking(log, cwd),
|
|
1739
|
-
gameStateUsed: await detectGameStateUsage(cwd)
|
|
1826
|
+
gameStateUsed: await detectGameStateUsage(cwd),
|
|
1827
|
+
uiPhases: await detectUiPhases(cwd)
|
|
1740
1828
|
};
|
|
1741
1829
|
}
|
|
1742
1830
|
function advisoryNudges(log, d) {
|
|
@@ -1755,6 +1843,18 @@ function advisoryNudges(log, d) {
|
|
|
1755
1843
|
"package.json declares genex.matchmaking but @genex-ai/multiplayer is not installed \u2014 matchmaking cannot work. Install the SDK and use matchmake() (see genex-threejs-multiplayer)."
|
|
1756
1844
|
);
|
|
1757
1845
|
}
|
|
1846
|
+
if (!d.uiPhases) {
|
|
1847
|
+
log.warn(
|
|
1848
|
+
"No menu/loader screens detected (no data-phase/setPhase) \u2014 players get a bare HUD with no title screen and no styled loading. Run the genex-threejs-game-ui plan (loader, menu, screen tiers); a cinematic menu is two commands away (genex-ai-menu)."
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
async function firstPreviewNudge(log, cwd = process.cwd()) {
|
|
1853
|
+
const meta = await readProject(cwd);
|
|
1854
|
+
if (!meta || meta.lastDeployAt || meta.status === "published") return;
|
|
1855
|
+
log.dim(
|
|
1856
|
+
" \u279C Draft page still the placeholder? Ship the first rough build now: npx genex preview \u2014 then keep building."
|
|
1857
|
+
);
|
|
1758
1858
|
}
|
|
1759
1859
|
|
|
1760
1860
|
// src/commands/publish.ts
|
|
@@ -1792,6 +1892,8 @@ async function runPublish(opts) {
|
|
|
1792
1892
|
process.exitCode = 1;
|
|
1793
1893
|
return;
|
|
1794
1894
|
}
|
|
1895
|
+
meta.lastDeployAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1896
|
+
await writeProject(meta);
|
|
1795
1897
|
} else {
|
|
1796
1898
|
log.info("Skipping build + deploy (--no-push).");
|
|
1797
1899
|
}
|
|
@@ -1875,8 +1977,9 @@ async function runPreview(opts) {
|
|
|
1875
1977
|
const freshStatus = await fetchProjectStatus(apiUrl, token, meta.slug);
|
|
1876
1978
|
if (freshStatus && freshStatus !== meta.status) {
|
|
1877
1979
|
meta.status = freshStatus;
|
|
1878
|
-
await writeProject(meta);
|
|
1879
1980
|
}
|
|
1981
|
+
meta.lastDeployAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1982
|
+
await writeProject(meta);
|
|
1880
1983
|
const published = meta.status === "published";
|
|
1881
1984
|
log.plain("");
|
|
1882
1985
|
if (published) {
|
|
@@ -2041,9 +2144,9 @@ async function awaitAndReport(apiUrl, token, id, kind, log) {
|
|
|
2041
2144
|
process.exitCode = 1;
|
|
2042
2145
|
return;
|
|
2043
2146
|
}
|
|
2044
|
-
reportTerminal(kind, view, log);
|
|
2147
|
+
await reportTerminal(kind, view, log);
|
|
2045
2148
|
}
|
|
2046
|
-
function reportTerminal(kind, view, log) {
|
|
2149
|
+
async function reportTerminal(kind, view, log) {
|
|
2047
2150
|
if (view.status !== "completed") {
|
|
2048
2151
|
log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
|
|
2049
2152
|
process.exitCode = 1;
|
|
@@ -2063,6 +2166,7 @@ function reportTerminal(kind, view, log) {
|
|
|
2063
2166
|
}
|
|
2064
2167
|
log.plain("");
|
|
2065
2168
|
printHint(kind, files, log);
|
|
2169
|
+
await firstPreviewNudge(log);
|
|
2066
2170
|
}
|
|
2067
2171
|
var WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
2068
2172
|
var KIND_WAIT_TIMEOUT_MS = {
|
|
@@ -2211,7 +2315,7 @@ async function runWait(opts) {
|
|
|
2211
2315
|
log.dim(` ${kind} ${id}`);
|
|
2212
2316
|
log.plain("");
|
|
2213
2317
|
if (TERMINAL2.has(view.status)) {
|
|
2214
|
-
reportTerminal(kind, view, log);
|
|
2318
|
+
await reportTerminal(kind, view, log);
|
|
2215
2319
|
return;
|
|
2216
2320
|
}
|
|
2217
2321
|
await awaitAndReport(apiUrl, token, id, kind, log);
|
|
@@ -2690,6 +2794,7 @@ async function runController(opts) {
|
|
|
2690
2794
|
for (const line of set.sketch) {
|
|
2691
2795
|
log.dim(` ${line}`);
|
|
2692
2796
|
}
|
|
2797
|
+
await firstPreviewNudge(log);
|
|
2693
2798
|
}
|
|
2694
2799
|
async function installOwnerAvatar(args) {
|
|
2695
2800
|
const { root, srcDir, apiUrl, token, log } = args;
|
package/package.json
CHANGED
|
@@ -157,7 +157,8 @@ const _identityMatrix = new THREE.Matrix4();
|
|
|
157
157
|
* const physics = await PhysicsWorld.create();
|
|
158
158
|
* physics.onBeforeStep(() => controller.update(physics.timeStep));
|
|
159
159
|
* renderer.setAnimationLoop(() => {
|
|
160
|
-
* physics.step(
|
|
160
|
+
* physics.step(delta); // fixed substeps + mesh sync + events
|
|
161
|
+
* // delta = capped performance.now() difference — avoid THREE.Clock.getDelta()
|
|
161
162
|
* // ...camera + render (render-delta code lives OUT here, never inside)
|
|
162
163
|
* });
|
|
163
164
|
* ```
|
|
@@ -346,7 +346,7 @@ after a bad single costs the whole chain).
|
|
|
346
346
|
|
|
347
347
|
## Troubleshooting
|
|
348
348
|
|
|
349
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
349
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it
|
|
350
350
|
writes your `GENEX_TOKEN`).
|
|
351
351
|
- **"Prompt rejected"** — the provider's content-safety filter blocked the
|
|
352
352
|
prompt. Non-retryable; rewrite the wording.
|
|
@@ -182,7 +182,7 @@ set belongs to `$genex-ai-hud` — both build on this command.
|
|
|
182
182
|
|
|
183
183
|
## Troubleshooting
|
|
184
184
|
|
|
185
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
185
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
186
186
|
- **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
|
|
187
187
|
This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
|
|
188
188
|
- **Decal is an opaque rectangle** — the image has no alpha. Regenerate with
|
|
@@ -272,7 +272,7 @@ upgrades, in order of effort:
|
|
|
272
272
|
|
|
273
273
|
## Troubleshooting
|
|
274
274
|
|
|
275
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
275
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it
|
|
276
276
|
writes your `GENEX_TOKEN`).
|
|
277
277
|
- **"Prompt rejected"** — the provider's content-safety filter blocked the
|
|
278
278
|
prompt. Non-retryable; rewrite the wording.
|
|
@@ -107,7 +107,7 @@ scene is a ghost: players and objects pass straight through it.
|
|
|
107
107
|
|
|
108
108
|
## Troubleshooting
|
|
109
109
|
|
|
110
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
110
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
111
111
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
112
112
|
this model generation. Tell the user the facts the CLI printed: their balance, this
|
|
113
113
|
generation's cost, and when their credits refill. Then offer to continue the build
|
|
@@ -72,7 +72,7 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
|
|
|
72
72
|
|
|
73
73
|
## Troubleshooting
|
|
74
74
|
|
|
75
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
75
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
76
76
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
77
77
|
this sound generation. Tell the user the facts the CLI printed: their balance, this
|
|
78
78
|
generation's cost, and when their credits refill. Then offer to continue the build
|
|
@@ -76,7 +76,7 @@ scene.background = texture; // keep the raw texture for the visible sky
|
|
|
76
76
|
|
|
77
77
|
## Troubleshooting
|
|
78
78
|
|
|
79
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
79
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
80
80
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
81
81
|
this skybox generation. Tell the user the facts the CLI printed: their balance, this
|
|
82
82
|
generation's cost, and when their credits refill. Then offer to continue the build
|
|
@@ -81,7 +81,7 @@ scene.add(ground);
|
|
|
81
81
|
|
|
82
82
|
## Troubleshooting
|
|
83
83
|
|
|
84
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
84
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
85
85
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
86
86
|
this texture generation. Tell the user the facts the CLI printed: their balance,
|
|
87
87
|
this generation's cost, and when their credits refill. Then offer to continue the
|
|
@@ -145,7 +145,7 @@ set belongs to `$genex-ai-hud` — both build on `npx genex image`/`video`.
|
|
|
145
145
|
|
|
146
146
|
## Troubleshooting
|
|
147
147
|
|
|
148
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
148
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
149
149
|
- **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
|
|
150
150
|
This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
|
|
151
151
|
- **Nothing plays / black surface** — the first `video.play()` must run inside a user
|
|
@@ -43,7 +43,7 @@ Add `--json` for machine-readable output.
|
|
|
43
43
|
**As a NEW game (start from the whole project):**
|
|
44
44
|
|
|
45
45
|
1. `git clone <clone URL from the output> <name>` — pick a short one-word name.
|
|
46
|
-
2. `cd <name>`, then `npx @genex-ai/cli-demo@
|
|
46
|
+
2. `cd <name>`, then `npx @genex-ai/cli-demo@dev init <name>` — never use
|
|
47
47
|
`--force`. This creates your own project; the original is untouched.
|
|
48
48
|
3. `npm install`, keep `base: './'` in `vite.config`, then build your changes
|
|
49
49
|
and ship with `npx genex preview`.
|
|
@@ -125,7 +125,7 @@ and re-link the clone to the same live game:
|
|
|
125
125
|
```bash
|
|
126
126
|
git clone <the game's repo url> my-game && cd my-game
|
|
127
127
|
npm install
|
|
128
|
-
npx @genex-ai/cli-demo@
|
|
128
|
+
npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
|
|
129
129
|
```
|
|
130
130
|
|
|
131
131
|
`link` never creates a project: it signs in if needed (the browser opens once)
|
|
@@ -148,7 +148,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
|
|
|
148
148
|
and your own files are never touched:
|
|
149
149
|
|
|
150
150
|
```bash
|
|
151
|
-
npx @genex-ai/cli-demo@
|
|
151
|
+
npx @genex-ai/cli-demo@dev init
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
Use `--force` only if you intentionally want your own existing files overwritten
|
|
@@ -94,9 +94,14 @@ physics.onBeforeStep(() => {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
// Render phase: step physics, then camera, then animations.
|
|
97
|
+
// Delta comes from performance.now(), NOT THREE.Clock — Clock.getDelta() has
|
|
98
|
+
// repeatedly frozen at 0 in real games (world renders, nothing moves).
|
|
97
99
|
const pivot = new THREE.Vector3();
|
|
100
|
+
let lastT = performance.now();
|
|
98
101
|
renderer.setAnimationLoop(() => {
|
|
99
|
-
const
|
|
102
|
+
const nowT = performance.now();
|
|
103
|
+
const delta = Math.min((nowT - lastT) / 1000, 0.1);
|
|
104
|
+
lastT = nowT;
|
|
100
105
|
physics.step(delta); // runs the fixed substeps + world.step() + mesh sync
|
|
101
106
|
|
|
102
107
|
pivot.copy(character.currPos).addScaledVector(character.bodyYAxis, 0.5);
|
|
@@ -169,7 +169,11 @@ Per fixed **substep** (inside `physics.onBeforeStep`), in this order:
|
|
|
169
169
|
Per **render** frame (inside `renderer.setAnimationLoop`), in this order:
|
|
170
170
|
|
|
171
171
|
```ts
|
|
172
|
-
|
|
172
|
+
// Delta from performance.now() — THREE.Clock.getDelta() has repeatedly frozen
|
|
173
|
+
// at 0 in real games (world renders, nothing moves).
|
|
174
|
+
const nowT = performance.now();
|
|
175
|
+
const delta = Math.min((nowT - lastT) / 1000, 0.1);
|
|
176
|
+
lastT = nowT;
|
|
173
177
|
physics.step(delta); // 1. fixed substeps + mesh sync
|
|
174
178
|
|
|
175
179
|
pivot.copy(character.currPos).addScaledVector(character.bodyYAxis, 0.5);
|
|
@@ -59,9 +59,11 @@ physics.onBeforeStep(() => {
|
|
|
59
59
|
// controller.update(), impulses, kinematic platform poses ...
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
let lastT = performance.now(); // NOT THREE.Clock — its getDelta() has frozen at 0 in real games
|
|
63
63
|
renderer.setAnimationLoop(() => {
|
|
64
|
-
|
|
64
|
+
const nowT = performance.now();
|
|
65
|
+
physics.step(Math.min((nowT - lastT) / 1000, 0.1)); // fixed substeps + mesh sync + events
|
|
66
|
+
lastT = nowT;
|
|
65
67
|
// camera follow, mixers, HUD go HERE (render-delta work, after the step) —
|
|
66
68
|
// never inside onBeforeStep above
|
|
67
69
|
renderer.render(scene, camera);
|
|
@@ -51,8 +51,11 @@ physics.onBeforeStep(() => {
|
|
|
51
51
|
drone.update(dt);
|
|
52
52
|
}); // 4. world.step() runs after
|
|
53
53
|
|
|
54
|
+
let lastT = performance.now();
|
|
54
55
|
renderer.setAnimationLoop(() => {
|
|
55
|
-
|
|
56
|
+
const nowT = performance.now();
|
|
57
|
+
physics.step(Math.min((nowT - lastT) / 1000, 0.1)); // fixed substeps + interpolated mesh sync
|
|
58
|
+
lastT = nowT; // performance.now() delta — THREE.Clock.getDelta() freezes at 0 in real games
|
|
56
59
|
// camera + render here — render-delta code never goes inside onBeforeStep
|
|
57
60
|
});
|
|
58
61
|
```
|
|
@@ -26,11 +26,16 @@ evidence, temporal checks, budgets, and explicit rejection criteria.
|
|
|
26
26
|
|
|
27
27
|
For plain game tasks — nothing from the procedural/visual-system pack loaded —
|
|
28
28
|
this is the whole acceptance gate, and it is also the *ceiling*: a smoke check,
|
|
29
|
-
not a certification. Run it ONCE per milestone
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
not a certification. Run it ONCE per milestone, and only AFTER that milestone's
|
|
30
|
+
`genex preview` is pushed — ship the rough draft first, then check it; the
|
|
31
|
+
player may already be walking around your build while you do. Catch obvious
|
|
32
|
+
breakage, fix what's clearly broken, and hand the feel/polish judgment to the
|
|
33
|
+
player — don't loop re-testing the same build, don't re-test after cosmetic
|
|
34
|
+
tweaks (sounds, colors, tuning values), and don't try to exercise every button
|
|
35
|
+
and edge case yourself. This browser check is the only testing a game draft
|
|
36
|
+
needs: never add unit tests or a test framework to a game unless the user asks.
|
|
37
|
+
The player deciding "does it feel right?" is faster and truer than you clicking
|
|
38
|
+
everything twice.
|
|
34
39
|
|
|
35
40
|
1. Load the page in a real browser; the canvas renders (no black screen, no
|
|
36
41
|
console errors). For an unpublished draft, open the dev server in local
|
|
@@ -38,7 +38,7 @@ update, so update immediately.)
|
|
|
38
38
|
Run exactly the command the nudge printed, from the game project root:
|
|
39
39
|
|
|
40
40
|
```bash
|
|
41
|
-
npm i -D @genex-ai/cli-demo@
|
|
41
|
+
npm i -D @genex-ai/cli-demo@dev # the genex CLI (a dev dependency)
|
|
42
42
|
npm i @genex-ai/embed-sdk@latest # identity/saves SDK (ships inside the game)
|
|
43
43
|
npm i @genex-ai/multiplayer@latest # multiplayer SDK (only if the game uses it)
|
|
44
44
|
```
|