Skip to content

Instantly share code, notes, and snippets.

@CharlieGreenman
Created July 10, 2025 07:56
Show Gist options
  • Save CharlieGreenman/372cf4e97cac2694676881a5fdfeca49 to your computer and use it in GitHub Desktop.
Save CharlieGreenman/372cf4e97cac2694676881a5fdfeca49 to your computer and use it in GitHub Desktop.
Grok 4 outerspace code 3d short video x space dogfight
<!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