Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Missile Defense Arcade</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/controls/OrbitControls.min.js"></script> | |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/examples/js/loaders/GLTFLoader.min.js"></script> | |
<style> | |
body { | |
margin: 0; | |
overflow: hidden; | |
font-family: 'Arial', sans-serif; | |
background-color: #111; | |
} | |
#canvas-container { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
} | |
#ui { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
z-index: 100; | |
color: white; | |
text-shadow: 1px 1px 3px black; | |
pointer-events: none; | |
} | |
#flag { | |
position: absolute; | |
width: 40px; | |
height: 60px; | |
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="red" d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6z"/></svg>'); | |
background-size: contain; | |
background-repeat: no-repeat; | |
cursor: move; | |
z-index: 50; | |
user-select: none; | |
} | |
#launch-btn { | |
position: absolute; | |
bottom: 30px; | |
left: 50%; | |
transform: translateX(-50%); | |
padding: 15px 30px; | |
background: linear-gradient(to right, #ff5e62, #ff9966); | |
color: white; | |
border: none; | |
border-radius: 30px; | |
font-size: 20px; | |
font-weight: bold; | |
cursor: pointer; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.3); | |
z-index: 100; | |
transition: all 0.3s; | |
text-transform: uppercase; | |
letter-spacing: 1px; | |
} | |
#launch-btn:hover { | |
transform: translateX(-50%) scale(1.05); | |
box-shadow: 0 6px 20px rgba(0,0,0,0.4); | |
background: linear-gradient(to right, #ff5156, #ff8a5c); | |
} | |
#launch-btn:active { | |
transform: translateX(-50%) scale(0.98); | |
} | |
#launch-btn:disabled { | |
background: #666; | |
transform: translateX(-50%); | |
cursor: not-allowed; | |
} | |
.explosion-particle { | |
position: absolute; | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
pointer-events: none; | |
} | |
#score-panel { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
color: white; | |
font-size: 18px; | |
text-shadow: 1px 1px 3px black; | |
z-index: 100; | |
background: rgba(0,0,0,0.5); | |
padding: 15px; | |
border-radius: 10px; | |
border: 2px solid rgba(255,255,255,0.2); | |
} | |
#message { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background-color: rgba(0,0,0,0.8); | |
color: white; | |
padding: 25px 50px; | |
border-radius: 15px; | |
font-size: 28px; | |
font-weight: bold; | |
opacity: 0; | |
transition: opacity 0.5s; | |
pointer-events: none; | |
z-index: 200; | |
text-align: center; | |
border: 3px solid rgba(255,255,255,0.3); | |
text-shadow: 0 0 10px rgba(255,0,0,0.7); | |
} | |
#health-bar { | |
position: absolute; | |
bottom: 100px; | |
left: 50%; | |
transform: translateX(-50%); | |
width: 300px; | |
height: 20px; | |
background: rgba(255,255,255,0.2); | |
border-radius: 10px; | |
overflow: hidden; | |
z-index: 100; | |
} | |
#health-fill { | |
height: 100%; | |
width: 100%; | |
background: linear-gradient(to right, #4CAF50, #8BC34A); | |
transition: width 0.3s; | |
} | |
#game-over { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: rgba(0,0,0,0.8); | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 300; | |
color: white; | |
opacity: 0; | |
pointer-events: none; | |
transition: opacity 0.5s; | |
} | |
#game-over.show { | |
opacity: 1; | |
pointer-events: all; | |
} | |
#start-screen { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
background: rgba(0,0,0,0.9); | |
display: flex; | |
flex-direction: column; | |
justify-content: center; | |
align-items: center; | |
z-index: 400; | |
color: white; | |
} | |
#ammo-counter { | |
position: absolute; | |
bottom: 80px; | |
left: 50%; | |
transform: translateX(-50%); | |
color: white; | |
font-size: 18px; | |
text-shadow: 1px 1px 3px black; | |
z-index: 100; | |
background: rgba(0,0,0,0.5); | |
padding: 10px 20px; | |
border-radius: 30px; | |
display: flex; | |
align-items: center; | |
gap: 10px; | |
} | |
.power-up { | |
position: absolute; | |
width: 30px; | |
height: 30px; | |
background-color: gold; | |
border-radius: 50%; | |
z-index: 50; | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
font-weight: bold; | |
color: black; | |
cursor: pointer; | |
box-shadow: 0 0 10px gold; | |
animation: pulse 1.5s infinite; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); } | |
50% { transform: scale(1.2); } | |
100% { transform: scale(1); } | |
} | |
#wave-indicator { | |
position: absolute; | |
top: 100px; | |
left: 50%; | |
transform: translateX(-50%); | |
background: rgba(0,0,0,0.7); | |
color: white; | |
padding: 10px 20px; | |
border-radius: 30px; | |
font-size: 18px; | |
z-index: 100; | |
opacity: 0; | |
transition: opacity 0.3s; | |
} | |
#difficulty-info { | |
position: absolute; | |
bottom: 20px; | |
left: 20px; | |
color: white; | |
background: rgba(0,0,0,0.5); | |
padding: 10px; | |
border-radius: 5px; | |
font-size: 14px; | |
z-index: 100; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="start-screen"> | |
<h1 class="text-6xl font-bold mb-8 text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-red-500">MISSILE DEFENSE ARCADE</h1> | |
<p class="text-xl mb-12 max-w-2xl text-center">Protect your base from enemy tanks! Drag the flag to target locations and launch missiles to destroy incoming threats.</p> | |
<button id="start-btn" class="px-12 py-4 bg-gradient-to-r from-green-500 to-blue-500 text-white text-2xl font-bold rounded-full hover:from-green-600 hover:to-blue-600 transition-all transform hover:scale-105 shadow-lg"> | |
START MISSION | |
</button> | |
<div class="mt-16 text-gray-400"> | |
<p>Controls:</p> | |
<p>- Drag flag to set target</p> | |
<p>- Click LAUNCH button or press SPACE to fire</p> | |
<p>- Collect power-ups for special abilities</p> | |
</div> | |
</div> | |
<div id="canvas-container"></div> | |
<div id="ui"> | |
<h1 class="text-4xl font-bold mb-2 text-transparent bg-clip-text bg-gradient-to-r from-yellow-400 to-red-500">MISSILE DEFENSE</h1> | |
<p class="text-lg">Drag flag to target, then LAUNCH!</p> | |
</div> | |
<div id="score-panel"> | |
<div>Score: <span id="score">0</span></div> | |
<div>Wave: <span id="wave">1</span></div> | |
<div>Tanks Destroyed: <span id="tank-count">0</span></div> | |
<div>Missiles Fired: <span id="rocket-count">0</span></div> | |
<div>Accuracy: <span id="accuracy">0</span>%</div> | |
</div> | |
<div id="health-bar"> | |
<div id="health-fill"></div> | |
</div> | |
<div id="ammo-counter"> | |
<span id="ammo-count">10</span> Missiles | |
</div> | |
<button id="launch-btn" disabled>LAUNCH MISSILE</button> | |
<div id="message"></div> | |
<div id="flag"></div> | |
<div id="wave-indicator">Wave <span id="wave-number">1</span> Incoming!</div> | |
<div id="difficulty-info">Easier Mode: Tanks come from farther away and move slower</div> | |
<div id="game-over"> | |
<h1 class="text-6xl font-bold mb-8 text-red-500">MISSION FAILED</h1> | |
<div class="text-2xl mb-8">Final Score: <span id="final-score">0</span></div> | |
<button id="restart-btn" class="px-12 py-4 bg-gradient-to-r from-green-500 to-blue-500 text-white text-2xl font-bold rounded-full hover:from-green-600 hover:to-blue-600 transition-all transform hover:scale-105 shadow-lg"> | |
TRY AGAIN | |
</button> | |
</div> | |
<script> | |
// Game state | |
const gameState = { | |
started: false, | |
score: 0, | |
wave: 1, | |
tanksDestroyed: 0, | |
rocketsFired: 0, | |
hits: 0, | |
baseHealth: 100, | |
ammo: 10, | |
maxAmmo: 10, | |
reloading: false, | |
gameOver: false, | |
powerUps: [] | |
}; | |
// DOM elements | |
const startScreen = document.getElementById('start-screen'); | |
const startBtn = document.getElementById('start-btn'); | |
const gameOverScreen = document.getElementById('game-over'); | |
const restartBtn = document.getElementById('restart-btn'); | |
const scoreElement = document.getElementById('score'); | |
const waveElement = document.getElementById('wave'); | |
const tankCountElement = document.getElementById('tank-count'); | |
const rocketCountElement = document.getElementById('rocket-count'); | |
const accuracyElement = document.getElementById('accuracy'); | |
const finalScoreElement = document.getElementById('final-score'); | |
const healthFill = document.getElementById('health-fill'); | |
const ammoCountElement = document.getElementById('ammo-count'); | |
const launchBtn = document.getElementById('launch-btn'); | |
const waveIndicator = document.getElementById('wave-indicator'); | |
const waveNumberElement = document.getElementById('wave-number'); | |
// Scene setup | |
const scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x111122); | |
scene.fog = new THREE.FogExp2(0x111133, 0.002); | |
// Camera | |
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.set(0, 15, 30); | |
// Renderer | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
renderer.shadowMap.type = THREE.PCFSoftShadowMap; | |
document.getElementById('canvas-container').appendChild(renderer.domElement); | |
// Controls | |
const controls = new THREE.OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
controls.minDistance = 10; | |
controls.maxDistance = 100; | |
controls.enablePan = false; | |
// Lights | |
const ambientLight = new THREE.AmbientLight(0x404040); | |
scene.add(ambientLight); | |
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); | |
directionalLight.position.set(10, 20, 10); | |
directionalLight.castShadow = true; | |
directionalLight.shadow.mapSize.width = 2048; | |
directionalLight.shadow.mapSize.height = 2048; | |
directionalLight.shadow.camera.near = 0.5; | |
directionalLight.shadow.camera.far = 500; | |
directionalLight.shadow.camera.left = -50; | |
directionalLight.shadow.camera.right = 50; | |
directionalLight.shadow.camera.top = 50; | |
directionalLight.shadow.camera.bottom = -50; | |
scene.add(directionalLight); | |
// Moon light | |
const moonLight = new THREE.DirectionalLight(0x5577aa, 0.3); | |
moonLight.position.set(-10, 20, -10); | |
scene.add(moonLight); | |
// Ground | |
const groundGeometry = new THREE.PlaneGeometry(300, 300); // Larger ground area | |
const groundMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x2a4a0b, | |
roughness: 0.8, | |
metalness: 0.2 | |
}); | |
const ground = new THREE.Mesh(groundGeometry, groundMaterial); | |
ground.rotation.x = -Math.PI / 2; | |
ground.receiveShadow = true; | |
scene.add(ground); | |
// Base (player structure to protect) | |
const baseGroup = new THREE.Group(); | |
// Main building | |
const baseGeometry = new THREE.BoxGeometry(6, 4, 6); | |
const baseMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x555577, | |
roughness: 0.7, | |
metalness: 0.4 | |
}); | |
const baseBuilding = new THREE.Mesh(baseGeometry, baseMaterial); | |
baseBuilding.position.y = 2; | |
baseBuilding.castShadow = true; | |
baseBuilding.receiveShadow = true; | |
baseGroup.add(baseBuilding); | |
// Radar dish | |
const radarGeometry = new THREE.CylinderGeometry(0.5, 2, 0.2, 32); | |
const radarMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x9999aa, | |
roughness: 0.5, | |
metalness: 0.6 | |
}); | |
const radar = new THREE.Mesh(radarGeometry, radarMaterial); | |
radar.position.set(0, 5, 0); | |
radar.rotation.x = Math.PI / 4; | |
radar.castShadow = true; | |
radar.receiveShadow = true; | |
baseGroup.add(radar); | |
// Antenna | |
const antennaGeometry = new THREE.CylinderGeometry(0.1, 0.1, 3, 16); | |
const antennaMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x777799, | |
roughness: 0.4, | |
metalness: 0.7 | |
}); | |
const antenna = new THREE.Mesh(antennaGeometry, antennaMaterial); | |
antenna.position.set(0, 6.5, 0); | |
antenna.castShadow = true; | |
baseGroup.add(antenna); | |
// Position base | |
baseGroup.position.set(0, 0, 0); | |
scene.add(baseGroup); | |
// Launch pad | |
const padGeometry = new THREE.CylinderGeometry(3, 3, 0.5, 32); | |
const padMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x555555, | |
roughness: 0.7, | |
metalness: 0.3 | |
}); | |
const launchPad = new THREE.Mesh(padGeometry, padMaterial); | |
launchPad.position.set(0, 0.25, 0); | |
launchPad.receiveShadow = true; | |
scene.add(launchPad); | |
// Game objects | |
let activeRockets = []; | |
let smokeParticles = []; | |
let tanks = []; | |
const messageElement = document.getElementById('message'); | |
// Show message | |
function showMessage(text, duration = 2000) { | |
messageElement.textContent = text; | |
messageElement.style.opacity = 1; | |
setTimeout(() => { | |
messageElement.style.opacity = 0; | |
}, duration); | |
} | |
// Show wave indicator | |
function showWaveIndicator(wave) { | |
waveNumberElement.textContent = wave; | |
waveIndicator.style.opacity = 1; | |
setTimeout(() => { | |
waveIndicator.style.opacity = 0; | |
}, 3000); | |
} | |
// Update UI | |
function updateUI() { | |
scoreElement.textContent = gameState.score; | |
waveElement.textContent = gameState.wave; | |
tankCountElement.textContent = gameState.tanksDestroyed; | |
rocketCountElement.textContent = gameState.rocketsFired; | |
// Calculate accuracy | |
const accuracy = gameState.rocketsFired > 0 | |
? Math.round((gameState.hits / gameState.rocketsFired) * 100) | |
: 0; | |
accuracyElement.textContent = accuracy; | |
// Update health bar | |
healthFill.style.width = `${gameState.baseHealth}%`; | |
if (gameState.baseHealth < 30) { | |
healthFill.style.background = 'linear-gradient(to right, #F44336, #E91E63)'; | |
} else if (gameState.baseHealth < 60) { | |
healthFill.style.background = 'linear-gradient(to right, #FF9800, #FF5722)'; | |
} else { | |
healthFill.style.background = 'linear-gradient(to right, #4CAF50, #8BC34A)'; | |
} | |
// Update ammo counter | |
ammoCountElement.textContent = gameState.ammo; | |
if (gameState.ammo === 0) { | |
launchBtn.disabled = true; | |
if (!gameState.reloading) { | |
gameState.reloading = true; | |
setTimeout(() => { | |
gameState.ammo = gameState.maxAmmo; | |
gameState.reloading = false; | |
launchBtn.disabled = false; | |
showMessage("Reloaded!"); | |
}, 2000); | |
} | |
} else { | |
launchBtn.disabled = false; | |
} | |
} | |
// Create a detailed tank | |
function createTank(position, isEnemy = true) { | |
const tankGroup = new THREE.Group(); | |
tankGroup.position.copy(position); | |
tankGroup.userData = { | |
isAlive: true, | |
health: 100, | |
isEnemy: isEnemy, | |
speed: 0.2 + Math.random() * 0.2, // Reduced speed | |
damage: 5, | |
lastAttack: 0 | |
}; | |
// Tank body | |
const bodyColor = isEnemy ? 0x556b2f : 0x4a6b8a; | |
const bodyGeometry = new THREE.BoxGeometry(3, 1.5, 5); | |
const bodyMaterial = new THREE.MeshStandardMaterial({ | |
color: bodyColor, | |
roughness: 0.8, | |
metalness: 0.3 | |
}); | |
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
body.position.y = 1; | |
body.castShadow = true; | |
body.receiveShadow = true; | |
tankGroup.add(body); | |
// Tank turret | |
const turretGeometry = new THREE.CylinderGeometry(1.2, 1.2, 1, 32); | |
const turretMaterial = new THREE.MeshStandardMaterial({ | |
color: bodyColor, | |
roughness: 0.8, | |
metalness: 0.3 | |
}); | |
const turret = new THREE.Mesh(turretGeometry, turretMaterial); | |
turret.position.y = 2.2; | |
turret.rotation.x = Math.PI / 2; | |
turret.castShadow = true; | |
turret.receiveShadow = true; | |
tankGroup.add(turret); | |
// Tank gun | |
const gunGeometry = new THREE.CylinderGeometry(0.3, 0.3, 4, 32); | |
const gunMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x444444, | |
roughness: 0.6, | |
metalness: 0.4 | |
}); | |
const gun = new THREE.Mesh(gunGeometry, gunMaterial); | |
gun.position.y = 2.2; | |
gun.position.z = -2; | |
gun.rotation.x = Math.PI / 2; | |
gun.castShadow = true; | |
gun.receiveShadow = true; | |
tankGroup.add(gun); | |
// Tank tracks (left) | |
const leftTrackGeometry = new THREE.BoxGeometry(3.5, 0.8, 5.5); | |
const leftTrackMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x333333, | |
roughness: 0.9, | |
metalness: 0.1 | |
}); | |
const leftTrack = new THREE.Mesh(leftTrackGeometry, leftTrackMaterial); | |
leftTrack.position.set(-1.8, 0.5, 0); | |
leftTrack.castShadow = true; | |
leftTrack.receiveShadow = true; | |
tankGroup.add(leftTrack); | |
// Tank tracks (right) | |
const rightTrackGeometry = new THREE.BoxGeometry(3.5, 0.8, 5.5); | |
const rightTrackMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x333333, | |
roughness: 0.9, | |
metalness: 0.1 | |
}); | |
const rightTrack = new THREE.Mesh(rightTrackGeometry, rightTrackMaterial); | |
rightTrack.position.set(1.8, 0.5, 0); | |
rightTrack.castShadow = true; | |
rightTrack.receiveShadow = true; | |
tankGroup.add(rightTrack); | |
// Tank details (hatch) | |
const hatchGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.2, 32); | |
const hatchMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x666666, | |
roughness: 0.7, | |
metalness: 0.5 | |
}); | |
const hatch = new THREE.Mesh(hatchGeometry, hatchMaterial); | |
hatch.position.set(0, 2.5, -1); | |
hatch.castShadow = true; | |
hatch.receiveShadow = true; | |
tankGroup.add(hatch); | |
// Tank details (lights) | |
const lightGeometry = new THREE.SphereGeometry(0.3, 16, 16); | |
const lightMaterial = new THREE.MeshStandardMaterial({ | |
color: isEnemy ? 0xffff00 : 0x00ffff, | |
emissive: isEnemy ? 0xffff00 : 0x00ffff, | |
emissiveIntensity: 0.5 | |
}); | |
// Left light | |
const leftLight = new THREE.Mesh(lightGeometry, lightMaterial); | |
leftLight.position.set(-1.5, 1.5, -2.5); | |
leftLight.castShadow = true; | |
tankGroup.add(leftLight); | |
// Right light | |
const rightLight = new THREE.Mesh(lightGeometry, lightMaterial); | |
rightLight.position.set(1.5, 1.5, -2.5); | |
rightLight.castShadow = true; | |
tankGroup.add(rightLight); | |
// Random rotation for enemies, face base for player | |
if (isEnemy) { | |
tankGroup.rotation.y = Math.random() * Math.PI * 2; | |
} else { | |
// Player tank would face outward (not used in this game) | |
} | |
// Add tank to scene | |
scene.add(tankGroup); | |
tanks.push(tankGroup); | |
return tankGroup; | |
} | |
// Spawn enemy tanks at random positions | |
function spawnEnemyTanks(count) { | |
for (let i = 0; i < count; i++) { | |
// Random position at edge of map (farther away) | |
const angle = Math.random() * Math.PI * 2; | |
const distance = 100 + Math.random() * 30; // Increased spawn distance | |
const x = Math.cos(angle) * distance; | |
const z = Math.sin(angle) * distance; | |
const position = new THREE.Vector3(x, 0, z); | |
createTank(position, true); | |
} | |
} | |
// Create power-up | |
function createPowerUp(type, position) { | |
const powerUp = document.createElement('div'); | |
powerUp.className = 'power-up'; | |
powerUp.textContent = type === 'ammo' ? 'A' : 'H'; | |
powerUp.title = type === 'ammo' ? 'Ammo Refill' : 'Health Pack'; | |
powerUp.style.backgroundColor = type === 'ammo' ? 'gold' : 'limegreen'; | |
powerUp.style.boxShadow = type === 'ammo' ? '0 0 10px gold' : '0 0 10px limegreen'; | |
// Convert 3D position to screen position | |
const vector = position.clone().project(camera); | |
const x = (vector.x * 0.5 + 0.5) * window.innerWidth; | |
const y = -(vector.y * 0.5 - 0.5) * window.innerHeight; | |
powerUp.style.left = `${x}px`; | |
powerUp.style.top = `${y}px`; | |
document.body.appendChild(powerUp); | |
// Store in game state | |
gameState.powerUps.push({ | |
element: powerUp, | |
type: type, | |
position: position, | |
collected: false | |
}); | |
// Click handler | |
powerUp.addEventListener('click', () => { | |
if (gameState.powerUps.find(p => p.element === powerUp)?.collected) return; | |
// Apply effect | |
if (type === 'ammo') { | |
gameState.maxAmmo += 5; | |
gameState.ammo = gameState.maxAmmo; | |
showMessage("Ammo Capacity Increased!"); | |
} else { | |
gameState.baseHealth = Math.min(100, gameState.baseHealth + 30); | |
showMessage("Base Repaired!"); | |
} | |
// Mark as collected | |
const powerUpObj = gameState.powerUps.find(p => p.element === powerUp); | |
if (powerUpObj) powerUpObj.collected = true; | |
// Remove from DOM | |
powerUp.remove(); | |
// Create visual effect | |
createExplosion(position, 1, type === 'ammo' ? | |
[new THREE.Color(0xffcc00), new THREE.Color(0xffffff)] : | |
[new THREE.Color(0x00ff00), new THREE.Color(0xffffff)]); | |
}); | |
// Auto-remove after some time | |
setTimeout(() => { | |
if (powerUp.parentNode) { | |
powerUp.remove(); | |
const index = gameState.powerUps.findIndex(p => p.element === powerUp); | |
if (index !== -1) gameState.powerUps.splice(index, 1); | |
} | |
}, 10000); | |
} | |
// Spawn random power-up | |
function spawnPowerUp() { | |
if (Math.random() > 0.3) return; // 30% chance to spawn | |
const type = Math.random() > 0.5 ? 'ammo' : 'health'; | |
const angle = Math.random() * Math.PI * 2; | |
const distance = 10 + Math.random() * 30; | |
const x = Math.cos(angle) * distance; | |
const z = Math.sin(angle) * distance; | |
const position = new THREE.Vector3(x, 0, z); | |
createPowerUp(type, position); | |
} | |
// Rocket | |
function createRocket() { | |
const group = new THREE.Group(); | |
group.userData = { | |
isFlying: false, | |
startTime: 0, | |
target: new THREE.Vector3(10, 0, 10), | |
lastPosition: new THREE.Vector3(0, 0, 0), | |
smokeTrail: [], | |
hasExploded: false, | |
damage: 50 | |
}; | |
// Rocket body | |
const bodyGeometry = new THREE.CylinderGeometry(0.5, 0.3, 3, 32); | |
const bodyMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xffffff, | |
roughness: 0.2, | |
metalness: 0.7 | |
}); | |
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); | |
body.position.y = 1.5; | |
body.castShadow = true; | |
group.add(body); | |
// Rocket nose | |
const noseGeometry = new THREE.ConeGeometry(0.5, 1, 32); | |
const noseMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xff0000, | |
roughness: 0.2, | |
metalness: 0.7 | |
}); | |
const nose = new THREE.Mesh(noseGeometry, noseMaterial); | |
nose.position.y = 3.5; | |
nose.castShadow = true; | |
group.add(nose); | |
// Fins | |
const finGeometry = new THREE.BoxGeometry(0.8, 0.1, 0.5); | |
const finMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x0000ff, | |
roughness: 0.5, | |
metalness: 0.3 | |
}); | |
for (let i = 0; i < 3; i++) { | |
const fin = new THREE.Mesh(finGeometry, finMaterial); | |
fin.position.y = 0.5; | |
fin.rotation.y = (i * Math.PI * 2) / 3; | |
fin.position.x = Math.sin(fin.rotation.y) * 0.6; | |
fin.position.z = Math.cos(fin.rotation.y) * 0.6; | |
fin.rotation.z = Math.PI / 4; | |
fin.castShadow = true; | |
group.add(fin); | |
} | |
// Engine | |
const engineGeometry = new THREE.CylinderGeometry(0.4, 0.5, 0.5, 32); | |
const engineMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x333333, | |
roughness: 0.8, | |
metalness: 0.1 | |
}); | |
const engine = new THREE.Mesh(engineGeometry, engineMaterial); | |
engine.position.y = 0.25; | |
engine.castShadow = true; | |
group.add(engine); | |
// Flame (will be animated) | |
const flameGeometry = new THREE.ConeGeometry(0.6, 1.5, 32); | |
const flameMaterial = new THREE.MeshStandardMaterial({ | |
color: 0xff6600, | |
emissive: 0xff6600, | |
emissiveIntensity: 1, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const flame = new THREE.Mesh(flameGeometry, flameMaterial); | |
flame.position.y = -0.5; | |
flame.rotation.x = Math.PI; | |
group.add(flame); | |
group.userData.flame = flame; | |
group.position.set(0, 0, 0); | |
group.scale.set(0.8, 0.8, 0.8); | |
return group; | |
} | |
// Create smoke particle | |
function createSmokeParticle(position) { | |
const size = 1 + Math.random() * 2; | |
const geometry = new THREE.SphereGeometry(size, 8, 8); | |
const material = new THREE.MeshStandardMaterial({ | |
color: 0xeeeeee, | |
transparent: true, | |
opacity: 0.7, | |
roughness: 0.9, | |
metalness: 0 | |
}); | |
const particle = new THREE.Mesh(geometry, material); | |
particle.position.copy(position); | |
particle.userData = { | |
createdAt: Date.now(), | |
lifeTime: 3000 + Math.random() * 2000, // 3-5 seconds | |
velocity: new THREE.Vector3( | |
(Math.random() - 0.5) * 0.1, | |
Math.random() * 0.05, | |
(Math.random() - 0.5) * 0.1 | |
), | |
growthRate: 0.02 + Math.random() * 0.03 | |
}; | |
scene.add(particle); | |
return particle; | |
} | |
// Flag dragging | |
const flag = document.getElementById('flag'); | |
let isDragging = false; | |
let offsetX, offsetY; | |
flag.addEventListener('mousedown', (e) => { | |
if (!gameState.started || gameState.gameOver) return; | |
isDragging = true; | |
offsetX = e.clientX - flag.offsetLeft; | |
offsetY = e.clientY - flag.offsetTop; | |
flag.style.opacity = '0.8'; | |
}); | |
document.addEventListener('mousemove', (e) => { | |
if (!isDragging || !gameState.started || gameState.gameOver) return; | |
flag.style.left = (e.clientX - offsetX) + 'px'; | |
flag.style.top = (e.clientY - offsetY) + 'px'; | |
}); | |
document.addEventListener('mouseup', () => { | |
isDragging = false; | |
flag.style.opacity = '1'; | |
}); | |
// Convert screen position to 3D world position | |
function screenToWorld(x, y) { | |
const vector = new THREE.Vector3( | |
(x / window.innerWidth) * 2 - 1, | |
-(y / window.innerHeight) * 2 + 1, | |
0.5 | |
); | |
vector.unproject(camera); | |
const dir = vector.sub(camera.position).normalize(); | |
const distance = -camera.position.y / dir.y; | |
const pos = camera.position.clone().add(dir.multiplyScalar(distance)); | |
return pos; | |
} | |
// Launch rocket | |
launchBtn.addEventListener('click', launchRocket); | |
// Spacebar to launch | |
document.addEventListener('keydown', (e) => { | |
if (e.code === 'Space' && gameState.started && !gameState.gameOver && gameState.ammo > 0) { | |
e.preventDefault(); | |
launchRocket(); | |
} | |
}); | |
function launchRocket() { | |
if (gameState.ammo <= 0 || gameState.gameOver) return; | |
// Get flag position in 3D world | |
const flagRect = flag.getBoundingClientRect(); | |
const flagCenterX = flagRect.left + flagRect.width / 2; | |
const flagCenterY = flagRect.top + flagRect.height / 2; | |
const target = screenToWorld(flagCenterX, flagCenterY); | |
// Create rocket | |
const rocket = createRocket(); | |
rocket.userData.isFlying = true; | |
rocket.userData.startTime = Date.now(); | |
rocket.userData.target = target; | |
rocket.userData.lastPosition = new THREE.Vector3(0, 0, 0); | |
scene.add(rocket); | |
activeRockets.push(rocket); | |
// Update game state | |
gameState.ammo--; | |
gameState.rocketsFired++; | |
// Animate flame | |
animateFlame(rocket); | |
// Update UI | |
updateUI(); | |
} | |
// Animate rocket flame | |
function animateFlame(rocket) { | |
if (!rocket.userData.isFlying || gameState.gameOver) return; | |
const flame = rocket.userData.flame; | |
if (flame) { | |
// Randomize flame size for flickering effect | |
const scale = 0.8 + Math.random() * 0.4; | |
flame.scale.set(scale, scale, scale); | |
// Change color slightly | |
flame.material.color.setHSL(0.08 + Math.random() * 0.04, 1, 0.5); | |
} | |
requestAnimationFrame(() => animateFlame(rocket)); | |
} | |
// Create explosion | |
function createExplosion(position, scale = 1, colorChoices = null) { | |
if (!colorChoices) { | |
colorChoices = [ | |
new THREE.Color(0xff6600), // Orange | |
new THREE.Color(0xff0000), // Red | |
new THREE.Color(0xffff00), // Yellow | |
new THREE.Color(0xffffff) // White | |
]; | |
} | |
// Create particles | |
const particleCount = Math.floor(200 * scale); | |
const particles = new THREE.BufferGeometry(); | |
const positions = new Float32Array(particleCount * 3); | |
const colors = new Float32Array(particleCount * 3); | |
const sizes = new Float32Array(particleCount); | |
for (let i = 0; i < particleCount; i++) { | |
// Random position in sphere | |
const theta = Math.random() * Math.PI * 2; | |
const phi = Math.random() * Math.PI; | |
const radius = 0.1 + Math.random() * 2 * scale; | |
positions[i * 3] = position.x + radius * Math.sin(phi) * Math.cos(theta); | |
positions[i * 3 + 1] = position.y + radius * Math.cos(phi); | |
positions[i * 3 + 2] = position.z + radius * Math.sin(phi) * Math.sin(theta); | |
// Random color | |
const color = colorChoices[Math.floor(Math.random() * colorChoices.length)]; | |
colors[i * 3] = color.r; | |
colors[i * 3 + 1] = color.g; | |
colors[i * 3 + 2] = color.b; | |
// Random size | |
sizes[i] = (0.5 + Math.random() * 1.5) * scale; | |
} | |
particles.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
particles.setAttribute('color', new THREE.BufferAttribute(colors, 3)); | |
particles.setAttribute('size', new THREE.BufferAttribute(sizes, 1)); | |
// Particle material | |
const particleMaterial = new THREE.PointsMaterial({ | |
size: 1 * scale, | |
vertexColors: true, | |
transparent: true, | |
opacity: 0.8, | |
blending: THREE.AdditiveBlending | |
}); | |
const particleSystem = new THREE.Points(particles, particleMaterial); | |
scene.add(particleSystem); | |
// Animate particles | |
const startTime = Date.now(); | |
const duration = 2000 * scale; // 2 seconds | |
function animateParticles() { | |
const elapsed = Date.now() - startTime; | |
const progress = elapsed / duration; | |
if (progress >= 1) { | |
scene.remove(particleSystem); | |
return; | |
} | |
// Update particle positions (fly outward and fade) | |
const positions = particles.attributes.position.array; | |
for (let i = 0; i < particleCount; i++) { | |
// Move particles outward | |
positions[i * 3] += (positions[i * 3] - position.x) * 0.02; | |
positions[i * 3 + 1] += (positions[i * 3 + 1] - position.y) * 0.02; | |
positions[i * 3 + 2] += (positions[i * 3 + 2] - position.z) * 0.02; | |
// Add some randomness | |
positions[i * 3] += (Math.random() - 0.5) * 0.2 * scale; | |
positions[i * 3 + 1] += (Math.random() - 0.5) * 0.2 * scale; | |
positions[i * 3 + 2] += (Math.random() - 0.5) * 0.2 * scale; | |
} | |
particles.attributes.position.needsUpdate = true; | |
particleMaterial.opacity = 1 - progress; | |
requestAnimationFrame(animateParticles); | |
} | |
animateParticles(); | |
} | |
// Create tank explosion | |
function createTankExplosion(position) { | |
// Big explosion | |
createExplosion(position, 2, [ | |
new THREE.Color(0xff0000), // Red | |
new THREE.Color(0xffff00), // Yellow | |
new THREE.Color(0x000000), // Black | |
new THREE.Color(0x333333) // Dark gray | |
]); | |
// Secondary explosions | |
for (let i = 0; i < 3; i++) { | |
setTimeout(() => { | |
const offset = new THREE.Vector3( | |
(Math.random() - 0.5) * 3, | |
Math.random() * 0.5, | |
(Math.random() - 0.5) * 3 | |
); | |
createExplosion(position.clone().add(offset), 0.5, [ | |
new THREE.Color(0xff6600), | |
new THREE.Color(0x333333) | |
]); | |
}, 100 + Math.random() * 400); | |
} | |
// Smoke plume | |
for (let i = 0; i < 10; i++) { | |
setTimeout(() => { | |
const smokePos = position.clone(); | |
smokePos.y += Math.random() * 2; | |
createSmokeParticle(smokePos); | |
}, i * 100); | |
} | |
} | |
// Check for rocket-tank collisions | |
function checkCollisions() { | |
for (let i = activeRockets.length - 1; i >= 0; i--) { | |
const rocket = activeRockets[i]; | |
if (rocket.userData.hasExploded) continue; | |
for (let j = tanks.length - 1; j >= 0; j--) { | |
const tank = tanks[j]; | |
if (!tank.userData.isAlive || !tank.userData.isEnemy) continue; | |
// Simple distance check | |
const distance = rocket.position.distanceTo(tank.position); | |
if (distance < 3) { // Collision detected | |
// Tank takes damage | |
tank.userData.health -= rocket.userData.damage; | |
// Create explosion at collision point | |
createExplosion(rocket.position); | |
rocket.userData.hasExploded = true; | |
// Remove rocket | |
scene.remove(rocket); | |
activeRockets.splice(i, 1); | |
// Update hits | |
gameState.hits++; | |
// Check if tank is destroyed | |
if (tank.userData.health <= 0) { | |
tank.userData.isAlive = false; | |
createTankExplosion(tank.position); | |
scene.remove(tank); | |
tanks.splice(j, 1); | |
// Update game state | |
gameState.tanksDestroyed++; | |
gameState.score += 100 * gameState.wave; | |
// Show message | |
const messages = [ | |
"Tank Destroyed!", | |
"Direct Hit!", | |
"Boom!", | |
"Target Eliminated!" | |
]; | |
showMessage(messages[Math.floor(Math.random() * messages.length)]); | |
// Chance to spawn power-up | |
spawnPowerUp(); | |
} | |
break; | |
} | |
} | |
} | |
} | |
// Update enemy tank behavior | |
function updateTanks() { | |
const now = Date.now(); | |
for (const tank of tanks) { | |
if (!tank.userData.isAlive || !tank.userData.isEnemy) continue; | |
// Move toward base (slower movement) | |
const direction = new THREE.Vector3().subVectors(baseGroup.position, tank.position).normalize(); | |
tank.position.x += direction.x * tank.userData.speed * 0.02; // Reduced movement speed | |
tank.position.z += direction.z * tank.userData.speed * 0.02; // Reduced movement speed | |
// Rotate to face base | |
tank.rotation.y = Math.atan2(direction.x, direction.z); | |
// Attack base if close enough | |
const distanceToBase = tank.position.distanceTo(baseGroup.position); | |
if (distanceToBase < 15 && now - tank.userData.lastAttack > 2000) { | |
// Attack! | |
gameState.baseHealth -= tank.userData.damage; | |
tank.userData.lastAttack = now; | |
// Create muzzle flash | |
const gun = tank.children.find(child => child.geometry instanceof THREE.CylinderGeometry && child.geometry.parameters.radiusTop === 0.3); | |
if (gun) { | |
const flashPos = gun.position.clone().applyMatrix4(tank.matrixWorld); | |
createExplosion(flashPos, 0.3, [ | |
new THREE.Color(0xff6600), | |
new THREE.Color(0xffff00) | |
]); | |
} | |
// Check for game over | |
if (gameState.baseHealth <= 0) { | |
gameState.baseHealth = 0; | |
gameOver(); | |
} | |
} | |
} | |
} | |
// Update power-up positions | |
function updatePowerUps() { | |
for (let i = gameState.powerUps.length - 1; i >= 0; i--) { | |
const powerUp = gameState.powerUps[i]; | |
if (powerUp.collected) { | |
gameState.powerUps.splice(i, 1); | |
continue; | |
} | |
// Convert 3D position to screen position | |
const vector = powerUp.position.clone().project(camera); | |
const x = (vector.x * 0.5 + 0.5) * window.innerWidth; | |
const y = -(vector.y * 0.5 - 0.5) * window.innerHeight; | |
powerUp.element.style.left = `${x}px`; | |
powerUp.element.style.top = `${y}px`; | |
} | |
} | |
// Update smoke particles | |
function updateSmokeParticles() { | |
const now = Date.now(); | |
// Update existing smoke particles | |
for (let i = smokeParticles.length - 1; i >= 0; i--) { | |
const particle = smokeParticles[i]; | |
const age = now - particle.userData.createdAt; | |
if (age > particle.userData.lifeTime) { | |
// Remove expired particles | |
scene.remove(particle); | |
smokeParticles.splice(i, 1); | |
} else { | |
// Update position and size | |
particle.position.x += particle.userData.velocity.x; | |
particle.position.y += particle.userData.velocity.y; | |
particle.position.z += particle.userData.velocity.z; | |
// Grow over time | |
const scale = 1 + (particle.userData.growthRate * age / 1000); | |
particle.scale.set(scale, scale, scale); | |
// Fade out | |
particle.material.opacity = 0.7 * (1 - (age / particle.userData.lifeTime)); | |
} | |
} | |
// Add new smoke particles for each rocket | |
for (const rocket of activeRockets) { | |
if (rocket.userData.isFlying && rocket.position.y > 1 && !rocket.userData.hasExploded) { | |
// Only add smoke when rocket is above ground | |
if (Math.random() < 0.3) { // Control density of smoke trail | |
const smokeParticle = createSmokeParticle(rocket.position.clone()); | |
smokeParticles.push(smokeParticle); | |
} | |
} | |
} | |
} | |
// Start new wave | |
function startNewWave() { | |
gameState.wave++; | |
showWaveIndicator(gameState.wave); | |
// Spawn more tanks based on wave number (but fewer in early waves) | |
const tanksToSpawn = Math.min(3 + Math.floor(gameState.wave * 0.8), 15); // Slower progression | |
spawnEnemyTanks(tanksToSpawn); | |
// Bonus for completing wave | |
gameState.score += 500 * gameState.wave; | |
gameState.ammo = gameState.maxAmmo; | |
// Update UI | |
updateUI(); | |
} | |
// Check if wave is complete | |
function checkWaveComplete() { | |
if (gameState.gameOver) return; | |
const enemyTanks = tanks.filter(t => t.userData.isEnemy && t.userData.isAlive); | |
if (enemyTanks.length === 0) { | |
// Wave complete! | |
setTimeout(() => { | |
startNewWave(); | |
}, 5000); // Longer delay between waves | |
} | |
} | |
// Game over | |
function gameOver() { | |
gameState.gameOver = true; | |
finalScoreElement.textContent = gameState.score; | |
gameOverScreen.classList.add('show'); | |
// Disable controls | |
controls.enabled = false; | |
} | |
// Start game | |
function startGame() { | |
gameState.started = true; | |
gameState.score = 0; | |
gameState.wave = 0; | |
gameState.tanksDestroyed = 0; | |
gameState.rocketsFired = 0; | |
gameState.hits = 0; | |
gameState.baseHealth = 100; | |
gameState.ammo = 10; | |
gameState.maxAmmo = 10; | |
gameState.reloading = false; | |
gameState.gameOver = false; | |
// Clear any existing game objects | |
activeRockets.forEach(rocket => scene.remove(rocket)); | |
activeRockets = []; | |
smokeParticles.forEach(particle => scene.remove(particle)); | |
smokeParticles = []; | |
tanks.forEach(tank => scene.remove(tank)); | |
tanks = []; | |
gameState.powerUps.forEach(powerUp => powerUp.element.remove()); | |
gameState.powerUps = []; | |
// Hide start screen | |
startScreen.style.display = 'none'; | |
// Start first wave | |
startNewWave(); | |
// Update UI | |
updateUI(); | |
} | |
// Restart game | |
function restartGame() { | |
gameOverScreen.classList.remove('show'); | |
startGame(); | |
} | |
// Event listeners | |
startBtn.addEventListener('click', startGame); | |
restartBtn.addEventListener('click', restartGame); | |
// Animation loop | |
function animate() { | |
requestAnimationFrame(animate); | |
if (!gameState.started || gameState.gameOver) return; | |
// Update controls | |
controls.update(); | |
// Update game elements | |
updateSmokeParticles(); | |
updateTanks(); | |
checkCollisions(); | |
updatePowerUps(); | |
checkWaveComplete(); | |
// Update all active rockets | |
for (let i = activeRockets.length - 1; i >= 0; i--) { | |
const rocket = activeRockets[i]; | |
const elapsed = Date.now() - rocket.userData.startTime; | |
const progress = Math.min(elapsed / 5000, 1); // 5 second flight duration | |
if (progress >= 1 && !rocket.userData.hasExploded) { | |
// Rocket reached target - explode! | |
createExplosion(rocket.position); | |
rocket.userData.hasExploded = true; | |
scene.remove(rocket); | |
activeRockets.splice(i, 1); | |
} else if (!rocket.userData.hasExploded) { | |
// Calculate bezier curve path | |
const startPoint = new THREE.Vector3(0, 0, 0); | |
const controlPoint1 = new THREE.Vector3( | |
rocket.userData.target.x * 0.3, | |
rocket.userData.target.y + 30, | |
rocket.userData.target.z * 0.3 | |
); | |
const controlPoint2 = new THREE.Vector3( | |
rocket.userData.target.x * 0.7, | |
rocket.userData.target.y + 20, | |
rocket.userData.target.z * 0.7 | |
); | |
const endPoint = rocket.userData.target.clone(); | |
// Get position on curve | |
const t = progress; | |
const u = 1 - t; | |
const tt = t * t; | |
const uu = u * u; | |
const uuu = uu * u; | |
const ttt = tt * t; | |
const point = new THREE.Vector3(0, 0, 0); | |
point.x = uuu * startPoint.x; | |
point.y = uuu * startPoint.y; | |
point.z = uuu * startPoint.z; | |
point.x += 3 * uu * t * controlPoint1.x; | |
point.y += 3 * uu * t * controlPoint1.y; | |
point.z += 3 * uu * t * controlPoint1.z; | |
point.x += 3 * u * tt * controlPoint2.x; | |
point.y += 3 * u * tt * controlPoint2.y; | |
point.z += 3 * u * tt * controlPoint2.z; | |
point.x += ttt * endPoint.x; | |
point.y += ttt * endPoint.y; | |
point.z += ttt * endPoint.z; | |
// Set rocket position and rotation | |
rocket.position.copy(point); | |
// Calculate direction for rotation | |
if (elapsed > 50) { // Wait a frame to have previous position | |
const direction = point.clone().sub(rocket.userData.lastPosition).normalize(); | |
rocket.lookAt(point.clone().add(direction)); | |
rocket.rotateX(Math.PI / 2); // Adjust because rocket model points up | |
} | |
rocket.userData.lastPosition = point.clone(); | |
} | |
} | |
renderer.render(scene, camera); | |
} | |
// Handle window resize | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
// Start animation | |
animate(); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LukasBe/missile-defense-3d" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |