@genex-ai/cli-demo 0.17.0 → 0.18.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 +299 -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 +13 -72
- 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 +31 -18
- 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
- package/templates/controllers/shared/NETWORKING.md +0 -11
- package/templates/skills/genex-threejs-multiplayer/references/host-physics.md +0 -111
|
@@ -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,16 +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.
|
|
41
|
-
|
|
42
|
-
## Trust model (say it plainly in your game's copy)
|
|
43
|
-
|
|
44
|
-
This is **casual, favor-the-player multiplayer** (Haxball, not Rocket League): you are
|
|
45
|
-
authoritative over yourself, match outcomes are self-reported, and there is no server-side
|
|
46
|
-
simulation or anti-cheat. The server DOES enforce identity (verified tokens), object
|
|
47
|
-
ownership, match seating/adjudication, and rate/size caps — but a modified client can still
|
|
48
|
-
lie about its own position or score. Great for friends and casual lobbies; don't promise
|
|
49
|
-
ranked-grade fairness.
|
|
40
|
+
This skill targets `@genex-ai/multiplayer` **≥ 0.7.1** (`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).
|
|
50
41
|
|
|
51
42
|
## Matchmaking (competitive presets — server-owned)
|
|
52
43
|
|
|
@@ -60,11 +51,13 @@ HUD from `mm.matchmaking` (its `status`, `queue.position`, `players`/`opponents`
|
|
|
60
51
|
// Pass auth as a FUNCTION — one matchmake() handle re-joins the queue/match many times (re-search,
|
|
61
52
|
// requeue) over a session, and embed tokens rotate (~10 min). A function is read fresh each (re)join;
|
|
62
53
|
// a static object goes stale and gets rejected mid-session.
|
|
54
|
+
await waitForPlayer(); // identity gate first (guest OR signed-in) — same rule as connect()
|
|
63
55
|
const mm = await matchmake<MyState>({ url, room: slug, auth: () => getColyseusAuth() });
|
|
64
56
|
mm.on('matched', () => {/* session is live — start the game */});
|
|
65
57
|
// each frame: if (mm.session) renderGame(mm.session); else renderSearchingHud(mm.matchmaking);
|
|
66
58
|
mm.on('matchEnded', ({ winnerId, scores, draw }) => {/* result screen */});
|
|
67
|
-
mm.on('error', (e) => {/*
|
|
59
|
+
mm.on('error', (e) => {/* queue join failed even after the SDK's automatic retries (backoff, ~10s)
|
|
60
|
+
— persistent, e.g. broken auth. Fix the cause, then call mm.retry(). */});
|
|
68
61
|
|
|
69
62
|
// Report ONLY your own outcome — the server adjudicates. Which call fits depends on the win condition:
|
|
70
63
|
mm.eliminated(); // I'm out (lastStanding)
|
|
@@ -73,8 +66,9 @@ mm.finish(); // I finished the race (firstToFinish)
|
|
|
73
66
|
```
|
|
74
67
|
|
|
75
68
|
Everything is **server-owned** — set once in `package.json` under `genex.matchmaking`, reported at
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
`genex preview` AND `genex publish` (removing it from package.json clears the stored config on the
|
|
70
|
+
next preview/publish); the client declares nothing. You never run matchmaking logic: the server
|
|
71
|
+
owns the queue, roles, winner-stays, forfeit, timeout, and the win condition.
|
|
78
72
|
|
|
79
73
|
```jsonc
|
|
80
74
|
"genex": {
|
|
@@ -129,36 +123,6 @@ const room = await connect<State>({
|
|
|
129
123
|
});
|
|
130
124
|
```
|
|
131
125
|
|
|
132
|
-
**Capacity:** a room holds up to **64 players** (up to 48 of them guests). Above that, the
|
|
133
|
-
relay opens a **second room for the same game** — two parallel worlds, no error. If your
|
|
134
|
-
game needs seated, one-world competition, use the matchmaking presets instead of one big
|
|
135
|
-
`connect()` room.
|
|
136
|
-
|
|
137
|
-
## Disconnects & reconnection (built in — render it, don't rebuild it)
|
|
138
|
-
|
|
139
|
-
The SDK auto-reconnects after a network blip or brief signal loss: the relay holds your seat
|
|
140
|
-
for a grace window (~30 s), and on recovery **nothing changed** — same session id, objects
|
|
141
|
-
still yours, host unchanged, and in a match **a blip is not a forfeit**. Your only job is UI:
|
|
142
|
-
|
|
143
|
-
```ts
|
|
144
|
-
room.on("reconnecting", ({ attempt }) => showOverlay(`Reconnecting… (${attempt})`));
|
|
145
|
-
room.on("reconnected", () => hideOverlay());
|
|
146
|
-
room.on("disconnect", (code) => {
|
|
147
|
-
// Terminal: server restart, revoked session, or the link never came back.
|
|
148
|
-
// To play again, read a FRESH token and connect() anew — never reuse the old auth object.
|
|
149
|
-
showMenu("Connection lost");
|
|
150
|
-
});
|
|
151
|
-
room.on("server:restart", () => flushSaves()); // the relay warns before a deploy — save now
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
Keep your render loop running during `reconnecting` — remote players freeze briefly and then
|
|
155
|
-
glide on; don't tear the scene down. A deliberate `room.leave()` never auto-reconnects.
|
|
156
|
-
|
|
157
|
-
**One seat per player (enforced server-side):** joining the same game again — a second tab,
|
|
158
|
-
another device, or a page reload — instantly evicts the previous session (it gets
|
|
159
|
-
`disconnect`, code 4409). You never need to handle "the same player twice" and a reload
|
|
160
|
-
never leaves a ghost avatar behind.
|
|
161
|
-
|
|
162
126
|
## Which channel for which data
|
|
163
127
|
|
|
164
128
|
**This table is the most important thing in this skill.** Every piece of networked state is one
|
|
@@ -188,7 +152,9 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
|
|
|
188
152
|
- `set(id, state)` — publish it (only lands while you own it; full flat object each call).
|
|
189
153
|
- `get(id)` → `{ id, owner, isMine, state, stateRaw }` or `undefined`. `state` is auto-smoothed
|
|
190
154
|
(or live if `isMine`); `stateRaw` is the raw latest.
|
|
191
|
-
- `release(id)` — give up ownership. `remove(id)` — destroy it (for transient bullets/pickups)
|
|
155
|
+
- `release(id)` — give up ownership. `remove(id)` — destroy it (for transient bullets/pickups);
|
|
156
|
+
**owner-only** — claim it first, or let the current owner remove it (the relay rejects a
|
|
157
|
+
non-owner's destroy).
|
|
192
158
|
- `ids()` — all object ids seen.
|
|
193
159
|
- `room.isHost` / `room.host` — you are (or who is) the elected authority. Use to pick the single
|
|
194
160
|
writer of `shared` scores/rounds and the single simulator of host-owned objects. Settles within
|
|
@@ -199,21 +165,13 @@ fights (many writers). A ball on `objects` glides and has one owner. That's the
|
|
|
199
165
|
`'host'` `(id)`, and any custom `send` name.
|
|
200
166
|
- `room.send(type, payload)` — fire-and-forget to all **other** clients. **It never echoes to
|
|
201
167
|
you**, so apply your own action's local effect directly (draw your own tracer at fire time),
|
|
202
|
-
not inside `on(...)`.
|
|
203
|
-
`destroy`, `match:*`, `__*`) are refused — pick your own event names. `room.leave()`.
|
|
204
|
-
- `room.inputs.send(payload)` / `room.inputs.on((fromId, payload) => …)` — the host-routed
|
|
205
|
-
input channel for host-authoritative physics: anyone sends, ONLY the current host receives.
|
|
206
|
-
See [references/host-physics.md](references/host-physics.md).
|
|
207
|
-
- `room.onHostTick(hz, cb)` — run a fixed simulation tick only while you are the host
|
|
208
|
-
(auto-starts/stops across host migration). Returns a disposer.
|
|
168
|
+
not inside `on(...)`. `room.leave()`.
|
|
209
169
|
|
|
210
170
|
## The loop you must build (input → local → tick → render)
|
|
211
171
|
|
|
212
172
|
1. **Input mutates a local object only** (`me.x += …`). Never network on keypress.
|
|
213
173
|
2. **A fixed tick publishes it:** `setInterval(() => room.me.set(me), 66)` (~15 Hz). If you own an
|
|
214
|
-
object, `objects.set` it in the same tick.
|
|
215
|
-
`Math.round(v * 100) / 100` (2 decimals ≈ cm precision) — raw floats serialize as 17-digit
|
|
216
|
-
JSON and are the #1 bandwidth waste; nobody can see a 0.001-unit difference.
|
|
174
|
+
object, `objects.set` it in the same tick.
|
|
217
175
|
3. **Render at your own framerate:** yourself from your *local* object; every other player from
|
|
218
176
|
`players.get(id).state` directly (already smoothed); every object from `objects.get(id).state`.
|
|
219
177
|
4. **Create-or-reuse one mesh per id**; remove a player's mesh on `'leave'`.
|
|
@@ -265,17 +223,6 @@ For host-simulated NPCs, the host claims and drives each enemy as an `object`; w
|
|
|
265
223
|
leaves, its enemies are reassigned to the new host, which reads their `stateRaw` and keeps
|
|
266
224
|
simulating. See the co-op recipe in [references/genre-recipes.md](references/genre-recipes.md).
|
|
267
225
|
|
|
268
|
-
### Contested physics (two players pushing ONE thing) — host-authoritative
|
|
269
|
-
|
|
270
|
-
Claim-on-touch is perfect for one-touch objects (kick a ball). It **breaks down under
|
|
271
|
-
sustained contact** — two players pushing the same crate steal ownership back and forth and
|
|
272
|
-
the crate judders. For contested objects, ONE simulation must own the contest: the **host**
|
|
273
|
-
runs the physics for those objects; everyone else sends **inputs**
|
|
274
|
-
(`room.inputs.send({ push })`), which the relay routes to the host only; the host applies
|
|
275
|
-
them on `room.onHostTick(...)` and publishes results via `objects` (smooth for everyone).
|
|
276
|
-
Full recipe — including surviving host migration and wiring the Rapier controllers —
|
|
277
|
-
in [references/host-physics.md](references/host-physics.md).
|
|
278
|
-
|
|
279
226
|
## Smoothness is felt, not seen — hand the feel to a human
|
|
280
227
|
|
|
281
228
|
Lag and stutter are *motion over time*. A screenshot is one frozen instant, so **you cannot tell
|
|
@@ -359,10 +306,7 @@ host-driven saving works as long as ANY account is in the room.
|
|
|
359
306
|
|
|
360
307
|
## Checklist
|
|
361
308
|
|
|
362
|
-
- [ ] `npm i @genex-ai/multiplayer` (≥ 0.
|
|
363
|
-
- [ ] `reconnecting`/`reconnected`/`disconnect` render an overlay (don't tear the scene down).
|
|
364
|
-
- [ ] Numbers rounded (~2 decimals) before `me.set`/`objects.set`.
|
|
365
|
-
- [ ] Contested (sustained-contact) objects use the host-physics pattern, not claim-on-touch.
|
|
309
|
+
- [ ] `npm i @genex-ai/multiplayer` (≥ 0.7.1); config wired into the build.
|
|
366
310
|
- [ ] `connect()` runs AFTER `await waitForPlayer()` (never `waitForAuth()` — guests would
|
|
367
311
|
hang) and passes `auth: getColyseusAuth()!` (the relay rejects tokenless joins —
|
|
368
312
|
see `genex-threejs-embed-auth`).
|
|
@@ -384,6 +328,3 @@ host-driven saving works as long as ANY account is in the room.
|
|
|
384
328
|
read `getColyseusAuth()` fresh at every connect). 403 "wrong game": the `room` value
|
|
385
329
|
doesn't match this game's own slug. 403 "guest capacity": the room is at its guest
|
|
386
330
|
limit — signing in gets the player a seat; surface the message as-is.
|
|
387
|
-
- **`disconnect` fired and the player wants back in** — the old session is dead; run your
|
|
388
|
-
connect flow again from the top with a FRESH `getColyseusAuth()` (a cached auth object is
|
|
389
|
-
the usual cause of a rejoin failing 401).
|