@vibes.diy/prompts 14.3.32 → 14.3.36

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/llms/backend.md CHANGED
@@ -384,8 +384,24 @@ const gh = await ctx.github.fetch("/repos/acme/site/issues", { method: "POST", b
384
384
  calling convention (codegen has been observed passing these positionally).
385
385
  The message lands in that person's bell, inbox, and push notifications.
386
386
  - `toHandle` is one handle, and the platform delivers to people already in
387
- this app: the owner, an accepted invite or approved request, or a handle
388
- named in the access function's persisted grant state.
387
+ this app: the owner, who always qualifies, an accepted invite or approved
388
+ request, a handle named in the access function's persisted grant state, or
389
+ somebody whose authenticated write reached the app (anonymous writes don't
390
+ count). Invite, request, and writer membership resolve the person through
391
+ their bound handle; the grant-state clause matches the named handle
392
+ itself. Explicit revocations deny anyone but the owner. A passing check
393
+ plus valid fields writes the notification into the inbox history of a
394
+ recipient the platform can resolve — an unresolvable handle fails the call
395
+ after the participation check. Their switches silence the live bell, the
396
+ push, and the email — not the history.
397
+ - When the notification is the opt-in kind — your turn, your order shipped —
398
+ seat the player the way a guest takes their own seat.
399
+ A turn notification goes to a seat its owner took themselves.
400
+ A join write where the added handle must equal `user.userHandle` qualifies
401
+ the writer by their own act. This is an app-side convention for notifiable
402
+ seats, not a rule about read grants: chosen-peer grants that share without
403
+ notifying — inviting a collaborator into a shared list — stay intentional,
404
+ and a grant-named handle stays eligible whether or not its owner seated it.
389
405
  - `body` is the whole message — plain text, 500 chars max. The platform
390
406
  titles it with the app's name and links it back to the app, so spend the
391
407
  body on what happened and who it concerns.
package/llms/three-js.md CHANGED
@@ -1000,7 +1000,13 @@ gltf.scene.traverse((child) => {
1000
1000
 
1001
1001
  ### Minecraft World Generator
1002
1002
 
1003
- Procedural voxel terrain with optimized geometry merging.
1003
+ Procedural voxel terrain. **For a block world, the voxel skill is the
1004
+ authority**: it draws each chunk as one `InstancedMesh` of the exposed blocks
1005
+ (or Block Craft's per-face `BufferGeometry` batching) and never a `Mesh` per
1006
+ block — follow it rather than the merge sketch below, which is only the noise
1007
+ half. The same rule holds outside voxels: many things of one shape are one
1008
+ `InstancedMesh` or `Points`, never a `Mesh` each, and nothing allocates a
1009
+ geometry or material inside the frame loop.
1004
1010
 
1005
1011
  ```javascript
1006
1012
  import { ImprovedNoise } from "three/addons/math/ImprovedNoise.js";
@@ -0,0 +1,244 @@
1
+ # Voxel worlds — the first draft: a viewport that plays
2
+
3
+ A voxel app's first draft is a full-screen 3D viewport with a block world you
4
+ can fly through and look around, drawn the fast way, with the HUD floating over
5
+ it. Placing and breaking blocks, walking with collision, endless terrain,
6
+ saving edits and sharing the world are the next pass. What the draft owes that
7
+ pass is the right skeleton: the world lives in a flat `Map`, the terrain is
8
+ drawn as one `InstancedMesh`, and the engine is built once inside an effect —
9
+ so the next pass adds to this file rather than rewriting it.
10
+
11
+ Unlike the page-shaped draft above, a world does not flow down the page: the
12
+ viewport is `fixed inset-0` and the controls sit on top of it.
13
+
14
+ ### `App.jsx`
15
+ ```jsx
16
+ import React, { useEffect, useRef, useState } from "react";
17
+ import { useFireproof } from "use-fireproof";
18
+ import * as THREE from "three";
19
+ import { Button, Card, CardHeader, CardTitle, CardContent } from "@vibes.diy/look";
20
+
21
+ // The block-in-hand palette (shared by materials and HUD swatches).
22
+ const VOXEL_PALETTE = {
23
+ grass: "#6fcf5b", dirt: "#8a5a3b", stone: "#9b9b9b", wood: "#b5793a", glass: "#9fe3ff", candy: "#ff6fb1",
24
+ neon: "#39ff9e", water: "#2f8fe0", lava: "#ff5a2f", erase: "#ff4d6d", ink: "#1a1325", light: "#ffffff",
25
+ };
26
+ const HOTBAR = ["grass", "dirt", "stone", "wood"];
27
+ const GAME_KEYS = new Set(["KeyW", "KeyA", "KeyS", "KeyD", "KeyC", "Space", "ShiftLeft"]);
28
+ const SIZE = 24; // half-width of the starting island, in blocks
29
+ const SEED = 7;
30
+
31
+ const keyOf = (x, y, z) => `${x}|${y}|${z}`;
32
+ const heightAt = (x, z) => Math.round(Math.sin((x + SEED) * 0.18) * 2 + Math.cos((z - SEED) * 0.15) * 2 + Math.sin((x + z) * 0.06) * 3) + 5;
33
+
34
+ // The world model: every block, buried or not. Collision and picking read this, never the scene.
35
+ function buildBlocks() {
36
+ const blocks = new Map();
37
+ for (let x = -SIZE; x <= SIZE; x++)
38
+ for (let z = -SIZE; z <= SIZE; z++) {
39
+ const h = heightAt(x, z);
40
+ for (let y = 0; y <= h; y++) blocks.set(keyOf(x, y, z), y === h ? "grass" : y > h - 3 ? "dirt" : "stone");
41
+ }
42
+ return blocks;
43
+ }
44
+
45
+ const NB = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
46
+ const isHidden = (blocks, x, y, z) => NB.every(([dx, dy, dz]) => blocks.has(keyOf(x + dx, y + dy, z + dz)));
47
+
48
+ // One shared cube, one shared material, one draw call: the exposed blocks as instances with a color each.
49
+ const CUBE = new THREE.BoxGeometry(1, 1, 1);
50
+ const WORLD_MAT = new THREE.MeshLambertMaterial({ color: 0xffffff });
51
+ function buildWorldMesh(blocks) {
52
+ const visible = [];
53
+ for (const [k, type] of blocks) {
54
+ const [x, y, z] = k.split("|").map(Number);
55
+ if (!isHidden(blocks, x, y, z)) visible.push([x, y, z, type]);
56
+ }
57
+ const mesh = new THREE.InstancedMesh(CUBE, WORLD_MAT, visible.length);
58
+ const m = new THREE.Matrix4();
59
+ const c = new THREE.Color();
60
+ visible.forEach(([x, y, z, type], i) => {
61
+ mesh.setMatrixAt(i, m.makeTranslation(x + 0.5, y + 0.5, z + 0.5));
62
+ mesh.setColorAt(i, c.set(VOXEL_PALETTE[type]));
63
+ });
64
+ mesh.instanceMatrix.needsUpdate = true;
65
+ mesh.instanceColor.needsUpdate = true;
66
+ return mesh;
67
+ }
68
+
69
+ export default function App() {
70
+ const { useLiveQuery } = useFireproof("skyIsland");
71
+ const { docs: worlds } = useLiveQuery("type", { key: "world" });
72
+ const world = worlds[0];
73
+ const mountRef = useRef(null);
74
+ const engineRef = useRef(null);
75
+ const [locked, setLocked] = useState(false);
76
+ const [held, setHeld] = useState(HOTBAR[0]);
77
+
78
+ // The engine, built once: renderer, scene, world mesh, input, loop. React only draws the HUD over it.
79
+ useEffect(() => {
80
+ const mount = mountRef.current;
81
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
82
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
83
+ renderer.setSize(mount.clientWidth, mount.clientHeight);
84
+ mount.appendChild(renderer.domElement);
85
+
86
+ const scene = new THREE.Scene();
87
+ scene.background = new THREE.Color(0x8ecbff);
88
+ scene.fog = new THREE.Fog(0x8ecbff, 40, 140);
89
+ const camera = new THREE.PerspectiveCamera(75, mount.clientWidth / mount.clientHeight, 0.1, 500);
90
+ scene.add(new THREE.HemisphereLight(0xffffff, 0x8a5a3b, 0.9));
91
+ const sun = new THREE.DirectionalLight(0xffffff, 1);
92
+ sun.position.set(30, 60, 20);
93
+ scene.add(sun);
94
+
95
+ const blocks = buildBlocks();
96
+ const worldMesh = buildWorldMesh(blocks);
97
+ scene.add(worldMesh);
98
+
99
+ const state = { pos: new THREE.Vector3(0, heightAt(0, 0) + 3, 8), yaw: 0, pitch: -0.2, keys: {}, running: true };
100
+ const canvas = renderer.domElement;
101
+ const onLock = () => setLocked(document.pointerLockElement === canvas);
102
+ const onMove = (e) => {
103
+ if (document.pointerLockElement !== canvas) return;
104
+ state.yaw -= e.movementX * 0.002;
105
+ state.pitch = Math.max(-1.5, Math.min(1.5, state.pitch - e.movementY * 0.002));
106
+ };
107
+ const onKeyDown = (e) => {
108
+ if (document.pointerLockElement !== canvas) return; // only while the world holds the mouse
109
+ if (e.target.isContentEditable || e.target.closest("input, textarea")) return; // typing stays typing
110
+ if (!GAME_KEYS.has(e.code)) return; // browser shortcuts stay browser shortcuts
111
+ state.keys[e.code] = true;
112
+ e.preventDefault(); // Space flies up, never scrolls the page
113
+ };
114
+ const onKeyUp = (e) => {
115
+ state.keys[e.code] = false;
116
+ };
117
+ const onBlur = () => { state.keys = {}; };
118
+ const onResize = () => {
119
+ renderer.setSize(mount.clientWidth, mount.clientHeight);
120
+ camera.aspect = mount.clientWidth / mount.clientHeight;
121
+ camera.updateProjectionMatrix();
122
+ };
123
+ document.addEventListener("pointerlockchange", onLock);
124
+ document.addEventListener("mousemove", onMove);
125
+ window.addEventListener("keydown", onKeyDown);
126
+ window.addEventListener("keyup", onKeyUp);
127
+ window.addEventListener("blur", onBlur);
128
+ window.addEventListener("resize", onResize);
129
+ engineRef.current = { requestLock: () => canvas.requestPointerLock() };
130
+
131
+ const clock = new THREE.Clock();
132
+ const forward = new THREE.Vector3();
133
+ const right = new THREE.Vector3();
134
+ let raf;
135
+ const loop = () => {
136
+ if (!state.running) return;
137
+ const dt = Math.min(clock.getDelta(), 0.05);
138
+ const speed = (state.keys.ShiftLeft ? 18 : 9) * dt;
139
+ forward.set(-Math.sin(state.yaw), 0, -Math.cos(state.yaw));
140
+ right.set(forward.z, 0, -forward.x);
141
+ if (state.keys.KeyW) state.pos.addScaledVector(forward, speed);
142
+ if (state.keys.KeyS) state.pos.addScaledVector(forward, -speed);
143
+ if (state.keys.KeyD) state.pos.addScaledVector(right, speed);
144
+ if (state.keys.KeyA) state.pos.addScaledVector(right, -speed);
145
+ if (state.keys.Space) state.pos.y += speed;
146
+ if (state.keys.KeyC) state.pos.y -= speed;
147
+ camera.position.copy(state.pos);
148
+ camera.rotation.set(state.pitch, state.yaw, 0, "YXZ");
149
+ renderer.render(scene, camera);
150
+ raf = requestAnimationFrame(loop);
151
+ };
152
+ loop();
153
+
154
+ return () => {
155
+ state.running = false;
156
+ cancelAnimationFrame(raf);
157
+ document.removeEventListener("pointerlockchange", onLock);
158
+ document.removeEventListener("mousemove", onMove);
159
+ window.removeEventListener("keydown", onKeyDown);
160
+ window.removeEventListener("keyup", onKeyUp);
161
+ window.removeEventListener("blur", onBlur);
162
+ window.removeEventListener("resize", onResize);
163
+ worldMesh.dispose();
164
+ renderer.dispose();
165
+ mount.removeChild(canvas);
166
+ };
167
+ }, []);
168
+
169
+ return (
170
+ <div className="fixed inset-0 select-none touch-none bg-background text-foreground font-sans">
171
+ <div ref={mountRef} className="absolute inset-0" />
172
+
173
+ <div className="absolute left-4 top-4 rounded-lg bg-card/90 px-3 py-2 text-card-foreground shadow-md">
174
+ <p className="font-bold">{world?.name ?? "Sky Island"}</p>
175
+ <p className="text-muted-foreground">WASD fly · Space up · C down · Shift fast</p>
176
+ </div>
177
+
178
+ <div className="absolute bottom-4 left-1/2 flex -translate-x-1/2 gap-2 rounded-lg bg-card/90 p-2 shadow-md">
179
+ {HOTBAR.map((type) => (
180
+ <Button key={type} size="sm" variant={type === held ? "default" : "outline"} onClick={() => setHeld(type)}
181
+ style={{ backgroundColor: type === held ? VOXEL_PALETTE[type] : undefined }}>
182
+ {type}
183
+ </Button>
184
+ ))}
185
+ </div>
186
+ {/* TODO(next pass): place and break the held block with a voxel raycast, walk with gravity and collision, save edits as documents */}
187
+
188
+ {!locked && (
189
+ <button onClick={() => engineRef.current?.requestLock()} className="absolute inset-0 z-20 flex items-center justify-center bg-black/55">
190
+ <Card className="max-w-sm">
191
+ <CardHeader>
192
+ <CardTitle>{world?.name ?? "Sky Island"}</CardTitle>
193
+ </CardHeader>
194
+ <CardContent>
195
+ <p>Click to play</p>
196
+ <p className="mt-2 text-muted-foreground">Mouse looks around. Esc gives the mouse back.</p>
197
+ </CardContent>
198
+ </Card>
199
+ </button>
200
+ )}
201
+ </div>
202
+ );
203
+ }
204
+ ```
205
+
206
+ ### `seed.json`
207
+ ```json
208
+ {
209
+ "skyIsland": [
210
+ { "key": "island", "type": "world", "name": "Sky Island", "sky": "clear" }
211
+ ]
212
+ }
213
+ ```
214
+
215
+ Draft note: the next pass adds placing and breaking blocks, walking with gravity and collision, saved edits per player, and inviting friends into the world.
216
+
217
+ ## What stays the same in a voxel draft
218
+
219
+ - **The viewport fills the screen**: the outer element is `fixed inset-0
220
+ select-none touch-none`, the renderer mounts into an `absolute inset-0` div,
221
+ and every control is an `absolute` layer over it. Never the page-flow `main`
222
+ with padding from the shape above.
223
+ - **The engine is built once** in `useEffect(() => {...}, [])` with the
224
+ renderer, scene, world mesh, listeners and loop inside it, `engineRef.current` for the few calls
225
+ React makes in, and a cleanup that stops the loop, removes every listener
226
+ and disposes the mesh and renderer. Per-frame values live in the `state`
227
+ object, never in React state.
228
+ - **`setPixelRatio(Math.min(window.devicePixelRatio, 2))`**, a resize handler
229
+ that updates the renderer size and the camera aspect, and a `Fog` that
230
+ matches the sky color.
231
+ - **The world is a flat `Map` keyed `"x|y|z"`**, and it is drawn as ONE
232
+ `InstancedMesh` of the exposed blocks with one shared `BoxGeometry` and one
233
+ shared material, the color per instance. Never a `THREE.Mesh` per block, and
234
+ never a geometry or material created inside the block loop.
235
+ - **Pointer lock comes from a "Click to play" overlay** whose visibility follows
236
+ the real `pointerlockchange` state, so it returns when the player presses
237
+ Esc. Mouse look clamps pitch to ±1.5 rad. A keydown calls `preventDefault`
238
+ only when the canvas holds the lock, the target is not a text field, and the
239
+ key is in the game's own set; held keys clear on blur.
240
+ - **The palette table** is the Blocktopia voxel palette, in full, named once
241
+ for both the instance colors and the HUD swatches.
242
+ - **What waits for the next pass** is named in the `TODO(next pass)` comment
243
+ and the draft note: placing and breaking, collision and gravity, endless
244
+ terrain, saving edits, sharing. The draft draws none of it.
package/llms/voxel.js CHANGED
@@ -13,6 +13,7 @@ export const voxelConfig = {
13
13
  "minecraft, voxel, block world, sandbox, blocks, mining, building, first-person, 3d game, " +
14
14
  "procedural world, chunks, terrain, overworld, creative mode, survival, crafting, nether",
15
15
  cues: ["voxel", "voxels", "minecraft", "block world", "blocky", "sandbox game"],
16
+ draftVariant: true,
16
17
  importModule: "three",
17
18
  importName: "THREE",
18
19
  importType: "namespace",
package/llms/voxel.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"voxel.js","sourceRoot":"","sources":["../../jsr/llms/voxel.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,WAAW,GAAc;IACpC,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,WAAW,EACT,yFAAyF;QACzF,6FAA6F;QAC7F,4FAA4F;QAC5F,yFAAyF;QACzF,uFAAuF;QACvF,gGAAgG;QAChG,2FAA2F;QAC3F,yFAAyF;QACzF,6CAA6C;QAC7C,2FAA2F;QAC3F,yFAAyF;IAC3F,IAAI,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,CAAC;IAC/E,YAAY,EAAE,OAAO;IACrB,UAAU,EAAE,OAAO;IACnB,UAAU,EAAE,WAAW;CACxB,CAAC"}
1
+ {"version":3,"file":"voxel.js","sourceRoot":"","sources":["../../jsr/llms/voxel.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,WAAW,GAAc;IACpC,IAAI,EAAE,OAAO;IACb,KAAK,EAAE,OAAO;IACd,WAAW,EACT,yFAAyF;QACzF,6FAA6F;QAC7F,4FAA4F;QAC5F,yFAAyF;QACzF,uFAAuF;QACvF,gGAAgG;QAChG,2FAA2F;QAC3F,yFAAyF;QACzF,6CAA6C;QAC7C,2FAA2F;QAC3F,yFAAyF;IAC3F,IAAI,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,CAAC;IAG/E,YAAY,EAAE,IAAI;IAClB,YAAY,EAAE,OAAO;IACrB,UAAU,EAAE,OAAO;IACnB,UAAU,EAAE,WAAW;CACxB,CAAC"}
package/llms/voxel.md CHANGED
@@ -13,9 +13,10 @@ veins, endless chunk streaming, day/night, live multiplayer edits, ambient
13
13
  wildlife — and its bigger successor **Blockcraft World**, which adds the full
14
14
  survival layer: crafting tiers, tool durability, smelting, armor, mob combat,
15
15
  hunger, creative mode, nether/end dimensions, per-player inventory, and a
16
- shared world reset. **Blocktopia** contributes material-colored controls,
17
- compact procedural island settings, and sparse per-cell edits; **Block Craft**
18
- contributes exposed-face chunk meshes and survival feedback. Adapt the useful
16
+ shared world reset. **Blocktopia** contributes the instanced chunk renderer,
17
+ material-colored controls, compact procedural island settings, and sparse
18
+ per-cell edits; **Block Craft** contributes exposed-face chunk meshes and
19
+ survival feedback. Adapt the useful
19
20
  patterns to the persistence contracts below: whole-world autosaves and durable
20
21
  movement heartbeats are expensive examples to improve, rather than defaults.
21
22
 
@@ -112,7 +113,8 @@ const state = {
112
113
  blocks: new Map(), // "x|y|z" -> type (the loaded world, terrain + edits)
113
114
  chunks: new Map(), // "cx,cz" -> Set(blockKey) currently-resident chunks
114
115
  edits: new Map(), // "x|y|z" -> type|null player edits layered over terrain
115
- meshes: new Map(), // "x|y|z" -> THREE.Mesh (only for EXPOSED blocks)
116
+ chunkMeshes: new Map(), // "cx,cz" -> THREE.InstancedMesh (EXPOSED blocks only, one per chunk)
117
+ dirty: new Set(), // "cx,cz" chunks whose mesh must be rebuilt after an edit
116
118
  pos: new THREE.Vector3(0, 20, 0),
117
119
  vel: new THREE.Vector3(),
118
120
  yaw: 0, pitch: 0, keys: {}, onGround: false, fly: false,
@@ -196,12 +198,17 @@ outline: the accent reinforces the selected mode rather than being its only
196
198
  signal. On touch, keep movement/look separate from Place/Break/Jump controls,
197
199
  and clear held input on pointer cancellation or lost capture.
198
200
 
199
- ## Rendering: mesh only the EXPOSED blocks (this is the whole performance game)
201
+ ## Rendering: instance the EXPOSED blocks (this is the whole performance game)
200
202
 
201
203
  The single most important rule: **do not create a `THREE.Mesh` per stored
202
- block.** A chunk is thousands of blocks and most are buried. Keep buried blocks
203
- in the `blocks` map (collision and raycasting read the map, not the scene), and
204
- only add a mesh when a block has at least one non-occluding neighbor.
204
+ block, and never allocate a geometry or material inside the block loop.** A
205
+ chunk is thousands of blocks and most are buried. Three.js pays one draw call
206
+ per mesh per material, so a mesh-per-block world with a six-material cube is
207
+ hundreds of thousands of draw calls a frame — CPU-bound, and a faster GPU does
208
+ not help. Keep buried blocks in the `blocks` map (collision and raycasting read
209
+ the map, not the scene) and draw the rest as **one `InstancedMesh` per chunk**.
210
+
211
+ First, the exposure test — a block with six occluding neighbors is never drawn:
205
212
 
206
213
  ```js
207
214
  const NB = [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];
@@ -209,26 +216,62 @@ function occludes(t) { return t && t !== "water" && t !== "glass" && t !== "leav
209
216
  function isHidden(x, y, z) {
210
217
  for (const [dx, dy, dz] of NB)
211
218
  if (!occludes(state.blocks.get(keyOf(x + dx, y + dy, z + dz)))) return false;
212
- return true; // fully enclosed → no mesh
219
+ return true; // fully enclosed → no instance
213
220
  }
214
221
  ```
215
222
 
216
- Share one `BoxGeometry` and one material array per type across every block (a
217
- 6-material array lets the top face differ from the sides). When a block is
218
- placed or broken, refresh that block's mesh **and its six neighbors'** — a
219
- neighbor that was buried may now be exposed, or vice-versa.
223
+ Then the chunk mesh: one shared `BoxGeometry` and one shared material for the
224
+ whole world, built once outside every loop, with the block color carried by
225
+ `instanceColor`. This is Blocktopia's terrain — the whole island is one draw
226
+ call, and the build is a `setMatrixAt` loop rather than vertex arithmetic:
220
227
 
221
- Raycasting and collision must read `state.blocks` (the full model), never the
222
- meshes — otherwise you can't hit or collide with a face that isn't currently
223
- meshed.
228
+ ```js
229
+ const CUBE = new THREE.BoxGeometry(1, 1, 1); // built ONCE
230
+ const TERRAIN_MAT = new THREE.MeshLambertMaterial({ color: 0xffffff }); // built ONCE; white so instanceColor shows
231
+ const tmpM = new THREE.Matrix4(), tmpC = new THREE.Color();
232
+
233
+ function buildChunkMesh(keys) { // keys: the chunk's resident block keys
234
+ const visible = [];
235
+ for (const k of keys) { const [x, y, z] = parseKey(k); if (!isHidden(x, y, z)) visible.push([x, y, z, state.blocks.get(k)]); }
236
+ const mesh = new THREE.InstancedMesh(CUBE, TERRAIN_MAT, visible.length);
237
+ visible.forEach(([x, y, z, type], i) => {
238
+ mesh.setMatrixAt(i, tmpM.makeTranslation(x + 0.5, y + 0.5, z + 0.5));
239
+ mesh.setColorAt(i, tmpC.setHex(BLOCK_TYPES[type].top));
240
+ });
241
+ mesh.instanceMatrix.needsUpdate = true;
242
+ mesh.instanceColor.needsUpdate = true;
243
+ mesh.frustumCulled = false; // a chunk's bounds are its blocks; let the fog bound distance
244
+ return mesh;
245
+ }
246
+ ```
224
247
 
225
- For larger worlds, use Block Craft's **exposed-face chunk batching**: append
226
- only visible faces into a `BufferGeometry` per material per chunk, sharing
227
- materials across chunks. An edit dirties its chunk and any neighboring chunk
228
- whose boundary face changes; rebuild only those chunks and dispose their old
229
- geometries. Keep transparent faces in a separate material pass with deliberate
230
- occlusion rules. The small-world shared-cube example above remains a simpler
231
- starting point.
248
+ Blocks whose *material* differs in kind — transparent water and glass, an
249
+ emissive lava — get their own `InstancedMesh` per type sharing the same `CUBE`,
250
+ so the count of draw calls per chunk stays "one plus the number of special
251
+ materials present". Never mint a material per block; the material table is a
252
+ module-level `Object` keyed by type, built once.
253
+
254
+ **Edits rebuild the chunk, not the block.** When a block is placed or broken,
255
+ mark its chunk dirty — and the neighboring chunk too when the block sits on a
256
+ chunk boundary, because a face across the border may have just become exposed
257
+ or buried. Each frame, rebuild at most one or two dirty chunks: `scene.remove`
258
+ the old `InstancedMesh`, `dispose()` it, and add the fresh one. Rebuilding a
259
+ 16×16 chunk is a few thousand `setMatrixAt` calls, well under a frame. A
260
+ just-placed block can pop in by starting its instance matrix at scale 0 and
261
+ easing it to 1 over a few frames.
262
+
263
+ Raycasting and collision must read `state.blocks` (the full model), never the
264
+ meshes or the instance buffers — otherwise you can't hit or collide with a face
265
+ that isn't currently drawn.
266
+
267
+ For texture-heavy or very large worlds, use Block Craft's **exposed-face chunk
268
+ batching** instead: append only the visible *faces* into a `BufferGeometry` per
269
+ material per chunk (position, normal, uv and an index per quad), sharing
270
+ materials across chunks. It uploads less than instancing — a buried face is
271
+ never sent at all — and allows a different texture per face, at the cost of
272
+ writing the six quad windings by hand. The dirty-chunk rule is identical.
273
+ Either of these two patterns is correct; a `Mesh` per block is not, even with
274
+ the exposure test in front of it.
232
275
 
233
276
  ## Procedural terrain: deterministic noise, layered
234
277
 
@@ -274,8 +317,9 @@ function loadChunk(cx, cz) {
274
317
  if (t === null) { state.blocks.delete(k); keys.delete(k); }
275
318
  else { state.blocks.set(k, t); keys.add(k); }
276
319
  }
277
- for (const k of keys) { const [x,y,z] = parseKey(k); if (!isHidden(x,y,z)) addMesh(k, state.blocks.get(k)); }
278
320
  state.chunks.set(chunkKey(cx, cz), keys);
321
+ const mesh = buildChunkMesh(keys); // one InstancedMesh for the chunk (see Rendering)
322
+ scene.add(mesh); state.chunkMeshes.set(chunkKey(cx, cz), mesh);
279
323
  }
280
324
  ```
281
325
 
@@ -298,7 +342,7 @@ Each frame, figure out which chunks should be resident around the player
298
342
  (`VIEW_RADIUS` chunks in each direction), enqueue the missing ones sorted
299
343
  nearest-first, and load **exactly one per frame** so the framerate never
300
344
  stalls. Unload chunks that fall outside the radius (delete their blocks and
301
- meshes). Never load a whole ring in one frame.
345
+ instanced meshes). Never load a whole ring in one frame.
302
346
 
303
347
  ## The player controller — where "Minecraft" is won or lost
304
348
 
@@ -1037,8 +1081,11 @@ None of this needs persistence — keep it purely visual and client-side:
1037
1081
 
1038
1082
  ## Performance checklist
1039
1083
 
1040
- - Mesh only exposed blocks; keep buried ones in the map for collision/raycast.
1041
- - Share one geometry + one material set across all blocks.
1084
+ - One `InstancedMesh` per chunk of exposed blocks (or Block Craft face
1085
+ batching); keep buried ones in the map for collision/raycast.
1086
+ - Share one geometry + one material table across all blocks, built once
1087
+ outside every loop; never a `Mesh`, geometry or material per block.
1088
+ - Edits dirty a chunk; rebuild at most a chunk or two per frame.
1042
1089
  - Stream one chunk per frame; unload out-of-range chunks (dispose meshes).
1043
1090
  - Raycast and collide against the block map, never the scene meshes.
1044
1091
  - Cap mobs and particles; dispose geometries/materials on unmount and on
@@ -1046,7 +1093,7 @@ None of this needs persistence — keep it purely visual and client-side:
1046
1093
  - `renderer.setPixelRatio(Math.min(devicePixelRatio, 2))` and a `Fog` to bound
1047
1094
  draw distance.
1048
1095
 
1049
- That set — exposed-face meshing, per-axis collision with step-up and un-stick,
1096
+ That set — instanced exposed-block chunks, per-axis collision with step-up and un-stick,
1050
1097
  a click-to-play pointer-lock overlay, a DDA raycast, per-voxel Fireproof docs
1051
1098
  folded forward monotonically, table-driven survival systems, and a flat
1052
1099
  multi-file split once it grows — is what turns a voxel tech demo into a game
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "14.3.32",
3
+ "version": "14.3.36",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "exports": {
@@ -37,9 +37,9 @@
37
37
  "license": "Apache-2.0",
38
38
  "dependencies": {
39
39
  "@adviser/cement": "~0.5.34",
40
- "@vibes.diy/call-ai-v2": "14.3.32",
41
- "@vibes.diy/identity": "14.3.32",
42
- "@vibes.diy/use-vibes-types": "14.3.32",
40
+ "@vibes.diy/call-ai-v2": "14.3.36",
41
+ "@vibes.diy/identity": "14.3.36",
42
+ "@vibes.diy/use-vibes-types": "14.3.36",
43
43
  "arktype": "~2.2.3",
44
44
  "json-schema-faker": "~0.6.3"
45
45
  },