🎮ArcadeLab

🏃 Subway Surfers - HTML

by RocketGlider81
1136 lines38.4 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>🏃 Subway Surfers - HTML</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            background: #0a0a1a;
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            font-family: 'Segoe UI', Arial, sans-serif;
            overflow: hidden;
            touch-action: none;
            user-select: none;
        }

        .game-wrapper {
            background: linear-gradient(145deg, #1a1a2e, #16213e);
            border-radius: 24px;
            padding: 16px;
            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.8), inset 0 0 40px rgba(0, 150, 255, 0.05);
            border: 1px solid rgba(255, 255, 255, 0.06);
        }

        canvas {
            display: block;
            width: 100%;
            max-width: 420px;
            height: auto;
            aspect-ratio: 9 / 16;
            border-radius: 16px;
            background: #0f0f23;
            box-shadow: inset 0 0 50px rgba(0, 0, 0, 0.6);
            touch-action: none;
            cursor: pointer;
            image-rendering: pixelated;
        }

        .controls {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-top: 14px;
            padding: 0 8px;
        }

        .score-display {
            color: #fff;
            font-size: 18px;
            font-weight: 700;
            text-shadow: 0 0 20px rgba(0, 200, 255, 0.3);
            background: rgba(0, 0, 0, 0.4);
            padding: 6px 18px;
            border-radius: 30px;
            border: 1px solid rgba(255, 255, 255, 0.08);
            backdrop-filter: blur(4px);
        }
        .score-display span {
            color: #ffd700;
        }

        .hint {
            color: rgba(255, 255, 255, 0.35);
            font-size: 12px;
            letter-spacing: 0.5px;
            background: rgba(0, 0, 0, 0.3);
            padding: 6px 14px;
            border-radius: 30px;
            border: 1px solid rgba(255, 255, 255, 0.04);
        }

        .hint i {
            font-style: normal;
            display: inline-block;
            margin: 0 4px;
        }

        @media (max-width: 480px) {
            .game-wrapper {
                padding: 10px;
                border-radius: 16px;
            }
            .score-display {
                font-size: 15px;
                padding: 4px 14px;
            }
            .hint {
                font-size: 10px;
                padding: 4px 10px;
            }
        }

        /* Mobile touch buttons overlay */
        .touch-controls {
            display: none;
            position: absolute;
            bottom: 30px;
            left: 0;
            right: 0;
            justify-content: space-between;
            padding: 0 20px;
            pointer-events: none;
        }

        .touch-btn {
            width: 64px;
            height: 64px;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.08);
            border: 2px solid rgba(255, 255, 255, 0.12);
            color: #fff;
            font-size: 28px;
            display: flex;
            align-items: center;
            justify-content: center;
            pointer-events: auto;
            backdrop-filter: blur(8px);
            transition: all 0.1s;
            box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
        }

        .touch-btn:active {
            transform: scale(0.88);
            background: rgba(255, 255, 255, 0.18);
            border-color: rgba(255, 255, 255, 0.3);
        }

        .touch-btn.left {
            margin-right: auto;
        }
        .touch-btn.right {
            margin-left: auto;
        }
        .touch-btn.jump {
            margin: 0 auto;
            width: 76px;
            height: 76px;
            font-size: 32px;
            background: rgba(0, 200, 255, 0.12);
            border-color: rgba(0, 200, 255, 0.2);
        }

        @media (pointer: coarse) {
            .touch-controls {
                display: flex;
            }
            .hint {
                display: none;
            }
        }
    </style>
</head>
<body>

    <div class="game-wrapper" style="position:relative;">
        <canvas id="gameCanvas" width="450" height="800"></canvas>

        <div class="controls">
            <div class="score-display">🏆 <span id="scoreDisplay">0</span></div>
            <div class="hint">← ↑ → &nbsp;|&nbsp; <i>🖱️ swipe</i></div>
        </div>

        <!-- Touch controls for mobile -->
        <div class="touch-controls" id="touchControls">
            <button class="touch-btn left" id="btnLeft">◀</button>
            <button class="touch-btn jump" id="btnJump">▲</button>
            <button class="touch-btn right" id="btnRight">▶</button>
        </div>
    </div>

    <script>
        // ============================================================
        //  🚇 SUBWAY SURFERS — HTML5 Canvas Edition
        //  A 3D endless runner with perspective projection.
        //  Controls: Arrow Keys / WASD / Swipe / Touch buttons
        // ============================================================

        const canvas = document.getElementById('gameCanvas');
        const ctx = canvas.getContext('2d');
        const scoreSpan = document.getElementById('scoreDisplay');

        // ---- Sizing ----
        const W = 450,
            H = 800;
        canvas.width = W;
        canvas.height = H;

        // ---- Game Constants ----
        const LANE_COUNT = 3;
        const LANE_WIDTH = 100;
        const PLAYER_WIDTH = 36;
        const PLAYER_DEPTH = 30;
        const ROAD_OFFSET_X = (W - LANE_WIDTH * LANE_COUNT) / 2;

        // ---- 3D Projection ----
        const FOV = 600;
        const CAMERA_Y = -120;
        const VANISH_Y = 80;

        // ---- Game State ----
        let player = { x: 1, z: 0, y: 0, targetX: 1, vy: 0, jumping: false, grounded: true };
        let obstacles = [];
        let coins = [];
        let score = 0;
        let highScore = parseInt(localStorage.getItem('subwayHighScore')) || 0;
        let gameOver = false;
        let gameStarted = false;
        let frameCount = 0;
        let speed = 6.5;
        let spawnTimer = 0;
        let coinTimer = 0;
        let shakeAmount = 0;
        let particles = [];

        // ---- Input ----
        const keys = { left: false, right: false, jump: false };
        let swipeStartX = 0;
        let swipeStartY = 0;

        // ---- DOM refs ----
        const btnLeft = document.getElementById('btnLeft');
        const btnRight = document.getElementById('btnRight');
        const btnJump = document.getElementById('btnJump');

        // ============================================================
        //  HELPERS
        // ============================================================

        function projectPoint(x, y, z) {
            // 3D → 2D with perspective
            const scale = FOV / (FOV + z);
            const px = W / 2 + (x - W / 2) * scale;
            const py = VANISH_Y + (y - VANISH_Y) * scale;
            return { x: px, y: py, scale: scale };
        }

        function laneToX(lane) {
            return ROAD_OFFSET_X + lane * LANE_WIDTH + LANE_WIDTH / 2;
        }

        function randomLane() {
            return Math.floor(Math.random() * LANE_COUNT);
        }

        function lerp(a, b, t) { return a + (b - a) * t; }

        function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }

        function dist3D(a, b) {
            return Math.sqrt((a.x - b.x) ** 2 + (a.z - b.z) ** 2);
        }

        // ============================================================
        //  PARTICLE SYSTEM
        // ============================================================

        function spawnParticles(x, y, z, color, count = 15) {
            for (let i = 0; i < count; i++) {
                particles.push({
                    x,
                    y,
                    z,
                    vx: (Math.random() - 0.5) * 8,
                    vy: (Math.random() - 0.5) * 8 - 2,
                    vz: (Math.random() - 0.5) * 6,
                    life: 1,
                    decay: 0.015 + Math.random() * 0.025,
                    size: 4 + Math.random() * 8,
                    color: color || '#ffd700',
                });
            }
        }

        function updateParticles() {
            for (let i = particles.length - 1; i >= 0; i--) {
                const p = particles[i];
                p.x += p.vx;
                p.y += p.vy;
                p.z += p.vz;
                p.vy += 0.15;
                p.life -= p.decay;
                if (p.life <= 0 || p.z > 300) {
                    particles.splice(i, 1);
                }
            }
        }

        function drawParticles() {
            for (const p of particles) {
                const proj = projectPoint(p.x, p.y, p.z);
                const size = p.size * proj.scale;
                const alpha = p.life * 0.9;
                ctx.globalAlpha = alpha;
                ctx.shadowColor = p.color;
                ctx.shadowBlur = 12;
                ctx.fillStyle = p.color;
                ctx.beginPath();
                ctx.arc(proj.x, proj.y, Math.max(size, 1), 0, Math.PI * 2);
                ctx.fill();
            }
            ctx.globalAlpha = 1;
            ctx.shadowBlur = 0;
        }

        // ============================================================
        //  OBSTACLES
        // ============================================================

        function spawnObstacle() {
            const lane = randomLane();
            const type = Math.random() < 0.35 ? 'train' : 'barrier';
            const x = laneToX(lane);
            obstacles.push({
                x: x,
                z: -250 - Math.random() * 100,
                lane: lane,
                type: type,
                width: type === 'train' ? 80 : 55,
                depth: type === 'train' ? 60 : 40,
                height: type === 'train' ? 70 : 45,
                color: type === 'train' ? '#e74c3c' : '#f39c12',
                hit: false,
            });
        }

        function spawnCoin() {
            const lane = randomLane();
            const x = laneToX(lane);
            coins.push({
                x: x + (Math.random() - 0.5) * 40,
                z: -200 - Math.random() * 150,
                y: -10 + Math.random() * 20,
                radius: 14,
                collected: false,
                bobPhase: Math.random() * Math.PI * 2,
            });
        }

        // ============================================================
        //  PLAYER
        // ============================================================

        function resetPlayer() {
            player.x = laneToX(1);
            player.targetX = laneToX(1);
            player.z = 0;
            player.y = 0;
            player.vy = 0;
            player.jumping = false;
            player.grounded = true;
        }

        function jumpPlayer() {
            if (player.grounded && !gameOver) {
                player.vy = -11;
                player.jumping = true;
                player.grounded = false;
                spawnParticles(player.x, 20, player.z, '#88ddff', 8);
            }
        }

        // ============================================================
        //  COLLISION
        // ============================================================

        function checkCollisions() {
            const pX = player.x;
            const pZ = player.z;
            const pY = player.y;

            // Obstacles
            for (const obs of obstacles) {
                if (obs.hit) continue;
                const halfW = obs.width / 2;
                const halfD = obs.depth / 2;
                const dx = Math.abs(pX - obs.x);
                const dz = Math.abs(pZ - obs.z);
                if (dx < halfW + PLAYER_WIDTH / 2 && dz < halfD + PLAYER_DEPTH / 2) {
                    // Check if player is above (jumping over)
                    if (pY > obs.height - 10) {
                        // Jumped over! (bonus points)
                        if (!obs.hit) {
                            obs.hit = true;
                            score += 5;
                            spawnParticles(obs.x, obs.height / 2, obs.z, '#4ecdc4', 10);
                            updateScore();
                        }
                        continue;
                    }
                    // Collision!
                    gameOver = true;
                    shakeAmount = 12;
                    spawnParticles(pX, pY, pZ, '#ff4444', 30);
                    return;
                }
            }

            // Coins
            for (const coin of coins) {
                if (coin.collected) continue;
                const dx = Math.abs(pX - coin.x);
                const dz = Math.abs(pZ - coin.z);
                const dy = Math.abs(pY - coin.y);
                if (dx < 30 && dz < 30 && dy < 35) {
                    coin.collected = true;
                    score += 10;
                    spawnParticles(coin.x, coin.y, coin.z, '#ffd700', 12);
                    updateScore();
                }
            }
        }

        // ============================================================
        //  SCORE
        // ============================================================

        function updateScore() {
            scoreSpan.textContent = Math.floor(score);
        }

        // ============================================================
        //  DRAWING
        // ============================================================

        function drawRoad() {
            // Road with perspective
            const segments = 40;
            const segLen = 18;
            const roadLeft = ROAD_OFFSET_X;
            const roadRight = ROAD_OFFSET_X + LANE_WIDTH * LANE_COUNT;

            for (let i = 0; i < segments; i++) {
                const z1 = -i * segLen;
                const z2 = -(i + 1) * segLen;

                const p1 = projectPoint(roadLeft, 0, z1);
                const p2 = projectPoint(roadRight, 0, z1);
                const p3 = projectPoint(roadRight, 0, z2);
                const p4 = projectPoint(roadLeft, 0, z2);

                const brightness = 0.25 + 0.75 * (1 - i / segments);
                const isEven = i % 2 === 0;
                ctx.fillStyle = isEven ?
                    `rgba(60, 70, 90, ${brightness * 0.5})` :
                    `rgba(80, 90, 110, ${brightness * 0.4})`;
                ctx.beginPath();
                ctx.moveTo(p1.x, p1.y);
                ctx.lineTo(p2.x, p2.y);
                ctx.lineTo(p3.x, p3.y);
                ctx.lineTo(p4.x, p4.y);
                ctx.closePath();
                ctx.fill();

                // Lane markers
                if (i % 2 === 0) {
                    for (let lane = 1; lane < LANE_COUNT; lane++) {
                        const lx = roadLeft + lane * LANE_WIDTH;
                        const lp1 = projectPoint(lx, 0, z1);
                        const lp2 = projectPoint(lx, 0, z2);
                        ctx.strokeStyle = `rgba(255,255,255,${0.08 * brightness})`;
                        ctx.lineWidth = 2 * p1.scale;
                        ctx.beginPath();
                        ctx.moveTo(lp1.x, lp1.y);
                        ctx.lineTo(lp2.x, lp2.y);
                        ctx.stroke();
                    }
                }
            }

            // Road edges glow
            const grad = ctx.createLinearGradient(0, 0, W, 0);
            grad.addColorStop(0, 'rgba(0,0,0,0.4)');
            grad.addColorStop(0.15, 'rgba(0,0,0,0)');
            grad.addColorStop(0.85, 'rgba(0,0,0,0)');
            grad.addColorStop(1, 'rgba(0,0,0,0.4)');
            ctx.fillStyle = grad;
            ctx.fillRect(0, 0, W, H);
        }

        function drawObstacle(obs) {
            const z = obs.z;
            const x = obs.x;
            const w = obs.width;
            const d = obs.depth;
            const h = obs.height;

            // Bottom
            const p1 = projectPoint(x - w / 2, 0, z - d / 2);
            const p2 = projectPoint(x + w / 2, 0, z - d / 2);
            const p3 = projectPoint(x + w / 2, 0, z + d / 2);
            const p4 = projectPoint(x - w / 2, 0, z + d / 2);

            // Top
            const p1t = projectPoint(x - w / 2, h, z - d / 2);
            const p2t = projectPoint(x + w / 2, h, z - d / 2);
            const p3t = projectPoint(x + w / 2, h, z + d / 2);
            const p4t = projectPoint(x - w / 2, h, z + d / 2);

            const color = obs.type === 'train' ? '#e74c3c' : '#f39c12';
            const darkColor = obs.type === 'train' ? '#a93226' : '#d68910';

            ctx.shadowColor = color;
            ctx.shadowBlur = 15 * Math.min(1, 1 + z / 100);

            // Draw as 3D box
            const drawFace = (pts, fill) => {
                ctx.fillStyle = fill;
                ctx.beginPath();
                ctx.moveTo(pts[0].x, pts[0].y);
                for (let i = 1; i < pts.length; i++) {
                    ctx.lineTo(pts[i].x, pts[i].y);
                }
                ctx.closePath();
                ctx.fill();
            };

            // Sides
            drawFace([p1, p2, p2t, p1t], darkColor);
            drawFace([p2, p3, p3t, p2t], color);
            drawFace([p3, p4, p4t, p3t], darkColor);
            drawFace([p4, p1, p1t, p4t], color);
            // Top
            drawFace([p1t, p2t, p3t, p4t], '#fff5e6');

            // Detail: windows for train
            if (obs.type === 'train') {
                const winColor = 'rgba(200,230,255,0.4)';
                const winW = w * 0.35;
                const winH = h * 0.25;
                const wy = h * 0.4;
                for (let side = -1; side <= 1; side += 2) {
                    const wx = x + side * w * 0.2;
                    const pWin1 = projectPoint(wx - winW / 2, wy, z - d / 2 + 2);
                    const pWin2 = projectPoint(wx + winW / 2, wy, z - d / 2 + 2);
                    const pWin3 = projectPoint(wx + winW / 2, wy + winH, z - d / 2 + 2);
                    const pWin4 = projectPoint(wx - winW / 2, wy + winH, z - d / 2 + 2);
                    drawFace([pWin1, pWin2, pWin3, pWin4], winColor);
                }
            }

            ctx.shadowBlur = 0;
        }

        function drawCoin(coin) {
            const bob = Math.sin(coin.bobPhase + frameCount * 0.04) * 5;
            const y = coin.y + bob;
            const proj = projectPoint(coin.x, y, coin.z);
            const radius = coin.radius * proj.scale;
            if (radius < 1) return;

            const alpha = 0.8 + 0.2 * Math.sin(frameCount * 0.06 + coin.bobPhase);
            ctx.shadowColor = '#ffd700';
            ctx.shadowBlur = 20 * proj.scale;

            // Glow
            const grad = ctx.createRadialGradient(proj.x, proj.y, 0, proj.x, proj.y, radius * 1.8);
            grad.addColorStop(0, `rgba(255,215,0,${alpha * 0.3})`);
            grad.addColorStop(1, 'rgba(255,215,0,0)');
            ctx.fillStyle = grad;
            ctx.beginPath();
            ctx.arc(proj.x, proj.y, radius * 1.8, 0, Math.PI * 2);
            ctx.fill();

            // Coin body
            const grad2 = ctx.createRadialGradient(
                proj.x - radius * 0.3, proj.y - radius * 0.3, 0,
                proj.x, proj.y, radius
            );
            grad2.addColorStop(0, '#ffe066');
            grad2.addColorStop(0.6, '#ffd700');
            grad2.addColorStop(1, '#b8860b');
            ctx.fillStyle = grad2;
            ctx.beginPath();
            ctx.arc(proj.x, proj.y, radius, 0, Math.PI * 2);
            ctx.fill();

            // Inner ring
            ctx.strokeStyle = `rgba(255,255,200,${alpha * 0.5})`;
            ctx.lineWidth = 2 * proj.scale;
            ctx.beginPath();
            ctx.arc(proj.x, proj.y, radius * 0.55, 0, Math.PI * 2);
            ctx.stroke();

            // Star symbol
            ctx.fillStyle = `rgba(255,255,200,${alpha * 0.7})`;
            ctx.font = `${radius * 0.7}px Arial`;
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';
            ctx.fillText('★', proj.x, proj.y + 1);

            ctx.shadowBlur = 0;
        }

        function drawPlayer() {
            const p = player;
            const proj = projectPoint(p.x, p.y, p.z);
            const scale = proj.scale;

            const w = PLAYER_WIDTH * scale;
            const h = 50 * scale;

            // Shadow
            const shadowProj = projectPoint(p.x, -2, p.z);
            ctx.shadowColor = 'rgba(0,0,0,0.3)';
            ctx.shadowBlur = 20 * scale;
            ctx.fillStyle = 'rgba(0,0,0,0.2)';
            ctx.beginPath();
            ctx.ellipse(shadowProj.x, shadowProj.y + 2, w * 0.5, w * 0.15, 0, 0, Math.PI * 2);
            ctx.fill();
            ctx.shadowBlur = 0;

            // Body
            const bodyColor = '#3498db';
            const bodyDark = '#2171a5';

            // Torso
            ctx.shadowColor = '#3498db';
            ctx.shadowBlur = 20 * scale;

            // Main body (rounded rect)
            const rx = proj.x - w / 2;
            const ry = proj.y - h;
            const rw = w;
            const rh = h * 0.6;
            const corner = 6 * scale;

            ctx.fillStyle = bodyColor;
            ctx.beginPath();
            ctx.moveTo(rx + corner, ry);
            ctx.lineTo(rx + rw - corner, ry);
            ctx.quadraticCurveTo(rx + rw, ry, rx + rw, ry + corner);
            ctx.lineTo(rx + rw, ry + rh - corner);
            ctx.quadraticCurveTo(rx + rw, ry + rh, rx + rw - corner, ry + rh);
            ctx.lineTo(rx + corner, ry + rh);
            ctx.quadraticCurveTo(rx, ry + rh, rx, ry + rh - corner);
            ctx.lineTo(rx, ry + corner);
            ctx.quadraticCurveTo(rx, ry, rx + corner, ry);
            ctx.closePath();
            ctx.fill();

            // Head
            const headR = w * 0.38;
            const headY = ry - headR * 0.5;
            ctx.fillStyle = '#f5cba7';
            ctx.beginPath();
            ctx.arc(proj.x, headY, headR, 0, Math.PI * 2);
            ctx.fill();

            // Hair / cap
            ctx.fillStyle = '#e74c3c';
            ctx.beginPath();
            ctx.ellipse(proj.x, headY - headR * 0.2, headR * 0.9, headR * 0.4, 0, 0, Math.PI * 2);
            ctx.fill();

            // Cap brim
            ctx.fillStyle = '#c0392b';
            ctx.fillRect(proj.x - headR * 0.7, headY - headR * 0.1, headR * 1.4, headR * 0.15);

            // Eyes
            ctx.fillStyle = '#2c3e50';
            const eyeOff = headR * 0.3;
            const eyeR = headR * 0.15;
            ctx.beginPath();
            ctx.arc(proj.x - eyeOff, headY - headR * 0.05, eyeR, 0, Math.PI * 2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(proj.x + eyeOff, headY - headR * 0.05, eyeR, 0, Math.PI * 2);
            ctx.fill();

            // Eye shine
            ctx.fillStyle = 'white';
            ctx.beginPath();
            ctx.arc(proj.x - eyeOff + eyeR * 0.4, headY - headR * 0.15, eyeR * 0.4, 0, Math.PI * 2);
            ctx.fill();
            ctx.beginPath();
            ctx.arc(proj.x + eyeOff + eyeR * 0.4, headY - headR * 0.15, eyeR * 0.4, 0, Math.PI * 2);
            ctx.fill();

            // Legs
            ctx.shadowBlur = 0;
            ctx.fillStyle = '#2c3e50';
            const legW = w * 0.25;
            const legH = h * 0.35;
            const legOffset = w * 0.2;
            const bob = player.jumping ? Math.sin(frameCount * 0.3) * 4 * scale : 0;
            ctx.fillRect(proj.x - legOffset - legW / 2, proj.y - legH + bob, legW, legH);
            ctx.fillRect(proj.x + legOffset - legW / 2, proj.y - legH - bob, legW, legH);

            // Shoes
            ctx.fillStyle = '#e74c3c';
            ctx.fillRect(proj.x - legOffset - legW / 2 - 2 * scale, proj.y - 2 * scale + bob, legW + 4 * scale, 4 * scale);
            ctx.fillRect(proj.x + legOffset - legW / 2 - 2 * scale, proj.y - 2 * scale - bob, legW + 4 * scale, 4 * scale);

            ctx.shadowBlur = 0;
        }

        function drawUI() {
            // Game Over overlay
            if (gameOver) {
                ctx.fillStyle = 'rgba(0,0,0,0.6)';
                ctx.fillRect(0, 0, W, H);

                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';

                ctx.shadowColor = '#ff4444';
                ctx.shadowBlur = 40;
                ctx.fillStyle = '#ff4444';
                ctx.font = 'bold 52px Arial';
                ctx.fillText('💥 GAME OVER', W / 2, H / 2 - 60);

                ctx.shadowBlur = 0;
                ctx.fillStyle = '#fff';
                ctx.font = '24px Arial';
                ctx.fillText(`Score: ${Math.floor(score)}`, W / 2, H / 2 + 20);

                if (score > highScore) {
                    highScore = Math.floor(score);
                    localStorage.setItem('subwayHighScore', highScore);
                    ctx.fillStyle = '#ffd700';
                    ctx.font = 'bold 22px Arial';
                    ctx.fillText('🏆 NEW BEST!', W / 2, H / 2 + 70);
                } else {
                    ctx.fillStyle = '#aaa';
                    ctx.font = '18px Arial';
                    ctx.fillText(`Best: ${highScore}`, W / 2, H / 2 + 70);
                }

                ctx.fillStyle = 'rgba(255,255,255,0.9)';
                ctx.font = '20px Arial';
                ctx.fillText('🔄 Tap or press Space to restart', W / 2, H / 2 + 140);

                // Glow pulse
                const pulse = 0.5 + 0.5 * Math.sin(frameCount * 0.04);
                ctx.shadowColor = `rgba(255,255,255,${pulse * 0.3})`;
                ctx.shadowBlur = 30;
                ctx.strokeStyle = `rgba(255,255,255,${pulse * 0.15})`;
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.arc(W / 2, H / 2 + 140, 60 + pulse * 10, 0, Math.PI * 2);
                ctx.stroke();
                ctx.shadowBlur = 0;

                return;
            }

            // High score display
            ctx.textAlign = 'right';
            ctx.textBaseline = 'top';
            ctx.fillStyle = 'rgba(255,215,0,0.5)';
            ctx.font = '14px Arial';
            ctx.fillText(`🏆 ${highScore}`, W - 16, 12);

            // Speed indicator
            ctx.textAlign = 'left';
            ctx.textBaseline = 'top';
            ctx.fillStyle = 'rgba(255,255,255,0.25)';
            ctx.font = '12px Arial';
            const speedKmh = Math.floor(speed * 3.5 + 20);
            ctx.fillText(`⚡ ${speedKmh} km/h`, 12, 12);
        }

        // ============================================================
        //  GAME LOOP
        // ============================================================

        function update() {
            if (gameOver) {
                updateParticles();
                if (shakeAmount > 0) shakeAmount *= 0.92;
                if (shakeAmount < 0.1) shakeAmount = 0;
                return;
            }

            frameCount++;

            // ---- Speed increase ----
            speed += 0.002;
            speed = Math.min(speed, 14);

            // ---- Player movement ----
            if (keys.left && player.targetX > laneToX(0)) {
                player.targetX -= LANE_WIDTH;
                keys.left = false;
            }
            if (keys.right && player.targetX < laneToX(LANE_COUNT - 1)) {
                player.targetX += LANE_WIDTH;
                keys.right = false;
            }

            // Smooth lane movement
            player.x = lerp(player.x, player.targetX, 0.18);

            // Jump physics
            if (player.jumping) {
                player.vy += 0.55;
                player.y += player.vy;
                if (player.y >= 0) {
                    player.y = 0;
                    player.vy = 0;
                    player.jumping = false;
                    player.grounded = true;
                    spawnParticles(player.x, 2, player.z, '#88ddff', 6);
                }
            }

            // ---- Spawn obstacles ----
            spawnTimer++;
            const spawnInterval = Math.max(20, 55 - speed * 1.5);
            if (spawnTimer > spawnInterval + Math.random() * 30) {
                spawnObstacle();
                // Sometimes spawn two at once
                if (Math.random() < 0.2 && speed > 8) {
                    spawnObstacle();
                }
                spawnTimer = 0;
            }

            // ---- Spawn coins ----
            coinTimer++;
            if (coinTimer > 12 + Math.random() * 20) {
                spawnCoin();
                if (Math.random() < 0.3) spawnCoin();
                coinTimer = 0;
            }

            // ---- Move obstacles ----
            for (let i = obstacles.length - 1; i >= 0; i--) {
                obstacles[i].z += speed;
                if (obstacles[i].z > 350) {
                    obstacles.splice(i, 1);
                }
            }

            // ---- Move coins ----
            for (let i = coins.length - 1; i >= 0; i--) {
                coins[i].z += speed;
                if (coins[i].z > 350) {
                    coins.splice(i, 1);
                }
            }

            // ---- Collisions ----
            checkCollisions();

            // ---- Score ----
            score += 0.15;
            updateScore();

            // ---- Particles ----
            updateParticles();

            // ---- Shake decay ----
            if (shakeAmount > 0) shakeAmount *= 0.9;
            if (shakeAmount < 0.05) shakeAmount = 0;
        }

        function draw() {
            ctx.save();

            // Screen shake
            if (shakeAmount > 0.5) {
                const sx = (Math.random() - 0.5) * shakeAmount * 1.2;
                const sy = (Math.random() - 0.5) * shakeAmount * 1.2;
                ctx.translate(sx, sy);
            }

            // Clear
            ctx.clearRect(-10, -10, W + 20, H + 20);

            // Sky gradient
            const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
            skyGrad.addColorStop(0, '#0a0e27');
            skyGrad.addColorStop(0.3, '#141c3a');
            skyGrad.addColorStop(0.6, '#1a2a4a');
            skyGrad.addColorStop(1, '#0f1a2e');
            ctx.fillStyle = skyGrad;
            ctx.fillRect(0, 0, W, H);

            // Stars
            ctx.fillStyle = 'rgba(255,255,255,0.15)';
            const seed = 12345;
            for (let i = 0; i < 40; i++) {
                const sx = ((i * 137 + 31) % W);
                const sy = ((i * 251 + 71) % (H * 0.6));
                const size = ((i * 7) % 3) + 1;
                const twinkle = 0.5 + 0.5 * Math.sin(frameCount * 0.02 + i * 1.7);
                ctx.globalAlpha = 0.2 * twinkle;
                ctx.fillRect(sx, sy, size, size);
            }
            ctx.globalAlpha = 1;

            // ---- Draw road ----
            drawRoad();

            // ---- Draw coins ----
            // Sort by z for proper depth
            const sortedCoins = [...coins].filter(c => !c.collected).sort((a, b) => a.z - b.z);
            for (const coin of sortedCoins) {
                if (coin.z > -50) drawCoin(coin);
            }

            // ---- Draw obstacles ----
            const sortedObs = [...obstacles].sort((a, b) => a.z - b.z);
            for (const obs of sortedObs) {
                if (obs.z > -30) drawObstacle(obs);
            }

            // ---- Draw particles ----
            drawParticles();

            // ---- Draw player ----
            drawPlayer();

            // ---- UI ----
            drawUI();

            ctx.restore();
        }

        function gameLoop() {
            update();
            draw();
            requestAnimationFrame(gameLoop);
        }

        // ============================================================
        //  RESTART
        // ============================================================

        function restartGame() {
            obstacles = [];
            coins = [];
            particles = [];
            score = 0;
            speed = 6.5;
            spawnTimer = 0;
            coinTimer = 0;
            frameCount = 0;
            shakeAmount = 0;
            gameOver = false;
            resetPlayer();
            updateScore();
        }

        // ============================================================
        //  INPUT HANDLING
        // ============================================================

        // ---- Keyboard ----
        document.addEventListener('keydown', (e) => {
            if (gameOver) {
                if (e.key === ' ' || e.key === 'Space' || e.key === 'Enter') {
                    e.preventDefault();
                    restartGame();
                }
                return;
            }
            switch (e.key) {
                case 'ArrowLeft':
                case 'a':
                case 'A':
                    e.preventDefault();
                    keys.left = true;
                    break;
                case 'ArrowRight':
                case 'd':
                case 'D':
                    e.preventDefault();
                    keys.right = true;
                    break;
                case 'ArrowUp':
                case 'w':
                case 'W':
                case ' ':
                case 'Space':
                    e.preventDefault();
                    jumpPlayer();
                    break;
            }
        });

        // ---- Touch / Swipe ----
        let touchStartX = 0,
            touchStartY = 0;
        let isTouching = false;

        canvas.addEventListener('touchstart', (e) => {
            e.preventDefault();
            const touch = e.touches[0];
            const rect = canvas.getBoundingClientRect();
            const x = (touch.clientX - rect.left) / rect.width * W;
            const y = (touch.clientY - rect.top) / rect.height * H;
            touchStartX = x;
            touchStartY = y;
            isTouching = true;

            if (gameOver) {
                restartGame();
                return;
            }
            // Check if tap is in lower half → jump
            if (y > H * 0.6) {
                jumpPlayer();
            }
        }, { passive: false });

        canvas.addEventListener('touchmove', (e) => {
            e.preventDefault();
            if (!isTouching || gameOver) return;
            const touch = e.touches[0];
            const rect = canvas.getBoundingClientRect();
            const x = (touch.clientX - rect.left) / rect.width * W;
            const dx = x - touchStartX;

            if (dx > 35 && player.targetX < laneToX(LANE_COUNT - 1)) {
                player.targetX += LANE_WIDTH;
                touchStartX = x;
            } else if (dx < -35 && player.targetX > laneToX(0)) {
                player.targetX -= LANE_WIDTH;
                touchStartX = x;
            }
        }, { passive: false });

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

        // ---- Mouse click (for desktop) ----
        canvas.addEventListener('click', (e) => {
            const rect = canvas.getBoundingClientRect();
            const x = (e.clientX - rect.left) / rect.width * W;
            const y = (e.clientY - rect.top) / rect.height * H;

            if (gameOver) {
                restartGame();
                return;
            }
            // Click lower half → jump
            if (y > H * 0.55) {
                jumpPlayer();
            } else {
                // Click left/right half → move
                if (x < W / 2 && player.targetX > laneToX(0)) {
                    player.targetX -= LANE_WIDTH;
                } else if (x >= W / 2 && player.targetX < laneToX(LANE_COUNT - 1)) {
                    player.targetX += LANE_WIDTH;
                }
            }
        });

        // ---- Mobile buttons ----
        btnLeft.addEventListener('touchstart', (e) => {
            e.preventDefault();
            if (gameOver) { restartGame(); return; }
            if (player.targetX > laneToX(0)) player.targetX -= LANE_WIDTH;
        }, { passive: false });
        btnLeft.addEventListener('mousedown', (e) => {
            e.preventDefault();
            if (gameOver) { restartGame(); return; }
            if (player.targetX > laneToX(0)) player.targetX -= LANE_WIDTH;
        });

        btnRight.addEventListener('touchstart', (e) => {
            e.preventDefault();
            if (gameOver) { restartGame(); return; }
            if (player.targetX < laneToX(LANE_COUNT - 1)) player.targetX += LANE_WIDTH;
        }, { passive: false });
        btnRight.addEventListener('mousedown', (e) => {
            e.preventDefault();
            if (gameOver) { restartGame(); return; }
            if (player.targetX < laneToX(LANE_COUNT - 1)) player.targetX += LANE_WIDTH;
        });

        btnJump.addEventListener('touchstart', (e) => {
            e.preventDefault();
            if (gameOver) { restartGame(); return; }
            jumpPlayer();
        }, { passive: false });
        btnJump.addEventListener('mousedown', (e) => {
            e.preventDefault();
            if (gameOver) { restartGame(); return; }
            jumpPlayer();
        });

        // ---- Prevent page scroll ----
        document.addEventListener('touchmove', (e) => {
            if (e.target.closest('.game-wrapper')) e.preventDefault();
        }, { passive: false });

        // ---- Handle resize ----
        function resizeCanvas() {
            const wrapper = canvas.parentElement;
            const maxWidth = Math.min(window.innerWidth - 32, 420);
            const ratio = 9 / 16;
            let w = maxWidth;
            let h = w / ratio;
            if (h > window.innerHeight - 120) {
                h = window.innerHeight - 120;
                w = h * ratio;
            }
            canvas.style.width = w + 'px';
            canvas.style.height = h + 'px';
        }
        window.addEventListener('resize', resizeCanvas);
        resizeCanvas();

        // ============================================================
        //  INIT
        // ============================================================

        resetPlayer();
        updateScore();
        // Spawn initial coins
        for (let i = 0; i < 6; i++) {
            setTimeout(() => spawnCoin(), i * 200);
        }
        gameLoop();

        console.log('🏃 Subway Surfers loaded!');
        console.log('Controls: Arrow keys / WASD / Swipe / Click');
        console.log(`🏆 High Score: ${highScore}`);
    </script>

</body>
</html>

Game Source: 🏃 Subway Surfers - HTML

Creator: RocketGlider81

Libraries: none

Complexity: complex (1136 lines, 38.4 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: subway-surfers-html-rocketglider81" to link back to the original. Then publish at arcadelab.ai/publish.