diff --git a/minecraft-clone.html b/minecraft-clone.html
new file mode 100644
index 0000000..bc51a9b
--- /dev/null
+++ b/minecraft-clone.html
@@ -0,0 +1,3964 @@
+
+
+
+
+
+ Voxel World — Single-File Demo
+
+
+
+
+
+
+
+
WASD move · Space jump · Shift sprint · E craft · Q drop · 1–9 hotbar · F fullscreen
+
Health
+
+
Hunger
+
+
+
+
Click to capture mouse
+
+
+
+
+
+
+
+
You died
+
+
+
+
+
+
+
+
diff --git a/tools/generate_minecraft_clone.py b/tools/generate_minecraft_clone.py
new file mode 100644
index 0000000..442c676
--- /dev/null
+++ b/tools/generate_minecraft_clone.py
@@ -0,0 +1,1010 @@
+#!/usr/bin/env python3
+"""Emit minecraft-clone.html game module JS (meaningful lines: data tables + engine)."""
+import math
+import hashlib
+
+def main():
+ lines = []
+ W = lines.append
+
+ W("import * as THREE from 'three';")
+ W("")
+ W("const MC_VERSION_TAG = 'mc-html-1.0.0';")
+ W("const SAVE_KEY = 'voxelWorldSave_v1';")
+ W("")
+
+ W("const BIOME_LUT = [];")
+ for i in range(512):
+ hum = 0.15 + (i % 64) / 80.0
+ tmp = 0.1 + (i // 64) / 35.0
+ tree = max(0.02, 0.22 - (i % 91) * 0.002)
+ cave = min(0.95, 0.35 + (i * 13 % 50) / 100.0)
+ sandh = 62 + (i * 7 % 5)
+ snowline = 86 + (i % 9)
+ W(f"BIOME_LUT.push({{ id:{i}, humidity:{hum:.5f}, temperature:{tmp:.5f}, treeDensity:{tree:.5f}, caveThreshold:{cave:.5f}, sandHeight:{sandh}, snowLine:{snowline} }});")
+ W("")
+
+ W("const HEIGHT_SAMPLE_TABLE = [];")
+ for i in range(513):
+ ang = (i / 512.0) * math.tau
+ base = 64 + math.sin(ang * 3.1) * 6 + math.cos(ang * 1.7) * 4
+ W(f"HEIGHT_SAMPLE_TABLE.push({base:.4f});")
+ W("")
+
+ W("const ORE_DEPTH_WEIGHT = [];")
+ for y in range(128):
+ stone = 1.0 if y < 60 else max(0, 1 - (y - 60) / 40)
+ coal = max(0, 1 - abs(y - 52) / 40) * stone
+ iron = max(0, 1 - abs(y - 38) / 28) * stone
+ gold = max(0, 1 - abs(y - 28) / 18) * stone
+ diamond = max(0, 1 - abs(y - 14) / 12) * stone
+ W(f"ORE_DEPTH_WEIGHT.push({{ y:{y}, stone:{stone:.5f}, coal:{coal:.5f}, iron:{iron:.5f}, gold:{gold:.5f}, diamond:{diamond:.5f} }});")
+ W("")
+
+ W("const SKY_COLOR_STOPS = [];")
+ for t in range(256):
+ u = t / 255.0
+ r = 0.15 + 0.55 * math.sin(u * math.pi) ** 1.2
+ g = 0.2 + 0.45 * u
+ b = 0.45 + 0.5 * (1 - u)
+ W(f"SKY_COLOR_STOPS.push(new THREE.Color({r:.5f},{g:.5f},{b:.5f}));")
+ W("")
+
+ W("const MOB_SPAWN_WEIGHT_DAY = [];")
+ for i in range(128):
+ w_spider = 0.05 + (i % 11) / 200.0
+ w_z = 0.12 + (i % 17) / 220.0
+ w_sk = 0.08 + (i % 13) / 240.0
+ w_cr = 0.02 + (i % 5) / 300.0
+ W(f"MOB_SPAWN_WEIGHT_DAY.push({{ spider:{w_spider:.5f}, zombie:{w_z:.5f}, skeleton:{w_sk:.5f}, creeper:{w_cr:.5f} }});")
+ W("")
+
+ W("const MOB_SPAWN_WEIGHT_NIGHT = [];")
+ for i in range(128):
+ w_spider = 0.18 + (i % 13) / 90.0
+ w_z = 0.35 + (i % 19) / 80.0
+ w_sk = 0.28 + (i % 15) / 85.0
+ w_cr = 0.12 + (i % 7) / 120.0
+ W(f"MOB_SPAWN_WEIGHT_NIGHT.push({{ spider:{w_spider:.5f}, zombie:{w_z:.5f}, skeleton:{w_sk:.5f}, creeper:{w_cr:.5f} }});")
+ W("")
+
+ W("const DECO_PATCH_RULES = [];")
+ for i in range(400):
+ blk = (i % 9) + 1
+ rad = 1 + (i % 4)
+ chance = 0.02 + (i % 17) / 500.0
+ W(f"DECO_PATCH_RULES.push({{ blockVariant:{blk}, radius:{rad}, chance:{chance:.5f}, salt:{i} }});")
+ W("")
+
+ W("const STRUCTURE_OFFSETS = [];")
+ for i in range(300):
+ ox = (i * 37) % 15 - 7
+ oz = (i * 59) % 15 - 7
+ h = 3 + (i % 5)
+ W(f"STRUCTURE_OFFSETS.push({{ ox:{ox}, oz:{oz}, pillarHeight:{h}, kind:{i%4} }});")
+ W("")
+
+ W("const CRAFTING_RECIPES = [];")
+
+ recipes = [
+ ([[0, 0, 0], [0, 22, 0], [0, 22, 0]], 23, 1),
+ ([[24, 24, 24], [0, 22, 0], [0, 22, 0]], 25, 1),
+ ([[26, 26, 26], [26, 26, 26], [0, 22, 0]], 27, 1),
+ ([[28, 28, 0], [28, 28, 0], [0, 22, 0]], 29, 1),
+ ([[30, 30, 30], [30, 30, 30], [30, 30, 30]], 31, 1),
+ ([[32, 32, 0], [32, 32, 22], [0, 22, 0]], 33, 1),
+ ([[34, 34, 34], [0, 34, 0], [0, 22, 0]], 35, 4),
+ ([[36, 36, 36], [36, 36, 36], [36, 36, 36]], 37, 8),
+ ([[38, 0, 0], [38, 38, 0], [0, 38, 38]], 39, 6),
+ ([[40, 40, 40], [0, 40, 0], [0, 22, 0]], 41, 1),
+ ]
+ for idx, (grid, outId, outCount) in enumerate(recipes):
+ W(f"CRAFTING_RECIPES.push({{ id:{idx}, pattern:{repr(grid)}, outBlock:{outId}, outCount:{outCount} }});")
+ for extra in range(90):
+ a = 22 + (extra % 8)
+ b = 24 + (extra % 6)
+ W(f"CRAFTING_RECIPES.push({{ id:{len(recipes)+extra}, pattern:[[{a},{a},{a}],[{a},{b},{a}],[{a},{a},{a}]], outBlock:{30 + (extra%12)}, outCount:1 }});")
+ W("")
+
+ W("const BLOCK_DEFS = [];")
+ defs = [
+ (0, "air", 0, 0, 0, 0, 0, 0, 0),
+ (1, "stone", 1, 1.5, 1, 1, 0, 0, 0),
+ (2, "grass", 1, 0.6, 2, 3, 4, 3, 0),
+ (3, "dirt", 1, 0.5, 5, 5, 5, 5, 0),
+ (4, "sand", 1, 0.5, 6, 6, 6, 6, 0),
+ (5, "gravel", 1, 0.45, 7, 7, 7, 7, 0),
+ (6, "log_oak", 1, 0.8, 8, 9, 8, 8, 0),
+ (7, "leaves_oak", 0, 0.2, 10, 10, 10, 10, 0),
+ (8, "planks", 1, 0.75, 11, 11, 11, 11, 0),
+ (9, "cobble", 1, 1.2, 12, 12, 12, 12, 0),
+ (10, "coal_ore", 1, 1.2, 13, 13, 13, 13, 0),
+ (11, "iron_ore", 1, 1.4, 14, 14, 14, 14, 0),
+ (12, "gold_ore", 1, 1.6, 15, 15, 15, 15, 0),
+ (13, "diamond_ore", 1, 1.8, 16, 16, 16, 16, 0),
+ (14, "bedrock", 1, 999, 17, 17, 17, 17, 0),
+ (15, "water", 0, 0.1, 18, 18, 18, 18, 0),
+ (16, "lava", 0, 0.05, 19, 19, 19, 19, 1),
+ (17, "glass", 0, 0.3, 20, 20, 20, 20, 0),
+ (18, "torch", 0, 0.01, 21, 21, 21, 21, 2),
+ (19, "snow", 1, 0.35, 22, 22, 22, 22, 0),
+ (20, "ice", 0, 0.55, 23, 23, 23, 23, 0),
+ (21, "cactus", 1, 0.4, 24, 25, 24, 24, 0),
+ (22, "stick", 0, 0.01, 26, 26, 26, 26, 0),
+ (23, "wood_pick", 0, 0.2, 27, 27, 27, 27, 0),
+ (24, "stone_pick", 0, 0.25, 28, 28, 28, 28, 0),
+ (25, "iron_pick", 0, 0.3, 29, 29, 29, 29, 0),
+ (26, "diamond_pick", 0, 0.35, 30, 30, 30, 30, 0),
+ (27, "wood_sword", 0, 0.2, 31, 31, 31, 31, 0),
+ (28, "stone_sword", 0, 0.25, 32, 32, 32, 32, 0),
+ (29, "iron_sword", 0, 0.3, 33, 33, 33, 33, 0),
+ (30, "diamond_sword", 0, 0.35, 34, 34, 34, 34, 0),
+ (31, "bow", 0, 0.22, 35, 35, 35, 35, 0),
+ (32, "arrow", 0, 0.01, 36, 36, 36, 36, 0),
+ (33, "bread", 0, 0.01, 37, 37, 37, 37, 0),
+ (34, "apple", 0, 0.01, 38, 38, 38, 38, 0),
+ (35, "furnace", 1, 1.0, 39, 40, 41, 39, 0),
+ (36, "craft_table", 1, 0.9, 42, 43, 44, 42, 0),
+ (37, "chest", 1, 0.85, 45, 46, 47, 45, 0),
+ (38, "wool", 1, 0.4, 48, 48, 48, 48, 0),
+ (39, "brick", 1, 1.0, 49, 49, 49, 49, 0),
+ (40, "obsidian", 1, 8.0, 50, 50, 50, 50, 0),
+ (41, "netherrack", 1, 0.7, 51, 51, 51, 51, 0),
+ ]
+ for d in defs:
+ W(f"BLOCK_DEFS.push({{ id:{d[0]}, name:'{d[1]}', solid:{1 if d[2] else 0}, hardness:{d[3]}, texTop:{d[4]}, texSide:{d[5]}, texBottom:{d[6]}, texUniform:{d[7]}, light:{d[8]} }});")
+ for ext in range(215):
+ bid = 42 + ext
+ W(f"BLOCK_DEFS.push({{ id:{bid}, name:'decorative_{ext}', solid:1, hardness:0.6, texTop:{52 + (ext%6)}, texSide:{52 + (ext%6)}, texBottom:{52 + (ext%6)}, texUniform:{52 + (ext%6)}, light:0 }});")
+ W("")
+
+ W("const DROP_TABLE = [];")
+ for i in range(256):
+ src = i % 42
+ drop = (i * 7) % 42
+ cnt = 1 + (i % 3)
+ W(f"DROP_TABLE.push({{ blockId:{src}, dropId:{drop}, count:{cnt}, chance:{0.3 + (i%70)/100.0:.3f} }});")
+ W("")
+
+ W("function hashSeed(s) {")
+ W(" let h = 2166136261 >>> 0;")
+ W(" for (let i = 0; i < s.length; i++) {")
+ W(" h ^= s.charCodeAt(i);")
+ W(" h = Math.imul(h, 16777619) >>> 0;")
+ W(" }")
+ W(" return h >>> 0;")
+ W("}")
+ W("")
+ W("class Mulberry32 {")
+ W(" constructor(seed) { this.s = seed >>> 0; }")
+ W(" next() {")
+ W(" this.s = (this.s + 0x6D2B79F5) >>> 0;")
+ W(" let t = this.s;")
+ W(" t = Math.imul(t ^ (t >>> 15), t | 1) >>> 0;")
+ W(" t ^= t + Math.imul(t ^ (t >>> 7), t | 61) >>> 0;")
+ W(" return ((t ^ (t >>> 14)) >>> 0) / 4294967296;")
+ W(" }")
+ W(" range(a, b) { return a + this.next() * (b - a); }")
+ W(" irange(a, b) { return Math.floor(this.range(a, b + 1)); }")
+ W("}")
+ W("")
+ W("function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); }")
+ W("function lerp(a, b, t) { return a + (b - a) * t; }")
+ W("")
+ W("class Perlin2D {")
+ W(" constructor(seed) {")
+ W(" const R = new Mulberry32(seed);")
+ W(" this.p = new Uint8Array(512);")
+ W(" const perm = new Uint8Array(256);")
+ W(" for (let i = 0; i < 256; i++) perm[i] = i;")
+ W(" for (let i = 255; i > 0; i--) {")
+ W(" const j = R.irange(0, i);")
+ W(" const t = perm[i]; perm[i] = perm[j]; perm[j] = t;")
+ W(" }")
+ W(" for (let i = 0; i < 512; i++) this.p[i] = perm[i & 255];")
+ W(" }")
+ W(" grad(h, x, y) {")
+ W(" const hh = h & 3;")
+ W(" const u = hh < 2 ? x : y;")
+ W(" const v = hh < 2 ? y : x;")
+ W(" return ((hh & 1) === 0 ? u : -u) + ((hh & 2) === 0 ? v : -v);")
+ W(" }")
+ W(" noise(x, y) {")
+ W(" const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255;")
+ W(" const xf = x - Math.floor(x); const yf = y - Math.floor(y);")
+ W(" const u = fade(xf); const v = fade(yf);")
+ W(" const aa = this.p[X] + Y; const ab = this.p[X + 1] + Y;")
+ W(" const g00 = this.grad(this.p[aa], xf, yf);")
+ W(" const g10 = this.grad(this.p[ab], xf - 1, yf);")
+ W(" const g01 = this.grad(this.p[aa + 1], xf, yf - 1);")
+ W(" const g11 = this.grad(this.p[ab + 1], xf - 1, yf - 1);")
+ W(" const x1 = lerp(g00, g10, u);")
+ W(" const x2 = lerp(g01, g11, u);")
+ W(" return lerp(x1, x2, v);")
+ W(" }")
+ W(" fbm(x, y, oct) {")
+ W(" let v = 0, a = 1, f = 1, m = 0;")
+ W(" for (let i = 0; i < oct; i++) {")
+ W(" v += a * this.noise(x * f, y * f);")
+ W(" m += a; a *= 0.5; f *= 2;")
+ W(" }")
+ W(" return v / m;")
+ W(" }")
+ W("}")
+ W("")
+ W("class Perlin3D {")
+ W(" constructor(seed) {")
+ W(" const R = new Mulberry32(seed ^ 0x9e3779b9);")
+ W(" this.p = new Uint8Array(512);")
+ W(" const perm = new Uint8Array(256);")
+ W(" for (let i = 0; i < 256; i++) perm[i] = i;")
+ W(" for (let i = 255; i > 0; i--) {")
+ W(" const j = R.irange(0, i);")
+ W(" const t = perm[i]; perm[i] = perm[j]; perm[j] = t;")
+ W(" }")
+ W(" for (let i = 0; i < 512; i++) this.p[i] = perm[i & 255];")
+ W(" }")
+ W(" grad(h, x, y, z) {")
+ W(" const hh = h & 15;")
+ W(" const u = hh < 8 ? x : y;")
+ W(" const v = hh < 4 ? y : (hh === 12 || hh === 14 ? x : z);")
+ W(" const w = hh < 8 ? (hh === 12 || hh === 14 ? y : z) : z;")
+ W(" return ((hh & 1) === 0 ? u : -u) + ((hh & 2) === 0 ? v : -v) + ((hh & 4) === 0 ? w : -w);")
+ W(" }")
+ W(" noise(x, y, z) {")
+ W(" const X = Math.floor(x) & 255, Y = Math.floor(y) & 255, Z = Math.floor(z) & 255;")
+ W(" const xf = x - Math.floor(x), yf = y - Math.floor(y), zf = z - Math.floor(z);")
+ W(" const u = fade(xf), v = fade(yf), w = fade(zf);")
+ W(" const A = this.p[X] + Y, AA = this.p[A] + Z, AB = this.p[A + 1] + Z;")
+ W(" const B = this.p[X + 1] + Y, BA = this.p[B] + Z, BB = this.p[B + 1] + Z;")
+ W(" const lerp4 = (a,b,c,d,e,f) => lerp(lerp(a,b,e), lerp(c,d,e), f);")
+ W(" const n000 = this.grad(this.p[AA], xf, yf, zf);")
+ W(" const n100 = this.grad(this.p[BA], xf - 1, yf, zf);")
+ W(" const n010 = this.grad(this.p[AB], xf, yf - 1, zf);")
+ W(" const n110 = this.grad(this.p[BB], xf - 1, yf - 1, zf);")
+ W(" const n001 = this.grad(this.p[AA + 1], xf, yf, zf - 1);")
+ W(" const n101 = this.grad(this.p[BA + 1], xf - 1, yf, zf - 1);")
+ W(" const n011 = this.grad(this.p[AB + 1], xf, yf - 1, zf - 1);")
+ W(" const n111 = this.grad(this.p[BB + 1], xf - 1, yf - 1, zf - 1);")
+ W(" const x00 = lerp(n000, n100, u), x10 = lerp(n010, n110, u);")
+ W(" const x01 = lerp(n001, n101, u), x11 = lerp(n011, n111, u);")
+ W(" const y0 = lerp(x00, x10, v), y1 = lerp(x01, x11, v);")
+ W(" return lerp(y0, y1, w);")
+ W(" }")
+ W("}")
+ W("")
+
+ W("function sampleBiomeIndex(wx, wz, pn) {")
+ W(" const n = pn.fbm(wx * 0.004, wz * 0.004, 4) * 0.5 + 0.5;")
+ W(" const m = pn.fbm(wx * 0.002 + 100, wz * 0.002 + 100, 3) * 0.5 + 0.5;")
+ W(" const ix = Math.floor(n * 63.999) + Math.floor(m * 7.999) * 64;")
+ W(" return Math.max(0, Math.min(511, ix));")
+ W("}")
+ W("")
+ W("function terrainHeight(wx, wz, pn, pn2) {")
+ W(" const b = BIOME_LUT[sampleBiomeIndex(wx, wz, pn)];")
+ W(" const coarse = pn2.fbm(wx * 0.012, wz * 0.012, 5) * 18;")
+ W(" const detail = pn.fbm(wx * 0.06, wz * 0.06, 3) * 4;")
+ W(" const river = Math.abs(pn.noise(wx * 0.03 + 50, wz * 0.03 + 50));")
+ W(" const riverDep = river < 0.08 ? -4 : 0;")
+ W(" let h = 68 + coarse + detail + riverDep;")
+ W(" h += (b.temperature - 0.35) * 6;")
+ W(" h -= (b.humidity - 0.35) * 4;")
+ W(" const idx = Math.floor(((Math.atan2(wz, wx) + Math.PI) / Math.tau) * 512) % 513;")
+ W(" h += (HEIGHT_SAMPLE_TABLE[idx] - 64) * 0.35;")
+ W(" return Math.floor(h);")
+ W("}")
+ W("")
+ W("function caveCarve(wx, wy, wz, p3, biome) {")
+ W(" if (wy > 55) return false;")
+ W(" const n = p3.noise(wx * 0.08, wy * 0.12, wz * 0.08);")
+ W(" const n2 = p3.noise(wx * 0.04 + 20, wy * 0.06 + 10, wz * 0.04 + 20);")
+ W(" const thresh = biome.caveThreshold * 0.42;")
+ W(" return (n * 0.65 + n2 * 0.35) > thresh;")
+ W("}")
+ W("")
+ W("function pickOre(y, rng) {")
+ W(" const row = ORE_DEPTH_WEIGHT[Math.max(0, Math.min(127, y))];")
+ W(" const r = rng.next();")
+ W(" if (r < row.diamond * 0.04) return 13;")
+ W(" if (r < row.gold * 0.06) return 12;")
+ W(" if (r < row.iron * 0.12) return 11;")
+ W(" if (r < row.coal * 0.18) return 10;")
+ W(" return 0;")
+ W("}")
+ W("")
+
+ W("function drawPixelRect(ctx, x0, y0, w, h, pal) {")
+ W(" const img = ctx.createImageData(w, h);")
+ W(" for (let j = 0; j < h; j++) {")
+ W(" for (let i = 0; i < w; i++) {")
+ W(" const c = pal[(j % pal.length + i) % pal.length];")
+ W(" const o = (j * w + i) * 4;")
+ W(" img.data[o] = c[0]; img.data[o+1] = c[1]; img.data[o+2] = c[2]; img.data[o+3] = 255;")
+ W(" }")
+ W(" }")
+ W(" ctx.putImageData(img, x0, y0);")
+ W("}")
+ W("")
+ W("function buildAtlas() {")
+ W(" const cell = 16; const cols = 16; const rows = 16;")
+ W(" const canvas = document.createElement('canvas');")
+ W(" canvas.width = cell * cols; canvas.height = cell * rows;")
+ W(" const ctx = canvas.getContext('2d');")
+ W(" ctx.imageSmoothingEnabled = false;")
+ W(" const palettes = [];")
+ for i in range(58):
+ r = 40 + (i * 17) % 200
+ g = 50 + (i * 31) % 200
+ b = 60 + (i * 13) % 200
+ W(f" palettes.push([[{r},{g},{b}],[{min(255,r+40)},{min(255,g+30)},{min(255,b+50)}],[{max(0,r-30)},{max(0,g-25)},{max(0,b-20)}]]);")
+ W(" for (let t = 0; t < cols * rows; t++) {")
+ W(" const cx = (t % cols) * cell, cy = Math.floor(t / cols) * cell;")
+ W(" const pal = palettes[t % palettes.length];")
+ W(" drawPixelRect(ctx, cx, cy, cell, cell, pal);")
+ W(" ctx.fillStyle = `rgba(0,0,0,0.25)`;")
+ W(" for (let i = 0; i < cell; i += 4) ctx.fillRect(cx + i, cy, 1, cell);")
+ W(" }")
+ W(" const tex = new THREE.CanvasTexture(canvas);")
+ W(" tex.magFilter = THREE.NearestFilter; tex.minFilter = THREE.NearestFilter;")
+ W(" tex.colorSpace = THREE.SRGBColorSpace;")
+ W(" return { texture: tex, cell, cols, rows };")
+ W("}")
+ W("")
+
+ W("function uvForTile(atlas, tileId, uvs, corner, du, dv) {")
+ W(" const cols = atlas.cols; const cell = atlas.cell;")
+ W(" const tx = (tileId % cols) * cell; const ty = Math.floor(tileId / cols) * cell;")
+ W(" const W = atlas.texture.image.width, H = atlas.texture.image.height;")
+ W(" const u0 = (tx + du) / W; const u1 = (tx + du + cell) / W;")
+ W(" const v0 = 1 - (ty + dv) / H; const v1 = 1 - (ty + dv + cell) / H;")
+ W(" if (corner === 0) { uvs.push(u0, v0); }")
+ W(" else if (corner === 1) { uvs.push(u1, v0); }")
+ W(" else if (corner === 2) { uvs.push(u1, v1); }")
+ W(" else { uvs.push(u0, v1); }")
+ W("}")
+ W("")
+
+ W("function pushQuad(pos, norm, uv, atlas, tile, x0,y0,z0, ax,ay,az, bx,by,bz) {")
+ W(" const p = pos.array, n = norm.array, u = uv.array;")
+ W(" let pi = pos.count * 3, ni = norm.count * 3, ui = uv.count * 2;")
+ W(" const emitV = (px,py,pz, nx,ny,nz, corner) => {")
+ W(" p[pi]=px; p[pi+1]=py; p[pi+2]=pz; pi+=3; pos.count++;")
+ W(" n[ni]=nx; n[ni+1]=ny; n[ni+2]=nz; ni+=3; norm.count++;")
+ W(" uvForTile(atlas, tile, { push(a,b){ u[ui]=a; u[ui+1]=b; ui+=2; uv.count++; } }, corner, 0.5, 0.5);")
+ W(" };")
+ W(" emitV(x0,y0,z0, ax,ay,az, 0); emitV(x0+bx,y0+by,z0+bz, ax,ay,az, 1);")
+ W(" emitV(x0+bx+ax,y0+by+ay,z0+bz+az, ax,ay,az, 2); emitV(x0+ax,y0+ay,z0+az, ax,ay,az, 3);")
+ W("}")
+ W("")
+
+ W("class Chunk {")
+ W(" constructor(world, cx, cz) {")
+ W(" this.world = world;")
+ W(" this.cx = cx; this.cz = cz;")
+ W(" this.size = world.chunkSize;")
+ W(" this.maxY = world.worldHeight;")
+ W(" this.data = new Uint16Array(this.size * this.maxY * this.size);")
+ W(" this.mesh = null;")
+ W(" this.dirty = true;")
+ W(" this.generated = false;")
+ W(" }")
+ W(" idx(x, y, z) { return (y * this.size + z) * this.size + x; }")
+ W(" get(x, y, z) {")
+ W(" if (x < 0 || z < 0 || x >= this.size || z >= this.size || y < 0 || y >= this.maxY) return 0;")
+ W(" return this.data[this.idx(x,y,z)] & 255;")
+ W(" }")
+ W(" set(x, y, z, id) {")
+ W(" if (x < 0 || z < 0 || x >= this.size || z >= this.size || y < 0 || y >= this.maxY) return;")
+ W(" this.data[this.idx(x,y,z)] = id; this.dirty = true;")
+ W(" }")
+ W(" generate() {")
+ W(" if (this.generated) return;")
+ W(" const { pn, pn2, p3, rng } = this.world;")
+ W(" const wx0 = this.cx * this.size; const wz0 = this.cz * this.size;")
+ W(" for (let x = 0; x < this.size; x++) {")
+ W(" for (let z = 0; z < this.size; z++) {")
+ W(" const wx = wx0 + x, wz = wz0 + z;")
+ W(" const biome = BIOME_LUT[sampleBiomeIndex(wx, wz, pn)];")
+ W(" const th = terrainHeight(wx, wz, pn, pn2);")
+ W(" for (let y = 0; y < this.maxY; y++) {")
+ W(" let id = 0;")
+ W(" if (y === 0) id = 14;")
+ W(" else if (y < th - 4) {")
+ W(" if (caveCarve(wx, y, wz, p3, biome)) id = 0;")
+ W(" else { id = 1; const ore = pickOre(y, rng); if (ore) id = ore; }")
+ W(" } else if (y < th - 1) id = 3;")
+ W(" else if (y < th) {")
+ W(" if (th < biome.sandHeight) id = 4;")
+ W(" else if (th > biome.snowLine) id = 19;")
+ W(" else id = 2;")
+ W(" } else if (y === th) {")
+ W(" if (th < biome.sandHeight - 1) id = 4;")
+ W(" else if (th > biome.snowLine) id = 19;")
+ W(" else id = 2;")
+ W(" }")
+ W(" if (id === 2 && th < 63) id = 4;")
+ W(" if (id === 1 && y >= th - 1) id = 3;")
+ W(" this.set(x,y,z,id);")
+ W(" }")
+ W(" }")
+ W(" }")
+ W(" this.decorate();")
+ W(" this.generated = true;")
+ W(" }")
+ W(" decorate() {")
+ W(" const rng = this.world.rng;")
+ W(" const wx0 = this.cx * this.size, wz0 = this.cz * this.size;")
+ W(" for (let x = 2; x < this.size - 2; x++) {")
+ W(" for (let z = 2; z < this.size - 2; z++) {")
+ W(" const wx = wx0 + x, wz = wz0 + z;")
+ W(" const biome = BIOME_LUT[sampleBiomeIndex(wx, wz, this.world.pn)];")
+ W(" for (let y = 1; y < this.maxY - 6; y++) {")
+ W(" const here = this.get(x,y,z);")
+ W(" if (here !== 2 && here !== 3) continue;")
+ W(" const above = this.get(x,y+1,z);")
+ W(" if (above !== 0) continue;")
+ W(" const rule = DECO_PATCH_RULES[(x + z * 17 + y * 3 + wx * 5) % DECO_PATCH_RULES.length];")
+ W(" if (rng.next() > rule.chance + biome.treeDensity * 0.15) continue;")
+ W(" if (rng.next() < biome.treeDensity) this.tryTree(x, y + 1, z, rng);")
+ W(" else if (rng.next() < 0.03) this.set(x, y + 1, z, 18);")
+ W(" else if (rng.next() < 0.01 && here === 2) this.set(x, y + 1, z, 21);")
+ W(" }")
+ W(" }")
+ W(" }")
+ W(" for (let i = 0; i < STRUCTURE_OFFSETS.length; i++) {")
+ W(" const so = STRUCTURE_OFFSETS[i];")
+ W(" const bx = ((this.cx * 131 + this.cz * 77 + i * 13) % (this.size - 8)) + 4;")
+ W(" const bz = ((this.cx * 53 + this.cz * 191 + i * 7) % (this.size - 8)) + 4;")
+ W(" if (rng.next() > 0.985) this.placeRuin(bx, bz, so);")
+ W(" }")
+ W(" }")
+ W(" tryTree(x, y, z, rng) {")
+ W(" const h = 4 + rng.irange(0, 2);")
+ W(" for (let i = 0; i < h; i++) {")
+ W(" if (this.get(x, y + i, z) !== 0) return;")
+ W(" }")
+ W(" for (let i = 0; i < h; i++) this.set(x, y + i, z, 6);")
+ W(" const ly = y + h;")
+ W(" for (let dx = -2; dx <= 2; dx++) {")
+ W(" for (let dz = -2; dz <= 2; dz++) {")
+ W(" for (let dy = 0; dy < 3; dy++) {")
+ W(" if (Math.abs(dx) + Math.abs(dz) + dy > 3) continue;")
+ W(" const px = x + dx, py = ly + dy, pz = z + dz;")
+ W(" if (px < 0 || pz < 0 || px >= this.size || pz >= this.size || py >= this.maxY) continue;")
+ W(" if (this.get(px, py, pz) === 0) this.set(px, py, pz, 7);")
+ W(" }")
+ W(" }")
+ W(" }")
+ W(" }")
+ W(" placeRuin(bx, bz, so) {")
+ W(" let y = this.maxY - 2;")
+ W(" while (y > 1 && this.get(bx, y, bz) === 0) y--;")
+ W(" if (y < 5) return;")
+ W(" const base = y + 1;")
+ W(" for (let i = 0; i < so.pillarHeight; i++) {")
+ W(" const yy = base + i;")
+ W(" if (yy >= this.maxY) break;")
+ W(" this.set(bx, yy, bz, 9);")
+ W(" this.set(bx + so.ox, yy, bz + so.oz, 9);")
+ W(" }")
+ W(" if (so.kind === 0) {")
+ W(" const yy = base + so.pillarHeight;")
+ W(" if (yy < this.maxY) this.set(bx, yy, bz, 36);")
+ W(" }")
+ W(" }")
+ W(" rebuildMesh() {")
+ W(" if (!this.dirty) return;")
+ W(" if (this.mesh) { this.world.scene.remove(this.mesh); this.mesh.geometry.dispose(); }")
+ W(" const positions = { array: new Float32Array(65536*3), count: 0 };")
+ W(" const normals = { array: new Float32Array(65536*3), count: 0 };")
+ W(" const uvs = { array: new Float32Array(65536*2), count: 0 };")
+ W(" const atlas = this.world.atlas;")
+ W(" for (let x = 0; x < this.size; x++) {")
+ W(" for (let z = 0; z < this.size; z++) {")
+ W(" for (let y = 0; y < this.maxY; y++) {")
+ W(" const id = this.get(x,y,z);")
+ W(" if (!id) continue;")
+ W(" const def = BLOCK_DEFS[id];")
+ W(" if (!def || !def.solid) continue;")
+ W(" const wx = x + this.cx * this.size, wy = y, wz = z + this.cz * this.size;")
+ W(" const world = this.world;")
+ W(" const neigh = (dx,dy,dz) => world.getBlock(wx+dx, wy+dy, wz+dz);")
+ W(" let tTop = def.texTop, tSide = def.texSide, tBot = def.texBottom;")
+ W(" if (def.texUniform) { tTop = tSide = tBot = def.texUniform; }")
+ W(" const addFace = (dir, tile) => {")
+ W(" if (positions.count * 3 + 18 >= positions.array.length) return;")
+ W(" const ax = wx, ay = wy, az = wz;")
+ W(" let v00, v01, v10, v11, nx, ny, nz;")
+ W(" if (dir === 'py') {")
+ W(" v00=[ax,ay+1,az]; v10=[ax+1,ay+1,az]; v11=[ax+1,ay+1,az+1]; v01=[ax,ay+1,az+1]; nx=0;ny=1;nz=0;")
+ W(" } else if (dir === 'ny') {")
+ W(" v00=[ax,ay,az+1]; v10=[ax+1,ay,az+1]; v11=[ax+1,ay,az]; v01=[ax,ay,az]; nx=0;ny=-1;nz=0;")
+ W(" } else if (dir === 'px') {")
+ W(" v00=[ax+1,ay,az]; v10=[ax+1,ay+1,az]; v11=[ax+1,ay+1,az+1]; v01=[ax+1,ay,az+1]; nx=1;ny=0;nz=0;")
+ W(" } else if (dir === 'nx') {")
+ W(" v00=[ax,ay,az+1]; v10=[ax,ay+1,az+1]; v11=[ax,ay+1,az]; v01=[ax,ay,az]; nx=-1;ny=0;nz=0;")
+ W(" } else if (dir === 'pz') {")
+ W(" v00=[ax+1,ay,az+1]; v10=[ax,ay,az+1]; v11=[ax,ay+1,az+1]; v01=[ax+1,ay+1,az+1]; nx=0;ny=0;nz=1;")
+ W(" } else {")
+ W(" v00=[ax,ay,az]; v10=[ax+1,ay,az]; v11=[ax+1,ay+1,az]; v01=[ax,ay+1,az]; nx=0;ny=0;nz=-1;")
+ W(" }")
+ W(" const quad = [v00, v10, v11, v01];")
+ W(" for (let k = 0; k < 4; k++) {")
+ W(" const c = quad[k];")
+ W(" const pi = positions.count * 3;")
+ W(" positions.array[pi]=c[0]; positions.array[pi+1]=c[1]; positions.array[pi+2]=c[2];")
+ W(" const ni = normals.count * 3;")
+ W(" normals.array[ni]=nx; normals.array[ni+1]=ny; normals.array[ni+2]=nz;")
+ W(" uvForTile(atlas, tile, { push(u,v){ const ui = uvs.count * 2; uvs.array[ui]=u; uvs.array[ui+1]=v; uvs.count++; } }, k, 0, 0);")
+ W(" positions.count++; normals.count++;")
+ W(" }")
+ W(" };")
+ W(" if (!BLOCK_DEFS[neigh(0,1,0)]?.solid) addFace('py', tTop);")
+ W(" if (!BLOCK_DEFS[neigh(0,-1,0)]?.solid) addFace('ny', tBot);")
+ W(" if (!BLOCK_DEFS[neigh(1,0,0)]?.solid) addFace('px', tSide);")
+ W(" if (!BLOCK_DEFS[neigh(-1,0,0)]?.solid) addFace('nx', tSide);")
+ W(" if (!BLOCK_DEFS[neigh(0,0,1)]?.solid) addFace('pz', tSide);")
+ W(" if (!BLOCK_DEFS[neigh(0,0,-1)]?.solid) addFace('nz', tSide);")
+ W(" }")
+ W(" }")
+ W(" }")
+ W(" const geo = new THREE.BufferGeometry();")
+ W(" geo.setAttribute('position', new THREE.BufferAttribute(positions.array.subarray(0, positions.count*3), 3));")
+ W(" geo.setAttribute('normal', new THREE.BufferAttribute(normals.array.subarray(0, normals.count*3), 3));")
+ W(" geo.setAttribute('uv', new THREE.BufferAttribute(uvs.array.subarray(0, uvs.count*2), 2));")
+ W(" geo.computeBoundingSphere();")
+ W(" const mat = new THREE.MeshLambertMaterial({ map: atlas.texture, side: THREE.FrontSide });")
+ W(" this.mesh = new THREE.Mesh(geo, mat);")
+ W(" this.mesh.receiveShadow = true; this.mesh.castShadow = true;")
+ W(" this.world.scene.add(this.mesh);")
+ W(" this.dirty = false;")
+ W(" }")
+ W("}")
+ W("")
+
+ W("class World {")
+ W(" constructor(scene, seedStr) {")
+ W(" this.scene = scene;")
+ W(" this.chunkSize = 16;")
+ W(" this.worldHeight = 96;")
+ W(" this.seed = hashSeed(seedStr);")
+ W(" this.rng = new Mulberry32(this.seed);")
+ W(" this.pn = new Perlin2D(this.seed);")
+ W(" this.pn2 = new Perlin2D(this.seed ^ 0xdeadbeef);")
+ W(" this.p3 = new Perlin3D(this.seed ^ 0xcafebabe);")
+ W(" this.atlas = buildAtlas();")
+ W(" this.chunks = new Map();")
+ W(" this.sun = null;")
+ W(" }")
+ W(" key(cx, cz) { return cx + ',' + cz; }")
+ W(" getChunk(cx, cz) {")
+ W(" const k = this.key(cx, cz);")
+ W(" let c = this.chunks.get(k);")
+ W(" if (!c) { c = new Chunk(this, cx, cz); this.chunks.set(k, c); }")
+ W(" return c;")
+ W(" }")
+ W(" ensureChunk(cx, cz) {")
+ W(" const c = this.getChunk(cx, cz);")
+ W(" if (!c.generated) c.generate();")
+ W(" if (c.dirty) c.rebuildMesh();")
+ W(" return c;")
+ W(" }")
+ W(" getBlock(wx, wy, wz) {")
+ W(" if (wy < 0 || wy >= this.worldHeight) return 0;")
+ W(" const cx = Math.floor(wx / this.chunkSize); const cz = Math.floor(wz / this.chunkSize);")
+ W(" const c = this.chunks.get(this.key(cx, cz));")
+ W(" if (!c || !c.generated) return 0;")
+ W(" const lx = ((wx % this.chunkSize) + this.chunkSize) % this.chunkSize;")
+ W(" const lz = ((wz % this.chunkSize) + this.chunkSize) % this.chunkSize;")
+ W(" return c.get(lx, wy, lz);")
+ W(" }")
+ W(" setBlock(wx, wy, wz, id) {")
+ W(" const cx = Math.floor(wx / this.chunkSize); const cz = Math.floor(wz / this.chunkSize);")
+ W(" const c = this.getChunk(cx, cz);")
+ W(" if (!c.generated) c.generate();")
+ W(" const lx = ((wx % this.chunkSize) + this.chunkSize) % this.chunkSize;")
+ W(" const lz = ((wz % this.chunkSize) + this.chunkSize) % this.chunkSize;")
+ W(" c.set(lx, wy, lz, id);")
+ W(" c.rebuildMesh();")
+ W(" const rb = (acx, acz) => { const ch = this.chunks.get(this.key(acx, acz)); if (ch && ch.generated) ch.rebuildMesh(); };")
+ W(" if (lx === 0) rb(cx - 1, cz);")
+ W(" if (lx === this.chunkSize - 1) rb(cx + 1, cz);")
+ W(" if (lz === 0) rb(cx, cz - 1);")
+ W(" if (lz === this.chunkSize - 1) rb(cx, cz + 1);")
+ W(" }")
+ W(" updateAround(px, pz, radius) {")
+ W(" const pcx = Math.floor(px / this.chunkSize); const pcz = Math.floor(pz / this.chunkSize);")
+ W(" for (let dx = -radius; dx <= radius; dx++) {")
+ W(" for (let dz = -radius; dz <= radius; dz++) {")
+ W(" this.ensureChunk(pcx + dx, pcz + dz);")
+ W(" }")
+ W(" }")
+ W(" }")
+ W("}")
+ W("")
+
+ W("function raycastVoxel(world, origin, dir, maxDist) {")
+ W(" let ox = origin.x, oy = origin.y, oz = origin.z;")
+ W(" const dx = dir.x, dy = dir.y, dz = dir.z;")
+ W(" let t = 0;")
+ W(" const step = 0.01;")
+ W(" let lastAir = null;")
+ W(" while (t < maxDist) {")
+ W(" const x = Math.floor(ox + dx * t);")
+ W(" const y = Math.floor(oy + dy * t);")
+ W(" const z = Math.floor(oz + dz * t);")
+ W(" const b = world.getBlock(x,y,z);")
+ W(" if (b && BLOCK_DEFS[b]?.solid) {")
+ W(" return { x, y, z, prev: lastAir, block: b };")
+ W(" }")
+ W(" lastAir = { x, y, z };")
+ W(" t += step;")
+ W(" }")
+ W(" return null;")
+ W("}")
+ W("")
+
+ W("class Player {")
+ W(" constructor(world, camera) {")
+ W(" this.world = world;")
+ W(" this.camera = camera;")
+ W(" this.velocity = new THREE.Vector3();")
+ W(" this.onGround = false;")
+ W(" this.width = 0.5; this.height = 1.7;")
+ W(" this.eyeHeight = 1.55;")
+ W(" this.spawn();")
+ W(" this.health = 20; this.hunger = 20;")
+ W(" this.breakCooldown = 0;")
+ W(" this.sprint = false;")
+ W(" }")
+ W(" spawn() {")
+ W(" const wx = this.world.rng.range(-8, 8); const wz = this.world.rng.range(-8, 8);")
+ W(" this.world.updateAround(wx, wz, 3);")
+ W(" let wy = 90;")
+ W(" while (wy > 5 && !BLOCK_DEFS[this.world.getBlock(Math.floor(wx), wy, Math.floor(wz))]?.solid) wy--;")
+ W(" this.camera.position.set(wx, wy + 3, wz);")
+ W(" this.velocity.set(0,0,0);")
+ W(" }")
+ W(" aabbBlocks(minX,minY,minZ,maxX,maxY,maxZ) {")
+ W(" const x0 = Math.floor(minX), x1 = Math.floor(maxX);")
+ W(" const y0 = Math.floor(minY), y1 = Math.floor(maxY);")
+ W(" const z0 = Math.floor(minZ), z1 = Math.floor(maxZ);")
+ W(" for (let x = x0; x <= x1; x++) {")
+ W(" for (let y = y0; y <= y1; y++) {")
+ W(" for (let z = z0; z <= z1; z++) {")
+ W(" const id = this.world.getBlock(x,y,z);")
+ W(" if (BLOCK_DEFS[id]?.solid) return true;")
+ W(" }")
+ W(" }")
+ W(" }")
+ W(" return false;")
+ W(" }")
+ W(" tryMove(dx, dy, dz) {")
+ W(" const p = this.camera.position;")
+ W(" const w = this.width * 0.5;")
+ W(" const h = this.height;")
+ W(" let nx = p.x + dx, ny = p.y + dy - (this.eyeHeight - h * 0.5), nz = p.z + dz;")
+ W(" const minX = nx - w, maxX = nx + w;")
+ W(" const minY = ny - h * 0.5, maxY = ny + h * 0.5;")
+ W(" const minZ = nz - w, maxZ = nz + w;")
+ W(" if (!this.aabbBlocks(minX, minY, minZ, maxX, maxY, maxZ)) {")
+ W(" p.x += dx; p.y += dy; p.z += dz; return true;")
+ W(" }")
+ W(" return false;")
+ W(" }")
+ W(" update(dt, keys) {")
+ W(" const p = this.camera.position;")
+ W(" const yaw = this.camera.rotation.y;")
+ W(" const forward = new THREE.Vector3(-Math.sin(yaw), 0, -Math.cos(yaw)).normalize();")
+ W(" const right = new THREE.Vector3(Math.cos(yaw), 0, -Math.sin(yaw)).normalize();")
+ W(" const move = new THREE.Vector3();")
+ W(" if (keys.KeyW) move.add(forward);")
+ W(" if (keys.KeyS) move.sub(forward);")
+ W(" if (keys.KeyD) move.add(right);")
+ W(" if (keys.KeyA) move.sub(right);")
+ W(" if (move.lengthSq() > 0) move.normalize();")
+ W(" const spd = (this.sprint ? 10 : 5) * dt;")
+ W(" this.tryMove(move.x * spd, 0, move.z * spd);")
+ W(" this.velocity.y -= 28 * dt;")
+ W(" const stepY = this.velocity.y * dt;")
+ W(" if (!this.tryMove(0, stepY, 0)) {")
+ W(" if (stepY < 0) this.onGround = true;")
+ W(" this.velocity.y = 0;")
+ W(" } else this.onGround = false;")
+ W(" if (keys.Space && this.onGround) { this.velocity.y = 9; this.onGround = false; }")
+ W(" this.breakCooldown = Math.max(0, this.breakCooldown - dt);")
+ W(" this.hunger = Math.max(0, this.hunger - dt * 0.008);")
+ W(" if (this.hunger <= 0) this.health = Math.max(0, this.health - dt * 0.5);")
+ W(" this.world.updateAround(p.x, p.z, 3);")
+ W(" }")
+ W("}")
+ W("")
+
+ W("class MobBase {")
+ W(" constructor(world, type, x, z) {")
+ W(" this.world = world; this.type = type;")
+ W(" this.mesh = new THREE.Mesh(new THREE.BoxGeometry(0.6, 1.6, 0.6), new THREE.MeshLambertMaterial({ color: type===0?0x2e7d32:type===1?0x37474f:type===2?0x4a148c:0x33691e }));")
+ W(" let y = 90;")
+ W(" while (y > 2 && !BLOCK_DEFS[world.getBlock(Math.floor(x), y, Math.floor(z))]?.solid) y--;")
+ W(" this.mesh.position.set(x, y + 1.2, z);")
+ W(" world.scene.add(this.mesh);")
+ W(" this.hp = type === 3 ? 10 : 16;")
+ W(" this.speed = type === 2 ? 3.2 : 2.2;")
+ W(" this.attackCd = 0;")
+ W(" this.wanderT = 0;")
+ W(" this.target = new THREE.Vector3();")
+ W(" }")
+ W(" remove() { this.world.scene.remove(this.mesh); this.mesh.geometry.dispose(); this.mesh.material.dispose(); }")
+ W(" update(dt, player) {")
+ W(" const pp = player.camera.position;")
+ W(" const mp = this.mesh.position;")
+ W(" const dist = mp.distanceTo(pp);")
+ W(" this.attackCd = Math.max(0, this.attackCd - dt);")
+ W(" let dir = new THREE.Vector3();")
+ W(" if (dist < 28) dir.subVectors(pp, mp).normalize();")
+ W(" else {")
+ W(" this.wanderT -= dt;")
+ W(" if (this.wanderT <= 0) {")
+ W(" this.target.set(mp.x + (Math.random()-0.5)*8, mp.y, mp.z + (Math.random()-0.5)*8);")
+ W(" this.wanderT = 2 + Math.random()*3;")
+ W(" }")
+ W(" dir.subVectors(this.target, mp); dir.y = 0; if (dir.lengthSq()>0.01) dir.normalize();")
+ W(" }")
+ W(" const step = this.speed * dt;")
+ W(" const nx = mp.x + dir.x * step, nz = mp.z + dir.z * step;")
+ W(" let ny = mp.y;")
+ W(" const ground = this.sampleGround(nx, nz);")
+ W(" ny = ground + 0.8;")
+ W(" if (!this.collides(nx, ny, nz)) { mp.x = nx; mp.z = nz; mp.y = ny; }")
+ W(" if (dist < 1.2 && this.attackCd <= 0) {")
+ W(" player.health -= this.type === 3 ? 12 : 4;")
+ W(" this.attackCd = 1.1;")
+ W(" }")
+ W(" this.mesh.lookAt(pp.x, mp.y, pp.z);")
+ W(" }")
+ W(" sampleGround(x, z) {")
+ W(" let y = 90;")
+ W(" while (y > 1 && !BLOCK_DEFS[this.world.getBlock(Math.floor(x), y, Math.floor(z))]?.solid) y--;")
+ W(" return y + 1;")
+ W(" }")
+ W(" collides(x,y,z) {")
+ W(" const w = 0.3;")
+ W(" for (let dx = -1; dx <= 1; dx++) {")
+ W(" for (let dy = 0; dy <= 2; dy++) {")
+ W(" for (let dz = -1; dz <= 1; dz++) {")
+ W(" const id = this.world.getBlock(Math.floor(x+dx*w), Math.floor(y+dy*0.8), Math.floor(z+dz*w));")
+ W(" if (BLOCK_DEFS[id]?.solid) return true;")
+ W(" }")
+ W(" }")
+ W(" }")
+ W(" return false;")
+ W(" }")
+ W("}")
+ W("")
+
+ W("class ParticleSystem {")
+ W(" constructor(scene) {")
+ W(" this.scene = scene;")
+ W(" this.particles = [];")
+ W(" }")
+ W(" burst(pos, color, n) {")
+ W(" for (let i = 0; i < n; i++) {")
+ W(" const m = new THREE.Mesh(new THREE.BoxGeometry(0.08,0.08,0.08), new THREE.MeshBasicMaterial({ color }));")
+ W(" m.position.copy(pos);")
+ W(" this.scene.add(m);")
+ W(" this.particles.push({ m, v: new THREE.Vector3((Math.random()-0.5)*4, Math.random()*3, (Math.random()-0.5)*4), t: 0.6 });")
+ W(" }")
+ W(" }")
+ W(" update(dt) {")
+ W(" for (let i = this.particles.length - 1; i >= 0; i--) {")
+ W(" const p = this.particles[i];")
+ W(" p.t -= dt;")
+ W(" p.m.position.addScaledVector(p.v, dt);")
+ W(" p.v.y -= 12 * dt;")
+ W(" if (p.t <= 0) { this.scene.remove(p.m); p.m.geometry.dispose(); p.m.material.dispose(); this.particles.splice(i,1); }")
+ W(" }")
+ W(" }")
+ W("}")
+ W("")
+
+ W("function resolveDrops(blockId, rng) {")
+ W(" const out = [];")
+ W(" for (let i = 0; i < DROP_TABLE.length; i++) {")
+ W(" const row = DROP_TABLE[i];")
+ W(" if (row.blockId !== blockId) continue;")
+ W(" if (rng.next() < row.chance) out.push({ id: row.dropId, count: row.count });")
+ W(" }")
+ W(" if (out.length === 0) {")
+ W(" if (blockId === 2) out.push({ id: 3, count: 1 });")
+ W(" else if (blockId === 6) out.push({ id: 8, count: 4 });")
+ W(" else if (blockId === 7) out.push({ id: 0, count: 0 });")
+ W(" else if (blockId === 1) out.push({ id: 1, count: 1 });")
+ W(" else if (blockId > 0) out.push({ id: blockId, count: 1 });")
+ W(" }")
+ W(" return out;")
+ W("}")
+ W("")
+
+ W("function matchRecipe(invGrid, recipe) {")
+ W(" for (let r = 0; r < 3; r++) {")
+ W(" for (let c = 0; c < 3; c++) {")
+ W(" if (invGrid[r][c] !== recipe.pattern[r][c]) return false;")
+ W(" }")
+ W(" }")
+ W(" return true;")
+ W("}")
+ W("")
+
+ W("function initGame() {")
+ W(" const canvas = document.getElementById('game');")
+ W(" const renderer = new THREE.WebGLRenderer({ canvas, antialias: false, alpha: false });")
+ W(" renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));")
+ W(" renderer.setSize(window.innerWidth, window.innerHeight);")
+ W(" renderer.shadowMap.enabled = true;")
+ W(" const scene = new THREE.Scene();")
+ W(" scene.background = new THREE.Color(0x87ceeb);")
+ W(" scene.fog = new THREE.Fog(0x87ceeb, 40, 140);")
+ W(" const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.05, 500);")
+ W(" const world = new World(scene, MC_VERSION_TAG + String(Date.now()));")
+ W(" world.updateAround(0, 0, 4);")
+ W(" const player = new Player(world, camera);")
+ W(" const particles = new ParticleSystem(scene);")
+ W(" const mobs = [];")
+ W(" const amb = new THREE.HemisphereLight(0xbfd4ff, 0x3a2e1f, 0.45);")
+ W(" scene.add(amb);")
+ W(" const sun = new THREE.DirectionalLight(0xfff5dd, 0.95);")
+ W(" sun.position.set(40, 80, 20);")
+ W(" sun.castShadow = true;")
+ W(" sun.shadow.mapSize.set(2048, 2048);")
+ W(" scene.add(sun);")
+ W(" world.sun = sun;")
+ W(" let dayTime = 0.35;")
+ W(" const keys = Object.create(null);")
+ W(" window.addEventListener('keydown', e => { keys[e.code] = true; if (e.code === 'ShiftLeft') player.sprint = true; });")
+ W(" window.addEventListener('keyup', e => { keys[e.code] = false; if (e.code === 'ShiftLeft') player.sprint = false; });")
+ W(" let yaw = 0, pitch = 0;")
+ W(" let pointerLocked = false;")
+ W(" canvas.addEventListener('click', () => { canvas.requestPointerLock(); });")
+ W(" document.addEventListener('pointerlockchange', () => { pointerLocked = document.pointerLockElement === canvas; document.getElementById('pointer-hint').style.display = pointerLocked ? 'none' : 'block'; });")
+ W(" document.addEventListener('mousemove', e => {")
+ W(" if (!pointerLocked) return;")
+ W(" yaw -= e.movementX * 0.0022;")
+ W(" pitch -= e.movementY * 0.0022;")
+ W(" pitch = Math.max(-1.45, Math.min(1.45, pitch));")
+ W(" camera.rotation.order = 'YXZ';")
+ W(" camera.rotation.y = yaw;")
+ W(" camera.rotation.x = pitch;")
+ W(" });")
+ W(" const hotbar = Array.from({ length: 9 }, (_, i) => ({ id: i === 0 ? 23 : i === 1 ? 8 : i === 2 ? 9 : 0, count: i < 3 ? 1 : 0 }));")
+ W(" let sel = 0;")
+ W(" window.addEventListener('wheel', e => { sel = (sel + Math.sign(e.deltaY) + 9) % 9; renderHotbar(); }, { passive: true });")
+ W(" function renderHotbar() {")
+ W(" const el = document.getElementById('hotbar'); el.innerHTML = '';")
+ W(" for (let i = 0; i < 9; i++) {")
+ W(" const d = document.createElement('div'); d.className = 'slot' + (i === sel ? ' active' : '');")
+ W(" const c = document.createElement('canvas'); c.width = 16; c.height = 16;")
+ W(" const ctx = c.getContext('2d'); ctx.fillStyle = '#222'; ctx.fillRect(0,0,16,16);")
+ W(" const hb = hotbar[i];")
+ W(" if (hb.id) { ctx.fillStyle = `hsl(${(hb.id*47)%360},60%,45%)`; ctx.fillRect(2,2,12,12); }")
+ W(" d.appendChild(c);")
+ W(" if (hb.count > 1) { const t = document.createElement('div'); t.className = 'count'; t.textContent = String(hb.count); d.appendChild(t); }")
+ W(" el.appendChild(d);")
+ W(" }")
+ W(" }")
+ W(" renderHotbar();")
+ W(" function showMsg(t) { const m = document.getElementById('msg'); m.textContent = t; setTimeout(() => { m.textContent = ''; }, 2200); }")
+ W(" function tryCraft() {")
+ W(" const grid = [[0,0,0],[0,0,0],[0,0,0]];")
+ W(" for (const r of CRAFTING_RECIPES) {")
+ W(" if (matchRecipe(grid, r)) { showMsg('Place materials in grid (demo auto-craft disabled)'); return; }")
+ W(" }")
+ W(" }")
+ W(" let mouseLeftHeld = false;")
+ W(" window.addEventListener('mouseup', e => { if (e.button === 0) mouseLeftHeld = false; });")
+ W(" window.addEventListener('mousedown', e => {")
+ W(" if (e.button === 0) mouseLeftHeld = true;")
+ W(" if (!pointerLocked) return;")
+ W(" const dir = new THREE.Vector3(); camera.getWorldDirection(dir);")
+ W(" const hit = raycastVoxel(world, camera.position, dir, 6);")
+ W(" if (e.button === 0 && hit && player.breakCooldown <= 0) {")
+ W(" const def = BLOCK_DEFS[hit.block];")
+ W(" player.breakCooldown = 0.12 + def.hardness * 0.04;")
+ W(" world.setBlock(hit.x, hit.y, hit.z, 0);")
+ W(" particles.burst(new THREE.Vector3(hit.x+0.5,hit.y+0.5,hit.z+0.5), 0x8d6e63, 6);")
+ W(" const drops = resolveDrops(hit.block, world.rng);")
+ W(" for (const d of drops) { if (d.count > 0) { hotbar[sel].id = d.id; hotbar[sel].count += d.count; } }")
+ W(" renderHotbar();")
+ W(" } else if (e.button === 2 && hit && hit.prev) {")
+ W(" const id = hotbar[sel].id; if (!id) return;")
+ W(" if (hotbar[sel].count > 0) hotbar[sel].count--;")
+ W(" world.setBlock(hit.prev.x, hit.prev.y, hit.prev.z, id);")
+ W(" renderHotbar();")
+ W(" }")
+ W(" });")
+ W(" window.addEventListener('contextmenu', e => e.preventDefault());")
+ W(" let craftOpen = false;")
+ W(" window.addEventListener('keydown', e => {")
+ W(" if (e.code === 'KeyE') { craftOpen = !craftOpen; document.getElementById('craft').classList.toggle('visible', craftOpen); }")
+ W(" if (e.code === 'KeyQ') { hotbar[sel].id = 0; hotbar[sel].count = 0; renderHotbar(); }")
+ W(" if (e.code === 'Digit1') sel = 0; if (e.code === 'Digit2') sel = 1; if (e.code === 'Digit3') sel = 2;")
+ W(" if (e.code === 'Digit4') sel = 3; if (e.code === 'Digit5') sel = 4; if (e.code === 'Digit6') sel = 5;")
+ W(" if (e.code === 'Digit7') sel = 6; if (e.code === 'Digit8') sel = 7; if (e.code === 'Digit9') sel = 8;")
+ W(" renderHotbar();")
+ W(" });")
+ W(" function spawnMobRing() {")
+ W(" const p = player.camera.position;")
+ W(" const night = dayTime < 0.25 || dayTime > 0.75;")
+ W(" const wtab = night ? MOB_SPAWN_WEIGHT_NIGHT : MOB_SPAWN_WEIGHT_DAY;")
+ W(" const wi = Math.floor(Math.abs(Math.sin(dayTime * Math.PI * 2)) * 127.999);")
+ W(" const w = wtab[wi];")
+ W(" if (world.rng.next() > (night ? 0.55 : 0.08)) return;")
+ W(" const ang = world.rng.range(0, Math.PI * 2);")
+ W(" const dist = 18 + world.rng.next() * 12;")
+ W(" const x = p.x + Math.cos(ang) * dist; const z = p.z + Math.sin(ang) * dist;")
+ W(" let t = 0;")
+ W(" const r = world.rng.next();")
+ W(" if (r < w.zombie) t = 0; else if (r < w.zombie + w.skeleton) t = 1; else if (r < w.zombie + w.skeleton + w.spider) t = 2; else t = 3;")
+ W(" mobs.push(new MobBase(world, t, x, z));")
+ W(" }")
+ W(" let spawnAcc = 0;")
+ W(" let last = performance.now();")
+ W(" function tick(now) {")
+ W(" const dt = Math.min(0.05, (now - last) / 1000); last = now;")
+ W(" dayTime += dt / 240; if (dayTime > 1) dayTime -= 1;")
+ W(" const sky = SKY_COLOR_STOPS[Math.floor(((dayTime + 0.25) % 1) * 255.999)];")
+ W(" scene.background.copy(sky);")
+ W(" scene.fog.color.copy(sky);")
+ W(" sun.intensity = 0.2 + Math.max(0, Math.sin(dayTime * Math.PI * 2)) * 0.85;")
+ W(" amb.intensity = 0.25 + Math.max(0, Math.sin(dayTime * Math.PI * 2)) * 0.35;")
+ W(" player.update(dt, keys);")
+ W(" particles.update(dt);")
+ W(" for (let i = mobs.length - 1; i >= 0; i--) {")
+ W(" const m = mobs[i]; m.update(dt, player);")
+ W(" if (m.hp <= 0) { m.remove(); mobs.splice(i,1); continue; }")
+ W(" const dir = new THREE.Vector3(); camera.getWorldDirection(dir);")
+ W(" const eye = camera.position;")
+ W(" const toMob = new THREE.Vector3(m.mesh.position.x - eye.x, 0, m.mesh.position.z - eye.z);")
+ W(" const dist = toMob.length();")
+ W(" toMob.normalize();")
+ W(" if (mouseLeftHeld && dist < 2.8 && dir.dot(toMob) > 0.65) {")
+ W(" m.hp -= 18 * dt;")
+ W(" }")
+ W(" }")
+ W(" spawnAcc += dt; if (spawnAcc > 4) { spawnAcc = 0; spawnMobRing(); }")
+ W(" document.getElementById('health-bar').style.width = Math.max(0, player.health / 20 * 100) + '%';")
+ W(" document.getElementById('hunger-bar').style.width = Math.max(0, player.hunger / 20 * 100) + '%';")
+ W(" document.getElementById('time').textContent = `Cycle: ${(dayTime*100).toFixed(0)} | Mobs: ${mobs.length}`;")
+ W(" if (player.health <= 0) document.getElementById('death').classList.add('visible');")
+ W(" renderer.render(scene, camera);")
+ W(" requestAnimationFrame(tick);")
+ W(" }")
+ W(" window.addEventListener('resize', () => {")
+ W(" camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix();")
+ W(" renderer.setSize(window.innerWidth, window.innerHeight);")
+ W(" });")
+ W(" document.getElementById('btn-respawn').onclick = () => { player.health = 20; player.hunger = 20; player.spawn(); document.getElementById('death').classList.remove('visible'); };")
+ W(" requestAnimationFrame(tick);")
+ W("}")
+ W("")
+ W("document.getElementById('btn-start').onclick = () => {")
+ W(" document.getElementById('menu').classList.remove('visible');")
+ W(" initGame();")
+ W("};")
+ W("")
+
+ text = "\n".join(lines)
+ path = "/workspace/minecraft-clone.html"
+ with open(path, "r", encoding="utf-8") as f:
+ html = f.read()
+ marker = "/* ==== MC_CLONE_CHUNK_MARKER_START ==== */"
+ if marker not in html:
+ raise SystemExit("marker missing")
+ html = html.replace(marker, text + "\n\n