Created
July 10, 2025 07:56
-
-
Save CharlieGreenman/372cf4e97cac2694676881a5fdfeca49 to your computer and use it in GitHub Desktop.
Grok 4 outerspace code 3d short video x space dogfight
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Short Story Animation: Swarm of Sci-Fi Airplanes Fighting in Space (3D Version - Enhanced)</title> | |
<style> | |
body { margin: 0; background: black; } | |
canvas { display: block; } | |
</style> | |
</head> | |
<body> | |
<script type="importmap"> | |
{ | |
"imports": { | |
"three": "https://esm.sh/[email protected]", | |
"three/addons/": "https://esm.sh/[email protected]/examples/jsm/" | |
} | |
} | |
</script> | |
<script type="module"> | |
import * as THREE from 'three'; | |
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js'; | |
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; | |
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js'; | |
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js'; | |
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js'; | |
// Scene setup | |
const scene = new THREE.Scene(); | |
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000); | |
camera.position.set(0, 0, 80); | |
const renderer = new THREE.WebGLRenderer({ antialias: true }); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
renderer.shadowMap.enabled = false; | |
renderer.toneMapping = THREE.ACESFilmicToneMapping; | |
renderer.toneMappingExposure = 1; | |
document.body.appendChild(renderer.domElement); | |
// Controls | |
const controls = new OrbitControls(camera, renderer.domElement); | |
controls.enableDamping = true; | |
controls.dampingFactor = 0.05; | |
// Lighting | |
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.2); | |
hemiLight.position.set(0, 20, 0); | |
scene.add(hemiLight); | |
const dirLight = new THREE.DirectionalLight(0xffffff, 0.5); | |
dirLight.position.set(5, 5, 5); | |
dirLight.castShadow = false; | |
scene.add(dirLight); | |
// Star field | |
const starsGeometry = new THREE.BufferGeometry(); | |
const starsMaterial = new THREE.PointsMaterial({ color: 0xFFFFFF, size: 0.02 }); | |
const starCount = 10000; | |
const positions = new Float32Array(starCount * 3); | |
for (let i = 0; i < starCount; i++) { | |
positions[i * 3] = (Math.random() - 0.5) * 2000; | |
positions[i * 3 + 1] = (Math.random() - 0.5) * 2000; | |
positions[i * 3 + 2] = (Math.random() - 0.5) * 2000; | |
} | |
starsGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); | |
const stars = new THREE.Points(starsGeometry, starsMaterial); | |
scene.add(stars); | |
// Set black background to show only stars | |
scene.background = new THREE.Color(0x000000); | |
// Post-processing | |
const composer = new EffectComposer(renderer); | |
const renderPass = new RenderPass(scene, camera); | |
composer.addPass(renderPass); | |
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85); | |
bloomPass.threshold = 0.1; | |
bloomPass.strength = 0.5; | |
bloomPass.radius = 0.5; | |
composer.addPass(bloomPass); | |
// Shared geometries and materials for performance | |
const bodyGeo = new THREE.CylinderGeometry(0.3, 0.4, 3, 12); | |
const noseGeo = new THREE.ConeGeometry(0.3, 1.2, 12); | |
const wingGeo = new THREE.BoxGeometry(2.5, 0.15, 1.2); | |
const tailGeo = new THREE.BoxGeometry(0.15, 1.2, 0.6); | |
const cockpitGeo = new THREE.SphereGeometry(0.25, 8, 8); | |
const engineGeo = new THREE.CylinderGeometry(0.12, 0.15, 0.8, 8); | |
const weaponGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.8, 6); | |
const detailPanelGeo = new THREE.BoxGeometry(0.3, 0.05, 0.4); | |
const antennaGeo = new THREE.CylinderGeometry(0.01, 0.01, 0.5, 4); | |
const matBlue = new THREE.MeshStandardMaterial({ color: 0x0066ff, roughness: 0.3, metalness: 0.9 }); | |
const matPurple = new THREE.MeshStandardMaterial({ color: 0xaa00ff, roughness: 0.3, metalness: 0.9 }); | |
const matCockpit = new THREE.MeshStandardMaterial({ color: 0x88ddff, roughness: 0.1, metalness: 0.1, transparent: true, opacity: 0.7 }); | |
const matEngine = new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.6, metalness: 0.8 }); | |
const matWeapon = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.4, metalness: 0.9 }); | |
const matDetail = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8, metalness: 0.6 }); | |
// Create sci-fi airplane model | |
function createSpaceship(team) { | |
const group = new THREE.Group(); | |
const mat = team === 1 ? matBlue : matPurple; | |
// Main body | |
const body = new THREE.Mesh(bodyGeo, mat); | |
body.rotation.x = Math.PI / 2; | |
group.add(body); | |
// Nose cone | |
const nose = new THREE.Mesh(noseGeo, mat); | |
nose.position.z = 2; | |
nose.rotation.x = Math.PI / 2; | |
group.add(nose); | |
// Cockpit | |
const cockpit = new THREE.Mesh(cockpitGeo, matCockpit); | |
cockpit.position.set(0, 0.15, 0.8); | |
group.add(cockpit); | |
// Main wings | |
const leftWing = new THREE.Mesh(wingGeo, mat); | |
leftWing.position.set(-1.4, 0, 0); | |
leftWing.rotation.y = Math.PI / 8; | |
group.add(leftWing); | |
const rightWing = leftWing.clone(); | |
rightWing.position.x = 1.4; | |
rightWing.rotation.y = -Math.PI / 8; | |
group.add(rightWing); | |
// Wing tip engines | |
const leftEngine = new THREE.Mesh(engineGeo, matEngine); | |
leftEngine.position.set(-2.2, 0, 0); | |
leftEngine.rotation.x = Math.PI / 2; | |
group.add(leftEngine); | |
const rightEngine = leftEngine.clone(); | |
rightEngine.position.x = 2.2; | |
group.add(rightEngine); | |
// Tail fins | |
const topTail = new THREE.Mesh(tailGeo, mat); | |
topTail.position.set(0, 0.6, -1.5); | |
group.add(topTail); | |
const bottomTail = topTail.clone(); | |
bottomTail.position.y = -0.6; | |
group.add(bottomTail); | |
// Side tail fins | |
const leftTailFin = new THREE.Mesh(tailGeo, mat); | |
leftTailFin.position.set(-0.4, 0, -1.3); | |
leftTailFin.rotation.z = Math.PI / 2; | |
group.add(leftTailFin); | |
const rightTailFin = leftTailFin.clone(); | |
rightTailFin.position.x = 0.4; | |
group.add(rightTailFin); | |
// Weapon hardpoints | |
const leftWeapon = new THREE.Mesh(weaponGeo, matWeapon); | |
leftWeapon.position.set(-1.8, -0.1, 0.2); | |
leftWeapon.rotation.x = Math.PI / 2; | |
group.add(leftWeapon); | |
const rightWeapon = leftWeapon.clone(); | |
rightWeapon.position.x = 1.8; | |
group.add(rightWeapon); | |
// Detail panels on body | |
const topPanel = new THREE.Mesh(detailPanelGeo, matDetail); | |
topPanel.position.set(0, 0.35, 0.5); | |
group.add(topPanel); | |
const bottomPanel = topPanel.clone(); | |
bottomPanel.position.y = -0.35; | |
group.add(bottomPanel); | |
// Side panels | |
const leftPanel = new THREE.Mesh(detailPanelGeo, matDetail); | |
leftPanel.position.set(-0.35, 0, 0.3); | |
leftPanel.rotation.z = Math.PI / 2; | |
group.add(leftPanel); | |
const rightPanel = leftPanel.clone(); | |
rightPanel.position.x = 0.35; | |
group.add(rightPanel); | |
// Antenna/sensor | |
const antenna = new THREE.Mesh(antennaGeo, matDetail); | |
antenna.position.set(0, 0.4, 0.8); | |
group.add(antenna); | |
// Landing gear details | |
const gearGeo = new THREE.BoxGeometry(0.05, 0.3, 0.05); | |
const leftGear = new THREE.Mesh(gearGeo, matDetail); | |
leftGear.position.set(-0.8, -0.4, 0.5); | |
group.add(leftGear); | |
const rightGear = leftGear.clone(); | |
rightGear.position.x = 0.8; | |
group.add(rightGear); | |
// Rear main engines | |
const mainEngineLeft = new THREE.Mesh(engineGeo, matEngine); | |
mainEngineLeft.position.set(-0.2, 0, -1.8); | |
mainEngineLeft.rotation.x = Math.PI / 2; | |
group.add(mainEngineLeft); | |
const mainEngineRight = mainEngineLeft.clone(); | |
mainEngineRight.position.x = 0.2; | |
group.add(mainEngineRight); | |
// Thruster particles (reduced count for performance) | |
const particleCount = 80; | |
const particleGeo = new THREE.BufferGeometry(); | |
const particlePos = new Float32Array(particleCount * 3); | |
for (let i = 0; i < particleCount; i++) { | |
particlePos[i * 3] = (Math.random() - 0.5) * 0.4; | |
particlePos[i * 3 + 1] = (Math.random() - 0.5) * 0.4; | |
particlePos[i * 3 + 2] = -Math.random() * 3 - 1.5; | |
} | |
particleGeo.setAttribute('position', new THREE.BufferAttribute(particlePos, 3)); | |
const particleMat = new THREE.PointsMaterial({ | |
color: team === 1 ? 0x0088ff : 0xff0088, | |
size: 0.08, | |
transparent: true, | |
opacity: 0.9 | |
}); | |
const thruster = new THREE.Points(particleGeo, particleMat); | |
group.add(thruster); | |
// Navigation lights | |
const navLightGeo = new THREE.SphereGeometry(0.02, 6, 6); | |
const redLightMat = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0x440000 }); | |
const greenLightMat = new THREE.MeshStandardMaterial({ color: 0x00ff00, emissive: 0x004400 }); | |
const redLight = new THREE.Mesh(navLightGeo, redLightMat); | |
redLight.position.set(-2.5, 0, 0.2); | |
group.add(redLight); | |
const greenLight = new THREE.Mesh(navLightGeo, greenLightMat); | |
greenLight.position.set(2.5, 0, 0.2); | |
group.add(greenLight); | |
group.scale.set(0.5, 0.5, 0.5); | |
return group; | |
} | |
// Spaceships | |
const numShipsPerTeam = 100; | |
const team1 = []; | |
const team2 = []; | |
for (let i = 0; i < numShipsPerTeam; i++) { | |
const ship1 = createSpaceship(1); | |
ship1.position.set( | |
-50 + (Math.random() - 0.5) * 20, | |
(Math.random() - 0.5) * 20, | |
(Math.random() - 0.5) * 20 | |
); | |
ship1.userData = { | |
team: 1, | |
target: null, | |
rollSpeed: Math.PI * (1 + Math.random() * 2), | |
lastShoot: Math.random() * 2, | |
health: 5, | |
destroyed: false | |
}; | |
team1.push(ship1); | |
scene.add(ship1); | |
} | |
for (let i = 0; i < numShipsPerTeam; i++) { | |
const ship2 = createSpaceship(2); | |
ship2.position.set( | |
50 + (Math.random() - 0.5) * 20, | |
(Math.random() - 0.5) * 20, | |
(Math.random() - 0.5) * 20 | |
); | |
ship2.userData = { | |
team: 2, | |
target: null, | |
rollSpeed: Math.PI * (1 + Math.random() * 2), | |
lastShoot: Math.random() * 2, | |
health: 5, | |
destroyed: false | |
}; | |
team2.push(ship2); | |
scene.add(ship2); | |
} | |
// Combat arrays | |
const lasers = []; | |
const missiles = []; | |
const explosions = []; | |
function shootLaser(from, to) { | |
const material = new THREE.LineBasicMaterial({ color: 0xff0000, linewidth: 2 }); | |
const points = [from.position.clone(), to.position.clone()]; | |
const geometry = new THREE.BufferGeometry().setFromPoints(points); | |
const line = new THREE.Line(geometry, material); | |
scene.add(line); | |
lasers.push({ line, createTime: clock.getElapsedTime() }); | |
// Apply damage with hit probability | |
if (Math.random() < 0.8 && !to.userData.destroyed) { | |
to.userData.health -= 1; | |
if (to.userData.health <= 0) { | |
to.userData.destroyed = true; | |
createExplosion(to.position); | |
scene.remove(to); | |
} | |
} | |
} | |
function shootMissile(from, to) { | |
const missileGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.3, 6); | |
const missileMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0x222222 }); | |
const missile = new THREE.Mesh(missileGeo, missileMat); | |
missile.position.copy(from.position); | |
// Missile trail particles | |
const trailCount = 20; | |
const trailGeo = new THREE.BufferGeometry(); | |
const trailPos = new Float32Array(trailCount * 3); | |
for (let i = 0; i < trailCount; i++) { | |
trailPos[i * 3] = 0; | |
trailPos[i * 3 + 1] = 0; | |
trailPos[i * 3 + 2] = -i * 0.1; | |
} | |
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPos, 3)); | |
const trailMat = new THREE.PointsMaterial({ | |
color: 0xffffff, | |
size: 0.03, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const trail = new THREE.Points(trailGeo, trailMat); | |
missile.add(trail); | |
scene.add(missile); | |
missiles.push({ | |
mesh: missile, | |
target: to, | |
speed: 40 + Math.random() * 20, | |
createTime: clock.getElapsedTime(), | |
trail: trail | |
}); | |
} | |
function createExplosion(position) { | |
const explosionGroup = new THREE.Group(); | |
explosionGroup.position.copy(position); | |
// Core flash - bright white center | |
const coreGeo = new THREE.SphereGeometry(0.3, 8, 8); | |
const coreMat = new THREE.MeshStandardMaterial({ | |
color: 0xffffff, | |
emissive: 0xffffff, | |
transparent: true, | |
opacity: 1 | |
}); | |
const core = new THREE.Mesh(coreGeo, coreMat); | |
explosionGroup.add(core); | |
// Outer fireball - orange/red | |
const fireballGeo = new THREE.SphereGeometry(0.8, 12, 12); | |
const fireballMat = new THREE.MeshStandardMaterial({ | |
color: 0xff4400, | |
emissive: 0xff2200, | |
transparent: true, | |
opacity: 0.8 | |
}); | |
const fireball = new THREE.Mesh(fireballGeo, fireballMat); | |
explosionGroup.add(fireball); | |
// Debris particles | |
const debrisCount = 30; | |
const debrisGeo = new THREE.BufferGeometry(); | |
const debrisPos = new Float32Array(debrisCount * 3); | |
const debrisVel = new Float32Array(debrisCount * 3); | |
const debrisColors = new Float32Array(debrisCount * 3); | |
for (let i = 0; i < debrisCount; i++) { | |
debrisPos[i * 3] = (Math.random() - 0.5) * 0.1; | |
debrisPos[i * 3 + 1] = (Math.random() - 0.5) * 0.1; | |
debrisPos[i * 3 + 2] = (Math.random() - 0.5) * 0.1; | |
// Random velocities in all directions | |
const angle = Math.random() * Math.PI * 2; | |
const elevation = (Math.random() - 0.5) * Math.PI; | |
const speed = 3 + Math.random() * 7; | |
debrisVel[i * 3] = Math.cos(angle) * Math.cos(elevation) * speed; | |
debrisVel[i * 3 + 1] = Math.sin(elevation) * speed; | |
debrisVel[i * 3 + 2] = Math.sin(angle) * Math.cos(elevation) * speed; | |
// Mix of bright orange and dark debris colors | |
if (Math.random() < 0.6) { | |
debrisColors[i * 3] = 1; // R | |
debrisColors[i * 3 + 1] = 0.3 + Math.random() * 0.4; // G | |
debrisColors[i * 3 + 2] = 0; // B | |
} else { | |
debrisColors[i * 3] = 0.3; // R | |
debrisColors[i * 3 + 1] = 0.3; // G | |
debrisColors[i * 3 + 2] = 0.3; // B | |
} | |
} | |
debrisGeo.setAttribute('position', new THREE.BufferAttribute(debrisPos, 3)); | |
debrisGeo.setAttribute('velocity', new THREE.BufferAttribute(debrisVel, 3)); | |
debrisGeo.setAttribute('color', new THREE.BufferAttribute(debrisColors, 3)); | |
const debrisMat = new THREE.PointsMaterial({ | |
size: 0.08, | |
transparent: true, | |
opacity: 1, | |
vertexColors: true, | |
blending: THREE.AdditiveBlending | |
}); | |
const debris = new THREE.Points(debrisGeo, debrisMat); | |
explosionGroup.add(debris); | |
// Shock wave ring | |
const shockGeo = new THREE.RingGeometry(0.1, 0.2, 16); | |
const shockMat = new THREE.MeshStandardMaterial({ | |
color: 0xffffff, | |
emissive: 0x666666, | |
transparent: true, | |
opacity: 0.8, | |
side: THREE.DoubleSide | |
}); | |
const shock = new THREE.Mesh(shockGeo, shockMat); | |
shock.rotation.x = Math.random() * Math.PI; | |
shock.rotation.y = Math.random() * Math.PI; | |
shock.rotation.z = Math.random() * Math.PI; | |
explosionGroup.add(shock); | |
scene.add(explosionGroup); | |
explosions.push({ | |
mesh: explosionGroup, | |
core: core, | |
fireball: fireball, | |
debris: debris, | |
shock: shock, | |
createTime: clock.getElapsedTime(), | |
scale: 0.1 | |
}); | |
} | |
// Animation parameters | |
const clock = new THREE.Clock(); | |
const duration = 60; // 1 minute | |
let startTime = null; | |
let progress = 0; | |
function animate(timestamp) { | |
requestAnimationFrame(animate); | |
const delta = clock.getDelta(); | |
const elapsed = clock.getElapsedTime(); | |
if (!startTime) startTime = timestamp; | |
const totalElapsed = (timestamp - startTime) / 1000; | |
if (totalElapsed > duration) return; | |
progress = totalElapsed / duration; | |
// Update lasers | |
for (let i = lasers.length - 1; i >= 0; i--) { | |
if (elapsed - lasers[i].createTime > 0.1) { | |
scene.remove(lasers[i].line); | |
lasers.splice(i, 1); | |
} | |
} | |
// Update missiles | |
for (let i = missiles.length - 1; i >= 0; i--) { | |
const missile = missiles[i]; | |
const age = elapsed - missile.createTime; | |
if (age > 5 || missile.target.userData.destroyed) { | |
scene.remove(missile.mesh); | |
missiles.splice(i, 1); | |
continue; | |
} | |
// Move missile toward target | |
const dir = new THREE.Vector3().subVectors(missile.target.position, missile.mesh.position).normalize(); | |
missile.mesh.position.add(dir.multiplyScalar(missile.speed * delta)); | |
missile.mesh.lookAt(missile.target.position); | |
// Check for impact | |
if (missile.mesh.position.distanceTo(missile.target.position) < 2) { | |
createExplosion(missile.mesh.position); | |
if (Math.random() < 0.9 && !missile.target.userData.destroyed) { | |
missile.target.userData.health -= 3; | |
if (missile.target.userData.health <= 0) { | |
missile.target.userData.destroyed = true; | |
createExplosion(missile.target.position); | |
scene.remove(missile.target); | |
} | |
} | |
scene.remove(missile.mesh); | |
missiles.splice(i, 1); | |
} | |
} | |
// Update explosions | |
for (let i = explosions.length - 1; i >= 0; i--) { | |
const explosion = explosions[i]; | |
const age = elapsed - explosion.createTime; | |
if (age > 1.5) { | |
scene.remove(explosion.mesh); | |
explosions.splice(i, 1); | |
continue; | |
} | |
// Animate core flash - quick bright fade | |
explosion.core.material.opacity = Math.max(1 - age / 0.2, 0); | |
explosion.core.scale.setScalar(1 + age * 3); | |
// Animate fireball - slower expansion and fade | |
explosion.fireball.scale.setScalar(1 + age * 4); | |
explosion.fireball.material.opacity = Math.max(0.8 - age / 1.5, 0); | |
// Animate shock wave - rapid expansion | |
explosion.shock.scale.setScalar(1 + age * 15); | |
explosion.shock.material.opacity = Math.max(0.8 - age / 0.5, 0); | |
// Animate debris particles | |
const positions = explosion.debris.geometry.attributes.position.array; | |
const velocities = explosion.debris.geometry.attributes.velocity.array; | |
for (let j = 0; j < positions.length; j += 3) { | |
positions[j] += velocities[j] * delta; | |
positions[j + 1] += velocities[j + 1] * delta; | |
positions[j + 2] += velocities[j + 2] * delta; | |
// Add slight gravity effect | |
velocities[j + 1] -= 0.5 * delta; | |
} | |
explosion.debris.geometry.attributes.position.needsUpdate = true; | |
explosion.debris.material.opacity = Math.max(1 - age / 1.5, 0); | |
} | |
// Update team1 ships | |
for (let ship of team1) { | |
if (ship.userData.destroyed) continue; | |
const otherTeam = team2; | |
// Initialize combat state if needed | |
if (!ship.userData.combatState) { | |
ship.userData.combatState = 'attack'; | |
ship.userData.stateChangeTime = elapsed; | |
ship.userData.wingman = null; | |
ship.userData.lastMissile = 0; | |
ship.userData.evasiveManeuver = null; | |
} | |
// Choose or change target occasionally | |
if (!ship.userData.target || ship.userData.target.userData.destroyed || Math.random() < 0.01) { | |
const aliveEnemies = otherTeam.filter(s => !s.userData.destroyed); | |
if (aliveEnemies.length > 0) { | |
ship.userData.target = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)]; | |
} else { | |
continue; // No enemies left | |
} | |
} | |
// Combat state machine | |
const timeSinceStateChange = elapsed - ship.userData.stateChangeTime; | |
if (timeSinceStateChange > 3 + Math.random() * 2) { | |
const states = ['attack', 'evade', 'formation', 'strafe']; | |
ship.userData.combatState = states[Math.floor(Math.random() * states.length)]; | |
ship.userData.stateChangeTime = elapsed; | |
ship.userData.evasiveManeuver = null; | |
} | |
// Formation flying - find a wingman | |
if (ship.userData.combatState === 'formation' && !ship.userData.wingman) { | |
const teammates = team1.filter(t => !t.userData.destroyed); | |
const nearby = teammates.filter(t => | |
t !== ship && | |
t.position.distanceTo(ship.position) < 15 && | |
(!t.userData.wingman || t.userData.wingman === ship) | |
); | |
if (nearby.length > 0) { | |
ship.userData.wingman = nearby[0]; | |
nearby[0].userData.wingman = ship; | |
} | |
} | |
let targetPosition = ship.userData.target.position.clone(); | |
let speed = 15 + Math.random() * 10; | |
// Behavior based on combat state | |
switch (ship.userData.combatState) { | |
case 'attack': | |
// Aggressive pursuit | |
speed = 20 + Math.random() * 10; | |
break; | |
case 'evade': | |
// Evasive maneuvers | |
if (!ship.userData.evasiveManeuver) { | |
ship.userData.evasiveManeuver = new THREE.Vector3( | |
(Math.random() - 0.5) * 2, | |
(Math.random() - 0.5) * 2, | |
(Math.random() - 0.5) * 2 | |
).normalize(); | |
} | |
targetPosition = ship.position.clone().add(ship.userData.evasiveManeuver.clone().multiplyScalar(20)); | |
speed = 25 + Math.random() * 15; | |
break; | |
case 'formation': | |
// Formation flying | |
if (ship.userData.wingman && !ship.userData.wingman.userData.destroyed) { | |
const offset = new THREE.Vector3( | |
(Math.random() - 0.5) * 8, | |
(Math.random() - 0.5) * 8, | |
(Math.random() - 0.5) * 8 | |
); | |
targetPosition = ship.userData.wingman.position.clone().add(offset); | |
} | |
speed = 12 + Math.random() * 6; | |
break; | |
case 'strafe': | |
// Strafing run | |
const perpendicular = new THREE.Vector3().crossVectors( | |
new THREE.Vector3().subVectors(ship.userData.target.position, ship.position).normalize(), | |
new THREE.Vector3(0, 1, 0) | |
).normalize(); | |
targetPosition = ship.position.clone().add(perpendicular.multiplyScalar(15)); | |
speed = 18 + Math.random() * 8; | |
break; | |
} | |
// Movement | |
if (progress < 0.1) { | |
// Approach phase | |
const approachSpeed = 25; | |
ship.position.x += (ship.userData.team === 1 ? 1 : -1) * approachSpeed * delta; | |
} else { | |
// Combat movement | |
const dir = new THREE.Vector3().subVectors(targetPosition, ship.position).normalize(); | |
ship.position.add(dir.clone().multiplyScalar(speed * delta)); | |
// Add barrel rolls and combat maneuvers | |
if (Math.random() < 0.02) { | |
ship.userData.rollSpeed = Math.PI * (2 + Math.random() * 4) * (Math.random() < 0.5 ? 1 : -1); | |
} | |
// Pull back if too far from center | |
const dist = ship.position.length(); | |
if (dist > 60) { | |
const toCenter = new THREE.Vector3().copy(ship.position).negate().normalize(); | |
ship.position.add(toCenter.multiplyScalar(8 * delta)); | |
} | |
} | |
// Look at target with some lead | |
const targetVelocity = new THREE.Vector3().subVectors( | |
ship.userData.target.position, | |
ship.userData.target.userData.lastPosition || ship.userData.target.position | |
).multiplyScalar(30); | |
const leadTarget = ship.userData.target.position.clone().add(targetVelocity); | |
ship.lookAt(leadTarget); | |
// Store last position for velocity calculation | |
ship.userData.lastPosition = ship.position.clone(); | |
// Add roll | |
ship.rotateZ(delta * ship.userData.rollSpeed); | |
// Shooting - vary weapon types | |
const shootCooldown = ship.userData.combatState === 'attack' ? 0.3 : 0.8; | |
if (elapsed - ship.userData.lastShoot > shootCooldown + Math.random() * 0.5) { | |
const distance = ship.position.distanceTo(ship.userData.target.position); | |
if (distance < 25 && Math.random() < 0.7) { | |
shootLaser(ship, ship.userData.target); | |
} else if (distance < 40 && elapsed - ship.userData.lastMissile > 2 + Math.random() * 3) { | |
shootMissile(ship, ship.userData.target); | |
ship.userData.lastMissile = elapsed; | |
} | |
ship.userData.lastShoot = elapsed; | |
} | |
} | |
// Update team2 ships (duplicate logic for team2) | |
for (let ship of team2) { | |
if (ship.userData.destroyed) continue; | |
const otherTeam = team1; | |
// Initialize combat state if needed | |
if (!ship.userData.combatState) { | |
ship.userData.combatState = 'attack'; | |
ship.userData.stateChangeTime = elapsed; | |
ship.userData.wingman = null; | |
ship.userData.lastMissile = 0; | |
ship.userData.evasiveManeuver = null; | |
} | |
// Choose or change target occasionally | |
if (!ship.userData.target || ship.userData.target.userData.destroyed || Math.random() < 0.01) { | |
const aliveEnemies = otherTeam.filter(s => !s.userData.destroyed); | |
if (aliveEnemies.length > 0) { | |
ship.userData.target = aliveEnemies[Math.floor(Math.random() * aliveEnemies.length)]; | |
} else { | |
continue; // No enemies left | |
} | |
} | |
// Combat state machine | |
const timeSinceStateChange = elapsed - ship.userData.stateChangeTime; | |
if (timeSinceStateChange > 3 + Math.random() * 2) { | |
const states = ['attack', 'evade', 'formation', 'strafe']; | |
ship.userData.combatState = states[Math.floor(Math.random() * states.length)]; | |
ship.userData.stateChangeTime = elapsed; | |
ship.userData.evasiveManeuver = null; | |
} | |
// Formation flying - find a wingman | |
if (ship.userData.combatState === 'formation' && !ship.userData.wingman) { | |
const teammates = team2.filter(t => !t.userData.destroyed); | |
const nearby = teammates.filter(t => | |
t !== ship && | |
t.position.distanceTo(ship.position) < 15 && | |
(!t.userData.wingman || t.userData.wingman === ship) | |
); | |
if (nearby.length > 0) { | |
ship.userData.wingman = nearby[0]; | |
nearby[0].userData.wingman = ship; | |
} | |
} | |
let targetPosition = ship.userData.target.position.clone(); | |
let speed = 15 + Math.random() * 10; | |
// Behavior based on combat state | |
switch (ship.userData.combatState) { | |
case 'attack': | |
// Aggressive pursuit | |
speed = 20 + Math.random() * 10; | |
break; | |
case 'evade': | |
// Evasive maneuvers | |
if (!ship.userData.evasiveManeuver) { | |
ship.userData.evasiveManeuver = new THREE.Vector3( | |
(Math.random() - 0.5) * 2, | |
(Math.random() - 0.5) * 2, | |
(Math.random() - 0.5) * 2 | |
).normalize(); | |
} | |
targetPosition = ship.position.clone().add(ship.userData.evasiveManeuver.clone().multiplyScalar(20)); | |
speed = 25 + Math.random() * 15; | |
break; | |
case 'formation': | |
// Formation flying | |
if (ship.userData.wingman && !ship.userData.wingman.userData.destroyed) { | |
const offset = new THREE.Vector3( | |
(Math.random() - 0.5) * 8, | |
(Math.random() - 0.5) * 8, | |
(Math.random() - 0.5) * 8 | |
); | |
targetPosition = ship.userData.wingman.position.clone().add(offset); | |
} | |
speed = 12 + Math.random() * 6; | |
break; | |
case 'strafe': | |
// Strafing run | |
const perpendicular = new THREE.Vector3().crossVectors( | |
new THREE.Vector3().subVectors(ship.userData.target.position, ship.position).normalize(), | |
new THREE.Vector3(0, 1, 0) | |
).normalize(); | |
targetPosition = ship.position.clone().add(perpendicular.multiplyScalar(15)); | |
speed = 18 + Math.random() * 8; | |
break; | |
} | |
// Movement | |
if (progress < 0.1) { | |
// Approach phase | |
const approachSpeed = 25; | |
ship.position.x += (ship.userData.team === 1 ? 1 : -1) * approachSpeed * delta; | |
} else { | |
// Combat movement | |
const dir = new THREE.Vector3().subVectors(targetPosition, ship.position).normalize(); | |
ship.position.add(dir.clone().multiplyScalar(speed * delta)); | |
// Add barrel rolls and combat maneuvers | |
if (Math.random() < 0.02) { | |
ship.userData.rollSpeed = Math.PI * (2 + Math.random() * 4) * (Math.random() < 0.5 ? 1 : -1); | |
} | |
// Pull back if too far from center | |
const dist = ship.position.length(); | |
if (dist > 60) { | |
const toCenter = new THREE.Vector3().copy(ship.position).negate().normalize(); | |
ship.position.add(toCenter.multiplyScalar(8 * delta)); | |
} | |
} | |
// Look at target with some lead | |
const targetVelocity = new THREE.Vector3().subVectors( | |
ship.userData.target.position, | |
ship.userData.target.userData.lastPosition || ship.userData.target.position | |
).multiplyScalar(30); | |
const leadTarget = ship.userData.target.position.clone().add(targetVelocity); | |
ship.lookAt(leadTarget); | |
// Store last position for velocity calculation | |
ship.userData.lastPosition = ship.position.clone(); | |
// Add roll | |
ship.rotateZ(delta * ship.userData.rollSpeed); | |
// Shooting - vary weapon types | |
const shootCooldown = ship.userData.combatState === 'attack' ? 0.3 : 0.8; | |
if (elapsed - ship.userData.lastShoot > shootCooldown + Math.random() * 0.5) { | |
const distance = ship.position.distanceTo(ship.userData.target.position); | |
if (distance < 25 && Math.random() < 0.7) { | |
shootLaser(ship, ship.userData.target); | |
} else if (distance < 40 && elapsed - ship.userData.lastMissile > 2 + Math.random() * 3) { | |
shootMissile(ship, ship.userData.target); | |
ship.userData.lastMissile = elapsed; | |
} | |
ship.userData.lastShoot = elapsed; | |
} | |
} | |
// Clean up destroyed ships from teams periodically | |
if (Math.random() < 0.01) { | |
for (let i = team1.length - 1; i >= 0; i--) { | |
if (team1[i].userData.destroyed) { | |
team1.splice(i, 1); | |
} | |
} | |
for (let i = team2.length - 1; i >= 0; i--) { | |
if (team2[i].userData.destroyed) { | |
team2.splice(i, 1); | |
} | |
} | |
} | |
controls.update(); | |
composer.render(); | |
} | |
animate(); | |
window.addEventListener('resize', () => { | |
camera.aspect = window.innerWidth / window.innerHeight; | |
camera.updateProjectionMatrix(); | |
renderer.setSize(window.innerWidth, window.innerHeight); | |
composer.setSize(window.innerWidth, window.innerHeight); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment