@genex-ai/cli-demo 0.62.0-dev.143 → 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.
@@ -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)**. These floors gate `npx genex publish` and the
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. If any is still pending, say which
237
- and publish only after an explicit go-ahead.
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: