Загрузка данных
<!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> — движение • <b>Пробел</b> — прыжок<br>
<b>Shift</b> — крадучись • <b>Ctrl</b> — бег<br>
<b>ЛКМ</b> — сломать блок • <b>ПКМ</b> — поставить блок<br>
<b>1–9 / колесо</b> — выбор блока • <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>