Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Eldoria: Sword & Sorcery</title> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/cannon-es@0.19.0/dist/cannon-es.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.min.js"></script> | |
| <style> | |
| body { margin: 0; overflow: hidden; } | |
| canvas { display: block; } | |
| #ui { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 20px; | |
| color: white; | |
| font-family: Arial; | |
| } | |
| #health-bar { | |
| width: 200px; | |
| height: 20px; | |
| background-color: #333; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| #health-fill { | |
| height: 100%; | |
| width: 100%; | |
| background-color: #4CAF50; | |
| transition: width 0.3s; | |
| } | |
| #power-bar { | |
| margin-top: 10px; | |
| width: 200px; | |
| height: 20px; | |
| background-color: #333; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| #power-fill { | |
| height: 100%; | |
| width: 100%; | |
| background-color: #2196F3; | |
| transition: width 0.3s; | |
| } | |
| #controls { | |
| position: absolute; | |
| right: 20px; | |
| bottom: 20px; | |
| } | |
| .btn { | |
| width: 60px; | |
| height: 60px; | |
| background-color: rgba(255,255,255,0.3); | |
| border-radius: 50%; | |
| display: inline-block; | |
| margin: 5px; | |
| text-align: center; | |
| line-height: 60px; | |
| color: white; | |
| font-size: 24px; | |
| user-select: none; | |
| } | |
| #mobile-ui { display: none; } | |
| @media (max-width: 768px) { | |
| #mobile-ui { display: block; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="story-modal" class="fixed inset-0 bg-black bg-opacity-80 z-50 flex items-center justify-center hidden"> | |
| <div class="bg-gray-800 p-6 rounded-lg max-w-2xl mx-4"> | |
| <h2 class="text-2xl text-yellow-400 mb-4">The Fall of Eldoria</h2> | |
| <div id="story-text" class="text-white mb-4"> | |
| The dark sorcerer Malakar has taken over Eldoria, turning its lands into his shadow empire. You are Kael, the last warrior-mage, wielding both sword and magic to defeat Malakar's forces. Survive his endless waves of minions and face him in the final battle... | |
| </div> | |
| <button id="close-story" class="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded">Begin Journey</button> | |
| </div> | |
| </div> | |
| <div id="ui"> | |
| <div class="flex items-center"> | |
| <div class="mr-4"> | |
| <div>Health: <span id="health-text">100</span></div> | |
| <div id="health-bar"><div id="health-fill"></div></div> | |
| </div> | |
| <div> | |
| <div>Power: <span id="power-text">100</span></div> | |
| <div id="power-bar"><div id="power-fill"></div></div> | |
| </div> | |
| <button id="story-btn" class="ml-4 bg-purple-600 hover:bg-purple-700 text-white px-3 py-1 rounded">Story</button> | |
| <div id="element-display" class="ml-4 flex"> | |
| <div class="w-8 h-8 rounded-full bg-red-500 mx-1" title="Fire"></div> | |
| <div class="w-8 h-8 rounded-full bg-blue-500 mx-1" title="Water"></div> | |
| <div class="w-8 h-8 rounded-full bg-green-500 mx-1" title="Earth"></div> | |
| <div class="w-8 h-8 rounded-full bg-gray-300 mx-1" title="Air"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="mobile-ui"> | |
| <div id="controls" class="flex"> | |
| <div class="btn" id="move-left-btn">←</div> | |
| <div class="btn" id="move-right-btn">→</div> | |
| <div class="btn" id="move-up-btn">↑</div> | |
| <div class="btn" id="move-down-btn">↓</div> | |
| <div class="btn bg-red-500" id="attack-btn">A</div> | |
| <div class="btn bg-orange-500" id="power-btn">P</div> | |
| </div> | |
| </div> | |
| <script> | |
| // Game setup | |
| const scene = new THREE.Scene(); | |
| scene.background = new THREE.Color(0x87CEEB); // Sky blue | |
| const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
| camera.position.set(0, 5, 10); | |
| camera.lookAt(0, 0, 0); | |
| const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| renderer.shadowMap.enabled = true; | |
| document.body.appendChild(renderer.domElement); | |
| // Lighting | |
| const ambientLight = new THREE.AmbientLight(0x404040); | |
| scene.add(ambientLight); | |
| const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
| directionalLight.position.set(1, 1, 1); | |
| directionalLight.castShadow = true; | |
| scene.add(directionalLight); | |
| // Ground | |
| const groundGeometry = new THREE.PlaneGeometry(30, 30); | |
| const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x228B22 }); | |
| const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
| ground.rotation.x = -Math.PI / 2; | |
| ground.receiveShadow = true; | |
| scene.add(ground); | |
| // Hero (Kael) - Warrior Mage | |
| const heroGeometry = new THREE.BoxGeometry(0.6, 1.8, 0.6); | |
| const heroMaterial = new THREE.MeshStandardMaterial({ color: 0x3333AA }); | |
| const hero = new THREE.Mesh(heroGeometry, heroMaterial); | |
| hero.position.y = 0.9; | |
| hero.castShadow = true; | |
| scene.add(hero); | |
| // Add sword to hero | |
| const swordGeometry = new THREE.BoxGeometry(0.1, 1.5, 0.1); | |
| const swordBladeGeometry = new THREE.BoxGeometry(0.3, 0.1, 0.1); | |
| const swordMaterial = new THREE.MeshStandardMaterial({ color: 0xCCCCCC }); | |
| const sword = new THREE.Mesh(swordGeometry, swordMaterial); | |
| const swordBlade = new THREE.Mesh(swordBladeGeometry, swordMaterial); | |
| sword.position.set(0.5, 0.5, 0); | |
| swordBlade.position.set(0.5, 1.25, 0); | |
| hero.add(sword); | |
| hero.add(swordBlade); | |
| // Sword animation variables | |
| let isSwinging = false; | |
| let swingProgress = 0; | |
| const maxSwingAngle = Math.PI * 1.5; | |
| // Story elements | |
| document.getElementById('story-btn').addEventListener('click', () => { | |
| document.getElementById('story-modal').classList.remove('hidden'); | |
| }); | |
| document.getElementById('close-story').addEventListener('click', () => { | |
| document.getElementById('story-modal').classList.add('hidden'); | |
| }); | |
| // Element switching | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === '1') changeElement('fire'); | |
| if (e.key === '2') changeElement('water'); | |
| if (e.key === '3') changeElement('earth'); | |
| if (e.key === '4') changeElement('air'); | |
| }); | |
| function changeElement(element) { | |
| activeElement = element; | |
| heroMaterial.color.setHex(elementColors[element]); | |
| updateElementDisplay(); | |
| } | |
| function updateElementDisplay() { | |
| const elements = document.querySelectorAll('#element-display div'); | |
| elements.forEach((el, index) => { | |
| if (Object.keys(elementColors)[index] === activeElement) { | |
| el.classList.add('ring-2', 'ring-yellow-400'); | |
| } else { | |
| el.classList.remove('ring-2', 'ring-yellow-400'); | |
| } | |
| }); | |
| } | |
| // Movement controls | |
| document.addEventListener('keydown', (e) => { | |
| if (keys.hasOwnProperty(e.key)) keys[e.key] = true; | |
| }); | |
| document.addEventListener('keyup', (e) => { | |
| if (keys.hasOwnProperty(e.key)) keys[e.key] = false; | |
| }); | |
| if (isMobile) { | |
| const mobileBtns = ['move-left-btn', 'move-right-btn', 'move-up-btn', 'move-down-btn']; | |
| mobileBtns.forEach(btn => { | |
| const element = document.getElementById(btn); | |
| element.addEventListener('touchstart', () => { | |
| keys[`Arrow${btn.split('-')[1].charAt(0).toUpperCase() + btn.split('-')[1].slice(1)}`] = true; | |
| }); | |
| element.addEventListener('touchend', () => { | |
| keys[`Arrow${btn.split('-')[1].charAt(0).toUpperCase() + btn.split('-')[1].slice(1)}`] = false; | |
| }); | |
| }); | |
| } | |
| // Game variables | |
| let health = 100; | |
| let power = 100; | |
| let enemies = []; | |
| let isMobile = window.innerWidth <= 768; | |
| let heroSpeed = 0.2; | |
| let activeElement = 'fire'; // fire, water, earth, air | |
| const elementColors = { | |
| fire: 0xFF4500, | |
| water: 0x4169E1, | |
| earth: 0x8B4513, | |
| air: 0x87CEEB | |
| }; | |
| let keys = { | |
| ArrowUp: false, | |
| ArrowDown: false, | |
| ArrowLeft: false, | |
| ArrowRight: false | |
| }; | |
| // Villain - Malakar | |
| const createMalakar = () => { | |
| const malakarGeometry = new THREE.BoxGeometry(1.2, 2.5, 1.2); | |
| const malakarMaterial = new THREE.MeshStandardMaterial({ color: 0x990000 }); | |
| const malakar = new THREE.Mesh(malakarGeometry, malakarMaterial); | |
| malakar.position.set(0, 1.25, -15); | |
| malakar.castShadow = true; | |
| // Dark aura | |
| const auraGeometry = new THREE.SphereGeometry(1.5, 16, 16); | |
| const auraMaterial = new THREE.MeshBasicMaterial({ | |
| color: 0x330000, | |
| transparent: true, | |
| opacity: 0.3 | |
| }); | |
| const aura = new THREE.Mesh(auraGeometry, auraMaterial); | |
| malakar.add(aura); | |
| scene.add(malakar); | |
| return { | |
| mesh: malakar, | |
| health: 500, | |
| isActive: false, | |
| attackCooldown: 0 | |
| }; | |
| }; | |
| const malakar = createMalakar(); | |
| // Create enemies | |
| function createEnemy() { | |
| if (malakar.isActive) return; // Don't spawn minions during boss fight | |
| const types = ['sorcerer', 'wraith', 'beast', 'knight']; | |
| const type = types[Math.floor(Math.random() * types.length)]; | |
| let geometry, material; | |
| switch(type) { | |
| case 'sorcerer': | |
| geometry = new THREE.ConeGeometry(0.5, 1.8, 4); | |
| material = new THREE.MeshStandardMaterial({ color: 0x8B0000 }); | |
| break; | |
| case 'wraith': | |
| geometry = new THREE.SphereGeometry(0.6, 16, 16); | |
| material = new THREE.MeshStandardMaterial({ color: 0x4B0082, transparent: true, opacity: 0.7 }); | |
| break; | |
| case 'beast': | |
| geometry = new THREE.BoxGeometry(1, 1.2, 1.5); | |
| material = new THREE.MeshStandardMaterial({ color: 0x556B2F }); | |
| break; | |
| case 'knight': | |
| geometry = new THREE.BoxGeometry(0.8, 1.8, 0.8); | |
| material = new THREE.MeshStandardMaterial({ color: 0x888888 }); | |
| break; | |
| } | |
| const enemy = new THREE.Mesh(geometry, material); | |
| // Random position around hero (ensure not too close) | |
| let angle, radius, x, z; | |
| do { | |
| angle = Math.random() * Math.PI * 2; | |
| radius = 5 + Math.random() * 10; | |
| x = Math.cos(angle) * radius; | |
| z = Math.sin(angle) * radius; | |
| } while (Math.abs(x - hero.position.x) < 3 && Math.abs(z - hero.position.z) < 3); | |
| enemy.position.set(x, type === 'wraith' ? 1.5 : 0.75, z); | |
| enemy.castShadow = true; | |
| // Add floating animation for wraiths | |
| if (type === 'wraith') { | |
| enemy.userData.floatOffset = Math.random() * Math.PI * 2; | |
| } | |
| scene.add(enemy); | |
| enemies.push({ | |
| mesh: enemy, | |
| type: type, | |
| speed: type === 'wraith' ? 0.3 + Math.random() * 0.3 : | |
| type === 'beast' ? 0.7 + Math.random() * 0.5 : 0.5 + Math.random() * 0.3, | |
| damage: type === 'sorcerer' ? 15 + Math.floor(Math.random() * 10) : | |
| type === 'beast' ? 20 + Math.floor(Math.random() * 15) : 10 + Math.floor(Math.random() * 5), | |
| health: type === 'sorcerer' ? 40 + Math.floor(Math.random() * 20) : | |
| type === 'beast' ? 60 + Math.floor(Math.random() * 30) : | |
| type === 'knight' ? 80 + Math.floor(Math.random() * 40) : 30 + Math.floor(Math.random() * 15), | |
| damage: type === 'sorcerer' ? 15 + Math.floor(Math.random() * 10) : | |
| type === 'beast' ? 20 + Math.floor(Math.random() * 15) : | |
| type === 'knight' ? 25 + Math.floor(Math.random() * 20) : 10 + Math.floor(Math.random() * 5) | |
| }); | |
| } | |
| // Sword attack | |
| function swordAttack() { | |
| if (isSwinging) return; | |
| isSwinging = true; | |
| swingProgress = 0; | |
| // Melee hit detection | |
| const swordRange = 2; | |
| enemies.forEach((enemy, index) => { | |
| const distance = hero.position.distanceTo(enemy.mesh.position); | |
| if (distance < swordRange) { | |
| enemy.health -= 30; // Sword does more base damage | |
| if (enemy.health <= 0) { | |
| scene.remove(enemy.mesh); | |
| enemies.splice(index, 1); | |
| } | |
| } | |
| }); | |
| } | |
| // Magic attack function | |
| function magicAttack() { | |
| if (power >= 10) { | |
| power -= 10; | |
| updateUI(); | |
| // Element-based magic attack | |
| let effectColor, damage, range; | |
| switch(activeElement) { | |
| case 'fire': | |
| effectColor = 0xFF4500; | |
| damage = 20; | |
| range = 3; | |
| break; | |
| case 'water': | |
| effectColor = 0x4169E1; | |
| damage = 15; | |
| range = 4; | |
| break; | |
| case 'earth': | |
| effectColor = 0x8B4513; | |
| damage = 25; | |
| range = 2.5; | |
| break; | |
| case 'air': | |
| effectColor = 0x87CEEB; | |
| damage = 10; | |
| range = 5; | |
| break; | |
| } | |
| // Visual effect | |
| const sphereGeometry = new THREE.SphereGeometry(0.5, 16, 16); | |
| const sphereMaterial = new THREE.MeshBasicMaterial({ color: effectColor, transparent: true, opacity: 0.7 }); | |
| const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); | |
| sphere.position.copy(hero.position); | |
| sphere.position.y += 1; | |
| scene.add(sphere); | |
| // Damage enemies in range | |
| enemies.forEach((enemy, index) => { | |
| const distance = hero.position.distanceTo(enemy.mesh.position); | |
| if (distance < range) { | |
| enemy.health -= damage; | |
| if (enemy.health <= 0) { | |
| scene.remove(enemy.mesh); | |
| enemies.splice(index, 1); | |
| } | |
| } | |
| }); | |
| // Animate and remove sphere | |
| const scale = { val: 0.5 }; | |
| anime({ | |
| targets: scale, | |
| val: range, | |
| duration: 300, | |
| easing: 'easeOutQuad', | |
| update: () => { | |
| sphere.scale.set(scale.val, scale.val, scale.val); | |
| }, | |
| complete: () => { | |
| scene.remove(sphere); | |
| } | |
| }); | |
| } | |
| } | |
| // Power attack | |
| function powerAttack() { | |
| if (power >= 30) { | |
| power -= 30; | |
| updateUI(); | |
| // Element-based power attack | |
| let effectColor, damage, range, particles; | |
| switch(activeElement) { | |
| case 'fire': | |
| effectColor = 0xFF8C00; | |
| damage = 50; | |
| range = 5; | |
| particles = 15; | |
| break; | |
| case 'water': | |
| effectColor = 0x1E90FF; | |
| damage = 40; | |
| range = 6; | |
| particles = 20; | |
| break; | |
| case 'earth': | |
| effectColor = 0xA0522D; | |
| damage = 60; | |
| range = 4.5; | |
| particles = 10; | |
| break; | |
| case 'air': | |
| effectColor = 0xADD8E6; | |
| damage = 30; | |
| range = 7; | |
| particles = 25; | |
| break; | |
| } | |
| // Main sphere | |
| const sphereGeometry = new THREE.SphereGeometry(1, 32, 32); | |
| const sphereMaterial = new THREE.MeshBasicMaterial({ color: effectColor, transparent: true, opacity: 0.6 }); | |
| const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); | |
| sphere.position.copy(hero.position); | |
| sphere.position.y += 1; | |
| scene.add(sphere); | |
| // Create particles | |
| for (let i = 0; i < particles; i++) { | |
| const particleGeo = new THREE.SphereGeometry(0.1 + Math.random() * 0.2); | |
| const particleMat = new THREE.MeshBasicMaterial({ color: effectColor }); | |
| const particle = new THREE.Mesh(particleGeo, particleMat); | |
| // Random direction | |
| const angle = Math.random() * Math.PI * 2; | |
| const speed = 0.05 + Math.random() * 0.1; | |
| particle.userData = { | |
| direction: new THREE.Vector3( | |
| Math.cos(angle) * speed, | |
| (Math.random() - 0.5) * 0.1, | |
| Math.sin(angle) * speed | |
| ), | |
| lifetime: 0 | |
| }; | |
| particle.position.copy(hero.position); | |
| particle.position.y += 1; | |
| scene.add(particle); | |
| // Remove after animation | |
| setTimeout(() => { | |
| if (particle.parent) scene.remove(particle); | |
| }, 800); | |
| } | |
| // Damage enemies | |
| enemies.forEach((enemy, index) => { | |
| const distance = hero.position.distanceTo(enemy.mesh.position); | |
| if (distance < range) { | |
| enemy.health -= damage; | |
| if (enemy.health <= 0) { | |
| scene.remove(enemy.mesh); | |
| enemies.splice(index, 1); | |
| } else { | |
| // Knockback effect | |
| const dir = new THREE.Vector3().subVectors(enemy.mesh.position, hero.position).normalize(); | |
| enemy.mesh.position.add(dir.multiplyScalar(2)); | |
| } | |
| } | |
| }); | |
| // Animate sphere | |
| anime({ | |
| targets: sphere.scale, | |
| x: range, | |
| y: range, | |
| z: range, | |
| duration: 500, | |
| easing: 'easeOutQuad', | |
| complete: () => { | |
| scene.remove(sphere); | |
| } | |
| }); | |
| // Animate particles in game loop | |
| scene.children.forEach(child => { | |
| if (child.userData?.direction) { | |
| child.position.add(child.userData.direction); | |
| child.userData.lifetime++; | |
| child.material.opacity = 1 - (child.userData.lifetime / 50); | |
| } | |
| }); | |
| } | |
| } | |
| // Update UI | |
| function updateUI() { | |
| document.getElementById('health-text').textContent = Math.max(0, Math.floor(health)); | |
| document.getElementById('health-fill').style.width = `${Math.max(0, health)}%`; | |
| document.getElementById('power-text').textContent = Math.max(0, Math.floor(power)); | |
| document.getElementById('power-fill').style.width = `${Math.max(0, power)}%`; | |
| // Update level display | |
| const level = Math.floor(gameTime / 60000) + 1; | |
| document.getElementById('level-display').textContent = `Level: ${level}`; | |
| } | |
| // Handle controls | |
| function initControls() { | |
| // Add help key | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'h' || e.key === 'H') { | |
| const display = document.getElementById('controls-display'); | |
| display.style.display = display.style.display === 'none' ? 'block' : 'none'; | |
| } | |
| }); | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === ' ' || e.key === 'Spacebar') swordAttack(); | |
| if (e.key === 'm' || e.key === 'M') magicAttack(); | |
| if (e.key === 'p' || e.key === 'P') powerAttack(); | |
| }); | |
| if (isMobile) { | |
| document.getElementById('attack-btn').addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| swordAttack(); | |
| }); | |
| document.getElementById('power-btn').addEventListener('touchstart', (e) => { | |
| e.preventDefault(); | |
| powerAttack(); | |
| }); | |
| } | |
| } | |
| initControls(); | |
| // Show story on load | |
| document.getElementById('story-modal').classList.remove('hidden'); | |
| updateElementDisplay(); | |
| // Add controls help to story modal | |
| document.getElementById('story-text').innerHTML += ` | |
| <br><br><strong>Controls:</strong><br> | |
| Arrow Keys: Move<br> | |
| Space: Sword Attack<br> | |
| M: Magic Attack<br> | |
| P: Power Attack<br> | |
| 1-4: Change Elements (Fire/Water/Earth/Air)<br><br> | |
| Press H anytime to toggle this help. | |
| `; | |
| // Add controls display | |
| const controlsDisplay = document.createElement('div'); | |
| controlsDisplay.id = 'controls-display'; | |
| controlsDisplay.style.position = 'absolute'; | |
| controlsDisplay.style.top = '20px'; | |
| controlsDisplay.style.right = '20px'; | |
| controlsDisplay.style.color = 'white'; | |
| controlsDisplay.style.fontFamily = 'Arial'; | |
| controlsDisplay.style.fontSize = '14px'; | |
| controlsDisplay.style.backgroundColor = 'rgba(0,0,0,0.5)'; | |
| controlsDisplay.style.padding = '10px'; | |
| controlsDisplay.style.borderRadius = '5px'; | |
| controlsDisplay.innerHTML = ` | |
| <strong>Controls:</strong><br> | |
| Arrow Keys: Move<br> | |
| Space: Sword Attack<br> | |
| M: Magic Attack<br> | |
| P: Power Attack<br> | |
| 1-4: Change Elements<br> | |
| (1-Fire, 2-Water, 3-Earth, 4-Air) | |
| `; | |
| document.body.appendChild(controlsDisplay); | |
| // Toggle controls display | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'h' || e.key === 'H') { | |
| controlsDisplay.style.display = controlsDisplay.style.display === 'none' ? 'block' : 'none'; | |
| } | |
| }); | |
| // Add level display | |
| const levelDisplay = document.createElement('div'); | |
| levelDisplay.id = 'level-display'; | |
| levelDisplay.style.position = 'absolute'; | |
| levelDisplay.style.top = '20px'; | |
| levelDisplay.style.left = '20px'; | |
| levelDisplay.style.color = 'white'; | |
| levelDisplay.style.fontFamily = 'Arial'; | |
| levelDisplay.style.fontSize = '24px'; | |
| levelDisplay.textContent = 'Level: 1'; | |
| document.body.appendChild(levelDisplay); | |
| // Game loop | |
| let lastEnemySpawn = 0; | |
| let lastPowerRegen = 0; | |
| let gameTime = 0; | |
| let waveCount = 0; | |
| let animationId = null; | |
| let lastTimestamp = 0; | |
| function animate(timestamp) { | |
| if (!lastTimestamp) lastTimestamp = timestamp; | |
| const deltaTime = timestamp - lastTimestamp; | |
| lastTimestamp = timestamp; | |
| animationId = requestAnimationFrame(animate); | |
| gameTime += deltaTime; | |
| // Activate Malakar after 3 minutes | |
| if (!malakar.isActive && gameTime > 180000) { | |
| malakar.isActive = true; | |
| alert('Malakar has entered the battlefield!'); | |
| const malakarObj = createMalakar(); | |
| malakar.mesh = malakarObj.mesh; | |
| scene.add(malakar.mesh); | |
| } | |
| // Regenerate power | |
| if (time - lastPowerRegen > 1000) { | |
| power = Math.min(100, power + 5); | |
| updateUI(); | |
| lastPowerRegen = time; | |
| } | |
| // Spawn enemies in waves | |
| const waveSize = 3 + Math.floor(waveCount / 3); | |
| if (time - lastEnemySpawn > 5000 && enemies.length < waveSize && !malakar.isActive) { | |
| const spawnCount = Math.min(waveSize - enemies.length, 3); | |
| for (let i = 0; i < spawnCount; i++) { | |
| createEnemy(); | |
| } | |
| lastEnemySpawn = time; | |
| waveCount++; | |
| } | |
| // Handle hero movement | |
| const moveSpeed = heroSpeed * (deltaTime / 16); | |
| if (keys.ArrowUp) hero.position.z -= moveSpeed; | |
| if (keys.ArrowDown) hero.position.z += moveSpeed; | |
| if (keys.ArrowLeft) hero.position.x -= moveSpeed; | |
| if (keys.ArrowRight) hero.position.x += moveSpeed; | |
| // Keep hero within bounds | |
| hero.position.x = Math.max(-14, Math.min(14, hero.position.x)); | |
| hero.position.z = Math.max(-14, Math.min(14, hero.position.z)); | |
| hero.position.y = 0.9; // Keep consistent height | |
| // Sword swing animation | |
| if (isSwinging) { | |
| swingProgress += 0.1; | |
| sword.rotation.z = Math.PI/4 + Math.sin(swingProgress) * maxSwingAngle; | |
| if (swingProgress > Math.PI) { | |
| isSwinging = false; | |
| sword.rotation.z = Math.PI/4; | |
| } | |
| } | |
| // Move enemies and handle special behaviors | |
| enemies.forEach(enemy => { | |
| // Wraith floating animation | |
| if (enemy.type === 'wraith') { | |
| enemy.mesh.position.y = 1.5 + Math.sin(Date.now() * 0.002 + enemy.userData.floatOffset) * 0.3; | |
| } | |
| // Different movement patterns | |
| let direction; | |
| if (enemy.type === 'knight') { | |
| // Knights move more strategically | |
| direction = new THREE.Vector3( | |
| hero.position.x - enemy.mesh.position.x + (Math.random() - 0.5), | |
| 0, | |
| hero.position.z - enemy.mesh.position.z + (Math.random() - 0.5) | |
| ).normalize(); | |
| } else { | |
| direction = new THREE.Vector3().subVectors(hero.position, enemy.mesh.position).normalize(); | |
| } | |
| enemy.mesh.position.add(direction.multiplyScalar(enemy.speed * 0.05)); | |
| // Sorcerers occasionally teleport | |
| if (enemy.type === 'sorcerer' && Math.random() < 0.005) { | |
| enemy.mesh.position.x = hero.position.x + (Math.random() * 10 - 5); | |
| enemy.mesh.position.z = hero.position.z + (Math.random() * 10 - 5); | |
| } | |
| enemy.mesh.lookAt(hero.position); | |
| // Knights block attacks sometimes | |
| if (enemy.type === 'knight' && Math.random() < 0.01) { | |
| enemy.mesh.material.color.setHex(0xAAAAFF); // Block stance | |
| setTimeout(() => { | |
| enemy.mesh.material.color.setHex(0x888888); | |
| }, 500); | |
| } | |
| // Malakar behavior | |
| if (malakar.isActive) { | |
| // Chase player aggressively | |
| const malakarDirection = new THREE.Vector3().subVectors( | |
| hero.position, | |
| malakar.mesh.position | |
| ).normalize(); | |
| malakar.mesh.position.add(malakarDirection.multiplyScalar(0.08)); | |
| malakar.mesh.lookAt(hero.position); | |
| // Special attacks | |
| if (malakar.attackCooldown <= 0) { | |
| // Shadow blast | |
| if (Math.random() < 0.02) { | |
| const blastGeometry = new THREE.SphereGeometry(0.3, 16, 16); | |
| const blastMaterial = new THREE.MeshBasicMaterial({ color: 0x330000 }); | |
| const blast = new THREE.Mesh(blastGeometry, blastMaterial); | |
| blast.position.copy(malakar.mesh.position); | |
| blast.position.y += 1; | |
| const blastDirection = new THREE.Vector3().subVectors( | |
| hero.position, | |
| malakar.mesh.position | |
| ).normalize(); | |
| scene.add(blast); | |
| malakar.attackCooldown = 100; | |
| // Animate blast | |
| const blastSpeed = 0.2; | |
| const animateBlast = () => { | |
| blast.position.add(blastDirection.multiplyScalar(blastSpeed)); | |
| // Check hit | |
| if (blast.position.distanceTo(hero.position) < 1.5) { | |
| health -= 30; | |
| updateUI(); | |
| scene.remove(blast); | |
| return; | |
| } | |
| // Remove if out of bounds | |
| if (Math.abs(blast.position.x) > 20 || Math.abs(blast.position.z) > 20) { | |
| scene.remove(blast); | |
| return; | |
| } | |
| requestAnimationFrame(animateBlast); | |
| }; | |
| animateBlast(); | |
| } | |
| } else { | |
| malakar.attackCooldown--; | |
| } | |
| // Check collision with Malakar | |
| if (hero.position.distanceTo(malakar.mesh.position) < 2.5) { | |
| health -= 2; | |
| updateUI(); | |
| } | |
| // Sword can damage Malakar | |
| if (isSwinging && hero.position.distanceTo(malakar.mesh.position) < 3) { | |
| malakar.health -= 15; | |
| if (malakar.health <= 0) { | |
| alert('You have defeated Malakar and saved Eldoria!'); | |
| malakar.isActive = false; | |
| scene.remove(malakar.mesh); | |
| } | |
| } | |
| } | |
| // Check collision with enemies | |
| enemies.forEach(enemy => { | |
| if (hero.position.distanceTo(enemy.mesh.position) < 1.2) { | |
| health -= enemy.damage * 0.05; | |
| updateUI(); | |
| if (health <= 0) { | |
| alert('Game Over! ' + (malakar.isActive ? 'Malakar has won...' : 'Try again warrior!')); | |
| health = 100; | |
| power = 100; | |
| enemies.forEach(e => scene.remove(e.mesh)); | |
| enemies = []; | |
| if (malakar.isActive) { | |
| scene.remove(malakar.mesh); | |
| malakar.isActive = false; | |
| malakar.health = 500; | |
| } | |
| updateUI(); | |
| gameTime = 0; | |
| waveCount = 0; | |
| document.getElementById('level-display').textContent = 'Level: 1'; | |
| } | |
| } | |
| }); | |
| renderer.render(scene, camera); | |
| } | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| camera.aspect = window.innerWidth / window.innerHeight; | |
| camera.updateProjectionMatrix(); | |
| renderer.setSize(window.innerWidth, window.innerHeight); | |
| isMobile = window.innerWidth <= 768; | |
| // Adjust controls position for mobile | |
| if (isMobile) { | |
| document.getElementById('controls').style.right = '20px'; | |
| document.getElementById('controls').style.bottom = '20px'; | |
| } | |
| }); | |
| // Start game | |
| function startGame() { | |
| if (animationId) cancelAnimationFrame(animationId); | |
| health = 100; | |
| power = 100; | |
| gameTime = 0; | |
| waveCount = 0; | |
| enemies = []; | |
| if (malakar.isActive) { | |
| scene.remove(malakar.mesh); | |
| malakar.isActive = false; | |
| malakar.health = 500; | |
| } | |
| updateUI(); | |
| animationId = requestAnimationFrame(animate); | |
| // Add controls help | |
| setTimeout(() => { | |
| alert("Controls:\nArrow Keys: Move\nSpace: Sword Attack\nM: Magic Attack\nP: Power Attack\n1-4: Change Elements\nDefeat Malakar to win!"); | |
| }, 1000); | |
| } | |
| startGame(); | |
| </script> | |
| <style> | |
| /* Add sword swing animation */ | |
| @keyframes swordSwing { | |
| 0% { transform: rotate(45deg); } | |
| 50% { transform: rotate(-90deg); } | |
| 100% { transform: rotate(45deg); } | |
| } | |
| .sword-attack { | |
| animation: swordSwing 0.3s ease-in-out; | |
| } | |
| </style> | |
| </body> | |
| </html> | |