🎮ArcadeLab

GTA 5: 2D Ultimate Edition

by CrystalPilot13
505 lines20.6 KB
▶ Play
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
<title>GTA 5: 2D Ultimate</title>
<style>
    * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Courier New', monospace; user-select: none; -webkit-user-select: none; }
    body { background: #000; overflow: hidden; touch-action: none; }
    canvas { display: block; width: 100vw; height: 100vh; }
    
    #ui { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; }
    
    /* Джойстик */
    .joystick-base { position: absolute; bottom: 20px; left: 20px; width: 130px; height: 130px; border-radius: 50%; background: rgba(255,255,255,0.1); border: 2px solid rgba(255,255,255,0.4); pointer-events: auto; }
    .joystick-stick { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 60px; height: 60px; border-radius: 50%; background: rgba(255,255,255,0.7); }
    
    /* Кнопки */
    .btn { position: absolute; border-radius: 50%; color: #fff; font-weight: bold; text-align: center; border: 2px solid #fff; pointer-events: auto; font-size: 14px; box-shadow: 0 0 15px rgba(0,0,0,0.7); transition: 0.1s; }
    .btn:active { transform: scale(0.9); }
    #btn-enter { bottom: 30px; right: 30px; width: 80px; height: 80px; line-height: 80px; background: rgba(39, 174, 96, 0.7); }
    #btn-shoot { bottom: 130px; right: 30px; width: 80px; height: 80px; line-height: 80px; background: rgba(231, 76, 60, 0.7); display: none; }
    #btn-switch { bottom: 30px; left: 50%; transform: translateX(-50%); width: 120px; height: 40px; line-height: 40px; background: rgba(52, 152, 219, 0.7); border-radius: 10px; font-size: 12px; }
    #btn-shop { bottom: 80px; left: 50%; transform: translateX(-50%); width: 120px; height: 40px; line-height: 40px; background: rgba(241, 196, 15, 0.7); border-radius: 10px; font-size: 12px; }
    
    /* HUD */
    #hud { position: absolute; top: 10px; left: 10px; color: #fff; text-shadow: 2px 2px 0 #000; font-size: 16px; font-weight: bold; }
    #wanted { position: absolute; top: 10px; right: 10px; color: #f00; font-size: 24px; text-shadow: 0 0 10px #f00; display: none; }
    #mission-text { position: absolute; bottom: 150px; left: 50%; transform: translateX(-50%); color: #ffd700; font-size: 16px; font-weight: bold; text-shadow: 2px 2px 0 #000; text-align: center; max-width: 80%; display: none; background: rgba(0,0,0,0.5); padding: 5px 15px; border-radius: 10px;}
    
    /* Мини-карта */
    #minimap { position: absolute; top: 10px; right: 10px; width: 120px; height: 120px; border: 2px solid rgba(255,255,255,0.5); border-radius: 50%; overflow: hidden; display: none; background: rgba(0,0,0,0.5); }
    
    /* Магазин */
    #shop-modal { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1a1a2e; border: 2px solid #f1c40f; border-radius: 15px; padding: 20px; display: none; flex-direction: column; gap: 10px; width: 90%; max-width: 400px; pointer-events: auto; z-index: 100; color: #fff; }
    #shop-modal h2 { text-align: center; color: #f1c40f; margin-bottom: 10px; }
    .shop-item { display: flex; justify-content: space-between; align-items: center; background: #2c3e50; padding: 10px; border-radius: 8px; }
    .shop-item button { background: #27ae60; color: #fff; border: none; padding: 5px 15px; border-radius: 5px; cursor: pointer; font-weight: bold; }
    .shop-item button:disabled { background: #555; color: #888; }
    #close-shop { background: #e74c3c; padding: 10px; border: none; border-radius: 5px; color: #fff; font-weight: bold; margin-top: 10px; cursor: pointer; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<div id="ui">
    <div class="joystick-base" id="joy-base"><div class="joystick-stick" id="joy-stick"></div></div>
    <div class="btn" id="btn-enter">ENTER</div>
    <div class="btn" id="btn-shoot">FIRE</div>
    <div class="btn" id="btn-switch">FRANKLIN</div>
    <div class="btn" id="btn-shop">МАГАЗИН</div>
    
    <div id="hud">
        <div>💰 $<span id="money">5000</span></div>
        <div>❤️ <span id="health">100</span></div>
        <div>🛡️ <span id="armor">0</span></div>
    </div>
    <div id="wanted">★</div>
    <div id="mission-text"></div>
    <div id="minimap"><canvas id="minimapCanvas" width="120" height="120"></canvas></div>

    <div id="shop-modal">
        <h2>🛒 Магазин (24/7)</h2>
        <div class="shop-item"><div>🍔 Еда (+50 HP)</div><button onclick="buyItem('food', 50)">$50</button></div>
        <div class="shop-item"><div>🛡️ Броня (+50 Armor)</div><button onclick="buyItem('armor', 100)">$100</button></div>
        <div class="shop-item"><div>🔫 Пистолет (Урон 50)</div><button onclick="buyItem('gun', 200)">$200</button></div>
        <button id="close-shop" onclick="toggleShop()">Закрыть</button>
    </div>
</div>

<script>
// === ИНИЦИАЛИЗАЦИЯ ===
const canvas = document.getElementById('game'), ctx = canvas.getContext('2d');
const miniCanvas = document.getElementById('minimapCanvas'), miniCtx = miniCanvas.getContext('2d');
let W, H;
function resize() { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }
window.addEventListener('resize', resize); resize();

const WORLD = 5000;
let camera = { x: 0, y: 0 };

// === ПЕРСОНАЖИ ===
const characters = [
    { name: 'Franklin', color: '#2ecc71', speed: 280 },
    { name: 'Michael', color: '#3498db', speed: 250 },
    { name: 'Trevor', color: '#e67e22', speed: 300 }
];
let currentChar = 0;

// === ИГРОК ===
let player = {
    x: WORLD/2, y: WORLD/2, r: 12, angle: 0, 
    hp: 100, maxHp: 100, armor: 0, maxArmor: 100,
    money: 5000, weaponDmg: 20, inVehicle: null
};

// === МИР ===
let buildings = [];
let roads = [];
let parks = [];
let vehicles = [];
let peds = [];
let bullets = [];
let police = [];
let particles = [];
let missionMarkers = [];
let wantedLevel = 0;
let wantedTimer = 0;
let isNight = false;
let weather = 'clear'; // clear, rain

// Генерация карты
for (let i = 0; i < 15; i++) {
    let x = Math.random() * WORLD, y = Math.random() * WORLD;
    let w = 100 + Math.random() * 150, h = 100 + Math.random() * 150;
    buildings.push({ x, y, w, h, color: '#2c3e50' });
}
for (let i = 0; i < 10; i++) {
    roads.push({ x: i * 500, y: 0, w: 80, h: WORLD, v: true });
    roads.push({ x: 0, y: i * 500, w: WORLD, h: 80, v: false });
}
for (let i = 0; i < 5; i++) {
    parks.push({ x: Math.random() * WORLD, y: Math.random() * WORLD, w: 300, h: 300 });
}

// === МАШИНЫ И ПЕШЕХОДЫ ===
function spawnVehicle() {
    let x = Math.random() * WORLD, y = Math.random() * WORLD;
    vehicles.push({
        x, y, angle: Math.random() * Math.PI * 2, speed: 0, maxSpeed: 400,
        color: `hsl(${Math.random() * 360}, 80%, 50%)`, driver: null,
        w: 40, h: 20, type: 'npc'
    });
}
for (let i = 0; i < 30; i++) spawnVehicle();
for (let i = 0; i < 40; i++) {
    peds.push({
        x: Math.random() * WORLD, y: Math.random() * WORLD,
        angle: Math.random() * Math.PI * 2, speed: 50,
        color: `hsl(${Math.random() * 360}, 60%, 60%)`
    });
}

// === МИССИИ ===
let currentMission = 0;
const missions = [
    { x: WORLD/2 + 400, y: WORLD/2 - 300, type: 'steal', text: 'Угони любую машину и доедь до точки Б.' },
    { x: WORLD/2 - 500, y: WORLD/2 + 200, type: 'kill', text: 'Убей 3 бандитов (красные пешеходы).' },
    { x: WORLD/2 + 300, y: WORLD/2 + 500, type: 'deliver', text: 'Доедь до порта, чтобы завершить сделку.' }
];
missionMarkers.push({ x: missions[0].x, y: missions[0].y, type: missions[0].type });

// === УПРАВЛЕНИЕ ===
let keys = {};
window.addEventListener('keydown', e => keys[e.code] = true);
window.addEventListener('keyup', e => keys[e.code] = false);

let joy = { active: false, dx: 0, dy: 0, maxR: 50 };
const joyBase = document.getElementById('joy-base'), joyStick = document.getElementById('joy-stick');
function handleJoy(e, start = false) {
    if (start) { joy.active = true; return; }
    if (!joy.active) return;
    let t = e.touches ? e.touches[0] : e, r = joyBase.getBoundingClientRect();
    let dx = t.clientX - (r.left + r.width / 2), dy = t.clientY - (r.top + r.height / 2), d = Math.hypot(dx, dy);
    if (d > joy.maxR) { dx = dx / d * joy.maxR; dy = dy / d * joy.maxR; }
    joyStick.style.transform = `translate(calc(-50% + ${dx}px), calc(-50% + ${dy}px))`;
    joy.dx = dx / joy.maxR; joy.dy = dy / joy.maxR;
}
joyBase.addEventListener('touchstart', e => { e.preventDefault(); handleJoy(e, true); });
joyBase.addEventListener('touchmove', e => { e.preventDefault(); handleJoy(e); });
joyBase.addEventListener('touchend', () => { joy.active = false; joy.dx = 0; joy.dy = 0; joyStick.style.transform = 'translate(-50%,-50%)'; });
joyBase.addEventListener('mousedown', e => handleJoy(e, true));
window.addEventListener('mousemove', e => handleJoy(e));
window.addEventListener('mouseup', () => { joy.active = false; joy.dx = 0; joy.dy = 0; joyStick.style.transform = 'translate(-50%,-50%)'; });

let isShooting = false;
document.getElementById('btn-shoot').addEventListener('touchstart', e => { e.preventDefault(); isShooting = true; });
document.getElementById('btn-shoot').addEventListener('touchend', () => isShooting = false);
document.getElementById('btn-shoot').addEventListener('mousedown', () => isShooting = true);
document.getElementById('btn-shoot').addEventListener('mouseup', () => isShooting = false);

let pendingAction = null;
document.getElementById('btn-enter').addEventListener('touchstart', e => { e.preventDefault(); pendingAction = 'enter'; });
document.getElementById('btn-enter').addEventListener('mousedown', () => pendingAction = 'enter');
document.getElementById('btn-switch').addEventListener('click', () => {
    if (!player.inVehicle) {
        currentChar = (currentChar + 1) % 3;
        document.getElementById('btn-switch').innerText = characters[currentChar].name;
        document.getElementById('btn-switch').style.background = characters[currentChar].color;
    }
});
document.getElementById('btn-shop').addEventListener('click', () => toggleShop());

// === МАГАЗИН ===
function toggleShop() {
    let modal = document.getElementById('shop-modal');
    modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex';
}
function buyItem(type, price) {
    if (player.money >= price) {
        player.money -= price;
        if (type === 'food') player.hp = Math.min(player.maxHp, player.hp + 50);
        if (type === 'armor') player.armor = Math.min(player.maxArmor, player.armor + 50);
        if (type === 'gun') player.weaponDmg = 50;
        updateHUD();
    }
}

// === HUD ===
function updateHUD() {
    document.getElementById('money').innerText = player.money;
    document.getElementById('health').innerText = Math.floor(player.hp);
    document.getElementById('armor').innerText = Math.floor(player.armor);
}

// === ЛОГИКА ===
function update(dt) {
    let kx = (keys['KeyD'] ? 1 : 0) - (keys['KeyA'] ? 1 : 0);
    let ky = (keys['KeyS'] ? 1 : 0) - (keys['KeyW'] ? 1 : 0);
    let mx = joy.dx + kx, my = joy.dy + ky;

    if (player.inVehicle) {
        let v = player.inVehicle;
        let throttle = -my, turn = mx;
        v.speed += throttle * v.maxSpeed * 2 * dt;
        v.speed *= 0.98;
        v.speed = Math.max(-v.maxSpeed * 0.5, Math.min(v.maxSpeed, v.speed));
        v.angle += turn * 2.5 * dt * (v.speed > 0 ? 1 : -1);
        
        // Коллизии с домами
        let newX = v.x + Math.cos(v.angle) * v.speed * dt;
        let newY = v.y + Math.sin(v.angle) * v.speed * dt;
        let collide = false;
        for (let b of buildings) {
            if (newX > b.x && newX < b.x + b.w && newY > b.y && newY < b.y + b.h) { collide = true; break; }
        }
        if (!collide) { v.x = newX; v.y = newY; } else { v.speed *= -0.5; }

        player.x = v.x; player.y = v.y;
    } else {
        if (Math.abs(mx) > 0.1 || Math.abs(my) > 0.1) {
            let len = Math.hypot(mx, my);
            let newX = player.x + (mx / len) * characters[currentChar].speed * dt;
            let newY = player.y + (my / len) * characters[currentChar].speed * dt;
            let collide = false;
            for (let b of buildings) {
                if (newX > b.x && newX < b.x + b.w && newY > b.y && newY < b.y + b.h) { collide = true; break; }
            }
            if (!collide) { player.x = newX; player.y = newY; }
            player.angle = Math.atan2(my, mx);
        }
        player.x = Math.max(0, Math.min(WORLD, player.x));
        player.y = Math.max(0, Math.min(WORLD, player.y));
    }

    // Стрельба
    if (isShooting && !player.inVehicle) {
        if (Math.random() < 0.15) {
            bullets.push({
                x: player.x, y: player.y,
                vx: Math.cos(player.angle) * 1000, vy: Math.sin(player.angle) * 1000,
                life: 1, dmg: player.weaponDmg
            });
            wantedLevel = Math.min(5, wantedLevel + 1);
            wantedTimer = 10;
        }
    }

    // Пули
    for (let i = bullets.length - 1; i >= 0; i--) {
        let b = bullets[i];
        b.x += b.vx * dt; b.y += b.vy * dt; b.life -= dt * 2;
        if (b.life <= 0) { bullets.splice(i, 1); continue; }
        
        for (let j = peds.length - 1; j >= 0; j--) {
            let p = peds[j];
            if (Math.hypot(b.x - p.x, b.y - p.y) < 15) {
                peds.splice(j, 1);
                bullets.splice(i, 1);
                player.money += 10;
                updateHUD();
                break;
            }
        }
    }

    // Полиция
    if (wantedLevel > 0) {
        wantedTimer -= dt;
        if (wantedTimer <= 0) { wantedLevel--; wantedTimer = 10; }
        if (Math.random() < 0.005 && police.length < wantedLevel * 2) {
            police.push({ x: player.x + 600, y: player.y + 600, angle: 0, speed: 250 });
        }
    }

    police.forEach(p => {
        let dx = player.x - p.x, dy = player.y - p.y, d = Math.hypot(dx, dy);
        if (d > 50) {
            p.angle = Math.atan2(dy, dx);
            p.x += Math.cos(p.angle) * p.speed * dt;
            p.y += Math.sin(p.angle) * p.speed * dt;
        }
        if (Math.hypot(p.x - player.x, p.y - player.y) < 30) {
            player.hp -= 20 * dt;
        }
    });

    // Миссии
    if (missionMarkers.length > 0) {
        let m = missionMarkers[0];
        if (Math.hypot(player.x - m.x, player.y - m.y) < 50) {
            missionMarkers.shift();
            document.getElementById('mission-text').innerText = missions[currentMission].text;
            document.getElementById('mission-text').style.display = 'block';
            currentMission++;
            if (currentMission < missions.length) {
                missionMarkers.push({ x: missions[currentMission].x, y: missions[currentMission].y });
            }
        }
    }

    // Камера
    camera.x = player.x - W / 2;
    camera.y = player.y - H / 2;

    // Погода и время
    if (Math.random() < 0.001) isNight = !isNight;
    if (Math.random() < 0.0005) weather = weather === 'clear' ? 'rain' : 'clear';

    // Обновление HUD
    updateHUD();
    document.getElementById('wanted').style.display = wantedLevel > 0 ? 'block' : 'none';
    document.getElementById('wanted').innerText = '★'.repeat(wantedLevel);
}

// === РИСОВАНИЕ ===
function draw() {
    // Фон (день/ночь)
    ctx.fillStyle = isNight ? '#0a0a1a' : '#2c3e50';
    ctx.fillRect(0, 0, W, H);

    // Дороги
    ctx.fillStyle = isNight ? '#1a1a2e' : '#555';
    roads.forEach(r => {
        if (r.v) ctx.fillRect(r.x - camera.x, 0, r.w, H);
        else ctx.fillRect(0, r.y - camera.y, W, r.h);
    });

    // Парки (зеленые зоны)
    ctx.fillStyle = isNight ? '#1a3a1a' : '#27ae60';
    parks.forEach(p => {
        ctx.fillRect(p.x - camera.x, p.y - camera.y, p.w, p.h);
    });

    // Дома
    buildings.forEach(b => {
        ctx.fillStyle = isNight ? '#111122' : b.color;
        ctx.fillRect(b.x - camera.x, b.y - camera.y, b.w, b.h);
        ctx.strokeStyle = isNight ? '#222233' : '#000';
        ctx.lineWidth = 3;
        ctx.strokeRect(b.x - camera.x, b.y - camera.y, b.w, b.h);
    });

    // Маркеры миссий
    missionMarkers.forEach(m => {
        ctx.fillStyle = '#ffd700';
        ctx.beginPath();
        ctx.arc(m.x - camera.x, m.y - camera.y, 20, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = '#000';
        ctx.stroke();
    });

    // Пешеходы
    peds.forEach(p => {
        ctx.fillStyle = p.color;
        ctx.beginPath();
        ctx.arc(p.x - camera.x, p.y - camera.y, 6, 0, Math.PI * 2);
        ctx.fill();
    });

    // Машины
    vehicles.forEach(v => {
        ctx.save();
        ctx.translate(v.x - camera.x, v.y - camera.y);
        ctx.rotate(v.angle);
        ctx.fillStyle = v.color;
        ctx.fillRect(-v.w / 2, -v.h / 2, v.w, v.h);
        ctx.restore();
    });

    // Полиция
    police.forEach(p => {
        ctx.save();
        ctx.translate(p.x - camera.x, p.y - camera.y);
        ctx.rotate(p.angle);
        ctx.fillStyle = '#111';
        ctx.fillRect(-20, -10, 40, 20);
        let time = Date.now() / 200;
        ctx.fillStyle = Math.floor(time) % 2 === 0 ? '#f00' : '#00f';
        ctx.fillRect(-5, -12, 10, 4);
        ctx.restore();
    });

    // Игрок
    if (!player.inVehicle) {
        ctx.fillStyle = characters[currentChar].color;
        ctx.beginPath();
        ctx.arc(player.x - camera.x, player.y - camera.y, player.r, 0, Math.PI * 2);
        ctx.fill();
        ctx.strokeStyle = '#000';
        ctx.stroke();
        // Ствол
        ctx.strokeStyle = '#fff';
        ctx.lineWidth = 3;
        ctx.beginPath();
        ctx.moveTo(player.x - camera.x, player.y - camera.y);
        ctx.lineTo(player.x - camera.x + Math.cos(player.angle) * 20, player.y - camera.y + Math.sin(player.angle) * 20);
        ctx.stroke();
    } else {
        ctx.save();
        ctx.translate(player.x - camera.x, player.y - camera.y);
        ctx.rotate(player.inVehicle.angle);
        ctx.fillStyle = characters[currentChar].color;
        ctx.fillRect(-20, -10, 40, 20);
        ctx.restore();
    }

    // Пули
    bullets.forEach(b => {
        ctx.fillStyle = '#ff0';
        ctx.beginPath();
        ctx.arc(b.x - camera.x, b.y - camera.y, 3, 0, Math.PI * 2);
        ctx.fill();
    });

    // Дождь
    if (weather === 'rain') {
        ctx.strokeStyle = 'rgba(255,255,255,0.3)';
        ctx.lineWidth = 1;
        for (let i = 0; i < 50; i++) {
            let x = Math.random() * W, y = Math.random() * H;
            ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - 5, y + 15); ctx.stroke();
        }
    }

    // Мини-карта
    document.getElementById('minimap').style.display = 'block';
    miniCtx.fillStyle = 'rgba(0,0,0,0.8)';
    miniCtx.fillRect(0, 0, 120, 120);
    // Игрок на мини-карте
    miniCtx.fillStyle = '#fff';
    miniCtx.beginPath();
    miniCtx.arc(60, 60, 4, 0, Math.PI * 2);
    miniCtx.fill();
    // Дома на мини-карте
    miniCtx.fillStyle = 'rgba(255,255,255,0.3)';
    buildings.forEach(b => {
        miniCtx.fillRect(b.x / WORLD * 120, b.y / WORLD * 120, b.w / WORLD * 120, b.h / WORLD * 120);
    });
    // Маркеры миссий
    missionMarkers.forEach(m => {
        miniCtx.fillStyle = '#ffd700';
        miniCtx.beginPath();
        miniCtx.arc(m.x / WORLD * 120, m.y / WORLD * 120, 3, 0, Math.PI * 2);
        miniCtx.fill();
    });
}

// === ГЛАВНЫЙ ЦИКЛ ===
let lastTime = performance.now();
function loop() {
    let now = performance.now(), dt = Math.min((now - lastTime) / 1000, 0.1);
    lastTime = now;

    if (pendingAction === 'enter') {
        if (player.inVehicle) {
            player.inVehicle.driver = null;
            player.x = player.inVehicle.x + 40;
            player.y = player.inVehicle.y;
            player.inVehicle = null;
            document.getElementById('btn-shoot').style.display = 'block';
        } else {
            let closest = null, best = 100;
            vehicles.forEach(v => {
                let d = Math.hypot(v.x - player.x, v.y - player.y);
                if (d < best && !v.driver) { best = d; closest = v; }
            });
            if (closest) {
                closest.driver = player;
                closest.maxSpeed = 400;
                player.inVehicle = closest;
                document.getElementById('btn-shoot').style.display = 'none';
            }
        }
        pendingAction = null;
    }

    update(dt);
    draw();
    requestAnimationFrame(loop);
}
loop();
</script>
</body>
</html>

Game Source: GTA 5: 2D Ultimate Edition

Creator: CrystalPilot13

Libraries: none

Complexity: complex (505 lines, 20.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: gta-5-2d-ultimate-edition-crystalpilot13" to link back to the original. Then publish at arcadelab.ai/publish.