🎮ArcadeLab

3D Песочница

by TurboMeteor43
1333 lines58.0 KB🛠️ Three.js (3D graphics)
▶ Play
<!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.