🎮ArcadeLab

Sandbox World

by TurboMeteor43
729 lines24.6 KB
▶ 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>Sandbox World</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
            background: #1a1a2e;
            overflow: hidden;
            touch-action: none;
            font-family: Arial, sans-serif;
        }
        canvas {
            display: block;
            background: #87CEEB;
            width: 100vw;
            height: 100vh;
            image-rendering: pixelated;
        }
        #ui {
            position: fixed;
            bottom: 0;
            left: 0;
            right: 0;
            padding: 8px;
            background: rgba(0,0,0,0.8);
            display: flex;
            flex-wrap: wrap;
            gap: 4px;
            justify-content: center;
            z-index: 10;
            pointer-events: none;
        }
        #ui > * { pointer-events: auto; }
        .btn {
            background: #4a4a6a;
            color: #fff;
            border: 2px solid #6a6a8a;
            border-radius: 8px;
            padding: 6px 12px;
            font-size: 12px;
            cursor: pointer;
            min-width: 44px;
            min-height: 44px;
            touch-action: manipulation;
        }
        .btn:active { background: #6a6a8a; }
        #hotbar {
            display: flex;
            gap: 2px;
            background: rgba(0,0,0,0.6);
            padding: 4px;
            border-radius: 8px;
        }
        .slot {
            width: 44px;
            height: 44px;
            background: rgba(255,255,255,0.1);
            border: 2px solid rgba(255,255,255,0.2);
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 10px;
            color: #fff;
            cursor: pointer;
            position: relative;
        }
        .slot.active { border-color: #ff0; background: rgba(255,255,0,0.2); }
        .slot .count {
            position: absolute;
            bottom: 2px;
            right: 4px;
            font-size: 9px;
            color: #fff;
        }
        #controls {
            display: flex;
            gap: 6px;
            align-items: center;
        }
        .ctrl-btn {
            background: rgba(255,255,255,0.2);
            border: 2px solid rgba(255,255,255,0.3);
            border-radius: 50%;
            width: 56px;
            height: 56px;
            color: #fff;
            font-size: 20px;
            display: flex;
            align-items: center;
            justify-content: center;
            touch-action: manipulation;
            user-select: none;
        }
        .ctrl-btn:active { background: rgba(255,255,255,0.4); }
        #info {
            position: fixed;
            top: 10px;
            left: 10px;
            color: #fff;
            font-size: 12px;
            background: rgba(0,0,0,0.6);
            padding: 6px 12px;
            border-radius: 8px;
            z-index: 10;
            pointer-events: none;
        }
        #guide {
            position: fixed;
            bottom: 100px;
            left: 50%;
            transform: translateX(-50%);
            color: #ff0;
            font-size: 14px;
            background: rgba(0,0,0,0.7);
            padding: 8px 16px;
            border-radius: 12px;
            z-index: 10;
            text-align: center;
            max-width: 90%;
            pointer-events: none;
            transition: opacity 0.3s;
        }
        @media (max-width: 600px) {
            .btn { font-size: 10px; padding: 4px 8px; min-width: 36px; min-height: 36px; }
            .slot { width: 36px; height: 36px; font-size: 8px; }
            .ctrl-btn { width: 48px; height: 48px; font-size: 16px; }
            #guide { font-size: 12px; bottom: 80px; }
        }
    </style>
</head>
<body>

<div id="info">🌍 Блоков: <span id="blockCount">0</span></div>
<div id="guide">💡 Добывай блоки (ЛКМ) | Ставь (ПКМ) | WASD/стрелки</div>

<canvas id="game"></canvas>

<div id="ui">
    <div id="hotbar"></div>
    <div id="controls">
        <div class="ctrl-btn" id="btnUp">▲</div>
        <div class="ctrl-btn" id="btnLeft">◄</div>
        <div class="ctrl-btn" id="btnDown">▼</div>
        <div class="ctrl-btn" id="btnRight">►</div>
        <div class="ctrl-btn" id="btnJump">⬆</div>
        <div class="ctrl-btn" id="btnBreak">⛏</div>
        <div class="ctrl-btn" id="btnPlace">📦</div>
    </div>
</div>

<script>
// ============================================================
// ПОЛНАЯ ИГРА SANDWORLD (Terraria/Minecraft стиль)
// ============================================================

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const blockCountEl = document.getElementById('blockCount');
const guideEl = document.getElementById('guide');

// Размеры
const TILE = 32;
const WORLD_W = 80;
const WORLD_H = 60;
const VIEW_W = Math.ceil(window.innerWidth / TILE) + 2;
const VIEW_H = Math.ceil(window.innerHeight / TILE) + 2;

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

// ============================================================
// ТИПЫ БЛОКОВ
// ============================================================
const BLOCKS = {
    AIR: 0,
    GRASS: 1,
    DIRT: 2,
    STONE: 3,
    WOOD: 4,
    LEAF: 5,
    SAND: 6,
    PLANKS: 7,
    COBBLESTONE: 8,
    GLASS: 9,
    BRICK: 10,
    IRON_ORE: 11,
    GOLD_ORE: 12,
    CHEST: 13,
    CRAFTING_TABLE: 14,
    TORCH: 15,
    WATER: 16
};

const BLOCK_NAMES = {
    0: 'Воздух', 1: 'Трава', 2: 'Земля', 3: 'Камень',
    4: 'Древесина', 5: 'Листва', 6: 'Песок', 7: 'Доски',
    8: 'Булыжник', 9: 'Стекло', 10: 'Кирпич',
    11: 'Железная руда', 12: 'Золотая руда', 13: 'Сундук',
    14: 'Верстак', 15: 'Факел', 16: 'Вода'
};

const BLOCK_COLORS = {
    0: null,
    1: '#4CAF50',
    2: '#8D6E63',
    3: '#78909C',
    4: '#6D4C41',
    5: '#2E7D32',
    6: '#FDD835',
    7: '#D7A86E',
    8: '#9E9E9E',
    9: '#B3E5FC',
    10: '#A1887F',
    11: '#FF8A65',
    12: '#FFD54F',
    13: '#FFB300',
    14: '#795548',
    15: '#FFD740',
    16: '#42A5F5'
};

// ============================================================
// МИР
// ============================================================
let world = [];
let seed = Date.now() % 10000;

function generateWorld() {
    world = [];
    for (let x = 0; x < WORLD_W; x++) {
        world[x] = [];
        let height = Math.floor(20 + 8 * Math.sin(x / 15 + seed) + 4 * Math.sin(x / 7 + seed * 2));
        let dirtDepth = 4 + Math.floor(Math.random() * 3);
        for (let y = 0; y < WORLD_H; y++) {
            if (y > height + 8) {
                world[x][y] = Math.random() < 0.3 ? BLOCKS.AIR : BLOCKS.STONE;
            } else if (y > height) {
                world[x][y] = (y < height + dirtDepth) ? BLOCKS.DIRT : BLOCKS.STONE;
            } else if (y === height) {
                world[x][y] = BLOCKS.GRASS;
            } else if (y > height - 4) {
                world[x][y] = BLOCKS.DIRT;
            } else {
                world[x][y] = BLOCKS.AIR;
            }
        }
    }
    // Деревья
    for (let x = 4; x < WORLD_W - 4; x += 3 + Math.floor(Math.random() * 4)) {
        let y = getSurfaceHeight(x);
        if (y > 0 && y < WORLD_H - 8 && Math.random() < 0.6) {
            let h = 4 + Math.floor(Math.random() * 4);
            for (let i = 0; i < h; i++) world[x][y - i] = BLOCKS.WOOD;
            for (let dx = -2; dx <= 2; dx++)
                for (let dy = -3; dy <= 1; dy++)
                    if (Math.abs(dx) + Math.abs(dy) <= 3 && x + dx >= 0 && x + dx < WORLD_W && y - h + dy >= 0)
                        world[x + dx][y - h + dy] = BLOCKS.LEAF;
        }
    }
    // Руды
    for (let i = 0; i < 30; i++) {
        let x = 3 + Math.floor(Math.random() * (WORLD_W - 6));
        let y = 3 + Math.floor(Math.random() * (getSurfaceHeight(x) + 10));
        if (world[x] && world[x][y] === BLOCKS.STONE) {
            world[x][y] = Math.random() < 0.7 ? BLOCKS.IRON_ORE : BLOCKS.GOLD_ORE;
        }
    }
}

function getSurfaceHeight(x) {
    for (let y = 0; y < WORLD_H; y++) {
        if (world[x] && (world[x][y] === BLOCKS.GRASS || world[x][y] === BLOCKS.DIRT || world[x][y] === BLOCKS.STONE))
            return y;
    }
    return WORLD_H - 5;
}

function getBlock(x, y) {
    if (x < 0 || x >= WORLD_W || y < 0 || y >= WORLD_H) return BLOCKS.AIR;
    return world[x][y] || BLOCKS.AIR;
}

function setBlock(x, y, type) {
    if (x < 0 || x >= WORLD_W || y < 0 || y >= WORLD_H) return;
    world[x][y] = type;
}

function isSolid(x, y) {
    let b = getBlock(x, y);
    return b !== BLOCKS.AIR && b !== BLOCKS.WATER;
}

// ============================================================
// ИГРОК
// ============================================================
const player = {
    x: WORLD_W / 2,
    y: 12,
    w: 0.6,
    h: 0.8,
    vx: 0,
    vy: 0,
    speed: 3.5,
    jump: 6.5,
    gravity: 0.35,
    onGround: false,
    selectedSlot: 0,
    inventory: [],
    maxInventory: 36
};

// Стартовый инвентарь
for (let i = 0; i < 9; i++) {
    let types = [BLOCKS.WOOD, BLOCKS.DIRT, BLOCKS.STONE, BLOCKS.PLANKS, BLOCKS.COBBLESTONE, BLOCKS.GRASS, BLOCKS.SAND, BLOCKS.BRICK, BLOCKS.GLASS];
    player.inventory.push({ type: types[i % types.length], count: 10 + Math.floor(Math.random() * 10) });
}

// ============================================================
// УПРАВЛЕНИЕ
// ============================================================
const keys = {};
let mouseX = 0, mouseY = 0;
let isMouseDown = false;
let isMouseDownRight = false;
let isMobile = false;

document.addEventListener('keydown', e => { keys[e.key] = true; if (e.key === 'w' || e.key === 'W') e.preventDefault(); });
document.addEventListener('keyup', e => { keys[e.key] = false; });

canvas.addEventListener('mousemove', e => {
    const rect = canvas.getBoundingClientRect();
    mouseX = (e.clientX - rect.left) / rect.width * canvas.width;
    mouseY = (e.clientY - rect.top) / rect.height * canvas.height;
});

canvas.addEventListener('mousedown', e => {
    if (e.button === 0) isMouseDown = true;
    if (e.button === 2) isMouseDownRight = true;
    e.preventDefault();
});

canvas.addEventListener('mouseup', e => {
    if (e.button === 0) isMouseDown = false;
    if (e.button === 2) isMouseDownRight = false;
});

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

// Сенсор
canvas.addEventListener('touchstart', e => {
    isMobile = true;
    const t = e.touches[0];
    const rect = canvas.getBoundingClientRect();
    mouseX = (t.clientX - rect.left) / rect.width * canvas.width;
    mouseY = (t.clientY - rect.top) / rect.height * canvas.height;
    isMouseDown = true;
    e.preventDefault();
});

canvas.addEventListener('touchmove', e => {
    const t = e.touches[0];
    const rect = canvas.getBoundingClientRect();
    mouseX = (t.clientX - rect.left) / rect.width * canvas.width;
    mouseY = (t.clientY - rect.top) / rect.height * canvas.height;
    e.preventDefault();
});

canvas.addEventListener('touchend', e => {
    isMouseDown = false;
    e.preventDefault();
});

// Мобильные кнопки
document.getElementById('btnUp').addEventListener('touchstart', () => keys['w'] = true);
document.getElementById('btnUp').addEventListener('touchend', () => keys['w'] = false);
document.getElementById('btnDown').addEventListener('touchstart', () => keys['s'] = true);
document.getElementById('btnDown').addEventListener('touchend', () => keys['s'] = false);
document.getElementById('btnLeft').addEventListener('touchstart', () => keys['a'] = true);
document.getElementById('btnLeft').addEventListener('touchend', () => keys['a'] = false);
document.getElementById('btnRight').addEventListener('touchstart', () => keys['d'] = true);
document.getElementById('btnRight').addEventListener('touchend', () => keys['d'] = false);
document.getElementById('btnJump').addEventListener('touchstart', () => keys[' '] = true);
document.getElementById('btnJump').addEventListener('touchend', () => keys[' '] = false);
document.getElementById('btnBreak').addEventListener('touchstart', () => isMouseDown = true);
document.getElementById('btnBreak').addEventListener('touchend', () => isMouseDown = false);
document.getElementById('btnPlace').addEventListener('touchstart', () => isMouseDownRight = true);
document.getElementById('btnPlace').addEventListener('touchend', () => isMouseDownRight = false);

// ============================================================
// ФИЗИКА
// ============================================================
function updatePlayer(dt) {
    let dx = 0, dy = 0;
    if (keys['a'] || keys['A'] || keys['ArrowLeft']) dx -= 1;
    if (keys['d'] || keys['D'] || keys['ArrowRight']) dx += 1;
    if (keys['w'] || keys['W'] || keys['ArrowUp']) dy -= 1;
    if (keys['s'] || keys['S'] || keys['ArrowDown']) dy += 1;

    if (dx !== 0 && dy !== 0) {
        dx *= 0.707;
        dy *= 0.707;
    }

    player.vx = dx * player.speed;

    if ((keys[' '] || keys['Space']) && player.onGround) {
        player.vy = -player.jump;
        player.onGround = false;
    }

    player.vy += player.gravity;
    if (player.vy > 10) player.vy = 10;

    // Горизонталь
    let newX = player.x + player.vx * dt;
    let testX = Math.round(newX);
    let testY = Math.round(player.y);
    if (!isSolid(testX, testY) && !isSolid(testX, testY - Math.floor(player.h))) {
        player.x = newX;
    } else {
        player.vx = 0;
    }

    // Вертикаль
    let newY = player.y + player.vy * dt;
    let testX2 = Math.round(player.x);
    let testY2 = Math.round(newY);
    let ty2 = Math.round(newY + player.h);
    if (!isSolid(testX2, testY2) && !isSolid(testX2, ty2)) {
        player.y = newY;
        player.onGround = false;
    } else {
        if (player.vy > 0) player.onGround = true;
        player.vy = 0;
    }

    // Края мира
    if (player.x < 0.5) player.x = 0.5;
    if (player.x > WORLD_W - 0.5) player.x = WORLD_W - 0.5;
    if (player.y < 0.5) { player.y = 0.5; player.vy = 0; }
    if (player.y > WORLD_H + 10) {
        player.y = getSurfaceHeight(Math.floor(player.x)) - 2;
        player.vy = 0;
        showGuide('Возврат из бездны!');
    }
}

// ============================================================
// ВЗАИМОДЕЙСТВИЕ
// ============================================================
let interactTimer = 0;
const INTERACT_COOLDOWN = 0.15;

function handleInteraction(dt) {
    interactTimer -= dt;
    if (interactTimer > 0) return;

    let worldX = Math.round(mouseX / TILE + (player.x - VIEW_W / 2));
    let worldY = Math.round(mouseY / TILE + (player.y - VIEW_H / 2));
    let dist = Math.sqrt((worldX - player.x) ** 2 + (worldY - player.y) ** 2);

    if (dist > 8) return;

    if (isMouseDown) {
        // Добыча
        let b = getBlock(worldX, worldY);
        if (b !== BLOCKS.AIR && b !== BLOCKS.WATER) {
            setBlock(worldX, worldY, BLOCKS.AIR);
            addToInventory(b, 1);
            showGuide(`Добыто: ${BLOCK_NAMES[b]}`);
            interactTimer = INTERACT_COOLDOWN;
        }
    } else if (isMouseDownRight) {
        // Установка
        let item = player.inventory[player.selectedSlot];
        if (item && item.count > 0 && item.type !== BLOCKS.AIR && item.type !== BLOCKS.WATER) {
            // Ставим рядом с целевым блоком
            let placeX = worldX, placeY = worldY;
            let dx = worldX - player.x;
            let dy = worldY - player.y;
            if (Math.abs(dx) > Math.abs(dy)) {
                placeX += (dx > 0 ? -1 : 1);
            } else {
                placeY += (dy > 0 ? -1 : 1);
            }
            if (getBlock(placeX, placeY) === BLOCKS.AIR && placeX >= 0 && placeX < WORLD_W && placeY >= 0 && placeY < WORLD_H) {
                setBlock(placeX, placeY, item.type);
                item.count--;
                if (item.count <= 0) {
                    player.inventory[player.selectedSlot] = { type: BLOCKS.AIR, count: 0 };
                }
                showGuide(`Установлен: ${BLOCK_NAMES[item.type]}`);
                interactTimer = INTERACT_COOLDOWN;
            }
        }
    }
}

function addToInventory(type, count) {
    for (let i = 0; i < player.inventory.length; i++) {
        if (player.inventory[i].type === type) {
            player.inventory[i].count += count;
            updateHotbar();
            return;
        }
    }
    for (let i = 0; i < player.inventory.length; i++) {
        if (player.inventory[i].type === BLOCKS.AIR || player.inventory[i].count === 0) {
            player.inventory[i] = { type: type, count: count };
            updateHotbar();
            return;
        }
    }
}

// ============================================================
// ОТРИСОВКА
// ============================================================
function draw() {
    ctx.fillStyle = '#87CEEB';
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Небо с градиентом
    let grad = ctx.createLinearGradient(0, 0, 0, canvas.height);
    grad.addColorStop(0, '#1a237e');
    grad.addColorStop(0.3, '#42A5F5');
    grad.addColorStop(0.6, '#87CEEB');
    grad.addColorStop(1, '#B3E5FC');
    ctx.fillStyle = grad;
    ctx.fillRect(0, 0, canvas.width, canvas.height);

    // Видимые блоки
    let offsetX = player.x * TILE - canvas.width / 2;
    let offsetY = player.y * TILE - canvas.height / 2;

    let startX = Math.max(0, Math.floor(offsetX / TILE));
    let endX = Math.min(WORLD_W, Math.ceil((offsetX + canvas.width) / TILE) + 1);
    let startY = Math.max(0, Math.floor(offsetY / TILE));
    let endY = Math.min(WORLD_H, Math.ceil((offsetY + canvas.height) / TILE) + 1);

    let total = 0;
    for (let x = startX; x < endX; x++) {
        for (let y = startY; y < endY; y++) {
            let b = getBlock(x, y);
            if (b === BLOCKS.AIR) continue;
            total++;
            let sx = x * TILE - offsetX;
            let sy = y * TILE - offsetY;
            let color = BLOCK_COLORS[b] || '#888';
            ctx.fillStyle = color;
            ctx.fillRect(sx, sy, TILE, TILE);
            ctx.strokeStyle = 'rgba(0,0,0,0.15)';
            ctx.lineWidth = 0.5;
            ctx.strokeRect(sx, sy, TILE, TILE);

            // Детали
            if (b === BLOCKS.GRASS) {
                ctx.fillStyle = 'rgba(0,100,0,0.3)';
                ctx.fillRect(sx, sy + TILE - 4, TILE, 4);
            }
            if (b === BLOCKS.WOOD) {
                ctx.fillStyle = 'rgba(0,0,0,0.2)';
                ctx.fillRect(sx + TILE / 2 - 1, sy, 2, TILE);
            }
            if (b === BLOCKS.LEAF) {
                ctx.fillStyle = 'rgba(0,200,50,0.2)';
                ctx.fillRect(sx, sy, TILE, TILE);
            }
        }
    }
    blockCountEl.textContent = total;

    // Игрок
    let px = player.x * TILE - offsetX;
    let py = player.y * TILE - offsetY;
    ctx.fillStyle = '#4FC3F7';
    ctx.fillRect(px - 8, py - 16, 16, 16);
    ctx.fillStyle = '#FFD740';
    ctx.fillRect(px - 6, py - 20, 4, 6);
    ctx.fillRect(px + 2, py - 20, 4, 6);
    ctx.fillStyle = '#FF8A65';
    ctx.fillRect(px - 10, py - 4, 4, 4);
    ctx.fillRect(px + 6, py - 4, 4, 4);

    // Выделение цели
    let worldX = Math.round(mouseX / TILE + (player.x - VIEW_W / 2));
    let worldY = Math.round(mouseY / TILE + (player.y - VIEW_H / 2));
    let dist = Math.sqrt((worldX - player.x) ** 2 + (worldY - player.y) ** 2);
    if (dist <= 8) {
        let sx = worldX * TILE - offsetX;
        let sy = worldY * TILE - offsetY;
        ctx.strokeStyle = 'rgba(255,255,255,0.6)';
        ctx.lineWidth = 2;
        ctx.strokeRect(sx, sy, TILE, TILE);
        ctx.fillStyle = 'rgba(255,255,255,0.1)';
        ctx.fillRect(sx, sy, TILE, TILE);
    }

    // Квесты (просто для красоты)
    ctx.fillStyle = 'rgba(0,0,0,0.5)';
    ctx.fillRect(10, 30, 160, 50);
    ctx.fillStyle = '#FFD740';
    ctx.font = '12px Arial';
    ctx.fillText('🎯 Квесты:', 16, 46);
    ctx.fillStyle = '#fff';
    ctx.font = '10px Arial';
    ctx.fillText('• Собери 10 дерева', 16, 62);
    ctx.fillText('• Добудь 20 камня', 16, 76);

    // Инвентарь на экране
    let invY = canvas.height - 60;
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    ctx.fillRect(canvas.width / 2 - 160, invY, 320, 50);
    for (let i = 0; i < 9; i++) {
        let slotX = canvas.width / 2 - 144 + i * 34;
        let item = player.inventory[i] || { type: BLOCKS.AIR, count: 0 };
        ctx.fillStyle = (i === player.selectedSlot) ? 'rgba(255,255,0,0.3)' : 'rgba(255,255,255,0.1)';
        ctx.fillRect(slotX, invY + 4, 30, 38);
        ctx.strokeStyle = (i === player.selectedSlot) ? '#FFD740' : 'rgba(255,255,255,0.2)';
        ctx.lineWidth = 1;
        ctx.strokeRect(slotX, invY + 4, 30, 38);
        if (item.type !== BLOCKS.AIR && item.count > 0) {
            ctx.fillStyle = BLOCK_COLORS[item.type] || '#888';
            ctx.fillRect(slotX + 4, invY + 8, 22, 22);
            ctx.fillStyle = '#fff';
            ctx.font = '9px Arial';
            ctx.fillText(item.count, slotX + 18, invY + 36);
        }
    }
}

// ============================================================
// HOTBAR UI
// ============================================================
function updateHotbar() {
    const 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: BLOCKS.AIR, count: 0 };
        if (item.type !== BLOCKS.AIR && item.count > 0) {
            div.textContent = BLOCK_NAMES[item.type]?.substring(0, 2) || '??';
            let span = document.createElement('span');
            span.className = 'count';
            span.textContent = item.count;
            div.appendChild(span);
        } else {
            div.textContent = '·';
        }
        div.onclick = () => { player.selectedSlot = i; updateHotbar(); };
        hotbar.appendChild(div);
    }
}

// ============================================================
// ПРОВОДНИК (ПОДСКАЗКИ)
// ============================================================
let guideTimeout = null;

function showGuide(msg) {
    guideEl.textContent = '💡 ' + msg;
    guideEl.style.opacity = 1;
    clearTimeout(guideTimeout);
    guideTimeout = setTimeout(() => { guideEl.style.opacity = 0; }, 3000);
}

// ============================================================
// ГЛАВНЫЙ ЦИКЛ
// ============================================================
let lastTime = 0;
let blockCount = 0;

function gameLoop(timestamp) {
    let dt = Math.min((timestamp - lastTime) / 1000, 0.05);
    lastTime = timestamp;

    updatePlayer(dt);
    handleInteraction(dt);

    // Проверка квестов (простая)
    let woodCount = 0, stoneCount = 0;
    for (let item of player.inventory) {
        if (item.type === BLOCKS.WOOD) woodCount += item.count;
        if (item.type === BLOCKS.STONE) stoneCount += item.count;
    }
    if (woodCount >= 10) {
        showGuide('✅ Квест выполнен: Собрано 10 дерева!');
        // Удаляем квестовый триггер
        for (let item of player.inventory) {
            if (item.type === BLOCKS.WOOD && item.count > 10) item.count = 10;
        }
    }
    if (stoneCount >= 20) {
        showGuide('✅ Квест выполнен: Добыто 20 камня!');
        for (let item of player.inventory) {
            if (item.type === BLOCKS.STONE && item.count > 20) item.count = 20;
        }
    }

    draw();
    updateHotbar();
    requestAnimationFrame(gameLoop);
}

// ============================================================
// ЗАПУСК
// ============================================================
generateWorld();
updateHotbar();
showGuide('Добро пожаловать! Добывай блоки (ЛКМ), ставь (ПКМ)');
requestAnimationFrame(gameLoop);

// Адаптация под размер экрана
window.addEventListener('resize', () => {
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
});

console.log('🌍 SandWorld запущена!');
console.log('🎮 Управление: WASD/стрелки, ЛКМ - добыть, ПКМ - поставить');
console.log('📱 На телефоне: кнопки внизу экрана');
</script>
</body>
</html>

Game Source: Sandbox World

Creator: TurboMeteor43

Libraries: none

Complexity: complex (729 lines, 24.6 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: sandbox-world-turbometeor43" to link back to the original. Then publish at arcadelab.ai/publish.