@genex-ai/cli-demo 0.17.0 → 0.20.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 +10 -0
- package/dist/index.js +317 -58
- package/package.json +1 -1
- package/templates/skills/genex-ai-model/SKILL.md +6 -1
- package/templates/skills/genex-ai-sfx/SKILL.md +9 -0
- package/templates/skills/genex-ai-skybox/SKILL.md +9 -1
- package/templates/skills/genex-ai-texture/SKILL.md +11 -1
- package/templates/skills/genex-getting-started/SKILL.md +44 -0
- package/templates/skills/genex-threejs-atmosphere-aerial-perspective/SKILL.md +4 -0
- package/templates/skills/genex-threejs-bloom/references/bloom.md +3 -1
- package/templates/skills/genex-threejs-camera-direction/SKILL.md +6 -0
- package/templates/skills/genex-threejs-camera-direction/references/camera-rigs.md +2 -2
- package/templates/skills/genex-threejs-character-controller/SKILL.md +6 -1
- package/templates/skills/genex-threejs-embed-auth/SKILL.md +9 -1
- package/templates/skills/genex-threejs-game-feel/SKILL.md +97 -0
- package/templates/skills/genex-threejs-game-ui/SKILL.md +119 -0
- package/templates/skills/genex-threejs-image-pipeline/references/image-pipeline.md +2 -0
- package/templates/skills/genex-threejs-multiplayer/SKILL.md +10 -5
- package/templates/skills/genex-threejs-multiplayer/references/realtime-patterns.md +16 -5
- package/templates/skills/genex-threejs-physics-rapier/SKILL.md +2 -1
- package/templates/skills/genex-threejs-physics-rapier/references/colliders-from-assets.md +1 -1
- package/templates/skills/genex-threejs-procedural-animation/references/procedural-motion.md +8 -8
- package/templates/skills/genex-threejs-screen-space-ambient-occlusion/SKILL.md +1 -1
- package/templates/skills/genex-threejs-screen-space-ambient-occlusion/references/ambient-occlusion.md +2 -0
- package/templates/skills/genex-threejs-shadow-systems/SKILL.md +6 -1
- package/templates/skills/genex-threejs-shadow-systems/references/shadow-systems.md +2 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +23 -3
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +30 -13
- package/templates/skills/genex-threejs-spectral-ocean/SKILL.md +2 -0
- package/templates/skills/genex-threejs-spectral-ocean/references/spectral-ocean.md +2 -0
- package/templates/skills/genex-threejs-vehicle-controllers/SKILL.md +24 -0
- package/templates/skills/genex-threejs-vehicle-controllers/references/car.md +30 -2
- package/templates/skills/genex-threejs-vehicle-controllers/references/drone.md +21 -2
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +15 -0
- package/templates/skills/genex-threejs-volumetric-clouds/SKILL.md +4 -0
- package/templates/skills/genex-threejs-water-optics/references/water-optics.md +3 -1
|
@@ -58,7 +58,7 @@ Use Three.js `GLTFLoader` (the game serves at the domain root
|
|
|
58
58
|
`https://<slug>.genex.technology/`, so relative or absolute paths both work):
|
|
59
59
|
|
|
60
60
|
```ts
|
|
61
|
-
import { GLTFLoader } from "three/
|
|
61
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
62
62
|
|
|
63
63
|
const loader = new GLTFLoader();
|
|
64
64
|
const gltf = await loader.loadAsync("./assets/models/weathered-wooden-barrel.glb");
|
|
@@ -71,6 +71,11 @@ scene.add(model);
|
|
|
71
71
|
To place many copies, `model.clone()` per instance. For animated GLBs, drive
|
|
72
72
|
`gltf.animations` with a `THREE.AnimationMixer`.
|
|
73
73
|
|
|
74
|
+
**If the game has physics, give the model a collider** — a GLB added only to the
|
|
75
|
+
scene is a ghost: players and objects pass straight through it.
|
|
76
|
+
`$genex-threejs-physics-rapier` has the decision table
|
|
77
|
+
(`collidersFromObject(model, "hull")` is the default for props).
|
|
78
|
+
|
|
74
79
|
## Publish checklist (so players see the model)
|
|
75
80
|
|
|
76
81
|
- Reference assets with **relative** paths (`./assets/...`); absolute
|
|
@@ -58,8 +58,17 @@ Reuse one loaded `buffer` across many plays; create a fresh `Audio`/`PositionalA
|
|
|
58
58
|
- Relative path `./assets/sfx/...`; `base: "./"` in `vite.config.ts`; files live under
|
|
59
59
|
`public/assets/` so Vite ships them; `npx genex publish` ships them to R2.
|
|
60
60
|
|
|
61
|
+
## Options
|
|
62
|
+
|
|
63
|
+
- `--duration <sec>` — target length 0.5–22 (shown in Run above); omit to let
|
|
64
|
+
the model pick.
|
|
65
|
+
- `--no-wait` — enqueue and return immediately (the file won't be downloaded;
|
|
66
|
+
re-run without `--no-wait` to fetch it).
|
|
67
|
+
- `--api-url <url>` — override the API base (local dev).
|
|
68
|
+
|
|
61
69
|
## Troubleshooting
|
|
62
70
|
|
|
71
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
63
72
|
- **No sound** — the `AudioContext` is suspended until a user gesture; trigger the
|
|
64
73
|
first play from a click/keydown. Confirm the camera has an `AudioListener`.
|
|
65
74
|
- **Too quiet/loud** — `sound.setVolume(0..1)`; for positional, tune
|
|
@@ -56,7 +56,8 @@ const pmrem = new THREE.PMREMGenerator(renderer);
|
|
|
56
56
|
const envMap = pmrem.fromEquirectangular(texture).texture;
|
|
57
57
|
scene.environment = envMap;
|
|
58
58
|
scene.background = texture; // keep the raw texture for the visible sky
|
|
59
|
-
|
|
59
|
+
// Do NOT dispose `texture` here — it IS the visible background. Dispose it only
|
|
60
|
+
// if you later stop using it as the sky.
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
## Publish checklist
|
|
@@ -66,8 +67,15 @@ texture.dispose; // (dispose the PMREM source later if you stop using it)
|
|
|
66
67
|
- Generated files live under `public/assets/` so Vite ships them; `npx genex publish`
|
|
67
68
|
ships that folder with the game.
|
|
68
69
|
|
|
70
|
+
## Options
|
|
71
|
+
|
|
72
|
+
- `--no-wait` — enqueue and return immediately (the file won't be downloaded;
|
|
73
|
+
re-run without `--no-wait` to fetch it).
|
|
74
|
+
- `--api-url <url>` — override the API base (local dev).
|
|
75
|
+
|
|
69
76
|
## Troubleshooting
|
|
70
77
|
|
|
78
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
71
79
|
- **Sky looks too dark/bright** — adjust `renderer.toneMappingExposure`, or scale
|
|
72
80
|
`scene.environment` influence via material `envMapIntensity`.
|
|
73
81
|
- **Seam/pole artifacts** — that's inherent to equirect images; keep the camera
|
|
@@ -41,8 +41,10 @@ Committed by `npx genex publish`, so it ships with your game.
|
|
|
41
41
|
```ts
|
|
42
42
|
import * as THREE from "three";
|
|
43
43
|
|
|
44
|
+
// The extension varies by provider (production emits .png) — use the EXACT
|
|
45
|
+
// path the CLI printed when generation finished, don't assume .jpg.
|
|
44
46
|
const map = await new THREE.TextureLoader().loadAsync(
|
|
45
|
-
"./assets/textures/lush-green-grass/basecolor.
|
|
47
|
+
"./assets/textures/lush-green-grass/basecolor.png",
|
|
46
48
|
);
|
|
47
49
|
map.colorSpace = THREE.SRGBColorSpace;
|
|
48
50
|
map.wrapS = map.wrapT = THREE.RepeatWrapping;
|
|
@@ -70,8 +72,16 @@ scene.add(ground);
|
|
|
70
72
|
- Relative path `./assets/textures/...`; `base: "./"` in `vite.config.ts`; files live under
|
|
71
73
|
`public/assets/` so Vite ships them; `npx genex publish` ships them to R2.
|
|
72
74
|
|
|
75
|
+
## Options
|
|
76
|
+
|
|
77
|
+
- `--terrain` — seamless tiling tuned for ground/terrain (shown in Run above).
|
|
78
|
+
- `--no-wait` — enqueue and return immediately (the file won't be downloaded;
|
|
79
|
+
re-run without `--no-wait` to fetch it).
|
|
80
|
+
- `--api-url <url>` — override the API base (local dev).
|
|
81
|
+
|
|
73
82
|
## Troubleshooting
|
|
74
83
|
|
|
84
|
+
- **"Not authorized"** — run `npx @genex-ai/cli-demo@latest init` first (it writes your `GENEX_TOKEN`).
|
|
75
85
|
- **Visible tiling seams** — use `--terrain` (tuned for seamless edges), lower the
|
|
76
86
|
`repeat`, or blend two textures. Perfect seamlessness is a known v1 limitation.
|
|
77
87
|
- **Colors look washed/dark** — ensure `map.colorSpace = THREE.SRGBColorSpace`.
|
|
@@ -45,6 +45,30 @@ npx genex texture "mossy cobblestone" --terrain # a tiling surface texture
|
|
|
45
45
|
Each has a focused skill with the exact loader code — `$genex-ai-model`,
|
|
46
46
|
`$genex-ai-skybox`, `$genex-ai-sfx`, `$genex-ai-texture`.
|
|
47
47
|
|
|
48
|
+
## Identity & saves (every game)
|
|
49
|
+
|
|
50
|
+
Every Genex game ships with built-in player identity: sign-in, guests, per-player
|
|
51
|
+
saves, a shared world slot, and leaderboards — all through `@genex-ai/embed-sdk`.
|
|
52
|
+
Load `$genex-threejs-embed-auth` before writing boot code; it wires the one-call
|
|
53
|
+
setup (`initEmbed` + `waitForPlayer`) and is also where multiplayer gets its
|
|
54
|
+
connection auth. A game without it loses all progress on refresh.
|
|
55
|
+
|
|
56
|
+
## Loading screens (recommendation)
|
|
57
|
+
|
|
58
|
+
Generated assets take a moment to fetch at runtime. Show a simple loading
|
|
59
|
+
screen or progress hint while models/textures/sounds load, and remove it when
|
|
60
|
+
the game is ready — players should never stare at a black screen. This is a
|
|
61
|
+
recommendation, not a rule: even a one-line "Loading…" overlay over the dark
|
|
62
|
+
page background is enough.
|
|
63
|
+
|
|
64
|
+
## Remixing an existing game
|
|
65
|
+
|
|
66
|
+
If the project folder already contains a game (a remix or any existing
|
|
67
|
+
project), don't scaffold a new app over it: read the existing code first and
|
|
68
|
+
keep its renderer, structure, and conventions — extend, don't rebuild.
|
|
69
|
+
Running `genex init` in such a folder only wires identity/publishing and
|
|
70
|
+
refreshes the genex-owned skills; the game's own files are never touched.
|
|
71
|
+
|
|
48
72
|
## Publishing
|
|
49
73
|
|
|
50
74
|
Before your first `genex preview`, the play URL (`https://<slug>.genex.technology/`)
|
|
@@ -66,6 +90,26 @@ If unsure, use `games` (also the server's fallback for anything unrecognized).
|
|
|
66
90
|
Your own files were left untouched. `genex init` only adds missing files and
|
|
67
91
|
refreshes the genex-owned ones.
|
|
68
92
|
|
|
93
|
+
## Reconnecting an existing game (`genex link`)
|
|
94
|
+
|
|
95
|
+
Each game's connection to its live page is folder-local (`.genex/project.json`
|
|
96
|
+
+ the `genex_key` deploy key). If that folder is gone — deleted, or the game
|
|
97
|
+
was built on another machine — **don't run `init` to "recover" it**: that
|
|
98
|
+
creates a brand-new game at a new URL. Instead, clone the game's source repo
|
|
99
|
+
and re-link the clone to the same live game:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone <the game's repo url> my-game && cd my-game
|
|
103
|
+
npm install
|
|
104
|
+
npx @genex-ai/cli-demo@latest link <slug> # slug = the name in the play URL
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`link` never creates a project: it signs in if needed (the browser opens once),
|
|
108
|
+
registers a fresh deploy key for this folder, and rewrites the local link.
|
|
109
|
+
After it, `npx genex preview` / `npx genex publish` update the **same live
|
|
110
|
+
game** — plays, likes, and comments stay. It also fixes a folder whose
|
|
111
|
+
authorization went stale ("Not authorized" from `preview`/`publish`).
|
|
112
|
+
|
|
69
113
|
## Re-running setup
|
|
70
114
|
|
|
71
115
|
Safe to run any time — genex-owned skills are refreshed to the latest version,
|
|
@@ -5,6 +5,10 @@ description: Implement sky and aerial perspective for Genex Three.js games. Use
|
|
|
5
5
|
|
|
6
6
|
# Genex Three.js Atmosphere And Aerial Perspective
|
|
7
7
|
|
|
8
|
+
**Static sky at a fixed time of day → `$genex-ai-skybox` instead** (one command,
|
|
9
|
+
a real 360° image + lighting). This skill is for dynamic, animated, or
|
|
10
|
+
scale-transitioning skies (time-of-day, ground-to-space).
|
|
11
|
+
|
|
8
12
|
Treat sky rendering and aerial perspective as two views of the same scattering model. They must share radii, density profiles, coefficients, sun direction, exposure scale, and coordinate transforms.
|
|
9
13
|
|
|
10
14
|
## Choose the implementation tier
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Use this reference to choose bloom ownership, signal order, selective contribution, and scene-relative emissive ranges without making bloom responsible for the underlying form.
|
|
4
4
|
|
|
5
|
+
> **Renderer note:** this reference assumes `WebGPURenderer` + TSL node materials. Check the project's actual renderer first — the Genex scaffold ships vanilla WebGL three.js. On WebGL, adapt the technique with standard materials / `EffectComposer` passes or pick a simpler alternative; never switch renderers mid-project.
|
|
6
|
+
|
|
5
7
|
## Contents
|
|
6
8
|
|
|
7
9
|
- production WebGPU pipeline signal order
|
|
@@ -119,7 +121,7 @@ Composer order:
|
|
|
119
121
|
scene -> SSAO -> volumetrics -> bloom -> lens flare -> fog/color
|
|
120
122
|
```
|
|
121
123
|
|
|
122
|
-
This is a useful comparison, not the quality target. The threshold is very low
|
|
124
|
+
This is a useful comparison, not the quality target — note its fog/color stage runs AFTER bloom, deviating from the canonical order in $genex-threejs-image-pipeline (fog belongs before bloom so bright fog does not glow). The threshold is very low
|
|
123
125
|
and can bloom ordinary bright surfaces. The wrapper exposes only enabled,
|
|
124
126
|
strength, and threshold, while radius stays at its constructor value.
|
|
125
127
|
|
|
@@ -5,6 +5,12 @@ description: Design advanced Three.js camera systems for Genex browser games. Us
|
|
|
5
5
|
|
|
6
6
|
# Genex Three.js Camera Direction
|
|
7
7
|
|
|
8
|
+
**Boundary:** the pack's default third-person camera is the bundled
|
|
9
|
+
`FollowCamera` (see `$genex-threejs-character-controller` — the vehicle
|
|
10
|
+
controllers hand off to it too). Extend and tune that first; load this skill
|
|
11
|
+
for rigs beyond it — top-down/side views, authored shots, cinematics, pointer
|
|
12
|
+
look, and camera handoffs.
|
|
13
|
+
|
|
8
14
|
Treat the camera as an authored visual system, not a passive viewport. Compose
|
|
9
15
|
the subject, establish scale, choose a stable up frame, and make every mode
|
|
10
16
|
handoff explicit.
|
|
@@ -289,8 +289,8 @@ Keys are cleared on:
|
|
|
289
289
|
|
|
290
290
|
Scene-specific constraints then run after controls:
|
|
291
291
|
|
|
292
|
-
-
|
|
293
|
-
-
|
|
292
|
+
- an interior room scene clamps X/Y/Z with floor, ceiling, and wall clearance.
|
|
293
|
+
- a terrain scene clamps camera Y above sampled terrain plus `0.2`.
|
|
294
294
|
- cinematic scenes block movement keys while retaining their authored camera.
|
|
295
295
|
|
|
296
296
|
Input control and spatial constraint are separate layers.
|
|
@@ -139,7 +139,12 @@ when done (punch-on-click is the default). Read
|
|
|
139
139
|
wiring, the **full 46-clip catalog** with genre hints, `playOneShot` options,
|
|
140
140
|
overrides, foot IK, and remote-player animation.
|
|
141
141
|
|
|
142
|
-
## Mobile: TouchJoystick + VirtualButton
|
|
142
|
+
## Mobile: TouchJoystick + VirtualButton (wire by default)
|
|
143
|
+
|
|
144
|
+
**Wire these whenever this controller is installed — not only when asked.**
|
|
145
|
+
Published games get opened on phones from shared links; the bundled touch
|
|
146
|
+
controls are ~6 lines and invisible on desktop. (Designing phone-specific
|
|
147
|
+
layouts or testing mobile viewports stays ask-only.)
|
|
143
148
|
|
|
144
149
|
```ts
|
|
145
150
|
import { TouchJoystick, VirtualButton } from "./controllers/character/touch-joystick.ts";
|
|
@@ -138,7 +138,11 @@ session is blocked):
|
|
|
138
138
|
- `getLeaderboard({ board?, limit?, order? }?)` → `Promise<{ items, me }>` —
|
|
139
139
|
top entries (verified display names — never trust client-side name input
|
|
140
140
|
for this) + the signed-in player's own `{ rank, score }`. Works for guests
|
|
141
|
-
too (`me: null`).
|
|
141
|
+
too (`me: null`). `limit` caps at 100 server-side.
|
|
142
|
+
|
|
143
|
+
Server write limits (per player, per minute): **60 player-saves, 120
|
|
144
|
+
world-saves, 30 score submits**. A debounced ~1/sec checkpoint never gets near
|
|
145
|
+
them — only a save-per-frame loop does (it surfaces as HTTP 429).
|
|
142
146
|
|
|
143
147
|
From `@genex-ai/embed-sdk/sentry` (crash reporting; exactly these two):
|
|
144
148
|
|
|
@@ -298,6 +302,10 @@ concerns.
|
|
|
298
302
|
header (401), a guest token (403 `guest_no_save`), or the token belongs to a
|
|
299
303
|
different game (403). All three mean the code bypassed the SDK helpers —
|
|
300
304
|
switch to `savePlayerState`/`saveWorldState`, which handle every case.
|
|
305
|
+
- **Saves or score submits return 429** — writing too often (per-player limits:
|
|
306
|
+
60 player-saves/min, 120 world-saves/min, 30 score submits/min). Debounce to
|
|
307
|
+
~1 write/sec at checkpoints and never save in the render loop; back off a
|
|
308
|
+
second and retry.
|
|
301
309
|
- **`saveWorldState` resolves `{ conflict: true }`** — another player wrote the
|
|
302
310
|
shared slot since your last read. Expected under concurrency: reload with
|
|
303
311
|
`loadWorldState()`, merge, retry with the fresh `version`. If it happens
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-threejs-game-feel
|
|
3
|
+
description: Make a Genex Three.js game feel alive — instant input response, acceleration curves, camera follow and impact shake, layered hit feedback, hitstop, readable cooldowns, difficulty ramp, and a fast fail/retry loop. Use when a game technically works but feels flat, floaty, or boring, and before calling any playable loop done.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Three.js Game Feel
|
|
7
|
+
|
|
8
|
+
"Works" and "feels good" are different finish lines. This skill is the second
|
|
9
|
+
one: the tuning pass that turns a functioning loop into a game people replay.
|
|
10
|
+
Run it once the mechanics exist — feel tuning before mechanics is polish on
|
|
11
|
+
nothing.
|
|
12
|
+
|
|
13
|
+
## The loop test (run this first)
|
|
14
|
+
|
|
15
|
+
A playable loop has four parts. If any is missing, fix that before tuning:
|
|
16
|
+
|
|
17
|
+
1. **Verb** — something the player does (move, jump, shoot, grab).
|
|
18
|
+
2. **Objective** — something to chase (score, survive, reach, beat).
|
|
19
|
+
3. **Feedback** — success and damage are unmistakable the moment they happen.
|
|
20
|
+
4. **Fail / retry** — the game can be lost, and restarting is instant.
|
|
21
|
+
|
|
22
|
+
A game that can't be lost — or takes more than a second to retry — gets boring
|
|
23
|
+
no matter how good it looks.
|
|
24
|
+
|
|
25
|
+
## Input: respond this frame
|
|
26
|
+
|
|
27
|
+
- Act on input the **same frame** it arrives. Animation follows the action; it
|
|
28
|
+
never gates it (the character moves now, the run cycle catches up).
|
|
29
|
+
- Add forgiveness windows: buffer a jump pressed ~100 ms early, allow a jump
|
|
30
|
+
~100 ms after walking off a ledge. Players read forgiveness as "tight
|
|
31
|
+
controls", not as cheating. (The bundled `CharacterController` ships with
|
|
32
|
+
NO input buffer or coyote time — add these in `character-controller.ts`
|
|
33
|
+
itself, or apply them only to hand-rolled movement.)
|
|
34
|
+
- If an action can't fire (cooldown, no ammo), say so instantly — a click, a
|
|
35
|
+
dimmed icon — silence reads as broken input.
|
|
36
|
+
|
|
37
|
+
## Movement: snappy beats realistic
|
|
38
|
+
|
|
39
|
+
- Reach max speed fast; stop even faster (deceleration stronger than
|
|
40
|
+
acceleration). Long ease-in reads as "floaty" — the most common complaint.
|
|
41
|
+
- Tune with live numbers, not rebuilds: keep the constants (accel, decel, max
|
|
42
|
+
speed, jump impulse) in one place and tweak while replaying the same ten
|
|
43
|
+
seconds.
|
|
44
|
+
- On-foot and vehicle movement ship pre-tuned in
|
|
45
|
+
`$genex-threejs-character-controller` and
|
|
46
|
+
`$genex-threejs-vehicle-controllers` — retune their exposed knobs before
|
|
47
|
+
writing new movement math.
|
|
48
|
+
|
|
49
|
+
## Camera: the invisible half of feel
|
|
50
|
+
|
|
51
|
+
- Smooth follow with **lookahead** — bias the camera toward where the player
|
|
52
|
+
is going, never behind the action hiding the next decision.
|
|
53
|
+
- FOV kick on speed/boost (a few degrees, eased) sells acceleration better
|
|
54
|
+
than particles.
|
|
55
|
+
- Impact shake: short (< 0.2 s), small, decaying, and always **caused** —
|
|
56
|
+
shake per event, never ambient. Rigs and handoffs live in
|
|
57
|
+
`$genex-threejs-camera-direction`.
|
|
58
|
+
- Feel work (shake, FOV kick, hitstop triggers) runs in the **render phase,
|
|
59
|
+
after `physics.step(...)`** — never inside `onBeforeStep` (that's fixed-step
|
|
60
|
+
physics territory; see the physics skill's ordering contract).
|
|
61
|
+
|
|
62
|
+
## Impact: layer the feedback
|
|
63
|
+
|
|
64
|
+
Every meaningful event the player caused should be **seen and heard**. For big
|
|
65
|
+
hits, layer two or three of:
|
|
66
|
+
|
|
67
|
+
- a flash or scale pop on the thing that was hit;
|
|
68
|
+
- a particle burst — `$genex-threejs-procedural-vfx`;
|
|
69
|
+
- a sound — generate real ones with `npx genex sfx` (`$genex-ai-sfx`);
|
|
70
|
+
- **hitstop**: freeze the simulation 30–80 ms on heavy impacts (skip rendering
|
|
71
|
+
pauses — just hold the physics/game clock). Longer than ~100 ms feels like lag.
|
|
72
|
+
With the bundled physics pack this is built in — `physics.paused = true` /
|
|
73
|
+
`physics.timeScale = 0.2` (see the physics skill's pause/slow-mo section) plus
|
|
74
|
+
`anims.setPaused(true)` / `anims.setTimeScale(0.2)` for the character's
|
|
75
|
+
animations. Don't hand-roll a second clock.
|
|
76
|
+
|
|
77
|
+
One layer per small event, three for the biggest — uniform intensity flattens
|
|
78
|
+
everything back out.
|
|
79
|
+
|
|
80
|
+
## Pacing and fail/retry
|
|
81
|
+
|
|
82
|
+
- Show cooldowns and charges visibly (a refilling ring beats a hidden timer).
|
|
83
|
+
- Ramp difficulty measurably (speed, spawn rate, tighter windows) so the
|
|
84
|
+
player feels progress; reward near-misses where the design allows.
|
|
85
|
+
- Death should teach: show what killed you, keep the score/best visible, and
|
|
86
|
+
restart on ONE key without a page reload. The faster the retry, the more
|
|
87
|
+
"one more try" the game becomes.
|
|
88
|
+
|
|
89
|
+
## Failure modes to catch
|
|
90
|
+
|
|
91
|
+
- A mechanic fires with zero feedback — it might as well not have happened.
|
|
92
|
+
- The camera hides what the player needs to react to next.
|
|
93
|
+
- The game cannot be lost, or losing dumps you to a dead screen.
|
|
94
|
+
- Restart requires reloading the page (kills the retry loop — and reloads
|
|
95
|
+
re-run auth and asset loading).
|
|
96
|
+
- Shake/flash spam with no cause — feedback inflation reads as noise.
|
|
97
|
+
- Feel constants scattered through the code where nobody dares touch them.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-threejs-game-ui
|
|
3
|
+
description: Design the 2D interface of a Genex Three.js game — HUD, menus, pause/win/lose screens, score and health displays, overlays, loading screens, and UI state flow. Use whenever the game needs on-screen text, meters, buttons, or menus, or when the interface reads as a generic dashboard, covers the action, or shifts around as numbers change.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Three.js Game UI
|
|
7
|
+
|
|
8
|
+
The interface is part of the game, not chrome around it. A strong HUD tells the
|
|
9
|
+
player what to protect, what to chase, and what just happened — without ever
|
|
10
|
+
stealing attention from the scene. This skill covers the 2D layer of a vanilla
|
|
11
|
+
Three.js game: architecture, the states every game needs, readability rules,
|
|
12
|
+
and wiring UI to game state.
|
|
13
|
+
|
|
14
|
+
## Architecture: DOM overlay by default
|
|
15
|
+
|
|
16
|
+
Genex games are plain Vite + Three.js apps, so the default UI layer is a DOM
|
|
17
|
+
overlay — HTML/CSS on top of the canvas, not text sprites inside the scene:
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<div id="ui"> <!-- fixed, full-screen, pointer-events: none -->
|
|
21
|
+
<div id="hud">…</div>
|
|
22
|
+
<!-- data-phase must exactly match a phase name in setPhase() below — that's how
|
|
23
|
+
screens toggle. A phase with no screen (e.g. "playing") just hides them all. -->
|
|
24
|
+
<div id="screen-loading" class="screen" data-phase="loading">…</div>
|
|
25
|
+
<div id="screen-pause" class="screen" data-phase="paused" hidden>…</div>
|
|
26
|
+
<div id="screen-over" class="screen" data-phase="over" hidden>…</div>
|
|
27
|
+
</div>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```css
|
|
31
|
+
#ui { position: fixed; inset: 0; pointer-events: none; font-variant-numeric: tabular-nums; }
|
|
32
|
+
#ui button, #ui .screen { pointer-events: auto; }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`pointer-events: none` on the root keeps the canvas playable; re-enable it only
|
|
36
|
+
on elements that are actually clickable. Keep ALL layout in CSS — never
|
|
37
|
+
position UI by mutating inline pixel styles per frame. In-world (diegetic) UI —
|
|
38
|
+
a health bar floating over an enemy, a scoreboard mesh in a stadium — is the
|
|
39
|
+
exception for things that belong to the world, not the default.
|
|
40
|
+
|
|
41
|
+
## The states every game needs
|
|
42
|
+
|
|
43
|
+
Build these as show/hide layers over one state machine, not as ad-hoc DOM
|
|
44
|
+
edits scattered through the code:
|
|
45
|
+
|
|
46
|
+
1. **Loading** — visible until assets are ready; players must never stare at a
|
|
47
|
+
black screen. Even "Loading… 3/5" over the dark background is enough.
|
|
48
|
+
2. **Playing HUD** — the minimal always-on layer (see hierarchy below).
|
|
49
|
+
3. **Pause** — freeze the loop, dim the scene, show resume/restart. (With the
|
|
50
|
+
bundled physics pack, freezing is built in: `physics.paused = true` plus
|
|
51
|
+
`anims.setPaused(true)` — don't hand-roll a second clock.)
|
|
52
|
+
4. **Fail / retry** — what happened, the score, and a ONE-KEY instant restart
|
|
53
|
+
(show which key). Restart must not reload the page.
|
|
54
|
+
5. **Win / next** — celebrate, then offer the next thing to do.
|
|
55
|
+
6. **Identity moments** — the player's name/guest identity comes from
|
|
56
|
+
`$genex-threejs-embed-auth` (`waitForPlayer()`); leaderboards render from
|
|
57
|
+
`getLeaderboard()`. Never invent your own login UI.
|
|
58
|
+
7. **Multiplayer lobby/points** — matchmaking status, countdowns, and match
|
|
59
|
+
HUD state come from `$genex-threejs-multiplayer`; render what the SDK
|
|
60
|
+
reports, don't guess at it.
|
|
61
|
+
|
|
62
|
+
## Hierarchy: if everything shouts, nothing reads
|
|
63
|
+
|
|
64
|
+
Order the HUD by what the player loses the game for ignoring:
|
|
65
|
+
|
|
66
|
+
1. **Survival/status** — health, time, fuel: biggest, most stable, edge/corner.
|
|
67
|
+
2. **Objective** — score, laps, wave: prominent but calmer.
|
|
68
|
+
3. **Moment feedback** — "+100" popups, damage flashes: transient, near the
|
|
69
|
+
action, gone in under a second.
|
|
70
|
+
4. **Flavor** — combo names, taunts: smallest, skippable.
|
|
71
|
+
|
|
72
|
+
## Readability rules
|
|
73
|
+
|
|
74
|
+
- **Never cover the player or incoming threats.** Corners and edges belong to
|
|
75
|
+
UI; the center of the screen belongs to the game.
|
|
76
|
+
- **Stable layout.** A score ticking 9 → 10 → 100 must not reflow anything:
|
|
77
|
+
tabular numerals (`font-variant-numeric: tabular-nums`), fixed-width slots,
|
|
78
|
+
meters that change fill — not size.
|
|
79
|
+
- **Contrast against the real scene.** Test text over the brightest AND
|
|
80
|
+
darkest areas of actual gameplay; a soft dark plate or text-shadow beats
|
|
81
|
+
restyling per level.
|
|
82
|
+
- **Desktop first.** Verify at desktop sizes and survive window resizes
|
|
83
|
+
without clipping; don't design phone layouts or test mobile viewports unless
|
|
84
|
+
the user asks. Exception: when a bundled controller (character/car/drone) is
|
|
85
|
+
installed, DO wire its ready-made touch controls behind
|
|
86
|
+
`navigator.maxTouchPoints > 0` — invisible on desktop, and the shared link
|
|
87
|
+
isn't dead on a phone (the controller skill has the wiring).
|
|
88
|
+
|
|
89
|
+
## Wire UI to game state, never the reverse
|
|
90
|
+
|
|
91
|
+
The game state machine is the single source of truth; the UI renders it.
|
|
92
|
+
Don't keep a second copy of rules in the UI layer (a timer in the HUD and a
|
|
93
|
+
timer in the game WILL drift apart). Buttons and menu keys emit the same
|
|
94
|
+
intents the gameplay input path uses — a "Restart" button and the R key must
|
|
95
|
+
run identical code.
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Each .screen carries data-phase (see the HTML block above); the names must match exactly.
|
|
99
|
+
function setPhase(phase: "loading" | "playing" | "paused" | "over" | "won") {
|
|
100
|
+
for (const s of document.querySelectorAll<HTMLElement>("#ui .screen"))
|
|
101
|
+
s.hidden = s.dataset.phase !== phase;
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
UI sounds (clicks, confirms, win stingers) are one `npx genex sfx` away — see
|
|
106
|
+
`$genex-ai-sfx`.
|
|
107
|
+
|
|
108
|
+
## Failure modes to catch before the player does
|
|
109
|
+
|
|
110
|
+
- A generic stat dashboard (rows of labels + numbers) instead of a designed
|
|
111
|
+
HUD — pick the 2–3 numbers that matter and style them by hierarchy.
|
|
112
|
+
- UI panels covering the player or the thing about to kill them.
|
|
113
|
+
- Layout shifting as numbers grow.
|
|
114
|
+
- A fail state with no visible restart key, or a restart that reloads the page.
|
|
115
|
+
- Buttons that render but don't emit the game's real input intents.
|
|
116
|
+
- UI logic duplicating game rules and drifting out of sync.
|
|
117
|
+
|
|
118
|
+
Before calling UI done, check it over real gameplay footage at desktop size —
|
|
119
|
+
`$genex-threejs-visual-validation` has the capture discipline.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Use this reference to compose shared scene buffers, lighting effects, atmosphere, bloom, exposure, tone mapping, grading, and feature-local render targets with explicit ownership.
|
|
4
4
|
|
|
5
|
+
> **Renderer note:** this reference assumes `WebGPURenderer` + TSL node materials. Check the project's actual renderer first — the Genex scaffold ships vanilla WebGL three.js. On WebGL, adapt the technique with standard materials / `EffectComposer` passes or pick a simpler alternative; never switch renderers mid-project.
|
|
6
|
+
|
|
5
7
|
## Contents
|
|
6
8
|
|
|
7
9
|
- production WebGPU pipeline WebGPU graph
|
|
@@ -37,7 +37,7 @@ example, the shared-object/ball code, rotation, and host usage. Read
|
|
|
37
37
|
npm i @genex-ai/multiplayer
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
This skill targets `@genex-ai/multiplayer` **≥ 0.8.0** (`objects`/`host` since 0.4; `matchmake()` since 0.5; presets + `score()`/`finish()` since 0.6; `createPrivate()`/`joinPrivate()` since 0.7; auto-reconnect + `inputs`/`onHostTick` since 0.8).
|
|
40
|
+
This skill targets `@genex-ai/multiplayer` **≥ 0.8.0** (`objects`/`host` since 0.4; `matchmake()` since 0.5; presets + `score()`/`finish()` since 0.6; `createPrivate()`/`joinPrivate()` since 0.7; matchmake auto-retry + `retry()` since 0.7.1; auto-reconnect + `inputs`/`onHostTick` since 0.8).
|
|
41
41
|
|
|
42
42
|
## Trust model (say it plainly in your game's copy)
|
|
43
43
|
|
|
@@ -60,11 +60,13 @@ HUD from `mm.matchmaking` (its `status`, `queue.position`, `players`/`opponents`
|
|
|
60
60
|
// Pass auth as a FUNCTION — one matchmake() handle re-joins the queue/match many times (re-search,
|
|
61
61
|
// requeue) over a session, and embed tokens rotate (~10 min). A function is read fresh each (re)join;
|
|
62
62
|
// a static object goes stale and gets rejected mid-session.
|
|
63
|
+
await waitForPlayer(); // identity gate first (guest OR signed-in) — same rule as connect()
|
|
63
64
|
const mm = await matchmake<MyState>({ url, room: slug, auth: () => getColyseusAuth() });
|
|
64
65
|
mm.on('matched', () => {/* session is live — start the game */});
|
|
65
66
|
// each frame: if (mm.session) renderGame(mm.session); else renderSearchingHud(mm.matchmaking);
|
|
66
67
|
mm.on('matchEnded', ({ winnerId, scores, draw }) => {/* result screen */});
|
|
67
|
-
mm.on('error', (e) => {/*
|
|
68
|
+
mm.on('error', (e) => {/* queue join failed even after the SDK's automatic retries (backoff, ~10s)
|
|
69
|
+
— persistent, e.g. broken auth. Fix the cause, then call mm.retry(). */});
|
|
68
70
|
|
|
69
71
|
// Report ONLY your own outcome — the server adjudicates. Which call fits depends on the win condition:
|
|
70
72
|
mm.eliminated(); // I'm out (lastStanding)
|
|
@@ -73,8 +75,9 @@ mm.finish(); // I finished the race (firstToFinish)
|
|
|
73
75
|
```
|
|
74
76
|
|
|
75
77
|
Everything is **server-owned** — set once in `package.json` under `genex.matchmaking`, reported at
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
`genex preview` AND `genex publish` (removing it from package.json clears the stored config on the
|
|
79
|
+
next preview/publish); the client declares nothing. You never run matchmaking logic: the server
|
|
80
|
+
owns the queue, roles, winner-stays, forfeit, timeout, and the win condition.
|
|
78
81
|
|
|
79
82
|
```jsonc
|
|
80
83
|
"genex": {
|
|
@@ -188,7 +191,9 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
|
|
|
188
191
|
- `set(id, state)` — publish it (only lands while you own it; full flat object each call).
|
|
189
192
|
- `get(id)` → `{ id, owner, isMine, state, stateRaw }` or `undefined`. `state` is auto-smoothed
|
|
190
193
|
(or live if `isMine`); `stateRaw` is the raw latest.
|
|
191
|
-
- `release(id)` — give up ownership. `remove(id)` — destroy it (for transient bullets/pickups)
|
|
194
|
+
- `release(id)` — give up ownership. `remove(id)` — destroy it (for transient bullets/pickups);
|
|
195
|
+
**owner-only** — claim it first, or let the current owner remove it (the relay rejects a
|
|
196
|
+
non-owner's destroy).
|
|
192
197
|
- `ids()` — all object ids seen.
|
|
193
198
|
- `room.isHost` / `room.host` — you are (or who is) the elected authority. Use to pick the single
|
|
194
199
|
writer of `shared` scores/rounds and the single simulator of host-owned objects. Settles within
|
|
@@ -18,13 +18,20 @@ already did, and stacking two smoothers adds visible lag.
|
|
|
18
18
|
```ts
|
|
19
19
|
import * as THREE from "three";
|
|
20
20
|
import { connect } from "@genex-ai/multiplayer";
|
|
21
|
+
import { waitForPlayer, getColyseusAuth } from "@genex-ai/embed-sdk";
|
|
21
22
|
import { GENEX } from "./genex.config";
|
|
22
23
|
|
|
23
24
|
// Your published state — EVERY synced field, including discrete ones like hp. `me.set`
|
|
24
25
|
// replaces your state wholesale, so the tick must send all of these every time (below).
|
|
25
26
|
type S = { x: number; z: number; q: number[]; hp: number };
|
|
26
27
|
|
|
27
|
-
const
|
|
28
|
+
const { user } = await waitForPlayer(); // player gate (guest OR signed-in) — never waitForAuth()
|
|
29
|
+
const room = await connect<S>({
|
|
30
|
+
url: GENEX.colyseusUrl,
|
|
31
|
+
room: GENEX.slug,
|
|
32
|
+
name: user.name,
|
|
33
|
+
auth: getColyseusAuth()!, // REQUIRED — tokenless joins are rejected. Read fresh each connect; NEVER log it.
|
|
34
|
+
});
|
|
28
35
|
|
|
29
36
|
// --- local player: input mutates this; we render yourself from it (zero latency) ---
|
|
30
37
|
// This ONE object holds everything you sync. Keep hp/ammo/etc. here too — see the warning below.
|
|
@@ -132,8 +139,11 @@ Rules that keep it correct:
|
|
|
132
139
|
|
|
133
140
|
## Host authority (scores, rounds, world)
|
|
134
141
|
|
|
135
|
-
One client is `room.host`
|
|
136
|
-
|
|
142
|
+
One client is `room.host` — the earliest **signed-in** player (guests host only while no account
|
|
143
|
+
is present), and the host can change on **join** as well as leave: the first account entering a
|
|
144
|
+
guest-hosted room takes over. Always react to `on('host')` / read `isHost` in your loop, never
|
|
145
|
+
once at startup. Let only the host write agreed state, so there's a single source of truth — no
|
|
146
|
+
"who increments the score" races:
|
|
137
147
|
|
|
138
148
|
```ts
|
|
139
149
|
function updateHud() {
|
|
@@ -147,8 +157,9 @@ function goal(team: "a" | "b") {
|
|
|
147
157
|
room.on("host", () => {/* host migrated — the new host takes over writing */});
|
|
148
158
|
```
|
|
149
159
|
|
|
150
|
-
`room.on('shared', …)` fires
|
|
151
|
-
read `room.shared.get(...)` in your loop (as
|
|
160
|
+
`room.on('shared', …)` fires on every **distinct** change of a key (a set to the same value is
|
|
161
|
+
de-duped) — but for a value you render every frame, read `room.shared.get(...)` in your loop (as
|
|
162
|
+
`updateHud` does) rather than wiring render state through the event.
|
|
152
163
|
|
|
153
164
|
## Custom events (shots, emotes, chat)
|
|
154
165
|
|
|
@@ -62,7 +62,8 @@ physics.onBeforeStep(() => {
|
|
|
62
62
|
const clock = new THREE.Clock();
|
|
63
63
|
renderer.setAnimationLoop(() => {
|
|
64
64
|
physics.step(clock.getDelta()); // fixed substeps + mesh sync + events
|
|
65
|
-
// camera follow,
|
|
65
|
+
// camera follow, mixers, HUD go HERE (render-delta work, after the step) —
|
|
66
|
+
// never inside onBeforeStep above
|
|
66
67
|
renderer.render(scene, camera);
|
|
67
68
|
});
|
|
68
69
|
```
|
|
@@ -179,7 +179,7 @@ reference).
|
|
|
179
179
|
## End-to-end: a genex model prop
|
|
180
180
|
|
|
181
181
|
```ts
|
|
182
|
-
import { GLTFLoader } from "three/
|
|
182
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
183
183
|
import { collidersFromObject } from "./controllers/shared/colliders.ts";
|
|
184
184
|
|
|
185
185
|
const loader = new GLTFLoader();
|