Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
<title>Doom-Style 3D Shooter</title> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/PointerLockControls.min.js"></script> | |
<style> | |
* { | |
margin: 0; | |
padding: 0; | |
box-sizing: border-box; | |
font-family: 'Courier New', monospace; | |
touch-action: none; | |
} | |
body { | |
overflow: hidden; | |
background: #000; | |
color: #ff5555; | |
height: 100vh; | |
perspective: 1px; | |
} | |
#gameContainer { | |
position: relative; | |
width: 100%; | |
height: 100vh; | |
overflow: hidden; | |
} | |
#gameCanvas { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: 1; | |
} | |
#ui { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: 2; | |
pointer-events: none; | |
} | |
#crosshair { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 30px; | |
height: 30px; | |
border: 2px solid #ff5555; | |
border-radius: 50%; | |
pointer-events: none; | |
} | |
#crosshair::before, #crosshair::after { | |
content: ''; | |
position: absolute; | |
background: #ff5555; | |
} | |
#crosshair::before { | |
width: 2px; | |
height: 10px; | |
top: -12px; | |
left: 50%; | |
transform: translateX(-50%); | |
} | |
#crosshair::after { | |
width: 10px; | |
height: 2px; | |
top: 50%; | |
right: -12px; | |
transform: translateY(-50%); | |
} | |
#healthBar { | |
position: absolute; | |
bottom: 30px; | |
left: 30px; | |
width: 200px; | |
height: 30px; | |
background: rgba(0, 0, 0, 0.7); | |
border: 2px solid #ff5555; | |
border-radius: 5px; | |
overflow: hidden; | |
} | |
#healthFill { | |
height: 100%; | |
width: 100%; | |
background: linear-gradient(90deg, #ff0000, #ff5555); | |
transition: width 0.3s; | |
} | |
#ammoCounter { | |
position: absolute; | |
bottom: 30px; | |
right: 30px; | |
font-size: 24px; | |
color: #ff5555; | |
text-shadow: 0 0 5px #ff0000; | |
background: rgba(0, 0, 0, 0.7); | |
padding: 10px 20px; | |
border: 2px solid #ff5555; | |
border-radius: 5px; | |
} | |
#score { | |
position: absolute; | |
top: 30px; | |
left: 30px; | |
font-size: 24px; | |
color: #ff5555; | |
text-shadow: 0 0 5px #ff0000; | |
} | |
#startScreen { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: linear-gradient(rgba(0, 0, 0, 0.8), rgba(20, 0, 0, 0.9)), | |
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><rect width="100" height="100" fill="%23110000"/><path d="M0 0L100 100M100 0L0 100" stroke="%23300" stroke-width="1"/></svg>'); | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 10; | |
text-align: center; | |
} | |
#title { | |
font-size: 72px; | |
margin-bottom: 30px; | |
text-shadow: 0 0 10px #ff0000, 0 0 20px #ff0000; | |
letter-spacing: 5px; | |
} | |
#subtitle { | |
font-size: 24px; | |
margin-bottom: 50px; | |
color: #ff9999; | |
} | |
#startButton { | |
background: #ff0000; | |
color: #fff; | |
border: none; | |
padding: 15px 50px; | |
font-size: 24px; | |
cursor: pointer; | |
border-radius: 5px; | |
text-transform: uppercase; | |
letter-spacing: 3px; | |
font-weight: bold; | |
box-shadow: 0 0 15px #ff0000; | |
transition: all 0.3s; | |
pointer-events: auto; | |
} | |
#startButton:hover { | |
background: #ff5555; | |
transform: scale(1.05); | |
box-shadow: 0 0 25px #ff0000; | |
} | |
#gameOverScreen { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: rgba(0, 0, 0, 0.85); | |
display: none; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 10; | |
} | |
#gameOverTitle { | |
font-size: 60px; | |
margin-bottom: 20px; | |
color: #ff0000; | |
text-shadow: 0 0 10px #ff0000; | |
} | |
#finalScore { | |
font-size: 36px; | |
margin-bottom: 40px; | |
color: #ff9999; | |
} | |
#restartButton { | |
background: #ff0000; | |
color: #fff; | |
border: none; | |
padding: 15px 50px; | |
font-size: 24px; | |
cursor: pointer; | |
border-radius: 5px; | |
text-transform: uppercase; | |
letter-spacing: 3px; | |
font-weight: bold; | |
box-shadow: 0 0 15px #ff0000; | |
transition: all 0.3s; | |
} | |
#restartButton:hover { | |
background: #ff5555; | |
transform: scale(1.05); | |
box-shadow: 0 0 25px #ff0000; | |
} | |
#weapon { | |
position: absolute; | |
bottom: 0; | |
left: 50%; | |
transform: translateX(-50%); | |
width: 300px; | |
height: 200px; | |
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 200"><rect x="50" y="100" width="200" height="30" fill="%23333"/><rect x="100" y="50" width="100" height="50" fill="%23444"/><rect x="130" y="30" width="40" height="20" fill="%23555"/><circle cx="150" cy="115" r="15" fill="%23222"/></svg>') no-repeat center bottom; | |
background-size: contain; | |
z-index: 3; | |
transition: transform 0.1s; | |
} | |
.hitEffect { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: rgba(255, 0, 0, 0.3); | |
pointer-events: none; | |
opacity: 0; | |
z-index: 4; | |
} | |
#bloodEffect { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: radial-gradient(circle, rgba(255,0,0,0.7) 0%, rgba(255,0,0,0) 70%); | |
pointer-events: none; | |
opacity: 0; | |
z-index: 5; | |
} | |
/* Mobile Controls */ | |
#mobileControls { | |
position: absolute; | |
bottom: 20px; | |
width: 100%; | |
display: none; | |
justify-content: space-between; | |
padding: 0 20px; | |
z-index: 10; | |
pointer-events: none; | |
} | |
.controlPad { | |
width: 120px; | |
height: 120px; | |
background: rgba(255, 0, 0, 0.3); | |
border-radius: 50%; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
pointer-events: auto; | |
} | |
.controlStick { | |
width: 50px; | |
height: 50px; | |
background: rgba(255, 85, 85, 0.7); | |
border-radius: 50%; | |
position: relative; | |
} | |
#shootButton { | |
width: 80px; | |
height: 80px; | |
background: rgba(255, 0, 0, 0.5); | |
border-radius: 50%; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
font-size: 24px; | |
color: white; | |
pointer-events: auto; | |
border: 2px solid #ff5555; | |
} | |
#instructions { | |
position: absolute; | |
bottom: 160px; | |
width: 100%; | |
text-align: center; | |
color: #ff9999; | |
font-size: 18px; | |
padding: 0 20px; | |
} | |
@media (max-width: 768px) { | |
#mobileControls { | |
display: flex; | |
} | |
#instructions { | |
display: block; | |
} | |
#title { | |
font-size: 48px; | |
} | |
#subtitle { | |
font-size: 18px; | |
} | |
} | |
</style> | |
</head> | |
<body> | |
<div id="gameContainer"> | |
<div id="gameCanvas"></div> | |
<div id="ui"> | |
<div id="crosshair"></div> | |
<div id="healthBar"> | |
<div id="healthFill"></div> | |
</div> | |
<div id="ammoCounter">AMMO: 30</div> | |
<div id="score">SCORE: 0</div> | |
<div id="weapon"></div> | |
<div class="hitEffect" id="hitEffect"></div> | |
<div id="bloodEffect"></div> | |
</div> | |
<div id="mobileControls"> | |
<div class="controlPad" id="movementPad"> | |
<div class="controlStick" id="movementStick"></div> | |
</div> | |
<div id="shootButton">FIRE</div> | |
</div> | |
<div id="instructions">Use joystick to move, tap FIRE to shoot</div> | |
<div id="startScreen"> | |
<h1 id="title">DEMON SLAYER</h1> | |
<p id="subtitle">Eliminate all demons to survive</p> | |
<button id="startButton">START MISSION</button> | |
</div> | |
<div id="gameOverScreen"> | |
<h1 id="gameOverTitle">MISSION FAILED</h1> | |
<p id="finalScore">SCORE: 0</p> | |
<button id="restartButton">TRY AGAIN</button> | |
</div> | |
</div> | |
<script> | |
// Game variables | |
let scene, camera, renderer, controls; | |
let player, enemies = [], bullets = []; | |
let playerHealth = 100; | |
let ammo = 30; | |
let score = 0; | |
let gameActive = false; | |
let moveForward = false; | |
let moveBackward = false; | |
let moveLeft = false; | |
let moveRight = false; | |
let canShoot = true; | |
let lastShotTime = 0; | |
let shotCooldown = 300; // ms | |
let enemySpawnTimer = 0; | |
let clock = new THREE.Clock(); | |
let isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); | |
// Mobile controls | |
let movementStickActive = false; | |
let movementStickPosition = { x: 0, y: 0 }; | |
let movementStickRadius = 35; | |
// Initialize the game | |
function init() { | |
// Create scene | |
scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x110000); | |
scene.fog = new THREE.Fog(0x110000, 10, 50); | |
// Create camera | |
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.y = 1.6; | |
// Create renderer | |
renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
document.getElementById('gameCanvas').appendChild(renderer.domElement); | |
// Add lighting | |
const ambientLight = new THREE.AmbientLight(0x404040, 0.5); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xff5555, 0.8); | |
directionalLight.position.set(10, 20, 10); | |
directionalLight.castShadow = true; | |
scene.add(directionalLight); | |
// Create floor | |
const floorGeometry = new THREE.PlaneGeometry(100, 100); | |
const floorMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x222222, | |
roughness: 0.8, | |
metalness: 0.2 | |
}); | |
const floor = new THREE.Mesh(floorGeometry, floorMaterial); | |
floor.rotation.x = -Math.PI / 2; | |
floor.receiveShadow = true; | |
scene.add(floor); | |
// Create walls | |
createWalls(); | |
// Create pillars | |
createPillars(); | |
// Create player | |
player = new THREE.Object3D(); | |
player.position.set(0, 1.6, 0); | |
scene.add(player); | |
player.add(camera); | |
// Add pointer lock controls for desktop | |
if (!isMobile) { | |
controls = new THREE.PointerLockControls(camera, document.body); | |
} | |
// Event listeners | |
if (!isMobile) { | |
document.addEventListener('keydown', onKeyDown, false); | |
document.addEventListener('keyup', onKeyUp, false); | |
document.addEventListener('mousedown', onMouseDown, false); | |
document.addEventListener('mousemove', onMouseMove, false); | |
} else { | |
setupMobileControls(); | |
} | |
window.addEventListener('resize', onWindowResize, false); | |
// Start button event | |
document.getElementById('startButton').addEventListener('click', startGame); | |
document.getElementById('restartButton').addEventListener('click', restartGame); | |
// Start animation loop | |
animate(); | |
} | |
function createWalls() { | |
const wallMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x330000, | |
roughness: 0.9, | |
metalness: 0.1 | |
}); | |
// Create a simple maze-like structure | |
const walls = [ | |
// Outer walls | |
{ x: 0, y: 2, z: -25, width: 50, height: 4, depth: 1 }, | |
{ x: 0, y: 2, z: 25, width: 50, height: 4, depth: 1 }, | |
{ x: -25, y: 2, z: 0, width: 1, height: 4, depth: 50 }, | |
{ x: 25, y: 2, z: 0, width: 1, height: 4, depth: 50 }, | |
// Inner walls | |
{ x: -10, y: 2, z: -10, width: 1, height: 4, depth: 20 }, | |
{ x: 10, y: 2, z: 10, width: 1, height: 4, depth: 20 }, | |
{ x: 0, y: 2, z: 0, width: 20, height: 4, depth: 1 }, | |
{ x: 0, y: 2, z: -20, width: 20, height: 4, depth: 1 } | |
]; | |
walls.forEach(wall => { | |
const wallGeometry = new THREE.BoxGeometry(wall.width, wall.height, wall.depth); | |
const wallMesh = new THREE.Mesh(wallGeometry, wallMaterial); | |
wallMesh.position.set(wall.x, wall.y, wall.z); | |
wallMesh.castShadow = true; | |
wallMesh.receiveShadow = true; | |
scene.add(wallMesh); | |
}); | |
} | |
function createPillars() { | |
const pillarMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x440000, | |
roughness: 0.7, | |
metalness: 0.3 | |
}); | |
const pillarGeometry = new THREE.CylinderGeometry(1, 1, 4, 16); | |
const positions = [ | |
{ x: -20, z: -20 }, | |
{ x: 20, z: -20 }, | |
{ x: -20, z: 20 }, | |
{ x: 20, z: 20 }, | |
{ x: 0, z: -15 }, | |
{ x: 0, z: 15 }, | |
{ x: -15, z: 0 }, | |
{ x: 15, z: 0 } | |
]; | |
positions.forEach(pos => { | |
const pillar = new THREE.Mesh(pillarGeometry, pillarMaterial); | |
pillar.position.set(pos.x, 2, pos.z); | |
pillar.castShadow = true; | |
pillar.receiveShadow = true; | |
scene.add(pillar); | |
}); | |
} | |
// Create a detailed demon model | |
function createDemon() { | |
const demonGroup = new THREE.Group(); | |
// Body | |
const bodyGeometry = new THREE.SphereGeometry(0.8, 16, 16); | |
const bodyMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x880000, | |
roughness: 0.7, | |
metalness: 0.3 | |
}); | |
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
body.position.y = 1; | |
body.castShadow = true; | |
demonGroup.add(body); | |
// Head | |
const headGeometry = new THREE.SphereGeometry(0.6, 16, 16); | |
const headMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xaa0000, | |
roughness: 0.7, | |
metalness: 0.3 | |
}); | |
const head = new THREE.Mesh(headGeometry, headMaterial); | |
head.position.y = 1.8; | |
head.castShadow = true; | |
demonGroup.add(head); | |
// Horns | |
const hornGeometry = new THREE.ConeGeometry(0.1, 0.8, 8); | |
const hornMaterial = new THREE.MeshStandardMaterial({ color: 0x333333 }); | |
const horn1 = new THREE.Mesh(hornGeometry, hornMaterial); | |
horn1.position.set(-0.2, 2.3, 0); | |
horn1.rotation.z = Math.PI / 6; | |
horn1.castShadow = true; | |
demonGroup.add(horn1); | |
const horn2 = new THREE.Mesh(hornGeometry, hornMaterial); | |
horn2.position.set(0.2, 2.3, 0); | |
horn2.rotation.z = -Math.PI / 6; | |
horn2.castShadow = true; | |
demonGroup.add(horn2); | |
// Eyes | |
const eyeGeometry = new THREE.SphereGeometry(0.15, 8, 8); | |
const eyeMaterial = new THREE.MeshStandardMaterial({ color: 0xffff00 }); | |
const eye1 = new THREE.Mesh(eyeGeometry, eyeMaterial); | |
eye1.position.set(-0.25, 1.9, 0.4); | |
demonGroup.add(eye1); | |
const eye2 = new THREE.Mesh(eyeGeometry, eyeMaterial); | |
eye2.position.set(0.25, 1.9, 0.4); | |
demonGroup.add(eye2); | |
// Arms | |
const armGeometry = new THREE.CylinderGeometry(0.15, 0.15, 1.2, 8); | |
const armMaterial = new THREE.MeshStandardMaterial({ color: 0x770000 }); | |
const arm1 = new THREE.Mesh(armGeometry, armMaterial); | |
arm1.position.set(-0.9, 1, 0); | |
arm1.rotation.z = Math.PI / 4; | |
arm1.castShadow = true; | |
demonGroup.add(arm1); | |
const arm2 = new THREE.Mesh(armGeometry, armMaterial); | |
arm2.position.set(0.9, 1, 0); | |
arm2.rotation.z = -Math.PI / 4; | |
arm2.castShadow = true; | |
demonGroup.add(arm2); | |
// Legs | |
const legGeometry = new THREE.CylinderGeometry(0.2, 0.2, 1, 8); | |
const legMaterial = new THREE.MeshStandardMaterial({ color: 0x660000 }); | |
const leg1 = new THREE.Mesh(legGeometry, legMaterial); | |
leg1.position.set(-0.4, 0.3, 0); | |
leg1.castShadow = true; | |
demonGroup.add(leg1); | |
const leg2 = new THREE.Mesh(legGeometry, legMaterial); | |
leg2.position.set(0.4, 0.3, 0); | |
leg2.castShadow = true; | |
demonGroup.add(leg2); | |
return demonGroup; | |
} | |
function spawnEnemy() { | |
const demon = createDemon(); | |
demon.position.set( | |
(Math.random() - 0.5) * 40, | |
0, | |
(Math.random() - 0.5) * 40 | |
); | |
demon.rotation.y = Math.random() * Math.PI * 2; | |
scene.add(demon); | |
enemies.push({ | |
mesh: demon, | |
health: 100, | |
speed: 0.02 + Math.random() * 0.03, | |
lastAttack: 0, | |
attackCooldown: 1000 + Math.random() * 2000 | |
}); | |
} | |
function startGame() { | |
document.getElementById('startScreen').style.display = 'none'; | |
if (!isMobile) { | |
controls.lock(); | |
} | |
gameActive = true; | |
// Spawn initial enemies | |
for (let i = 0; i < 5; i++) { | |
spawnEnemy(); | |
} | |
} | |
function restartGame() { | |
document.getElementById('gameOverScreen').style.display = 'none'; | |
playerHealth = 100; | |
ammo = 30; | |
score = 0; | |
updateUI(); | |
// Remove all enemies | |
enemies.forEach(enemy => { | |
scene.remove(enemy.mesh); | |
}); | |
enemies = []; | |
// Remove all bullets | |
bullets.forEach(bullet => { | |
scene.remove(bullet.mesh); | |
}); | |
bullets = []; | |
// Reset player position | |
player.position.set(0, 1.6, 0); | |
// Spawn initial enemies | |
for (let i = 0; i < 5; i++) { | |
spawnEnemy(); | |
} | |
if (!isMobile) { | |
controls.lock(); | |
} | |
gameActive = true; | |
} | |
function gameOver() { | |
gameActive = false; | |
if (!isMobile) { | |
controls.unlock(); | |
} | |
document.getElementById('finalScore').textContent = `SCORE: ${score}`; | |
document.getElementById('gameOverScreen').style.display = 'flex'; | |
} | |
function updateUI() { | |
document.getElementById('healthFill').style.width = `${playerHealth}%`; | |
document.getElementById('ammoCounter').textContent = `AMMO: ${ammo}`; | |
document.getElementById('score').textContent = `SCORE: ${score}`; | |
} | |
function onKeyDown(event) { | |
switch (event.code) { | |
case 'KeyW': moveForward = true; break; | |
case 'KeyS': moveBackward = true; break; | |
case 'KeyA': moveLeft = true; break; | |
case 'KeyD': moveRight = true; break; | |
} | |
} | |
function onKeyUp(event) { | |
switch (event.code) { | |
case 'KeyW': moveForward = false; break; | |
case 'KeyS': moveBackward = false; break; | |
case 'KeyA': moveLeft = false; break; | |
case 'KeyD': moveRight = false; break; | |
} | |
} | |
function onMouseDown() { | |
if (!gameActive) return; | |
if (ammo > 0 && canShoot) { | |
shoot(); | |
canShoot = false; | |
setTimeout(() => { canShoot = true; }, shotCooldown); | |
} | |
} | |
function onMouseMove(event) { | |
if (!gameActive || isMobile) return; | |
// Add weapon sway effect | |
const weapon = document.getElementById('weapon'); | |
const moveX = (event.movementX || 0) * 0.05; | |
const moveY = (event.movementY || 0) * 0.05; | |
weapon.style.transform = `translateX(calc(-50% + ${moveX}px)) translateY(${moveY}px)`; | |
} | |
function shoot() { | |
ammo--; | |
updateUI(); | |
// Create bullet | |
const bulletGeometry = new THREE.SphereGeometry(0.1, 8, 8); | |
const bulletMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 }); | |
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial); | |
// Position bullet at camera | |
bullet.position.set(0, 0, 0); | |
camera.getWorldPosition(bullet.position); | |
// Direction vector | |
const direction = new THREE.Vector3(0, 0, -1); | |
direction.applyQuaternion(camera.quaternion); | |
scene.add(bullet); | |
bullets.push({ | |
mesh: bullet, | |
direction: direction, | |
speed: 1.0, | |
distance: 0 | |
}); | |
// Weapon recoil effect | |
const weapon = document.getElementById('weapon'); | |
weapon.style.transform = 'translateX(-50%) translateY(20px)'; | |
setTimeout(() => { | |
weapon.style.transform = 'translateX(-50%) translateY(0)'; | |
}, 100); | |
// Muzzle flash effect | |
const flash = document.createElement('div'); | |
flash.style.position = 'absolute'; | |
flash.style.width = '50px'; | |
flash.style.height = '50px'; | |
flash.style.background = 'radial-gradient(circle, #ffff00, #ff5500, transparent)'; | |
flash.style.borderRadius = '50%'; | |
flash.style.top = '50%'; | |
flash.style.left = '50%'; | |
flash.style.transform = 'translate(-50%, -50%)'; | |
flash.style.pointerEvents = 'none'; | |
flash.style.zIndex = '10'; | |
document.getElementById('ui').appendChild(flash); | |
setTimeout(() => { | |
document.getElementById('ui').removeChild(flash); | |
}, 50); | |
} | |
function setupMobileControls() { | |
const movementPad = document.getElementById('movementPad'); | |
const movementStick = document.getElementById('movementStick'); | |
const shootButton = document.getElementById('shootButton'); | |
// Movement pad touch events | |
movementPad.addEventListener('touchstart', (e) => { | |
movementStickActive = true; | |
updateMovementStick(e.touches[0]); | |
}); | |
movementPad.addEventListener('touchmove', (e) => { | |
if (movementStickActive) { | |
e.preventDefault(); | |
updateMovementStick(e.touches[0]); | |
} | |
}); | |
movementPad.addEventListener('touchend', () => { | |
movementStickActive = false; | |
movementStickPosition = { x: 0, y: 0 }; | |
movementStick.style.transform = 'translate(0, 0)'; | |
moveForward = false; | |
moveBackward = false; | |
moveLeft = false; | |
moveRight = false; | |
}); | |
// Shoot button touch events | |
shootButton.addEventListener('touchstart', (e) => { | |
e.preventDefault(); | |
if (gameActive && ammo > 0 && canShoot) { | |
shoot(); | |
canShoot = false; | |
setTimeout(() => { canShoot = true; }, shotCooldown); | |
} | |
}); | |
function updateMovementStick(touch) { | |
const padRect = movementPad.getBoundingClientRect(); | |
const centerX = padRect.left + padRect.width / 2; | |
const centerY = padRect.top + padRect.height / 2; | |
let deltaX = touch.clientX - centerX; | |
let deltaY = touch.clientY - centerY; | |
// Limit to pad radius | |
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY); | |
if (distance > movementStickRadius) { | |
deltaX = deltaX * movementStickRadius / distance; | |
deltaY = deltaY * movementStickRadius / distance; | |
} | |
movementStickPosition = { x: deltaX, y: deltaY }; | |
movementStick.style.transform = `translate(${deltaX}px, ${deltaY}px)`; | |
// Update movement flags | |
moveForward = deltaY < -10; | |
moveBackward = deltaY > 10; | |
moveLeft = deltaX < -10; | |
moveRight = deltaX > 10; | |
} | |
} | |
function onWindowResize() { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
} | |
function animate() { | |
requestAnimationFrame(animate); | |
const delta = clock.getDelta(); | |
if (gameActive) { | |
// Player movement | |
const speed = 0.1; | |
if (isMobile) { | |
// Mobile movement based on joystick | |
if (moveForward) player.translateZ(-speed); | |
if (moveBackward) player.translateZ(speed); | |
if (moveLeft) player.translateX(-speed); | |
if (moveRight) player.translateX(speed); | |
} else { | |
// Desktop movement | |
if (moveForward) player.translateZ(-speed); | |
if (moveBackward) player.translateZ(speed); | |
if (moveLeft) player.translateX(-speed); | |
if (moveRight) player.translateX(speed); | |
} | |
// Keep player within bounds | |
player.position.x = Math.max(-24, Math.min(24, player.position.x)); | |
player.position.z = Math.max(-24, Math.min(24, player.position.z)); | |
// Enemy AI | |
enemies.forEach((enemy, index) => { | |
// Move towards player | |
const direction = new THREE.Vector3(); | |
direction.subVectors(player.position, enemy.mesh.position).normalize(); | |
enemy.mesh.position.add(direction.multiplyScalar(enemy.speed)); | |
// Rotate to face player | |
enemy.mesh.lookAt(player.position); | |
enemy.mesh.rotation.x = 0; // Keep upright | |
enemy.mesh.rotation.z = 0; | |
// Attack player if close | |
const distance = player.position.distanceTo(enemy.mesh.position); | |
if (distance < 3 && Date.now() - enemy.lastAttack > enemy.attackCooldown) { | |
playerHealth -= 10; | |
updateUI(); | |
enemy.lastAttack = Date.now(); | |
// Show hit effect | |
const hitEffect = document.getElementById('hitEffect'); | |
hitEffect.style.opacity = '1'; | |
setTimeout(() => { | |
hitEffect.style.opacity = '0'; | |
}, 100); | |
// Show blood effect | |
const bloodEffect = document.getElementById('bloodEffect'); | |
bloodEffect.style.opacity = '0.7'; | |
setTimeout(() => { | |
bloodEffect.style.opacity = '0'; | |
}, 200); | |
if (playerHealth <= 0) { | |
gameOver(); | |
} | |
} | |
// Remove dead enemies | |
if (enemy.health <= 0) { | |
scene.remove(enemy.mesh); | |
enemies.splice(index, 1); | |
score += 100; | |
updateUI(); | |
// Spawn new enemy | |
if (Math.random() > 0.7) { | |
spawnEnemy(); | |
} | |
} | |
}); | |
// Update bullets | |
for (let i = bullets.length - 1; i >= 0; i--) { | |
const bullet = bullets[i]; | |
bullet.mesh.position.add(bullet.direction.clone().multiplyScalar(bullet.speed)); | |
bullet.distance += bullet.speed; | |
// Remove bullets that travel too far | |
if (bullet.distance > 50) { | |
scene.remove(bullet.mesh); | |
bullets.splice(i, 1); | |
continue; | |
} | |
// Check for collisions with enemies | |
for (let j = enemies.length - 1; j >= 0; j--) { | |
const enemy = enemies[j]; | |
const distance = bullet.mesh.position.distanceTo(enemy.mesh.position); | |
if (distance < 1.5) { | |
// Hit enemy | |
enemy.health -= 25; | |
scene.remove(bullet.mesh); | |
bullets.splice(i, 1); | |
// Enemy hit effect | |
enemy.mesh.children[0].material.color.set(0xff0000); | |
setTimeout(() => { | |
if (enemy.mesh && enemy.mesh.children[0]) { | |
enemy.mesh.children[0].material.color.set(0x880000); | |
} | |
}, 100); | |
break; | |
} | |
} | |
} | |
// Spawn enemies periodically | |
enemySpawnTimer += delta; | |
if (enemySpawnTimer > 5) { | |
spawnEnemy(); | |
enemySpawnTimer = 0; | |
} | |
} | |
renderer.render(scene, camera); | |
} | |
// Initialize the game when the page loads | |
window.onload = init; | |
</script> | |
</body> | |
</html> |