Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>3D Rocket Launcher with Tanks</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; | |
} | |
#canvas-container { | |
position: absolute; | |
width: 100%; | |
height: 100%; | |
} | |
#ui { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
z-index: 100; | |
color: white; | |
text-shadow: 1px 1px 2px black; | |
} | |
#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: 12px 24px; | |
background: linear-gradient(to right, #ff5e62, #ff9966); | |
color: white; | |
border: none; | |
border-radius: 30px; | |
font-size: 18px; | |
font-weight: bold; | |
cursor: pointer; | |
box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
z-index: 100; | |
transition: all 0.3s; | |
} | |
#launch-btn:hover { | |
transform: translateX(-50%) scale(1.05); | |
box-shadow: 0 6px 20px rgba(0,0,0,0.3); | |
} | |
#launch-btn:active { | |
transform: translateX(-50%) scale(0.98); | |
} | |
.explosion-particle { | |
position: absolute; | |
width: 8px; | |
height: 8px; | |
border-radius: 50%; | |
pointer-events: none; | |
} | |
#counter { | |
position: absolute; | |
top: 20px; | |
right: 20px; | |
color: white; | |
font-size: 18px; | |
text-shadow: 1px 1px 2px black; | |
z-index: 100; | |
} | |
#tank-counter { | |
position: absolute; | |
top: 50px; | |
right: 20px; | |
color: white; | |
font-size: 18px; | |
text-shadow: 1px 1px 2px black; | |
z-index: 100; | |
} | |
#message { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
background-color: rgba(0,0,0,0.7); | |
color: white; | |
padding: 20px 40px; | |
border-radius: 10px; | |
font-size: 24px; | |
font-weight: bold; | |
opacity: 0; | |
transition: opacity 0.5s; | |
pointer-events: none; | |
z-index: 200; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="canvas-container"></div> | |
<div id="ui"> | |
<h1 class="text-3xl font-bold">🚀 Rocket Launcher</h1> | |
<p class="text-lg">Drag the flag to set target, then launch!</p> | |
</div> | |
<div id="counter">Rockets launched: <span id="rocket-count">0</span></div> | |
<div id="tank-counter">Tanks destroyed: <span id="tank-count">0</span></div> | |
<div id="message"></div> | |
<div id="flag"></div> | |
<button id="launch-btn">LAUNCH ROCKET</button> | |
<script> | |
// Scene setup | |
const scene = new THREE.Scene(); | |
scene.background = new THREE.Color(0x87CEEB); | |
scene.fog = new THREE.FogExp2(0x87CEEB, 0.002); | |
// Camera | |
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); | |
camera.position.set(0, 10, 30); | |
// Renderer | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = true; | |
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; | |
// 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; | |
scene.add(directionalLight); | |
// Ground | |
const groundGeometry = new THREE.PlaneGeometry(200, 200); | |
const groundMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x3a5f0b, | |
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); | |
// 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); | |
// Active rockets and smoke trails | |
let activeRockets = []; | |
let smokeParticles = []; | |
let rocketCount = 0; | |
let tankCount = 0; | |
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); | |
} | |
// Create a detailed tank | |
function createTank(position) { | |
const tankGroup = new THREE.Group(); | |
tankGroup.position.copy(position); | |
tankGroup.userData = { | |
isAlive: true, | |
health: 100 | |
}; | |
// Tank body | |
const bodyGeometry = new THREE.BoxGeometry(3, 1.5, 5); | |
const bodyMaterial = new THREE.MeshStandardMaterial({ | |
color: 0x556b2f, | |
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: 0x556b2f, | |
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: 0xffff00, | |
emissive: 0xffff00, | |
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 | |
tankGroup.rotation.y = Math.random() * Math.PI * 2; | |
// Add tank to scene | |
scene.add(tankGroup); | |
tanks.push(tankGroup); | |
return tankGroup; | |
} | |
// Spawn tanks at random positions | |
function spawnTanks(count) { | |
for (let i = 0; i < count; i++) { | |
// Random position within bounds | |
const x = (Math.random() - 0.5) * 80; | |
const z = (Math.random() - 0.5) * 80; | |
const position = new THREE.Vector3(x, 0, z); | |
// Make sure position is not too close to launch pad | |
if (position.distanceTo(new THREE.Vector3(0, 0, 0)) > 15) { | |
createTank(position); | |
} | |
} | |
} | |
// Initialize tanks | |
spawnTanks(5); | |
// 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 | |
}; | |
// 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) => { | |
isDragging = true; | |
offsetX = e.clientX - flag.offsetLeft; | |
offsetY = e.clientY - flag.offsetTop; | |
flag.style.opacity = '0.8'; | |
}); | |
document.addEventListener('mousemove', (e) => { | |
if (!isDragging) 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 | |
const launchBtn = document.getElementById('launch-btn'); | |
const rocketCountElement = document.getElementById('rocket-count'); | |
const tankCountElement = document.getElementById('tank-count'); | |
launchBtn.addEventListener('click', () => { | |
// 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 counter | |
rocketCount++; | |
rocketCountElement.textContent = rocketCount; | |
// Animate flame | |
animateFlame(rocket); | |
}); | |
// Animate rocket flame | |
function animateFlame(rocket) { | |
if (!rocket.userData.isFlying) 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); | |
} | |
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) continue; | |
// Simple distance check | |
const distance = rocket.position.distanceTo(tank.position); | |
if (distance < 3) { // Collision detected | |
// Tank takes damage | |
tank.userData.health -= 50; | |
// Create explosion at collision point | |
createExplosion(rocket.position); | |
rocket.userData.hasExploded = true; | |
// Remove rocket | |
scene.remove(rocket); | |
activeRockets.splice(i, 1); | |
// 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 counter | |
tankCount++; | |
tankCountElement.textContent = tankCount; | |
// Show message | |
const messages = [ | |
"Tank Destroyed!", | |
"Direct Hit!", | |
"Boom!", | |
"Target Eliminated!" | |
]; | |
showMessage(messages[Math.floor(Math.random() * messages.length)]); | |
// Spawn new tank after a delay | |
setTimeout(() => { | |
spawnTanks(1); | |
}, 3000); | |
} | |
break; | |
} | |
} | |
} | |
} | |
// 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); | |
} | |
} | |
} | |
} | |
// Animation loop | |
function animate() { | |
requestAnimationFrame(animate); | |
// Update controls | |
controls.update(); | |
// Update smoke particles | |
updateSmokeParticles(); | |
// Check for collisions | |
checkCollisions(); | |
// 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/3d-tanks-shooter" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |