@genex-ai/cli-demo 0.51.0-dev.110 → 0.51.0-dev.98
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 +10 -146
- package/package.json +1 -1
- package/templates/skills/genex-ai-hud/SKILL.md +0 -18
- package/templates/skills/genex-ai-image/SKILL.md +0 -12
- package/templates/skills/genex-ai-texture/SKILL.md +2 -22
- package/templates/skills/genex-threejs-game-ui/SKILL.md +7 -61
- package/templates/skills/genex-threejs-skill-router/SKILL.md +5 -7
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +7 -13
package/dist/index.js
CHANGED
|
@@ -1652,13 +1652,7 @@ async function callPublish(ctx, commit, opts, log) {
|
|
|
1652
1652
|
async function pushSource(cwd, ctx, log) {
|
|
1653
1653
|
const target = await fetchPushUrl(ctx, log);
|
|
1654
1654
|
if (!target) return false;
|
|
1655
|
-
|
|
1656
|
-
if (!target.managed) return false;
|
|
1657
|
-
log.info("Retrying the source push\u2026");
|
|
1658
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
1659
|
-
const fresh = await fetchPushUrl(ctx, log);
|
|
1660
|
-
if (!fresh) return false;
|
|
1661
|
-
return pushWorktree(cwd, fresh.pushUrl, fresh.managed, log);
|
|
1655
|
+
return pushWorktree(cwd, target.pushUrl, target.managed, log);
|
|
1662
1656
|
}
|
|
1663
1657
|
async function pushWorktree(cwd, pushUrl, managed, log) {
|
|
1664
1658
|
const EMPTY_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
@@ -2125,44 +2119,6 @@ async function runPreview(opts) {
|
|
|
2125
2119
|
}
|
|
2126
2120
|
}
|
|
2127
2121
|
|
|
2128
|
-
// src/lib/open.ts
|
|
2129
|
-
import { spawn as spawn4 } from "child_process";
|
|
2130
|
-
function tokenize(cmd) {
|
|
2131
|
-
return cmd.trim().split(/\s+/).filter(Boolean);
|
|
2132
|
-
}
|
|
2133
|
-
function openUrl(url) {
|
|
2134
|
-
let command;
|
|
2135
|
-
let args;
|
|
2136
|
-
const custom = (process.env.GENEX_BROWSER || process.env.BROWSER)?.trim();
|
|
2137
|
-
const customParts = custom ? tokenize(custom) : [];
|
|
2138
|
-
if (customParts.length > 0) {
|
|
2139
|
-
command = customParts[0];
|
|
2140
|
-
args = [...customParts.slice(1), url];
|
|
2141
|
-
} else {
|
|
2142
|
-
switch (process.platform) {
|
|
2143
|
-
case "darwin":
|
|
2144
|
-
command = "open";
|
|
2145
|
-
args = [url];
|
|
2146
|
-
break;
|
|
2147
|
-
case "win32":
|
|
2148
|
-
command = "cmd";
|
|
2149
|
-
args = ["/c", "start", "", url];
|
|
2150
|
-
break;
|
|
2151
|
-
default:
|
|
2152
|
-
command = "xdg-open";
|
|
2153
|
-
args = [url];
|
|
2154
|
-
break;
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
try {
|
|
2158
|
-
const child = spawn4(command, args, { stdio: "ignore", detached: true });
|
|
2159
|
-
child.on("error", () => {
|
|
2160
|
-
});
|
|
2161
|
-
child.unref();
|
|
2162
|
-
} catch {
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
|
|
2166
2122
|
// src/lib/sse.ts
|
|
2167
2123
|
async function* readSSE(body) {
|
|
2168
2124
|
const decoder = new TextDecoder();
|
|
@@ -2299,9 +2255,9 @@ async function runGenerate(kind, opts) {
|
|
|
2299
2255
|
log.dim(" (Re-running the generate command would start \u2014 and bill \u2014 a NEW generation.)");
|
|
2300
2256
|
return;
|
|
2301
2257
|
}
|
|
2302
|
-
await awaitAndReport(apiUrl, token, id, kind, log
|
|
2258
|
+
await awaitAndReport(apiUrl, token, id, kind, log);
|
|
2303
2259
|
}
|
|
2304
|
-
async function awaitAndReport(apiUrl, token, id, kind, log
|
|
2260
|
+
async function awaitAndReport(apiUrl, token, id, kind, log) {
|
|
2305
2261
|
log.step("Generating\u2026 (this can take up to a minute)");
|
|
2306
2262
|
const onProgress = (p) => log.dim(` ${p}%`);
|
|
2307
2263
|
const timeoutMs = waitTimeoutFor(kind);
|
|
@@ -2313,9 +2269,9 @@ async function awaitAndReport(apiUrl, token, id, kind, log, open = false) {
|
|
|
2313
2269
|
process.exitCode = 1;
|
|
2314
2270
|
return;
|
|
2315
2271
|
}
|
|
2316
|
-
await reportTerminal(kind, view, log
|
|
2272
|
+
await reportTerminal(kind, view, log);
|
|
2317
2273
|
}
|
|
2318
|
-
async function reportTerminal(kind, view, log
|
|
2274
|
+
async function reportTerminal(kind, view, log) {
|
|
2319
2275
|
if (view.status !== "completed") {
|
|
2320
2276
|
log.error(`Generation ${view.status}${view.error ? `: ${view.error}` : ""}.`);
|
|
2321
2277
|
process.exitCode = 1;
|
|
@@ -2334,14 +2290,6 @@ async function reportTerminal(kind, view, log, open = false) {
|
|
|
2334
2290
|
log.plain(` ${label}${f.url}`);
|
|
2335
2291
|
}
|
|
2336
2292
|
log.plain("");
|
|
2337
|
-
if (open && files[0]) {
|
|
2338
|
-
openUrl(files[0].url);
|
|
2339
|
-
log.plain(
|
|
2340
|
-
` ${c.bold("\u{1F441} Show this to the user")} \u2014 it just opened in their browser; paste this link in chat too so they can open it themselves:`
|
|
2341
|
-
);
|
|
2342
|
-
log.plain(` ${files[0].url}`);
|
|
2343
|
-
log.plain("");
|
|
2344
|
-
}
|
|
2345
2293
|
printHint(kind, files, log);
|
|
2346
2294
|
await firstPreviewNudge(log);
|
|
2347
2295
|
}
|
|
@@ -2492,10 +2440,10 @@ async function runWait(opts) {
|
|
|
2492
2440
|
log.dim(` ${kind} ${id}`);
|
|
2493
2441
|
log.plain("");
|
|
2494
2442
|
if (TERMINAL2.has(view.status)) {
|
|
2495
|
-
await reportTerminal(kind, view, log
|
|
2443
|
+
await reportTerminal(kind, view, log);
|
|
2496
2444
|
return;
|
|
2497
2445
|
}
|
|
2498
|
-
await awaitAndReport(apiUrl, token, id, kind, log
|
|
2446
|
+
await awaitAndReport(apiUrl, token, id, kind, log);
|
|
2499
2447
|
}
|
|
2500
2448
|
|
|
2501
2449
|
// src/commands/controller.ts
|
|
@@ -3235,8 +3183,7 @@ var UI_NUMBER_FLAGS = {
|
|
|
3235
3183
|
"--edge-flush-max": "edgeFlushMax",
|
|
3236
3184
|
"--quantize": "quantize",
|
|
3237
3185
|
"--ink-dist": "inkDist",
|
|
3238
|
-
"--min-frac": "minFrac"
|
|
3239
|
-
"--seam-tolerance": "seamTolerance"
|
|
3186
|
+
"--min-frac": "minFrac"
|
|
3240
3187
|
};
|
|
3241
3188
|
var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or https URL>)")}
|
|
3242
3189
|
ui extract --out-dir <dir> --names a,b,c (reading order) \u2014 split an
|
|
@@ -3252,10 +3199,7 @@ var UI_USAGE = `${c.bold("Options for `ui` (all tools read --in <png path or htt
|
|
|
3252
3199
|
ink candidates as JSON. [--quantize 12] [--ink-dist 60]
|
|
3253
3200
|
[--min-frac 0.004] [--out <json>] [--crop <png>]
|
|
3254
3201
|
ui trim [--out <png>] \u2014 crop to the alpha bbox (default: overwrite
|
|
3255
|
-
--in), print real dims + refresh the .bbox.json sidecar
|
|
3256
|
-
ui seams measure a tiling texture's wrap seam (opposite edges vs
|
|
3257
|
-
interior detail) \u2192 ratio + verdict as JSON; nonzero exit on
|
|
3258
|
-
a visible seam. [--seam-tolerance 3]`;
|
|
3202
|
+
--in), print real dims + refresh the .bbox.json sidecar.`;
|
|
3259
3203
|
async function runUi(opts) {
|
|
3260
3204
|
const log = createLogger({ quiet: opts.quiet });
|
|
3261
3205
|
try {
|
|
@@ -3272,12 +3216,9 @@ async function runUi(opts) {
|
|
|
3272
3216
|
case "trim":
|
|
3273
3217
|
await uiTrim(opts, log);
|
|
3274
3218
|
return;
|
|
3275
|
-
case "seams":
|
|
3276
|
-
await uiSeams(opts, log);
|
|
3277
|
-
return;
|
|
3278
3219
|
default:
|
|
3279
3220
|
log.error(
|
|
3280
|
-
opts.sub ? `Unknown ui tool "${opts.sub}". Tools: extract, masks, text-color, trim
|
|
3221
|
+
opts.sub ? `Unknown ui tool "${opts.sub}". Tools: extract, masks, text-color, trim.` : "Missing ui tool. Tools: extract, masks, text-color, trim."
|
|
3281
3222
|
);
|
|
3282
3223
|
log.plain(UI_USAGE);
|
|
3283
3224
|
process.exitCode = 1;
|
|
@@ -3984,78 +3925,6 @@ async function uiTrim(opts, log) {
|
|
|
3984
3925
|
`
|
|
3985
3926
|
);
|
|
3986
3927
|
}
|
|
3987
|
-
async function uiSeams(opts, log) {
|
|
3988
|
-
const input = requireOpt(opts.input, "--in", "seams");
|
|
3989
|
-
const tol = opts.seamTolerance ?? 3;
|
|
3990
|
-
const png = await loadPng(input);
|
|
3991
|
-
const { width: w, height: h, data } = png;
|
|
3992
|
-
if (w < 4 || h < 4) fail("Image is too small to test for tiling.", { width: w, height: h });
|
|
3993
|
-
const chan = (x, y, ch) => data[(y * w + x) * 4 + ch] ?? 0;
|
|
3994
|
-
const colDiff = (a, b) => {
|
|
3995
|
-
let s = 0;
|
|
3996
|
-
for (let y = 0; y < h; y++) {
|
|
3997
|
-
s += Math.abs(chan(a, y, 0) - chan(b, y, 0));
|
|
3998
|
-
s += Math.abs(chan(a, y, 1) - chan(b, y, 1));
|
|
3999
|
-
s += Math.abs(chan(a, y, 2) - chan(b, y, 2));
|
|
4000
|
-
}
|
|
4001
|
-
return s / (h * 3);
|
|
4002
|
-
};
|
|
4003
|
-
const rowDiff = (a, b) => {
|
|
4004
|
-
let s = 0;
|
|
4005
|
-
for (let x = 0; x < w; x++) {
|
|
4006
|
-
s += Math.abs(chan(x, a, 0) - chan(x, b, 0));
|
|
4007
|
-
s += Math.abs(chan(x, a, 1) - chan(x, b, 1));
|
|
4008
|
-
s += Math.abs(chan(x, a, 2) - chan(x, b, 2));
|
|
4009
|
-
}
|
|
4010
|
-
return s / (w * 3);
|
|
4011
|
-
};
|
|
4012
|
-
const seamH = colDiff(0, w - 1);
|
|
4013
|
-
const seamV = rowDiff(0, h - 1);
|
|
4014
|
-
const stepX = Math.max(1, Math.floor(w / 64));
|
|
4015
|
-
const stepY = Math.max(1, Math.floor(h / 64));
|
|
4016
|
-
let baseH = 0;
|
|
4017
|
-
let nH = 0;
|
|
4018
|
-
for (let x = 1; x < w - 1; x += stepX) {
|
|
4019
|
-
baseH += colDiff(x, x + 1);
|
|
4020
|
-
nH++;
|
|
4021
|
-
}
|
|
4022
|
-
let baseV = 0;
|
|
4023
|
-
let nV = 0;
|
|
4024
|
-
for (let y = 1; y < h - 1; y += stepY) {
|
|
4025
|
-
baseV += rowDiff(y, y + 1);
|
|
4026
|
-
nV++;
|
|
4027
|
-
}
|
|
4028
|
-
baseH = Math.max(baseH / Math.max(1, nH), 1e-6);
|
|
4029
|
-
baseV = Math.max(baseV / Math.max(1, nV), 1e-6);
|
|
4030
|
-
const round = (n) => Math.round(n * 100) / 100;
|
|
4031
|
-
const ratioH = round(seamH / baseH);
|
|
4032
|
-
const ratioV = round(seamV / baseV);
|
|
4033
|
-
const worst = Math.max(ratioH, ratioV);
|
|
4034
|
-
const axis = ratioH >= ratioV ? "horizontal" : "vertical";
|
|
4035
|
-
const seamless = worst <= tol;
|
|
4036
|
-
const name = input.split("/").pop() ?? input;
|
|
4037
|
-
process.stdout.write(
|
|
4038
|
-
`${JSON.stringify({
|
|
4039
|
-
input,
|
|
4040
|
-
width: w,
|
|
4041
|
-
height: h,
|
|
4042
|
-
ratioHorizontal: ratioH,
|
|
4043
|
-
ratioVertical: ratioV,
|
|
4044
|
-
worstRatio: worst,
|
|
4045
|
-
worstAxis: axis,
|
|
4046
|
-
tolerance: tol,
|
|
4047
|
-
seamless
|
|
4048
|
-
})}
|
|
4049
|
-
`
|
|
4050
|
-
);
|
|
4051
|
-
if (seamless) {
|
|
4052
|
-
log.success(`${name}: tiles cleanly \u2014 worst seam ${worst}\xD7 the texture's own detail (\u2264 ${tol}\xD7).`);
|
|
4053
|
-
return;
|
|
4054
|
-
}
|
|
4055
|
-
fail(
|
|
4056
|
-
`Visible ${axis} tiling seam in ${name}: the tile boundary jumps ${worst}\xD7 the texture's own detail (want \u2264 ${tol}\xD7). Regenerate it seamless/tileable, or lower the mesh's UV repeat so the seam falls off-camera.`
|
|
4057
|
-
);
|
|
4058
|
-
}
|
|
4059
3928
|
|
|
4060
3929
|
// src/index.ts
|
|
4061
3930
|
var GEN_KINDS = /* @__PURE__ */ new Set(["model", "skybox", "sfx", "texture", "image", "video"]);
|
|
@@ -4100,8 +3969,6 @@ ${c.bold("Options for the generators (`model` `skybox` `sfx` `texture` `image` `
|
|
|
4100
3969
|
--terrain (texture) seamless tiling surface for terrain/ground.
|
|
4101
3970
|
--duration <sec> (sfx, video) target clip length in seconds.
|
|
4102
3971
|
--transparent (image) transparent background (alpha) \u2014 for decals/stickers.
|
|
4103
|
-
--open (image/video) open the finished asset in the user's browser +
|
|
4104
|
-
print a link \u2014 show them the concept frame / anything they approve.
|
|
4105
3972
|
--aspect <ratio> (image) aspect preset (e.g. square, landscape, portrait).
|
|
4106
3973
|
--quality <q> (image) quality preset: low | medium | high.
|
|
4107
3974
|
--candidates <n> (image) generate 2-4 variants in ONE call and print every
|
|
@@ -4286,9 +4153,6 @@ function parseArgs(argv) {
|
|
|
4286
4153
|
case "--no-wait":
|
|
4287
4154
|
parsed.options.noWait = true;
|
|
4288
4155
|
break;
|
|
4289
|
-
case "--open":
|
|
4290
|
-
parsed.options.open = true;
|
|
4291
|
-
break;
|
|
4292
4156
|
case "--terrain":
|
|
4293
4157
|
parsed.options.terrain = true;
|
|
4294
4158
|
break;
|
package/package.json
CHANGED
|
@@ -114,24 +114,6 @@ The test: "is this fixed identity artwork, or should runtime code own it?"
|
|
|
114
114
|
Baking a runtime value into a sprite is a failure; stripping a carved identity
|
|
115
115
|
label flattens the art.
|
|
116
116
|
|
|
117
|
-
## Icons & mechanics must match the game — diff before Stage 2
|
|
118
|
-
|
|
119
|
-
Words are only half of it. Every ICON, slot, gauge, and pictogram in the mockup
|
|
120
|
-
implies a MECHANIC: a grenade icon promises grenades, three ability slots
|
|
121
|
-
promise three abilities, an armor bar promises armor. Before Stage 2, walk each
|
|
122
|
-
one against the game contract — the mechanics the code actually has (or that you
|
|
123
|
-
are committing to build):
|
|
124
|
-
|
|
125
|
-
- **Backed by a real mechanic** → keep it and wire it to that state.
|
|
126
|
-
- **No backing mechanic** → it is a promise the player will notice is empty.
|
|
127
|
-
CUT it (prompt Stage 2, or a region re-edit, to omit that icon/slot) OR add
|
|
128
|
-
the mechanic to the game. Shipping a depicted control the game can't do — a
|
|
129
|
-
grenade slot on a rifle-only HUD, an armor bar with no armor, a minimap over
|
|
130
|
-
no map — is a failure, and "the model drew it so I kept it" is how it happens.
|
|
131
|
-
|
|
132
|
-
The mockup (and any concept frame) anchors STYLE; the game contract owns WHAT
|
|
133
|
-
EXISTS. When the picture and the mechanics disagree, the mechanics win.
|
|
134
|
-
|
|
135
117
|
## The pipeline
|
|
136
118
|
|
|
137
119
|
Each step is one command; every `<...-url>` is the R2 URL the previous command
|
|
@@ -129,18 +129,6 @@ Gotchas (all bite in practice):
|
|
|
129
129
|
moving or skinned target, use a small `PlaneGeometry` quad offset along the normal
|
|
130
130
|
(`hit.point + n * 0.01`, `quad.lookAt(hit.point.clone().add(n))`) instead.
|
|
131
131
|
|
|
132
|
-
### Impact marks (bullet holes, scorch, blood) — spawn on the hit, not a keypress
|
|
133
|
-
|
|
134
|
-
A game that shoots, throws, or explodes and leaves NO mark on what it hits reads
|
|
135
|
-
as unfinished — and it's the same recipe, driven by the weapon's raycast you
|
|
136
|
-
already have. Generate ONE small `--transparent` bullet-hole / scorch / impact
|
|
137
|
-
image, then on each confirmed **world** hit place a decal at `hit.point` with
|
|
138
|
-
`hit.face.normal` (exactly the `spray()` body above), capped in a FIFO ring
|
|
139
|
-
(~30–50) reusing one shared material. On enemy/skinned hits skip the decal and
|
|
140
|
-
use the quad fallback or a hit-spark VFX instead. Impact marks are a first-class
|
|
141
|
-
generated surface for any weapon/collision game — inventory them up front with
|
|
142
|
-
the rest of your art, don't discover the bare walls at the end.
|
|
143
|
-
|
|
144
132
|
## Multiplayer
|
|
145
133
|
|
|
146
134
|
The asset URL is public, permanent, and CORS-open, so it is safe to broadcast the
|
|
@@ -67,24 +67,6 @@ map.repeat.set(64, 64);
|
|
|
67
67
|
scene.add(ground);
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
## Verify it actually tiles — don't eyeball it
|
|
71
|
-
|
|
72
|
-
A texture that isn't truly seamless draws a repeating seam grid the moment
|
|
73
|
-
`repeat` is raised — the single most common "asset-flip" tell, and `--terrain`
|
|
74
|
-
asks for seamless but does not guarantee it. So measure the seam, don't guess:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
npx genex ui seams --in <texture-url> # a local .png works too
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
It compares the texture's opposite edges against its own interior detail and
|
|
81
|
-
prints a ratio + verdict (`seamless: false` and a nonzero exit on a visible
|
|
82
|
-
seam). A fail is a real defect — fix it in this order: **regenerate** (add
|
|
83
|
-
"seamless tiling, no visible edges" to the prompt, or `--terrain`), then **lower
|
|
84
|
-
`repeat`** so any residual seam falls off-camera. Run this on every tiled
|
|
85
|
-
floor/wall texture before you call the scene done — a large flat surface with a
|
|
86
|
-
high `repeat` and an unchecked texture is exactly how seams reach the player.
|
|
87
|
-
|
|
88
70
|
## Publish checklist
|
|
89
71
|
|
|
90
72
|
- Load it from the **URL** the command printed — absolute and permanent, so it resolves
|
|
@@ -110,8 +92,6 @@ high `repeat` and an unchecked texture is exactly how seams reach the player.
|
|
|
110
92
|
- **"Email not verified" (`email_verification_required`)** — generation credits
|
|
111
93
|
unlock after the account's email is verified. Give the user the verify link the
|
|
112
94
|
CLI printed, wait for them to confirm, then re-run the command.
|
|
113
|
-
- **Visible tiling seams** —
|
|
114
|
-
|
|
115
|
-
`--terrain` / a "seamless tiling" prompt, lower the `repeat`, or blend two
|
|
116
|
-
textures until the check passes.
|
|
95
|
+
- **Visible tiling seams** — use `--terrain` (tuned for seamless edges), lower the
|
|
96
|
+
`repeat`, or blend two textures. Perfect seamlessness is a known v1 limitation.
|
|
117
97
|
- **Colors look washed/dark** — ensure `map.colorSpace = THREE.SRGBColorSpace`.
|
|
@@ -79,25 +79,13 @@ lap counter in a game without laps). `--aspect 16:9 --quality high
|
|
|
79
79
|
--no-wait`, enqueued FIRST of all art; the URL goes into the style-brief
|
|
80
80
|
comment.
|
|
81
81
|
|
|
82
|
-
**Share it before building on it
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
change something?" — with 2–3 concrete adjustment options. Keep building
|
|
90
|
-
gameplay-neutral work (scaffold, physics, netcode) while waiting, but do NOT
|
|
91
|
-
anchor further art to an unapproved frame. **Approval is a LOOP, not a
|
|
92
|
-
one-shot:** if the user dislikes ANYTHING, regenerate with their exact notes
|
|
93
|
-
(`--candidates 2–3` gives them options to choose from), open + link the new
|
|
94
|
-
frame, and ask again — repeat until they actually approve. Carry every note
|
|
95
|
-
forward so each round compounds; if two rounds don't converge, offer 2–3
|
|
96
|
-
distinct directions as a structured question instead of re-rolling blind. The
|
|
97
|
-
HUD mockup, the menu, and every downstream asset wait for a frame the user has
|
|
98
|
-
signed off — a single "meh" is never permission to move on. The same "open it +
|
|
99
|
-
paste the link" rule covers every image the user weighs in on — the menu still,
|
|
100
|
-
the HUD mockup candidates.
|
|
82
|
+
**Share it before building on it.** The moment the frame lands, show it to
|
|
83
|
+
the user — "this is roughly how the game will look: keep it, or change
|
|
84
|
+
something?" — as ONE structured question with 2–3 concrete adjustment
|
|
85
|
+
options. Keep building gameplay-neutral work (scaffold, physics, netcode)
|
|
86
|
+
while waiting, but do NOT anchor further art to an unapproved frame; if the
|
|
87
|
+
user redirects, regenerate with their notes before the HUD mockup and menu
|
|
88
|
+
frame go out.
|
|
101
89
|
|
|
102
90
|
**The concept anchors STYLE, not truth.** Palette, materials, light, and
|
|
103
91
|
register come from the frame; CONTENT comes from the game contract. When
|
|
@@ -207,48 +195,6 @@ edits scattered through the code:
|
|
|
207
195
|
HUD state come from `$genex-threejs-multiplayer`; render what the SDK
|
|
208
196
|
reports, don't guess at it.
|
|
209
197
|
|
|
210
|
-
## Escape must pause the game — not shrink the window
|
|
211
|
-
|
|
212
|
-
For a game that captures the mouse (first-person or pointer-lock aim — see the
|
|
213
|
-
pointer bucket in `$genex-threejs-camera-direction`), pressing Escape does two
|
|
214
|
-
**browser-reserved** things you CANNOT stop with `preventDefault`: it exits
|
|
215
|
-
fullscreen and releases pointer lock, and the player sees the window "shrink".
|
|
216
|
-
The real fix is the **Keyboard Lock API**, which routes Escape to your code
|
|
217
|
-
instead — Chromium only, and only in fullscreen. Enter fullscreen on a gesture
|
|
218
|
-
(the Deploy/Resume click), capture Escape, and own the pause:
|
|
219
|
-
|
|
220
|
-
```ts
|
|
221
|
-
// call ONLY from a user gesture (the Deploy click, the Resume click) — never at boot
|
|
222
|
-
async function enterImmersive(): Promise<void> {
|
|
223
|
-
try {
|
|
224
|
-
if (!document.fullscreenElement)
|
|
225
|
-
await document.documentElement.requestFullscreen({ navigationUI: "hide" });
|
|
226
|
-
} catch { /* sandboxed iframe or no gesture — the pointer-lock path below still pauses */ }
|
|
227
|
-
// Chromium + fullscreen only: deliver Escape to us instead of exiting fullscreen.
|
|
228
|
-
try {
|
|
229
|
-
await (navigator as unknown as { keyboard?: { lock?: (k: string[]) => Promise<void> } })
|
|
230
|
-
.keyboard?.lock?.(["Escape"]);
|
|
231
|
-
} catch { /* Keyboard Lock unsupported — fine */ }
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
document.addEventListener("keydown", (e) => {
|
|
235
|
-
if (e.code !== "Escape") return;
|
|
236
|
-
e.preventDefault(); // stops any *other* default; the reserved ones the lock handles
|
|
237
|
-
if (phase === "playing") { document.exitPointerLock?.(); setPhase("paused"); } // free cursor for the menu
|
|
238
|
-
else if (phase === "paused") { void enterImmersive(); canvas.requestPointerLock?.(); } // Esc resumes
|
|
239
|
-
});
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
- **Degradation is built in.** Where Keyboard Lock is absent (Safari, Firefox) or
|
|
243
|
-
the game isn't fullscreen, the browser still releases pointer lock on Escape —
|
|
244
|
-
so ALSO keep the pointer-lock-loss → pause path (`pointerlockchange`: if
|
|
245
|
-
unlocked while `playing`, `setPhase("paused")`). Escape then always pauses; it
|
|
246
|
-
only *also* drops fullscreen on browsers without the lock — unavoidable there.
|
|
247
|
-
- **Only for immersive/pointer-lock games.** A top-down or menu-driven game never
|
|
248
|
-
captures the mouse and has no window to shrink — skip all of this.
|
|
249
|
-
- **Dashboard embed:** full coverage needs the game iframe to permit keyboard
|
|
250
|
-
lock; standalone play (`<slug>.genex.technology`) works today regardless.
|
|
251
|
-
|
|
252
198
|
## The loader
|
|
253
199
|
|
|
254
200
|
The loader is the first thing every player sees — a bare "Loading… 3/5" over
|
|
@@ -67,13 +67,11 @@ direction in the same plan block — the camera rig + pointer bucket
|
|
|
67
67
|
(`$genex-threejs-camera-direction`), the renderer baseline (tone mapping,
|
|
68
68
|
exposure, output color space — set deliberately at boot; stock three.js
|
|
69
69
|
defaults are not a look, `$genex-threejs-exposure-color-grading`), the post
|
|
70
|
-
stack this game ships —
|
|
71
|
-
|
|
72
|
-
on
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
`$genex-threejs-image-pipeline` owns ordering when 2+ compose), one named
|
|
76
|
-
**ambient-motion loop** that keeps
|
|
70
|
+
stack this game ships (bloom/AO/grade — at least ONE built render-pass effect
|
|
71
|
+
for EVERY game, justified by the shared style brief; the UI vignette div or a
|
|
72
|
+
CSS filter on the canvas does not count, and a plan that "deliberately skips
|
|
73
|
+
everything" is the stock default, not a look; `$genex-threejs-image-pipeline`
|
|
74
|
+
owns ordering when 2+ compose), one named **ambient-motion loop** that keeps
|
|
77
75
|
the scene alive at rest (emissive pulse, shimmer, drifting dust — shader
|
|
78
76
|
work, zero generations), and the lighting/atmosphere mood from that
|
|
79
77
|
same brief. Planning is not building — effects still land last in the
|
|
@@ -41,19 +41,13 @@ Three.js release or branch, and do not blindly copy demo architecture.
|
|
|
41
41
|
- **renderer baseline**: tone mapping, exposure, and output color space set
|
|
42
42
|
deliberately at boot (`$genex-threejs-exposure-color-grading` owns the
|
|
43
43
|
staging — stock three.js defaults are not a look);
|
|
44
|
-
- **the post stack**:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
calls for, each effect named and justified. ONE scene-serving render-pass
|
|
52
|
-
effect (bloom, AO, or a LUT/shader grade — a UI vignette div or a CSS
|
|
53
|
-
canvas filter does NOT count) is the FLOOR against "no post", never the
|
|
54
|
-
target: match the concept's richness, don't stop at one token pass. "No
|
|
55
|
-
post at all" is the stock default, not a plan, and "it's only a draft" is
|
|
56
|
-
not a lower floor. When 2+ effects compose, `$genex-threejs-image-pipeline`
|
|
44
|
+
- **the post stack**: which image effects THIS game ships (bloom? AO?
|
|
45
|
+
grade?) — each named and justified by the shared style brief. The floor
|
|
46
|
+
for EVERY game is the deliberate renderer baseline PLUS at least one
|
|
47
|
+
scene-serving render-pass effect (bloom, AO, or a LUT/shader grade — the
|
|
48
|
+
UI vignette div or a CSS filter on the canvas does not count); "no post
|
|
49
|
+
at all" is the stock default, not a plan, and "it's only a draft" is not
|
|
50
|
+
a lower floor. When 2+ effects compose, `$genex-threejs-image-pipeline`
|
|
57
51
|
owns the pass ordering;
|
|
58
52
|
- **references**: name 2–3 AAA games whose look this game borrows
|
|
59
53
|
(conventions, lighting mood, palette, post — never trade dress); the
|