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> |