@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,189 @@
1
+ ---
2
+ name: genex-threejs-game-content
3
+ description: Turn a content-shaped request — quests, NPCs, dialogue, shops, loot, XP — into a countable content contract and the data-driven systems that ship it. Use when the ask names game content in the plural or a content genre (RPG, adventure, story, open world), BEFORE the asset batch, and before calling such a game done.
4
+ ---
5
+
6
+ # Genex Three.js Game Content
7
+
8
+ The most common way a content-shaped request fails is not bad code — it is the
9
+ ask silently shrinking: "quests" ships as one hardcoded integer, "NPCs" as
10
+ empty textured huts, "a big world" as one small fogged plane, while every
11
+ visual floor passes, because content is invisible to a screenshot. This skill
12
+ gives content the same teeth the look has: a countable contract written up
13
+ front, data-driven systems that make each contract line cheap to ship, and a
14
+ floor that gates publish.
15
+
16
+ ## The content contract — write it before the asset batch
17
+
18
+ Run this the moment the game concept is locked, in the SAME plan message as
19
+ `$genex-threejs-game-ui`'s UI gate, whenever the request names content in the
20
+ plural (quests, enemies, bosses, locations, spells, items, factions…) or a
21
+ content genre (an RPG, an adventure, an open world, a story game, a survival
22
+ game). **Walk every plural noun of the request and make each one a countable
23
+ line.** The template:
24
+
25
+ ```
26
+ CONTENT CONTRACT — <game>
27
+ world: <size class, as a number: one arena | a district (~500 m) | open world (km-class — $genex-threejs-open-world)>
28
+ locations: <N, named: village, crypt, bandit camp, watchtower, boss lair…>
29
+ quests: <N total — a main chain of M gated steps + side quests; each giver named>
30
+ NPCs: <N speaking (quest givers, merchants) + M flavor villagers>
31
+ enemies: <N types + where they spawn; which are bosses and what makes each boss FIGHT differently>
32
+ progression: <what grows: XP/levels, gear tiers, learnable abilities — the numbers and their rewards>
33
+ economy: <currency sources AND sinks — what the player earns and what they spend it on>
34
+ minute ten: <one sentence: what is the player DOING ten minutes in, and why is it different from minute one?>
35
+ ```
36
+
37
+ Rules that make the contract real:
38
+
39
+ - **It comes before the asset batch.** The asset set derives from the
40
+ contract — a world with six locations and four enemy types needs a
41
+ different `npx genex model` list than one arena, and finding that out after
42
+ the batch means the world gets shaped around the wrong assets.
43
+ - **Every line is a floor.** Publish and the final handoff wait for the
44
+ contract's countables exactly the way they wait for the sprite HUD: a
45
+ request that said "quests" is not done with one; a request that said "big
46
+ world" is not done with an arena.
47
+ - **Scope belongs to the user.** Building a small first slice is the right
48
+ ORDER (`$genex-threejs-game-ui`'s v0 beat still applies) — but the slice is
49
+ a milestone on the way to the contract, never a quiet replacement for it.
50
+ If the full ask genuinely doesn't fit, shrinking any line is a structured
51
+ question to the user with real options — never a silent cut justified as
52
+ "standard practice".
53
+ - **Minute ten is the design test.** If the honest answer is "the same sixty
54
+ seconds, again", the contract needs another beat (a new area unlocks, a
55
+ quest chain escalates, a build comes online) before any polish work.
56
+
57
+ ## Data-driven, or you won't finish in one session
58
+
59
+ The reason a solo agent can ship seven quests and seventeen items in an
60
+ afternoon is architecture, not typing speed:
61
+
62
+ - **Systems read tables; content lives in tables.** The quest engine, the
63
+ dialogue walker, the merchant screen, and the spawner are each written
64
+ ONCE; quest #5, item #12, and enemy #4 are table rows. If adding a quest
65
+ means touching engine code, the engine is wrong.
66
+ - **Hand-author the beats, table-drive the bulk, seed the placement.** Quest
67
+ prose, boss mechanics, and location identities deserve human-quality
68
+ authoring; stats, stocks, and rewards are data; trees, rocks, and chest
69
+ scatter come from a seeded RNG so "more world" costs zero authoring.
70
+ - **One event bus.** Quests advance on events the game already emits
71
+ (`enemy:died`, `item:pickup`, `chest:opened`, `npc:talked`, `zone:entered`)
72
+ — the quest system subscribes; combat and loot never know quests exist.
73
+ This is also what makes quest logic testable in isolation.
74
+
75
+ The full engine — quest defs + state machine, dialogue trees with quest
76
+ hooks, merchant stocks, XP curve — is in
77
+ [references/content-tables.md](references/content-tables.md) as copy-paste
78
+ modules. Copy them and fill the tables; don't re-derive the shape.
79
+
80
+ ## Quests: defs + events, never an integer
81
+
82
+ The minimum honest quest system is a table of defs and an event-driven state
83
+ machine (`locked → available → active → ready → done`), with prereq gating for
84
+ the main chain:
85
+
86
+ ```ts
87
+ type Objective =
88
+ | { kind: "kill"; target: string; count: number }
89
+ | { kind: "collect"; item: string; count: number }
90
+ | { kind: "reach"; location: string }
91
+ | { kind: "talk"; npc: string };
92
+
93
+ type QuestDef = {
94
+ id: string;
95
+ giver: string; // NPC id — every quest has a face, not a board
96
+ prereq?: string; // quest id that must be done first (main chain)
97
+ title: string;
98
+ brief: string; // 2–3 sentences of authored prose, not filler
99
+ objective: Objective;
100
+ rewards: { gold?: number; xp?: number; items?: string[] };
101
+ };
102
+ ```
103
+
104
+ Progress comes only from the event bus (see the reference for the ~60-line
105
+ engine). A tracked quest gets a compass/journal marker
106
+ (`$genex-threejs-game-ui` inventories the journal as a screen element), and
107
+ turn-in happens in dialogue — handing in a quest should feel like talking to
108
+ a person, not watching a counter flip.
109
+
110
+ ## Dialogue: trees with quest hooks
111
+
112
+ Quest givers and merchants speak. The walker is a dozen lines (reference file)
113
+ over nodes of `{ text, options: [{ label, next | action }] }`; what makes it a
114
+ QUEST system is three dynamic option states injected per NPC: **offer** (quest
115
+ available → "I might have work for you"), **remind** (active → restate the
116
+ objective), **turn-in** (ready → hand rewards, open the next chain link). Add
117
+ a couple of lore branches per named NPC — three authored lines is the
118
+ difference between a person and a signpost. A quest board is an acceptable
119
+ extra for arcade-shaped games; it is never the replacement for speaking
120
+ NPCs when the ask said "NPCs".
121
+
122
+ ## Items, shops, and an economy that closes
123
+
124
+ An item catalog (id, kind, price, stats), stack-based inventory, and merchant
125
+ stock tables per vendor NPC — all data (reference file). The rule that keeps
126
+ it a game: **currency needs sinks.** Gold the player can only accumulate is a
127
+ score with a coin icon; gold that buys potions, a better sword, and a spell
128
+ tome is an economy. Price the first upgrade to be affordable after the first
129
+ quest, and let drops + chest loot + quest rewards all feed the same wallet.
130
+
131
+ ## Progression: something must grow
132
+
133
+ Pick at least one growth axis and wire its rewards into the quest/enemy
134
+ tables: an XP curve (`xpNext = 100 * level ** 1.4` is a fine default) with
135
+ flat stat gains per level, gear tiers on the merchant, or learnable abilities
136
+ gated behind tomes/trainers. The contract names which; "nothing grows" is what
137
+ makes minute ten feel like minute one.
138
+
139
+ ## NPCs: the minimum that reads as alive
140
+
141
+ A named NPC with a role, a home spot, an idle bob, a face-the-player turn
142
+ within a few meters, and dialogue reads as a person — schedules and pathing
143
+ are optional upgrades, speech is not. Place speaking NPCs (givers, merchants)
144
+ by hand at their locations; scatter flavor villagers with one-line barks from
145
+ a table. An empty textured hut village fails the "NPCs" line of any contract.
146
+ For host-simulated NPCs/enemies in multiplayer, `$genex-threejs-multiplayer`
147
+ owns the authority rules.
148
+
149
+ ## Content that can't dead-end — check before shipping
150
+
151
+ - A kill/collect quest counts progress made BEFORE acceptance (or the brief
152
+ says why not) — "kill 5 bandits" accepted after clearing the camp must not
153
+ strand at 0/5.
154
+ - A quest item from a unique source (a boss drop, a one-time chest) must
155
+ persist until picked up — the source never respawns, so expiring the drop
156
+ dead-ends the chain.
157
+ - The main chain gates on prereqs, not on geography alone — reaching the
158
+ final lair early should show a locked door or a warning, not a sequence
159
+ break that skips the story.
160
+ - The turn-in NPC is reachable after the objective (didn't die in the wave,
161
+ isn't locked behind the boss arena).
162
+ - Saves restore quest state AND its world side-effects — the opened chest
163
+ stays open, the armed boss stays armed. Quest stages, inventory, XP, and
164
+ gold go in the per-player slot via `$genex-threejs-embed-auth`.
165
+
166
+ ## Wiring into the rest of the pack
167
+
168
+ - World layout, terrain, and location placement at scale:
169
+ `$genex-threejs-open-world` (the contract's `world:` line decides whether
170
+ it loads).
171
+ - Quest journal, tracker, toasts, vendor screens: inventory them at
172
+ `$genex-threejs-game-ui`'s gate — they are screen elements like any other.
173
+ - Level-ups, quest completion, and boss kills are exactly the "moments" the
174
+ feel pass layers feedback on: `$genex-threejs-game-feel` + a real
175
+ `npx genex sfx` fanfare (`$genex-ai-sfx`).
176
+ - Location set pieces and speaking-NPC meshes: `$genex-ai-model` /
177
+ `$genex-ai-character` — generated assets decorate the contract's landmarks;
178
+ they never decide how many exist.
179
+
180
+ ## Failure modes to catch
181
+
182
+ - "Quests" (plural, in the ask) shipped as one quest — a stage integer with
183
+ hardcoded strings and no giver.
184
+ - A village of textured huts where nobody speaks.
185
+ - A gold counter with nothing to spend it on.
186
+ - No growth axis: the player at minute thirty plays exactly like minute one.
187
+ - The contract posted, then quietly abandoned when the first slice previewed
188
+ well — the slice is a milestone, not the destination.
189
+ - Contract lines silently shrunk without a structured question to the user.
@@ -0,0 +1,269 @@
1
+ # Genex game content — copy-paste systems
2
+
3
+ Four small modules that turn contract lines into shipped content: an event
4
+ bus, a quest engine, a dialogue walker with quest hooks, and items/economy +
5
+ XP. Copy them, fill the tables, and keep content OUT of engine code — adding
6
+ a quest, an item, or a vendor must always be a table edit.
7
+
8
+ All snippets are plain vanilla-ts (no enums, no decorators) and assume a
9
+ single shared game-state object `G` you already have (player stats, wallet,
10
+ the scene). Adapt names, not shapes.
11
+
12
+ ## 1. Event bus — the only coupling
13
+
14
+ ```ts
15
+ // events.ts — combat, loot, and movement EMIT; quests and UI SUBSCRIBE.
16
+ type GameEvent =
17
+ | { type: "enemy:died"; enemyKind: string }
18
+ | { type: "item:pickup"; item: string; count: number }
19
+ | { type: "chest:opened"; chestId: string }
20
+ | { type: "npc:talked"; npc: string }
21
+ | { type: "zone:entered"; location: string };
22
+
23
+ type Handler = (e: GameEvent) => void;
24
+ const handlers: Handler[] = [];
25
+
26
+ export function onEvent(h: Handler): void {
27
+ handlers.push(h);
28
+ }
29
+ export function emit(e: GameEvent): void {
30
+ for (const h of handlers) h(e);
31
+ }
32
+ ```
33
+
34
+ ## 2. Quest engine — defs are data, progress is events
35
+
36
+ ```ts
37
+ // quests.ts
38
+ import { onEvent } from "./events.ts";
39
+
40
+ type Objective =
41
+ | { kind: "kill"; target: string; count: number }
42
+ | { kind: "collect"; item: string; count: number }
43
+ | { kind: "reach"; location: string }
44
+ | { kind: "talk"; npc: string };
45
+
46
+ type QuestDef = {
47
+ id: string;
48
+ giver: string;
49
+ prereq?: string;
50
+ title: string;
51
+ brief: string;
52
+ turnIn: string; // authored prose for the hand-in moment
53
+ objective: Objective;
54
+ rewards: { gold?: number; xp?: number; items?: string[] };
55
+ };
56
+
57
+ // CONTENT — the whole quest list lives here. Main chain via prereq; the
58
+ // prose is authored, not generated filler. Counts must match the contract.
59
+ export const QUESTS: QuestDef[] = [
60
+ {
61
+ id: "q_wolves",
62
+ giver: "elder",
63
+ title: "Teeth in the Tall Grass",
64
+ brief:
65
+ "Wolves have taken the shepherd's flock and now they circle the palisade at dusk. Thin the pack before someone's child is next.",
66
+ turnIn: "Five pelts. The flock sleeps easy tonight — and so do we. Take this.",
67
+ objective: { kind: "kill", target: "wolf", count: 5 },
68
+ rewards: { gold: 40, xp: 60 },
69
+ },
70
+ {
71
+ id: "q_amulet",
72
+ giver: "elder",
73
+ prereq: "q_wolves",
74
+ title: "What the Crypt Keeps",
75
+ brief:
76
+ "Our founder's amulet lies in the old crypt east of the fields. The dead have grown restless around it — bring it back, and mind the narrow dark.",
77
+ turnIn: "The amulet… after all these years. You have the village's gratitude, and its coin.",
78
+ objective: { kind: "collect", item: "founder_amulet", count: 1 },
79
+ rewards: { gold: 80, xp: 120, items: ["potion_health"] },
80
+ },
81
+ // …side quests: no prereq, different givers, different objective kinds.
82
+ ];
83
+
84
+ type QuestStatus = "locked" | "available" | "active" | "ready" | "done";
85
+ type QuestState = { status: QuestStatus; progress: number };
86
+
87
+ export const questState = new Map<string, QuestState>();
88
+ for (const q of QUESTS) {
89
+ questState.set(q.id, { status: q.prereq ? "locked" : "available", progress: 0 });
90
+ }
91
+
92
+ const def = (id: string): QuestDef => QUESTS.find((q) => q.id === id)!;
93
+
94
+ export function accept(id: string): void {
95
+ const s = questState.get(id)!;
96
+ if (s.status !== "available") return;
97
+ s.status = "active";
98
+ // Contract check "can't dead-end": count pre-acceptance progress where the
99
+ // fiction allows it (kills already made), or reset knowingly.
100
+ }
101
+
102
+ export function turnIn(id: string): { gold: number; xp: number; items: string[] } | null {
103
+ const s = questState.get(id)!;
104
+ if (s.status !== "ready") return null;
105
+ s.status = "done";
106
+ for (const q of QUESTS) {
107
+ if (q.prereq === id && questState.get(q.id)!.status === "locked") {
108
+ questState.get(q.id)!.status = "available"; // next chain link opens
109
+ }
110
+ }
111
+ const r = def(id).rewards;
112
+ return { gold: r.gold ?? 0, xp: r.xp ?? 0, items: r.items ?? [] };
113
+ }
114
+
115
+ function bump(q: QuestDef, s: QuestState, amount = 1): void {
116
+ const needed = "count" in q.objective ? q.objective.count : 1;
117
+ s.progress = Math.min(needed, s.progress + amount);
118
+ if (s.progress >= needed) s.status = "ready"; // journal + marker flip to "return"
119
+ }
120
+
121
+ onEvent((e) => {
122
+ for (const q of QUESTS) {
123
+ const s = questState.get(q.id)!;
124
+ if (s.status !== "active") continue;
125
+ const o = q.objective;
126
+ if (o.kind === "kill" && e.type === "enemy:died" && e.enemyKind === o.target) bump(q, s);
127
+ if (o.kind === "collect" && e.type === "item:pickup" && e.item === o.item) bump(q, s, e.count);
128
+ if (o.kind === "reach" && e.type === "zone:entered" && e.location === o.location) bump(q, s);
129
+ if (o.kind === "talk" && e.type === "npc:talked" && e.npc === o.npc) bump(q, s);
130
+ }
131
+ });
132
+ ```
133
+
134
+ Journal UI, the tracked-quest compass marker, and toasts subscribe to the
135
+ same state — inventory them as screen elements at `$genex-threejs-game-ui`'s
136
+ gate. Persist `[...questState]` (plus world side-effects like opened chests)
137
+ in the per-player slot via `$genex-threejs-embed-auth`.
138
+
139
+ ## 3. Dialogue walker — quest states become options
140
+
141
+ ```ts
142
+ // dialogue.ts
143
+ import { QUESTS, questState, accept, turnIn } from "./quests.ts";
144
+
145
+ type DialogueOption = { label: string; next?: DialogueNode; action?: () => void };
146
+ type DialogueNode = { text: string; options: DialogueOption[] };
147
+
148
+ // CONTENT — per-NPC roots: greeting + a couple of authored lore branches.
149
+ // Three real lines is the difference between a person and a signpost.
150
+ const ROOTS: Record<string, DialogueNode> = {
151
+ elder: {
152
+ text: "Maren watches the road as she talks. “Strangers used to mean trade. Lately they mean trouble.”",
153
+ options: [
154
+ {
155
+ label: "Tell me about this village.",
156
+ next: {
157
+ text: "“Three generations behind this palisade. The crypt east of here is older than all of it — and lately, louder.”",
158
+ options: [],
159
+ },
160
+ },
161
+ ],
162
+ },
163
+ // blacksmith, herbalist… — every giver and merchant has a root.
164
+ };
165
+
166
+ // The whole walker: render node.text + numbered options; a click (or the
167
+ // 1–9 key) runs option.action?.(), then shows option.next or closes. Emit
168
+ // { type: "npc:talked", npc } when a conversation OPENS — that's what "talk"
169
+ // objectives listen for.
170
+ export function pickOption(node: DialogueNode, index: number): DialogueNode | null {
171
+ const opt = node.options[index];
172
+ if (!opt) return node;
173
+ opt.action?.();
174
+ return opt.next ?? null; // null = close the dialogue panel
175
+ }
176
+
177
+ export function openDialogue(npc: string): DialogueNode {
178
+ const root = ROOTS[npc];
179
+ const options = [...root.options];
180
+ for (const q of QUESTS) {
181
+ if (q.giver !== npc) continue;
182
+ const s = questState.get(q.id)!;
183
+ if (s.status === "available") {
184
+ options.unshift({
185
+ label: `[Quest] ${q.title}`,
186
+ next: {
187
+ text: q.brief,
188
+ options: [
189
+ { label: "I'll do it.", action: () => accept(q.id) },
190
+ { label: "Not now." },
191
+ ],
192
+ },
193
+ });
194
+ } else if (s.status === "active") {
195
+ options.unshift({ label: `[${q.title}] Remind me.`, next: { text: q.brief, options: [] } });
196
+ } else if (s.status === "ready") {
197
+ options.unshift({
198
+ label: `[Complete] ${q.title}`,
199
+ next: { text: q.turnIn, options: [] },
200
+ action: () => {
201
+ const r = turnIn(q.id);
202
+ // …grant r.gold / r.xp / r.items through your wallet + inventory.
203
+ },
204
+ });
205
+ }
206
+ }
207
+ return { text: root.text, options };
208
+ }
209
+ ```
210
+
211
+ ## 4. Items, merchants, XP — the economy that closes
212
+
213
+ ```ts
214
+ // items.ts
215
+ type ItemDef = {
216
+ id: string;
217
+ kind: "weapon" | "armor" | "potion" | "quest" | "tome";
218
+ name: string;
219
+ price: number; // what merchants charge — the SINK side of the economy
220
+ stats?: { dmg?: number; armor?: number; heal?: number };
221
+ };
222
+
223
+ export const ITEMS: ItemDef[] = [
224
+ { id: "sword_iron", kind: "weapon", name: "Iron Sword", price: 120, stats: { dmg: 18 } },
225
+ { id: "potion_health", kind: "potion", name: "Health Draught", price: 25, stats: { heal: 40 } },
226
+ { id: "founder_amulet", kind: "quest", name: "Founder's Amulet", price: 0 },
227
+ // …the catalog. Price the first upgrade to land right after the first quest's gold.
228
+ ];
229
+
230
+ // Merchant stock is per-NPC data — a second vendor is one more entry.
231
+ export const STOCKS: Record<string, string[]> = {
232
+ blacksmith: ["sword_iron", "armor_leather"],
233
+ herbalist: ["potion_health", "potion_mana"],
234
+ };
235
+
236
+ // XP curve + level rewards: one growth axis, wired to quest/enemy rewards.
237
+ export const xpNext = (level: number): number => Math.round(100 * level ** 1.4);
238
+ export function addXp(player: { level: number; xp: number; maxHp: number; hp: number }, amount: number): void {
239
+ player.xp += amount;
240
+ while (player.xp >= xpNext(player.level)) {
241
+ player.xp -= xpNext(player.level);
242
+ player.level += 1;
243
+ player.maxHp += 12;
244
+ player.hp = player.maxHp; // level-up heals — a reward the player FEELS
245
+ }
246
+ }
247
+ ```
248
+
249
+ ## Seeded placement — bulk content for free
250
+
251
+ Scatter the non-authored bulk (chests, camps, flavor spawns) with a seeded
252
+ RNG so the world is deterministic across loads and machines:
253
+
254
+ ```ts
255
+ export function seededRng(seed: number): () => number {
256
+ let s = seed >>> 0;
257
+ return () => {
258
+ s = (s * 1664525 + 1013904223) >>> 0;
259
+ return s / 0xffffffff;
260
+ };
261
+ }
262
+ // const rng = seededRng(1337): place 10 chests at rng()-driven offsets around
263
+ // locations, skip water/steep slopes via the world's height/biome lookups
264
+ // ($genex-threejs-open-world), and hand-place only the authored few.
265
+ ```
266
+
267
+ Hand-authored where it matters (quest prose, boss mechanics, unique loot),
268
+ tables for the bulk, seeds for the scatter — that split is what lets one
269
+ session ship the whole contract.
@@ -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`).