@vibes.diy/prompts 14.3.35 → 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/voxel.draft.md +244 -0
- package/llms/voxel.js +1 -0
- package/llms/voxel.js.map +1 -1
- package/package.json +4 -4
|
@@ -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;
|
|
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibes.diy/prompts",
|
|
3
|
-
"version": "14.3.
|
|
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.
|
|
41
|
-
"@vibes.diy/identity": "14.3.
|
|
42
|
-
"@vibes.diy/use-vibes-types": "14.3.
|
|
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
|
},
|