@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 +14 -0
- package/llms/index.d.ts +2 -1
- package/llms/index.js +3 -0
- package/llms/index.js.map +1 -1
- package/llms/voxel.d.ts +2 -0
- package/llms/voxel.js +15 -0
- package/llms/voxel.js.map +1 -0
- package/llms/voxel.md +343 -0
- package/package.json +4 -4
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"}
|
package/llms/voxel.d.ts
ADDED
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.
|
|
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.
|
|
28
|
-
"@vibes.diy/identity": "^5.5.
|
|
29
|
-
"@vibes.diy/use-vibes-types": "^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
|
},
|