<!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>Three.js Multi-Vehicle Simulator</title>
    <style>
        body { margin: 0; overflow: hidden; background-color: #87CEEB; font-family: Arial, sans-serif; }
        canvas { display: block; }
        #info {
            position: absolute;
            top: 10px;
            left: 10px;
            color: white;
            background-color: rgba(0,0,0,0.5);
            padding: 5px 10px;
            border-radius: 5px;
            font-size: 12px;
        }
        #controls {
            position: absolute;
            bottom: 10px;
            left: 10px;
            display: grid;
            grid-template-areas:
                ". up ."
                "left fwd right"
                ". down back";
            gap: 5px;
            opacity: 0.7;
        }
        #controls button {
            padding: 15px;
            font-size: 18px;
            background-color: rgba(80, 80, 80, 0.8);
            color: white;
            border: 1px solid #555;
            border-radius: 8px;
            user-select: none; /* Prevent text selection on hold */
            -webkit-user-select: none; /* Safari */
            touch-action: manipulation; /* Prevent zoom on double tap */
        }
        #btn-fwd { grid-area: fwd; }
        #btn-back { grid-area: back; }
        #btn-left { grid-area: left; }
        #btn-right { grid-area: right; }
        #btn-up { grid-area: up; }
        #btn-down { grid-area: down; }

        #switch-vehicle {
            position: absolute;
            bottom: 10px;
            right: 10px;
            padding: 15px;
            font-size: 14px;
            background-color: rgba(0, 100, 200, 0.8);
            color: white;
            border: 1px solid #005;
            border-radius: 8px;
            opacity: 0.8;
        }

        /* Hide desktop-only instructions on small screens */
        @media (max-width: 768px) {
            #desktop-instructions { display: none; }
        }
         /* Hide mobile buttons on large screens */
        @media (min-width: 769px) {
            #controls { display: none; }
        }

    </style>
</head>
<body>
    <div id="info">
        Loading...
        <div id="desktop-instructions"><br>Controls: [W/S] or [Up/Down] = Forward/Back | [A/D] or [Left/Right] = Turn | [Q/E] or [Shift/Ctrl] = Altitude/Strafe(Car)</div>
    </div>
    <div id="controls">
        <button id="btn-fwd"></button>
        <button id="btn-back"></button>
        <button id="btn-left"></button>
        <button id="btn-right"></button>
        <button id="btn-up">U</button>  <!-- Up/Altitude -->
        <button id="btn-down">D</button> <!-- Down/Altitude -->
    </div>
    <button id="switch-vehicle">Switch Vehicle</button>

    <!-- Import map for Three.js modules -->
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js"
            }
        }
    </script>

    <script type="module">
        import * as THREE from 'three';

        let scene, camera, renderer, clock;
        let playerVehicles = {};
        let currentVehicleType = 'car'; // 'car', 'plane', 'rocket'
        let activeVehicle;
        let aiPlanes = [];
        let train;
        let trainPath;
        const trainSpeed = 20; // units per second
        let trainT = 0; // parameter along the path

        const keyboardState = {};
        const touchState = { forward: false, backward: false, left: false, right: false, up: false, down: false };

        const vehicleSettings = {
            car: { speed: 50, turnSpeed: Math.PI / 2 },
            plane: { speed: 100, turnSpeed: Math.PI / 3, altitudeSpeed: 20, minAltitude: 10, maxAltitude: 500 },
            rocket: { speed: 150, turnSpeed: Math.PI / 4, altitudeSpeed: 50, minAltitude: 1 }
        };

        const cameraOffset = new THREE.Vector3(0, 5, -15); // Behind and slightly above

        init();
        animate();

        function init() {
            // Basic Setup
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x87CEEB); // Sky blue
            scene.fog = new THREE.Fog(0x87CEEB, 100, 800);

            camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(0, 20, 30); // Initial camera position

            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setSize(window.innerWidth, window.innerHeight);
            document.body.appendChild(renderer.domElement);

            clock = new THREE.Clock();

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

            const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
            directionalLight.position.set(50, 100, 75);
            // // Basic shadow setup (uncomment if needed, impacts performance)
            // directionalLight.castShadow = true;
            // directionalLight.shadow.mapSize.width = 1024;
            // directionalLight.shadow.mapSize.height = 1024;
            // directionalLight.shadow.camera.near = 0.5;
            // directionalLight.shadow.camera.far = 500;
            // renderer.shadowMap.enabled = true;
            scene.add(directionalLight);


            // --- Create Environment ---
            createGround();
            createMountains(15, 100, 400); // Number, max size, spread radius
            createTrees(100, 400); // Number, spread radius
            createRoad(200, 10); // Length, width
            createBuildings(30, 150, 400); // Number, max size, spread radius
            createHouse(new THREE.Vector3(30, 0, 30));
            createClouds(50, 200, 500); // Number, average altitude, spread radius
            createTrainAndTrack(150, 5); // Track radius, number of carriages

            // --- Create Player Vehicles ---
            playerVehicles.car = createCar();
            playerVehicles.plane = createPlane();
            playerVehicles.rocket = createRocket();

            // Position vehicles initially
            playerVehicles.car.position.set(0, 0.5, 10);
            playerVehicles.plane.position.set(50, vehicleSettings.plane.minAltitude, -50);
            playerVehicles.rocket.position.set(-50, vehicleSettings.rocket.minAltitude, -50);

            scene.add(playerVehicles.car);
            scene.add(playerVehicles.plane);
            scene.add(playerVehicles.rocket);

            // --- Create AI Planes ---
            aiPlanes.push(createAIPlane(new THREE.Vector3(100, 150, 0), 80)); // Position, speed
            aiPlanes.push(createAIPlane(new THREE.Vector3(-150, 120, 100), 90));
            aiPlanes.forEach(p => scene.add(p.mesh));

            // --- Initial Setup ---
            switchVehicle(currentVehicleType); // Set initial active vehicle

            // --- Event Listeners ---
            window.addEventListener('resize', onWindowResize);
            window.addEventListener('keydown', (event) => { keyboardState[event.code] = true; });
            window.addEventListener('keyup', (event) => { keyboardState[event.code] = false; });

            setupMobileControls();
            document.getElementById('switch-vehicle').addEventListener('click', () => {
                if (currentVehicleType === 'car') switchVehicle('plane');
                else if (currentVehicleType === 'plane') switchVehicle('rocket');
                else switchVehicle('car');
            });

            document.getElementById('info').innerHTML = `Controlling: ${currentVehicleType.toUpperCase()}<br><span id="desktop-instructions">Controls: [W/S] or [Up/Down] = Fwd/Back | [A/D] or [Left/Right] = Turn | [Q/E] or [Shift/Ctrl] = Altitude/Strafe</span>`;
        }

        // --- Creation Functions ---

        function createGround() {
            const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
            const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x55aa55, side: THREE.DoubleSide }); // Greenish
            const ground = new THREE.Mesh(groundGeometry, groundMaterial);
            ground.rotation.x = -Math.PI / 2; // Rotate flat
            // ground.receiveShadow = true; // Uncomment for shadows
            scene.add(ground);
        }

        function createMountains(count, maxSize, radius) {
            const mountainMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 }); // Brown
            const snowMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff }); // White

            for (let i = 0; i < count; i++) {
                 const height = Math.random() * maxSize + maxSize * 0.5;
                 const width = height * (0.8 + Math.random() * 0.4);
                 const mountainGeometry = new THREE.ConeGeometry(width / 2, height, 4 + Math.floor(Math.random() * 4)); // Fewer sides for jagged look
                 const mountain = new THREE.Mesh(mountainGeometry, mountainMaterial);

                 const angle = Math.random() * Math.PI * 2;
                 const dist = radius * 0.5 + Math.random() * radius * 0.5;
                 mountain.position.set(
                     Math.cos(angle) * dist,
                     height / 2 - 1, // Base slightly below ground
                     Math.sin(angle) * dist
                 );
                 mountain.rotation.y = Math.random() * Math.PI * 2;
                 // mountain.castShadow = true; // Uncomment for shadows
                 scene.add(mountain);

                 // Add snow cap (optional)
                 if (height > maxSize * 0.8) {
                     const snowHeight = height * 0.2;
                     const snowGeometry = new THREE.ConeGeometry(width/3, snowHeight, mountainGeometry.parameters.radialSegments);
                     const snowCap = new THREE.Mesh(snowGeometry, snowMaterial);
                     snowCap.position.y = height/2 - snowHeight/2 + 0.1; // Position on top
                     mountain.add(snowCap); // Add to mountain group
                 }
            }
        }

        function createTrees(count, radius) {
            const trunkMaterial = new THREE.MeshStandardMaterial({ color: 0x8B4513 }); // Brown
            const leavesMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 }); // Forest Green

            for (let i = 0; i < count; i++) {
                const treeGroup = new THREE.Group();

                const height = Math.random() * 8 + 4; // 4 to 12 units high
                const trunkHeight = height * 0.4;
                const leavesHeight = height * 0.6;
                const trunkRadius = height * 0.05;
                const leavesRadius = height * 0.2;

                const trunkGeometry = new THREE.CylinderGeometry(trunkRadius, trunkRadius, trunkHeight, 8);
                const trunk = new THREE.Mesh(trunkGeometry, trunkMaterial);
                trunk.position.y = trunkHeight / 2;
                // trunk.castShadow = true; // Uncomment for shadows

                const leavesGeometry = new THREE.ConeGeometry(leavesRadius, leavesHeight, 8);
                const leaves = new THREE.Mesh(leavesGeometry, leavesMaterial);
                leaves.position.y = trunkHeight + leavesHeight / 2;
                // leaves.castShadow = true; // Uncomment for shadows

                treeGroup.add(trunk);
                treeGroup.add(leaves);

                const angle = Math.random() * Math.PI * 2;
                const dist = Math.random() * radius;

                // Avoid placing trees directly on the road/track area
                 if (Math.abs(Math.cos(angle) * dist) < 10 && Math.abs(Math.sin(angle) * dist) < 220) continue; // Skip if near road center
                 if (Math.pow(Math.cos(angle) * dist, 2) + Math.pow(Math.sin(angle) * dist, 2) < Math.pow(160,2)) continue; // Skip if inside train track radius


                treeGroup.position.set(
                    Math.cos(angle) * dist,
                    0,
                    Math.sin(angle) * dist
                );
                treeGroup.rotation.y = Math.random() * Math.PI;

                scene.add(treeGroup);
            }
        }

         function createRoad(length, width) {
            const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x444444 });
            const roadGeometry = new THREE.PlaneGeometry(width, length);
            const road = new THREE.Mesh(roadGeometry, roadMaterial);
            road.rotation.x = -Math.PI / 2;
            road.position.set(0, 0.01, 0); // Slightly above ground
            // road.receiveShadow = true; // Uncomment for shadows
            scene.add(road);

            // Center line (optional)
            const lineMaterial = new THREE.MeshBasicMaterial({ color: 0xFFFF00 });
            const lineGeometry = new THREE.PlaneGeometry(0.3, length);
            const centerLine = new THREE.Mesh(lineGeometry, lineMaterial);
            centerLine.rotation.x = -Math.PI / 2;
            centerLine.position.set(0, 0.02, 0); // Slightly above road
            scene.add(centerLine);
         }

        function createBuildings(count, maxSize, radius) {
             const buildingMaterial = new THREE.MeshStandardMaterial({ color: 0xAAAAAA }); // Grey

             for (let i = 0; i < count; i++) {
                 const height = Math.random() * maxSize + 20;
                 const width = Math.random() * (maxSize / 4) + 10;
                 const depth = Math.random() * (maxSize / 4) + 10;

                 const buildingGeometry = new THREE.BoxGeometry(width, height, depth);
                 const building = new THREE.Mesh(buildingGeometry, buildingMaterial);
                 // building.castShadow = true; // Uncomment for shadows
                 // building.receiveShadow = true; // Uncomment for shadows

                 const angle = Math.random() * Math.PI * 2;
                 // Place buildings further out
                 const dist = radius * 0.6 + Math.random() * radius * 0.4;

                 // Avoid placing buildings on road or near house/track
                 const x = Math.cos(angle) * dist;
                 const z = Math.sin(angle) * dist;
                 if (Math.abs(x) < width/2 + 10 && Math.abs(z) < 110) continue; // Avoid road
                 if (Math.abs(x - 30) < width/2 + 15 && Math.abs(z - 30) < depth/2 + 15) continue; // Avoid house
                 if (Math.pow(x, 2) + Math.pow(z, 2) < Math.pow(170, 2) && Math.pow(x, 2) + Math.pow(z, 2) > Math.pow(130, 2) ) continue; // Avoid track


                 building.position.set(x, height / 2, z);
                 building.rotation.y = (Math.random() > 0.5 ? 0 : Math.PI / 2); // Align some buildings

                 scene.add(building);
             }
         }

        function createHouse(position) {
            const houseGroup = new THREE.Group();
            const baseMaterial = new THREE.MeshStandardMaterial({ color: 0xF5DEB3 }); // Wheat
            const roofMaterial = new THREE.MeshStandardMaterial({ color: 0xA0522D }); // Sienna

            const baseWidth = 10, baseHeight = 6, baseDepth = 12;
            const baseGeometry = new THREE.BoxGeometry(baseWidth, baseHeight, baseDepth);
            const base = new THREE.Mesh(baseGeometry, baseMaterial);
            base.position.y = baseHeight / 2;
            // base.castShadow = true; // Uncomment for shadows
            houseGroup.add(base);

            const roofGeometry = new THREE.ConeGeometry(baseWidth * 0.7, baseHeight * 0.7, 4); // Pyramid roof
            const roof = new THREE.Mesh(roofGeometry, roofMaterial);
            roof.position.y = baseHeight + (baseHeight * 0.7) / 2;
            roof.rotation.y = Math.PI / 4; // Align roof edges
            // roof.castShadow = true; // Uncomment for shadows
            houseGroup.add(roof);

            houseGroup.position.copy(position);
            scene.add(houseGroup);
        }

         function createClouds(count, altitude, radius) {
             const cloudMaterial = new THREE.MeshBasicMaterial({
                 color: 0xffffff,
                 transparent: true,
                 opacity: 0.7
             });

             for (let i = 0; i < count; i++) {
                 const cloudGroup = new THREE.Group();
                 const numPuffs = 5 + Math.floor(Math.random() * 5);

                 for (let j = 0; j < numPuffs; j++) {
                     const puffSize = Math.random() * 15 + 10;
                     const puffGeometry = new THREE.SphereGeometry(puffSize, 8, 6);
                     const puff = new THREE.Mesh(puffGeometry, cloudMaterial);
                     puff.position.set(
                         (Math.random() - 0.5) * puffSize * 2,
                         (Math.random() - 0.5) * puffSize * 0.5,
                         (Math.random() - 0.5) * puffSize * 2
                     );
                     cloudGroup.add(puff);
                 }

                 const angle = Math.random() * Math.PI * 2;
                 const dist = Math.random() * radius;
                 const cloudAltitude = altitude + (Math.random() - 0.5) * 50;

                 cloudGroup.position.set(
                     Math.cos(angle) * dist,
                     cloudAltitude,
                     Math.sin(angle) * dist
                 );
                 scene.add(cloudGroup);
             }
         }

        function createTrainAndTrack(radius, numCarriages) {
            // Track (visual only)
            const trackMaterial = new THREE.MeshBasicMaterial({ color: 0x666666 });
            const trackGeometry = new THREE.TorusGeometry(radius, 0.5, 8, 100);
            const trackMesh = new THREE.Mesh(trackGeometry, trackMaterial);
            trackMesh.rotation.x = Math.PI / 2;
            trackMesh.position.y = 0.1;
            scene.add(trackMesh);

             // Define path for train movement
            trainPath = new THREE.CatmullRomCurve3(
                Array.from({ length: 101 }, (_, i) => {
                    const angle = (i / 100) * Math.PI * 2;
                    return new THREE.Vector3(Math.cos(angle) * radius, 1, Math.sin(angle) * radius);
                })
            );
             trainPath.curveType = 'catmullrom';
             trainPath.closed = true;


            // Create Train
            train = new THREE.Group();
            const carriageLength = 8;
            const carriageWidth = 4;
            const carriageHeight = 4;
            const gap = 1;

            const engineMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 }); // Red
            const carriageMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff }); // Blue

            for (let i = 0; i < numCarriages; i++) {
                const geometry = new THREE.BoxGeometry(carriageLength, carriageHeight, carriageWidth);
                const material = i === 0 ? engineMaterial : carriageMaterial;
                const carriage = new THREE.Mesh(geometry, material);
                // carriage.castShadow = true; // Uncomment for shadows

                // Position along the group's local Z axis initially (will be oriented by path later)
                carriage.position.z = -(i * (carriageLength + gap));
                train.add(carriage);
            }

            scene.add(train);
        }


        function createCar() {
            const carGroup = new THREE.Group();
            const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xfff00 }); // Yellow
            const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x222222 }); // Dark Grey

            // Body
            const bodyGeometry = new THREE.BoxGeometry(6, 2, 3);
            const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
            body.position.y = 1;
            // body.castShadow = true; // Uncomment for shadows
            carGroup.add(body);

            // Cabin
             const cabinGeometry = new THREE.BoxGeometry(3, 1.5, 2.5);
             const cabin = new THREE.Mesh(bodyGeometry, bodyMaterial); // Use same material or different
             cabin.position.set(0, 2.25, 0); // Place on top of body
             carGroup.add(cabin);


            // Wheels
            const wheelRadius = 0.7;
            const wheelWidth = 0.5;
            const wheelGeometry = new THREE.CylinderGeometry(wheelRadius, wheelRadius, wheelWidth, 16);
            wheelGeometry.rotateX(Math.PI / 2); // Orient wheels correctly

            const wheelPositions = [
                { x: 2.5, y: 0.7, z: 1.5 },
                { x: -2.5, y: 0.7, z: 1.5 },
                { x: 2.5, y: 0.7, z: -1.5 },
                { x: -2.5, y: 0.7, z: -1.5 },
            ];

            wheelPositions.forEach(pos => {
                const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
                wheel.position.set(pos.x, pos.y, pos.z);
                // wheel.castShadow = true; // Uncomment for shadows
                carGroup.add(wheel);
            });

            return carGroup;
        }

        function createPlane() {
            const planeGroup = new THREE.Group();
            const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc }); // Light Grey
            const wingMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa }); // Darker Grey

            // Fuselage
            const fuselageGeometry = new THREE.CylinderGeometry(1, 1, 10, 12);
            fuselageGeometry.rotateX(Math.PI / 2); // Point forward
            const fuselage = new THREE.Mesh(fuselageGeometry, bodyMaterial);
            // fuselage.castShadow = true; // Uncomment for shadows
            planeGroup.add(fuselage);

            // Wings
            const wingGeometry = new THREE.BoxGeometry(12, 0.5, 3);
            const wings = new THREE.Mesh(wingGeometry, wingMaterial);
            wings.position.y = 0;
            // wings.castShadow = true; // Uncomment for shadows
            planeGroup.add(wings);

            // Tail Wing (Horizontal Stabilizer)
            const tailWingGeometry = new THREE.BoxGeometry(5, 0.3, 1.5);
            const tailWing = new THREE.Mesh(tailWingGeometry, wingMaterial);
            tailWing.position.set(0, 0.5, 4.5); // Back and slightly up
            // tailWing.castShadow = true; // Uncomment for shadows
            planeGroup.add(tailWing);

             // Tail Fin (Vertical Stabilizer)
             const tailFinGeometry = new THREE.BoxGeometry(0.3, 2.5, 1.5);
             const tailFin = new THREE.Mesh(tailFinGeometry, bodyMaterial);
             tailFin.position.set(0, 1.5, 4.5); // Back and up
             // tailFin.castShadow = true; // Uncomment for shadows
             planeGroup.add(tailFin);

            // Propeller (simple)
             const propGeometry = new THREE.BoxGeometry(0.2, 3, 0.2);
             const propeller = new THREE.Mesh(propGeometry, wingMaterial);
             propeller.position.z = -5.2; // Front of fuselage
             planeGroup.add(propeller);


            planeGroup.rotation.order = 'YXZ'; // Rotation order for intuitive flight controls
            return planeGroup;
        }

         function createRocket() {
             const rocketGroup = new THREE.Group();
             const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xdddddd }); // Light Grey
             const tipMaterial = new THREE.MeshStandardMaterial({ color: 0xff4444 }); // Reddish
             const finMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa }); // Darker Grey

             const bodyHeight = 10;
             const bodyRadius = 1;
             const tipHeight = 3;

             // Main Body
             const bodyGeometry = new THREE.CylinderGeometry(bodyRadius, bodyRadius, bodyHeight, 16);
             const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
             body.position.y = bodyHeight / 2;
             // body.castShadow = true; // Uncomment for shadows
             rocketGroup.add(body);

             // Nose Cone
             const tipGeometry = new THREE.ConeGeometry(bodyRadius, tipHeight, 16);
             const tip = new THREE.Mesh(tipGeometry, tipMaterial);
             tip.position.y = bodyHeight + tipHeight / 2;
             // tip.castShadow = true; // Uncomment for shadows
             rocketGroup.add(tip);

             // Fins (simple triangles)
              const finShape = new THREE.Shape();
              finShape.moveTo(0, 0);
              finShape.lineTo(0, 3); // Height of fin
              finShape.lineTo(-1.5, 0); // Width away from body
              finShape.lineTo(0, 0);

              const extrudeSettings = { depth: 0.2, bevelEnabled: false };
              const finGeometry = new THREE.ExtrudeGeometry(finShape, extrudeSettings);
              const finMesh = new THREE.Mesh(finGeometry, finMaterial);
              // finMesh.castShadow = true; // Uncomment for shadows

              // Add 3 fins
              for (let i = 0; i < 3; i++) {
                  const fin = finMesh.clone();
                  fin.rotation.y = (i * Math.PI * 2) / 3;
                  fin.position.y = 0.5; // Position near bottom of body
                  // Rotate fin to be perpendicular to radius
                  fin.rotateOnWorldAxis(new THREE.Vector3(0,1,0), (i * Math.PI * 2) / 3);
                  fin.rotateOnAxis(new THREE.Vector3(1,0,0), -Math.PI / 2); // Make upright
                  rocketGroup.add(fin);
              }


             // Initially point straight up
             rocketGroup.rotation.x = -Math.PI / 2;
             rocketGroup.rotation.order = 'YXZ';

             return rocketGroup;
         }

        function createAIPlane(startPos, speed) {
            const planeMesh = createPlane(); // Reuse plane creation function
            planeMesh.scale.set(0.5, 0.5, 0.5); // Make AI planes smaller
            planeMesh.position.copy(startPos);
            const radius = 80 + Math.random() * 40; // Circular path radius
            const angleOffset = Math.random() * Math.PI * 2; // Start at random point on circle
             const altitude = startPos.y;

            return {
                mesh: planeMesh,
                speed: speed,
                radius: radius,
                altitude: altitude,
                angle: angleOffset, // Current angle on the circle
                center: new THREE.Vector3(startPos.x - Math.cos(angleOffset) * radius, altitude, startPos.z - Math.sin(angleOffset) * radius) // Center of the circular path
            };
        }

        // --- Control & Update Functions ---

        function switchVehicle(type) {
            // Hide all vehicles
            Object.values(playerVehicles).forEach(v => v.visible = false);

            // Show the selected one
            activeVehicle = playerVehicles[type];
            if (activeVehicle) {
                activeVehicle.visible = true;
                currentVehicleType = type;
                document.getElementById('info').innerHTML = `Controlling: ${currentVehicleType.toUpperCase()}<br><span id="desktop-instructions">Controls: [W/S] or [Up/Down] = Fwd/Back | [A/D] or [Left/Right] = Turn | [Q/E] or [Shift/Ctrl] = Altitude/Strafe</span>`;
            } else {
                console.error("Tried to switch to unknown vehicle type:", type);
                currentVehicleType = 'car'; // Default back to car
                activeVehicle = playerVehicles.car;
                activeVehicle.visible = true;
                 document.getElementById('info').innerHTML = `Controlling: ${currentVehicleType.toUpperCase()}<br><span id="desktop-instructions">Controls: [W/S] or [Up/Down] = Fwd/Back | [A/D] or [Left/Right] = Turn | [Q/E] or [Shift/Ctrl] = Altitude/Strafe</span>`;
            }
             // Reset controls to avoid sticky keys/buttons
            Object.keys(keyboardState).forEach(k => keyboardState[k] = false);
            Object.keys(touchState).forEach(k => touchState[k] = false);
        }

        function updatePlayerVehicle(delta) {
            if (!activeVehicle) return;

            const settings = vehicleSettings[currentVehicleType];
            let moveForward = 0;
            let turn = 0;
            let altitudeChange = 0;

            // Keyboard Input
            if (keyboardState['KeyW'] || keyboardState['ArrowUp']) moveForward += 1;
            if (keyboardState['KeyS'] || keyboardState['ArrowDown']) moveForward -= 1;
            if (keyboardState['KeyA'] || keyboardState['ArrowLeft']) turn += 1;
            if (keyboardState['KeyD'] || keyboardState['ArrowRight']) turn -= 1;
            if (keyboardState['KeyE'] || keyboardState['ShiftLeft'] || keyboardState['ShiftRight']) altitudeChange += 1; // Q/Shift up
            if (keyboardState['KeyQ'] || keyboardState['ControlLeft'] || keyboardState['ControlRight']) altitudeChange -= 1; // E/Ctrl down

            // Touch Input
            if (touchState.forward) moveForward += 1;
            if (touchState.backward) moveForward -= 1;
            if (touchState.left) turn += 1;
            if (touchState.right) turn -= 1;
            if (touchState.up) altitudeChange += 1;
            if (touchState.down) altitudeChange -= 1;

            // Normalize diagonal movement (optional but good practice)
            if(moveForward !== 0 && turn !== 0) {
                 // moveForward *= 0.707; // Or just cap speed
            }

            // --- Apply Movement based on Vehicle Type ---

            const moveSpeed = settings.speed * delta;
            const turnSpeed = settings.turnSpeed * delta;
            const altitudeSpeed = (settings.altitudeSpeed || 0) * delta;

            // Rotation (Yaw) - applies to all
            activeVehicle.rotation.y += turn * turnSpeed;

            // Movement
            const forwardVector = new THREE.Vector3();
            activeVehicle.getWorldDirection(forwardVector);

            if (currentVehicleType === 'car') {
                activeVehicle.position.addScaledVector(forwardVector, moveForward * moveSpeed);

                // Simple strafe for car using altitude controls
                const rightVector = new THREE.Vector3();
                rightVector.crossVectors(activeVehicle.up, forwardVector).normalize(); // Get right vector relative to car
                activeVehicle.position.addScaledVector(rightVector, -altitudeChange * moveSpeed * 0.5); // Strafe with Q/E or Up/Down on mobile

                // Keep car on the ground (simple)
                activeVehicle.position.y = 0.5;
                 activeVehicle.rotation.x = 0; // Keep it flat
                 activeVehicle.rotation.z = 0;

            } else if (currentVehicleType === 'plane' || currentVehicleType === 'rocket') {
                // Forward/Backward Movement
                activeVehicle.position.addScaledVector(forwardVector, moveForward * moveSpeed);

                // Altitude Change
                activeVehicle.position.y += altitudeChange * altitudeSpeed;

                 // Banking Turn for Plane/Rocket (Visual Only - Roll)
                 const targetRoll = -turn * Math.PI / 6; // Max roll angle
                 activeVehicle.rotation.z += (targetRoll - activeVehicle.rotation.z) * delta * 5; // Smoothly lerp to target roll


                // Pitch Control (linked to altitude change for simplicity)
                 const targetPitch = altitudeChange * Math.PI / 8; // Pitch up/down slightly when changing altitude
                 activeVehicle.rotation.x += (targetPitch - activeVehicle.rotation.x) * delta * 3;


                 // Altitude Constraints
                 if (settings.minAltitude && activeVehicle.position.y < settings.minAltitude) {
                     activeVehicle.position.y = settings.minAltitude;
                 }
                 if (settings.maxAltitude && activeVehicle.position.y > settings.maxAltitude) {
                    activeVehicle.position.y = settings.maxAltitude;
                }
            }
             // Simple world bounds
             const bound = 490;
             activeVehicle.position.x = Math.max(-bound, Math.min(bound, activeVehicle.position.x));
             activeVehicle.position.z = Math.max(-bound, Math.min(bound, activeVehicle.position.z));

        }

        function updateAIPlanes(delta) {
            aiPlanes.forEach(plane => {
                 const angularSpeed = plane.speed / plane.radius; // Radians per second
                 plane.angle += angularSpeed * delta;
                 plane.angle %= (Math.PI * 2); // Keep angle within 0 to 2*PI

                 const x = plane.center.x + Math.cos(plane.angle) * plane.radius;
                 const z = plane.center.z + Math.sin(plane.angle) * plane.radius;
                 plane.mesh.position.set(x, plane.altitude, z);

                 // Make the plane look in the direction of travel (tangent to the circle)
                 const lookAtX = plane.center.x + Math.cos(plane.angle + angularSpeed * delta * 10) * plane.radius; // Look slightly ahead
                 const lookAtZ = plane.center.z + Math.sin(plane.angle + angularSpeed * delta * 10) * plane.radius;
                 plane.mesh.lookAt(lookAtX, plane.altitude, lookAtZ);
                 plane.mesh.rotation.z = -Math.PI / 8; // Constant bank angle

                  // Simple propeller spin (visual only)
                 const propeller = plane.mesh.children.find(child => child.geometry.type === 'BoxGeometry' && child.position.z < -5); // Find propeller mesh
                 if (propeller) {
                     propeller.rotation.z += delta * 50; // Spin fast
                 }
            });
        }

        function updateTrain(delta) {
             if (!train || !trainPath) return;

             const pathLength = trainPath.getLength();
             trainT += (trainSpeed / pathLength) * delta; // Increment t based on speed and path length
             trainT %= 1.0; // Loop back to start

             const currentPos = trainPath.getPointAt(trainT);
             const nextPos = trainPath.getPointAt((trainT + 0.001) % 1.0); // Point slightly ahead for orientation

             train.position.copy(currentPos);
             train.lookAt(nextPos);

              // Adjust individual carriages slightly based on path curvature (optional - basic approximation)
             const tangent = trainPath.getTangentAt(trainT);
             const normal = new THREE.Vector3(0,1,0); // Assume flat ground
             const binormal = new THREE.Vector3().crossVectors(tangent, normal).normalize();

             train.children.forEach((carriage, index) => {
                  // Apply a slight offset based on binormal for curve effect
                  const offsetFactor = (index - (train.children.length - 1) / 2) * 0.1; // Small offset factor
                  // carriage.position.x = offsetFactor * carriage.geometry.parameters.width * 0.1; // Adjust side position slightly (can cause issues if not careful)
             });
        }


        function updateCamera(delta) {
            if (!activeVehicle) return;

            const targetCameraPosition = new THREE.Vector3();
            activeVehicle.localToWorld(targetCameraPosition.copy(cameraOffset)); // Calculate target position relative to vehicle

            // Smoothly interpolate camera position (Lerp)
            camera.position.lerp(targetCameraPosition, delta * 4.0);

             // Smoothly interpolate camera target (LookAt)
             const targetLookAt = new THREE.Vector3();
             // Look slightly *ahead* of the vehicle for planes/rockets, directly at car
             const lookAheadDistance = (currentVehicleType === 'car' ? 0 : 5);
             const lookAtOffset = new THREE.Vector3(0, 1.5, lookAheadDistance); // Offset slightly up and forward from vehicle center
             activeVehicle.localToWorld(targetLookAt.copy(lookAtOffset));

             // Store current lookAt target
             const currentLookAt = new THREE.Vector3(); // Need a vector to store the current target
             // Get the current direction the camera is pointing
             camera.getWorldDirection(currentLookAt);
             // Add the current camera position to get the point in space it's looking at
             currentLookAt.add(camera.position);

             // Lerp the lookAt point
             currentLookAt.lerp(targetLookAt, delta * 3.0);

            camera.lookAt(currentLookAt); // Use the interpolated lookAt point
        }

        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
        }

        function setupMobileControls() {
            const buttons = [
                { id: 'btn-fwd', stateKey: 'forward' },
                { id: 'btn-back', stateKey: 'backward' },
                { id: 'btn-left', stateKey: 'left' },
                { id: 'btn-right', stateKey: 'right' },
                { id: 'btn-up', stateKey: 'up' },
                { id: 'btn-down', stateKey: 'down' },
            ];

            buttons.forEach(({ id, stateKey }) => {
                const button = document.getElementById(id);
                if (button) {
                    button.addEventListener('touchstart', (e) => { e.preventDefault(); touchState[stateKey] = true; }, { passive: false });
                    button.addEventListener('touchend', (e) => { e.preventDefault(); touchState[stateKey] = false; }, { passive: false });
                    button.addEventListener('touchcancel', (e) => { e.preventDefault(); touchState[stateKey] = false; }, { passive: false });
                    // Mouse events for desktop testing
                    button.addEventListener('mousedown', () => { touchState[stateKey] = true; });
                    button.addEventListener('mouseup', () => { touchState[stateKey] = false; });
                    button.addEventListener('mouseleave', () => { touchState[stateKey] = false; });
                } else {
                    console.warn(`Button with ID ${id} not found.`);
                }
            });
        }

        function animate() {
            requestAnimationFrame(animate);

            const delta = clock.getDelta();

            updatePlayerVehicle(delta);
            updateAIPlanes(delta);
            updateTrain(delta);
            updateCamera(delta);

            renderer.render(scene, camera);
        }

    </script>
</body>
</html>

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.