Загрузка данных


<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Minecraft 1.12.2 — Web Edition</title>
<style>
  *{margin:0;padding:0;box-sizing:border-box}
  html,body{width:100%;height:100%;overflow:hidden;background:#87ceeb;
    font-family:'Courier New',monospace;user-select:none}
  canvas{display:block}

  /* ---------- Прицел ---------- */
  #crosshair{position:fixed;left:50%;top:50%;width:20px;height:20px;
    margin:-10px 0 0 -10px;pointer-events:none;z-index:20;mix-blend-mode:difference}
  #crosshair::before,#crosshair::after{content:'';position:absolute;background:#fff}
  #crosshair::before{left:9px;top:0;width:2px;height:20px}
  #crosshair::after{top:9px;left:0;height:2px;width:20px}

  /* ---------- Инвентарь ---------- */
  #hotbar{position:fixed;left:50%;bottom:14px;transform:translateX(-50%);
    display:flex;gap:3px;z-index:20;padding:3px;
    background:rgba(0,0,0,.35);border:2px solid rgba(0,0,0,.6);border-radius:4px}
  .slot{width:52px;height:52px;border:2px solid #5a5a5a;background:rgba(139,139,139,.35);
    display:flex;align-items:center;justify-content:center;position:relative}
  .slot.sel{border-color:#fff;background:rgba(255,255,255,.22);
    box-shadow:0 0 6px rgba(255,255,255,.7)}
  .slot canvas{image-rendering:pixelated;width:40px;height:40px}

  /* ---------- Название блока ---------- */
  #blockname{position:fixed;left:50%;bottom:80px;transform:translateX(-50%);
    color:#fff;text-shadow:2px 2px 0 #000;font-size:16px;font-weight:bold;
    z-index:20;opacity:0;transition:opacity .35s;pointer-events:none;white-space:nowrap}
  #blockname.show{opacity:1}

  /* ---------- Отладка ---------- */
  #debug{position:fixed;left:8px;top:8px;color:#fff;text-shadow:1px 1px 0 #000;
    font-size:13px;line-height:1.45;z-index:20;pointer-events:none;white-space:pre}

  /* ---------- Стартовый экран ---------- */
  #overlay{position:fixed;inset:0;background:rgba(0,0,0,.62);z-index:50;
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    color:#fff;text-align:center;cursor:pointer;backdrop-filter:blur(2px)}
  #overlay h1{font-size:44px;letter-spacing:4px;text-shadow:4px 4px 0 #3a3a3a;margin-bottom:6px}
  #overlay h2{font-size:16px;color:#bdbdbd;font-weight:normal;margin-bottom:28px}
  #overlay .keys{font-size:14px;line-height:2;color:#e0e0e0}
  #overlay .keys b{color:#7ec850}
  #overlay .hint{margin-top:26px;font-size:17px;color:#ffe57f;animation:blink 1.4s infinite}
  @keyframes blink{0%,100%{opacity:1}50%{opacity:.35}}
  #overlay.hidden{display:none}
</style>
</head>
<body>

<div id="crosshair"></div>
<div id="debug"></div>
<div id="blockname"></div>
<div id="hotbar"></div>

<div id="overlay">
  <h1>MINECRAFT</h1>
  <h2>1.12.2 — Web Edition</h2>
  <div class="keys">
    <b>W A S D</b> — движение &nbsp;•&nbsp; <b>Пробел</b> — прыжок<br>
    <b>Shift</b> — крадучись &nbsp;•&nbsp; <b>Ctrl</b> — бег<br>
    <b>ЛКМ</b> — сломать блок &nbsp;•&nbsp; <b>ПКМ</b> — поставить блок<br>
    <b>1–9 / колесо</b> — выбор блока &nbsp;•&nbsp; <b>F</b> — режим полёта<br>
    <b>Двойной пробел</b> — вкл/выкл полёт
  </div>
  <div class="hint">Нажмите, чтобы играть</div>
</div>

<script type="module">
import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';

/* =========================================================
   1. ГЕНЕРАЦИЯ ТЕКСТУРНОГО АТЛАСА  (16 тайлов 16×16)
   ========================================================= */
const TILE = 16, COLS = 4, ROWS = 4;
const atlasCanvas = document.createElement('canvas');
atlasCanvas.width  = TILE * COLS;
atlasCanvas.height = TILE * ROWS;
const actx = atlasCanvas.getContext('2d');
actx.imageSmoothingEnabled = false;

function hash2(x, y, s) {
  let n = Math.imul(x | 0, 374761393) ^ Math.imul(y | 0, 668265263) ^ Math.imul(s | 0, 1274126177);
  n = Math.imul(n ^ (n >>> 13), 1274126177);
  n ^= n >>> 16;
  return (n >>> 0) / 4294967296;
}
function px(tile, x, y, r, g, b, a = 1) {
  const cx = (tile % COLS) * TILE, cy = ((tile / COLS) | 0) * TILE;
  actx.fillStyle = `rgba(${r | 0},${g | 0},${b | 0},${a})`;
  actx.fillRect(cx + x, cy + y, 1, 1);
}
function noiseTile(tile, r, g, b, spread, seed) {
  for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++) {
    const n = (hash2(x, y, seed) - 0.5) * spread;
    px(tile, x, y, r + n, g + n, b + n);
  }
}
function cobbleTile(tile, base, spread, seed) {
  for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++) {
    const c = hash2(x >> 2, y >> 2, seed);
    let v = base + (c - 0.5) * spread + (hash2(x, y, seed + 1) - 0.5) * 18;
    if ((x & 3) === 3 || (y & 3) === 3) v -= 34;
    px(tile, x, y, v, v, v);
  }
}

/* --- 0: верх травы --- */
noiseTile(0, 96, 152, 62, 42, 11);
/* --- 1: земля --- */
noiseTile(1, 134, 96, 67, 34, 22);
/* --- 2: бок травы --- */
noiseTile(2, 134, 96, 67, 34, 22);
for (let x = 0; x < 16; x++) {
  const h = 3 + Math.floor(hash2(x, 77, 5) * 3);
  for (let y = 0; y < h; y++) {
    const n = (hash2(x, y, 31) - 0.5) * 40;
    px(2, x, y, 96 + n, 152 + n, 62 + n);
  }
}
/* --- 3: камень --- */
noiseTile(3, 128, 128, 128, 30, 33);
/* --- 4: булыжник --- */
cobbleTile(4, 124, 46, 44);
/* --- 5: песок --- */
noiseTile(5, 219, 211, 160, 24, 55);
/* --- 6: бок бревна --- */
for (let x = 0; x < 16; x++) {
  const s = hash2(x, 0, 66);
  const dark = hash2(x, 0, 67) > 0.78 ? 0.78 : 1;
  for (let y = 0; y < 16; y++) {
    const n = (hash2(x, y, 68) - 0.5) * 14;
    px(6, x, y, (104 + s * 34 + n) * dark, (76 + s * 26 + n) * dark, (44 + s * 18 + n) * dark);
  }
}
/* --- 7: спил бревна --- */
for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++) {
  const dx = x - 7.5, dy = y - 7.5;
  const d = Math.sqrt(dx * dx + dy * dy);
  const ring = Math.sin(d * 2.3) * 0.5 + 0.5;
  const n = (hash2(x, y, 77) - 0.5) * 12;
  let r = 168 + ring * 30 + n, g = 132 + ring * 24 + n, b = 84 + ring * 16 + n;
  if (d > 7.2) { r *= .7; g *= .7; b *= .7; }
  px(7, x, y, r, g, b);
}
/* --- 8: листва --- */
for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++) {
  const h = hash2(x, y, 88);
  let r = 52 + h * 34, g = 112 + h * 56, b = 40 + h * 26;
  if (h > 0.86) { r *= .55; g *= .55; b *= .55; }
  if (h < 0.10) { r *= .7;  g *= .75; b *= .7; }
  px(8, x, y, r, g, b);
}
/* --- 9: доски --- */
for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++) {
  const row = y >> 2;
  let v = (hash2(x, row, 99) - 0.5) * 26;
  let r = 168 + v, g = 128 + v, b = 78 + v;
  if ((y & 3) === 3) { r -= 52; g -= 42; b -= 30; }
  const seam = (row * 5 + 3) % 16;
  if (x === seam) { r -= 44; g -= 36; b -= 26; }
  px(9, x, y, r, g, b);
}
/* --- 10: кирпич --- */
for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++) {
  const row = y >> 2;
  const bx = (x + (row % 2) * 4) % 8;
  const mortar = (y & 3) === 3 || bx === 7;
  const n = (hash2(x, y, 111) - 0.5) * 22;
  if (mortar) px(10, x, y, 176 + n, 172 + n, 168 + n);
  else        px(10, x, y, 152 + n, 74 + n, 58 + n);
}
/* --- 11: бедрок --- */
cobbleTile(11, 74, 70, 121);
/* --- 12: угольная руда --- */
noiseTile(12, 128, 128, 128, 30, 131);
for (let i = 0; i < 9; i++) {
  const cx = Math.floor(hash2(i, 3, 141) * 14) + 1;
  const cy = Math.floor(hash2(i, 7, 142) * 14) + 1;
  const s  = 1 + Math.floor(hash2(i, 9, 143) * 2);
  for (let y = 0; y < s; y++) for (let x = 0; x < s; x++) {
    const n = (hash2(cx + x, cy + y, 144) - 0.5) * 24;
    px(12, cx + x, cy + y, 26 + n, 26 + n, 28 + n);
  }
}
/* --- 13: гравий --- */
cobbleTile(13, 132, 60, 151);
for (let y = 0; y < 16; y++) for (let x = 0; x < 16; x++) {
  if (hash2(x, y, 161) > 0.88) {
    const n = (hash2(x, y, 162) - 0.5) * 26;
    px(13, x, y, 116 + n, 106 + n, 96 + n);
  }
}
/* --- 14: светящийся камень --- */
noiseTile(14, 210, 178, 96, 40, 171);
for (let i = 0; i < 26; i++) {
  const cx = Math.floor(hash2(i, 1, 181) * 16);
  const cy = Math.floor(hash2(i, 2, 182) * 16);
  px(14, cx, cy, 255, 244, 190);
}
/* --- 15: обсидиан --- */
noiseTile(15, 32, 24, 52, 26, 191);

/* =========================================================
   2. ОПРЕДЕЛЕНИЯ БЛОКОВ
   ========================================================= */
const BLOCKS = {
  0:  { name: 'Воздух',            tiles: [0, 0, 0] },
  1:  { name: 'Трава',             tiles: [0, 1, 2] },
  2:  { name: 'Земля',             tiles: [1, 1, 1] },
  3:  { name: 'Камень',            tiles: [3, 3, 3] },
  4:  { name: 'Булыжник',          tiles: [4, 4, 4] },
  5:  { name: 'Песок',             tiles: [5, 5, 5] },
  6:  { name: 'Дерево',            tiles: [7, 7, 6] },
  7:  { name: 'Листва',            tiles: [8, 8, 8] },
  8:  { name: 'Доски',             tiles: [9, 9, 9] },
  9:  { name: 'Кирпич',            tiles: [10, 10, 10] },
  10: { name: 'Бедрок',            tiles: [11, 11, 11] },
  11: { name: 'Угольная руда',     tiles: [12, 12, 12] },
  12: { name: 'Гравий',            tiles: [13, 13, 13] },
  13: { name: 'Светокамень',       tiles: [14, 14, 14] },
  14: { name: 'Обсидиан',          tiles: [15, 15, 15] },
};
const HOTBAR = [1, 2, 3, 4, 5, 6, 8, 9, 13];   // 9 слотов

/* =========================================================
   3. СЦЕНА
   ========================================================= */
const scene = new THREE.Scene();
const SKY = 0x8ec9f0;
scene.background = new THREE.Color(SKY);
scene.fog = new THREE.Fog(SKY, 34, 76);

const camera = new THREE.PerspectiveCamera(70, innerWidth / innerHeight, 0.05, 600);
camera.rotation.order = 'YXZ';

const renderer = new THREE.WebGLRenderer({ antialias: false, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5));
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

const tex = new THREE.CanvasTexture(atlasCanvas);
tex.magFilter = THREE.NearestFilter;
tex.minFilter = THREE.NearestFilter;
tex.generateMipmaps = false;
tex.colorSpace = THREE.SRGBColorSpace;

const blockMaterial = new THREE.MeshBasicMaterial({ map: tex, vertexColors: true });

addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

/* =========================================================
   4. МИР (чанки 16×48×16)
   ========================================================= */
const CH = 16, WH = 48;
const RENDER_DIST = 5;
const SEED = 20120601;

const chunks = new Map();        // "cx,cz" -> Uint8Array
const chunkMeshes = new Map();   // "cx,cz" -> THREE.Mesh

const ckey = (cx, cz) => cx + ',' + cz;

function getBlock(x, y, z) {
  if (y < 0 || y >= WH) return 0;
  const cx = Math.floor(x / CH), cz = Math.floor(z / CH);
  const d = chunks.get(ckey(cx, cz));
  if (!d) return 0;
  const lx = x - cx * CH, lz = z - cz * CH;
  return d[lx + lz * CH + y * CH * CH];
}
function setBlock(x, y, z, v) {
  if (y < 0 || y >= WH) return;
  const cx = Math.floor(x / CH), cz = Math.floor(z / CH);
  const d = chunks.get(ckey(cx, cz));
  if (!d) return;
  const lx = x - cx * CH, lz = z - cz * CH;
  d[lx + lz * CH + y * CH * CH] = v;
}
const isSolid = (x, y, z) => getBlock(x, y, z) !== 0;

/* ---------- Шум ---------- */
function noise2(x, z, s) {
  const x0 = Math.floor(x), z0 = Math.floor(z);
  const fx = x - x0, fz = z - z0;
  const sx = fx * fx * (3 - 2 * fx), sz = fz * fz * (3 - 2 * fz);
  const n00 = hash2(x0, z0, s),     n10 = hash2(x0 + 1, z0, s);
  const n01 = hash2(x0, z0 + 1, s), n11 = hash2(x0 + 1, z0 + 1, s);
  return (n00 * (1 - sx) + n10 * sx) * (1 - sz) + (n01 * (1 - sx) + n11 * sx) * sz;
}
function fbm(x, z, s, oct = 4) {
  let a = 0.5, f = 1, sum = 0, norm = 0;
  for (let i = 0; i < oct; i++) {
    sum += a * noise2(x * f, z * f, s + i * 37);
    norm += a; a *= 0.5; f *= 2;
  }
  return sum / norm;
}

/* ---------- Генерация чанка ---------- */
function genChunk(cx, cz) {
  const k = ckey(cx, cz);
  if (chunks.has(k)) return;
  const data = new Uint8Array(CH * CH * WH);
  chunks.set(k, data);

  const heights = new Int16Array(CH * CH);

  for (let lz = 0; lz < CH; lz++) {
    for (let lx = 0; lx < CH; lx++) {
      const wx = cx * CH + lx, wz = cz * CH + lz;
      const f = fbm(wx * 0.021, wz * 0.021, SEED, 4);
      const h = Math.max(4, Math.min(WH - 12, Math.floor(11 + f * 24)));
      heights[lx + lz * CH] = h;
      const beach = h <= 16;

      for (let y = 0; y <= h; y++) {
        let b;
        if (y === 0)          b = 10;                       // бедрок
        else if (y === h)     b = beach ? 5 : 1;            // песок / трава
        else if (y > h - 4)   b = beach ? 5 : 2;            // песок / земля
        else                  b = 3;                        // камень

        if (b === 3 && hash2(wx * 7 + y, wz * 13 + y, 999) > 0.986) b = 11; // уголь
        if (b === 3 && hash2(wx * 3 + y, wz * 5 + y, 555)  > 0.992) b = 12; // гравий
        data[lx + lz * CH + y * CH * CH] = b;
      }
    }
  }

  /* ---------- Деревья (целиком внутри чанка) ---------- */
  for (let lz = 3; lz < CH - 3; lz++) {
    for (let lx = 3; lx < CH - 3; lx++) {
      const wx = cx * CH + lx, wz = cz * CH + lz;
      const h = heights[lx + lz * CH];
      if (h <= 16) continue;
      if (hash2(wx, wz, 7777) < 0.986) continue;
      if (data[lx + lz * CH + h * CH * CH] !== 1) continue;

      const th = 4 + Math.floor(hash2(wx, wz, 7778) * 3);
      if (h + th + 2 >= WH) continue;

      for (let i = 1; i <= th; i++) data[lx + lz * CH + (h + i) * CH * CH] = 6;

      const top = h + th;
      for (let dy = -2; dy <= 2; dy++) {
        for (let dx = -2; dx <= 2; dx++) {
          for (let dz = -2; dz <= 2; dz++) {
            const r = Math.abs(dx) + Math.abs(dz) + Math.abs(dy) * 1.4;
            if (r > 3.6) continue;
            const px_ = lx + dx, pz = lz + dz, py = top + dy;
            if (px_ < 0 || px_ >= CH || pz < 0 || pz >= CH || py < 0 || py >= WH) continue;
            const idx = px_ + pz * CH + py * CH * CH;
            if (data[idx] === 0) data[idx] = 7;
          }
        }
      }
    }
  }
}

/* =========================================================
   5. ПОСТРОЕНИЕ МЕША ЧАНКА
   ========================================================= */
const FACES = [
  { dir: [-1, 0, 0], corners: [                       // -X
    { pos: [0, 1, 0], uv: [0, 1] }, { pos: [0, 0, 0], uv: [0, 0] },
    { pos: [0, 1, 1], uv: [1, 1] }, { pos: [0, 0, 1], uv: [1, 0] } ] },
  { dir: [1, 0, 0], corners: [                        // +X
    { pos: [1, 1, 1], uv: [0, 1] }, { pos: [1, 0, 1], uv: [0, 0] },
    { pos: [1, 1, 0], uv: [1, 1] }, { pos: [1, 0, 0], uv: [1, 0] } ] },
  { dir: [0, -1, 0], corners: [                       // -Y
    { pos: [1, 0, 1], uv: [1, 0] }, { pos: [0, 0, 1], uv: [0, 0] },
    { pos: [1, 0, 0], uv: [1, 1] }, { pos: [0, 0, 0], uv: [0, 1] } ] },
  { dir: [0, 1, 0], corners: [                        // +Y
    { pos: [0, 1, 1], uv: [1, 1] }, { pos: [1, 1, 1], uv: [0, 1] },
    { pos: [0, 1, 0], uv: [1, 0] }, { pos: [1, 1, 0], uv: [0, 0] } ] },
  { dir: [0, 0, -1], corners: [                       // -Z
    { pos: [1, 0, 0], uv: [0, 0] }, { pos: [0, 0, 0], uv: [1, 0] },
    { pos: [1, 1, 0], uv: [0, 1] }, { pos: [0, 1, 0], uv: [1, 1] } ] },
  { dir: [0, 0, 1], corners: [                        // +Z
    { pos: [0, 0, 1], uv: [0, 0] }, { pos: [1, 0, 1], uv: [1, 0] },
    { pos: [0, 1, 1], uv: [0, 1] }, { pos: [1, 1, 1], uv: [1, 1] } ] },
];

function buildChunkMesh(cx, cz) {
  const k = ckey(cx, cz);
  const data = chunks.get(k);
  if (!data) return;

  const positions = [], uvs = [], colors = [], indices = [];
  let vcount = 0;
  const baseX = cx * CH, baseZ = cz * CH;

  for (let y = 0; y < WH; y++) {
    for (let lz = 0; lz < CH; lz++) {
      for (let lx = 0; lx < CH; lx++) {
        const b = data[lx + lz * CH + y * CH * CH];
        if (b === 0) continue;
        const def = BLOCKS[b];
        if (!def) continue;

        const wx = baseX + lx, wz = baseZ + lz;

        for (let fi = 0; fi < 6; fi++) {
          const f = FACES[fi];
          if (getBlock(wx + f.dir[0], y + f.dir[1], wz + f.dir[2]) !== 0) continue;

          const tileIndex = f.dir[1] === 1 ? def.tiles[0]
                          : f.dir[1] === -1 ? def.tiles[1]
                          : def.tiles[2];
          const tc = tileIndex % COLS;
          const tr = (tileIndex / COLS) | 0;

          const shade = f.dir[1] === 1 ? 1.0
                      : f.dir[1] === -1 ? 0.48
                      : (f.dir[0] !== 0 ? 0.72 : 0.86);

          for (let ci = 0; ci < 4; ci++) {
            const c = f.corners[ci];
            positions.push(lx + c.pos[0], y + c.pos[1], lz + c.pos[2]);
            uvs.push((tc + c.uv[0]) / COLS, (ROWS - 1 - tr + c.uv[1]) / ROWS);
            colors.push(shade, shade, shade);
          }
          indices.push(vcount, vcount + 1, vcount + 2, vcount + 2, vcount + 1, vcount + 3);
          vcount += 4;
        }
      }
    }
  }

  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
  geo.setAttribute('uv',       new THREE.Float32BufferAttribute(uvs, 2));
  geo.setAttribute('color',    new THREE.Float32BufferAttribute(colors, 3));
  geo.setIndex(indices);
  geo.computeBoundingSphere();

  const mesh = new THREE.Mesh(geo, blockMaterial);
  mesh.position.set(baseX, 0, baseZ);
  mesh.frustumCulled = true;

  const old = chunkMeshes.get(k);
  if (old) { scene.remove(old); old.geometry.dispose(); }
  scene.add(mesh);
  chunkMeshes.set(k, mesh);
}

function rebuildAt(x, z) {
  const cx = Math.floor(x / CH), cz = Math.floor(z / CH);
  const keys = new Set([ckey(cx, cz)]);
  const lx = x - cx * CH, lz = z - cz * CH;
  if (lx === 0)      keys.add(ckey(cx - 1, cz));
  if (lx === CH - 1) keys.add(ckey(cx + 1, cz));
  if (lz === 0)      keys.add(ckey(cx, cz - 1));
  if (lz === CH - 1) keys.add(ckey(cx, cz + 1));
  for (const k of keys) {
    if (!chunkMeshes.has(k)) continue;
    const [a, b] = k.split(',').map(Number);
    buildChunkMesh(a, b);
  }
}

/* ---------- Подгрузка / выгрузка чанков ---------- */
function updateChunks() {
  const pcx = Math.floor(player.pos.x / CH);
  const pcz = Math.floor(player.pos.z / CH);

  const need = [];
  for (let dz = -RENDER_DIST; dz <= RENDER_DIST; dz++) {
    for (let dx = -RENDER_DIST; dx <= RENDER_DIST; dx++) {
      const d = Math.sqrt(dx * dx + dz * dz);
      if (d > RENDER_DIST + 0.4) continue;
      need.push([pcx + dx, pcz + dz, d]);
    }
  }
  need.sort((a, b) => a[2] - b[2]);

  let built = 0;
  for (const [cx, cz] of need) {
    if (!chunks.has(ckey(cx, cz))) genChunk(cx, cz);
    if (!chunkMeshes.has(ckey(cx, cz))) {
      genChunk(cx + 1, cz); genChunk(cx - 1, cz);
      genChunk(cx, cz + 1); genChunk(cx, cz - 1);
      buildChunkMesh(cx, cz);
      if (++built >= 3) break;
    }
  }

  /* выгрузка далёких */
  for (const [k, mesh] of chunkMeshes) {
    const [cx, cz] = k.split(',').map(Number);
    if (Math.hypot(cx - pcx, cz - pcz) > RENDER_DIST + 2) {
      scene.remove(mesh); mesh.geometry.dispose(); chunkMeshes.delete(k);
    }
  }
  for (const k of chunks.keys()) {
    const [cx, cz] = k.split(',').map(Number);
    if (Math.hypot(cx - pcx, cz - pcz) > RENDER_DIST + 3) chunks.delete(k);
  }
}

/* =========================================================
   6. ИГРОК И ФИЗИКА
   ========================================================= */
const player = {
  pos: new THREE.Vector3(8, 40, 8),
  vel: new THREE.Vector3(),
  onGround: false,
  flying: false,
};

const WIDTH = 0.3, HEIGHT = 1.8, EYE = 1.62;
const GRAVITY = 28, JUMP = 8.4;

let yaw = 0, pitch = 0;
const keys = {};
let lastSpaceTap = 0;

function playerAABB(p) {
  return {
    minX: p.x - WIDTH, maxX: p.x + WIDTH,
    minY: p.y,         maxY: p.y + HEIGHT,
    minZ: p.z - WIDTH, maxZ: p.z + WIDTH,
  };
}

function moveAxis(axis, delta) {
  if (delta === 0) return false;
  const p = player.pos;
  p[axis] += delta;

  const box = playerAABB(p);
  const x0 = Math.floor(box.minX), x1 = Math.floor(box.maxX);
  const y0 = Math.floor(box.minY), y1 = Math.floor(box.maxY);
  const z0 = Math.floor(box.minZ), z1 = Math.floor(box.maxZ);

  for (let x = x0; x <= x1; x++) {
    for (let y = y0; y <= y1; y++) {
      for (let z = z0; z <= z1; z++) {
        if (!isSolid(x, y, z)) continue;
        if (axis === 'x') p.x = delta > 0 ? x - WIDTH - 1e-4 : x + 1 + WIDTH + 1e-4;
        else if (axis === 'y') p.y = delta > 0 ? y - HEIGHT - 1e-4 : y + 1 + 1e-4;
        else p.z = delta > 0 ? z - WIDTH - 1e-4 : z + 1 + WIDTH + 1e-4;
        player.vel[axis] = 0;
        return true;
      }
    }
  }
  return false;
}

function updatePhysics(dt) {
  const forward = new THREE.Vector3(-Math.sin(yaw), 0, -Math.cos(yaw));
  const right   = new THREE.Vector3( Math.cos(yaw), 0, -Math.sin(yaw));

  let ix = 0, iz = 0;
  if (keys['KeyW']) iz += 1;
  if (keys['KeyS']) iz -= 1;
  if (keys['KeyD']) ix += 1;
  if (keys['KeyA']) ix -= 1;

  const wish = new THREE.Vector3()
    .addScaledVector(forward, iz)
    .addScaledVector(right, ix);
  if (wish.lengthSq() > 0) wish.normalize();

  const sneaking = keys['ShiftLeft'] || keys['ShiftRight'];
  const sprinting = (keys['ControlLeft'] || keys['ControlRight']) && !sneaking;

  let speed = 4.317;
  if (sprinting) speed = 5.612;
  if (sneaking)  speed = 1.3;
  if (player.flying) speed = sprinting ? 22 : 11;

  wish.multiplyScalar(speed);

  if (player.flying) {
    const accel = 12;
    player.vel.x += (wish.x - player.vel.x) * Math.min(1, accel * dt);
    player.vel.z += (wish.z - player.vel.z) * Math.min(1, accel * dt);
    let vy = 0;
    if (keys['Space']) vy += speed * 0.8;
    if (sneaking)      vy -= speed * 0.8;
    player.vel.y += (vy - player.vel.y) * Math.min(1, accel * dt);
    player.onGround = false;
  } else {
    const accel = player.onGround ? 16 : 4;
    player.vel.x += (wish.x - player.vel.x) * Math.min(1, accel * dt);
    player.vel.z += (wish.z - player.vel.z) * Math.min(1, accel * dt);

    player.vel.y -= GRAVITY * dt;
    if (player.vel.y < -60) player.vel.y = -60;

    if (keys['Space'] && player.onGround) {
      player.vel.y = JUMP;
      player.onGround = false;
    }
  }

  player.onGround = false;
  if (moveAxis('y', player.vel.y * dt) && player.vel.y < 0) player.onGround = true;
  moveAxis('x', player.vel.x * dt);
  moveAxis('z', player.vel.z * dt);

  if (player.pos.y < -20) {           // защита от падения в пустоту
    player.pos.set(8, 60, 8);
    player.vel.set(0, 0, 0);
  }

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

/* =========================================================
   7. ТРАССИРОВКА ЛУЧА (выбор блока)
   ========================================================= */
function raycastVoxel(origin, dir, maxDist) {
  let x = Math.floor(origin.x), y = Math.floor(origin.y), z = Math.floor(origin.z);

  const stepX = dir.x > 0 ? 1 : -1;
  const stepY = dir.y > 0 ? 1 : -1;
  const stepZ = dir.z > 0 ? 1 : -1;

  const tDeltaX = dir.x !== 0 ? Math.abs(1 / dir.x) : Infinity;
  const tDeltaY = dir.y !== 0 ? Math.abs(1 / dir.y) : Infinity;
  const tDeltaZ = dir.z !== 0 ? Math.abs(1 / dir.z) : Infinity;

  let tMaxX = dir.x !== 0 ? ((dir.x > 0 ? x + 1 - origin.x : origin.x - x) / Math.abs(dir.x)) : Infinity;
  let tMaxY = dir.y !== 0 ? ((dir.y > 0 ? y + 1 - origin.y : origin.y - y) / Math.abs(dir.y)) : Infinity;
  let tMaxZ = dir.z !== 0 ? ((dir.z > 0 ? z + 1 - origin.z : origin.z - z) / Math.abs(dir.z)) : Infinity;

  let nx = 0, ny = 0, nz = 0;
  let t = 0;

  while (t <= maxDist) {
    const b = getBlock(x, y, z);
    if (b !== 0) return { x, y, z, nx, ny, nz };

    if (tMaxX < tMaxY && tMaxX < tMaxZ) {
      if (tMaxX > maxDist) break;
      x += stepX; t = tMaxX; tMaxX += tDeltaX;
      nx = -stepX; ny = 0; nz = 0;
    } else if (tMaxY < tMaxZ) {
      if (tMaxY > maxDist) break;
      y += stepY; t = tMaxY; tMaxY += tDeltaY;
      nx = 0; ny = -stepY; nz = 0;
    } else {
      if (tMaxZ > maxDist) break;
      z += stepZ; t = tMaxZ; tMaxZ += tDeltaZ;
      nx = 0; ny = 0; nz = -stepZ;
    }
  }
  return null;
}

function cameraDir() {
  return new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
}

/* ---------- Подсветка выбранного блока ---------- */
const highlightGeo = new THREE.BoxGeometry(1.002, 1.002, 1.002);
const highlightEdges = new THREE.EdgesGeometry(highlightGeo);
const highlight = new THREE.LineSegments(
  highlightEdges,
  new THREE.LineBasicMaterial({ color: 0x000000, transparent: true, opacity: 0.55, depthTest: true })
);
highlight.visible = false;
scene.add(highlight);

/* =========================================================
   8. ДЕЙСТВИЯ: ЛОМАТЬ / СТАВИТЬ
   ========================================================= */
let selectedSlot = 0;

function breakBlock() {
  const hit = raycastVoxel(camera.position, cameraDir(), 6);
  if (!hit) return;
  if (getBlock(hit.x, hit.y, hit.z) === 10) return; // бедрок неразрушим
  setBlock(hit.x, hit.y, hit.z, 0);
  rebuildAt(hit.x, hit.z);
}

function blockIntersectsPlayer(bx, by, bz) {
  const b = playerAABB(player.pos);
  return b.maxX > bx && b.minX < bx + 1 &&
         b.maxY > by && b.minY < by + 1 &&
         b.maxZ > bz && b.minZ < bz + 1;
}

function placeBlock() {
  const hit = raycastVoxel(camera.position, cameraDir(), 6);
  if (!hit) return;
  const x = hit.x + hit.nx, y = hit.y + hit.ny, z = hit.z + hit.nz;
  if (y < 1 || y >= WH - 1) return;
  if (getBlock(x, y, z) !== 0) return;
  if (blockIntersectsPlayer(x, y, z)) return;

  setBlock(x, y, z, HOTBAR[selectedSlot]);
  rebuildAt(x, z);
  rebuildAt(hit.x, hit.z);
}

/* =========================================================
   9. ИНТЕРФЕЙС
   ========================================================= */
const hotbarEl = document.getElementById('hotbar');
const blockNameEl = document.getElementById('blockname');
const debugEl = document.getElementById('debug');
const overlayEl = document.getElementById('overlay');

function tileToDataURL(tileIndex, size = 64) {
  const c = document.createElement('canvas');
  c.width = c.height = size;
  const g = c.getContext('2d');
  g.imageSmoothingEnabled = false;
  const sx = (tileIndex % COLS) * TILE;
  const sy = ((tileIndex / COLS) | 0) * TILE;
  g.drawImage(atlasCanvas, sx, sy, TILE, TILE, 0, 0, size, size);
  return c.toDataURL();
}

const slotEls = [];
for (let i = 0; i < 9; i++) {
  const div = document.createElement('div');
  div.className = 'slot' + (i === 0 ? ' sel' : '');
  const img = document.createElement('img');
  img.src = tileToDataURL(BLOCKS[HOTBAR[i]].tiles[2], 64);
  img.style.width = '40px';
  img.style.height = '40px';
  img.style.imageRendering = 'pixelated';
  img.draggable = false;
  div.appendChild(img);
  hotbarEl.appendChild(div);
  slotEls.push(div);
}

let nameTimer = null;
function selectSlot(i) {
  if (i < 0 || i > 8) return;
  selectedSlot = i;
  slotEls.forEach((el, k) => el.classList.toggle('sel', k === i));
  blockNameEl.textContent = BLOCKS[HOTBAR[i]].name;
  blockNameEl.classList.add('show');
  clearTimeout(nameTimer);
  nameTimer = setTimeout(() => blockNameEl.classList.remove('show'), 1300);
}

/* =========================================================
   10. УПРАВЛЕНИЕ
   ========================================================= */
const canvas = renderer.domElement;

canvas.addEventListener('click', () => {
  if (document.pointerLockElement !== canvas) {
    canvas.requestPointerLock();
  }
});

document.addEventListener('pointerlockchange', () => {
  const locked = document.pointerLockElement === canvas;
  overlayEl.classList.toggle('hidden', locked);
});

document.addEventListener('mousemove', (e) => {
  if (document.pointerLockElement !== canvas) return;
  const sens = 0.0022;
  yaw   -= e.movementX * sens;
  pitch -= e.movementY * sens;
  const lim = Math.PI / 2 - 0.001;
  pitch = Math.max(-lim, Math.min(lim, pitch));
});

document.addEventListener('mousedown', (e) => {
  if (document.pointerLockElement !== canvas) return;
  if (e.button === 0) breakBlock();
  if (e.button === 2) placeBlock();
});

document.addEventListener('contextmenu', (e) => e.preventDefault());

document.addEventListener('wheel', (e) => {
  if (document.pointerLockElement !== canvas) return;
  selectSlot((selectedSlot + (e.deltaY > 0 ? 1 : -1) + 9) % 9);
}, { passive: true });

addEventListener('keydown', (e) => {
  keys[e.code] = true;

  if (e.code.startsWith('Digit')) {
    const n = parseInt(e.code.slice(5), 10);
    if (n >= 1 && n <= 9) selectSlot(n - 1);
  }

  if (e.code === 'Space') {
    const now = performance.now();
    if (now - lastSpaceTap < 320) {
      player.flying = !player.flying;
      player.vel.set(0, 0, 0);
    }
    lastSpaceTap = now;
    e.preventDefault();
  }

  if (e.code === 'KeyF') {
    player.flying = !player.flying;
    player.vel.set(0, 0, 0);
  }

  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab'].includes(e.code)) e.preventDefault();
});

addEventListener('keyup', (e) => { keys[e.code] = false; });

addEventListener('blur', () => { for (const k in keys) keys[k] = false; });

/* =========================================================
   11. СПАВН
   ========================================================= */
function findSpawn() {
  genChunk(0, 0);
  const d = chunks.get(ckey(0, 0));
  for (let y = WH - 3; y > 1; y--) {
    if (d[8 + 8 * CH + y * CH * CH] !== 0) {
      return new THREE.Vector3(8.5, y + 1.05, 8.5);
    }
  }
  return new THREE.Vector3(8.5, 40, 8.5);
}
player.pos.copy(findSpawn());

/* =========================================================
   12. ГЛАВНЫЙ ЦИКЛ
   ========================================================= */
let lastTime = performance.now();
let fpsCounter = 0, fpsTime = 0, fps = 0;

function animate(now) {
  requestAnimationFrame(animate);

  let dt = (now - lastTime) / 1000;
  lastTime = now;
  if (dt > 0.1) dt = 0.1;

  updatePhysics(dt);
  updateChunks();

  /* подсветка блока под прицелом */
  const hit = raycastVoxel(camera.position, cameraDir(), 6);
  if (hit) {
    highlight.position.set(hit.x + 0.5, hit.y + 0.5, hit.z + 0.5);
    highlight.visible = true;
  } else {
    highlight.visible = false;
  }

  /* отладка */
  fpsCounter++; fpsTime += dt;
  if (fpsTime >= 0.5) { fps = Math.round(fpsCounter / fpsTime); fpsCounter = 0; fpsTime = 0; }

  const p = player.pos;
  debugEl.textContent =
    `Minecraft 1.12.2 — Web Edition\n` +
    `FPS: ${fps}\n` +
    `XYZ: ${p.x.toFixed(2)} / ${p.y.toFixed(2)} / ${p.z.toFixed(2)}\n` +
    `Чанк: ${Math.floor(p.x / CH)}, ${Math.floor(p.z / CH)}\n` +
    `Блок: ${hit ? BLOCKS[getBlock(hit.x, hit.y, hit.z)]?.name ?? '—' : '—'}\n` +
    `Режим: ${player.flying ? 'полёт' : (player.onGround ? 'земля' : 'воздух')}\n` +
    `Чанков: ${chunkMeshes.size}`;

  renderer.render(scene, camera);
}

requestAnimationFrame(animate);
</script>
</body>
</html>