Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8"><title>Sonic Adventure 2: Green Hill Mini</title>
<style>
body { margin: 0; overflow: hidden; background: #4a90e2; font-family: monospace; }
#hud { position: absolute; top: 15px; left: 15px; color: #fff; font-size: 22px; font-weight: bold; text-shadow: 2px 2px #000; pointer-events: none; }
.y { color: #f1c40f; }
</style>
<script src="https://cloudflare.com"></script>
</head>
<body>
<div id="hud"><span class="y">RINGS:</span> <span id="r">000</span> | <span class="y">SCORE:</span> <span id="s">0000</span></div>
<script>
// --- ИНИЦИАЛИЗАЦИЯ СЦЕНЫ ---
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);
// --- СОЗДАНИЕ МОДЕЛИ СОНИКА (СТИЛЬ SA2 С SOAP SHOES) ---
const sonic = new THREE.Group();
const blueM = new THREE.MeshStandardMaterial({ color: 0x002fa7, roughness: 0.4 }); // Более глубокий синий SA2
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 }); // Для деталей Soap Shoes
// Стройное тело и голова (Модерн пропорции SA2)
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);
// Длинные, загнутые назад иглы (Специфика SA2 арт-стиля)
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;
// Легендарные кроссовки Soap Shoes (сложная low-poly форма с подошвой для гринда)
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);
// --- ОКРУЖЕНИЕ GREEN HILL ZONE ---
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);
const rings = [];
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 hillGeo = new THREE.ConeGeometry(8, 12, 5);
const hillMat = new THREE.MeshStandardMaterial({ color: 0x2ecc71, roughness: 0.9 });
for (let i = 0; i < 25; i++) {
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.position.set((Math.random() - 0.5) * 8, 1.2, -30 - i * 14);
scene.add(ring); rings.push(ring);
const hillL = new THREE.Mesh(hillGeo, hillMat); hillL.position.set(-16 - Math.random()*10, 5, -i * 16);
const hillR = new THREE.Mesh(hillGeo, hillMat); hillR.position.set(16 + Math.random()*10, 5, -i * 16);
scene.add(hillL, hillR);
}
// --- ЛОГИКА И УПРАВЛЕНИЕ ---
let score = 0, ringsCount = 0, velX = 0, velY = 0, isJumping = false;
const keys = { a: false, d: 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 === '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 === 'Space') keys.space = false;
});
// --- ИГРОВОЙ ЦИКЛ ---
function animate() {
requestAnimationFrame(animate);
sonic.position.z -= 0.45; // Чуть быстрее, в стиле SA2
if (sonic.position.z < -350) {
sonic.position.z = 0;
rings.forEach((r, idx) => { r.position.z = -30 - idx * 14; r.visible = true; });
}
if (keys.a && sonic.position.x > -7) velX -= 0.05;
if (keys.d && sonic.position.x < 7) velX += 0.05;
velX *= 0.85;
sonic.position.x += velX;
// Наклон корпуса при поворотах (динамика из SA2)
sonic.rotation.y = -velX * 2;
sonic.rotation.z = -velX * 1.5;
if (keys.space && !isJumping) { velY = 0.26; isJumping = true; }
if (isJumping) {
velY -= 0.012;
sonic.position.y += velY;
if (sonic.position.y <= 1.3) { sonic.position.y = 1.3; isJumping = false; }
// Сворачивание в шар при прыжке
head.position.y = 0.8;
sonic.rotation.x -= 0.2;
} else {
head.position.y = 1.5;
sonic.rotation.x = 0;
// Размашистая SA2-анимация бега для ног и рук
const t = Date.now() * 0.012;
legL.position.z = Math.sin(t) * 0.6; legL.position.y = Math.max(0, Math.cos(t) * 0.3);
legR.position.z = -Math.sin(t) * 0.6; legR.position.y = Math.max(0, -Math.cos(t) * 0.3);
armL.rotation.x = -Math.sin(t) * 0.8; armR.rotation.x = Math.sin(t) * 0.8;
gloveL.position.z = Math.cos(t) * 0.4 + 0.1; gloveR.position.z = -Math.cos(t) * 0.4 + 0.1;
}
rings.forEach(ring => {
ring.rotation.y += 0.05;
if (ring.visible && sonic.position.distanceTo(ring.position) < 1.8) {
ring.visible = false;
ringsCount++; score += 100;
document.getElementById('r').innerText = String(ringsCount).padStart(3, '0');
document.getElementById('s').innerText = String(score).padStart(4, '0');
}
});
camera.position.set(sonic.position.x, sonic.position.y + 3.5, sonic.position.z + 8.5);
camera.lookAt(sonic.position.x, sonic.position.y + 1, sonic.position.z - 5);
renderer.render(scene, camera);
}
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
animate();