Загрузка данных
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tails 3D Platformer</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #000;
font-family: sans-serif;
}
#ui {
position: absolute;
top: 20px;
left: 20px;
color: #fff;
font-size: 24px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
pointer-events: none;
z-index: 10;
}
#instructions {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
color: #fff;
background: rgba(0,0,0,0.6);
padding: 10px 20px;
border-radius: 8px;
pointer-events: none;
text-align: center;
z-index: 10;
}
#loader-screen {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
background: #fe982a; /* Оранжевый цвет под стиль Тейлза */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
font-size: 28px;
font-weight: bold;
text-shadow: 1px 1px 5px rgba(0,0,0,0.3);
z-index: 100;
transition: opacity 0.5s ease;
}
</style>
<!-- Библиотеки Three.js, OrbitControls и GLTFLoader -->
<script src="https://cloudflare.com"></script>
<script src="https://jsdelivr.net"></script>
<script src="https://jsdelivr.net"></script>
</head>
<body>
<div id="loader-screen">Загрузка Тейлза из сети...</div>
<div id="ui">Кольца: <span id="score">0</span></div>
<div id="instructions">WASD — Движение | ПРОБЕЛ — Прыжок | Мышь — Обзор камеры</div>
<script>
// --- Настройка сцены ---
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xa0c0ff);
scene.fog = new THREE.FogExp2(0xa0c0ff, 0.015);
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);
// --- Освещение ---
const ambientLight = new THREE.AmbientLight(0xffffff, 0.8);
scene.add(ambientLight);
const dirLight = new THREE.DirectionLight(0xffffff, 1.0);
dirLight.position.set(20, 40, 20);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2000;
dirLight.shadow.mapSize.height = 2000;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 100;
const d = 40;
dirLight.shadow.camera.left = -d;
dirLight.shadow.camera.right = d;
dirLight.shadow.camera.top = d;
dirLight.shadow.camera.bottom = -d;
scene.add(dirLight);
// --- Контроллер камеры (Третье лицо) ---
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
controls.minDistance = 4;
controls.maxDistance = 10;
controls.enablePan = false;
// --- Игровые переменные и физика ---
let score = 0;
const gravity = -35;
const jumpForce = 13;
const moveSpeed = 11;
const player = {
mesh: new THREE.Group(),
velocity: new THREE.Vector3(),
isGrounded: false,
radius: 0.6
};
scene.add(player.mesh);
const platforms = [];
const rings = [];
// --- АВТОЗАГРУЗКА 3D-МОДЕЛИ ТЕЙЛЗА ИЗ GITHUB ---
const loader = new THREE.GLTFLoader();
// Используем прямую проверенную ссылку на GLTF модель Тейлза с открытого репозитория
const modelUrl = 'https://githubusercontent.com';
loader.load(modelUrl, function(gltf) {
const model = gltf.scene;
// Настройка размера и центрирования
model.scale.set(0.6, 0.6, 0.6);
model.position.y = -0.6;
// Поворачиваем модель лицом к движению
model.rotation.y = Math.PI;
// Включаем тени
model.traverse(node => {
if (node.isMesh) {
node.castShadow = true;
node.receiveShadow = true;
// Исправляем возможные проблемы с отображением текстур при импорте
if(node.material) node.material.side = THREE.DoubleSide;
}
});
player.mesh.add(model);
player.mesh.position.set(0, 5, 0);
// Убираем экран загрузки
const loaderScreen = document.getElementById('loader-screen');
loaderScreen.style.opacity = '0';
setTimeout(() => loaderScreen.remove(), 500);
}, undefined, function(error) {
console.error('Ошибка загрузки:', error);
document.getElementById('loader-screen').innerText = "Ошибка сети. Попробуйте обновить страницу.";
});
// --- Генерация уровня ---
function createPlatform(x, y, z, w, h, d) {
const geo = new THREE.BoxGeometry(w, h, d);
const mat = new THREE.MeshStandardMaterial({ color: 0x5c4033, roughness: 0.9 });
const topGeo = new THREE.BoxGeometry(w, 0.2, d);
const topMat = new THREE.MeshStandardMaterial({ color: 0x3cb371, roughness: 0.6 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y - h/2, z);
mesh.receiveShadow = true;
const topMesh = new THREE.Mesh(topGeo, topMat);
topMesh.position.set(0, h/2 + 0.1, 0);
topMesh.receiveShadow = true;
mesh.add(topMesh);
scene.add(mesh);
platforms.push({ mesh: mesh, box: new THREE.Box3().setFromObject(mesh), topY: y });
}
function createRing(x, y, z) {
const geo = new THREE.TorusGeometry(0.35, 0.08, 8, 24);
const mat = new THREE.MeshStandardMaterial({ color: 0xFFD700, metalness: 0.9, roughness: 0.1, emissive: 0x332200 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, y, z);
mesh.castShadow = true;
scene.add(mesh);
rings.push(mesh);
}
// Строим классическую зону Green Hill из островов
createPlatform(0, 0, 0, 14, 10, 14);
createRing(0, 1.2, 2);
createRing(0, 1.2, 4);
createPlatform(0, 2, -14, 10, 10, 10);
createRing(0, 3.2, -14);
createPlatform(12, 4, -24, 8, 12, 8);
createRing(12, 5.2, -24);
createPlatform(0, 6, -38, 12, 15, 12);
createRing(0, 7.2, -38);
createRing(2, 7.2, -38);
createRing(-2, 7.2, -38);
createPlatform(-14, 9, -38, 8, 18, 8);
createRing(-14, 10.2, -38);
// --- Обработка ввода ---
const keys = { w: false, a: false, s: false, d: false, space: false };
window.addEventListener('keydown', (e) => {
if(e.code === 'KeyW') keys.w = true;
if(e.code === 'KeyS') keys.s = true;
if(e.code === 'KeyA') keys.a = true;
if(e.code === 'KeyD') keys.d = true;
if(e.code === 'Space') keys.space = true;
});
window.addEventListener('keyup', (e) => {
if(e.code === 'KeyW') keys.w = false;
if(e.code === 'KeyS') keys.s = false;
if(e.code === 'KeyA') keys.a = false;
if(e.code === 'KeyD') keys.d = false;
if(e.code === 'Space') keys.space = false;
});
// --- Главный Цикл ---
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.1);
// Направление бега от камеры
const camForward = new THREE.Vector3();
camera.getWorldDirection(camForward);
camForward.y = 0;
camForward.normalize();
const camRight = new THREE.Vector3();
camRight.crossVectors(camForward, camera.up).normalize().negate();
const moveDirection = new THREE.Vector3();
if (keys.w) moveDirection.add(camForward);
if (keys.s) moveDirection.add(camForward.clone().negate());
if (keys.a) moveDirection.add(camRight.clone().negate());
if (keys.d) moveDirection.add(camRight);
moveDirection.normalize();
player.velocity.x = moveDirection.x * moveSpeed;
player.velocity.z = moveDirection.z * moveSpeed;
// Физика прыжка и падения
player.velocity.y += gravity * dt;
if (keys.space && player.isGrounded) {
player.velocity.y = jumpForce;
player.isGrounded = false;
}
player.mesh.position.addScaledVector(player.velocity, dt);
// Плавный поворот и анимационный наклон при движении
if (moveDirection.lengthSq() > 0) {
const targetAngle = Math.atan2(moveDirection.x, moveDirection.z);
let diff = targetAngle - player.mesh.rotation.y;