@genex-ai/cli-demo 0.61.0-dev.141 → 0.62.0-dev.144
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 +5 -2
- package/dist/index.js +1199 -878
- package/package.json +1 -1
- package/templates/controllers/character/meshy/meshy-loader.ts +15 -1
- package/templates/skills/genex-ai-character/SKILL.md +19 -4
- package/templates/skills/genex-ai-menu/SKILL.md +13 -0
- package/templates/skills/genex-threejs-character-controller/SKILL.md +11 -0
- package/templates/skills/genex-threejs-character-controller/references/animations.md +5 -0
- package/templates/skills/genex-threejs-game-content/SKILL.md +189 -0
- package/templates/skills/genex-threejs-game-content/references/content-tables.md +269 -0
- package/templates/skills/genex-threejs-open-world/SKILL.md +149 -0
- package/templates/skills/genex-threejs-open-world/references/terrain-streaming.md +215 -0
- package/templates/skills/genex-threejs-skill-router/SKILL.md +20 -0
- package/templates/skills/genex-threejs-skill-router/references/routing-map.md +38 -4
- package/templates/skills/genex-threejs-visual-validation/SKILL.md +4 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: genex-threejs-open-world
|
|
3
|
+
description: Build a big explorable world that stays fast — seeded heightfield terrain with chunk streaming, biomes, points of interest, instanced scatter, and honest world bounds. Use when the ask says big or open world, multiple regions or locations, exploration, or any map beyond one arena — before the first terrain code, instead of one flat plane with fog.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Genex Three.js Open World
|
|
7
|
+
|
|
8
|
+
"Big world" in a request is a size class, not a mood. A 300 m plane with fog
|
|
9
|
+
pulled in to hide the walls is an arena wearing a costume — the player finds
|
|
10
|
+
the invisible wall in the first two minutes, and no amount of dressing
|
|
11
|
+
survives that. This skill is the recipe for a world that is actually big —
|
|
12
|
+
kilometers-class, streamed, varied — at a cost one session can afford.
|
|
13
|
+
|
|
14
|
+
## Decide the scale first, as a number
|
|
15
|
+
|
|
16
|
+
Put the world's size in the plan (and in the content contract's `world:` line
|
|
17
|
+
when `$genex-threejs-game-content` is loaded — a big world exists to hold
|
|
18
|
+
content, so the two skills almost always load together):
|
|
19
|
+
|
|
20
|
+
- **One arena** (~100–300 m): only when the ask says so (a shooter map, a
|
|
21
|
+
sports pitch, a boss rush). Never the silent fallback for "big world".
|
|
22
|
+
- **A district** (~500 m – 1 km): a town + surroundings; streaming optional.
|
|
23
|
+
- **Open world** (2–4 km bounded): the default meaning of "big/open world" —
|
|
24
|
+
a dozen locations, real travel time between them, streamed terrain. Beyond
|
|
25
|
+
~4 km you're spending budget on emptiness; density beats acreage.
|
|
26
|
+
|
|
27
|
+
Bound the world honestly: an edge-mountain ring, a coastline, or a cliff
|
|
28
|
+
reads as "the world ends here" — an invisible wall on a flat horizon reads as
|
|
29
|
+
a bug. Fog is atmosphere and draw-distance management
|
|
30
|
+
(`$genex-threejs-atmosphere-aerial-perspective` for the real thing), never a
|
|
31
|
+
wall to hide how small the map is.
|
|
32
|
+
|
|
33
|
+
## The scaling law: procedural fabric, generated landmarks
|
|
34
|
+
|
|
35
|
+
The single decision that determines whether a big world is affordable:
|
|
36
|
+
|
|
37
|
+
- **The world's FABRIC is procedural** — a seeded heightfield, instanced
|
|
38
|
+
vegetation, scattered rocks and props built from primitives and the
|
|
39
|
+
procedural skills (`$genex-threejs-procedural-vegetation`,
|
|
40
|
+
`$genex-threejs-procedural-geometry`). Procedural fabric costs the same at
|
|
41
|
+
4 km as at 300 m; that is what makes the size class reachable.
|
|
42
|
+
- **Generated assets are LANDMARKS** — `npx genex model` set pieces placed at
|
|
43
|
+
points of interest (the village well, the boss lair gate, the shrine), a
|
|
44
|
+
ground texture (`npx genex texture --terrain`, UVs from `worldUV` — never a
|
|
45
|
+
hand-picked repeat), a skybox. Hero assets decorate the world's landmarks;
|
|
46
|
+
they never decide the world's size, because a world assembled from
|
|
47
|
+
hand-placed generated meshes caps out at a diorama.
|
|
48
|
+
|
|
49
|
+
Wire generated pieces in as upgrades over procedural placeholders (the
|
|
50
|
+
standard swap discipline), so the world is walkable at full size from the
|
|
51
|
+
first hour.
|
|
52
|
+
|
|
53
|
+
## One height function to rule everything
|
|
54
|
+
|
|
55
|
+
Terrain is a seeded fBm heightfield (`$genex-threejs-procedural-fields` owns
|
|
56
|
+
the noise craft) with ridges for drama, an edge-mountain ring for the bound,
|
|
57
|
+
and **flattened pads blended in around each location** so structures sit on
|
|
58
|
+
level ground. The load-bearing rule: **exactly one canonical
|
|
59
|
+
`getHeightAt(x, z)`** that chunk meshing, physics grounding, placement
|
|
60
|
+
scatter, NPC spawns, and the minimap all share. The moment a second height
|
|
61
|
+
formula exists, trees float and feet sink — every large-world bug report
|
|
62
|
+
starts there.
|
|
63
|
+
|
|
64
|
+
Full copy-paste module — noise, fBm, location flattening, chunk manager,
|
|
65
|
+
instanced scatter — in
|
|
66
|
+
[references/terrain-streaming.md](references/terrain-streaming.md).
|
|
67
|
+
|
|
68
|
+
## Stream chunks, budget the frame
|
|
69
|
+
|
|
70
|
+
Build the terrain as fixed-size chunks (128 m is a good default) around the
|
|
71
|
+
player: load radius ~5 chunks (~600 m view with fog), unload behind, and
|
|
72
|
+
build queued chunks inside a **per-frame time budget** (a few ms) so streaming
|
|
73
|
+
never hitches the game. Seam rule: compute normals with a one-vertex apron
|
|
74
|
+
into the neighbor chunk, or every chunk border shows as a lighting crease.
|
|
75
|
+
Far distance is fog + the skybox — a low-res far ring is an upgrade, not a
|
|
76
|
+
requirement.
|
|
77
|
+
|
|
78
|
+
Physics grounding: keep the character/vehicle on the ground via the canonical
|
|
79
|
+
`getHeightAt` (cheap, always loaded) or per-chunk Rapier heightfield
|
|
80
|
+
colliders (`$genex-threejs-physics-rapier`) when projectiles and ragdolls
|
|
81
|
+
need real collision — but never build colliders for chunks the player isn't
|
|
82
|
+
near.
|
|
83
|
+
|
|
84
|
+
## Biomes: variety from two noise fields
|
|
85
|
+
|
|
86
|
+
Two low-frequency noise fields (temperature, moisture) + altitude + slope
|
|
87
|
+
give 4–7 biomes from one lookup: meadow, forest, marsh, desert, alpine, snow.
|
|
88
|
+
The biome function drives everything downstream from ONE place — vertex
|
|
89
|
+
colors or texture blend, vegetation species and density
|
|
90
|
+
(`$genex-threejs-procedural-vegetation`, instanced), enemy spawn tables and
|
|
91
|
+
ambient audio per biome (that's the content hookup). "Different locations" in
|
|
92
|
+
an ask is only half-answered by placed structures; the other half is the
|
|
93
|
+
ground itself changing as you travel.
|
|
94
|
+
|
|
95
|
+
## Points of interest: the world's content skeleton
|
|
96
|
+
|
|
97
|
+
A data table of locations — id, name, position, radius, builder function —
|
|
98
|
+
is the spine the content contract hangs from (quest givers live somewhere;
|
|
99
|
+
"locations: 8" means eight entries here):
|
|
100
|
+
|
|
101
|
+
- **Density beats acreage:** a point of interest every ~300–500 m of travel
|
|
102
|
+
on the natural routes. A 4 km world with three POIs is emptier than a 1 km
|
|
103
|
+
world with eight.
|
|
104
|
+
- Each location: terrain pad flattened (blend into the heightfield — see the
|
|
105
|
+
reference), a hand-authored builder (walls, tents, standing stones — merged
|
|
106
|
+
primitive geometry + generated landmark pieces), spawns, loot, and its
|
|
107
|
+
compass/minimap marker.
|
|
108
|
+
- Light budget: point lights per location, hard cap world-wide (~6 active) —
|
|
109
|
+
swap distant ones for emissive materials (`$genex-threejs-lighting-design`
|
|
110
|
+
owns the walk).
|
|
111
|
+
- Roads or worn paths between major POIs guide travel and double as the
|
|
112
|
+
navigation answer ("follow the road north beats a quest arrow").
|
|
113
|
+
|
|
114
|
+
## Performance floors (the size class depends on them)
|
|
115
|
+
|
|
116
|
+
- **Instancing for everything repeated**: trees, grass, rocks are
|
|
117
|
+
`InstancedMesh` per chunk — thousands of draw calls is the classic
|
|
118
|
+
big-world death; hundreds is the target.
|
|
119
|
+
- **Merge static location geometry** per material into a handful of meshes.
|
|
120
|
+
- Shadows at scale need a strategy, not defaults: a tight shadow camera
|
|
121
|
+
following the player, or cascades — `$genex-threejs-shadow-systems`.
|
|
122
|
+
- Keep per-frame allocation out of the loop (reuse vectors), and keep chunk
|
|
123
|
+
building inside its time budget.
|
|
124
|
+
- Phone-survivable stays the bar: pixel ratio cap, texture ≤ 2048², and the
|
|
125
|
+
chunk radius is the quality knob to shrink first.
|
|
126
|
+
|
|
127
|
+
## Day/night: cheap and almost expected
|
|
128
|
+
|
|
129
|
+
A big explorable world reads twice as alive with a sun cycle: one animated
|
|
130
|
+
`DirectionalLight` angle + a palette lerp (sky, fog, ambient) + practical
|
|
131
|
+
lights that matter at night. It's ~50 lines against the lighting rig
|
|
132
|
+
(`$genex-threejs-lighting-design`), and it makes travel time feel like time.
|
|
133
|
+
Optional — but when the ask says "like the big RPGs", this is one of the
|
|
134
|
+
three things they mean.
|
|
135
|
+
|
|
136
|
+
## Failure modes to catch
|
|
137
|
+
|
|
138
|
+
- A flat plane with invisible walls and close fog shipped as "big world" —
|
|
139
|
+
the defining failure this skill exists to prevent.
|
|
140
|
+
- Two height functions (mesh vs physics vs placement) drifting apart —
|
|
141
|
+
floating trees, buried chests.
|
|
142
|
+
- Hand-placing every tree at world scale — placement must be seeded scatter
|
|
143
|
+
with biome rules, or the world stays empty.
|
|
144
|
+
- Per-chunk geometry that never unloads or shares materials — memory climbs,
|
|
145
|
+
draw calls explode.
|
|
146
|
+
- POIs as map dots only — a named location with nothing to do fails the
|
|
147
|
+
content contract it was meant to serve (`$genex-threejs-game-content`).
|
|
148
|
+
- Seams: normal creases at every chunk border (missing apron), or texture
|
|
149
|
+
tiling picked by eye instead of `worldUV` (`$genex-ai-texture`).
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# Genex open-world terrain — copy-paste streaming heightfield
|
|
2
|
+
|
|
3
|
+
One module: seeded noise → fBm heightfield with location pads → chunked,
|
|
4
|
+
time-budgeted streaming with seam-free normals → instanced scatter. Plain
|
|
5
|
+
vanilla-ts, three.js only. Copy it, then tune the named constants at the top —
|
|
6
|
+
they are the whole difficulty/size surface.
|
|
7
|
+
|
|
8
|
+
## Constants + the canonical height function
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
// world-terrain.ts
|
|
12
|
+
import * as THREE from "three";
|
|
13
|
+
|
|
14
|
+
export const WORLD_SEED = 1337; // one seed → same world on every machine
|
|
15
|
+
export const WORLD_BOUND = 2048; // half-size: playable area is 4096×4096 m
|
|
16
|
+
export const CHUNK = 128; // chunk side in meters
|
|
17
|
+
export const CHUNK_RES = 32; // vertices per side (33×33 grid)
|
|
18
|
+
export const VIEW_CHUNKS = 5; // load radius → ~600 m view with fog
|
|
19
|
+
|
|
20
|
+
// --- seeded value noise + fBm (swap in $genex-threejs-procedural-fields'
|
|
21
|
+
// simplex if it's already loaded; the shape below is what matters) ---
|
|
22
|
+
const hash2 = (x: number, y: number): number => {
|
|
23
|
+
let h = (Math.imul(x, 374761393) + Math.imul(y, 668265263) + WORLD_SEED) | 0;
|
|
24
|
+
h = Math.imul(h ^ (h >>> 13), 1274126177);
|
|
25
|
+
return ((h ^ (h >>> 16)) >>> 0) / 0xffffffff;
|
|
26
|
+
};
|
|
27
|
+
const smooth = (t: number): number => t * t * (3 - 2 * t);
|
|
28
|
+
function valueNoise(x: number, y: number): number {
|
|
29
|
+
const xi = Math.floor(x), yi = Math.floor(y);
|
|
30
|
+
const tx = smooth(x - xi), ty = smooth(y - yi);
|
|
31
|
+
const a = hash2(xi, yi), b = hash2(xi + 1, yi);
|
|
32
|
+
const c = hash2(xi, yi + 1), d = hash2(xi + 1, yi + 1);
|
|
33
|
+
return (a + (b - a) * tx) * (1 - ty) + (c + (d - c) * tx) * ty; // 0..1
|
|
34
|
+
}
|
|
35
|
+
function fbm(x: number, y: number, octaves = 5): number {
|
|
36
|
+
let sum = 0, amp = 0.5, freq = 1;
|
|
37
|
+
for (let i = 0; i < octaves; i++) {
|
|
38
|
+
sum += amp * (valueNoise(x * freq, y * freq) * 2 - 1);
|
|
39
|
+
amp *= 0.5; freq *= 2;
|
|
40
|
+
}
|
|
41
|
+
return sum; // ~-1..1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- location pads: level ground for structures, BLENDED into the field ---
|
|
45
|
+
type Pad = { x: number; z: number; radius: number; height: number };
|
|
46
|
+
export const PADS: Pad[] = []; // fill from your POI table before first chunk build
|
|
47
|
+
|
|
48
|
+
function baseHeight(x: number, z: number): number {
|
|
49
|
+
const n = fbm(x * 0.0016, z * 0.0016); // rolling base, ~600 m features
|
|
50
|
+
const ridge = 1 - Math.abs(fbm(x * 0.004, z * 0.004)); // sharp ridge lines
|
|
51
|
+
let h = n * 26 + ridge * ridge * 18;
|
|
52
|
+
// edge-mountain ring: the honest world bound the player can SEE
|
|
53
|
+
const edge = Math.max(Math.abs(x), Math.abs(z)) / WORLD_BOUND; // 0..1
|
|
54
|
+
if (edge > 0.85) h += ((edge - 0.85) / 0.15) ** 2 * 90;
|
|
55
|
+
return h;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* THE canonical height. Chunk meshing, physics grounding, scatter, spawns,
|
|
60
|
+
* and the minimap all call THIS — a second height formula anywhere is how
|
|
61
|
+
* trees float and feet sink.
|
|
62
|
+
*/
|
|
63
|
+
export function getHeightAt(x: number, z: number): number {
|
|
64
|
+
let h = baseHeight(x, z);
|
|
65
|
+
for (const p of PADS) {
|
|
66
|
+
const d = Math.hypot(x - p.x, z - p.z);
|
|
67
|
+
if (d < p.radius) {
|
|
68
|
+
const t = smooth(1 - d / p.radius); // 1 at center → 0 at rim
|
|
69
|
+
h = h * (1 - t) + p.height * t; // flat pad blended into the hills
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return h;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Chunk manager — streamed, time-budgeted, seam-free
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// world-chunks.ts
|
|
80
|
+
const chunkKey = (cx: number, cz: number): string => `${cx},${cz}`;
|
|
81
|
+
const live = new Map<string, THREE.Mesh>();
|
|
82
|
+
const queue: Array<{ cx: number; cz: number }> = [];
|
|
83
|
+
const material = new THREE.MeshStandardMaterial({ vertexColors: true });
|
|
84
|
+
|
|
85
|
+
function buildChunk(cx: number, cz: number): THREE.Mesh {
|
|
86
|
+
const geo = new THREE.PlaneGeometry(CHUNK, CHUNK, CHUNK_RES, CHUNK_RES);
|
|
87
|
+
geo.rotateX(-Math.PI / 2);
|
|
88
|
+
const pos = geo.attributes.position;
|
|
89
|
+
const colors = new Float32Array(pos.count * 3);
|
|
90
|
+
const x0 = cx * CHUNK, z0 = cz * CHUNK;
|
|
91
|
+
for (let i = 0; i < pos.count; i++) {
|
|
92
|
+
const wx = x0 + pos.getX(i), wz = z0 + pos.getZ(i);
|
|
93
|
+
const h = getHeightAt(wx, wz);
|
|
94
|
+
pos.setY(i, h);
|
|
95
|
+
const c = biomeColor(wx, wz, h); // your biome lookup (see below)
|
|
96
|
+
colors.set([c.r, c.g, c.b], i * 3);
|
|
97
|
+
}
|
|
98
|
+
geo.setAttribute("color", new THREE.BufferAttribute(colors, 3));
|
|
99
|
+
// Seam rule: analytic normals from the SAME height field (an implicit
|
|
100
|
+
// one-vertex apron) — computeVertexNormals() per chunk creases every border.
|
|
101
|
+
const nrm = geo.attributes.normal;
|
|
102
|
+
const eps = CHUNK / CHUNK_RES;
|
|
103
|
+
for (let i = 0; i < pos.count; i++) {
|
|
104
|
+
const wx = x0 + pos.getX(i), wz = z0 + pos.getZ(i);
|
|
105
|
+
const hx = getHeightAt(wx + eps, wz) - getHeightAt(wx - eps, wz);
|
|
106
|
+
const hz = getHeightAt(wx, wz + eps) - getHeightAt(wx, wz - eps);
|
|
107
|
+
const n = new THREE.Vector3(-hx, 2 * eps, -hz).normalize();
|
|
108
|
+
nrm.setXYZ(i, n.x, n.y, n.z);
|
|
109
|
+
}
|
|
110
|
+
const mesh = new THREE.Mesh(geo, material);
|
|
111
|
+
mesh.position.set(x0, 0, z0);
|
|
112
|
+
mesh.receiveShadow = true;
|
|
113
|
+
return mesh;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Call every frame. Streams chunks around the player inside a ms budget. */
|
|
117
|
+
export function updateChunks(scene: THREE.Scene, px: number, pz: number, budgetMs = 5): void {
|
|
118
|
+
const ccx = Math.round(px / CHUNK), ccz = Math.round(pz / CHUNK);
|
|
119
|
+
for (let dz = -VIEW_CHUNKS; dz <= VIEW_CHUNKS; dz++) {
|
|
120
|
+
for (let dx = -VIEW_CHUNKS; dx <= VIEW_CHUNKS; dx++) {
|
|
121
|
+
const key = chunkKey(ccx + dx, ccz + dz);
|
|
122
|
+
if (!live.has(key) && !queue.some((q) => chunkKey(q.cx, q.cz) === key)) {
|
|
123
|
+
queue.push({ cx: ccx + dx, cz: ccz + dz });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const start = performance.now();
|
|
128
|
+
while (queue.length && performance.now() - start < budgetMs) {
|
|
129
|
+
const { cx, cz } = queue.shift()!;
|
|
130
|
+
const mesh = buildChunk(cx, cz);
|
|
131
|
+
live.set(chunkKey(cx, cz), mesh);
|
|
132
|
+
scene.add(mesh);
|
|
133
|
+
// scatterChunk(scene, cx, cz) — instanced vegetation, below
|
|
134
|
+
}
|
|
135
|
+
for (const [key, mesh] of live) {
|
|
136
|
+
const [cx, cz] = key.split(",").map(Number);
|
|
137
|
+
if (Math.max(Math.abs(cx - ccx), Math.abs(cz - ccz)) > VIEW_CHUNKS + 1) {
|
|
138
|
+
scene.remove(mesh);
|
|
139
|
+
mesh.geometry.dispose(); // material is shared — never dispose it here
|
|
140
|
+
live.delete(key);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Biomes — one lookup drives color, scatter, and spawns
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
type Biome = "meadow" | "forest" | "marsh" | "desert" | "alpine";
|
|
150
|
+
export function biomeAt(x: number, z: number, h: number): Biome {
|
|
151
|
+
if (h > 55) return "alpine";
|
|
152
|
+
const temp = valueNoise(x * 0.0006 + 100, z * 0.0006); // slow fields:
|
|
153
|
+
const moist = valueNoise(x * 0.0006, z * 0.0006 + 100); // ~1.5 km regions
|
|
154
|
+
if (temp > 0.62 && moist < 0.4) return "desert";
|
|
155
|
+
if (moist > 0.62 && h < 12) return "marsh";
|
|
156
|
+
if (moist > 0.45) return "forest";
|
|
157
|
+
return "meadow";
|
|
158
|
+
}
|
|
159
|
+
const BIOME_TINT: Record<Biome, THREE.Color> = {
|
|
160
|
+
meadow: new THREE.Color(0x5d7f3a), forest: new THREE.Color(0x3f5f31),
|
|
161
|
+
marsh: new THREE.Color(0x4a5a3c), desert: new THREE.Color(0xb59a63),
|
|
162
|
+
alpine: new THREE.Color(0x8d8d94),
|
|
163
|
+
};
|
|
164
|
+
function biomeColor(x: number, z: number, h: number): THREE.Color {
|
|
165
|
+
const c = BIOME_TINT[biomeAt(x, z, h)].clone();
|
|
166
|
+
// slope → rock, shoreline → sand: cheap reads that sell the terrain
|
|
167
|
+
return c;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Spawn tables and ambient audio key off `biomeAt` too — that is the content
|
|
172
|
+
hookup (`$genex-threejs-game-content`'s enemy line usually reads "wolves in
|
|
173
|
+
the forest, bandits on the roads": this function is where that sentence
|
|
174
|
+
becomes true).
|
|
175
|
+
|
|
176
|
+
## Instanced scatter — a forest for one draw call
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
export function scatterChunk(scene: THREE.Scene, cx: number, cz: number): void {
|
|
180
|
+
const rng = seededRng((cx * 73856093) ^ (cz * 19349663) ^ WORLD_SEED);
|
|
181
|
+
const spots: THREE.Matrix4[] = [];
|
|
182
|
+
for (let i = 0; i < 40; i++) {
|
|
183
|
+
const x = (cx + rng()) * CHUNK, z = (cz + rng()) * CHUNK;
|
|
184
|
+
const h = getHeightAt(x, z);
|
|
185
|
+
if (biomeAt(x, z, h) !== "forest") continue;
|
|
186
|
+
if (PADS.some((p) => Math.hypot(x - p.x, z - p.z) < p.radius + 6)) continue;
|
|
187
|
+
const s = 0.8 + rng() * 0.7;
|
|
188
|
+
spots.push(new THREE.Matrix4().compose(
|
|
189
|
+
new THREE.Vector3(x, h, z),
|
|
190
|
+
new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), rng() * Math.PI * 2),
|
|
191
|
+
new THREE.Vector3(s, s, s),
|
|
192
|
+
));
|
|
193
|
+
}
|
|
194
|
+
if (!spots.length) return;
|
|
195
|
+
const tree = new THREE.InstancedMesh(sharedTreeGeometry, sharedTreeMaterial, spots.length);
|
|
196
|
+
spots.forEach((m, i) => tree.setMatrixAt(i, m));
|
|
197
|
+
tree.castShadow = true;
|
|
198
|
+
scene.add(tree); // track per chunk-key and remove alongside the chunk
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
(`seededRng` is the one from `$genex-threejs-game-content`'s reference —
|
|
203
|
+
share it. `sharedTreeGeometry`/`sharedTreeMaterial` are module-level ONE-time
|
|
204
|
+
allocations: per-chunk geometry allocation is the classic memory leak here.
|
|
205
|
+
Real trees come from `$genex-threejs-procedural-vegetation`; a cone-on-
|
|
206
|
+
cylinder placeholder is fine until it loads.)
|
|
207
|
+
|
|
208
|
+
## Grounding the player
|
|
209
|
+
|
|
210
|
+
For the character/vehicle, the cheap path is the canonical function itself —
|
|
211
|
+
`y = getHeightAt(x, z)` plus a small offset for walking, with slopes from the
|
|
212
|
+
same finite differences as the normals. Move to per-chunk Rapier heightfield
|
|
213
|
+
colliders (`$genex-threejs-physics-rapier`) when projectiles, ragdolls, or
|
|
214
|
+
rolling props need true collision — build them only for the chunks around the
|
|
215
|
+
player, and from the SAME `getHeightAt`, never a copy.
|
|
@@ -43,6 +43,8 @@ map, execution order, and acceptance gate.
|
|
|
43
43
|
| eye adaptation, tone mapping, LUT grading, output color | `$genex-threejs-exposure-color-grading` |
|
|
44
44
|
| shared depth/normal/velocity ownership and multi-pass ordering | `$genex-threejs-image-pipeline` |
|
|
45
45
|
| fixed-view diagnostics, seed sweeps, temporal and budget evidence | `$genex-threejs-visual-validation` |
|
|
46
|
+
| game content named in the plural or a content genre: quests, objectives, NPCs, dialogue, shops, inventory, loot, XP/progression, an RPG/adventure/story game — **mandatory whenever the ask names content**, and its content contract is written before the asset batch | `$genex-threejs-game-content` |
|
|
47
|
+
| a big/open world: kilometers of terrain, multiple regions or locations, exploration, points of interest, biomes, world streaming | `$genex-threejs-open-world` |
|
|
46
48
|
| the 2D interface — HUD, menus, pause/win/lose screens, loaders, lobby, on-screen text and buttons, UI state flow — **mandatory for every game**, and its "Plan the UI first" gate runs right after the concept is locked | `$genex-threejs-game-ui` |
|
|
47
49
|
| a cinematic menu — main menu/title/pause/victory/defeat/lobby/credits with a looping generated video backdrop behind DOM buttons | `$genex-ai-menu` |
|
|
48
50
|
| a cohesive art-directed HUD — generated sprite set (matched frames, masks, icons in one style) wired with masked fills | `$genex-ai-hud` |
|
|
@@ -73,6 +75,24 @@ while the game is being built — they never block a playable v0, and the game i
|
|
|
73
75
|
not done, published, or handed off until they've been picked up (`npx genex wait`)
|
|
74
76
|
and wired in.
|
|
75
77
|
|
|
78
|
+
**Content is mandatory routing when the ask names it:** if the request names
|
|
79
|
+
game content in the plural — quests, enemies, bosses, locations, spells, items,
|
|
80
|
+
factions — or a content genre (an RPG, an adventure, an open world, a story
|
|
81
|
+
game), load `$genex-threejs-game-content` and write its **content contract**
|
|
82
|
+
into the same plan message as the UI gate, BEFORE the asset batch: every
|
|
83
|
+
plural noun of the request becomes a countable line (how many quests and what
|
|
84
|
+
chain, which locations, what progression), plus the minute-ten answer. Those
|
|
85
|
+
lines are hard floors exactly like the sprite HUD — the game is not done,
|
|
86
|
+
published, or handed off while a contract line is missing, and shrinking one
|
|
87
|
+
is a structured question to the user, never a silent cut ("vertical slice
|
|
88
|
+
first" is a build order, not a smaller destination). A request that says
|
|
89
|
+
**big/open world** also loads `$genex-threejs-open-world` and fixes the
|
|
90
|
+
world's scale as a number in the same plan — world size comes from streamed
|
|
91
|
+
terrain, never from fog hiding the edges of a small plane. This routing
|
|
92
|
+
exists because the failure it prevents is invisible to every visual gate: a
|
|
93
|
+
"Skyrim-like big world with quests" can pass every look floor as a
|
|
94
|
+
300-meter single-quest vignette.
|
|
95
|
+
|
|
76
96
|
**The look is planned up front too:** right after the UI gate, lock the visual
|
|
77
97
|
direction in the same plan block — the camera rig + pointer bucket
|
|
78
98
|
(`$genex-threejs-camera-direction`), the renderer baseline (tone mapping,
|
|
@@ -16,7 +16,22 @@ Three.js release or branch, and do not blindly copy demo architecture.
|
|
|
16
16
|
## Execution order
|
|
17
17
|
|
|
18
18
|
1. Define the game contract: player verb, win/interaction loop, target device,
|
|
19
|
-
camera distance, scene scale, motion, and frame budget.
|
|
19
|
+
camera distance, scene scale, motion, and frame budget. **When the request
|
|
20
|
+
names content in the plural (quests, enemies, locations, bosses, items) or
|
|
21
|
+
a content genre (RPG, adventure, open world, story game), the contract has
|
|
22
|
+
a second half — the content contract from `$genex-threejs-game-content`:**
|
|
23
|
+
every plural noun becomes a countable line (N quests and their chain, N
|
|
24
|
+
named locations, N enemy types and which are bosses, the progression axis,
|
|
25
|
+
the economy's sources and sinks), plus the minute-ten answer ("what is the
|
|
26
|
+
player doing ten minutes in?"). Write it BEFORE step 3's art enqueue and
|
|
27
|
+
before the asset batch — the asset set derives from it, and a world shaped
|
|
28
|
+
around the wrong assets can't be reshaped later. A **big/open world** ask
|
|
29
|
+
additionally fixes the world's scale class as a number here via
|
|
30
|
+
`$genex-threejs-open-world` — kilometers of streamed terrain, never one
|
|
31
|
+
fogged plane. Scope belongs to the user: shipping fewer or smaller than
|
|
32
|
+
the ask requires their explicit OK through a structured question — a
|
|
33
|
+
"vertical slice first" is a build order, never a license to shrink the
|
|
34
|
+
destination silently.
|
|
20
35
|
2. Wire player identity before any boot code: `$genex-threejs-embed-auth` is
|
|
21
36
|
mandatory for every game (`initEmbed(...)` + the `waitForPlayer()` gate) —
|
|
22
37
|
saves, leaderboards, and multiplayer auth all come from it.
|
|
@@ -189,6 +204,18 @@ without them (Cursor), the same order simply runs sequentially and the
|
|
|
189
204
|
- **Verification runner.** Browser evidence — screenshots, control presses,
|
|
190
205
|
the menu loop-seam watch, in-situ checks of placed art — can run in a
|
|
191
206
|
subagent while the main agent keeps building.
|
|
207
|
+
- **Content module fan-out (big scopes).** When the content contract names a
|
|
208
|
+
big world or several content systems, serial hand-typing is what runs out
|
|
209
|
+
of session: split the gameplay modules across parallel subagents instead —
|
|
210
|
+
one each for world/terrain, quest + dialogue DATA, enemies/AI, items/
|
|
211
|
+
economy — against a short written module contract (the shared state
|
|
212
|
+
object's shape, the event names, the content tables from
|
|
213
|
+
`$genex-threejs-game-content`, exact file ownership). Ship the
|
|
214
|
+
walking-skeleton v0 preview FIRST, then let the content agents land behind
|
|
215
|
+
it — the preview cadence and a long parallel build are not in conflict:
|
|
216
|
+
the user walks the skeleton while the world grows. Each subagent reports
|
|
217
|
+
deviations from the module contract; the main agent reconciles and stays
|
|
218
|
+
the only writer of the entry point.
|
|
192
219
|
|
|
193
220
|
Hard rules: **one writer per file** — each subagent owns a disjoint file set
|
|
194
221
|
(the HUD worker owns the hud modules and `public/assets/hud/`, the shepherd
|
|
@@ -209,7 +236,13 @@ and the look floor (the step-4 renderer baseline + named post stack actually
|
|
|
209
236
|
built; every placed 2D/media piece — decals, posters, in-world screens —
|
|
210
237
|
screenshot-verified in situ; no UI element left as default browser CSS; no
|
|
211
238
|
placeholder primitive left where a generated asset was
|
|
212
|
-
planned)
|
|
239
|
+
planned)**, **plus the content floor whenever a step-1 content contract
|
|
240
|
+
exists: every countable line of the contract is present and reachable in the
|
|
241
|
+
shipped game, or the user explicitly re-scoped it in chat. "Big world"
|
|
242
|
+
shipped as one small fogged arena, "quests" shipped as a single kill
|
|
243
|
+
counter, a village of textured huts where nobody speaks — each is a scope
|
|
244
|
+
violation the same way a CSS-placeholder HUD is, and it gates publish the
|
|
245
|
+
same way.** These floors gate `npx genex publish` and the
|
|
213
246
|
final handoff of a session the same way — "I never said it was done" is not
|
|
214
247
|
an exemption. The list below applies to routed *visual-system* scenes, and each
|
|
215
248
|
system-specific item (debug views, seed manifests, tier knobs) applies only when
|
|
@@ -233,8 +266,9 @@ A routed Genex scene is incomplete until it exposes:
|
|
|
233
266
|
**Publishing IS calling it done.** Before `npx genex publish`, every completion
|
|
234
267
|
gate above must pass — sprite HUD wired, Escape pause working, branded loader
|
|
235
268
|
with its key art, fonts loaded, renderer baseline + one built post effect,
|
|
236
|
-
world dressing placed or validly waived
|
|
237
|
-
|
|
269
|
+
world dressing placed or validly waived, and the content contract's
|
|
270
|
+
countables present or explicitly re-scoped by the user. If any is still
|
|
271
|
+
pending, say which and publish only after an explicit go-ahead.
|
|
238
272
|
|
|
239
273
|
Do not invent unavailable Genex service APIs. When preparing a game for Genex
|
|
240
274
|
publishing or multiplayer, inspect the project first. Prefer clean boundaries:
|
|
@@ -54,6 +54,10 @@ everything twice.
|
|
|
54
54
|
visibly crouched pose both respond. If a control calls `playOneShot()`, a
|
|
55
55
|
false result or missing-clip warning is an installation failure even when
|
|
56
56
|
locomotion continues normally.
|
|
57
|
+
During Meshy validation, record the action ID actually bound to every slot.
|
|
58
|
+
A public preview is not evidence when the game is playing a different clip
|
|
59
|
+
or a rig-basic fallback. Do not repair hands, arms, or legs at runtime: if a
|
|
60
|
+
native pose is bad or a required binding is missing, stop and regenerate.
|
|
57
61
|
3. Capture one screenshot of live gameplay — of the **game**, not a sign-in
|
|
58
62
|
gate or loading screen. A capture of the SDK's "Sign in to play" overlay is
|
|
59
63
|
NOT gameplay evidence. Evidence captured in local test mode must be labeled
|