<?xml version="1.0" encoding="utf-8" standalone="yes"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><script src="https://www.rss.style/js/atom-style.js" xmlns="http://www.w3.org/1999/xhtml"/><title>Tower of Kubes</title><link rel="self" type="application/atom+xml" hreflang="en" href="https://www.towerofkubes.com/games/feed.xml"/><link rel="alternate" type="text/html" hreflang="en" href="https://www.towerofkubes.com/games/"/><link rel="alternate" type="application/rss+xml" hreflang="en" href="https://www.towerofkubes.com/games/index.xml"/><id>/</id><updated>0001-01-01T00:00:00Z</updated><author><name>Ro'i Bandel</name></author><generator>Hugo 0.157.0</generator><entry><title>Kubecraft</title><link rel="alternate" type="text/html" hreflang="en" href="https://www.towerofkubes.com/games/kubecraft/"/><id>https://www.towerofkubes.com/games/kubecraft/</id><updated>0001-01-01T00:00:00Z</updated><summary type="html"></summary><content type="html"><![CDATA[<img src="https://www.towerofkubes.com/img/logo_hu_e1a060b0eecd7c7f.jpg" />

<div id="kubecraft" class="kc-wrap">
  <div class="kc-hud">
    <span>Block <b class="kc-name" data-kc-name>Kubernetes</b></span>
    <button class="kc-btn" type="button" data-kc-reset>New World</button>
  </div>
  <div class="kc-holder" data-kc-holder>
    <canvas class="kc-canvas" data-kc-canvas aria-label="Kubecraft: a tiny 3D voxel sandbox built with Kubernetes-ecosystem blocks"></canvas>
    <div class="kc-cross" aria-hidden="true"></div>
    <div class="kc-water" data-kc-water aria-hidden="true"></div>
    <div class="kc-lava" data-kc-lava aria-hidden="true"></div>
    <div class="kc-overlay kc-hidden" data-kc-overlay>Click to play</div>
    <button class="kc-jump" type="button" data-kc-jump aria-label="Jump">Jump</button>
    <button class="kc-jump kc-down" type="button" data-kc-down aria-label="Descend">Down</button>
  </div>
  <div class="kc-hotbar" data-kc-hotbar></div>
  <p class="kc-hint">Click to play. WASD move, Ctrl sprint, Space jump or swim, double-tap Space to fly (Shift descends), L-click break, R-click place, wheel or 1-9 select. Touch: left joystick, drag look, tap place, hold break, double-tap Jump to fly.</p>
</div>

<script type="module">
import * as THREE from 'https://cdn.jsdelivr.net/npm/three@0.185.1/build/three.module.js';

const root = document.getElementById('kubecraft');
const holder = root.querySelector('[data-kc-holder]');
const canvas = root.querySelector('[data-kc-canvas]');
const nameEl = root.querySelector('[data-kc-name]');
const resetBtn = root.querySelector('[data-kc-reset]');
const hotbarEl = root.querySelector('[data-kc-hotbar]');
const overlay = root.querySelector('[data-kc-overlay]');
const jumpBtn = root.querySelector('[data-kc-jump]');
const downBtn = root.querySelector('[data-kc-down]');
const waterEl = root.querySelector('[data-kc-water]');
const lavaEl = root.querySelector('[data-kc-lava]');

const REACH = 5;
const GRAV = 32;
const JUMP = 9.0;
const SPEED = 4.3;
const SPRINT = 5.6;
const EYE = 1.62;
const PHW = 0.3;       
const PHT = 1.8;       
const CHUNK = 16;      
const RVIEW = 5;       
let SEED = 0;          
const WATER = 6;       
const LAVA = 7;        
const TERRAIN = 8;     
const SEA = 9;         
const STORE_KEY = 'kubecraft-world-v1';


function hash2(x, z) {
  let h = (x * 374761393 + z * 668265263 + SEED * 1442695041) | 0;
  h = Math.imul(h ^ (h >>> 13), 1274126177);
  return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
}
const smooth = (t) => t * t * (3 - 2 * t);
function vnoise(x, z) {
  const xi = Math.floor(x), zi = Math.floor(z);
  const xf = smooth(x - xi), zf = smooth(z - zi);
  const a = hash2(xi, zi), b = hash2(xi + 1, zi), c = hash2(xi, zi + 1), d = hash2(xi + 1, zi + 1);
  return a + (b - a) * xf + (c - a) * zf + (a - b - c + d) * xf * zf;
}
function heightAt(x, z) {
  const n = 2 + vnoise(x / 28, z / 28) * 14 + vnoise(x / 9, z / 9) * 4 + vnoise(x / 4.5, z / 4.5) * 1.6;
  return Math.max(1, Math.min(22, Math.round(n)));
}


function hash3(x, y, z, s) {
  let h = (x * 374761393 + y * 668265263 + z * 1610612741 + (SEED + s) * 1442695041) | 0;
  h = Math.imul(h ^ (h >>> 13), 1274126177);
  return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
}
function vnoise3(x, y, z, s) {
  const xi = Math.floor(x), yi = Math.floor(y), zi = Math.floor(z);
  const xf = smooth(x - xi), yf = smooth(y - yi), zf = smooth(z - zi);
  const c000 = hash3(xi, yi, zi, s), c100 = hash3(xi + 1, yi, zi, s);
  const c010 = hash3(xi, yi + 1, zi, s), c110 = hash3(xi + 1, yi + 1, zi, s);
  const c001 = hash3(xi, yi, zi + 1, s), c101 = hash3(xi + 1, yi, zi + 1, s);
  const c011 = hash3(xi, yi + 1, zi + 1, s), c111 = hash3(xi + 1, yi + 1, zi + 1, s);
  const x00 = c000 + (c100 - c000) * xf, x10 = c010 + (c110 - c010) * xf;
  const x01 = c001 + (c101 - c001) * xf, x11 = c011 + (c111 - c011) * xf;
  const y0 = x00 + (x10 - x00) * yf, y1 = x01 + (x11 - x01) * yf;
  return y0 + (y1 - y0) * zf;
}

function carveAt(x, y, z) {
  const s1 = Math.abs(vnoise3(x / 14, y / 10, z / 14, 0) - 0.5) < 0.065;
  const s2 = Math.abs(vnoise3(x / 14, y / 10, z / 14, 777) - 0.5) < 0.065;
  const cavern = vnoise3(x / 16, y / 12, z / 16, 3333) > 0.80;
  return (s1 && s2) || cavern;
}


function findSpawn() {
  for (let r = 0; r <= 40; r++) {
    for (let x = -r; x <= r; x++) {
      for (let z = -r; z <= r; z++) {
        if (Math.max(Math.abs(x), Math.abs(z)) !== r) continue;
        const h = heightAt(x, z);
        if (h > SEA + 1 && hash2(x * 3 + 7, z * 3 + 11) >= 0.006 && !carveAt(x, h, z)) return [x + 0.5, h + 1, z + 0.5];
      }
    }
  }
  return [0.5, heightAt(0, 0) + 2, 0.5];
}


let audioCtx = null;
let master = null;
function initAudio() {
  try {
    if (!audioCtx) {
      audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      master = audioCtx.createGain();
      master.gain.value = 0.6;
      master.connect(audioCtx.destination);
    }
    if (audioCtx.state === 'suspended') audioCtx.resume();
  } catch (e) {   }
}


function noiseHit(dur, filterType, f0, f1, gain, delay = 0) {
  if (!audioCtx) return;
  try {
    const t0 = audioCtx.currentTime + delay;
    const buf = audioCtx.createBuffer(1, Math.max(1, Math.floor(audioCtx.sampleRate * dur)), audioCtx.sampleRate);
    const data = buf.getChannelData(0);
    for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1;
    const src = audioCtx.createBufferSource();
    src.buffer = buf;
    const filter = audioCtx.createBiquadFilter();
    filter.type = filterType;
    filter.frequency.setValueAtTime(f0, t0);
    filter.frequency.exponentialRampToValueAtTime(Math.max(1, f1), t0 + dur);
    const g = audioCtx.createGain();
    g.gain.setValueAtTime(Math.max(0.001, gain), t0);
    g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
    src.connect(filter).connect(g).connect(master);
    src.start(t0);
    src.stop(t0 + dur + 0.02);
  } catch (e) {   }
}


function tone(wave, f0, f1, dur, gain, delay = 0) {
  if (!audioCtx) return;
  try {
    const t0 = audioCtx.currentTime + delay;
    const osc = audioCtx.createOscillator();
    osc.type = wave;
    osc.frequency.setValueAtTime(f0, t0);
    osc.frequency.exponentialRampToValueAtTime(Math.max(1, f1), t0 + dur);
    const g = audioCtx.createGain();
    g.gain.setValueAtTime(Math.max(0.001, gain), t0);
    g.gain.exponentialRampToValueAtTime(0.001, t0 + dur);
    osc.connect(g).connect(master);
    osc.start(t0);
    osc.stop(t0 + dur + 0.02);
  } catch (e) {   }
}




function matFam(type) {
  if (type === 4 || type === 8) return 2;   
  if (type === 0 || type === 1 || type === 3 || type === 5) return 0;
  return 1;
}
function sndPlace(type) {
  const r = 0.85 + Math.random() * 0.3;
  const f = matFam(type);
  if (f === 0) noiseHit(0.08, 'lowpass', 620 * r, 320 * r, 0.22);
  else if (f === 2) { tone('sine', 170 * r, 110 * r, 0.09, 0.16); noiseHit(0.05, 'lowpass', 900 * r, 500 * r, 0.1); }
  else { noiseHit(0.07, 'bandpass', 1300 * r, 700 * r, 0.16); noiseHit(0.06, 'lowpass', 700 * r, 350 * r, 0.12); }
}
function sndBreak(type) {
  const r = 0.85 + Math.random() * 0.3;
  const f = matFam(type);
  if (f === 0) noiseHit(0.14, 'lowpass', 500 * r, 260 * r, 0.24);
  else if (f === 2) { tone('sine', 140 * r, 90 * r, 0.12, 0.18); noiseHit(0.1, 'lowpass', 800 * r, 400 * r, 0.12); }
  else noiseHit(0.16, 'bandpass', 1000 * r, 420 * r, 0.22);
}
let stepAlt = false;
function sndStep() {
  const r = 0.8 + Math.random() * 0.4;
  stepAlt = !stepAlt;
  noiseHit(0.05, 'lowpass', (stepAlt ? 420 : 360) * r, 240 * r, 0.045);
}
function sndClick() {
  noiseHit(0.03, 'bandpass', 2600, 2000, 0.08);
}




const KUBES = [
  
  { name: 'Wood', base: '#b8945f', draw(P) {
    const wood = '#b8945f', seam = '#755b36', dark = '#5f4826';
    P.rect(0, 0, 16, 16, wood);                 
    P.rect(0, 3, 16, 1, seam);                  
    P.rect(0, 7, 16, 1, seam);
    P.rect(0, 11, 16, 1, seam);
    P.rect(11, 0, 1, 3, seam);                  
    P.rect(3, 4, 1, 3, seam);
    P.rect(13, 8, 1, 3, seam);
    P.rect(7, 12, 1, 4, seam);
    P.px(4, 1, dark); P.px(5, 1, dark);         
    P.px(8, 5, dark); P.px(9, 5, dark);
    P.px(5, 9, dark); P.px(6, 9, dark);
    P.px(11, 13, dark); P.px(12, 13, dark);
  } },

  
  { name: 'Kubernetes', base: '#326ce5', draw(P) {
    const c = '#ffffff';
    const M4 = (x, y) => { P.px(x, y, c); P.px(15 - x, y, c); P.px(x, 15 - y, c); P.px(15 - x, 15 - y, c); };
    
    for (let x = 5; x <= 7; x++) M4(x, 2);          
    for (let y = 5; y <= 7; y++) M4(2, y);          
    M4(4, 3); M4(3, 4);                             
    
    M4(5, 5); M4(6, 6);
    
    P.rect(6, 6, 4, 4, c);
    P.px(6, 6, '#326ce5'); P.px(9, 6, '#326ce5'); P.px(6, 9, '#326ce5'); P.px(9, 9, '#326ce5');
    
    P.rect(7, 0, 2, 1, c); P.rect(7, 15, 2, 1, c);
    P.rect(0, 7, 1, 2, c); P.rect(15, 7, 1, 2, c);
    M4(1, 1); M4(2, 2);
  } },

  
  
  { name: 'Argo', base: '#ffffff', draw(P) {
    const glass = '#cfe9f7', rim = '#8fb9d6', body = '#f26430', eye = '#ffffff', pup = '#222222', smile = '#b3421d';
    
    const spans = [[0, 5], [1, 5], [2, 5], [3, 4], [4, 3], [5, 2]];
    for (const [k, hw] of spans) {
      P.rect(7 - hw, 6 - k, 2 * hw + 2, 1, glass);
      P.rect(7 - hw, 7 + k, 2 * hw + 2, 1, glass);
    }
    const R = (x, y) => { P.px(x, y, rim); P.px(15 - x, y, rim); };
    for (let x = 5; x <= 7; x++) { R(x, 1); R(x, 12); }
    for (let y = 4; y <= 6; y++) { R(2, y); R(2, 13 - y); }
    R(4, 2); R(3, 3); R(4, 11); R(3, 10);
    
    P.rect(4, 4, 8, 7, body);
    P.px(4, 4, glass); P.px(11, 4, glass);
    P.px(4, 10, glass); P.px(11, 10, glass);
    P.rect(4, 5, 3, 3, eye); P.rect(9, 5, 3, 3, eye);
    P.px(5, 6, pup); P.px(10, 6, pup);
    P.px(6, 9, smile); P.px(9, 9, smile);              
    P.px(7, 10, smile); P.px(8, 10, smile);            
    
    P.rect(4, 11, 8, 1, body);
    P.rect(3, 12, 10, 1, body);
    P.rect(3, 13, 10, 1, body);
    P.px(5, 13, '#ffffff'); P.px(10, 13, '#ffffff');   
    P.px(7, 13, '#ffffff'); P.px(8, 13, '#ffffff');
    const T = (x, y) => { P.px(x, y, body); P.px(15 - x, y, body); };
    T(3, 14); T(6, 14);                                 
  
  } },

  
  { name: 'Postgres', base: '#336791', draw(P) {
    const c = '#ffffff', b = '#336791';
    P.rect(5, 5, 9, 7, c);
    P.rect(6, 4, 7, 1, c);
    P.rect(2, 4, 5, 6, c);
    P.rect(2, 10, 2, 4, c);
    P.px(4, 13, c);
    P.rect(6, 12, 2, 3, c); P.rect(11, 12, 2, 3, c);
    P.rect(6, 5, 2, 4, b);
    P.px(3, 6, b);
  } },

  
  { name: 'Helm', base: '#0f1689', draw(P) {
    const c = '#ffffff', b = '#0f1689';
    const M4 = (x, y) => { P.px(x, y, c); P.px(15 - x, y, c); P.px(x, 15 - y, c); P.px(15 - x, 15 - y, c); };
    for (let x = 5; x <= 7; x++) M4(x, 2);          
    for (let y = 5; y <= 7; y++) M4(2, y);          
    M4(4, 3); M4(3, 4);                             
    P.rect(6, 6, 4, 4, c);                          
    P.px(6, 6, b); P.px(9, 6, b); P.px(6, 9, b); P.px(9, 9, b);
    P.rect(7, 0, 2, 2, c); P.rect(7, 14, 2, 2, c);  
    M4(1, 1); M4(2, 2);
  } },

  
  { name: 'Prometheus', base: '#e6522c', draw(P) {
    const c = '#ffffff', b = '#e6522c';
    P.rect(7, 2, 2, 2, c);
    P.rect(6, 4, 4, 2, c);
    P.rect(5, 6, 6, 2, c);
    P.rect(4, 8, 8, 3, c);
    P.rect(7, 6, 2, 4, b);            
    P.rect(4, 12, 8, 1, c);           
    P.rect(5, 13, 6, 1, c);
    P.rect(6, 14, 4, 1, c);
  } },

  
  
  { name: 'Longhorn', base: '#5f224a', draw(P) {
    const c = '#ffffff', b = '#5f224a';
    const M = (x, y) => { P.px(x, y, c); P.px(15 - x, y, c); };
    
    for (let x = 2; x <= 7; x++) { M(x, 5); M(x, 6); }
    
    M(2, 4); M(2, 3); M(1, 3);
    
    P.rect(5, 7, 6, 3, c);
    P.rect(6, 10, 4, 2, c);
    P.rect(7, 12, 2, 1, c);
    
    P.rect(6, 7, 4, 1, b);
    P.rect(7, 8, 2, 2, b);
  } },

  
  { name: 'Artifact Hub', base: '#33648c', draw(P) {
    const c = '#ffffff';
    const M = (x, y) => { P.px(x, y, c); P.px(15 - x, y, c); };
    
    M(7, 1);
    M(6, 2); M(5, 2); M(4, 3); M(3, 3); M(2, 4);
    for (let y = 5; y <= 10; y++) M(2, y);
    M(2, 11); M(3, 12); M(4, 12); M(5, 13); M(6, 13);
    M(7, 14);
  } },

  
  
  { name: 'containerd', base: '#1e1e1e', draw(P) {
    const w = '#ffffff', b = '#1e1e1e';
    P.rect(3, 5, 10, 10, w);    
    P.rect(6, 8, 4, 4, b);      
    P.rect(10, 1, 3, 4, w);     
  } },
];



function makeRng(seed) {
  let s = seed >>> 0;
  return () => {
    s = (s * 1664525 + 1013904223) >>> 0;
    return s / 4294967296;
  };
}


function darken(hex, f) {
  const n = parseInt(hex.slice(1), 16);
  const r = Math.round(((n >> 16) & 255) * (1 - f));
  const g = Math.round(((n >> 8) & 255) * (1 - f));
  const b = Math.round((n & 255) * (1 - f));
  return `rgb(${r},${g},${b})`;
}


function makePainter(ctx) {
  return {
    px(x, y, c) { ctx.fillStyle = c; ctx.fillRect(x, y, 1, 1); },
    rect(x, y, w, h, c) { ctx.fillStyle = c; ctx.fillRect(x, y, w, h); },
    disc(cx, cy, r, c) {
      ctx.fillStyle = c;
      const lim = r * r + r * 0.8;
      for (let y = -r - 1; y <= r + 1; y++) {
        for (let x = -r - 1; x <= r + 1; x++) {
          if (x * x + y * y <= lim) ctx.fillRect(cx + x, cy + y, 1, 1);
        }
      }
    },
    ring(cx, cy, r, c) {
      ctx.fillStyle = c;
      const lo = (r - 0.6) * (r - 0.6);
      const hi = (r + 0.6) * (r + 0.6);
      for (let y = -r - 1; y <= r + 1; y++) {
        for (let x = -r - 1; x <= r + 1; x++) {
          const d = x * x + y * y;
          if (d >= lo && d <= hi) ctx.fillRect(cx + x, cy + y, 1, 1);
        }
      }
    },
    line(x0, y0, x1, y1, c) {
      ctx.fillStyle = c;
      let dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0);
      let sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
      let err = dx + dy;
      for (;;) {
        ctx.fillRect(x0, y0, 1, 1);
        if (x0 === x1 && y0 === y1) break;
        const e2 = 2 * err;
        if (e2 >= dy) { err += dy; x0 += sx; }
        if (e2 <= dx) { err += dx; y0 += sy; }
      }
    },
  };
}


function paintBase(ctx, base, seed, sym = false) {
  ctx.fillStyle = base;
  ctx.fillRect(0, 0, 16, 16);
  const rng = makeRng(seed);
  
  
  for (let y = 0; y < 16; y++) {
    for (let x = 0; x < (sym ? 8 : 16); x++) {
      if (rng() < 0.3) {
        ctx.fillStyle = rng() < 0.5 ? 'rgba(0,0,0,0.06)' : 'rgba(255,255,255,0.06)';
        ctx.fillRect(x, y, 1, 1);
        if (sym) ctx.fillRect(15 - x, y, 1, 1);
      }
    }
  }
  ctx.strokeStyle = darken(base, 0.25);
  ctx.lineWidth = 1;
  ctx.strokeRect(0.5, 0.5, 15, 15);
}

function newTile() {
  const c = document.createElement('canvas');
  c.width = 64;
  c.height = 64;
  const ctx = c.getContext('2d');
  ctx.imageSmoothingEnabled = false;
  ctx.scale(4, 4);
  return { c, ctx };
}

function tex(canvas) {
  const t = new THREE.CanvasTexture(canvas);
  t.magFilter = THREE.NearestFilter;
  t.minFilter = THREE.NearestFilter;
  t.generateMipmaps = false;
  t.colorSpace = THREE.SRGBColorSpace;
  return t;
}


function buildTextures() {
  
  const top = newTile();
  paintBase(top.ctx, '#7cbd4b', 101);
  const topP = makePainter(top.ctx);
  const grng = makeRng(55);
  for (let i = 0; i < 40; i++) {
    const x = Math.floor(grng() * 16), y = Math.floor(grng() * 16);
    topP.px(x, y, grng() < 0.5 ? '#6fae40' : '#89c95a');
  }

  
  const side = newTile();
  paintBase(side.ctx, '#8a5a3b', 202);
  const sideP = makePainter(side.ctx);
  sideP.rect(0, 0, 16, 3, '#7cbd4b');
  const srng = makeRng(77);
  for (let x = 0; x < 16; x++) {
    if (srng() < 0.5) sideP.px(x, 3, '#7cbd4b');
  }

  
  const bottom = newTile();
  paintBase(bottom.ctx, '#8a5a3b', 303);

  const topTex = tex(top.c);
  const sideTex = tex(side.c);
  const botTex = tex(bottom.c);
  const grassMats = [
    new THREE.MeshLambertMaterial({ map: sideTex }),
    new THREE.MeshLambertMaterial({ map: sideTex }),
    new THREE.MeshLambertMaterial({ map: topTex }),
    new THREE.MeshLambertMaterial({ map: botTex }),
    new THREE.MeshLambertMaterial({ map: sideTex }),
    new THREE.MeshLambertMaterial({ map: sideTex }),
  ];

  const kubeMats = [];
  const icons = [];
  KUBES.forEach((k, i) => {
    const t = newTile();
    paintBase(t.ctx, k.base, 400 + i * 13, true);
    k.draw(makePainter(t.ctx));
    
    
    const kt = tex(t.c);
    kubeMats.push(new THREE.MeshLambertMaterial({
      map: kt, emissive: 0xffffff, emissiveMap: kt, emissiveIntensity: 0.42,
    }));
    icons.push(t.c.toDataURL());
  });

  
  const dirt = newTile();
  paintBase(dirt.ctx, '#8a5a3b', 810);
  const dirtMat = new THREE.MeshLambertMaterial({ map: tex(dirt.c) });

  
  const stone = newTile();
  paintBase(stone.ctx, '#8b8b8b', 811);
  const stoneP = makePainter(stone.ctx);
  const stoneRng = makeRng(8110);
  for (let i = 0; i < 10; i++) {
    stoneP.rect(Math.floor(stoneRng() * 15), Math.floor(stoneRng() * 16), 2, 1, '#6f6f6f');
  }
  const stoneMat = new THREE.MeshLambertMaterial({ map: tex(stone.c) });

  
  const sand = newTile();
  paintBase(sand.ctx, '#dbc681', 812);
  const sandP = makePainter(sand.ctx);
  const sandRng = makeRng(8120);
  for (let i = 0; i < 6; i++) {
    sandP.px(Math.floor(sandRng() * 16), Math.floor(sandRng() * 16), '#cbb56f');
  }
  const sandMat = new THREE.MeshLambertMaterial({ map: tex(sand.c) });

  
  const logSide = newTile();
  paintBase(logSide.ctx, '#6b4a2b', 813);
  const logSideP = makePainter(logSide.ctx);
  const logRng = makeRng(8130);
  for (const bx of [2, 5, 8, 11, 14]) {
    const jx = Math.max(0, Math.min(15, bx + Math.floor(logRng() * 3) - 1)); 
    logSideP.line(jx, 0, jx, 15, '#54381f');
  }
  const logTop = newTile();
  paintBase(logTop.ctx, '#a9825a', 814);
  const logTopP = makePainter(logTop.ctx);
  logTopP.ring(8, 8, 3, '#7d5a35');
  logTopP.ring(8, 8, 6, '#7d5a35');
  const logSideTex = tex(logSide.c), logTopTex = tex(logTop.c);
  const logMats = [
    new THREE.MeshLambertMaterial({ map: logSideTex }),
    new THREE.MeshLambertMaterial({ map: logSideTex }),
    new THREE.MeshLambertMaterial({ map: logTopTex }),
    new THREE.MeshLambertMaterial({ map: logTopTex }),
    new THREE.MeshLambertMaterial({ map: logSideTex }),
    new THREE.MeshLambertMaterial({ map: logSideTex }),
  ];

  
  const leaves = newTile();
  paintBase(leaves.ctx, '#3e7a2a', 815);
  const leavesP = makePainter(leaves.ctx);
  const leafRng = makeRng(8150);
  for (let i = 0; i < 50; i++) {
    leavesP.px(Math.floor(leafRng() * 16), Math.floor(leafRng() * 16), i % 2 === 0 ? '#356b23' : '#4c8f35');
  }
  for (let i = 0; i < 8; i++) {
    leavesP.px(Math.floor(leafRng() * 16), Math.floor(leafRng() * 16), '#2c5c1e');
  }
  const leavesMat = new THREE.MeshLambertMaterial({ map: tex(leaves.c) });

  
  const water = newTile();
  paintBase(water.ctx, '#3b6fd4', 816);
  const waterP = makePainter(water.ctx);
  const waterRng = makeRng(8160);
  for (let i = 0; i < 5; i++) {
    waterP.rect(Math.floor(waterRng() * 13), Math.floor(waterRng() * 16), 3, 1, '#5c8ce0');
  }
  const waterMat = new THREE.MeshLambertMaterial({ map: tex(water.c), transparent: true, opacity: 0.7 });

  
  const lava = newTile();
  paintBase(lava.ctx, '#d94a12', 817);
  const lavaP = makePainter(lava.ctx);
  const lavaRng = makeRng(8170);
  for (let i = 0; i < 12; i++) {
    lavaP.disc(Math.floor(lavaRng() * 16), Math.floor(lavaRng() * 16), 1 + Math.floor(lavaRng() * 2), '#ff9a3c');
  }
  for (let i = 0; i < 8; i++) {
    lavaP.px(Math.floor(lavaRng() * 16), Math.floor(lavaRng() * 16), '#a83208');
  }
  const lavaMat = new THREE.MeshBasicMaterial({ map: tex(lava.c) });

  
  const materialsByType = [grassMats, dirtMat, stoneMat, sandMat, logMats, leavesMat, waterMat, lavaMat, ...kubeMats];

  
  const terrainIcons = [top.c.toDataURL(), dirt.c.toDataURL(), stone.c.toDataURL(), sand.c.toDataURL(), logSide.c.toDataURL(), leaves.c.toDataURL()];

  return { materialsByType, icons, terrainIcons };
}


let renderer;
try {
  renderer = new THREE.WebGLRenderer({ canvas, antialias: false });
} catch (e) {
  renderer = null;
}
if (!renderer) {
  canvas.style.display = 'none';
  const fb = document.createElement('div');
  fb.className = 'kc-fallback';
  fb.textContent = 'WebGL is not available, so Kubecraft cannot run in this browser.';
  holder.appendChild(fb);
} else {
  runGame();
}

function runGame() {
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  renderer.outputColorSpace = THREE.SRGBColorSpace;

  const { materialsByType, icons, terrainIcons } = buildTextures();

  const scene = new THREE.Scene();
  scene.background = new THREE.Color('#87ceeb');
  scene.fog = new THREE.Fog('#87ceeb', 45, 78);

  const camera = new THREE.PerspectiveCamera(75, 16 / 10, 0.1, 110);
  camera.rotation.order = 'YXZ';

  scene.add(new THREE.HemisphereLight(0xffffff, 0x8899bb, 0.75));
  const sun = new THREE.DirectionalLight(0xffffff, 0.9);
  sun.position.set(0.6, 1, 0.35);
  scene.add(sun);

  
  const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  const clouds = [];
  const crng = makeRng(9001);
  for (let i = 0; i < 10; i++) {
    const w = 4 + crng() * 4, d = 3 + crng() * 3;
    const m = new THREE.Mesh(
      new THREE.BoxGeometry(w, 1, d),
      new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.85, fog: false })
    );
    m.position.set((crng() - 0.5) * 200, 30 + crng() * 6, (crng() - 0.5) * 160);
    scene.add(m);
    clouds.push(m);
  }

  
  const highlight = new THREE.LineSegments(
    new THREE.EdgesGeometry(new THREE.BoxGeometry(1.002, 1.002, 1.002)),
    new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.7 })
  );
  highlight.visible = false;
  scene.add(highlight);

  
  
  const world = new Map();
  
  
  
  const fluidLvl = new Map();
  const activeWater = new Set();
  const activeLava = new Set();
  const TYPES = TERRAIN + KUBES.length;        
  const keysByType = Array.from({ length: TYPES }, () => []);
  const keySets = Array.from({ length: TYPES }, () => new Set());
  const caps = new Array(TYPES).fill(0);
  const geom = new THREE.BoxGeometry(1, 1, 1);
  const meshes = [];

  const edits = new Map();     
  const tmpMat = new THREE.Matrix4();
  const tmpQuat = new THREE.Quaternion();
  const tmpScale = new THREE.Vector3();
  const tmpPos = new THREE.Vector3();
  const key = (x, y, z) => `${x},${y},${z}`;
  const NB = [[1, 0, 0], [-1, 0, 0], [0, 1, 0], [0, -1, 0], [0, 0, 1], [0, 0, -1]];
  const HB = [[1, 0], [-1, 0], [0, 1], [0, -1]];   

  
  const loadedChunks = new Map();   
  const editsByChunk = new Map();   
  const genQueue = [];              
  let curChunk = null;              
  const unloadCands = [];           
  const chunkKey = (cx, cz) => `${cx},${cz}`;
  const chunkOf = (x, z) => `${Math.floor(x / CHUNK)},${Math.floor(z / CHUNK)}`;
  const chunkLoaded = (x, z) => loadedChunks.has(chunkOf(x, z));

  
  function recordEdit(x, y, z, t) {
    const k = key(x, y, z);
    edits.set(k, t);
    const ck = chunkOf(x, z);
    let m = editsByChunk.get(ck);
    if (!m) { m = new Map(); editsByChunk.set(ck, m); }
    m.set(k, t);
  }

  
  function solidAt(x, y, z) {
    const t = world.get(key(x, y, z));
    return t !== undefined && t !== WATER && t !== LAVA;
  }

  
  
  
  function isExposed(x, y, z, t) {
    for (let i = 0; i < 6; i++) {
      const nx = x + NB[i][0], ny = y + NB[i][1], nz = z + NB[i][2];
      if (ny < 0) continue;
      if (!chunkLoaded(nx, nz)) continue;
      const nt = world.get(key(nx, ny, nz));
      if (t === WATER || t === LAVA) {
        if (nt === undefined) return true;
      } else if (nt === undefined || nt === WATER || nt === LAVA) {
        return true;
      }
    }
    return false;
  }

  
  function ensureCap(t, n) {
    if (n <= caps[t]) return;
    const old = meshes[t];
    if (old) { old.removeFromParent(); old.dispose(); }
    const cap = Math.max(n, caps[t] * 2);
    const im = new THREE.InstancedMesh(geom, materialsByType[t], cap);
    im.count = 0;
    im.frustumCulled = false;
    if (t === WATER) im.renderOrder = 1;
    scene.add(im);
    meshes[t] = im;
    caps[t] = cap;
  }

  function rebuildType(t) {
    const arr = keysByType[t];
    ensureCap(t, arr.length);
    const im = meshes[t];
    const fluid = t === WATER || t === LAVA;
    for (let i = 0; i < arr.length; i++) {
      const k = arr[i];
      const [x, y, z] = k.split(',').map(Number);
      if (fluid) {                            
        const L = fluidLvl.get(k) ?? 8;
        const h = 0.11 * L;
        tmpMat.compose(tmpPos.set(x + 0.5, y + h / 2, z + 0.5), tmpQuat.identity(), tmpScale.set(1, h, 1));
      } else {
        tmpMat.makeTranslation(x + 0.5, y + 0.5, z + 0.5);
      }
      im.setMatrixAt(i, tmpMat);
    }
    im.count = arr.length;
    im.instanceMatrix.needsUpdate = true;
  }

  
  function show(k, t, touched) {
    if (keySets[t].has(k)) return;
    keySets[t].add(k);
    keysByType[t].push(k);
    touched.add(t);
  }
  function hide(k, t, touched) {
    if (!keySets[t].has(k)) return;
    keySets[t].delete(k);
    const arr = keysByType[t];
    const idx = arr.indexOf(k);
    if (idx >= 0) arr.splice(idx, 1);
    touched.add(t);
  }
  function reevalCell(x, y, z, touched) {
    if (y < 0) return;
    const k = key(x, y, z);
    const t = world.get(k);
    if (t === undefined) return;
    if (isExposed(x, y, z, t)) show(k, t, touched);
    else hide(k, t, touched);
  }

  function setBlock(x, y, z, t, record = true) {
    if (!chunkLoaded(x, z)) return false;
    const k = key(x, y, z);
    const touched = new Set();
    const old = world.get(k);
    if (old !== undefined) hide(k, old, touched);
    world.set(k, t);
    fluidLvl.delete(k);   
    
    if (old === undefined) loadedChunks.get(chunkOf(x, z)).push(k);
    reevalCell(x, y, z, touched);
    for (let i = 0; i < 6; i++) reevalCell(x + NB[i][0], y + NB[i][1], z + NB[i][2], touched);
    touched.forEach(rebuildType);
    if (record) { recordEdit(x, y, z, t); scheduleSave(); }
    activateFluidsAround(x, y, z);
    return true;
  }

  function removeBlock(x, y, z, record = true) {
    const k = key(x, y, z);
    const t = world.get(k);
    if (t === undefined) return false;
    const touched = new Set();
    hide(k, t, touched);
    world.delete(k);
    fluidLvl.delete(k);   
    for (let i = 0; i < 6; i++) reevalCell(x + NB[i][0], y + NB[i][1], z + NB[i][2], touched);
    touched.forEach(rebuildType);
    if (record) { recordEdit(x, y, z, -1); scheduleSave(); }
    activateFluidsAround(x, y, z);
    return true;
  }

  
  
  function activateFluidsAround(x, y, z) {
    const at = world.get(key(x, y, z));
    if (at === WATER) activeWater.add(key(x, y, z));
    else if (at === LAVA) activeLava.add(key(x, y, z));
    for (let i = 0; i < 6; i++) {
      const nk = key(x + NB[i][0], y + NB[i][1], z + NB[i][2]);
      const nt = world.get(nk);
      if (nt === WATER) activeWater.add(nk);
      else if (nt === LAVA) activeLava.add(nk);
    }
  }

  
  
  function isSourceCell(nk) { return !fluidLvl.has(nk); }

  function addFluid(x, y, z, F, L, touched) {
    const k = key(x, y, z);
    const existed = world.has(k);
    world.set(k, F);
    if (!existed) loadedChunks.get(chunkOf(x, z)).push(k);
    fluidLvl.set(k, L);
    reevalCell(x, y, z, touched);
    for (let i = 0; i < 6; i++) reevalCell(x + NB[i][0], y + NB[i][1], z + NB[i][2], touched);
  }

  function removeFluid(x, y, z, touched) {
    const k = key(x, y, z);
    const t = world.get(k);
    if (t !== undefined && keySets[t].has(k)) hide(k, t, touched);
    world.delete(k);
    fluidLvl.delete(k);
    for (let i = 0; i < 6; i++) reevalCell(x + NB[i][0], y + NB[i][1], z + NB[i][2], touched);
  }

  
  
  function toStone(x, y, z) {
    fluidLvl.delete(key(x, y, z));
    setBlock(x, y, z, 2);
  }

  
  
  function fluidTick(F, active, dec, minL) {
    const cells = [...active]; active.clear();
    const touched = new Set(); let dirtyLevels = false;
    for (const k of cells) {
      if (world.get(k) !== F) { fluidLvl.delete(k); continue; }
      const i = k.indexOf(','), j = k.lastIndexOf(',');
      const x = +k.slice(0, i), y = +k.slice(i + 1, j), z = +k.slice(j + 1);
      const isSource = !fluidLvl.has(k);
      const L = isSource ? 8 : fluidLvl.get(k);
      if (!isSource) {
        
        let sup = 0;
        if (world.get(key(x, y + 1, z)) === F) sup = 8;
        else for (let n = 0; n < 4; n++) {
          const nk = key(x + HB[n][0], y, z + HB[n][1]);
          if (world.get(nk) === F) sup = Math.max(sup, (fluidLvl.get(nk) ?? 8) - dec);
        }
        if (sup < L) {
          if (sup >= minL) { fluidLvl.set(k, sup); dirtyLevels = true; active.add(k); }
          else removeFluid(x, y, z, touched);
          for (let n = 0; n < 4; n++) {                 
            const nk = key(x + HB[n][0], y, z + HB[n][1]);
            if (world.get(nk) === F) active.add(nk);
          }
          const bk = key(x, y - 1, z);
          if (world.get(bk) === F) active.add(bk);
          continue;   
        }
      }
      
      const belowK = key(x, y - 1, z); const bt = world.get(belowK);
      if (y - 1 >= 0) {
        if (bt === undefined) {
          if (chunkLoaded(x, z)) { addFluid(x, y - 1, z, F, 8, touched); active.add(belowK); }
          continue;   
        }
        if ((F === WATER && bt === LAVA) || (F === LAVA && bt === WATER)) { toStone(x, y - 1, z); continue; }
        if (bt === F && (fluidLvl.get(belowK) ?? 8) < 8) { fluidLvl.set(belowK, 8); dirtyLevels = true; active.add(belowK); }
      }
      
      if (bt !== undefined && bt !== WATER && bt !== LAVA) {
        const nl = L - dec;
        if (nl >= minL) for (let n = 0; n < 4; n++) {
          const nx = x + HB[n][0], nz = z + HB[n][1];
          const nk = key(nx, y, nz);
          const nt = world.get(nk);
          if (nt === undefined) { if (chunkLoaded(nx, nz)) { addFluid(nx, y, nz, F, nl, touched); active.add(nk); } }
          else if ((F === WATER && nt === LAVA) || (F === LAVA && nt === WATER)) toStone(nx, y, nz);
          else if (nt === F && !isSourceCell(nk) && (fluidLvl.get(nk) ?? 8) < nl) { fluidLvl.set(nk, nl); dirtyLevels = true; active.add(nk); }
        }
      }
    }
    touched.forEach(rebuildType);
    if (dirtyLevels && !touched.has(F)) rebuildType(F);
  }

  const CH_MAXY = 64;   

  
  function placeTree(x, y, z, t, x0, z0, created) {
    if (x < x0 || x >= x0 + CHUNK || z < z0 || z >= z0 + CHUNK) return;
    const k = key(x, y, z);
    if (t === 5 && world.has(k)) return;   
    world.set(k, t);
    created.push(k);
  }

  
  function reevalBorders(cx, cz, touched) {
    const x0 = cx * CHUNK, z0 = cz * CHUNK;
    if (loadedChunks.has(chunkKey(cx + 1, cz)))
      for (let z = z0; z < z0 + CHUNK; z++) for (let y = 0; y <= CH_MAXY; y++) reevalCell(x0 + CHUNK, y, z, touched);
    if (loadedChunks.has(chunkKey(cx - 1, cz)))
      for (let z = z0; z < z0 + CHUNK; z++) for (let y = 0; y <= CH_MAXY; y++) reevalCell(x0 - 1, y, z, touched);
    if (loadedChunks.has(chunkKey(cx, cz + 1)))
      for (let x = x0; x < x0 + CHUNK; x++) for (let y = 0; y <= CH_MAXY; y++) reevalCell(x, y, z0 + CHUNK, touched);
    if (loadedChunks.has(chunkKey(cx, cz - 1)))
      for (let x = x0; x < x0 + CHUNK; x++) for (let y = 0; y <= CH_MAXY; y++) reevalCell(x, y, z0 - 1, touched);
  }

  
  function genChunk(cx, cz) {
    const ck = chunkKey(cx, cz);
    if (loadedChunks.has(ck)) return;
    const created = [];
    loadedChunks.set(ck, created);
    const x0 = cx * CHUNK, z0 = cz * CHUNK;

    
    for (let x = x0; x < x0 + CHUNK; x++) {
      for (let z = z0; z < z0 + CHUNK; z++) {
        const h = heightAt(x, z);
        const carve = h > SEA;
        for (let y = 0; y <= h; y++) {
          let t;
          if (y === 0) t = 2;                            
          else if (y === h) t = h <= SEA + 1 ? 3 : 0;    
          else if (y >= Math.max(1, h - 3)) t = 1;       
          else t = 2;                                    
          if (carve && y >= 2 && carveAt(x, y, z)) {
            if (y <= 4) t = LAVA;                        
            else continue;                              
          }
          const kk = key(x, y, z);
          world.set(kk, t);
          created.push(kk);
        }
        if (h < SEA) {
          for (let y = h + 1; y <= SEA; y++) {           
            const kk = key(x, y, z);
            world.set(kk, WATER);
            created.push(kk);
          }
        }
      }
    }

    
    for (let x = x0 - 2; x <= x0 + 17; x++) {
      for (let z = z0 - 2; z <= z0 + 17; z++) {
        const h = heightAt(x, z);
        if (h <= SEA + 1) continue;
        if (hash2(x * 3 + 7, z * 3 + 11) >= 0.006) continue;
        const inChunk = x >= x0 && x < x0 + CHUNK && z >= z0 && z < z0 + CHUNK;
        const survived = inChunk ? world.get(key(x, h, z)) === 0 : !carveAt(x, h, z);
        if (!survived) continue;                         
        const trunkH = hash2(x, z) < 0.5 ? 5 : 4;
        const top = h + trunkH;
        for (let y = h + 1; y <= top; y++) placeTree(x, y, z, 4, x0, z0, created); 
        for (const ly of [top - 1, top]) {               
          for (let dx = -2; dx <= 2; dx++) {
            for (let dz = -2; dz <= 2; dz++) {
              if (Math.abs(dx) === 2 && Math.abs(dz) === 2) continue;
              placeTree(x + dx, ly, z + dz, 5, x0, z0, created);
            }
          }
        }
        for (let dx = -1; dx <= 1; dx++) {               
          for (let dz = -1; dz <= 1; dz++) placeTree(x + dx, top + 1, z + dz, 5, x0, z0, created);
        }
        placeTree(x, top + 2, z, 5, x0, z0, created);    
        placeTree(x + 1, top + 2, z, 5, x0, z0, created);
        placeTree(x - 1, top + 2, z, 5, x0, z0, created);
        placeTree(x, top + 2, z + 1, 5, x0, z0, created);
        placeTree(x, top + 2, z - 1, 5, x0, z0, created);
      }
    }

    
    const em = editsByChunk.get(ck);
    if (em) {
      em.forEach((t, kk) => {
        if (t === -1) { world.delete(kk); return; }
        if (t < 0 || t >= TYPES) return;   
        if (!world.has(kk)) created.push(kk);
        world.set(kk, t);
      });
    }

    
    const touched = new Set();
    for (const kk of created) {
      const i = kk.indexOf(','), j = kk.lastIndexOf(',');
      reevalCell(+kk.slice(0, i), +kk.slice(i + 1, j), +kk.slice(j + 1), touched);
    }
    reevalBorders(cx, cz, touched);
    touched.forEach(rebuildType);
    
    if (em) em.forEach((t, kk) => {
      const i = kk.indexOf(','), j = kk.lastIndexOf(',');
      activateFluidsAround(+kk.slice(0, i), +kk.slice(i + 1, j), +kk.slice(j + 1));
    });
    trySpawnMobs(cx, cz);
  }

  function unloadChunk(cx, cz) {
    const ck = chunkKey(cx, cz);
    const arr = loadedChunks.get(ck);
    if (!arr) return;
    const touched = new Set();
    for (const kk of arr) {
      const t = world.get(kk);
      if (t === undefined) continue;
      if (keySets[t].has(kk)) hide(kk, t, touched);
      world.delete(kk);
      fluidLvl.delete(kk);
    }
    loadedChunks.delete(ck);
    reevalBorders(cx, cz, touched);   
    touched.forEach(rebuildType);
  }

  function forceSpawnArea(x, z) {
    const scx = Math.floor(x / CHUNK), scz = Math.floor(z / CHUNK);
    for (let dz = -1; dz <= 1; dz++) for (let dx = -1; dx <= 1; dx++) genChunk(scx + dx, scz + dz);
  }

  
  
  function streamChunks() {
    const pcx = Math.floor(player.pos.x / CHUNK), pcz = Math.floor(player.pos.z / CHUNK);
    const pc = chunkKey(pcx, pcz);
    if (pc !== curChunk) {
      curChunk = pc;
      genQueue.length = 0;
      for (let dz = -RVIEW; dz <= RVIEW; dz++) {
        for (let dx = -RVIEW; dx <= RVIEW; dx++) {
          if (dx * dx + dz * dz > RVIEW * RVIEW) continue;
          const ck = chunkKey(pcx + dx, pcz + dz);
          if (!loadedChunks.has(ck)) genQueue.push(ck);
        }
      }
      genQueue.sort((a, b) => distSq(a, pcx, pcz) - distSq(b, pcx, pcz));
      unloadCands.length = 0;
      const scx = Math.floor(SPAWN.x / CHUNK), scz = Math.floor(SPAWN.z / CHUNK);
      loadedChunks.forEach((_, ck) => {
        const i = ck.indexOf(',');
        const cx = +ck.slice(0, i), cz = +ck.slice(i + 1);
        if (Math.max(Math.abs(cx - pcx), Math.abs(cz - pcz)) <= RVIEW + 1) return;
        if (Math.max(Math.abs(cx - scx), Math.abs(cz - scz)) <= 1) return;  
        unloadCands.push(ck);
      });
    }
    for (let g = 0; g < 2 && genQueue.length; g++) {
      const ck = genQueue.shift();
      const i = ck.indexOf(',');
      genChunk(+ck.slice(0, i), +ck.slice(i + 1));
    }
    if (unloadCands.length) {
      const ck = unloadCands.shift();
      const i = ck.indexOf(',');
      unloadChunk(+ck.slice(0, i), +ck.slice(i + 1));
    }
  }
  function distSq(ck, pcx, pcz) {
    const i = ck.indexOf(',');
    const dx = (+ck.slice(0, i)) - pcx, dz = (+ck.slice(i + 1)) - pcz;
    return dx * dx + dz * dz;
  }

  
  let saveTimer = 0;
  function scheduleSave() {
    clearTimeout(saveTimer);
    saveTimer = setTimeout(save, 500);
  }
  function save() {
    try {
      const arr = [];
      edits.forEach((t, k) => {
        const [x, y, z] = k.split(',').map(Number);
        arr.push([x, y, z, t]);
      });
      localStorage.setItem(STORE_KEY, JSON.stringify({ seed: SEED, edits: arr }));
    } catch (e) {   }
  }
  function load() {
    try {
      const raw = localStorage.getItem(STORE_KEY);
      return raw ? JSON.parse(raw) : null;
    } catch (e) { return null; }
  }

  
  const saved = load();
  const freshSeed = !(saved && typeof saved.seed === 'number');
  SEED = freshSeed ? (Math.random() * 2 ** 31) | 0 : saved.seed;

  
  for (let t = 0; t < TYPES; t++) {
    const im = new THREE.InstancedMesh(geom, materialsByType[t], 512);
    im.count = 0;
    im.frustumCulled = false;
    caps[t] = 512;
    if (t === WATER) im.renderOrder = 1;
    scene.add(im);
    meshes[t] = im;
  }

  
  if (saved && Array.isArray(saved.edits)) {
    for (const [x, y, z, t] of saved.edits) recordEdit(x, y, z, t);
  }
  if (freshSeed) scheduleSave();   

  
  
  const MOB_CAP = 12;
  const mobs = [];
  let mobsReady = false;   
  function makeMob(kind) {
    const group = new THREE.Group();
    const legs = [];
    const box = (w, h, d, color, x, y, z) => {
      const m = new THREE.Mesh(new THREE.BoxGeometry(w, h, d), new THREE.MeshLambertMaterial({ color }));
      m.position.set(x, y, z);
      group.add(m);
      return m;
    };
    if (kind === 0) {                                    
      box(0.9, 0.5, 0.55, '#e89a9a', 0, 0.55, 0);        
      box(0.42, 0.42, 0.42, '#e89a9a', 0.55, 0.7, 0);    
      box(0.18, 0.14, 0.3, '#d4726f', 0.85, 0.66, 0);    
      box(0.06, 0.09, 0.1, '#f5f5f5', 0.77, 0.8, 0.13);  
      box(0.06, 0.09, 0.1, '#f5f5f5', 0.77, 0.8, -0.13);
      box(0.05, 0.06, 0.05, '#1a1a1a', 0.785, 0.79, 0.11);  
      box(0.05, 0.06, 0.05, '#1a1a1a', 0.785, 0.79, -0.11);
      for (const sx of [-0.3, 0.3]) for (const sz of [-0.18, 0.18])
        legs.push(box(0.16, 0.3, 0.16, '#e89a9a', sx, 0.15, sz));
    } else {                                             
      box(0.5, 0.42, 0.4, '#f4f4f4', 0, 0.45, 0);        
      box(0.26, 0.26, 0.26, '#f4f4f4', 0.28, 0.72, 0);   
      box(0.12, 0.08, 0.16, '#e8a33d', 0.45, 0.7, 0);    
      box(0.05, 0.07, 0.07, '#e9e9ef', 0.415, 0.78, 0.08);  
      box(0.05, 0.07, 0.07, '#e9e9ef', 0.415, 0.78, -0.08);
      box(0.04, 0.05, 0.04, '#141414', 0.43, 0.775, 0.07);  
      box(0.04, 0.05, 0.04, '#141414', 0.43, 0.775, -0.07);
      box(0.07, 0.1, 0.08, '#c0392b', 0.44, 0.6, 0);     
      for (const sz of [-0.12, 0.12])
        legs.push(box(0.07, 0.25, 0.07, '#e8a33d', 0, 0.125, sz));
    }
    return { group, legs };
  }
  function spawnMob(x, y, z) {
    const kind = Math.random() < 0.5 ? 0 : 1;
    const { group, legs } = makeMob(kind);
    group.position.set(x, y, z);
    scene.add(group);
    mobs.push({
      kind, group, legs,
      pos: new THREE.Vector3(x, y, z),
      vel: new THREE.Vector3(),
      yaw: Math.random() * Math.PI * 2,
      state: 'idle', timer: 1 + Math.random() * 2,
      phase: 0,
      grounded: false, hitWall: false,
    });
  }
  function trySpawnMobs(cx, cz) {
    if (!mobsReady || mobs.length >= MOB_CAP) return;
    if (hash2(cx * 91 + 13, cz * 57 + 5) >= 0.15) return;
    const x0 = cx * CHUNK, z0 = cz * CHUNK;
    const attempts = 1 + Math.floor(Math.random() * 2);
    for (let a = 0; a < attempts; a++) {
      if (mobs.length >= MOB_CAP) return;
      const x = x0 + Math.floor(Math.random() * CHUNK);
      const z = z0 + Math.floor(Math.random() * CHUNK);
      const h = heightAt(x, z);
      if (world.get(key(x, h, z)) !== 0) continue;               
      if (world.get(key(x, h + 1, z)) !== undefined) continue;   
      if (world.get(key(x, h + 2, z)) !== undefined) continue;
      const dx = x + 0.5 - player.pos.x, dz = z + 0.5 - player.pos.z;
      if (dx * dx + dz * dz <= 12 * 12) continue;
      spawnMob(x + 0.5, h + 1, z + 0.5);
    }
  }
  function updateMobs(dt) {
    for (let i = mobs.length - 1; i >= 0; i--) {
      const m = mobs[i];
      const dxp = m.pos.x - player.pos.x, dzp = m.pos.z - player.pos.z;
      if (!chunkLoaded(m.pos.x, m.pos.z) || (dxp * dxp + dzp * dzp) > 100 * 100) {
        scene.remove(m.group);
        mobs.splice(i, 1);
        continue;
      }
      m.timer -= dt;
      if (m.timer <= 0) {
        if (m.state === 'idle') { m.state = 'walk'; m.yaw = Math.random() * Math.PI * 2; m.timer = 1.5 + Math.random() * 2.5; }
        else { m.state = 'idle'; m.timer = 1 + Math.random() * 2; }
      }
      const walking = m.state === 'walk';
      const hw = m.kind === 0 ? 0.3 : 0.22;
      const ht = m.kind === 0 ? 0.75 : 0.5;
      if (walking) {
        const speed = m.kind === 0 ? 1.3 : 1.6;
        let fwdX = Math.cos(m.yaw), fwdZ = -Math.sin(m.yaw);
        
        const ax = Math.floor(m.pos.x + fwdX), az = Math.floor(m.pos.z + fwdZ), ay = Math.floor(m.pos.y);
        const ahead = world.get(key(ax, ay, az));
        let drop = true;
        for (let d = 1; d <= 3; d++) { if (solidAt(ax, ay - d, az)) { drop = false; break; } }
        if (ahead === WATER || ahead === LAVA || drop) {
          m.yaw += Math.PI + (Math.random() - 0.5);
          fwdX = Math.cos(m.yaw); fwdZ = -Math.sin(m.yaw);
        }
        m.vel.x = fwdX * speed;
        m.vel.z = fwdZ * speed;
      } else {
        m.vel.x = 0; m.vel.z = 0;
      }
      m.vel.y -= 32 * dt;
      m.grounded = false; m.hitWall = false;
      moveEnt(m, hw, ht, 'x', m.vel.x * dt);
      moveEnt(m, hw, ht, 'z', m.vel.z * dt);
      moveEnt(m, hw, ht, 'y', m.vel.y * dt);
      if (walking && m.hitWall) m.yaw = Math.random() * Math.PI * 2;
      if (walking) {
        m.phase += dt * 8;
        for (let l = 0; l < m.legs.length; l++) m.legs[l].rotation.x = Math.sin(m.phase) * 0.6 * (l % 2 === 0 ? 1 : -1);
      } else {
        for (const leg of m.legs) leg.rotation.x += (0 - leg.rotation.x) * Math.min(1, 10 * dt);
      }
      m.group.position.copy(m.pos);
      m.group.rotation.y = m.yaw;
    }
  }

  
  
  const spawnPos = findSpawn();
  forceSpawnArea(spawnPos[0], spawnPos[2]);
  const SPAWN = new THREE.Vector3(...spawnPos);
  const player = {
    pos: SPAWN.clone(),
    vel: new THREE.Vector3(),
    yaw: 0,
    pitch: 0,
    grounded: false,
    hitWall: false,
  };
  mobsReady = true;   
  let wasInWater = false;
  let underwater = false;
  let bubbleTimer = 0;

  
  const PART_CAP = 160;
  const partMesh = new THREE.InstancedMesh(
    new THREE.BoxGeometry(0.12, 0.12, 0.12),
    new THREE.MeshBasicMaterial({ color: 0xcfe4ff, transparent: true, opacity: 0.9 }),
    PART_CAP
  );
  partMesh.count = 0;
  partMesh.frustumCulled = false;
  scene.add(partMesh);
  const particles = [];
  const unitQuat = new THREE.Quaternion();
  const scaleVec = new THREE.Vector3();
  function spawnPart(x, y, z, vx, vy, vz, life, s = 1) {
    if (particles.length < PART_CAP) {
      particles.push({ pos: new THREE.Vector3(x, y, z), vel: new THREE.Vector3(vx, vy, vz), life, s });
    }
  }
  function burst(x, y, z, n, up) {
    for (let i = 0; i < n; i++) {
      spawnPart(x, y, z,
        Math.random() * 4 - 2, up * (0.5 + Math.random() * 0.5), Math.random() * 4 - 2,
        0.5 + Math.random() * 0.3, 0.6 + Math.random() * 0.6);
    }
  }
  
  function splashBurst(x, z, footY, strength) {
    let surface = Math.floor(footY);
    const fx = Math.floor(x), fz = Math.floor(z);
    for (let i = 0; i < 8; i++) {
      if (world.get(key(fx, surface + 1, fz)) === WATER) surface++;
      else break;
    }
    surface += 1;                                  
    const n = 18 + Math.round(strength * 14);
    for (let i = 0; i < n; i++) {
      const a = (i / n) * Math.PI * 2;
      const rv = 1.2 + Math.random() * 2 + strength * 1.5;
      spawnPart(x + Math.cos(a) * 0.25, surface, z + Math.sin(a) * 0.25,
        Math.cos(a) * rv, 2.5 + Math.random() * 2.5 + strength * 1.5, Math.sin(a) * rv,
        0.45 + Math.random() * 0.4, 0.5 + Math.random() * 0.9);
    }
    for (let i = 0; i < 10; i++) {                 
      spawnPart(x, surface, z,
        Math.random() * 1.2 - 0.6, 1 + Math.random(), Math.random() * 1.2 - 0.6,
        0.3 + Math.random() * 0.2, 0.4 + Math.random() * 0.4);
    }
  }
  function updateParticles(dt) {
    for (let i = particles.length - 1; i >= 0; i--) {
      const p = particles[i];
      p.life -= dt;
      if (p.life <= 0) { particles.splice(i, 1); continue; }
      p.vel.y -= 12 * dt;
      p.pos.addScaledVector(p.vel, dt);
    }
    for (let i = 0; i < particles.length; i++) {
      const p = particles[i];
      tmpMat.compose(p.pos, unitQuat, scaleVec.setScalar(p.s));
      partMesh.setMatrixAt(i, tmpMat);
    }
    partMesh.count = particles.length;
    partMesh.instanceMatrix.needsUpdate = true;
  }

  const keys = { w: false, a: false, s: false, d: false, space: false, sprint: false, down: false };
  let flying = false;
  let lastSpaceDown = 0;
  let stepTimer = 0;
  let swimTimer = 0;
  let lavaOn = false;
  
  function updateFlyUI() {
    if (downBtn) downBtn.style.display = (flying && coarse) ? 'block' : 'none';
  }

  
  const TERRAIN_NAMES = ['Grass', 'Dirt', 'Stone', 'Sand', 'Log', 'Leaves'];
  const PLACEABLE = [0, 1, 2, 3, 4, 5];
  for (let i = 0; i < KUBES.length; i++) PLACEABLE.push(TERRAIN + i);
  const slotInfo = (type) => type < TERRAIN
    ? { name: TERRAIN_NAMES[type], icon: terrainIcons[type] }
    : { name: KUBES[type - TERRAIN].name, icon: icons[type - TERRAIN] };
  let selected = 0;               
  const slots = [];
  PLACEABLE.forEach((type, i) => {
    const info = slotInfo(type);
    const b = document.createElement('button');
    b.type = 'button';
    b.className = 'kc-slot';
    b.title = info.name;
    b.style.backgroundImage = `url(${info.icon})`;
    b.addEventListener('click', () => selectSlot(i));
    hotbarEl.appendChild(b);
    slots.push(b);
  });
  function selectSlot(i) {
    const prev = selected;
    selected = ((i % PLACEABLE.length) + PLACEABLE.length) % PLACEABLE.length;
    slots.forEach((s, j) => s.classList.toggle('kc-sel', j === selected));
    slots[selected].scrollIntoView({ block: 'nearest', inline: 'nearest' });
    nameEl.textContent = slotInfo(PLACEABLE[selected]).name;
    if (selected !== prev) sndClick();
  }
  selectSlot(0);

  
  const fwd = new THREE.Vector3();
  function raycastVoxel() {
    camera.getWorldDirection(fwd);
    let x = Math.floor(camera.position.x);
    let y = Math.floor(camera.position.y);
    let z = Math.floor(camera.position.z);
    const sx = Math.sign(fwd.x), sy = Math.sign(fwd.y), sz = Math.sign(fwd.z);
    const tdx = fwd.x !== 0 ? Math.abs(1 / fwd.x) : Infinity;
    const tdy = fwd.y !== 0 ? Math.abs(1 / fwd.y) : Infinity;
    const tdz = fwd.z !== 0 ? Math.abs(1 / fwd.z) : Infinity;
    const bound = (o, s) => s > 0 ? (Math.floor(o) + 1 - o) : (o - Math.floor(o));
    let tx = fwd.x !== 0 ? bound(camera.position.x, sx) * tdx : Infinity;
    let ty = fwd.y !== 0 ? bound(camera.position.y, sy) * tdy : Infinity;
    let tz = fwd.z !== 0 ? bound(camera.position.z, sz) * tdz : Infinity;
    let px = x, py = y, pz = z;
    let dist = 0;
    while (dist <= REACH) {
      if (solidAt(x, y, z)) return { hit: [x, y, z], prev: [px, py, pz] };
      px = x; py = y; pz = z;
      if (tx < ty && tx < tz) { x += sx; dist = tx; tx += tdx; }
      else if (ty < tz) { y += sy; dist = ty; ty += tdy; }
      else { z += sz; dist = tz; tz += tdz; }
    }
    return null;
  }

  
  function voxelHitsBox(vx, vy, vz, p, hw, ht) {
    return vx + 1 > p.x - hw && vx < p.x + hw &&
           vy + 1 > p.y && vy < p.y + ht &&
           vz + 1 > p.z - hw && vz < p.z + hw;
  }

  function place() {
    const r = raycastVoxel();
    if (!r) return;
    const [x, y, z] = r.prev;
    if (y < 1 || y > 48) return;
    const type = PLACEABLE[selected];
    
    
    if (voxelHitsBox(x, y, z, player.pos, PHW, PHT)) {
      const pen = (y + 1) - player.pos.y;
      const fx = Math.floor(player.pos.x), fz = Math.floor(player.pos.z);
      const headroom = !solidAt(fx, y + 1, fz) && !solidAt(fx, y + 2, fz);
      const ok = !player.grounded && player.pitch < -0.6 && pen > 0 && pen <= 0.55 && headroom;
      if (!ok) return;
      if (setBlock(x, y, z, type)) {
        player.pos.y = y + 1 + 0.001;    
        player.vel.y = Math.max(player.vel.y, 0);
        sndPlace(type);
      }
      return;
    }
    if (setBlock(x, y, z, type)) sndPlace(type);
  }

  function breakBlock() {
    const r = raycastVoxel();
    if (!r) return;
    const [x, y, z] = r.hit;
    if (y === 0) return;                 
    const t = world.get(key(x, y, z));   
    if (removeBlock(x, y, z)) sndBreak(t);
  }

  
  
  
  function moveEnt(ent, hw, ht, a, amount) {
    ent.pos[a] += amount;
    const p = ent.pos;
    const minX = Math.floor(p.x - hw), maxX = Math.floor(p.x + hw);
    const minY = Math.floor(p.y), maxY = Math.floor(p.y + ht);
    const minZ = Math.floor(p.z - hw), maxZ = Math.floor(p.z + hw);
    for (let x = minX; x <= maxX; x++) {
      for (let y = minY; y <= maxY; y++) {
        for (let z = minZ; z <= maxZ; z++) {
          if (!solidAt(x, y, z)) continue;
          if (!voxelHitsBox(x, y, z, p, hw, ht)) continue;
          const eps = 0.001;
          if (a === 'x') { p.x = amount > 0 ? x - hw - eps : x + 1 + hw + eps; ent.hitWall = true; }
          else if (a === 'z') { p.z = amount > 0 ? z - hw - eps : z + 1 + hw + eps; ent.hitWall = true; }
          else {
            if (amount < 0) { p.y = y + 1 + eps; ent.grounded = true; }
            else p.y = y - ht - eps;
          }
          ent.vel[a] = 0;
          return;
        }
      }
    }
  }

  const wish = new THREE.Vector3();
  const joy = { x: 0, z: 0 };     
  function physics(dt) {
    
    let fx = 0, fz = 0;
    if (keys.w) fz -= 1;
    if (keys.s) fz += 1;
    if (keys.a) fx -= 1;
    if (keys.d) fx += 1;
    fx += joy.x; fz += joy.z;
    wish.set(fx, 0, fz);
    if (wish.lengthSq() > 1) wish.normalize();
    fx = wish.x; fz = wish.z;
    const moving = fx !== 0 || fz !== 0;

    
    
    
    const px = Math.floor(player.pos.x), pz = Math.floor(player.pos.z);
    const fy = player.pos.y;
    const ankleCell = world.get(key(px, Math.floor(fy + 0.1), pz));
    const kneeCell = world.get(key(px, Math.floor(fy + 0.5), pz));
    const chestCell = world.get(key(px, Math.floor(fy + 1.1), pz));
    const eyeCell = world.get(key(px, Math.floor(fy + EYE), pz));
    const eyeInWater = eyeCell === WATER;
    const chestInWater = chestCell === WATER;
    const inWater = eyeInWater || chestInWater || ankleCell === WATER || kneeCell === WATER;
    const eyeInLava = eyeCell === LAVA;
    const chestInLava = chestCell === LAVA;
    const inLava = eyeInLava || chestInLava || ankleCell === LAVA || kneeCell === LAVA;

    
    
    if (inLava !== lavaOn) {
      lavaOn = inLava;
      lavaEl.style.display = lavaOn ? 'block' : 'none';
    }

    
    const vy0 = player.vel.y;
    if (inWater && !wasInWater) {
      splashBurst(player.pos.x, player.pos.z, player.pos.y, Math.min(1, Math.abs(vy0) / 8));
    } else if (!inWater && wasInWater && vy0 > 0) {
      burst(player.pos.x, Math.floor(player.pos.y) + 1, player.pos.z, 8, 2.5);
    }
    wasInWater = inWater;

    let speed = (keys.sprint && moving) ? SPRINT : SPEED;
    if (inWater && !flying) speed *= 0.5;
    else if (inLava && !flying) speed *= 0.35;
    if (flying) speed = keys.sprint ? 17 : 10.8;

    
    const cos = Math.cos(player.yaw), sin = Math.sin(player.yaw);
    const tx = (fx * cos + fz * sin) * speed;
    const tz = (-fx * sin + fz * cos) * speed;
    const accel = flying ? 12 : ((inWater || inLava) ? 8 : (player.grounded ? 25 : 6));
    const k = Math.min(1, accel * dt);
    player.vel.x += (tx - player.vel.x) * k;
    player.vel.z += (tz - player.vel.z) * k;

    if (flying) {
      const vy = keys.space ? 9 : (keys.down ? -9 : 0);
      player.vel.y += (vy - player.vel.y) * Math.min(1, 10 * dt);
    } else if (chestInWater || chestInLava) {
      
      
      
      const targetY = chestInLava
        ? (keys.space ? (eyeInLava ? 3.0 : 1.5) : -1.2)
        : (keys.space ? (eyeInWater ? 4.5 : 2.2) : -2.0);
      player.vel.y += (targetY - player.vel.y) * Math.min(1, 4 * dt);
    } else {
      
      player.vel.y -= GRAV * dt;
      if (keys.space && player.grounded) player.vel.y = JUMP;
    }

    player.grounded = false;
    player.hitWall = false;
    moveEnt(player, PHW, PHT, 'x', player.vel.x * dt);
    moveEnt(player, PHW, PHT, 'z', player.vel.z * dt);
    
    if (!flying && (inWater || inLava) && player.hitWall && keys.space) player.vel.y = Math.max(player.vel.y, 6.5);
    moveEnt(player, PHW, PHT, 'y', player.vel.y * dt);

    if (player.pos.y < -16) {
      player.pos.copy(SPAWN);
      player.vel.set(0, 0, 0);
    }

    
    const hSpeed = Math.hypot(player.vel.x, player.vel.z);
    if (player.grounded && !flying && !inWater && hSpeed > 2) {
      stepTimer -= dt;
      if (stepTimer <= 0) { stepTimer = keys.sprint ? 0.3 : 0.38; sndStep(); }
    } else {
      stepTimer = 0;
    }
    if (inWater && !flying && !eyeInWater && hSpeed > 2) {
      swimTimer -= dt;
      if (swimTimer <= 0) {
        swimTimer = 0.15;
        for (let i = 0; i < 2; i++) {
          spawnPart(player.pos.x, player.pos.y + 0.9, player.pos.z,
            Math.random() * 2 - 1, 1.5, Math.random() * 2 - 1, 0.35, 0.5);
        }
      }
    }

    
    if (eyeInWater) {
      bubbleTimer -= dt;
      if (bubbleTimer <= 0) {
        bubbleTimer = 0.35;
        for (let i = 0; i < 2; i++) {
          spawnPart(player.pos.x, player.pos.y + EYE, player.pos.z,
            Math.random() * 0.4 - 0.2, 1.5, Math.random() * 0.4 - 0.2, 0.8);
        }
      }
    } else {
      bubbleTimer = 0;
    }
    if (eyeInWater !== underwater) {
      underwater = eyeInWater;
      scene.fog.near = underwater ? 0.5 : 45;
      scene.fog.far = underwater ? 14 : 78;
      scene.background.set(underwater ? '#2a5aa0' : '#87ceeb');
      waterEl.style.display = underwater ? 'block' : 'none';
    }

    camera.position.set(player.pos.x, player.pos.y + EYE, player.pos.z);
    camera.rotation.y = player.yaw;
    camera.rotation.x = player.pitch;
  }

  
  const coarse = window.matchMedia('(pointer: coarse)').matches;
  let locked = false;
  function updateOverlay() {
    overlay.classList.toggle('kc-hidden', locked || coarse);
  }
  updateOverlay();

  canvas.addEventListener('click', () => {
    initAudio();
    if (!coarse && !locked) canvas.requestPointerLock();
  });
  document.addEventListener('pointerlockchange', () => {
    locked = document.pointerLockElement === canvas;
    wheelAcc = 0;
    updateOverlay();
  });
  document.addEventListener('mousemove', (e) => {
    if (!locked) return;
    player.yaw -= e.movementX * 0.0025;
    player.pitch -= e.movementY * 0.0025;
    player.pitch = Math.max(-1.55, Math.min(1.55, player.pitch));
  });
  canvas.addEventListener('mousedown', (e) => {
    if (!locked) return;
    e.preventDefault();
    if (e.button === 0) breakBlock();
    else if (e.button === 2) place();
  });
  canvas.addEventListener('contextmenu', (e) => e.preventDefault());
  
  
  
  let wheelAcc = 0, lastWheelStep = 0;
  canvas.addEventListener('wheel', (e) => {
    if (!locked) return;
    e.preventDefault();
    const now = performance.now();
    if (now - lastWheelStep < 60) return;
    const d = e.deltaMode === 1 ? e.deltaY * 33 : e.deltaY;   
    wheelAcc += d;
    if (Math.abs(wheelAcc) >= 60) {
      selectSlot(selected + (wheelAcc > 0 ? 1 : -1));
      wheelAcc = 0;
      lastWheelStep = now;
    }
  }, { passive: false });

  
  const codeMap = { KeyW: 'w', KeyA: 'a', KeyS: 's', KeyD: 'd' };
  window.addEventListener('keydown', (e) => {
    if (!locked) return;
    if (codeMap[e.code]) { keys[codeMap[e.code]] = true; e.preventDefault(); }
    else if (e.code === 'Space') {
      
      if (!e.repeat) {
        const n = performance.now();
        if (n - lastSpaceDown < 300) { flying = !flying; player.vel.y = 0; updateFlyUI(); }
        lastSpaceDown = n;
      }
      keys.space = true; e.preventDefault();
    }
    else if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') { keys.down = true; e.preventDefault(); }
    else if (e.code === 'ControlLeft' || e.code === 'ControlRight') { keys.sprint = true; e.preventDefault(); }
    else if (e.code === 'Digit0') selectSlot(9);
    else if (/^Digit[1-9]$/.test(e.code)) selectSlot(Number(e.code.slice(5)) - 1);
  });
  window.addEventListener('keyup', (e) => {
    if (codeMap[e.code]) keys[codeMap[e.code]] = false;
    else if (e.code === 'Space') keys.space = false;
    else if (e.code === 'ShiftLeft' || e.code === 'ShiftRight') keys.down = false;
    else if (e.code === 'ControlLeft' || e.code === 'ControlRight') keys.sprint = false;
  });

  
  let joyId = null, joyOx = 0, joyOy = 0;
  let lookId = null, lookX = 0, lookY = 0, lookStartX = 0, lookStartY = 0, lookStart = 0, lookMoved = 0;
  let holdTimer = 0;
  const rect = () => canvas.getBoundingClientRect();

  canvas.addEventListener('pointerdown', (e) => {
    if (e.pointerType !== 'touch') return;
    initAudio();
    const r = rect();
    const lx = e.clientX - r.left;
    if (lx < r.width * 0.4 && joyId === null) {
      joyId = e.pointerId;
      joyOx = e.clientX; joyOy = e.clientY;
      canvas.setPointerCapture(e.pointerId);
    } else if (lookId === null) {
      lookId = e.pointerId;
      lookX = e.clientX; lookY = e.clientY;
      lookStartX = e.clientX; lookStartY = e.clientY;
      lookStart = performance.now();
      lookMoved = 0;
      canvas.setPointerCapture(e.pointerId);
      holdTimer = setTimeout(() => { breakBlock(); holdTimer = 0; }, 450);
    }
  });
  canvas.addEventListener('pointermove', (e) => {
    if (e.pointerType !== 'touch') return;
    if (e.pointerId === joyId) {
      let dx = e.clientX - joyOx, dy = e.clientY - joyOy;
      const dz = Math.hypot(dx, dy);
      if (dz < 10) { joy.x = 0; joy.z = 0; return; }
      const s = Math.min(dz, 50) / 50 / dz;
      joy.x = dx * s; joy.z = dy * s;
    } else if (e.pointerId === lookId) {
      const mx = e.clientX - lookX, my = e.clientY - lookY;
      lookX = e.clientX; lookY = e.clientY;
      lookMoved += Math.abs(mx) + Math.abs(my);
      player.yaw -= mx * 0.005;
      player.pitch -= my * 0.005;
      player.pitch = Math.max(-1.55, Math.min(1.55, player.pitch));
      if (lookMoved > 8 && holdTimer) { clearTimeout(holdTimer); holdTimer = 0; }
    }
  });
  function endTouch(e) {
    if (e.pointerType !== 'touch') return;
    if (e.pointerId === joyId) { joyId = null; joy.x = 0; joy.z = 0; }
    else if (e.pointerId === lookId) {
      if (holdTimer) { clearTimeout(holdTimer); holdTimer = 0; }
      const held = performance.now() - lookStart;
      const dist = Math.hypot(e.clientX - lookStartX, e.clientY - lookStartY);
      if (held < 300 && dist < 8) place();
      lookId = null;
    }
  }
  canvas.addEventListener('pointerup', endTouch);
  canvas.addEventListener('pointercancel', endTouch);

  jumpBtn.addEventListener('pointerdown', (e) => {
    e.preventDefault();
    initAudio();
    
    const n = performance.now();
    if (n - lastSpaceDown < 300) { flying = !flying; player.vel.y = 0; updateFlyUI(); }
    lastSpaceDown = n;
    keys.space = true;
  });
  jumpBtn.addEventListener('pointerup', (e) => { e.preventDefault(); keys.space = false; });
  jumpBtn.addEventListener('pointercancel', () => { keys.space = false; });
  downBtn.addEventListener('pointerdown', (e) => { e.preventDefault(); keys.down = true; });
  downBtn.addEventListener('pointerup', (e) => { e.preventDefault(); keys.down = false; });
  downBtn.addEventListener('pointercancel', () => { keys.down = false; });

  
  resetBtn.addEventListener('click', () => {
    try { localStorage.removeItem(STORE_KEY); } catch (e) {   }
    edits.clear();
    editsByChunk.clear();
    loadedChunks.clear();
    genQueue.length = 0;
    unloadCands.length = 0;
    curChunk = null;
    world.clear();
    fluidLvl.clear();
    activeWater.clear();
    activeLava.clear();
    for (let t = 0; t < TYPES; t++) {
      keysByType[t].length = 0;
      keySets[t].clear();
      meshes[t].count = 0;
      meshes[t].instanceMatrix.needsUpdate = true;
    }
    for (const m of mobs) scene.remove(m.group);
    mobs.length = 0;
    flying = false;
    updateFlyUI();
    SEED = (Math.random() * 2 ** 31) | 0;
    const sp = findSpawn();
    forceSpawnArea(sp[0], sp[2]);
    SPAWN.set(...sp);
    player.pos.copy(SPAWN);
    player.vel.set(0, 0, 0);
    scheduleSave();    
  });

  
  function resize() {
    const w = holder.clientWidth, h = holder.clientHeight;
    if (!w || !h) return;
    renderer.setSize(w, h, false);
    camera.aspect = w / h;
    camera.updateProjectionMatrix();
  }
  new ResizeObserver(resize).observe(holder);
  resize();

  streamChunks();   

  
  let last = performance.now();
  let waterAcc = 0, lavaAcc = 0;
  function frame(now) {
    const dt = Math.min((now - last) / 1000, 0.05);
    last = now;

    streamChunks();
    physics(dt);
    if (activeWater.size) { waterAcc += dt; while (waterAcc >= 0.25) { waterAcc -= 0.25; fluidTick(WATER, activeWater, 1, 1); } }
    if (activeLava.size) { lavaAcc += dt; while (lavaAcc >= 0.5) { lavaAcc -= 0.5; fluidTick(LAVA, activeLava, 2, 2); } }
    updateParticles(dt);
    updateMobs(dt);

    const r = raycastVoxel();
    if (r) {
      highlight.visible = true;
      highlight.position.set(r.hit[0] + 0.5, r.hit[1] + 0.5, r.hit[2] + 0.5);
    } else {
      highlight.visible = false;
    }

    if (!reduceMotion) {
      for (const c of clouds) {
        c.position.x += dt * 0.5;
        
        if (c.position.x - player.pos.x > 120) c.position.x -= 240;
        if (c.position.x - player.pos.x < -120) c.position.x += 240;
        if (c.position.z - player.pos.z > 120) c.position.z -= 240;
        if (c.position.z - player.pos.z < -120) c.position.z += 240;
      }
    }

    renderer.render(scene, camera);
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
}
</script>

]]></content><author><name>Ro'i Bandel</name></author><published>0001-01-01T00:00:00Z</published></entry></feed>