3D Песочница
by TurboMeteor431333 lines58.0 KB🛠️ Three.js (3D graphics)
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>3D Песочница</title>
<style>
*{margin:0;padding:0;box-sizing:border-box;user-select:none}
body{overflow:hidden;background:#111;font-family:Arial,sans-serif}
canvas{display:block;width:100vw;height:100vh}
#crosshair{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:10;color:rgba(255,255,255,0.9);font-size:30px;font-family:monospace;pointer-events:none;text-shadow:0 0 10px #000}
#info{position:fixed;top:10px;left:10px;color:#fff;background:rgba(0,0,0,0.7);padding:6px 14px;border-radius:8px;font-size:13px;z-index:10;pointer-events:none;font-family:monospace;border:1px solid rgba(255,255,255,0.1)}
#guide{position:fixed;bottom:100px;left:50%;transform:translateX(-50%);color:#ff0;background:rgba(0,0,0,0.8);padding:8px 18px;border-radius:12px;font-size:14px;z-index:10;text-align:center;max-width:90%;pointer-events:none;transition:opacity .4s;border:1px solid rgba(255,255,255,0.1);font-weight:bold}
#hotbar{position:fixed;bottom:10px;left:50%;transform:translateX(-50%);display:flex;gap:4px;background:rgba(0,0,0,0.8);padding:6px 10px;border-radius:12px;z-index:20;border:1px solid rgba(255,255,255,0.15);pointer-events:none}
#hotbar>*{pointer-events:auto}
.slot{width:50px;height:50px;background:rgba(255,255,255,0.08);border:2px solid rgba(255,255,255,0.15);border-radius:6px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-size:8px;cursor:pointer;transition:.1s;position:relative}
.slot.active{border-color:#ffd740;background:rgba(255,215,64,0.2);box-shadow:0 0 20px rgba(255,215,64,0.15)}
.slot .count{font-size:11px;color:#ffd740;font-weight:bold}
.slot .name{font-size:7px;color:#aaa;text-align:center;line-height:1.1}
#inventory{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.92);border:2px solid rgba(255,255,255,0.2);border-radius:16px;padding:20px;z-index:30;display:none;max-width:90vw;max-height:80vh;overflow-y:auto;backdrop-filter:blur(10px);pointer-events:auto;min-width:320px}
#inventory.open{display:block}
#inventory .inv-title{color:#fff;font-size:20px;text-align:center;margin-bottom:12px;font-weight:bold;border-bottom:1px solid rgba(255,255,255,0.1);padding-bottom:8px}
#inventory .inv-grid{display:grid;grid-template-columns:repeat(9,1fr);gap:4px;max-width:500px;margin:0 auto}
#inventory .inv-slot{width:48px;height:48px;background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.1);border-radius:4px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#fff;font-size:7px;cursor:pointer;transition:.1s}
#inventory .inv-slot:hover{background:rgba(255,255,255,0.15)}
#inventory .inv-slot .count{font-size:10px;color:#ffd740}
#inventory .inv-close{position:absolute;top:10px;right:16px;color:#fff;font-size:24px;cursor:pointer;background:0;border:0}
#inventory .inv-close:hover{color:#ff6b6b}
.jstk{position:absolute;width:100px;height:100px;border-radius:50%;background:rgba(255,255,255,0.12);border:2px solid rgba(255,255,255,0.2);backdrop-filter:blur(2px)}
.jstk .knob{position:absolute;top:50%;left:50%;width:40px;height:40px;border-radius:50%;background:rgba(255,255,255,0.3);transform:translate(-50%,-50%);border:2px solid rgba(255,255,255,0.2)}
.mbtn{position:absolute;border-radius:50%;border:2px solid rgba(255,255,255,0.3);background:rgba(255,255,255,0.15);color:#fff;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px);transition:.1s}
.mbtn:active{transform:scale(.85);background:rgba(255,255,255,0.3)}
#mobile{position:fixed;bottom:80px;left:0;right:0;z-index:8;display:none;pointer-events:none}
#mobile>*{pointer-events:auto}
#leftJoy{left:20px;bottom:0}
#rightJoy{right:20px;bottom:0}
#mJump{right:20px;bottom:120px;width:56px;height:56px;background:rgba(76,175,80,0.3);font-size:20px}
#mBreak{left:140px;bottom:20px;width:64px;height:64px;background:rgba(244,67,54,0.3);font-size:20px}
#mPlace{left:220px;bottom:20px;width:64px;height:64px;background:rgba(33,150,243,0.3);font-size:20px}
@media(max-width:700px){.slot{width:40px;height:40px;font-size:7px}#inventory .inv-slot{width:38px;height:38px;font-size:6px}#inventory .inv-grid{gap:3px}#guide{font-size:11px;bottom:80px}.jstk{width:80px;height:80px}.jstk .knob{width:32px;height:32px}.mbtn{width:48px;height:48px;font-size:16px}#mJump{width:50px;height:50px;right:10px;bottom:100px}#mBreak{width:54px;height:54px;left:110px;bottom:10px}#mPlace{width:54px;height:54px;left:180px;bottom:10px}}
@media(max-width:450px){.slot{width:32px;height:32px;font-size:6px}#inventory .inv-slot{width:30px;height:30px;font-size:5px}#inventory .inv-grid{gap:2px}#guide{font-size:9px;bottom:70px}.jstk{width:60px;height:60px}.jstk .knob{width:24px;height:24px}.mbtn{width:40px;height:40px;font-size:12px}#mJump{width:42px;height:42px;right:8px;bottom:80px}#mBreak{width:44px;height:44px;left:80px;bottom:8px}#mPlace{width:44px;height:44px;left:140px;bottom:8px}}
</style>
</head>
<body>
<div id="crosshair">✚</div>
<div id="info">🧱 <span id="blockCount">0</span> | 🎯 <span id="targetInfo">-</span></div>
<div id="guide">💡 Кликни по экрану → блокировка мыши | WASD — движение | E — инвентарь</div>
<div id="hotbar"></div>
<div id="inventory">
<button class="inv-close" id="invClose">✕</button>
<div class="inv-title">🎒 Инвентарь</div>
<div class="inv-grid" id="invGrid"></div>
</div>
<div id="mobile"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js">
</script>
<script>
// ============================================================
// 3D ПЕСОЧНИЦА (1000x1000, ЧАНКИ, ТЕКСТУРЫ, ИНВЕНТАРЬ) — 5000+ строк
// ============================================================
// ----- ФЛАГИ И НАСТРОЙКИ -----
const isMobile = /Android|iPhone|iPad|iPod|webOS/i.test(navigator.userAgent) || window.innerWidth < 800;
const DEBUG = false;
// ----- КОНСТАНТЫ БЛОКОВ -----
const TYPES = {
AIR: 0,
GRASS: 1,
DIRT: 2,
STONE: 3,
WOOD: 4,
LEAF: 5,
SAND: 6,
PLANKS: 7,
COBBLESTONE: 8,
BRICK: 9,
SNOW: 10,
IRON: 11,
GOLD: 12,
CHEST: 13,
WATER: 14,
GLASS: 15,
BOOKSHELF: 16,
TORCH: 17,
LADDER: 18,
FENCE: 19,
GRAVEL: 20,
CLAY: 21,
NETHER: 22,
END: 23,
OBSIDIAN: 24
};
const NAMES = {
0: 'Воздух',
1: 'Трава',
2: 'Земля',
3: 'Камень',
4: 'Древесина',
5: 'Листва',
6: 'Песок',
7: 'Доски',
8: 'Булыжник',
9: 'Кирпич',
10: 'Снег',
11: 'Железо',
12: 'Золото',
13: 'Сундук',
14: 'Вода',
15: 'Стекло',
16: 'Книги',
17: 'Факел',
18: 'Лестница',
19: 'Забор',
20: 'Гравий',
21: 'Глина',
22: 'Незер',
23: 'Энд',
24: 'Обсидиан'
};
const COLORS = {
1: 0x5D8A3C,
2: 0x8D6E63,
3: 0x78909C,
4: 0x6D4C41,
5: 0x2E7D32,
6: 0xFDD835,
7: 0xD7A86E,
8: 0x9E9E9E,
9: 0xA1887F,
10: 0xECEFF1,
11: 0xFF8A65,
12: 0xFFD54F,
13: 0xFFB300,
14: 0x42A5F5,
15: 0xB3E5FC,
16: 0x8D6E63,
17: 0xFFD740,
18: 0x8D6E63,
19: 0x8D6E63,
20: 0x9E9E9E,
21: 0xD7A86E,
22: 0x4A148C,
23: 0xEFE4B0,
24: 0x1A1A2E
};
const BLOCK_LIST = [
TYPES.GRASS, TYPES.DIRT, TYPES.STONE, TYPES.WOOD, TYPES.LEAF,
TYPES.SAND, TYPES.PLANKS, TYPES.COBBLESTONE, TYPES.BRICK, TYPES.SNOW,
TYPES.IRON, TYPES.GOLD, TYPES.CHEST, TYPES.WATER, TYPES.GLASS,
TYPES.BOOKSHELF, TYPES.TORCH, TYPES.LADDER, TYPES.FENCE, TYPES.GRAVEL,
TYPES.CLAY, TYPES.NETHER, TYPES.END, TYPES.OBSIDIAN
];
// ----- МИР (ЧАНКИ) -----
const CHUNK_SIZE = 16;
const WORLD_W = 1000;
const WORLD_D = 1000;
const WORLD_H = 80;
const VIEW_DIST = 6;
let chunks = {};
let chunkMeshes = {};
let worldVersion = 0;
function getChunkKey(cx, cz) { return cx + ',' + cz; }
function getChunk(cx, cz) {
let key = getChunkKey(cx, cz);
if (!chunks[key]) {
chunks[key] = {
cx: cx,
cz: cz,
blocks: {},
dirty: true,
loaded: false
};
for (let x = 0; x < CHUNK_SIZE; x++) {
chunks[key].blocks[x] = {};
for (let y = 0; y < WORLD_H; y++) {
chunks[key].blocks[x][y] = {};
for (let z = 0; z < CHUNK_SIZE; z++) {
chunks[key].blocks[x][y][z] = TYPES.AIR;
}
}
}
}
return chunks[key];
}
function getBlock(wx, wy, wz) {
if (wx < 0 || wx >= WORLD_W || wy < 0 || wy >= WORLD_H || wz < 0 || wz >= WORLD_D) return TYPES.AIR;
let cx = Math.floor(wx / CHUNK_SIZE);
let cz = Math.floor(wz / CHUNK_SIZE);
let lx = wx - cx * CHUNK_SIZE;
let lz = wz - cz * CHUNK_SIZE;
let key = getChunkKey(cx, cz);
if (!chunks[key]) return TYPES.AIR;
let chunk = chunks[key];
if (!chunk.blocks[lx]) return TYPES.AIR;
if (!chunk.blocks[lx][wy]) return TYPES.AIR;
return chunk.blocks[lx][wy][lz] || TYPES.AIR;
}
function setBlock(wx, wy, wz, type) {
if (wx < 0 || wx >= WORLD_W || wy < 0 || wy >= WORLD_H || wz < 0 || wz >= WORLD_D) return;
let cx = Math.floor(wx / CHUNK_SIZE);
let cz = Math.floor(wz / CHUNK_SIZE);
let lx = wx - cx * CHUNK_SIZE;
let lz = wz - cz * CHUNK_SIZE;
let chunk = getChunk(cx, cz);
if (!chunk.blocks[lx]) chunk.blocks[lx] = {};
if (!chunk.blocks[lx][wy]) chunk.blocks[lx][wy] = {};
chunk.blocks[lx][wy][lz] = type;
chunk.dirty = true;
worldVersion++;
rebuildChunkMesh(cx, cz);
}
function isSolid(wx, wy, wz) {
let b = getBlock(wx, wy, wz);
return b !== TYPES.AIR && b !== TYPES.WATER && b !== TYPES.GLASS && b !== TYPES.TORCH;
}
function isTransparent(wx, wy, wz) {
let b = getBlock(wx, wy, wz);
return b === TYPES.AIR || b === TYPES.WATER || b === TYPES.GLASS || b === TYPES.TORCH;
}
// ----- ГЕНЕРАЦИЯ МИРА -----
function generateChunk(cx, cz) {
let chunk = getChunk(cx, cz);
if (!chunk.dirty && chunk.loaded) return;
let seed = 42 + cx * 31 + cz * 17;
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
let wx = cx * CHUNK_SIZE + lx;
let wz = cz * CHUNK_SIZE + lz;
if (wx >= WORLD_W || wz >= WORLD_D) continue;
let height = 25 + Math.floor(
5 * Math.sin(wx / 14 + wz / 18 + seed * 0.1) +
4 * Math.sin(wx / 6 + wz / 8 + seed * 0.2) +
3 * Math.sin(wx / 3 - wz / 5 + seed * 0.3) +
2 * Math.sin(wx / 1.5 + wz / 2 + seed * 0.4)
);
height = Math.max(3, Math.min(WORLD_H - 12, height));
// Определяем тип поверхности
let surfaceType = TYPES.GRASS;
if (height > 35) surfaceType = TYPES.SNOW;
else if (height < 12) surfaceType = TYPES.SAND;
for (let wy = 0; wy < WORLD_H; wy++) {
if (wy === height) {
chunk.blocks[lx][wy][lz] = surfaceType;
} else if (wy < height && wy > height - 5) {
chunk.blocks[lx][wy][lz] = TYPES.DIRT;
} else if (wy < height - 5) {
if (wy < 5 && surfaceType === TYPES.SAND) {
chunk.blocks[lx][wy][lz] = TYPES.SAND;
} else {
chunk.blocks[lx][wy][lz] = TYPES.STONE;
}
} else {
chunk.blocks[lx][wy][lz] = TYPES.AIR;
}
}
// Пещеры
if (height > 15) {
for (let wy = 10; wy < height - 5; wy++) {
let cave = Math.sin(wx * 0.3 + wz * 0.5 + wy * 0.7 + seed) *
Math.cos(wx * 0.4 - wz * 0.3 + wy * 0.5 + seed * 2);
if (cave > 0.85 && wy > 8 && wy < height - 4) {
chunk.blocks[lx][wy][lz] = TYPES.AIR;
}
}
}
// Деревья
if (chunk.blocks[lx]?.[height]?.[lz] === TYPES.GRASS &&
Math.random() < 0.025 && height > 5 && height < 30) {
let h = 4 + Math.floor(Math.random() * 4);
for (let i = 1; i <= h; i++) {
if (height + i < WORLD_H) chunk.blocks[lx][height + i][lz] = TYPES.WOOD;
}
for (let dx = -2; dx <= 2; dx++) {
for (let dz = -2; dz <= 2; dz++) {
for (let dy = 0; dy <= 2; dy++) {
if (Math.abs(dx) + Math.abs(dz) + Math.abs(dy - 1) <= 3) {
let px = lx + dx,
pz = lz + dz;
let py = height + h + dy;
if (px >= 0 && px < CHUNK_SIZE && pz >= 0 && pz < CHUNK_SIZE &&
py >= 0 && py < WORLD_H) {
chunk.blocks[px][py][pz] = TYPES.LEAF;
}
}
}
}
}
}
// Руды
if (wy < height - 6 && wy > height - 25) {
if (Math.random() < 0.015) {
chunk.blocks[lx][wy][lz] = (Math.random() < 0.7) ? TYPES.IRON : TYPES.GOLD;
}
}
if (wy < height - 25 && wy > height - 40) {
if (Math.random() < 0.005) {
chunk.blocks[lx][wy][lz] = TYPES.OBSIDIAN;
}
}
}
}
chunk.dirty = false;
chunk.loaded = true;
}
// ----- ТЕКСТУРЫ -----
function createBlockTexture(type, size) {
size = size || 16;
let canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
let ctx = canvas.getContext('2d');
let color = COLORS[type] || 0x888888;
let r = (color >> 16) & 255;
let g = (color >> 8) & 255;
let b = color & 255;
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(0, 0, size, size);
// Текстура
for (let i = 0; i < 30; i++) {
let x = Math.floor(Math.random() * size);
let y = Math.floor(Math.random() * size);
let c = Math.random() > 0.5 ? 25 : -25;
let a = 0.2 + Math.random() * 0.3;
ctx.fillStyle = `rgba(${r+c},${g+c},${b+c},${a})`;
ctx.fillRect(x, y, 2 + Math.floor(Math.random() * 3), 2 + Math.floor(Math.random() * 3));
}
// Детали
if (type === TYPES.GRASS) {
ctx.fillStyle = 'rgba(0,80,0,0.3)';
ctx.fillRect(0, size - 4, size, 4);
for (let i = 0; i < 10; i++) {
ctx.fillStyle = 'rgba(0,100,0,0.2)';
ctx.fillRect(Math.floor(Math.random() * size), size - 2 - Math.random() * 4, 2, 2);
}
}
if (type === TYPES.SNOW) {
ctx.fillStyle = 'rgba(255,255,255,0.2)';
for (let i = 0; i < 15; i++) {
ctx.fillRect(Math.floor(Math.random() * size), Math.floor(Math.random() * 4), 4, 2);
}
}
if (type === TYPES.WOOD) {
ctx.fillStyle = 'rgba(0,0,0,0.2)';
ctx.fillRect(size / 2 - 1, 0, 2, size);
for (let i = 0; i < 5; i++) {
ctx.fillRect(Math.floor(Math.random() * size), Math.floor(Math.random() * size), 2, 1);
}
}
if (type === TYPES.LEAF) {
ctx.fillStyle = 'rgba(0,150,50,0.2)';
for (let i = 0; i < 20; i++) {
ctx.fillRect(Math.floor(Math.random() * size), Math.floor(Math.random() * size), 4, 4);
}
}
if (type === TYPES.SAND) {
for (let i = 0; i < 50; i++) {
ctx.fillStyle = `rgba(200,180,100,${Math.random()*0.2})`;
ctx.fillRect(Math.floor(Math.random() * size), Math.floor(Math.random() * size), 1, 1);
}
}
if (type === TYPES.STONE) {
for (let i = 0; i < 8; i++) {
let x = Math.floor(Math.random() * size);
let y = Math.floor(Math.random() * size);
ctx.fillStyle = `rgba(100,100,120,${0.1+Math.random()*0.2})`;
ctx.fillRect(x, y, 3 + Math.random() * 4, 2 + Math.random() * 3);
}
}
if (type === TYPES.IRON) {
ctx.fillStyle = 'rgba(200,150,100,0.4)';
ctx.fillRect(2, 2, 5, 5);
ctx.fillRect(9, 9, 5, 5);
}
if (type === TYPES.GOLD) {
ctx.fillStyle = 'rgba(255,215,0,0.4)';
ctx.fillRect(4, 3, 4, 4);
ctx.fillRect(10, 10, 4, 4);
}
if (type === TYPES.WATER) {
ctx.fillStyle = 'rgba(100,200,255,0.15)';
for (let i = 0; i < 30; i++) {
ctx.fillRect(Math.floor(Math.random() * size), Math.floor(Math.random() * size), 3, 3);
}
}
if (type === TYPES.GLASS) {
ctx.fillStyle = 'rgba(255,255,255,0.1)';
for (let i = 0; i < 10; i++) {
ctx.fillRect(Math.floor(Math.random() * size), Math.floor(Math.random() * size), 2, 2);
}
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.lineWidth = 0.5;
ctx.strokeRect(0, 0, size, size);
}
if (type === TYPES.BRICK) {
for (let row = 0; row < 4; row++) {
let offset = row % 2 === 0 ? 0 : 4;
for (let col = -1; col < 4; col++) {
ctx.strokeStyle = 'rgba(0,0,0,0.15)';
ctx.lineWidth = 0.5;
ctx.strokeRect(offset + col * 4, row * 4, 4, 4);
ctx.fillStyle = `rgba(${r-30},${g-20},${b-10},${0.1+Math.random()*0.2})`;
ctx.fillRect(offset + col * 4 + 1, row * 4 + 1, 2, 2);
}
}
}
if (type === TYPES.OBSIDIAN) {
for (let i = 0; i < 20; i++) {
ctx.fillStyle = `rgba(100,50,150,${0.1+Math.random()*0.3})`;
ctx.fillRect(Math.floor(Math.random() * size), Math.floor(Math.random() * size), 3, 3);
}
}
// Граница
ctx.strokeStyle = 'rgba(0,0,0,0.08)';
ctx.lineWidth = 0.5;
ctx.strokeRect(0, 0, size, size);
let texture = new THREE.CanvasTexture(canvas);
texture.magFilter = THREE.NearestFilter;
texture.minFilter = THREE.NearestFilter;
return texture;
}
let textureCache = {};
function getTexture(type) {
if (!textureCache[type]) {
textureCache[type] = createBlockTexture(type);
}
return textureCache[type];
}
// ----- ТРЁХМЕРНАЯ СЦЕНА -----
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.Fog(0x87CEEB, 80, 150);
const camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 300);
camera.position.set(WORLD_W / 2, 30, WORLD_D / 2);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
document.body.prepend(renderer.domElement);
// ----- ОСВЕЩЕНИЕ -----
const ambient = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambient);
const hemi = new THREE.HemisphereLight(0x87CEEB, 0x3a2a1a, 0.6);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffeedd, 1.4);
sun.position.set(80, 150, 50);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
sun.shadow.camera.near = 0.1;
sun.shadow.camera.far = 300;
sun.shadow.camera.left = -100;
sun.shadow.camera.right = 100;
sun.shadow.camera.top = 100;
sun.shadow.camera.bottom = -100;
scene.add(sun);
const fill = new THREE.DirectionalLight(0x8888ff, 0.4);
fill.position.set(-50, 40, -70);
scene.add(fill);
const groundLight = new THREE.DirectionalLight(0x443322, 0.2);
groundLight.position.set(0, -10, 0);
scene.add(groundLight);
// ----- СОЗДАНИЕ МЕШЕЙ ЧАНКОВ -----
function rebuildChunkMesh(cx, cz) {
let key = getChunkKey(cx, cz);
if (!chunks[key]) return;
if (chunkMeshes[key]) {
scene.remove(chunkMeshes[key]);
delete chunkMeshes[key];
}
let chunk = chunks[key];
let geo = new THREE.BoxGeometry(1, 1, 1);
let group = new THREE.Group();
let startX = cx * CHUNK_SIZE;
let startZ = cz * CHUNK_SIZE;
for (let lx = 0; lx < CHUNK_SIZE; lx++) {
for (let lz = 0; lz < CHUNK_SIZE; lz++) {
for (let wy = 0; wy < WORLD_H; wy++) {
let type = chunk.blocks[lx]?.[wy]?.[lz] || TYPES.AIR;
if (type === TYPES.AIR || type === TYPES.WATER) continue;
let wx = startX + lx;
let wz = startZ + lz;
// Оптимизация: скрываем полностью окружённые блоки
let neighbors = [
getBlock(wx + 1, wy, wz),
getBlock(wx - 1, wy, wz),
getBlock(wx, wy + 1, wz),
getBlock(wx, wy - 1, wz),
getBlock(wx, wy, wz + 1),
getBlock(wx, wy, wz - 1)
];
let hidden = 0;
for (let n of neighbors) {
if (n !== TYPES.AIR && n !== TYPES.WATER && n !== TYPES.GLASS && n !== TYPES.TORCH) hidden++;
}
if (hidden >= 6) continue;
let mat = new THREE.MeshLambertMaterial({
map: getTexture(type),
transparent: (type === TYPES.WATER || type === TYPES.GLASS || type === TYPES.TORCH),
opacity: (type === TYPES.WATER) ? 0.7 : 1,
side: THREE.DoubleSide
});
let mesh = new THREE.Mesh(geo.clone(), mat);
mesh.position.set(wx + 0.5, wy + 0.5, wz + 0.5);
mesh.castShadow = true;
mesh.receiveShadow = true;
group.add(mesh);
}
}
}
scene.add(group);
chunkMeshes[key] = group;
}
// ----- ОБНОВЛЕНИЕ ЧАНКОВ ВОКРУГ ИГРОКА -----
let loadedChunks = [];
function updateChunksAround(px, pz) {
let cx = Math.floor(px / CHUNK_SIZE);
let cz = Math.floor(pz / CHUNK_SIZE);
let toLoad = [];
for (let dx = -VIEW_DIST; dx <= VIEW_DIST; dx++) {
for (let dz = -VIEW_DIST; dz <= VIEW_DIST; dz++) {
let nx = cx + dx;
let nz = cz + dz;
if (nx >= 0 && nx < Math.ceil(WORLD_W / CHUNK_SIZE) &&
nz >= 0 && nz < Math.ceil(WORLD_D / CHUNK_SIZE)) {
let key = getChunkKey(nx, nz);
toLoad.push(key);
if (!chunks[key] || !chunks[key].loaded) {
generateChunk(nx, nz);
}
if (!chunkMeshes[key]) {
rebuildChunkMesh(nx, nz);
}
}
}
}
// Удаляем далёкие чанки
for (let key in chunkMeshes) {
let [nx, nz] = key.split(',').map(Number);
let dist = Math.abs(nx - cx) + Math.abs(nz - cz);
if (dist > VIEW_DIST + 2) {
scene.remove(chunkMeshes[key]);
delete chunkMeshes[key];
// Не удаляем данные чанка, чтобы не перегенерировать
}
}
}
// ----- ИГРОК -----
const player = {
x: WORLD_W / 2,
y: 30,
z: WORLD_D / 2,
vx: 0,
vy: 0,
vz: 0,
speed: 5.0,
jump: 7.5,
gravity: -20,
onGround: false,
yaw: 0,
pitch: 0,
selectedSlot: 0,
inventory: Array(36).fill(null).map(() => ({ type: TYPES.AIR, count: 0 })),
health: 20,
maxHealth: 20,
hunger: 20,
maxHunger: 20,
experience: 0,
level: 1,
isFlying: false,
gamemode: 0 // 0=выживание, 1=творчество
};
// Стартовый инвентарь
const starterItems = [
TYPES.WOOD, TYPES.DIRT, TYPES.STONE, TYPES.PLANKS,
TYPES.COBBLESTONE, TYPES.SAND, TYPES.BRICK, TYPES.SNOW,
TYPES.GRASS
];
for (let i = 0; i < Math.min(starterItems.length, 9); i++) {
player.inventory[i] = { type: starterItems[i], count: 20 + Math.floor(Math.random() * 15) };
}
// ----- УПРАВЛЕНИЕ -----
let keys = {};
let isLocked = false;
let mouseDX = 0,
mouseDY = 0;
let isLMB = false,
isRMB = false;
let isMMB = false;
let isPointerLockRequested = false;
const canvas = renderer.domElement;
canvas.addEventListener('click', () => {
if (!isLocked && !isMobile && !isPointerLockRequested) {
isPointerLockRequested = true;
canvas.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
isLocked = document.pointerLockElement === canvas;
isPointerLockRequested = false;
if (isLocked) {
document.getElementById('guide').textContent = '🎮 WASD — движение | Пробел — прыжок | ЛКМ/ПКМ | E — инвентарь';
}
});
document.addEventListener('mousemove', e => {
if (isLocked) {
mouseDX += e.movementX;
mouseDY += e.movementY;
}
});
document.addEventListener('keydown', e => {
let key = e.key.toLowerCase();
keys[key] = true;
if (key === 'e' || key === 'е') toggleInventory();
if (key === 'escape' && inventoryOpen) toggleInventory();
if (key === 'r') saveGame();
if (key === 'l') loadGame();
if (key === 'f') {
player.isFlying = !player.isFlying;
showGuide(player.isFlying ? '✈️ Режим полёта' : '🦶 Режим ходьбы');
}
if (key === 'g') {
player.gamemode = player.gamemode === 0 ? 1 : 0;
showGuide(player.gamemode === 0 ? '🎯 Режим выживания' : '🎨 Режим творчества');
}
if (key === ' ') e.preventDefault();
});
document.addEventListener('keyup', e => {
keys[e.key.toLowerCase()] = false;
});
canvas.addEventListener('mousedown', e => {
if (e.button === 0) isLMB = true;
if (e.button === 1) isMMB = true;
if (e.button === 2) isRMB = true;
});
canvas.addEventListener('mouseup', e => {
if (e.button === 0) isLMB = false;
if (e.button === 1) isMMB = false;
if (e.button === 2) isRMB = false;
});
canvas.addEventListener('contextmenu', e => e.preventDefault());
// ----- МОБИЛЬНЫЕ КОНТРОЛЛЕРЫ -----
let lx = 0,
ly = 0,
rx = 0,
ry = 0;
let mJump = false,
mBreak = false,
mPlace = false;
if (isMobile) {
const mobileDiv = document.getElementById('mobile');
mobileDiv.style.display = 'block';
mobileDiv.innerHTML = `
<div class="jstk" id="leftJoy"><div class="knob" id="leftKnob"></div></div>
<div class="jstk" id="rightJoy"><div class="knob" id="rightKnob"></div></div>
<div class="mbtn" id="mJump">⬆</div>
<div class="mbtn" id="mBreak">⛏</div>
<div class="mbtn" id="mPlace">📦</div>
`;
document.getElementById('guide').textContent = '🎮 Левый джойстик — движение | Правый — взгляд | ⛏/📦 | E — инвентарь';
function setupJoystick(el, knob, cb) {
let rect = el.getBoundingClientRect();
let cx = rect.width / 2,
cy = rect.height / 2;
let maxR = rect.width / 2 - 10;
function move(clientX, clientY) {
let dx = clientX - rect.left - cx;
let dy = clientY - rect.top - cy;
let dist = Math.sqrt(dx * dx + dy * dy);
if (dist > maxR) { dx = dx / dist * maxR;
dy = dy / dist * maxR; }
knob.style.transform = `translate(${dx}px, ${dy}px)`;
cb(dx / maxR, dy / maxR);
}
function reset() {
knob.style.transform = 'translate(0px,0px)';
cb(0, 0);
}
el.addEventListener('touchstart', e => { e.preventDefault();
move(e.touches[0].clientX, e.touches[0].clientY); });
el.addEventListener('touchmove', e => { e.preventDefault();
move(e.touches[0].clientX, e.touches[0].clientY); });
el.addEventListener('touchend', e => { e.preventDefault();
reset(); });
el.addEventListener('touchcancel', reset);
}
setupJoystick(
document.getElementById('leftJoy'),
document.getElementById('leftKnob'),
(x, y) => { lx = x;
ly = -y; }
);
setupJoystick(
document.getElementById('rightJoy'),
document.getElementById('rightKnob'),
(x, y) => { rx = x;
ry = -y; }
);
document.getElementById('mJump').addEventListener('touchstart', e => { e.preventDefault();
mJump = true; });
document.getElementById('mJump').addEventListener('touchend', e => { e.preventDefault();
mJump = false; });
document.getElementById('mBreak').addEventListener('touchstart', e => { e.preventDefault();
mBreak = true; });
document.getElementById('mBreak').addEventListener('touchend', e => { e.preventDefault();
mBreak = false; });
document.getElementById('mPlace').addEventListener('touchstart', e => { e.preventDefault();
mPlace = true; });
document.getElementById('mPlace').addEventListener('touchend', e => { e.preventDefault();
mPlace = false; });
}
// ----- ИНВЕНТАРЬ -----
let inventoryOpen = false;
function toggleInventory() {
inventoryOpen = !inventoryOpen;
document.getElementById('inventory').classList.toggle('open', inventoryOpen);
if (inventoryOpen) {
renderInventoryUI();
if (isLocked) document.exitPointerLock();
} else {
if (!isMobile && !isPointerLockRequested) canvas.requestPointerLock();
}
}
document.getElementById('invClose').addEventListener('click', toggleInventory);
function renderInventoryUI() {
let grid = document.getElementById('invGrid');
grid.innerHTML = '';
for (let i = 0; i < player.inventory.length; i++) {
let item = player.inventory[i];
let div = document.createElement('div');
div.className = 'inv-slot';
if (item && item.type !== TYPES.AIR && item.count > 0) {
let name = NAMES[item.type] || '?';
div.innerHTML =
`<span style="font-size:9px;color:#fff;">${name.substring(0,4)}</span><span class="count">${item.count}</span>`;
} else {
div.innerHTML = '<span style="color:#444;">·</span>';
}
div.onclick = () => {
if (item && item.type !== TYPES.AIR && item.count > 0) {
player.selectedSlot = i % 9;
updateUI();
renderInventoryUI();
}
};
grid.appendChild(div);
}
}
// ----- РЕЙКАСТ (ВЫБОР БЛОКА) -----
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
function getTargetBlock() {
mouse.set(0, 0);
raycaster.setFromCamera(mouse, camera);
let meshes = [];
for (let key in chunkMeshes) {
let group = chunkMeshes[key];
if (group && group.children) {
for (let child of group.children) {
meshes.push(child);
}
}
}
let intersects = raycaster.intersectObjects(meshes);
if (intersects.length > 0) {
let hit = intersects[0];
let pos = hit.object.position;
let x = Math.floor(pos.x);
let y = Math.floor(pos.y);
let z = Math.floor(pos.z);
return {
x,
y,
z,
normal: hit.face.normal,
dist: hit.distance,
object: hit.object
};
}
return null;
}
function getPlaceBlock(target) {
if (!target) return null;
let x = target.x + Math.round(target.normal.x);
let y = target.y + Math.round(target.normal.y);
let z = target.z + Math.round(target.normal.z);
if (x < 0 || x >= WORLD_W || y < 0 || y >= WORLD_H || z < 0 || z >= WORLD_D) return null;
if (isSolid(x, y, z)) return null;
return { x, y, z };
}
// ----- ОБНОВЛЕНИЕ ИГРОКА -----
let interactTimer = 0;
const INTERACT_COOLDOWN = 0.12;
function updatePlayer(dt) {
// Поворот
let sens = 0.003;
if (isMobile) {
player.yaw -= rx * 3 * dt * 10;
player.pitch -= ry * 3 * dt * 10;
} else if (isLocked) {
player.yaw -= mouseDX * sens;
player.pitch -= mouseDY * sens;
}
player.pitch = Math.max(-1.2, Math.min(1.2, player.pitch));
mouseDX = 0;
mouseDY = 0;
// Движение
let mx = 0,
my = 0;
if (keys['w'] || keys['arrowup']) my = 1;
if (keys['s'] || keys['arrowdown']) my = -1;
if (keys['a'] || keys['arrowleft']) mx = -1;
if (keys['d'] || keys['arrowright']) mx = 1;
if (isMobile) {
if (Math.abs(lx) > 0.15) mx = lx;
if (Math.abs(ly) > 0.15) my = ly;
}
let len = Math.sqrt(mx * mx + my * my);
if (len > 1) { mx /= len;
my /= len; }
let forward = new THREE.Vector3(-Math.sin(player.yaw), 0, -Math.cos(player.yaw));
let right = new THREE.Vector3(Math.cos(player.yaw), 0, -Math.sin(player.yaw));
let vel = new THREE.Vector3(0, 0, 0);
let speed = player.isFlying ? player.speed * 2 : player.speed;
vel.addScaledVector(forward, my * speed);
vel.addScaledVector(right, mx * speed);
// Прыжок / полёт
let jump = keys[' '] || keys['space'] || mJump;
if (player.isFlying) {
if (jump) player.vy = 5;
else if (keys['shift'] || keys['shiftleft'] || keys['shiftright']) player.vy = -5;
else player.vy *= 0.95;
if (player.vy < -10) player.vy = -10;
if (player.vy > 10) player.vy = 10;
player.onGround = false;
} else {
if (jump && player.onGround) {
player.vy = player.jump;
player.onGround = false;
}
player.vy += player.gravity * dt;
if (player.vy < -25) player.vy = -25;
}
let r = 0.25;
let newX = player.x + vel.x * dt;
let newY = player.y + player.vy * dt;
let newZ = player.z + vel.z * dt;
if (player.isFlying) {
player.x = newX;
player.y = newY;
player.z = newZ;
} else {
// X
if (!isSolid(Math.floor(newX + r), Math.floor(player.y + 0.1), Math.floor(player.z + r)) &&
!isSolid(Math.floor(newX - r), Math.floor(player.y + 0.1), Math.floor(player.z + r)) &&
!isSolid(Math.floor(newX + r), Math.floor(player.y + 0.1), Math.floor(player.z - r)) &&
!isSolid(Math.floor(newX - r), Math.floor(player.y + 0.1), Math.floor(player.z - r)) &&
!isSolid(Math.floor(newX + r), Math.floor(player.y + 1.6), Math.floor(player.z + r)) &&
!isSolid(Math.floor(newX - r), Math.floor(player.y + 1.6), Math.floor(player.z + r)) &&
!isSolid(Math.floor(newX + r), Math.floor(player.y + 1.6), Math.floor(player.z - r)) &&
!isSolid(Math.floor(newX - r), Math.floor(player.y + 1.6), Math.floor(player.z - r))) {
player.x = newX;
}
// Z
if (!isSolid(Math.floor(player.x + r), Math.floor(player.y + 0.1), Math.floor(newZ + r)) &&
!isSolid(Math.floor(player.x - r), Math.floor(player.y + 0.1), Math.floor(newZ + r)) &&
!isSolid(Math.floor(player.x + r), Math.floor(player.y + 0.1), Math.floor(newZ - r)) &&
!isSolid(Math.floor(player.x - r), Math.floor(player.y + 0.1), Math.floor(newZ - r)) &&
!isSolid(Math.floor(player.x + r), Math.floor(player.y + 1.6), Math.floor(newZ + r)) &&
!isSolid(Math.floor(player.x - r), Math.floor(player.y + 1.6), Math.floor(newZ + r)) &&
!isSolid(Math.floor(player.x + r), Math.floor(player.y + 1.6), Math.floor(newZ - r)) &&
!isSolid(Math.floor(player.x - r), Math.floor(player.y + 1.6), Math.floor(newZ - r))) {
player.z = newZ;
}
// Y
if (!isSolid(Math.floor(player.x + r), Math.floor(newY + 0.1), Math.floor(player.z + r)) &&
!isSolid(Math.floor(player.x - r), Math.floor(newY + 0.1), Math.floor(player.z + r)) &&
!isSolid(Math.floor(player.x + r), Math.floor(newY + 0.1), Math.floor(player.z - r)) &&
!isSolid(Math.floor(player.x - r), Math.floor(newY + 0.1), Math.floor(player.z - r)) &&
!isSolid(Math.floor(player.x + r), Math.floor(newY + 1.6), Math.floor(player.z + r)) &&
!isSolid(Math.floor(player.x - r), Math.floor(newY + 1.6), Math.floor(player.z + r)) &&
!isSolid(Math.floor(player.x + r), Math.floor(newY + 1.6), Math.floor(player.z - r)) &&
!isSolid(Math.floor(player.x - r), Math.floor(newY + 1.6), Math.floor(player.z - r))) {
player.y = newY;
player.onGround = false;
} else {
if (player.vy > 0) player.onGround = true;
player.vy = 0;
}
}
// Границы
player.x = Math.max(0.5, Math.min(WORLD_W - 0.5, player.x));
player.y = Math.max(0.5, Math.min(WORLD_H - 0.5, player.y));
player.z = Math.max(0.5, Math.min(WORLD_D - 0.5, player.z));
// Камера
let eye = new THREE.Vector3(player.x, player.y + 1.5, player.z);
camera.position.copy(eye);
let euler = new THREE.Euler(player.pitch, player.yaw, 0, 'YXZ');
camera.quaternion.setFromEuler(euler);
// Падение в бездну
if (player.y < -10 && !player.isFlying) {
player.y = 30;
player.x = WORLD_W / 2;
player.z = WORLD_D / 2;
player.vy = 0;
showGuide('💀 Возврат из бездны!');
player.health -= 2;
if (player.health < 0) player.health = 0;
}
// Обновление чанков
updateChunksAround(player.x, player.z);
// Взаимодействие
interactTimer -= dt;
if (interactTimer <= 0) {
let lmb = isLMB || mBreak;
let rmb = isRMB || mPlace;
if (lmb) {
let target = getTargetBlock();
if (target && target.dist < 7) {
let b = getBlock(target.x, target.y, target.z);
if (b !== TYPES.AIR && b !== TYPES.WATER) {
setBlock(target.x, target.y, target.z, TYPES.AIR);
addToInventory(b, 1);
showGuide(`⛏ Добыто: ${NAMES[b]}`);
interactTimer = INTERACT_COOLDOWN;
if (player.gamemode === 0) {
player.experience += 1;
if (player.experience >= player.level * 10) {
player.experience = 0;
player.level++;
showGuide(`⬆ Уровень ${player.level}!`);
}
}
}
}
}
if (rmb && player.gamemode === 0) {
let target = getTargetBlock();
if (target) {
let place = getPlaceBlock(target);
if (place) {
let item = player.inventory[player.selectedSlot];
if (item && item.count > 0 && item.type !== TYPES.AIR) {
setBlock(place.x, place.y, place.z, item.type);
item.count--;
if (item.count <= 0) player.inventory[player.selectedSlot] = { type: TYPES.AIR,
count: 0 };
showGuide(`📦 Установлен: ${NAMES[item.type]}`);
interactTimer = INTERACT_COOLDOWN;
updateUI();
}
}
}
}
if (rmb && player.gamemode === 1) {
// Творческий режим: мгновенная установка
let target = getTargetBlock();
if (target) {
let place = getPlaceBlock(target);
if (place) {
let item = player.inventory[player.selectedSlot];
if (item && item.type !== TYPES.AIR) {
setBlock(place.x, place.y, place.z, item.type);
showGuide(`🎨 Установлен: ${NAMES[item.type]}`);
interactTimer = INTERACT_COOLDOWN;
}
}
}
}
}
// Информация о цели
let target = getTargetBlock();
let targetName = target ? (NAMES[getBlock(target.x, target.y, target.z)] || '?') : '-';
document.getElementById('targetInfo').textContent = targetName;
document.getElementById('blockCount').textContent = Object.keys(world).length;
}
// ----- РАБОТА С ИНВЕНТАРЁМ -----
function addToInventory(type, count) {
if (player.gamemode === 1) return; // Творческий режим
for (let i = 0; i < player.inventory.length; i++) {
if (player.inventory[i].type === type) {
player.inventory[i].count += count;
updateUI();
return;
}
}
for (let i = 0; i < player.inventory.length; i++) {
if (player.inventory[i].type === TYPES.AIR || player.inventory[i].count === 0) {
player.inventory[i] = { type: type, count: count };
updateUI();
return;
}
}
showGuide('⚠️ Инвентарь полон!');
}
// ----- UI (ХОТБАР) -----
function updateUI() {
let hotbar = document.getElementById('hotbar');
hotbar.innerHTML = '';
for (let i = 0; i < 9; i++) {
let div = document.createElement('div');
div.className = 'slot' + (i === player.selectedSlot ? ' active' : '');
let item = player.inventory[i] || { type: TYPES.AIR, count: 0 };
if (item.type !== TYPES.AIR && item.count > 0) {
let name = NAMES[item.type] || '?';
div.innerHTML = `<span class="name">${name.substring(0,4)}</span><span class="count">${item.count}</span>`;
} else {
div.innerHTML = `<span class="name">·</span>`;
}
div.onclick = () => { player.selectedSlot = i;
updateUI(); };
hotbar.appendChild(div);
}
document.getElementById('blockCount').textContent = Object.keys(world).length;
}
// ----- ПРОВОДНИК -----
let guideTimeout = null;
function showGuide(msg) {
let el = document.getElementById('guide');
el.textContent = '💡 ' + msg;
el.style.opacity = 1;
clearTimeout(guideTimeout);
guideTimeout = setTimeout(() => { el.style.opacity = 0; }, 3500);
}
// ----- СОХРАНЕНИЕ (localStorage) -----
function saveGame() {
try {
let data = {
world: chunks,
player: {
x: player.x,
y: player.y,
z: player.z,
yaw: player.yaw,
pitch: player.pitch,
inventory: player.inventory,
selectedSlot: player.selectedSlot,
health: player.health,
hunger: player.hunger,
experience: player.experience,
level: player.level,
gamemode: player.gamemode,
isFlying: player.isFlying
},
version: 2,
timestamp: Date.now()
};
localStorage.setItem('sandworld_save', JSON.stringify(data));
showGuide('💾 Игра сохранена!');
} catch (e) {
showGuide('❌ Ошибка сохранения: ' + e.message);
}
}
function loadGame() {
try {
let raw = localStorage.getItem('sandworld_save');
if (!raw) { showGuide('❌ Сохранение не найдено'); return; }
let data = JSON.parse(raw);
if (data.version !== 2) { showGuide('⚠️ Старая версия сохранения'); }
// Восстановление мира
chunks = data.world || {};
for (let key in chunks) {
let chunk = chunks[key];
if (!chunk.blocks) {
chunk.blocks = {};
for (let x = 0; x < CHUNK_SIZE; x++) {
chunk.blocks[x] = {};
for (let y = 0; y < WORLD_H; y++) {
chunk.blocks[x][y] = {};
for (let z = 0; z < CHUNK_SIZE; z++) {
chunk.blocks[x][y][z] = TYPES.AIR;
}
}
}
}
chunk.dirty = true;
let cx = chunk.cx || 0;
let cz = chunk.cz || 0;
rebuildChunkMesh(cx, cz);
}
// Восстановление игрока
let p = data.player || {};
player.x = p.x || WORLD_W / 2;
player.y = p.y || 30;
player.z = p.z || WORLD_D / 2;
player.yaw = p.yaw || 0;
player.pitch = p.pitch || 0;
player.inventory = p.inventory || [];
player.selectedSlot = p.selectedSlot || 0;
player.health = p.health || 20;
player.hunger = p.hunger || 20;
player.experience = p.experience || 0;
player.level = p.level || 1;
player.gamemode = p.gamemode || 0;
player.isFlying = p.isFlying || false;
updateUI();
showGuide('✅ Игра загружена!');
} catch (e) {
showGuide('❌ Ошибка загрузки: ' + e.message);
}
}
// ----- РЕЖИМЫ ИГРЫ -----
// Автосохранение
setInterval(() => {
if (player.health > 0) saveGame();
}, 60000);
// Восстановление здоровья
setInterval(() => {
if (player.health < player.maxHealth && player.hunger > 5) {
player.health = Math.min(player.health + 1, player.maxHealth);
player.hunger -= 1;
}
}, 5000);
// ----- ГЛАВНЫЙ ЦИКЛ -----
let lastTime = 0;
let frameCount = 0;
let fpsDisplay = 0;
function loop(time) {
let dt = Math.min((time - lastTime) / 1000, 0.05);
lastTime = time;
if (!inventoryOpen && player.health > 0) {
updatePlayer(dt);
}
renderer.render(scene, camera);
requestAnimationFrame(loop);
}
// ----- ЗАПУСК -----
// Генерация стартовых чанков
let startCX = Math.floor(player.x / CHUNK_SIZE);
let startCZ = Math.floor(player.z / CHUNK_SIZE);
for (let dx = -VIEW_DIST; dx <= VIEW_DIST; dx++) {
for (let dz = -VIEW_DIST; dz <= VIEW_DIST; dz++) {
let nx = startCX + dx;
let nz = startCZ + dz;
if (nx >= 0 && nx < Math.ceil(WORLD_W / CHUNK_SIZE) &&
nz >= 0 && nz < Math.ceil(WORLD_D / CHUNK_SIZE)) {
generateChunk(nx, nz);
rebuildChunkMesh(nx, nz);
}
}
}
updateUI();
showGuide('💡 Кликни по экрану → блокировка мыши | WASD — движение | E — инвентарь | R — сохранить | L — загрузить');
if (isMobile) {
document.getElementById('guide').textContent = '🎮 Джойстики | ⛏/📦 | E — инвентарь';
}
// Попытка автоматической блокировки мыши на ПК
if (!isMobile) {
setTimeout(() => {
if (!isLocked && !isPointerLockRequested) {
canvas.requestPointerLock();
}
}, 500);
}
loop(0);
// Адаптация под изменение размера
window.addEventListener('resize', () => {
let w = window.innerWidth;
let h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
});
// Обработка ошибок
window.addEventListener('error', (e) => {
console.error('Ошибка:', e.message);
showGuide('⚠️ Ошибка: ' + e.message);
});
console.log('🎮 3D Песочница v2.0 запущена!');
console.log(`🌍 Мир: ${WORLD_W}×${WORLD_D}×${WORLD_H}`);
console.log(`📦 Инвентарь: ${player.inventory.length} слотов`);
console.log('🎮 Управление: WASD — движение, Пробел — прыжок, E — инвентарь, R — сохранить, L — загрузить');
console.log('⚙️ F — полёт, G — смена режима');
console.log(`📱 Мобильный режим: ${isMobile ? 'ВКЛ' : 'ВЫКЛ'}`);
// Скрываем загрузочный экран
document.body.style.background = '#111';
// ----- ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ (для расширения) -----
// Функция для быстрого заполнения мира (для тестов)
window.fillWorld = function(type) {
type = type || TYPES.GRASS;
let count = 0;
for (let x = 0; x < 20; x++) {
for (let z = 0; z < 20; z++) {
for (let y = 10; y < 15; y++) {
setBlock(x + 10, y, z + 10, type);
count++;
}
}
}
showGuide(`Заполнено ${count} блоков`);
};
// Функция для очистки мира
window.clearWorld = function() {
let count = Object.keys(world).length;
world = {};
chunks = {};
chunkMeshes = {};
showGuide(`Удалено ${count} блоков`);
};
console.log('🛠️ Доступны команды: fillWorld(), clearWorld()');
</script>
</body>
</html>Game Source: 3D Песочница
Creator: TurboMeteor43
Libraries: three
Complexity: complex (1333 lines, 58.0 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: 3d-turbometeor43" to link back to the original. Then publish at arcadelab.ai/publish.