🎮ArcadeLab

Subway Surfer 3D: Neon Rush

by BlazeShark73
1078 lines48.1 KB🛠️ Three.js (3D graphics)
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Subway Surfer 3D: Neon Rush</title>
    <!-- Tailwind CSS -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- Three.js -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <style>
        body, html {
            margin: 0;
            padding: 0;
            overflow: hidden;
            background-color: #0a0c16;
            user-select: none;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
            width: 100%;
            height: 100%;
        }
        #webgl-container {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 0;
        }
        canvas {
            display: block;
            width: 100%;
            height: 100%;
        }
        .text-neon {
            text-shadow: 0 0 10px rgba(59, 130, 246, 0.8), 0 0 20px rgba(59, 130, 246, 0.4);
        }
        .text-gold {
            text-shadow: 0 0 10px rgba(234, 179, 8, 0.8), 0 0 20px rgba(234, 179, 8, 0.4);
        }
    </style>
</head>
<body>

    <!-- HUD Overlay -->
    <div id="hud" class="absolute top-0 left-0 w-full p-4 md:p-6 flex justify-between items-start pointer-events-none z-10 hidden">
        <div class="flex flex-col gap-1">
            <div class="bg-gray-900/80 backdrop-blur-md px-4 py-2 rounded-xl border border-blue-500/30 flex items-center gap-3">
                <span class="text-xs font-bold text-blue-400 uppercase tracking-wider">Score</span>
                <span id="score-val" class="text-2xl font-black text-white text-neon">0</span>
            </div>
            <div class="bg-gray-900/60 backdrop-blur-sm px-3 py-1 rounded-lg border border-gray-700/30 flex items-center gap-2 mt-1">
                <span class="text-[10px] font-bold text-gray-400 uppercase">Multiplier</span>
                <span id="multiplier-val" class="text-sm font-extrabold text-blue-300">x1.0</span>
            </div>
        </div>

        <div class="flex flex-col items-end gap-2">
            <div class="bg-gray-900/80 backdrop-blur-md px-4 py-2 rounded-xl border border-yellow-500/30 flex items-center gap-3">
                <span class="text-xs font-bold text-yellow-500 uppercase tracking-wider">Coins</span>
                <div class="flex items-center gap-1">
                    <svg class="w-5 h-5 text-yellow-400 animate-spin" style="animation-duration: 3s" fill="currentColor" viewBox="0 0 20 20"><path d="M8.433 7.418c.554-.589 1.448-.589 2.002 0l.207.22 1.343-1.43a3.91 3.91 0 00-5.517 0L4.126 8.59a3.91 3.91 0 000 5.517l1.342-1.43-.207-.22a1.411 1.411 0 010-2.002l3.172-3.172z"/><path d="M11.567 12.582c-.554.589-1.448.589-2.002 0l-.207-.22-1.343 1.43a3.91 3.91 0 005.517 0l2.342-2.38a3.91 3.91 0 000-5.517l-1.342 1.43.207.22a1.411 1.411 0 010 2.002l-3.172 3.172z"/></svg>
                    <span id="coins-val" class="text-2xl font-black text-yellow-400 text-gold">0</span>
                </div>
            </div>
            <!-- Speed indicator -->
            <div class="w-24 bg-gray-900/50 rounded-full h-1.5 overflow-hidden border border-gray-700/50">
                <div id="speed-bar" class="bg-blue-500 h-full w-0 transition-all duration-300"></div>
            </div>
        </div>
    </div>

    <!-- UI Overlay Screens -->
    <div id="ui-container" class="absolute inset-0 flex items-center justify-center bg-gray-950/80 z-20 transition-all duration-300">
        <!-- Main / Start Menu -->
        <div id="start-screen" class="max-w-md w-full mx-4 bg-gray-900/95 p-8 rounded-2xl border border-blue-500/40 text-center shadow-2xl flex flex-col items-center">
            <h1 class="text-4xl md:text-5xl font-black text-white mb-2 tracking-tight">NEON <span class="text-blue-500 text-neon">RUNNER</span></h1>
            <p class="text-xs text-blue-300/80 tracking-widest uppercase mb-8">Subway Surfers Clone 3D</p>
            
            <div class="w-full bg-gray-950/80 rounded-xl p-4 border border-gray-800 mb-8 text-left text-sm text-gray-300">
                <h3 class="text-blue-400 font-bold mb-2 flex items-center gap-2">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
                    HOW TO PLAY:
                </h3>
                <div class="grid grid-cols-2 gap-3 text-xs">
                    <div class="flex items-center gap-2">
                        <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">A/D</span> or <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">←/→</span>
                        <span>Change Lanes</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">W</span> or <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">↑</span>
                        <span>Jump Over</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">S</span> or <span class="bg-gray-800 text-white px-2 py-1 rounded font-mono border border-gray-700">↓</span>
                        <span>Slide Under</span>
                    </div>
                    <div class="flex items-center gap-2">
                        <span class="text-gray-400">Mobile: Swipe Screen</span>
                    </div>
                </div>
            </div>

            <button id="start-btn" class="w-full py-4 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-400 hover:to-indigo-500 text-white font-black text-lg rounded-xl shadow-lg hover:shadow-blue-500/20 transform hover:-translate-y-0.5 active:translate-y-0 transition-all duration-150 border-b-4 border-indigo-800">
                START SURFING
            </button>
            <button id="toggle-audio-btn" class="mt-4 text-xs text-gray-400 hover:text-white transition flex items-center gap-1.5">
                <span id="audio-icon">🔊</span> Music & Sound: ON
            </button>
        </div>

        <!-- Game Over Screen -->
        <div id="gameover-screen" class="max-w-md w-full mx-4 bg-gray-900/95 p-8 rounded-2xl border border-red-500/40 text-center shadow-2xl hidden flex flex-col items-center">
            <div class="w-16 h-16 bg-red-500/10 rounded-full border border-red-500/30 flex items-center justify-center mb-4">
                <svg class="w-8 h-8 text-red-500 animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
            </div>
            <h2 class="text-3xl font-black text-white mb-1">CRASH DETECTED!</h2>
            <p class="text-xs text-red-400 uppercase tracking-widest mb-6">Game Over</p>

            <div class="grid grid-cols-2 gap-4 w-full mb-8">
                <div class="bg-gray-950/80 rounded-xl p-4 border border-gray-800">
                    <span class="block text-xs text-gray-400 font-bold uppercase tracking-wider mb-1">Final Score</span>
                    <span id="final-score" class="text-3xl font-black text-blue-400 text-neon">0</span>
                </div>
                <div class="bg-gray-950/80 rounded-xl p-4 border border-gray-800">
                    <span class="block text-xs text-gray-400 font-bold uppercase tracking-wider mb-1">Coins Collected</span>
                    <span id="final-coins" class="text-3xl font-black text-yellow-400 text-gold">0</span>
                </div>
            </div>

            <div id="highscore-banner" class="hidden w-full bg-yellow-500/10 border border-yellow-500/30 text-yellow-400 text-sm font-bold py-2 px-4 rounded-lg mb-6 flex items-center justify-center gap-2">
                🏆 NEW PERSONAL HIGH SCORE!
            </div>

            <div class="w-full text-left mb-6 text-xs text-gray-400 border-t border-gray-800 pt-4">
                <div class="flex justify-between mb-1">
                    <span>Personal Best Score:</span>
                    <span id="best-score-val" class="font-bold text-white">0</span>
                </div>
                <div class="flex justify-between">
                    <span>Total Coins Count:</span>
                    <span id="best-coins-val" class="font-bold text-white">0</span>
                </div>
            </div>

            <button id="restart-btn" class="w-full py-4 bg-gradient-to-r from-red-500 to-pink-600 hover:from-red-400 hover:to-pink-500 text-white font-black text-lg rounded-xl shadow-lg hover:shadow-red-500/20 transform hover:-translate-y-0.5 active:translate-y-0 transition-all duration-150 border-b-4 border-pink-800">
                RUN AGAIN
            </button>
        </div>
    </div>

    <!-- On-screen controls for mobile/tablet players -->
    <div id="mobile-controls" class="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-4 z-10 opacity-30 hover:opacity-100 transition-opacity duration-300 md:hidden">
        <button id="ctrl-left" class="w-14 h-14 bg-gray-900/80 border border-blue-500/40 rounded-xl flex items-center justify-center text-white text-xl active:bg-blue-600/50">←</button>
        <div class="flex flex-col gap-2">
            <button id="ctrl-up" class="w-14 h-14 bg-gray-900/80 border border-blue-500/40 rounded-xl flex items-center justify-center text-white text-xl active:bg-blue-600/50">↑</button>
            <button id="ctrl-down" class="w-14 h-14 bg-gray-900/80 border border-blue-500/40 rounded-xl flex items-center justify-center text-white text-xl active:bg-blue-600/50">↓</button>
        </div>
        <button id="ctrl-right" class="w-14 h-14 bg-gray-900/80 border border-blue-500/40 rounded-xl flex items-center justify-center text-white text-xl active:bg-blue-600/50">→</button>
    </div>

    <!-- WebGL Canvas Container -->
    <div id="webgl-container"></div>

    <script>
        // ==========================================
        // AUDIO SYNTHESIZER (Web Audio API)
        // ==========================================
        const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
        let soundEnabled = true;

        function playSound(type) {
            if (!soundEnabled) return;
            try {
                if (audioCtx.state === 'suspended') {
                    audioCtx.resume();
                }

                const osc = audioCtx.createOscillator();
                const gain = audioCtx.createGain();
                osc.connect(gain);
                gain.connect(audioCtx.destination);

                const now = audioCtx.currentTime;

                if (type === 'coin') {
                    osc.type = 'sine';
                    osc.frequency.setValueAtTime(587.33, now); // D5
                    osc.frequency.setValueAtTime(880, now + 0.08); // A5
                    gain.gain.setValueAtTime(0.1, now);
                    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.3);
                    osc.start(now);
                    osc.stop(now + 0.3);
                } else if (type === 'jump') {
                    osc.type = 'triangle';
                    osc.frequency.setValueAtTime(150, now);
                    osc.frequency.exponentialRampToValueAtTime(400, now + 0.15);
                    gain.gain.setValueAtTime(0.15, now);
                    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.2);
                    osc.start(now);
                    osc.stop(now + 0.25);
                } else if (type === 'slide') {
                    osc.type = 'sawtooth';
                    osc.frequency.setValueAtTime(250, now);
                    osc.frequency.exponentialRampToValueAtTime(80, now + 0.2);
                    gain.gain.setValueAtTime(0.12, now);
                    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
                    osc.start(now);
                    osc.stop(now + 0.25);
                } else if (type === 'crash') {
                    // Explosion noise
                    osc.type = 'sawtooth';
                    osc.frequency.setValueAtTime(100, now);
                    osc.frequency.linearRampToValueAtTime(30, now + 0.5);
                    gain.gain.setValueAtTime(0.3, now);
                    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.6);
                    osc.start(now);
                    osc.stop(now + 0.6);
                } else if (type === 'start') {
                    // Energizing chime
                    osc.type = 'triangle';
                    osc.frequency.setValueAtTime(261.63, now); // C4
                    osc.frequency.setValueAtTime(329.63, now + 0.1); // E4
                    osc.frequency.setValueAtTime(392.00, now + 0.2); // G4
                    osc.frequency.setValueAtTime(523.25, now + 0.3); // C5
                    gain.gain.setValueAtTime(0.2, now);
                    gain.gain.exponentialRampToValueAtTime(0.01, now + 0.5);
                    osc.start(now);
                    osc.stop(now + 0.5);
                }
            } catch (e) {
                console.warn('Audio play failed:', e);
            }
        }

        // Handle Audio Toggle Setup
        const toggleAudioBtn = document.getElementById('toggle-audio-btn');
        const audioIcon = document.getElementById('audio-icon');
        toggleAudioBtn.addEventListener('click', () => {
            soundEnabled = !soundEnabled;
            if (soundEnabled) {
                audioIcon.textContent = '🔊';
                toggleAudioBtn.innerHTML = '🔊 Music & Sound: ON';
                if (audioCtx.state === 'suspended') audioCtx.resume();
            } else {
                audioIcon.textContent = '🔇';
                toggleAudioBtn.innerHTML = '🔇 Music & Sound: OFF';
            }
        });

        // ==========================================
        // STATE & PERSISTENCE
        // ==========================================
        let state = {
            isPlaying: false,
            score: 0,
            coins: 0,
            speed: 80, // initial relative units per sec
            maxSpeed: 200,
            laneWidth: 4,
            currentLane: 1, // 0 = Left, 1 = Center, 2 = Right
            targetX: 0,
            playerY: 0,
            isJumping: false,
            isSliding: false,
            highScore: parseInt(localStorage.getItem('subway_high_score')) || 0,
            totalCoins: parseInt(localStorage.getItem('subway_total_coins')) || 0
        };

        // ==========================================
        // GRAPHICS & 3D ENVIRONMENT
        // ==========================================
        let scene, camera, renderer;
        let playerGroup, boardMesh, bodyMesh;
        let subwayRails = [];
        let activeElements = []; // coins, obstacles, etc.
        let spawnTimer = 0;
        let gameClock;
        let particleSystems = [];

        // Setup colors
        const colors = {
            accent: 0x00f3ff,
            train: 0xff0055,
            coin: 0xffd700,
            ground: 0x0c0e18,
            laneMark: 0x22d3ee,
            barrier: 0xf59e0b
        };

        function init3D() {
            // Container
            const container = document.getElementById('webgl-container');
            
            // Scene
            scene = new THREE.Scene();
            scene.fog = new THREE.FogExp2(0x0a0c16, 0.015);
            scene.background = new THREE.Color(0x0a0c16); // Match the fog to prevent visual clipping

            // Camera
            camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
            camera.position.set(0, 7, 10);
            camera.lookAt(0, 2, -15);

            // Renderer
            renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
            renderer.setSize(container.clientWidth, container.clientHeight);
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            renderer.shadowMap.enabled = true;
            renderer.shadowMap.type = THREE.PCFSoftShadowMap;
            container.appendChild(renderer.domElement);

            // Lights
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.45);
            scene.add(ambientLight);

            const dirLight = new THREE.DirectionalLight(0xffffff, 0.85);
            dirLight.position.set(10, 25, 10);
            dirLight.castShadow = true;
            dirLight.shadow.mapSize.width = 1024;
            dirLight.shadow.mapSize.height = 1024;
            scene.add(dirLight);

            // Neon ambient points
            const neonBlueLight = new THREE.PointLight(0x00f3ff, 2, 50);
            neonBlueLight.position.set(-10, 5, -20);
            scene.add(neonBlueLight);

            const neonPinkLight = new THREE.PointLight(0xff0055, 2, 50);
            neonPinkLight.position.set(10, 5, -30);
            scene.add(neonPinkLight);

            // Generate Floor & Environment elements
            createTrackGround();
            createTunnelLights();
            createPlayer();
        }

        // Create the running platform with three visible lanes
        function createTrackGround() {
            const groundGeo = new THREE.PlaneGeometry(30, 1000);
            const groundMat = new THREE.MeshStandardMaterial({
                color: colors.ground,
                roughness: 0.8,
                metalness: 0.2
            });
            const ground = new THREE.Mesh(groundGeo, groundMat);
            ground.rotation.x = -Math.PI / 2;
            ground.position.z = -400;
            ground.receiveShadow = true;
            scene.add(ground);

            // Visual lanes markers
            for (let i = -1; i <= 1; i++) {
                const laneX = i * state.laneWidth;
                
                // Rail lines
                const railGeo = new THREE.BoxGeometry(0.15, 0.1, 1000);
                const railMat = new THREE.MeshStandardMaterial({
                    color: 0x475569,
                    metalness: 0.8,
                    roughness: 0.2
                });
                const railL = new THREE.Mesh(railGeo, railMat);
                railL.position.set(laneX - 0.6, 0.05, -400);
                scene.add(railL);

                const railR = new THREE.Mesh(railGeo, railMat);
                railR.position.set(laneX + 0.6, 0.05, -400);
                scene.add(railR);

                // Neon center lane dash markers
                const markerGeo = new THREE.BoxGeometry(0.08, 0.02, 10);
                const markerMat = new THREE.MeshBasicMaterial({ color: colors.laneMark });
                for (let z = 0; z > -1000; z -= 30) {
                    const marker = new THREE.Mesh(markerGeo, markerMat);
                    marker.position.set(laneX, 0.02, z);
                    scene.add(marker);
                    subwayRails.push(marker);
                }
            }

            // Create side walls / industrial looking grid pillars
            const pillarGeo = new THREE.BoxGeometry(1, 15, 1);
            const pillarMat = new THREE.MeshStandardMaterial({ color: 0x1e293b, roughness: 0.5 });
            for (let z = 0; z > -1000; z -= 25) {
                const pL = new THREE.Mesh(pillarGeo, pillarMat);
                pL.position.set(-15, 7.5, z);
                scene.add(pL);
                subwayRails.push(pL);

                const pR = new THREE.Mesh(pillarGeo, pillarMat);
                pR.position.set(15, 7.5, z);
                scene.add(pR);
                subwayRails.push(pR);
                
                const beamGeo = new THREE.BoxGeometry(32, 0.5, 0.5);
                const beam = new THREE.Mesh(beamGeo, pillarMat);
                beam.position.set(0, 15, z);
                scene.add(beam);
                subwayRails.push(beam);
            }
        }

        function createTunnelLights() {
            const lightGeo = new THREE.BoxGeometry(2, 0.1, 8);
            const lightMat = new THREE.MeshBasicMaterial({ color: 0x3b82f6 });
            for (let z = 0; z > -800; z -= 80) {
                const neonPlank = new THREE.Mesh(lightGeo, lightMat);
                neonPlank.position.set(0, 14.8, z);
                scene.add(neonPlank);
                subwayRails.push(neonPlank);
            }
        }

        function createPlayer() {
            playerGroup = new THREE.Group();
            
            // Hoverboard
            const boardGeo = new THREE.BoxGeometry(1.2, 0.15, 2.5);
            const boardMat = new THREE.MeshStandardMaterial({
                color: colors.accent,
                roughness: 0.1,
                metalness: 0.8,
                emissive: colors.accent,
                emissiveIntensity: 0.2
            });
            boardMesh = new THREE.Mesh(boardGeo, boardMat);
            boardMesh.position.y = 0.3;
            boardMesh.castShadow = true;
            playerGroup.add(boardMesh);

            // Thrusters
            const thrusterGeo = new THREE.CylinderGeometry(0.15, 0.1, 0.4, 8);
            const thrusterMat = new THREE.MeshBasicMaterial({ color: 0xff3b30 });
            
            const thrusterL = new THREE.Mesh(thrusterGeo, thrusterMat);
            thrusterL.rotation.x = Math.PI / 2;
            thrusterL.position.set(-0.35, 0.2, 1.2);
            playerGroup.add(thrusterL);

            const thrusterR = thrusterL.clone();
            thrusterR.position.x = 0.35;
            playerGroup.add(thrusterR);

            // Suit Body
            const bodyGeo = new THREE.BoxGeometry(0.8, 1.2, 0.6);
            const bodyMat = new THREE.MeshStandardMaterial({ color: 0x1e1b4b, roughness: 0.4 });
            bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
            bodyMesh.position.set(0, 1.1, 0);
            bodyMesh.castShadow = true;
            playerGroup.add(bodyMesh);

            // Chest decal
            const logoGeo = new THREE.PlaneGeometry(0.4, 0.4);
            const logoMat = new THREE.MeshBasicMaterial({ color: colors.accent, side: THREE.DoubleSide });
            const logo = new THREE.Mesh(logoGeo, logoMat);
            logo.position.set(0, 1.1, -0.31);
            playerGroup.add(logo);

            // Helmet
            const headGeo = new THREE.BoxGeometry(0.6, 0.6, 0.6);
            const headMat = new THREE.MeshStandardMaterial({ color: 0x312e81 });
            const head = new THREE.Mesh(headGeo, headMat);
            head.position.set(0, 2.0, 0);
            head.castShadow = true;
            playerGroup.add(head);

            // Visor
            const visorGeo = new THREE.BoxGeometry(0.5, 0.18, 0.1);
            const visorMat = new THREE.MeshBasicMaterial({ color: 0x00f3ff });
            const visor = new THREE.Mesh(visorGeo, visorMat);
            visor.position.set(0, 2.05, -0.27);
            playerGroup.add(visor);

            const boardLight = new THREE.PointLight(0x00f3ff, 2.5, 4);
            boardLight.position.set(0, 0, 0);
            playerGroup.add(boardLight);

            playerGroup.position.set(0, 0, 0);
            scene.add(playerGroup);
        }

        // ==========================================
        // OBSTACLES, COINS & DYNAMIC SPAWNING
        // ==========================================
        class GameElement {
            constructor(type, lane) {
                this.type = type; // 'train', 'low_barrier', 'high_barrier', 'coin'
                this.lane = lane;
                this.x = (lane - 1) * state.laneWidth;
                this.z = -200;
                this.mesh = new THREE.Group();
                this.radius = 1.0;
                this.hit = false;
                
                this.createMesh();
            }

            createMesh() {
                if (this.type === 'coin') {
                    const coinGeo = new THREE.TorusGeometry(0.5, 0.18, 8, 24);
                    const coinMat = new THREE.MeshStandardMaterial({
                        color: colors.coin,
                        metalness: 0.9,
                        roughness: 0.1,
                        emissive: 0x7a6000,
                        emissiveIntensity: 0.2
                    });
                    const coin = new THREE.Mesh(coinGeo, coinMat);
                    coin.castShadow = true;
                    this.mesh.add(coin);
                    this.mesh.position.set(this.x, 1.2, this.z);
                    this.radius = 0.8;
                } 
                else if (this.type === 'train') {
                    const trainLength = 22;
                    const trainHeight = 5.5;
                    const trainGeo = new THREE.BoxGeometry(3.0, trainHeight, trainLength);
                    
                    const trainMat = new THREE.MeshStandardMaterial({
                        color: colors.train,
                        metalness: 0.7,
                        roughness: 0.2,
                    });
                    
                    const train = new THREE.Mesh(trainGeo, trainMat);
                    train.castShadow = true;
                    train.receiveShadow = true;
                    this.mesh.add(train);

                    const glassGeo = new THREE.BoxGeometry(2.8, 1.2, 0.5);
                    const glassMat = new THREE.MeshBasicMaterial({ color: 0x00f3ff });
                    const glass = new THREE.Mesh(glassGeo, glassMat);
                    glass.position.set(0, 1.8, trainLength/2 + 0.05);
                    this.mesh.add(glass);

                    const grillGeo = new THREE.BoxGeometry(2.2, 1.0, 0.2);
                    const grillMat = new THREE.MeshStandardMaterial({ color: 0x111827 });
                    const grill = new THREE.Mesh(grillGeo, grillMat);
                    grill.position.set(0, -1.5, trainLength/2 + 0.05);
                    this.mesh.add(grill);

                    const lightStripGeo = new THREE.BoxGeometry(0.05, 0.15, trainLength - 2);
                    const stripMat = new THREE.MeshBasicMaterial({ color: 0x00ffaa });
                    
                    const stripL = new THREE.Mesh(lightStripGeo, stripMat);
                    stripL.position.set(-1.51, 1.0, 0);
                    this.mesh.add(stripL);

                    const stripR = stripL.clone();
                    stripR.position.x = 1.51;
                    this.mesh.add(stripR);

                    this.mesh.position.set(this.x, trainHeight / 2, this.z - trainLength/2);
                    this.radius = 2.0;
                } 
                else if (this.type === 'low_barrier') {
                    const barGeo = new THREE.BoxGeometry(3.4, 1.2, 0.8);
                    const barMat = new THREE.MeshStandardMaterial({ color: colors.barrier, roughness: 0.5 });
                    const barrier = new THREE.Mesh(barGeo, barMat);
                    barrier.castShadow = true;
                    this.mesh.add(barrier);

                    const stripeGeo = new THREE.BoxGeometry(3.42, 0.2, 0.82);
                    const stripeMat = new THREE.MeshStandardMaterial({ color: 0x111827 });
                    const stripe1 = new THREE.Mesh(stripeGeo, stripeMat);
                    stripe1.position.y = 0.2;
                    this.mesh.add(stripe1);

                    const stripe2 = stripe1.clone();
                    stripe2.position.y = -0.3;
                    this.mesh.add(stripe2);

                    this.mesh.position.set(this.x, 0.6, this.z);
                    this.radius = 1.1;
                } 
                else if (this.type === 'high_barrier') {
                    const frameGeo = new THREE.Group();
                    
                    const barGeo = new THREE.BoxGeometry(3.6, 0.4, 0.6);
                    const barMat = new THREE.MeshStandardMaterial({ color: 0xef4444 });
                    const topBar = new THREE.Mesh(barGeo, barMat);
                    topBar.position.y = 2.4;
                    frameGeo.add(topBar);

                    const legGeo = new THREE.BoxGeometry(0.3, 2.4, 0.3);
                    const legMat = new THREE.MeshStandardMaterial({ color: 0x475569 });
                    const legL = new THREE.Mesh(legGeo, legMat);
                    legL.position.set(-1.6, 1.2, 0);
                    frameGeo.add(legL);

                    const legR = legL.clone();
                    legR.position.x = 1.6;
                    frameGeo.add(legR);

                    const signGeo = new THREE.BoxGeometry(1.5, 0.6, 0.1);
                    const signMat = new THREE.MeshBasicMaterial({ color: 0xffe600 });
                    const sign = new THREE.Mesh(signGeo, signMat);
                    sign.position.y = 1.8;
                    frameGeo.add(sign);

                    this.mesh.add(frameGeo);
                    this.mesh.position.set(this.x, 0, this.z);
                    this.radius = 1.3;
                }

                scene.add(this.mesh);
            }

            destroy() {
                scene.remove(this.mesh);
                this.mesh.traverse(child => {
                    if (child.isMesh) {
                        child.geometry.dispose();
                        if (Array.isArray(child.material)) {
                            child.material.forEach(m => m.dispose());
                        } else {
                            child.material.dispose();
                        }
                    }
                });
            }
        }

        function spawnElements(deltaTime) {
            spawnTimer += deltaTime;
            const spawnInterval = Math.max(0.7, 1.8 - (state.speed / 120));

            if (spawnTimer >= spawnInterval) {
                spawnTimer = 0;
                const spawnLane = Math.floor(Math.random() * 3);
                const rand = Math.random();

                if (rand < 0.25) {
                    spawnCoinChain(spawnLane);
                } else if (rand < 0.50) {
                    activeElements.push(new GameElement('train', spawnLane));
                } else if (rand < 0.75) {
                    activeElements.push(new GameElement('low_barrier', spawnLane));
                } else {
                    activeElements.push(new GameElement('high_barrier', spawnLane));
                }
            }
        }

        function spawnCoinChain(lane) {
            const numCoins = Math.floor(Math.random() * 3) + 3;
            const initialZ = -220;
            
            for (let i = 0; i < numCoins; i++) {
                const coin = new GameElement('coin', lane);
                coin.z = initialZ - (i * 7);
                coin.mesh.position.z = coin.z;
                
                if (numCoins > 3 && i > 0 && i < numCoins - 1) {
                    coin.mesh.position.y = 2.8; 
                }
                
                activeElements.push(coin);
            }
        }

        // ==========================================
        // PARTICLE EFFECTS ENGINE
        // ==========================================
        function createExplosion(pos, color, count = 20) {
            const geom = new THREE.BufferGeometry();
            const positions = [];
            const velocities = [];
            
            for (let i = 0; i < count; i++) {
                positions.push(pos.x, pos.y, pos.z);
                velocities.push(
                    (Math.random() - 0.5) * 12,
                    (Math.random() * 8) + 2,
                    (Math.random() - 0.5) * 12
                );
            }
            
            geom.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
            
            const mat = new THREE.PointsMaterial({
                color: color,
                size: 0.4,
                transparent: true,
                opacity: 1.0,
                blending: THREE.AdditiveBlending
            });
            
            const pSystem = new THREE.Points(geom, mat);
            scene.add(pSystem);
            
            particleSystems.push({
                system: pSystem,
                velocities: velocities,
                age: 0,
                maxAge: 0.8
            });
        }

        function updateParticles(deltaTime) {
            for (let i = particleSystems.length - 1; i >= 0; i--) {
                const p = particleSystems[i];
                p.age += deltaTime;
                
                if (p.age >= p.maxAge) {
                    scene.remove(p.system);
                    p.system.geometry.dispose();
                    p.system.material.dispose();
                    particleSystems.splice(i, 1);
                    continue;
                }
                
                const posArr = p.system.geometry.attributes.position.array;
                
                for (let j = 0; j < posArr.length / 3; j++) {
                    posArr[j * 3] += p.velocities[j * 3] * deltaTime;
                    posArr[j * 3 + 1] += p.velocities[j * 3 + 1] * deltaTime;
                    posArr[j * 3 + 1] -= 9.8 * deltaTime;
                    posArr[j * 3 + 2] += p.velocities[j * 3 + 2] * deltaTime;
                }
                
                p.system.geometry.attributes.position.needsUpdate = true;
                p.system.material.opacity = 1 - (p.age / p.maxAge);
            }
        }

        // ==========================================
        // INPUT CONTROLS SYSTEM
        // ==========================================
        function setupControls() {
            window.addEventListener('keydown', (e) => {
                if (!state.isPlaying) return;
                
                switch (e.key.toLowerCase()) {
                    case 'arrowleft':
                    case 'a':
                        changeLane(-1);
                        break;
                    case 'arrowright':
                    case 'd':
                        changeLane(1);
                        break;
                    case 'arrowup':
                    case 'w':
                        jump();
                        break;
                    case 'arrowdown':
                    case 's':
                        slide();
                        break;
                }
            });

            // Touch Swipes
            let touchStartX = 0;
            let touchStartY = 0;
            const swipeThreshold = 35;

            window.addEventListener('touchstart', (e) => {
                touchStartX = e.changedTouches[0].screenX;
                touchStartY = e.changedTouches[0].screenY;
            }, { passive: true });

            window.addEventListener('touchend', (e) => {
                if (!state.isPlaying) return;

                const diffX = e.changedTouches[0].screenX - touchStartX;
                const diffY = e.changedTouches[0].screenY - touchStartY;

                if (Math.abs(diffX) > Math.abs(diffY)) {
                    if (Math.abs(diffX) > swipeThreshold) {
                        if (diffX > 0) changeLane(1);
                        else changeLane(-1);
                    }
                } else {
                    if (Math.abs(diffY) > swipeThreshold) {
                        if (diffY < 0) jump();
                        else slide();
                    }
                }
            }, { passive: true });

            document.getElementById('ctrl-left').addEventListener('click', () => changeLane(-1));
            document.getElementById('ctrl-right').addEventListener('click', () => changeLane(1));
            document.getElementById('ctrl-up').addEventListener('click', () => jump());
            document.getElementById('ctrl-down').addEventListener('click', () => slide());
        }

        function changeLane(direction) {
            let nextLane = state.currentLane + direction;
            if (nextLane >= 0 && nextLane <= 2) {
                state.currentLane = nextLane;
                state.targetX = (state.currentLane - 1) * state.laneWidth;
                
                boardMesh.rotation.z = -direction * 0.4;
                setTimeout(() => {
                    if (boardMesh) boardMesh.rotation.z = 0;
                }, 200);

                playSound('slide');
            }
        }

        let jumpVelocity = 0;
        const gravity = -32;

        function jump() {
            if (state.isJumping || state.isSliding) return;
            state.isJumping = true;
            jumpVelocity = 14;
            playSound('jump');
            playerGroup.scale.set(0.9, 1.2, 0.9);
        }

        function slide() {
            if (state.isJumping || state.isSliding) return;
            state.isSliding = true;
            playSound('slide');
            playerGroup.scale.set(1.2, 0.4, 1.2);
            playerGroup.position.y = 0.1;

            setTimeout(() => {
                if (state.isSliding) {
                    state.isSliding = false;
                    playerGroup.scale.set(1, 1, 1);
                }
            }, 650);
        }

        // ==========================================
        // GAMEPLAY CYCLE & ENGINE
        // ==========================================
        function startGame() {
            state.score = 0;
            state.coins = 0;
            state.speed = 80;
            state.currentLane = 1;
            state.targetX = 0;
            state.playerY = 0;
            state.isJumping = false;
            state.isSliding = false;
            state.isPlaying = true;
            jumpVelocity = 0;

            document.getElementById('score-val').textContent = "0";
            document.getElementById('coins-val').textContent = "0";
            document.getElementById('multiplier-val').textContent = "x1.0";
            document.getElementById('speed-bar').style.width = "0%";
            
            document.getElementById('ui-container').classList.add('hidden');
            document.getElementById('hud').classList.remove('hidden');

            activeElements.forEach(el => el.destroy());
            activeElements = [];
            
            particleSystems.forEach(p => scene.remove(p.system));
            particleSystems = [];

            playerGroup.position.set(0, 0, 0);
            playerGroup.scale.set(1, 1, 1);

            gameClock = new THREE.Clock();
            playSound('start');
        }

        function gameOver() {
            state.isPlaying = false;
            playSound('crash');

            state.totalCoins += state.coins;
            localStorage.setItem('subway_total_coins', state.totalCoins);

            let isNewHighScore = false;
            if (state.score > state.highScore) {
                state.highScore = state.score;
                localStorage.setItem('subway_high_score', state.highScore);
                isNewHighScore = true;
            }

            createExplosion(playerGroup.position, colors.train, 35);
            createExplosion(playerGroup.position, colors.accent, 20);

            document.getElementById('final-score').textContent = Math.floor(state.score);
            document.getElementById('final-coins').textContent = state.coins;
            document.getElementById('best-score-val').textContent = Math.floor(state.highScore);
            document.getElementById('best-coins-val').textContent = state.totalCoins;

            const highscoreBanner = document.getElementById('highscore-banner');
            if (isNewHighScore) {
                highscoreBanner.classList.remove('hidden');
            } else {
                highscoreBanner.classList.add('hidden');
            }

            document.getElementById('hud').classList.add('hidden');
            document.getElementById('start-screen').classList.add('hidden');
            document.getElementById('gameover-screen').classList.remove('hidden');
            document.getElementById('ui-container').classList.remove('hidden');
        }

        function checkCollisions(playerBox, element) {
            const elPos = new THREE.Vector3();
            element.mesh.getWorldPosition(elPos);

            let elMin, elMax;
            
            if (element.type === 'coin') {
                elMin = new THREE.Vector3(elPos.x - 0.6, elPos.y - 0.6, elPos.z - 0.6);
                elMax = new THREE.Vector3(elPos.x + 0.6, elPos.y + 0.6, elPos.z + 0.6);
            } 
            else if (element.type === 'train') {
                elMin = new THREE.Vector3(elPos.x - 1.5, 0, elPos.z - 11);
                elMax = new THREE.Vector3(elPos.x + 1.5, 5.5, elPos.z + 11);
            } 
            else if (element.type === 'low_barrier') {
                elMin = new THREE.Vector3(elPos.x - 1.7, 0, elPos.z - 0.4);
                elMax = new THREE.Vector3(elPos.x + 1.7, 1.2, elPos.z + 0.4);
            } 
            else if (element.type === 'high_barrier') {
                elMin = new THREE.Vector3(elPos.x - 1.8, 1.6, elPos.z - 0.3);
                elMax = new THREE.Vector3(elPos.x + 1.8, 2.6, elPos.z + 0.3);
            }

            const elBox = new THREE.Box3(elMin, elMax);
            return playerBox.intersectsBox(elBox);
        }

        // ==========================================
        // MAIN ANIMATION LOOP
        // ==========================================
        function animate() {
            requestAnimationFrame(animate);

            const deltaTime = gameClock ? Math.min(gameClock.getDelta(), 0.1) : 0.016;

            // Constantly render and update background elements for seamless presentation
            if (state.isPlaying) {
                if (state.speed < state.maxSpeed) {
                    state.speed += 1.2 * deltaTime;
                }
                const speedPercentage = (state.speed / state.maxSpeed) * 100;
                document.getElementById('speed-bar').style.width = `${speedPercentage}%`;

                state.score += state.speed * 0.1 * deltaTime;
                document.getElementById('score-val').textContent = Math.floor(state.score);
                
                const mult = (1.0 + (state.speed - 80) / 40).toFixed(1);
                document.getElementById('multiplier-val').textContent = `x${mult}`;

                // Smooth lane change movement interpolation
                playerGroup.position.x += (state.targetX - playerGroup.position.x) * 15 * deltaTime;

                // Jump mechanics
                if (state.isJumping) {
                    state.playerY += jumpVelocity * deltaTime;
                    jumpVelocity += gravity * deltaTime;

                    if (state.playerY <= 0) {
                        state.playerY = 0;
                        state.isJumping = false;
                        playerGroup.scale.set(1, 1, 1);
                    }
                    playerGroup.position.y = state.playerY;
                }

                // Dynamic Bounding Boxes
                let pMin, pMax;
                if (state.isSliding) {
                    pMin = new THREE.Vector3(playerGroup.position.x - 0.5, 0, playerGroup.position.z - 0.8);
                    pMax = new THREE.Vector3(playerGroup.position.x + 0.5, 0.6, playerGroup.position.z + 0.8);
                } else {
                    pMin = new THREE.Vector3(playerGroup.position.x - 0.5, playerGroup.position.y, playerGroup.position.z - 0.8);
                    pMax = new THREE.Vector3(playerGroup.position.x + 0.5, playerGroup.position.y + 2.1, playerGroup.position.z + 0.8);
                }
                const playerBox = new THREE.Box3(pMin, pMax);

                spawnElements(deltaTime);

                // Run rails backwards to simulate forward velocity
                subwayRails.forEach(mesh => {
                    mesh.position.z += state.speed * deltaTime;
                    if (mesh.position.z > 25) {
                        mesh.position.z -= 800;
                    }
                });

                // Update obstacles/coins
                for (let i = activeElements.length - 1; i >= 0; i--) {
                    const el = activeElements[i];
                    el.mesh.position.z += state.speed * deltaTime;

                    if (el.type === 'coin') {
                        el.mesh.rotation.y += 3 * deltaTime;
                    }

                    if (!el.hit && checkCollisions(playerBox, el)) {
                        el.hit = true;
                        if (el.type === 'coin') {
                            state.coins += 1;
                            document.getElementById('coins-val').textContent = state.coins;
                            playSound('coin');

                            const splashPos = new THREE.Vector3();
                            el.mesh.getWorldPosition(splashPos);
                            createExplosion(splashPos, colors.coin, 8);

                            el.destroy();
                            activeElements.splice(i, 1);
                        } else {
                            gameOver();
                            break;
                        }
                        continue;
                    }

                    if (el.mesh.position.z > 15) {
                        el.destroy();
                        activeElements.splice(i, 1);
                    }
                }

                // Idle floating hoverboard effect
                if (!state.isJumping && !state.isSliding) {
                    playerGroup.position.y = 0.15 + Math.sin(Date.now() * 0.008) * 0.08;
                    boardMesh.rotation.x = Math.sin(Date.now() * 0.01) * 0.05;
                }

                // Engine Sparks
                if (Math.random() < 0.4) {
                    const tailPos = new THREE.Vector3(playerGroup.position.x, playerGroup.position.y + 0.2, playerGroup.position.z + 1.2);
                    createExplosion(tailPos, 0x00f3ff, 2);
                }
            } else {
                // If the game isn't running, run tracks at slow idle pace for menu screen aesthetics
                subwayRails.forEach(mesh => {
                    mesh.position.z += 8 * deltaTime;
                    if (mesh.position.z > 25) {
                        mesh.position.z -= 800;
                    }
                });
                if (playerGroup) {
                    playerGroup.position.y = 0.15 + Math.sin(Date.now() * 0.003) * 0.08;
                    boardMesh.rotation.x = Math.sin(Date.now() * 0.004) * 0.05;
                }
            }

            updateParticles(deltaTime);
            renderer.render(scene, camera);
        }

        window.addEventListener('resize', onWindowResize, false);
        function onWindowResize() {
            const container = document.getElementById('webgl-container');
            camera.aspect = container.clientWidth / container.clientHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(container.clientWidth, container.clientHeight);
        }

        // ==========================================
        // INITIALIZATION
        // ==========================================
        window.onload = function() {
            init3D();
            setupControls();
            
            gameClock = new THREE.Clock();
            
            // Kick off the animation render loop immediately!
            animate();

            document.getElementById('start-btn').addEventListener('click', startGame);
            document.getElementById('restart-btn').addEventListener('click', startGame);
        };
    </script>
</body>
</html>

Game Source: Subway Surfer 3D: Neon Rush

Creator: BlazeShark73

Libraries: three

Complexity: complex (1078 lines, 48.1 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-surfer-3d-neon-rush-blazeshark73" to link back to the original. Then publish at arcadelab.ai/publish.