Загрузка данных
<!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(