Загрузка данных


<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Sonic Adventure 2: Green Hill Infinite</title>
    <style>
        body { margin: 0; overflow: hidden; background: #4a90e2; font-family: monospace; user-select: none; }
        #hud { position: absolute; top: 15px; left: 15px; color: #fff; font-size: 22px; font-weight: bold; text-shadow: 2px 2px #000; pointer-events: none; z-index: 10; }
        .y { color: #f1c40f; }

        /* Экран старта и Game Over */
        #ui-layer { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; background: rgba(0,0,0,0.65); color: #fff; pointer-events: auto; }
        .screen-start, .screen-gameover { display: none; text-align: center; padding: 40px; background: rgba(255,255,255,0.12); border-radius: 16px; border: 1px solid rgba(255,255,255,0.25); max-width: 420px; }
        .screen-start h1, .screen-gameover h1 { margin: 0 0 16px 0; font-size: 36px; text-transform: uppercase; letter-spacing: 4px; }
        .btn-play { background: #3b82f6; color: white; border: none; padding: 14px 32px; font-size: 18px; font-weight: 700; border-radius: 8px; cursor: pointer; text-transform: uppercase; }
        .btn-play:hover { background: #2563eb; }
        .score-final { font-size: 24px; margin: 16px 0 24px 0; color: #f1c40f; }
    </style>
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
            }
        }
    </script>
</head>
<body>
    <!-- HUD -->
    <div id="hud"><span class="y">RINGS:</span> <span id="r">000</span> | <span class="y">SCORE:</span> <span id="s">0000</span></div>

    <!-- UI: старт и Game Over -->
    <div id="ui-layer">
        <div class="screen-start" id="start-screen">
            <h1>Sonic Infinite</h1>
            <p style="font-size:16px; line-height:1.5;">W/A/S/D — движение<br>Пробел — прыжок<br>Собирай кольца, избегай шипов.</p>
            <button class="btn-play" onclick="startGame()">Играть</button>
        </div>
        <div class="screen-gameover" id="gameover-screen">
            <h1>Game Over</h1>
            <div class="score-final">Счёт: <span id="final-score">0000</span></div>
            <button class="btn-play" onclick="resetGame()">Рестарт</button>
        </div>
    </div>

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

        // --- ГЛОБАЛЬНЫЕ ПЕРЕМЕННЫЕ ---
        const scene = new THREE.Scene();
        scene.fog = new THREE.Fog(0x4a90e2, 20, 70);
        const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
        const renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.shadowMap.enabled = true;
        document.body.appendChild(renderer.domElement);

        scene.add(new THREE.AmbientLight(0xffffff, 0.7));
        const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
        dirLight.position.set(10, 20, 10);
        dirLight.castShadow = true;
        scene.add(dirLight);

        // --- СОНИК ---
        const sonic = new THREE.Group();
        const blueM = new THREE.MeshStandardMaterial({ color: 0x002fa7, roughness: 0.4 });
        const peachM = new THREE.MeshStandardMaterial({ color: 0xffcc99, roughness: 0.6 });
        const whiteM = new THREE.MeshStandardMaterial({ color: 0xffffff });
        const redM = new THREE.MeshStandardMaterial({ color: 0xcc1111 });
        const darkM = new THREE.MeshStandardMaterial({ color: 0x222222 });

        const body = new THREE.Mesh(new THREE.CylinderGeometry(0.4, 0.3, 1.4, 8), blueM);
        body.position.y = 0.5;
        const belly = new THREE.Mesh(new THREE.SphereGeometry(0.38, 8, 8), peachM);
        belly.position.set(0, 0.5, 0.15);
        belly.scale.set(1, 1.2, 0.6);
        const head = new THREE.Mesh(new THREE.SphereGeometry(0.7, 8, 8), blueM);
        head.position.set(0, 1.5, 0.1);
        const muzzle = new THREE.Mesh(new THREE.ConeGeometry(0.35, 0.7, 8), peachM);
        muzzle.rotation.x = Math.PI / 1.8;
        muzzle.position.set(0, 1.4, 0.7);
        sonic.add(body, belly, head, muzzle);

        for (let i = 0; i < 3; i++) {
            const spike = new THREE.Mesh(new THREE.ConeGeometry(0.28, 1.5, 5), blueM);
            spike.rotation.x = -Math.PI / 2.3;
            spike.position.set(0, 1.7 - i * 0.35, -0.4 - i * 0.15);
            sonic.add(spike);
        }

        const armL = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 1, 4), peachM); armL.position.set(-0.6, 0.6, 0.1);
        const armR = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, 1, 4), peachM); armR.position.set(0.6, 0.6, 0.1);
        const gloveL = new THREE.Mesh(new THREE.SphereGeometry(0.22, 6, 6), whiteM); gloveL.position.set(-0.6, 0.1, 0.1);
        const gloveR = new THREE.Mesh(new THREE.SphereGeometry(0.22, 6, 6), whiteM); gloveR.position.set(0.6, 0.1, 0.1);

        const legL = new THREE.Group();
        const legR = new THREE.Group();
        const limbL = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 1, 4), blueM); limbL.position.y = -0.5;
        const limbR = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 1, 4), blueM); limbR.position.y = -0.5;

        const shoeBoxL = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.3, 1.1), redM); shoeBoxL.position.set(0, -1.1, 0.2);
        const soleL = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.1, 1.15), darkM); soleL.position.set(0, -1.25, 0.2);
        const stripeL = new THREE.Mesh(new THREE.BoxGeometry(0.47, 0.15, 0.3), whiteM); stripeL.position.set(0, -0.96, 0.2);
        legL.add(limbL, shoeBoxL, soleL, stripeL); legL.position.set(-0.4, 0, 0);

        const shoeBoxR = new THREE.Mesh(new THREE.BoxGeometry(0.45, 0.3, 1.1), redM); shoeBoxR.position.set(0, -1.1, 0.2);
        const soleR = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.1, 1.15), darkM); soleR.position.set(0, -1.25, 0.2);
        const stripeR = new THREE.Mesh(new THREE.BoxGeometry(0.47, 0.15, 0.3), whiteM); stripeR.position.set(0, -0.96, 0.2);
        legR.add(limbR, shoeBoxR, soleR, stripeR); legR.position.set(0.4, 0, 0);

        sonic.add(armL, armR, gloveL, gloveR, legL, legR);
        sonic.position.set(0, 1.3, 0);
        sonic.traverse(child => { if(child.isMesh) child.castShadow = true; });
        scene.add(sonic);

        // --- ОКРУЖЕНИЕ ---
        const canvas = document.createElement('canvas');
        canvas.width = 128; canvas.height = 128;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = '#e67e22'; ctx.fillRect(0,0,128,128);
        ctx.fillStyle = '#d35400';
        for(let x=0; x<4; x++) for(let y=0; y<4; y++) if((x+y)%2===0) ctx.fillRect(x*32, y*32, 32, 32);
        const groundTex = new THREE.CanvasTexture(canvas);
        groundTex.wrapS = groundTex.wrapT = THREE.RepeatWrapping;
        groundTex.repeat.set(2, 20);

        const track = new THREE.Mesh(new THREE.BoxGeometry(16, 1, 400), new THREE.MeshStandardMaterial({ map: groundTex, roughness: 0.9 }));
        track.position.set(0, -0.5, -150); track.receiveShadow = true;
        scene.add(track);

        // --- СПИСКИ ОБЪЕКТОВ ---
        let rings = [];
        let spikes = [];
        let hills = [];

        const ringGeo = new THREE.TorusGeometry(0.5, 0.1, 6, 16);
        const ringMat = new THREE.MeshStandardMaterial({ color: 0xf1c40f, metalness: 0.9, roughness: 0.1 });

        const spikeGeo = new THREE.ConeGeometry(0.4, 1.6, 8);
        const spikeMat = new THREE.MeshStandardMaterial({ color: 0xc0392b, roughness: 0.7 });

        const hillGeo = new THREE.ConeGeometry(8, 12, 5);
        const hillMat = new THREE.MeshStandardMaterial({ color: 0x2ecc71, roughness: 0.9 });

        // --- УПРАВЛЕНИЕ ---
        let score = 0, ringsCount = 0, velX = 0, velY = 0, isJumping = false;
        let gameActive = false;
        const keys = { a: false, d: false, w: false, s: false, space: false };

        window.addEventListener('keydown', (e) => {
            if(e.code === 'KeyA' || e.code === 'ArrowLeft') keys.a = true;
            if(e.code === 'KeyD' || e.code === 'ArrowRight') keys.d = true;
            if(e.code === 'KeyW' || e.code === 'ArrowUp') keys.w = true;
            if(e.code === 'KeyS' || e.code === 'ArrowDown') keys.s = true;
            if(e.code === 'Space') keys.space = true;
        });
        window.addEventListener('keyup', (e) => {
            if(e.code === 'KeyA' || e.code === 'ArrowLeft') keys.a = false;
            if(e.code === 'KeyD' || e.code === 'ArrowRight') keys.d = false;
            if(e.code === 'KeyW' || e.code === 'ArrowUp') keys.w = false;
            if(e.code === 'KeyS' || e.code === 'ArrowDown') keys.s = false;
            if(e.code === 'Space') keys.space = false;
        });

        // --- ГЕНЕРАЦИЯ УРОВНЯ ---
        function spawnObjects(zPos) {
            // Кольцо
            const ring = new THREE.Mesh(ringGeo, ringMat);
            ring.position.set((Math.random() - 0.5) * 7, 1.2, zPos);
            scene.add(ring);
            rings.push(ring);

            // Колючка (с шансом)
            if (Math.random() < 0.3) {
                const spike = new THREE.Mesh(spikeGeo, spikeMat);
                spike.rotation.z = Math.PI; // остриём вверх
                spike.position.set((Math.random() - 0.5) * 6, -0.2, zPos + Math.random() * 10 - 5);
                scene.add(spike);
                spikes.push(spike);
            }

            // Холм
            const hill = new THREE.Mesh(hillGeo, hillMat);
            hill.position.set(Math.random() * 14 - 7, 5, zPos - 12);
            scene.add(hill);
            hills.push(hill);
        }

        function cleanupObjects() {
            const removeThreshold = sonic.position.z - 200;

            for (let i = rings.length - 1; i >= 0; i--) {
                if (rings[i].position.z < removeThreshold) {
                    scene.remove(rings[i]);
                    rings.splice(i, 1);
                }
            }
            for (let i = spikes.length - 1; i >= 0; i--) {
                if (spikes[i].position.z < removeThreshold) {
                    scene.remove(spikes[i]);
                    spikes.splice(i, 1);
                }
            }
            for (let i = hills.length - 1; i >= 0; i--) {
                if (hills[i].position.z < removeThreshold) {
                    scene.remove(hills[i]);
                    hills.splice(i, 1);
                }
            }
        }

        // --- ИГРОВОЙ ЦИКЛ ---
        function animate() {
            requestAnimationFrame(animate);
            if (!gameActive) return;

            sonic.position.z -= 0.48; // скорость бега
            const currentZ = Math.floor(