@vibes.diy/prompts 5.5.5 → 5.5.7

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/image-gen.md CHANGED
@@ -57,6 +57,20 @@ The input image is automatically resized (max 1024px) and compressed as JPEG bef
57
57
 
58
58
  Load a previously generated image by `_id` — if the doc has a `prompt` but no `_files` yet, the component generates one: `<ImgGen _id="my-image-id" database={database} />`
59
59
 
60
+ ## Illustrations That Are Part of an Item's Identity
61
+
62
+ When an item's picture is intrinsic to what it is — a card in a deck, a creature in a bestiary, a product in a catalog, a character in a cast — render `<ImgGen>` **by default** for every such item that lacks an image, so the illustration appears as soon as the item does. Because ImgGen auto-generates when the target doc has a `prompt` but no `_files` yet, mounting it directly on the item is all it takes:
63
+
64
+ ```jsx
65
+ <ImgGen
66
+ _id={card._id}
67
+ database={database}
68
+ prompt={`Illustration of ${card.title}. ${card.description ?? ""}`}
69
+ />
70
+ ```
71
+
72
+ Do **not** hide first generation behind an "Illustrate" / "Generate image" button — a button leaves every item blank until tapped, which reads as "the app has no pictures." Reserve buttons and `showControls` for *regeneration* of an image that already exists. (For incidental or user-optional images — an avatar upload, a one-off hero graphic — an explicit action is still fine; this rule is about items whose identity includes a picture.)
73
+
60
74
  ## Gallery Pattern
61
75
 
62
76
  Browse stored images with `useLiveQuery`. Building a gallery below the generator:
package/llms/index.d.ts CHANGED
@@ -6,9 +6,10 @@ export { imageGenConfig } from "./image-gen.js";
6
6
  export { webAudioConfig } from "./web-audio.js";
7
7
  export { d3Config } from "./d3.js";
8
8
  export { threeJsConfig } from "./three-js.js";
9
+ export { voxelConfig } from "./voxel.js";
9
10
  export { webxrConfig } from "./webxr.js";
10
11
  export { useViewerConfig } from "./use-viewer.js";
11
12
  export { useVibeConfig } from "./use-vibe.js";
12
13
  export { createVibeConfig } from "./create-vibe.js";
13
14
  export type { LlmConfig } from "./types.js";
14
- export declare const allConfigs: readonly [import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig];
15
+ export declare const allConfigs: readonly [import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig, import("./types.js").LlmConfig];
package/llms/index.js CHANGED
@@ -6,6 +6,7 @@ import { imageGenConfig } from "./image-gen.js";
6
6
  import { webAudioConfig } from "./web-audio.js";
7
7
  import { d3Config } from "./d3.js";
8
8
  import { threeJsConfig } from "./three-js.js";
9
+ import { voxelConfig } from "./voxel.js";
9
10
  import { webxrConfig } from "./webxr.js";
10
11
  import { useViewerConfig } from "./use-viewer.js";
11
12
  import { useVibeConfig } from "./use-vibe.js";
@@ -18,6 +19,7 @@ export { imageGenConfig } from "./image-gen.js";
18
19
  export { webAudioConfig } from "./web-audio.js";
19
20
  export { d3Config } from "./d3.js";
20
21
  export { threeJsConfig } from "./three-js.js";
22
+ export { voxelConfig } from "./voxel.js";
21
23
  export { webxrConfig } from "./webxr.js";
22
24
  export { useViewerConfig } from "./use-viewer.js";
23
25
  export { useVibeConfig } from "./use-vibe.js";
@@ -28,6 +30,7 @@ export const allConfigs = [
28
30
  webAudioConfig,
29
31
  d3Config,
30
32
  threeJsConfig,
33
+ voxelConfig,
31
34
  fireproofConfig,
32
35
  webxrConfig,
33
36
  useViewerConfig,
package/llms/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../jsr/llms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAIpD,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,YAAY;IACZ,cAAc;IACd,cAAc;IACd,QAAQ;IACR,aAAa;IACb,eAAe;IACf,WAAW;IACX,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,cAAc;CACN,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../jsr/llms/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAIpD,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,YAAY;IACZ,cAAc;IACd,cAAc;IACd,QAAQ;IACR,aAAa;IACb,WAAW;IACX,eAAe;IACf,WAAW;IACX,eAAe;IACf,aAAa;IACb,gBAAgB;IAChB,aAAa;IACb,cAAc;CACN,CAAC"}
@@ -0,0 +1,2 @@
1
+ import type { LlmConfig } from "./types.js";
2
+ export declare const voxelConfig: LlmConfig;
package/llms/voxel.js ADDED
@@ -0,0 +1,15 @@
1
+ export const voxelConfig = {
2
+ name: "voxel",
3
+ label: "Voxel",
4
+ description: "Build a Minecraft-style first-person voxel sandbox with Three.js: chunked block world, " +
5
+ "procedural terrain (noise heightmaps, biomes, caves, ore veins, trees), place/break with a " +
6
+ "DDA voxel raycast, an AABB player controller with gravity, jump, sprint, auto step-up and " +
7
+ "un-stick, pointer-lock look with a click-to-play overlay and touch drag, endless chunk " +
8
+ "streaming, a day/night cycle, ambient mobs, and live multiplayer block edits over Fireproof. " +
9
+ "minecraft, voxel, block world, sandbox, blocks, mining, building, first-person, 3d game, " +
10
+ "procedural world, chunks, terrain, overworld, creative mode, survival",
11
+ importModule: "three",
12
+ importName: "THREE",
13
+ importType: "namespace",
14
+ };
15
+ //# sourceMappingURL=voxel.js.map
@@ -0,0 +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,+FAA+F;QAC/F,2FAA2F;QAC3F,uEAAuE;IACzE,YAAY,EAAE,OAAO;IACrB,UAAU,EAAE,OAAO;IACnB,UAAU,EAAE,WAAW;CACxB,CAAC"}
package/llms/voxel.md ADDED
@@ -0,0 +1,343 @@
1
+ # Voxel worlds — a Minecraft-style first-person block sandbox
2
+
3
+ When someone asks for "Minecraft", a "block world", a "voxel game", or a
4
+ first-person building/mining sandbox, this is the recipe. It builds on the
5
+ Three.js docs (all the `THREE.*` classes there apply) — this page is the
6
+ voxel-specific architecture that makes such a game actually playable, and it
7
+ calls out the handful of things that make the difference between "cool" and
8
+ "I keep getting stuck / I can't lock my mouse".
9
+
10
+ The worked example throughout is a real community app, **Voxel World** by
11
+ `isaac-subach` (shared with permission). It ships procedural biomes, caves,
12
+ ore veins, endless chunk streaming, a day/night cycle, live multiplayer edits,
13
+ and ambient wildlife — in a single `App.jsx`. Every pattern below is pulled
14
+ from it.
15
+
16
+ ## The one-file shape
17
+
18
+ A voxel game is one big imperative Three.js engine wrapped in a thin React
19
+ shell. Build the engine **once** inside a `useEffect(() => {...}, [])` that owns
20
+ the renderer, the world model, and the animation loop; expose a few methods on
21
+ a mutable `state` object (`engineRef.current`) so React can call in. React
22
+ renders only the HUD, the hotbar, and overlays as normal DOM on top of the
23
+ `<canvas>`. Never re-create the engine on re-render, and never drive
24
+ per-frame values through React state — read live values through refs.
25
+
26
+ ```jsx
27
+ function CanvasStage({ engineRef, onReady }) {
28
+ const canvasRef = React.useRef(null);
29
+ React.useEffect(() => {
30
+ const canvas = canvasRef.current;
31
+ // ... build scene, camera, renderer, world model, loop ...
32
+ const state = { /* see below */ };
33
+ engineRef.current = state;
34
+ // ... requestAnimationFrame(loop); onReady?.() ...
35
+ return () => { state.running = false; /* dispose everything */ };
36
+ }, []);
37
+ return <section className="absolute inset-0"><canvas ref={canvasRef} className="w-full h-full block touch-none" /></section>;
38
+ }
39
+ ```
40
+
41
+ ## The data model: a flat block map, not a 3D array
42
+
43
+ Store the world as **one `Map` keyed by `"x|y|z"` → block-type string**. Sparse,
44
+ trivial to stream, and the same structure works for collision and raycasting.
45
+
46
+ ```js
47
+ const keyOf = (x, y, z) => `${x}|${y}|${z}`;
48
+ const parseKey = (k) => k.split("|").map(Number);
49
+
50
+ const CHUNK_SIZE = 16;
51
+ const chunkOf = (x, z) => [Math.floor(x / CHUNK_SIZE), Math.floor(z / CHUNK_SIZE)];
52
+ const chunkKey = (cx, cz) => `${cx},${cz}`; // stable string id — compare these, never raw [cx,cz] arrays
53
+
54
+ const state = {
55
+ blocks: new Map(), // "x|y|z" -> type (the loaded world, terrain + edits)
56
+ chunks: new Map(), // "cx,cz" -> Set(blockKey) currently-resident chunks
57
+ edits: new Map(), // "x|y|z" -> type|null player edits layered over terrain
58
+ meshes: new Map(), // "x|y|z" -> THREE.Mesh (only for EXPOSED blocks)
59
+ pos: new THREE.Vector3(0, 20, 0),
60
+ vel: new THREE.Vector3(),
61
+ yaw: 0, pitch: 0, keys: {}, onGround: false, fly: false,
62
+ };
63
+ ```
64
+
65
+ Keep block types as a table of face colors so one material set covers the world:
66
+
67
+ ```js
68
+ const BLOCK_TYPES = {
69
+ grass: { top: 0x5fbf3f, side: 0x7a5230 }, dirt: { top: 0x7a5230, side: 0x6b4626 },
70
+ stone: { top: 0x8a8a8a, side: 0x7a7a7a }, sand: { top: 0xe0d29a, side: 0xd6c78a },
71
+ water: { top: 0x3f6fbf, side: 0x3f6fbf }, wood: { top: 0x9a6b3f, side: 0x6b4626 },
72
+ leaves:{ top: 0x3f8f2f, side: 0x3f8f2f }, /* ...ores, planks, snow, etc. */
73
+ };
74
+ ```
75
+
76
+ ## Rendering: mesh only the EXPOSED blocks (this is the whole performance game)
77
+
78
+ The single most important rule: **do not create a `THREE.Mesh` per stored
79
+ block.** A chunk is thousands of blocks and most are buried. Keep buried blocks
80
+ in the `blocks` map (collision and raycasting read the map, not the scene), and
81
+ only add a mesh when a block has at least one non-occluding neighbor.
82
+
83
+ ```js
84
+ const NB = [[1,0,0],[-1,0,0],[0,1,0],[0,-1,0],[0,0,1],[0,0,-1]];
85
+ function occludes(t) { return t && t !== "water" && t !== "glass" && t !== "leaves"; }
86
+ function isHidden(x, y, z) {
87
+ for (const [dx, dy, dz] of NB)
88
+ if (!occludes(state.blocks.get(keyOf(x + dx, y + dy, z + dz)))) return false;
89
+ return true; // fully enclosed → no mesh
90
+ }
91
+ ```
92
+
93
+ Share one `BoxGeometry` and one material array per type across every block (a
94
+ 6-material array lets the top face differ from the sides). When a block is
95
+ placed or broken, refresh that block's mesh **and its six neighbors'** — a
96
+ neighbor that was buried may now be exposed, or vice-versa.
97
+
98
+ Raycasting and collision must read `state.blocks` (the full model), never the
99
+ meshes — otherwise you can't hit or collide with a face that isn't currently
100
+ meshed.
101
+
102
+ ## Procedural terrain: deterministic noise, layered
103
+
104
+ Terrain must be a **pure function of world coordinates** so chunks generate
105
+ identically no matter when they stream in. Use hash-based value noise (no
106
+ library, no seeds to thread):
107
+
108
+ ```js
109
+ function hash2(x, z) {
110
+ let h = x * 374761393 + z * 668265263;
111
+ h = (h ^ (h >> 13)) * 1274126177; h = h ^ (h >> 16);
112
+ return (h >>> 0) / 4294967295;
113
+ }
114
+ function smoothNoise(x, z) { /* bilinear-interpolated hash2 with smoothstep */ }
115
+ ```
116
+
117
+ Layer octaves for the height field, add a low-frequency biome field, carve
118
+ caves and place ore with 3D noise, and generate trees in a margin around each
119
+ chunk so canopies that cross chunk borders stay whole:
120
+
121
+ ```js
122
+ function heightAt(x, z) {
123
+ let h = smoothNoise(x*0.06, z*0.06)*8 + smoothNoise(x*0.03, z*0.03)*5;
124
+ const mtn = smoothNoise(x*0.012 + 2000, z*0.012 + 2000);
125
+ if (mtn > 0.6) h += (mtn - 0.6) * 70; // steep mountain ranges
126
+ return Math.floor(h);
127
+ }
128
+ function biomeAt(x, z) { /* temp + moisture fields -> plains/forest/desert/snowy/mountains */ }
129
+ function carvedCave(x, y, z) { /* 3D noise > threshold, but keep y<2 solid */ }
130
+ function oreAt(x, y, z) { /* rarer + deeper = diamond; common + shallow = coal */ }
131
+ ```
132
+
133
+ Generate a chunk into a `Map` and let **player edits override terrain** on load
134
+ so a mined/placed block survives streaming out and back:
135
+
136
+ ```js
137
+ function loadChunk(cx, cz) {
138
+ const keys = new Set();
139
+ for (const [k, t] of generateChunkBlocks(cx, cz)) { state.blocks.set(k, t); keys.add(k); }
140
+ for (const [k, t] of state.edits) { // edits win over terrain
141
+ const [ex, , ez] = parseKey(k);
142
+ if (chunkKey(...chunkOf(ex, ez)) !== chunkKey(cx, cz)) continue; // compare stable keys, not arrays
143
+ if (t === null) { state.blocks.delete(k); keys.delete(k); }
144
+ else { state.blocks.set(k, t); keys.add(k); }
145
+ }
146
+ for (const k of keys) { const [x,y,z] = parseKey(k); if (!isHidden(x,y,z)) addMesh(k, state.blocks.get(k)); }
147
+ state.chunks.set(chunkKey(cx, cz), keys);
148
+ }
149
+ ```
150
+
151
+ ## Endless streaming: one chunk per frame
152
+
153
+ Each frame, figure out which chunks should be resident around the player
154
+ (`VIEW_RADIUS` chunks in each direction), enqueue the missing ones sorted
155
+ nearest-first, and load **exactly one per frame** so the framerate never
156
+ stalls. Unload chunks that fall outside the radius (delete their blocks and
157
+ meshes). Never load a whole ring in one frame.
158
+
159
+ ## The player controller — where "Minecraft" is won or lost
160
+
161
+ ### Collision: per-axis AABB + auto step-up + un-stick
162
+
163
+ Represent the player as an AABB (~0.6 wide, 1.8 tall) and test it against the
164
+ block map. Resolve **each horizontal axis separately** so sliding along a wall
165
+ works. Then add the two things that separate a good controller from a
166
+ frustrating one — these are the exact complaints new voxel apps get ("I keep
167
+ getting stuck", "I get stuck jumping", "I'm trapped in a block"):
168
+
169
+ - **Auto step-up.** If a horizontal move is blocked but the space one block
170
+ higher is clear (and you're grounded), step up onto it. This is what lets you
171
+ walk up stairs and 1-block ledges without hammering the jump key.
172
+ - **Un-stick.** At the top of each frame, if the player's AABB is already
173
+ inside solid terrain (a block synced in from another player, or fresh terrain
174
+ generated around you), lift the player up until free. Without this a synced
175
+ block traps the player permanently.
176
+
177
+ ```js
178
+ function collides(px, py, pz) {
179
+ const minX = Math.floor(px-0.3+0.5), maxX = Math.floor(px+0.3+0.5);
180
+ const minZ = Math.floor(pz-0.3+0.5), maxZ = Math.floor(pz+0.3+0.5);
181
+ const minY = Math.floor(py-1.6+0.5), maxY = Math.floor(py+0.2+0.5);
182
+ for (let x=minX;x<=maxX;x++) for (let z=minZ;z<=maxZ;z++) for (let y=minY;y<=maxY;y++) {
183
+ const t = state.blocks.get(keyOf(x,y,z));
184
+ if (t && t !== "water") return true;
185
+ }
186
+ return false;
187
+ }
188
+
189
+ // each frame, before moving:
190
+ let lifts = 0;
191
+ while (collides(np.x, np.y, np.z) && lifts < 6) { np.y += 1; lifts++; } // un-stick
192
+
193
+ const stepAxis = (nx, nz) => { // move + auto step-up
194
+ if (!collides(nx, np.y, nz)) { np.x = nx; np.z = nz; return; }
195
+ if (state.onGround && !state.fly && !collides(nx, np.y+1, nz) && !collides(np.x, np.y+1, np.z)) {
196
+ np.x = nx; np.z = nz; np.y += 1;
197
+ }
198
+ };
199
+ stepAxis(np.x + move.x, np.z);
200
+ stepAxis(np.x, np.z + move.z);
201
+ ```
202
+
203
+ Gravity/jump: integrate `vel.y -= g*dt`, and when a downward move collides set
204
+ `onGround = true` and zero the velocity; jump only when grounded. Add a **fly
205
+ toggle** (e.g. `F`) that swaps gravity for direct vertical thrust — great for
206
+ building. Add **sprint** (hold `Shift` while moving forward → higher speed, and
207
+ nudge the camera FOV up a few degrees for a sense of speed).
208
+
209
+ ### Looking: pointer lock with a click-to-play overlay (+ touch drag)
210
+
211
+ Pointer lock is the number-one confusion in browser voxel games: players click
212
+ and nothing happens, or they don't know the mouse controls the camera at all.
213
+ **Always** show an explicit "Click to play" overlay with the control legend,
214
+ and only request lock from that click:
215
+
216
+ ```jsx
217
+ {worldReady && !isTouch && !locked && (
218
+ <button onClick={() => engineRef.current?.requestLock()}
219
+ className="absolute inset-0 z-20 flex flex-col items-center justify-center bg-black/55 backdrop-blur-sm">
220
+ <div className="panel">
221
+ <h2>Voxel World</h2><p>Click to play</p>
222
+ {/* WASD move · Mouse look · Shift sprint · Space jump · Left-click break · Right-click place · F fly · 1–9 blocks */}
223
+ </div>
224
+ <p className="text-xs opacity-60">Press Esc to release the mouse</p>
225
+ </button>
226
+ )}
227
+ ```
228
+
229
+ Drive the overlay off the real lock state so it reappears when the player hits
230
+ Esc:
231
+
232
+ ```js
233
+ React.useEffect(() => {
234
+ const onLock = () => setLocked(!!document.pointerLockElement);
235
+ document.addEventListener("pointerlockchange", onLock);
236
+ return () => document.removeEventListener("pointerlockchange", onLock);
237
+ }, []);
238
+ ```
239
+
240
+ In the engine, read `movementX/Y` when `document.pointerLockElement === canvas`
241
+ (clamp per-event deltas — browsers spike them on lock acquire), and fall back to
242
+ **drag-to-look on touch** so phones work without pointer lock. Clamp pitch to
243
+ ±1.5 rad. Provide on-screen move/jump/place/break buttons for touch.
244
+
245
+ ### Block targeting: a DDA voxel raycast, not `Raycaster`
246
+
247
+ Do **not** use `THREE.Raycaster` against the meshes — it misses unmeshed faces
248
+ and is slow. March a DDA ray through the block map from the camera along its
249
+ forward vector; the first solid voxel is the target, and the face you entered
250
+ through gives the placement cell.
251
+
252
+ ```js
253
+ function castTarget() {
254
+ const d = new THREE.Vector3(0,0,-1).applyEuler(camera.rotation);
255
+ // ... standard 3D-DDA over integer voxels, max ~7 blocks reach ...
256
+ // return { x, y, z, face:[fx,fy,fz] } for the first solid, non-water voxel, else null
257
+ }
258
+ ```
259
+
260
+ Left-click breaks the targeted block; right-click (`contextmenu`, prevent
261
+ default) places `selected` on the entered face. **Guard placement** so a player
262
+ can never place a block inside their own AABB. Show a wireframe highlight box on
263
+ the current target each frame, and spawn a quick burst of small colored cubes
264
+ when a block breaks — cheap, and it makes mining feel real.
265
+
266
+ ## Multiplayer: live block edits over Fireproof
267
+
268
+ Persist each edit as **exactly one document per voxel coordinate** so concurrent
269
+ builders converge and late joiners see the whole world. Give that document a
270
+ **deterministic `_id` of `block:<key>`** so every edit at a coordinate *updates
271
+ the same doc* — do NOT `put` a fresh doc per edit. Without the deterministic id
272
+ Fireproof mints a new `_id` each time, the database grows without bound, and the
273
+ live query below replays every historical value in index order, so a stale doc
274
+ can overwrite a newer break/place. A break writes a `removed: true` tombstone at
275
+ the same id (keep it so the removal survives streaming, rather than deleting it):
276
+
277
+ ```js
278
+ // on place/break — apply locally for instant feedback, then upsert the ONE doc for this cell
279
+ eng.applyBlock(key, type); // type=null for a break
280
+ await database.put(brk
281
+ ? { _id: `block:${key}`, type: "block", key, removed: true, authorHandle: me?.userHandle, at: Date.now() }
282
+ : { _id: `block:${key}`, type: "block", key, blockType: type, authorHandle: me?.userHandle, at: Date.now() });
283
+
284
+ // stream everyone's edits back into the engine — one current doc per coordinate
285
+ const { docs: blockDocs } = useLiveQuery("type", { key: "block" });
286
+ React.useEffect(() => {
287
+ for (const d of blockDocs) eng.applyBlock(d.key, d.removed ? null : d.blockType);
288
+ }, [blockDocs]);
289
+ ```
290
+
291
+ Apply the local edit **before** the `put` so the world never waits on the
292
+ network, and read live handles/permissions through refs (a value captured in the
293
+ engine closure at load time goes stale — bind live state via `state.on*`
294
+ callbacks or refs, never the closure). Keep the shared world **open** so anyone
295
+ with the link can build — an `access.js` that channels block docs publicly and
296
+ allows anonymous writers:
297
+
298
+ ```js
299
+ export default function (doc, oldDoc, user, ctx) {
300
+ const WORLD = "world:overworld";
301
+ if (doc.type === "block") {
302
+ if (user && doc.authorHandle && doc.authorHandle !== user.userHandle) throw { forbidden: "not author" };
303
+ return { channels: [WORLD], grant: { public: [WORLD] }, allowAnonymous: true };
304
+ }
305
+ throw { forbidden: "unknown document type" };
306
+ }
307
+ ```
308
+
309
+ Do the world's block editing on the **client** regardless of write
310
+ permission (`applyBlock` always runs locally); gate only the **sync** on
311
+ `can.create(...)` so a slow or stale rule binding can never freeze the game.
312
+
313
+ ## Ambient life & atmosphere (cheap, high-impact)
314
+
315
+ None of this needs persistence — keep it purely visual and client-side:
316
+
317
+ - **Day/night cycle.** Advance a `dayTime` 0..1, drive the sun/moon position and
318
+ a blended sky/fog color, and fade light intensity. A full day every few
319
+ minutes feels right.
320
+ - **Mobs.** Spawn a small capped population near the player: animals that wander
321
+ by day, zombies that shuffle toward the player at night. Build each from a few
322
+ `BoxGeometry` parts in a `Group`, apply gravity against `groundYAt(x,z)` (scan
323
+ the block map downward for the top solid block), let them hop 1-block ledges,
324
+ and despawn when far or at the wrong time of day.
325
+ - **Clouds.** A `Group` of flattened white boxes high overhead with
326
+ `fog: false`, following the player horizontally and drifting slowly.
327
+ - **Lighting.** `AmbientLight` + a `DirectionalLight` sun, plus a
328
+ `HemisphereLight` for soft sky/ground fill so faces aren't flat.
329
+
330
+ ## Performance checklist
331
+
332
+ - Mesh only exposed blocks; keep buried ones in the map for collision/raycast.
333
+ - Share one geometry + one material set across all blocks.
334
+ - Stream one chunk per frame; unload out-of-range chunks (dispose meshes).
335
+ - Raycast and collide against the block map, never the scene meshes.
336
+ - Cap mobs and particles; dispose geometries/materials on unmount and on
337
+ chunk unload.
338
+ - `renderer.setPixelRatio(Math.min(devicePixelRatio, 2))` and a `Fog` to bound
339
+ draw distance.
340
+
341
+ That set — exposed-face meshing, per-axis collision with step-up and un-stick,
342
+ a click-to-play pointer-lock overlay, a DDA raycast, and per-voxel Fireproof
343
+ docs — is what turns a voxel tech demo into a game people actually play.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibes.diy/prompts",
3
- "version": "5.5.5",
3
+ "version": "5.5.7",
4
4
  "type": "module",
5
5
  "main": "./index.js",
6
6
  "description": "",
@@ -24,9 +24,9 @@
24
24
  "license": "Apache-2.0",
25
25
  "dependencies": {
26
26
  "@adviser/cement": "~0.5.34",
27
- "@vibes.diy/call-ai-v2": "^5.5.5",
28
- "@vibes.diy/identity": "^5.5.5",
29
- "@vibes.diy/use-vibes-types": "^5.5.5",
27
+ "@vibes.diy/call-ai-v2": "^5.5.7",
28
+ "@vibes.diy/identity": "^5.5.7",
29
+ "@vibes.diy/use-vibes-types": "^5.5.7",
30
30
  "arktype": "~2.2.3",
31
31
  "json-schema-faker": "~0.6.2"
32
32
  },