@genex-ai/cli-demo 0.98.0 → 0.99.0-dev.286
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 +25 -2
- package/package.json +1 -1
- package/templates/skills/genex-ai-music/SKILL.md +10 -3
- package/templates/skills/genex-ai-sfx/SKILL.md +105 -3
- 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-getting-started/SKILL.md +2 -2
- package/templates/skills/genex-threejs-adaptive-quality/SKILL.md +25 -0
- package/templates/skills/genex-threejs-vehicle-controllers/references/drone.md +6 -0
- 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" },
|
|
@@ -4908,7 +4908,12 @@ function splitArgs(text) {
|
|
|
4908
4908
|
var lineOf = (content, index) => content.slice(0, index).split("\n").length;
|
|
4909
4909
|
var DEPTH_RATIO_LIMIT = 1e6;
|
|
4910
4910
|
async function detectSurfaceScan(cwd = process.cwd()) {
|
|
4911
|
-
const found = {
|
|
4911
|
+
const found = {
|
|
4912
|
+
guessedRepeat: [],
|
|
4913
|
+
squarePoints: [],
|
|
4914
|
+
depthRange: [],
|
|
4915
|
+
mediaElementAudio: []
|
|
4916
|
+
};
|
|
4912
4917
|
const srcDir = path15.join(cwd, "src");
|
|
4913
4918
|
let entries;
|
|
4914
4919
|
try {
|
|
@@ -4955,6 +4960,18 @@ async function detectSurfaceScan(cwd = process.cwd()) {
|
|
|
4955
4960
|
ratio
|
|
4956
4961
|
});
|
|
4957
4962
|
}
|
|
4963
|
+
const threeAudioImport = /import\s+(?:type\s+)?\{[^}]*\bAudio\b[^}]*\}\s+from\s*["']three["']/.test(
|
|
4964
|
+
content
|
|
4965
|
+
);
|
|
4966
|
+
const mediaRe = /\bnew\s+Audio\s*\(|\bdocument\s*\.\s*createElement\s*\(\s*["'`]audio["'`]\s*\)/g;
|
|
4967
|
+
while (m = mediaRe.exec(content)) {
|
|
4968
|
+
const isCtor = m[0].startsWith("new");
|
|
4969
|
+
if (isCtor && threeAudioImport) continue;
|
|
4970
|
+
found.mediaElementAudio.push({
|
|
4971
|
+
where: `${rel}:${lineOf(content, m.index)}`,
|
|
4972
|
+
detail: isCtor ? "new Audio(...)" : 'document.createElement("audio")'
|
|
4973
|
+
});
|
|
4974
|
+
}
|
|
4958
4975
|
}
|
|
4959
4976
|
return found;
|
|
4960
4977
|
}
|
|
@@ -5079,6 +5096,12 @@ function surfaceNudges(log, s) {
|
|
|
5079
5096
|
`Particles will render as hard SQUARES: ${list} builds a PointsMaterial with no map/alphaMap, so every point is an opaque camera-facing quad. Give it a soft sprite (genex image --transparent) or a round gl_PointCoord cutout \u2014 and check the particle belongs there at all: genex-threejs-procedural-vfx decides that per moment.`
|
|
5080
5097
|
);
|
|
5081
5098
|
}
|
|
5099
|
+
if (s.mediaElementAudio.length) {
|
|
5100
|
+
const list = s.mediaElementAudio.map((d) => `${d.where} \u2014 ${d.detail}`).join("; ");
|
|
5101
|
+
log.warn(
|
|
5102
|
+
`HTML <audio> elements are carrying this game's sound: ${list}. On iPhone an <audio> element is a handle on the operating system's media pipeline in ANOTHER PROCESS: every write to .volume or .playbackRate is a cross-process call, and play() is slow and asynchronous. A sound driven from a live value \u2014 wheels from speed, engine from RPM \u2014 writes 60-120 of those a second and stutters the whole game, while one-shots arrive late. It disguises itself as a graphics problem perfectly: only while moving, identical at every quality setting, desktop fine. THREE.Audio/PositionalAudio on one AudioListener have none of this \u2014 genex-ai-sfx ships the pooled one-shot, the live-value loop, and the decode-during-the-menu preload.`
|
|
5103
|
+
);
|
|
5104
|
+
}
|
|
5082
5105
|
for (const d of s.depthRange) {
|
|
5083
5106
|
log.warn(
|
|
5084
5107
|
`Camera depth range ${d.detail} (far/near = ${d.ratio.toExponential(1)}) at ${d.where} is past what a 24-bit depth buffer can serve \u2014 touching surfaces will z-fight and flicker, and it reads fine right up until it doesn't. near is the expensive knob (resolvable depth \u2248 z\xB2 / (near \xD7 2^24)), so raise near to the closest the camera can actually get. If the range is genuinely astronomical, that is what WebGLRenderer's logarithmicDepthBuffer is for: genex-threejs-camera-direction.`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genex-ai/cli-demo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.99.0-dev.286",
|
|
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": {
|
|
@@ -87,6 +87,11 @@ document.addEventListener("visibilitychange", () => {
|
|
|
87
87
|
});
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
Decode the track (and every sfx clip) while the **menu** is up, not on PLAY:
|
|
91
|
+
`loadAsync` runs `decodeAudioData`, which does not need a running `AudioContext` —
|
|
92
|
+
only playing does. `$genex-ai-sfx` has the preload-everything recipe; run the
|
|
93
|
+
music through the same pass so the first second of gameplay waits on nothing.
|
|
94
|
+
|
|
90
95
|
**Menu reuse:** the same `music` object at lower volume (`setVolume(0.18)`)
|
|
91
96
|
during the menu phase, restored to the setting's value on PLAY — zero extra
|
|
92
97
|
generations. If the loop point is audible (the track came back with an
|
|
@@ -98,9 +103,11 @@ intro/outro despite the prompt), crossfade the tail into the head with a second
|
|
|
98
103
|
A game with generated audio and no volume control is a failure mode. The
|
|
99
104
|
settings screen gets **two sliders — Music and SFX** (music default 0.30, sfx
|
|
100
105
|
~0.7), persisted (localStorage or the save slot) and applied live:
|
|
101
|
-
`music.setVolume(v)` for the music bed
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
`music.setVolume(v)` for the music bed, and one shared `sfxVolume` multiplier
|
|
107
|
+
every sound effect is scaled by (the `playSfx` / live-value recipes in
|
|
108
|
+
`$genex-ai-sfx` already read it — keep it a single exported value, never a
|
|
109
|
+
number copied into each call site). `$genex-threejs-game-ui` carries this as a
|
|
110
|
+
hard settings rule — wire the sliders in the same milestone as the track.
|
|
104
111
|
|
|
105
112
|
## Publish checklist
|
|
106
113
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: genex-ai-sfx
|
|
3
|
-
description: Generate a real sound effect (mp3) from a text prompt with `npx genex sfx`, then
|
|
3
|
+
description: Generate a real sound effect (mp3) from a text prompt with `npx genex sfx`, then wire it in Three.js — one-shots, rapid repeats, and a looping sound whose pitch and volume track a live value (engine, wheels, wind). Use for any specific sound triggered by a game event, and whenever audio must react continuously to gameplay without stuttering phones.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Genex AI · SFX
|
|
@@ -54,8 +54,105 @@ if (!sound.isPlaying) sound.play();
|
|
|
54
54
|
**Global** (UI/non-spatial — same volume everywhere): use `new THREE.Audio(listener)`
|
|
55
55
|
instead of `PositionalAudio` and don't attach it to a mesh.
|
|
56
56
|
|
|
57
|
-
Reuse one loaded `buffer` across many plays
|
|
58
|
-
|
|
57
|
+
Reuse one loaded `buffer` across many plays — the three recipes below are how.
|
|
58
|
+
|
|
59
|
+
## Never drive game sound through an `<audio>` element
|
|
60
|
+
|
|
61
|
+
`new Audio(url)`, `document.createElement("audio")`, an `<audio>` tag — same object,
|
|
62
|
+
all wrong here. On iPhone it is a **handle on the OS media pipeline in another
|
|
63
|
+
process**: writing `.volume`/`.playbackRate` is a **cross-process call** (a sound
|
|
64
|
+
driven from a live value writes 60–120 of those a second and stutters the whole
|
|
65
|
+
game), and `play()` is **async and slow**, so one-shots land after the event.
|
|
66
|
+
|
|
67
|
+
It disguises itself as a graphics problem: only while moving, identical at every
|
|
68
|
+
quality setting, desktop fine. The one-second tell — **the music plays throughout
|
|
69
|
+
and costs nothing**, same object and same code path, because its volume and pitch
|
|
70
|
+
never change. *A sound playing is free; a sound written to every frame is not.*
|
|
71
|
+
|
|
72
|
+
`THREE.Audio`/`PositionalAudio` are WebAudio and have none of this — use them for
|
|
73
|
+
everything, including UI clicks.
|
|
74
|
+
|
|
75
|
+
## Decode everything before the player needs it
|
|
76
|
+
|
|
77
|
+
A clip that hasn't finished decoding stalls on its first play — exactly the first
|
|
78
|
+
second of gameplay. **Decoding needs no running `AudioContext`, only playing does**,
|
|
79
|
+
so decode while the menu is up:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import * as THREE from "three";
|
|
83
|
+
|
|
84
|
+
const listener = new THREE.AudioListener();
|
|
85
|
+
camera.add(listener); // ONE listener serves every sound and the music
|
|
86
|
+
|
|
87
|
+
const CLIPS = {
|
|
88
|
+
jump: "https://assets.genex.technology/generations/<id>/audio-sfx",
|
|
89
|
+
wheels: "https://assets.genex.technology/generations/<id>/audio-sfx",
|
|
90
|
+
} as const;
|
|
91
|
+
type ClipName = keyof typeof CLIPS;
|
|
92
|
+
|
|
93
|
+
const buffers = new Map<ClipName, AudioBuffer>();
|
|
94
|
+
|
|
95
|
+
/** Call this while the menu/loading screen is up — no user gesture needed. */
|
|
96
|
+
export async function preloadAudio(): Promise<void> {
|
|
97
|
+
const loader = new THREE.AudioLoader();
|
|
98
|
+
await Promise.all(
|
|
99
|
+
Object.entries(CLIPS).map(async ([name, url]) => {
|
|
100
|
+
buffers.set(name as ClipName, await loader.loadAsync(url));
|
|
101
|
+
}),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Rapid repeats: pool the voices
|
|
107
|
+
|
|
108
|
+
Footsteps, hits and gunfire re-trigger faster than a clip ends, one `THREE.Audio`
|
|
109
|
+
plays once at a time, and a fresh one per shot allocates during combat. Keep a
|
|
110
|
+
small pool per clip and reuse whatever is idle:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const pool = new Map<ClipName, THREE.Audio[]>();
|
|
114
|
+
|
|
115
|
+
export function playSfx(name: ClipName, volume = 0.7): void {
|
|
116
|
+
const buffer = buffers.get(name);
|
|
117
|
+
if (!buffer) return; // still decoding — silence beats a stall
|
|
118
|
+
const voices = pool.get(name) ?? [];
|
|
119
|
+
let voice = voices.find((v) => !v.isPlaying);
|
|
120
|
+
if (!voice) {
|
|
121
|
+
voice = new THREE.Audio(listener);
|
|
122
|
+
voice.setBuffer(buffer);
|
|
123
|
+
voices.push(voice);
|
|
124
|
+
pool.set(name, voices);
|
|
125
|
+
}
|
|
126
|
+
voice.setVolume(volume * sfxVolume); // sfxVolume = the settings slider
|
|
127
|
+
voice.play();
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Positional one-shots: same pool built from `THREE.PositionalAudio`, `mesh.add(voice)`.
|
|
132
|
+
|
|
133
|
+
## A sound that tracks a live value
|
|
134
|
+
|
|
135
|
+
Engine, wheels, wind, rotor wash, a charging weapon — start the loop **once**, then
|
|
136
|
+
write to it every frame. Both calls below are AudioParam writes on the audio thread:
|
|
137
|
+
no allocation, no cross-process call.
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
const wheels = new THREE.Audio(listener);
|
|
141
|
+
wheels.setBuffer(buffers.get("wheels")!);
|
|
142
|
+
wheels.setLoop(true);
|
|
143
|
+
wheels.setVolume(0);
|
|
144
|
+
wheels.play(); // ONCE, from the first user gesture — never per frame
|
|
145
|
+
|
|
146
|
+
// in the render loop:
|
|
147
|
+
const speed01 = THREE.MathUtils.clamp(speed / topSpeed, 0, 1);
|
|
148
|
+
wheels.setVolume(speed01 * 0.6 * sfxVolume);
|
|
149
|
+
wheels.setPlaybackRate(0.8 + speed01 * 0.7); // pitch rises with speed
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
If the driving value can jump (a gear change, a hit, a respawn), a step is an
|
|
153
|
+
audible click — glide instead:
|
|
154
|
+
`wheels.gain.gain.setTargetAtTime(target, listener.context.currentTime, 0.05)`.
|
|
155
|
+
To silence a loop, ride the volume to 0 and leave it running — never `stop()`/`play()`.
|
|
59
156
|
|
|
60
157
|
## Publish checklist
|
|
61
158
|
|
|
@@ -86,3 +183,8 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
|
|
|
86
183
|
first play from a click/keydown. Confirm the camera has an `AudioListener`.
|
|
87
184
|
- **Too quiet/loud** — `sound.setVolume(0..1)`; for positional, tune
|
|
88
185
|
`setRefDistance` / `setRolloffFactor`.
|
|
186
|
+
- **Stutters on a phone while moving, at every quality setting** — audio, not
|
|
187
|
+
graphics: see the `<audio>` section above (`$genex-threejs-adaptive-quality` has
|
|
188
|
+
the same triage from the performance side).
|
|
189
|
+
- **A sound is late the first time, then fine** — it was still decoding. Preload
|
|
190
|
+
during the menu.
|
|
@@ -116,7 +116,7 @@ scene.background = texture; // keep the raw texture for the visible sky
|
|
|
116
116
|
|
|
117
117
|
## Troubleshooting
|
|
118
118
|
|
|
119
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
119
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
120
120
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
121
121
|
this skybox generation. Tell the user the facts the CLI printed: their balance, this
|
|
122
122
|
generation's cost, and when their credits refill. Then offer to continue the build
|
|
@@ -225,7 +225,7 @@ first one is the one a screenshot of the whole arena will not show you.
|
|
|
225
225
|
|
|
226
226
|
## Troubleshooting
|
|
227
227
|
|
|
228
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
228
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
229
229
|
- **"Out of credits" (`insufficient_credits`)** — the account has no credits left for
|
|
230
230
|
this texture generation. Tell the user the facts the CLI printed: their balance,
|
|
231
231
|
this generation's cost, and when their credits refill. Then offer to continue the
|
|
@@ -151,7 +151,7 @@ set belongs to `$genex-ai-hud` — both build on `npx genex image`/`video`.
|
|
|
151
151
|
|
|
152
152
|
## Troubleshooting
|
|
153
153
|
|
|
154
|
-
- **"Not authorized"** — run `npx @genex-ai/cli-demo@
|
|
154
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@dev init` first (it writes your `GENEX_TOKEN`).
|
|
155
155
|
- **"Prompt rejected"** — the provider's content-safety filter blocked the prompt.
|
|
156
156
|
This is non-retryable; retrying the same wording fails again. Rewrite the prompt.
|
|
157
157
|
- **Nothing plays / black surface** — the first `video.play()` must run inside a user
|
|
@@ -132,7 +132,7 @@ and re-link the clone to the same live game:
|
|
|
132
132
|
```bash
|
|
133
133
|
git clone <the game's repo url> my-game && cd my-game
|
|
134
134
|
npm install
|
|
135
|
-
npx @genex-ai/cli-demo@
|
|
135
|
+
npx @genex-ai/cli-demo@dev link <slug> # slug = the name in the play URL
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
Don't know the slug? **`npx genex list`** prints every game on your account —
|
|
@@ -159,7 +159,7 @@ Safe to run any time — genex-owned skills are refreshed to the latest version,
|
|
|
159
159
|
and your own files are never touched:
|
|
160
160
|
|
|
161
161
|
```bash
|
|
162
|
-
npx @genex-ai/cli-demo@
|
|
162
|
+
npx @genex-ai/cli-demo@dev init
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
Use `--force` only if you intentionally want your own existing files overwritten
|
|
@@ -17,6 +17,28 @@ This is a completion gate like the post stack: every game wires the tier at
|
|
|
17
17
|
boot before it is called done. It costs three lines, not a testing burden —
|
|
18
18
|
you still verify on desktop only.
|
|
19
19
|
|
|
20
|
+
## Arriving here because a phone stutters? Rule out audio first
|
|
21
|
+
|
|
22
|
+
Not every mobile performance problem is a rendering problem, and one impostor
|
|
23
|
+
costs whole afternoons — shadows, bloom, draw calls, physics and memory all get
|
|
24
|
+
investigated before anyone looks at the sound. Read these three symptoms
|
|
25
|
+
together before you touch a single quality knob:
|
|
26
|
+
|
|
27
|
+
- it only happens **while moving** (or while some value is continuously changing),
|
|
28
|
+
- it is **identical at every quality setting**, including the lowest,
|
|
29
|
+
- **desktop is fine.**
|
|
30
|
+
|
|
31
|
+
That is the signature of an `<audio>` element being written to every frame — on
|
|
32
|
+
iPhone each `.volume`/`.playbackRate` write is a call into another process. The
|
|
33
|
+
one-second confirmation: **the music plays throughout and costs nothing**, because
|
|
34
|
+
music's volume and pitch never change, so nothing is ever written to it. Same
|
|
35
|
+
object, same code path, opposite cost. `$genex-ai-sfx` has the fix and the
|
|
36
|
+
WebAudio recipes; nothing in this skill will help.
|
|
37
|
+
|
|
38
|
+
Genuine graphics symptoms look different: a quality setting changes it, it is
|
|
39
|
+
worse when more is on screen, and it degrades over minutes (thermal) rather than
|
|
40
|
+
tracking one input.
|
|
41
|
+
|
|
20
42
|
## Install
|
|
21
43
|
|
|
22
44
|
```bash
|
|
@@ -205,3 +227,6 @@ runtime).
|
|
|
205
227
|
- Testing quality tiers by resizing the desktop window → tiers key off touch +
|
|
206
228
|
OS, not viewport. Trust desktop verification plus the preflight report
|
|
207
229
|
`genex preview` prints.
|
|
230
|
+
- Stepping quality down to chase a stutter the quality settings never change →
|
|
231
|
+
that is the audio impostor above, and every rung you spend on it makes the
|
|
232
|
+
game uglier without touching the cause.
|
|
@@ -167,3 +167,9 @@ propeller: `finalThrottle` (0..1 mixer output — rotor-wash/audio intensity),
|
|
|
167
167
|
`worldThrustPos`/`worldThrustDir` (where and which way to emit), and
|
|
168
168
|
`thrustImpulse`. `drone.hoverThrottle` reads the last computed hover value.
|
|
169
169
|
All vectors are reused internal instances — copy, never mutate.
|
|
170
|
+
|
|
171
|
+
Driving rotor audio from `finalThrottle` is a per-frame write, so it must go
|
|
172
|
+
through WebAudio: start ONE looped `THREE.Audio` and call
|
|
173
|
+
`setVolume`/`setPlaybackRate` on it each frame — never an `<audio>` element,
|
|
174
|
+
whose `.volume`/`.playbackRate` writes are cross-process calls on iPhone and
|
|
175
|
+
stutter the whole game. `$genex-ai-sfx` has the live-value loop recipe.
|
|
@@ -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
|
```
|