SPACE #3 — Space Ships, Mothership AI & Archetypes: https://pastebin.com/GDAS0KL8
Source:
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Space — Version #3: Motherships, Archetypes & Combat</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<style>
html,body{height:100%;margin:0;background:#04050a;color:#cfe;overflow:hidden;font-family:Inter,system-ui, -apple-system, "Segoe UI", Roboto;}
canvas{display:block;width:100vw;height:100vh;}
#hud{position:fixed;left:10px;top:10px;z-index:30;background:rgba(0,0,0,0.45);padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.03)}
#hud button{margin:4px;padding:6px 8px;border-radius:6px;border:0;background:#0b1220;color:#9ff;cursor:pointer}
.note{font-size:12px;color:#9aa;margin-top:6px}
/* pie menu */
.pie{position:fixed;pointer-events:none;z-index:35}
.pie .slice{position:absolute;width:120px;height:120px;margin:-60px;border-radius:50%;transform-origin:60px 60px;display:flex;align-items:center;justify-content:center;pointer-events:auto;color:#012;font-weight:700; cursor:pointer;}
.label{pointer-events:none;font-size:12px;text-align:center;color:#ccf}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div id="hud">
<div style="font-weight:700;color:#7ef">SPACE v3 — Motherships & Archetypes</div>
<div style="margin-top:6px">
<button id="spawnEnemy">Spawn Enemy Mothership</button>
<button id="center">Center</button>
<button id="clear">Clear</button>
<button id="spawnChaos">Chaos Wave</button>
</div>
<div class="note">Right-click — pie spawn. Left-click — select. WASD — control selected ship (if player).</div>
</div>
<script>
/* ---------- Setup ---------- */
const canvas = document.getElementById('c'), ctx = canvas.getContext('2d');
let W = canvas.width = innerWidth, H = canvas.height = innerHeight;
onresize = ()=>{ W = canvas.width = innerWidth; H = canvas.height = innerHeight; };
let running = true, followId = null;
const G = 0.0007; // gravity base
const MAX_PROJECTILES = 400;
let entities = []; // unified: ships, celestials, projectiles, effects
let nextId = 1;
/* ---------- Helpers ---------- */
function rand(range){ return (Math.random()-0.5)*2*range; }
function dist2(a,b){ const dx=a.x-b.x, dy=a.y-b.y; return dx*dx+dy*dy; }
function mag(vx,vy){ return Math.hypot(vx,vy); }
function hexToRGBA(hex, a=1){ const h=hex.replace('#',''); const r=parseInt(h.slice(0,2),16), g=parseInt(h.slice(2,4),16), b=parseInt(h.slice(4,6),16); return `rgba(${r},${g},${b},${a})`; }
/* ---------- Entity creation ---------- */
function mkMothership(x,y,side='enemy',player=false, archetype='carrier'){
// mothership: spawns drones, heavy hull, basic weapons
const ms = { id: nextId++, kind:'ship', subtype:'mothership', archetype, side, playerControl:player, x,y, vx:rand(0.5), vy:rand(0.5),
angle:0, mass:5000, size:24, hp:1200, maxHp:1200, spawnTimer:0, spawnRate:2.5, target:null };
entities.push(ms); return ms;
}
function mkShip(x,y,type='fighter',side='enemy',parent=null,player=false, arche='fighter'){
// ship archetypes: fighter, brawler, sniper, support, kamikaze
let archetype = arche;
const def = {
fighter: { mass:160, size:9, hp:120, speed:2.0, fireRate:0.5, weapon:'bullet', range:260, damage:20, aggression:0.6 },
brawler: { mass:300, size:12, hp:260, speed:1.4, fireRate:0.25, weapon:'ram', range:28, damage:80, aggression:0.9 },
sniper: { mass:140, size:8, hp:90, speed:1.6, fireRate:1.2, weapon:'laser', range:520, damage:45, aggression:0.4 },
support:{ mass:200, size:10, hp:150, speed:1.5, fireRate:1.0, weapon:'repair', range:160, damage:-18, aggression:0.2 },
kamikaze:{ mass:120, size:8, hp:80, speed:2.6, fireRate:0.0, weapon:'ram', range:18, damage:200, aggression:1.0 }
}[archetype] || def.fighter;
const s = Object.assign({
id: nextId++, kind:'ship', subtype:type, archetype, side, parent, playerControl:player,
x,y, vx:rand(0.6), vy:rand(0.6), angle:Math.random()*Math.PI*2,
lastShot:0, target:null
}, def);
entities.push(s); return s;
}
function mkCelestial(x,y,mass,subtype='planet',fixed=false,color='#ccc'){
const c = { id: nextId++, kind:'celestial', subtype, x,y, vx:0, vy:0, mass, fixed, color, size: Math.cbrt(mass)*0.7 };
entities.push(c); return c;
}
function mkProjectile(x,y,vx,vy,owner,weapon='bullet',life=2){
if(entities.filter(e=>e.kind==='proj').length > MAX_PROJECTILES) return null;
const p = { id: nextId++, kind:'proj', x,y, vx,vy, owner, weapon, life, damage: weapon==='laser'?30:18, size: weapon==='laser'?2:2, created: performance.now() };
entities.push(p); return p;
}
function mkEffect(x,y,type='expl',life=0.5){
const e = { id: nextId++, kind:'fx', x,y, type, life, t:0 };
entities.push(e); return e;
}
/* ---------- Initial demo ---------- */
mkCelestial(W*0.5,H*0.5, 300000, 'star', true, '#ffd166');
mkMothership(W*0.5+220, H*0.5, 'enemy', false, 'carrier');
mkMothership(W*0.5-300, H*0.5, 'enemy', false, 'aggressive');
for(let i=0;i<8;i++) mkShip(W*0.5+rand(300), H*0.5+rand(300), 'fighter','enemy', null, false, ['fighter','brawler','sniper','kamikaze'][Math.floor(Math.random()*4)] );
/* ---------- Combat & AI ---------- */
function chooseTarget(source){
// prefer opposite side ships within some range, else nearest anything else
const enemies = entities.filter(e=>e.kind==='ship' && e.side !== source.side && e !== source);
if(enemies.length===0) return null;
// prioritize by proximity and archetype: snipers dodge close, brawlers prefer close
enemies.sort((a,b)=>{
const da = Math.hypot(a.x-source.x,a.y-source.y), db = Math.hypot(b.x-source.x,b.y-source.y);
const scoreA = da * (a.archetype==='sniper'?1.4:1) - (a.hp || 0)*0.001;
const scoreB = db * (b.archetype==='sniper'?1.4:1) - (b.hp || 0)*0.001;
return scoreA - scoreB;
});
return enemies[0];
}
/* ---------- Physics step ---------- */
function step(dt){
// gravity from celestials affects ships and projectiles slightly
const celestials = entities.filter(e=>e.kind==='celestial');
const ships = entities.filter(e=>e.kind==='ship');
// ships acceleration from celestials (pairwise)
for(const s of ships){
let ax = 0, ay = 0;
for(const c of celestials){
const dx = c.x - s.x, dy = c.y - s.y;
const r2 = dx*dx + dy*dy + 64;
const inv = 1/Math.sqrt(r2);
const f = G * c.mass * s.mass / r2;
ax += f * dx * inv / s.mass; ay += f * dy * inv / s.mass;
}
s.vx += ax * dt * 60; s.vy += ay * dt * 60;
}
// Ship AI + actions
for(const s of ships){
// reduce target if dead
if(s.target && (!entities.find(e=>e.id===s.target.id && e.kind==='ship' && e.hp>0))) s.target=null;
// choose target occasionally
if(!s.target && Math.random() < 0.02) s.target = chooseTarget(s);
// archetype behaviors
if(!s.playerControl){
// aggression factor influences picking fights
if(s.archetype === 'fighter'){
// attempt to intercept: steer towards target
if(s.target){
const dx=s.target.x-s.x, dy=s.target.y-s.y, r=Math.hypot(dx,dy)+0.01;
const desiredVel = s.speed;
s.vx += (dx/r) * 0.02; s.vy += (dy/r) * 0.02;
} else {
// patrol
s.vx += Math.cos((s.id + performance.now()*0.0005))*0.001;
s.vy += Math.sin((s.id + performance.now()*0.0004))*0.001;
}
} else if(s.archetype === 'brawler'){
if(s.target){
const dx=s.target.x-s.x, dy=s.target.y-s.y, r=Math.hypot(dx,dy)+0.01;
// charge in
s.vx += (dx/r) * 0.03; s.vy += (dy/r) * 0.03;
} else {
s.vx += Math.cos(s.aiTimer || 0)*0.001; s.vy += Math.sin((s.aiTimer||0) + s.id*0.1)*0.001;
}
} else if(s.archetype === 'sniper'){
if(s.target){
const dx=s.target.x-s.x, dy=s.target.y-s.y, r=Math.hypot(dx,dy)+0.01;
// maintain distance: back up if too close
if(r < s.range * 0.6){ s.vx -= (dx/r)*0.02; s.vy -= (dy/r)*0.02; }
else { s.vx += (Math.cos(s.id + performance.now()*0.0004))*0.0006; s.vy += (Math.sin(performance.now()*0.0006 + s.id))*0.0006; }
} else { s.vx *= 0.999; s.vy *= 0.999; }
} else if(s.archetype === 'support'){
// find allied damaged ship to approach and repair (negative damage)
let ally = entities.filter(e=>e.kind==='ship' && e.side === s.side && e.hp < e.maxHp).sort((a,b)=>a.hp - b.hp)[0];
if(ally){
const dx=ally.x-s.x, dy=ally.y-s.y, r=Math.hypot(dx,dy)+0.01;
s.vx += dx/r*0.015; s.vy += dy/r*0.015;
if(r < s.range*0.6 && (performance.now() - (s.lastShot||0) > s.fireRate*1000)){
// "repair" by firing negative-damage projectile (instant apply)
ally.hp = Math.min(ally.maxHp, ally.hp + Math.abs(s.damage));
s.lastShot = performance.now();
}
} else {
s.vx += Math.cos(s.id*0.3 + performance.now()*0.0003)*0.0006;
}
} else if(s.archetype === 'kamikaze'){
if(s.target){
const dx=s.target.x-s.x, dy=s.target.y-s.y, r=Math.hypot(dx,dy)+0.01;
s.vx += (dx/r)*0.05; s.vy += (dy/r)*0.05;
} else { s.vx += rand(0.01); s.vy += rand(0.01); }
}
} else {
// player-controlled ships: thrust managed elsewhere (keyboard)
}
// shooting logic (non-ram weapons)
if(s.weapon !== 'ram'){
const now = performance.now();
s.lastShot = s.lastShot || 0;
if(s.target && s.weapon !== 'repair' && (now - s.lastShot) > (s.fireRate*1000)){
// if in range, fire projectile
const dx = s.target.x - s.x, dy = s.target.y - s.y;
const r = Math.hypot(dx,dy);
if(r < s.range){
// fire projectile towards predicted target (simple lead)
const lead = 0.5; // crude
const tx = s.target.x + s.target.vx * lead, ty = s.target.y + s.target.vy * lead;
const dirx = tx - s.x, diry = ty - s.y; const magd = Math.hypot(dirx,diry)+0.001;
const speed = s.weapon === 'laser' ? 7.5 : 6.0;
mkProjectile(s.x + (dirx/magd)*s.size, s.y + (diry/magd)*s.size, (dirx/magd)*speed + s.vx, (diry/magd)*speed + s.vy, s, s.weapon);
s.lastShot = now;
}
}
}
// spawn drones if mothership
if(s.subtype === 'mothership'){
s.spawnTimer += dt;
const spawnInterval = (s.archetype === 'carrier')? s.spawnRate : 6.0;
if(s.spawnTimer > spawnInterval){
s.spawnTimer = 0;
// spawn type choice depends on archetype
const droneTypes = s.archetype === 'aggressive' ? ['brawler','fighter'] : (s.archetype === 'carrier'? ['fighter','support'] : ['fighter']);
const choice = droneTypes[Math.floor(Math.random()*droneTypes.length)];
mkShip(s.x + rand(30), s.y + rand(30), 'drone', s.side, s, false, choice);
}
}
// integrate movement
s.x += s.vx * dt * 60; s.y += s.vy * dt * 60;
// keep within map wrap
if(s.x < -300) s.x = W + 300;
if(s.x > W + 300) s.x = -300;
if(s.y < -300) s.y = H + 300;
if(s.y > H + 300) s.y = -300;
}
// projectiles: move and decay
for(const p of entities.filter(e=>e.kind==='proj')){
// gravity affect tiny bit from celestials
let ax=0, ay=0;
for(const c of celestials){
const dx = c.x - p.x, dy = c.y - p.y; const r2 = dx*dx + dy*dy + 64;
const inv = 1/Math.sqrt(r2); const f = G * c.mass / r2;
ax += f * dx * inv; ay += f * dy * inv;
}
p.vx += ax * dt * 60; p.vy += ay * dt * 60;
p.x += p.vx * dt * 60; p.y += p.vy * dt * 60;
p.life -= dt;
if(p.life <= 0) { mkEffect(p.x,p.y,'small',0.15); removeById(p.id); continue; }
// collision with ships
const hit = entities.find(e=>e.kind==='ship' && e.side !== p.owner.side && ((e.x-p.x)**2 + (e.y-p.y)**2) < (e.size+4)**2);
if(hit){
// apply damage
hit.hp -= p.damage;
mkEffect(p.x,p.y,'hit',0.18);
removeById(p.id);
if(hit.hp <= 0){ shipDestroyed(hit); }
}
}
// ship-ship collisions (ram/hull damage)
for(const a of ships){
for(const b of ships){
if(a.id >= b.id) continue;
const r = a.size + b.size;
const dx = b.x - a.x, dy = b.y - a.y;
if(dx*dx + dy*dy < r*r){
// collision: apply bump impulse and damage if aggressive/hull-types
const relx = b.vx - a.vx, rely = b.vy - a.vy;
const impact = Math.min(200, Math.hypot(relx,rely) * (a.mass+b.mass)*0.02);
a.hp -= impact*0.02; b.hp -= impact*0.02;
// simple separation impulse
const nx = dx/Math.hypot(dx,dy || 1), ny = dy/Math.hypot(dx,dy || 1);
a.vx -= nx*0.5; a.vy -= ny*0.5; b.vx += nx*0.5; b.vy += ny*0.5;
mkEffect((a.x+b.x)/2,(a.y+b.y)/2,'spark',0.16);
if(a.hp <= 0) shipDestroyed(a);
if(b.hp <= 0) shipDestroyed(b);
}
}
}
// effects lifecycle
for(const fx of entities.filter(e=>e.kind==='fx')){ fx.t += dt; if(fx.t > fx.life) removeById(fx.id); }
// cleanup celestials (none for now)
}
/* ---------- Remove & destruction ---------- */
function removeById(id){
const idx = entities.findIndex(e=>e.id===id); if(idx>=0) entities.splice(idx,1);
}
function shipDestroyed(s){
mkEffect(s.x,s.y,'expl',0.8);
// spawn debris / small projectiles
for(let i=0;i<6;i++){
mkProjectile(s.x, s.y, rand(6), rand(6), s, 'shrap', 0.8);
}
removeById(s.id);
}
/* ---------- Draw ---------- */
function draw(){
ctx.fillStyle = 'rgba(1,2,6,0.6)'; ctx.fillRect(0,0,W,H);
// starfield
ctx.fillStyle = 'rgba(255,255,255,0.03)';
for(let i=0;i<140;i++){ const x=(i*71.1)%W, y=(i*127.3)%H; ctx.fillRect(x,y,1,1); }
ctx.save();
if(followId){
const f = entities.find(e=>e.id===followId); if(f) ctx.translate(W/2 - f.x, H/2 - f.y);
}
// celestials
for(const c of entities.filter(e=>e.kind==='celestial')){
const r = Math.max(2, c.size);
if(c.subtype === 'star'){ const g = ctx.createRadialGradient(c.x,c.y,0,c.x,c.y,r*7); g.addColorStop(0,hexToRGBA(c.color,0.5)); g.addColorStop(1,hexToRGBA(c.color,0)); ctx.fillStyle=g; ctx.beginPath(); ctx.arc(c.x,c.y,r*7,0,Math.PI*2); ctx.fill(); }
ctx.beginPath(); ctx.fillStyle = c.color; ctx.arc(c.x,c.y,r,0,Math.PI*2); ctx.fill(); ctx.lineWidth=1; ctx.strokeStyle='rgba(255,255,255,0.04)'; ctx.stroke();
}
// ships
for(const s of entities.filter(e=>e.kind==='ship')){
ctx.save(); ctx.translate(s.x,s.y);
const ang = Math.atan2(s.vy,s.vx);
ctx.rotate(ang + Math.PI/2);
// hull color by side
const col = s.side === 'player' ? '#6ee7b7' : (s.archetype==='brawler' ? '#ff9b6b' : '#ff6b6b');
// draw ship triangle (size varies)
ctx.beginPath(); ctx.moveTo(0, -s.size); ctx.lineTo(-s.size*0.6, s.size); ctx.lineTo(s.size*0.6, s.size); ctx.closePath();
ctx.fillStyle = col; ctx.fill();
ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(255,255,255,0.06)'; ctx.stroke();
// health bar
const hW = s.size*2; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(-hW/2, s.size+6, hW, 5);
ctx.fillStyle = 'rgba(0,255,100,0.9)'; ctx.fillRect(-hW/2, s.size+6, hW * Math.max(0, (s.hp||0)/s.maxHp), 5);
ctx.restore();
}
// projectiles
for(const p of entities.filter(e=>e.kind==='proj')){
ctx.beginPath(); ctx.fillStyle = p.weapon === 'laser' ? 'rgba(160,220,255,0.95)' : 'rgba(255,220,150,0.95)';
ctx.arc(p.x,p.y, p.size, 0, Math.PI*2); ctx.fill();
}
// effects
for(const fx of entities.filter(e=>e.kind==='fx')){
if(fx.type==='expl'){
const s = (fx.t/fx.life); const r= (s*30 + 10);
ctx.beginPath(); ctx.fillStyle = `rgba(255,150,50,${1-s})`; ctx.arc(fx.x,fx.y,r,0,Math.PI*2); ctx.fill();
}
}
ctx.restore();
// HUD bottom-left
ctx.fillStyle = 'rgba(255,255,255,0.06)'; ctx.font='12px system-ui';
ctx.fillText(`Entities: ${entities.length} Follow: ${followId||'none'} Running: ${running}`, 12, H-10);
}
/* ---------- Loop ---------- */
let last = performance.now();
function loop(now){
const dt = Math.min(0.033, (now-last)/1000); last = now;
if(running) step(dt);
draw();
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
/* ---------- Selection / Pie Menu / Input ---------- */
let mouse = { x:0,y:0,down:false,btn:0 }, pie=null, selectedId=null, dragging=null, dragOffset={x:0,y:0}, spawnDrag=null;
canvas.addEventListener('mousemove', e=>{
const r = canvas.getBoundingClientRect(); mouse.x = e.clientX - r.left; mouse.y = e.clientY - r.top;
if(dragging){ const world = screenToWorld(mouse.x, mouse.y); dragging.x = world.x + dragOffset.x; dragging.y = world.y + dragOffset.y; dragging.vx=dragging.vy=0; }
if(spawnDrag) spawnDrag.current = { x:mouse.x, y:mouse.y };
});
canvas.addEventListener('mousedown', e=>{
mouse.down=true; mouse.btn=e.button;
const r = canvas.getBoundingClientRect(); const sx = e.clientX - r.left, sy = e.clientY - r.top;
if(e.button === 2){ openPie(sx,sy); return; }
const world = screenToWorld(sx,sy);
const hit = entities.slice().reverse().find(ent=>{ const rr = (ent.kind==='ship'? ent.size : Math.max(4, ent.size)); const dx = ent.x - world.x, dy = ent.y - world.y; return dx*dx + dy*dy <= rr*rr; });
if(hit){
selectedId = hit.id;
if(hit.kind === 'celestial'){ dragging = hit; dragOffset.x = hit.x - world.x; dragOffset.y = hit.y - world.y; }
if(hit.kind === 'ship'){ if(hit.side === 'player') hit.playerControl = true; }
closePie();
} else selectedId = null;
});
canvas.addEventListener('mouseup', e=>{
mouse.down=false; dragging=null;
if(spawnDrag){ const pd = spawnDrag; spawnDrag=null; const w=screenToWorld(pd.start.x,pd.start.y); const v = {x:(pd.start.x-pd.current.x)*0.02, y:(pd.start.y-pd.current.y)*0.02}; const s = mkShip(w.x,w.y,'fighter','player',null,true,'fighter'); s.vx=v.x; s.vy=v.y; selectedId = s.id; closePie(); }
});
canvas.addEventListener('contextmenu', e=>e.preventDefault());
function screenToWorld(sx,sy){ if(followId){ const f = entities.find(e=>e.id===followId); if(f) return {x: sx - W/2 + f.x, y: sy - H/2 + f.y}; } return {x:sx,y:sy}; }
/* Pie menu */
function openPie(sx,sy){
closePie();
pie = document.createElement('div'); pie.className='pie'; pie.style.left = sx+'px'; pie.style.top = sy+'px';
const options = [
{label:'Player Mothership', action:()=>{ const w=screenToWorld(sx,sy); mkMothership(w.x,w.y,'player',true,'carrier'); }},
{label:'Player Fighter (drag)', actionStart:()=>{ spawnDrag = { start:{x:sx,y:sy}, current:{x:sx,y:sy} }; }},
{label:'Enemy Mothership', action:()=>{ const w=screenToWorld(sx,sy); mkMothership(w.x,w.y,'enemy',false,['aggressive','carrier'][Math.floor(Math.random()*2)]); }},
{label:'Place Star', action:()=>{ const w=screenToWorld(sx,sy); mkCelestial(w.x,w.y,220000,'star',true,'#ffd166'); }},
{label:'Place Planet', action:()=>{ const w=screenToWorld(sx,sy); mkCelestial(w.x,w.y,14000,'planet',false,'#88c'); }},
{label:'Place Asteroid Field', action:()=>{ const w=screenToWorld(sx,sy); for(let i=0;i<30;i++){ mkCelestial(w.x+rand(140), w.y+rand(140), 40+Math.random()*120,'asteroid', false, '#b7a'); } }},
{label:'Chaos (wave)', action:()=>{ for(let i=0;i<20;i++){ mkShip(Math.random()*W, Math.random()*H,'fighter','enemy', null, false, ['fighter','brawler','sniper'][Math.floor(Math.random()*3)]); } }}
];
const N = options.length;
for(let i=0;i<N;i++){
const s = document.createElement('div'); s.className='slice'; const angle = -Math.PI/2 + (i/N)*Math.PI*2;
s.style.transform = `rotate(${angle}rad) translate(${80}px) rotate(${-angle}rad)`;
s.style.background = 'linear-gradient(180deg, rgba(255,255,255,0.02), rgba(0,0,0,0.2))'; s.style.border = '1px solid rgba(255,255,255,0.04)';
s.innerHTML = `<div class="label">${options[i].label}</div>`;
s.onclick = (ev)=>{ ev.stopPropagation(); if(options[i].action) options[i].action(); closePie(); };
s.onmousedown = (ev)=>{ ev.stopPropagation(); if(options[i].actionStart) options[i].actionStart(); };
pie.appendChild(s);
}
document.body.appendChild(pie); setTimeout(()=>window.addEventListener('mousedown', onDocDown), 10);
}
function closePie(){ if(pie){ pie.remove(); pie=null; spawnDrag=null; window.removeEventListener('mousedown', onDocDown); } }
function onDocDown(){ closePie(); }
/* ---------- Keyboard controls (player ship) ---------- */
const keys = {};
window.addEventListener('keydown', e=>{ keys[e.key.toLowerCase()] = true; if(e.key==='f' || e.key==='F'){ cycleFollow(); } if(e.key==='p'||e.key==='P'){ running=!running; } if(e.key==='c'||e.key==='C'){ entities = []; nextId=1; } });
window.addEventListener('keyup', e=> keys[e.key.toLowerCase()] = false );
function cycleFollow(){ const ships = entities.filter(e=>e.kind==='ship'); if(ships.length===0){ followId = null; return; } let idx = ships.findIndex(s=>s.id===followId); idx = (idx+1)%ships.length; followId = ships[idx].id; }
// Apply player thrust if selected ship is playerControl
setInterval(()=>{
if(!selectedId) return;
const s = entities.find(e=>e.id===selectedId && e.kind==='ship');
if(!s) return;
if(s.playerControl){
// rotate with A/D, thrust with W, brake with S
if(keys['a']) s.angle -= 0.12;
if(keys['d']) s.angle += 0.12;
if(keys['w']) { s.vx += Math.cos(s.angle)*0.06; s.vy += Math.sin(s.angle)*0.06; }
if(keys['s']) { s.vx *= 0.98; s.vy *= 0.98; }
// shoot with space
if(keys[' ']) {
const now = performance.now();
if((now - (s.lastShot||0)) > (s.fireRate*1000 || 250)){
const speed = 7;
const vx = Math.cos(s.angle)*speed + (s.vx||0), vy = Math.sin(s.angle)*speed + (s.vy||0);
mkProjectile(s.x + Math.cos(s.angle)*s.size, s.y + Math.sin(s.angle)*s.size, vx, vy, s, s.weapon || 'bullet', 2.2);
s.lastShot = now;
}
}
} else {
// toggle control with T
if(keys['t']){ s.playerControl = !s.playerControl; keys['t']=false; }
}
}, 40);
/* ---------- Buttons ---------- */
document.getElementById('spawnEnemy').onclick = ()=> mkMothership(Math.random()*W, Math.random()*H, 'enemy', false, ['carrier','aggressive'][Math.floor(Math.random()*2)]);
document.getElementById('center').onclick = ()=> followId = null;
document.getElementById('clear').onclick = ()=> entities=[], nextId=1;
document.getElementById('spawnChaos').onclick = ()=>{ for(let i=0;i<40;i++) mkShip(Math.random()*W, Math.random()*H,'fighter','enemy',null,false,['fighter','brawler','sniper'][Math.floor(Math.random()*3)]); };
/* ---------- Effects / misc ---------- */
function mkShipDefaults(){
for(const s of entities.filter(e=>e.kind==='ship')){
s.maxHp = s.hp || s.maxHp || s.hp || 100;
s.hp = s.hp || s.maxHp || 100;
// assign weapons from archetype if not present
if(!s.weapon){
if(s.archetype==='sniper'){ s.weapon='laser'; s.fireRate = 1.2; s.range = 520; s.damage = 45; s.maxHp = 90; }
else if(s.archetype==='brawler'){ s.weapon='ram'; s.fireRate = 0.2; s.range = 28; s.damage = 80; s.maxHp=260; }
else if(s.archetype==='support'){ s.weapon='repair'; s.fireRate=1.0; s.range=150; s.damage=-18; s.maxHp=150;}
else if(s.archetype==='kamikaze'){ s.weapon='ram'; s.fireRate=0; s.range=18; s.damage=200; s.maxHp=80; }
else { s.weapon='bullet'; s.fireRate=0.55; s.range=260; s.damage=20; s.maxHp=120; }
s.hp = s.maxHp = s.maxHp || s.hp;
}
}
}
setInterval(mkShipDefaults, 700);
/* ---------- Helper: remove expired dead etc ---------- */
function prune(){
// remove projectiles out of bounds long life handled via life var
// nothing here now
}
setInterval(prune, 2000);
/* ---------- Small helper effects draw overlay (spawn preview) ---------- */
function renderSpawnPreview(){
if(!spawnDrag) return;
const s = spawnDrag;
ctx.save();
ctx.strokeStyle = 'rgba(180,255,200,0.95)'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(s.start.x, s.start.y); ctx.lineTo(s.current.x, s.current.y); ctx.stroke();
ctx.fillStyle = 'rgba(180,255,200,0.9)'; ctx.beginPath(); ctx.arc(s.start.x, s.start.y, 6,0,Math.PI*2); ctx.fill();
ctx.restore();
}
/* ---------- Integrate draws with main draw to show preview ---------- */
const baseDraw = draw;
draw = function(){ baseDraw(); renderSpawnPreview(); };
/* ---------- Start message for guidance in console ---------- */
console.log('SPACE v3 ready — right-click to spawn, select ship and press WASD + Space to fire (if controlled).');
/* ---------- Done ---------- */
</script>
</body>
</html>