Space V4H2 --> https://pastebin.com/CH9K2tU3
Source:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SPACE v4H — Pie Menu FIXED</title>
<style>
:root{
--bg:#010214; --panel:rgba(2,6,12,0.78); --muted:#9aa; --accent:#7ef;
--teal:#6ee7b7; --danger:#ff6b6b;
}
*{box-sizing:border-box}
html,body{height:100%;margin:0;background:var(--bg);color:#dfe;overflow:hidden;font-family:Inter,system-ui,-apple-system,"Segoe UI",Roboto}
canvas{display:block;width:100vw;height:100vh;cursor:crosshair;background:linear-gradient(180deg,#000011,#02031a)}
#hud{position:fixed;left:12px;top:12px;z-index:60;background:var(--panel);padding:12px;border-radius:10px;border:1px solid rgba(255,255,255,0.04);backdrop-filter:blur(4px)}
#hud .title{font-weight:800;color:var(--accent);margin-bottom:6px;font-size:14px}
#hud .row{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px}
#hud button{background:#07202a;border:none;color:#bfe;padding:8px 12px;border-radius:8px;cursor:pointer;font-weight:600;font-size:12px}
#hud button:hover{background:#0a2a36}
#hud small{display:block;color:var(--muted);margin-top:6px;font-size:11px;line-height:1.3}
.pie-container{position:fixed;z-index:120;user-select:none;pointer-events:auto;transform-origin:center center;transition:transform .12s ease,opacity .12s ease}
.pie-slice{position:absolute;width:150px;height:46px;display:flex;align-items:center;justify-content:center;pointer-events:auto;cursor:pointer;border-radius:8px;padding:6px;box-shadow:0 4px 12px rgba(0,0,0,0.45)}
.pie-slice .label{color:#fff;font-weight:700;text-shadow:0 0 6px rgba(0,0,0,0.6);font-size:13px;text-align:center}
.pie-slice:hover{transform:scale(1.06)}
#mega{position:fixed;right:12px;top:12px;width:360px;max-height:calc(100vh - 24px);overflow-y:auto;z-index:80;background:linear-gradient(180deg, rgba(6,10,16,0.95), rgba(3,6,10,0.85));border-radius:12px;padding:12px;border:1px solid rgba(255,255,255,0.03);display:none;backdrop-filter:blur(10px)}
#mega h2{margin:0 0 12px 0;color:#8ef;font-size:16px}
.spawn-row{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px}
.spawn-btn{flex:1 1 calc(50% - 3px);background:#06101a;color:#bfe;padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.03);cursor:pointer;font-weight:600;font-size:12px;text-align:center}
.spawn-btn:hover{background:#0a1520;border-color:rgba(255,255,255,0.08);transform:translateY(-1px)}
#debug{position:fixed;left:12px;bottom:12px;color:var(--muted);font-size:12px;z-index:55;background:rgba(2,6,12,0.5);padding:8px;border-radius:8px;border:1px solid rgba(255,255,255,0.02)}
@media (max-width:640px){ .pie-slice{width:110px;height:40px} #mega{width:92%;right:4%;left:4%} }
</style>
</head>
<body>
<canvas id="c" tabindex=0></canvas>
<div id="hud">
<div class="title">SPACE — v4H (Pie fixed)</div>
<div class="row">
<button id="btnPause">Pause (P)</button>
<button id="btnCenter">Center View</button>
<button id="btnClear">Clear All</button>
<button id="btnMegaToggle">Toggle Menu (M)</button>
</div>
<small>Right-click = Player pie | Ctrl/Cmd+Right-click = Enemy pie | Alt+Right-click = Astro pie<br>
Left-click = select | WASD = control selected ship | Space = fire | F = cycle follow</small>
</div>
<div id="mega" aria-hidden="true">
<h2>🚀 Mega Spawner</h2>
<div class="spawn-row">
<button class="spawn-btn" data-action="playerMothership">🛸 Player Mothership</button>
<button class="spawn-btn" data-action="enemyMothership">👾 Enemy Mothership</button>
</div>
<div class="spawn-row">
<button class="spawn-btn" data-action="fighter">⚡ Fighter</button>
<button class="spawn-btn" data-action="brawler">💪 Brawler</button>
</div>
<div class="spawn-row">
<button class="spawn-btn" data-action="sniper">🎯 Sniper</button>
<button class="spawn-btn" data-action="support">🔧 Support</button>
</div>
<div class="spawn-row">
<button class="spawn-btn" data-action="kamikaze">💥 Kamikaze</button>
<button class="spawn-btn" data-action="asteroids">☄️ Asteroid Field</button>
</div>
<div class="spawn-row">
<button class="spawn-btn" data-action="star">⭐ Star</button>
<button class="spawn-btn" data-action="planet">🪐 Planet</button>
</div>
<div style="font-size:12px;color:#9aa;margin-top:8px">• Mega spawns near camera center; pies spawn at your cursor.</div>
</div>
<div id="debug">Entities: 0 — Projectiles: 0 — FPS: 0</div>
<script>
(() => {
// ---------- CONFIG ----------
const CONFIG = {
GRAVITY_BASE: 0.0007,
MAX_PROJECTILES: 900,
MAX_ENTITIES: 5000,
POOL_PROJECTILES: 1200,
SPATIAL_CELL: 220,
DT_MAX: 0.05,
FPS_INTERVAL: 1000
};
// ---------- Canvas ----------
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d', { alpha: false });
let DPR = Math.max(1, window.devicePixelRatio || 1);
function resizeCanvas(){
DPR = Math.max(1, window.devicePixelRatio || 1);
canvas.width = Math.floor(window.innerWidth * DPR);
canvas.height = Math.floor(window.innerHeight * DPR);
canvas.style.width = window.innerWidth + 'px';
canvas.style.height = window.innerHeight + 'px';
ctx.setTransform(DPR,0,0,DPR,0,0);
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
const W = () => window.innerWidth;
const H = () => window.innerHeight;
// ---------- Utils ----------
const TAU = Math.PI * 2;
const rand = (r=1) => (Math.random()-0.5)*2*r;
const hexToRGBA = (hex, a=1) => {
if(!hex) return `rgba(255,255,255,${a})`;
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})`;
};
// ---------- State ----------
let entities = [];
let nextId = 1;
let running = true;
let selectedId = null;
let followId = null;
let projCount = 0;
let lastTime = performance.now();
let fps = 0, frameCount = 0, lastFpsTime = performance.now();
const camera = { x:0, y:0, targetX:0, targetY:0, lerp:0.08 };
const mouse = { screenX: W()/2, screenY: H()/2, x:0, y:0, down:false, btn:-1 };
const TEAL = 'TEAL', RED = 'RED';
let factions = {}, sideFactions = [];
function initFactions(){
factions = {
[TEAL]: { id:TEAL, name:'TEAL', color:'#6ee7b7', members:[] },
[RED]: { id:RED, name:'RED', color:'#ff6b6b', members:[] }
};
sideFactions = [{id:'PIRATES',name:'Pirates',color:'#ffd166'},{id:'MERCHANTS',name:'Merchants',color:'#a78bfa'}];
}
initFactions();
// ---------- Pools & Spatial ----------
const projPool = [];
function poolInit(){ projPool.length=0; for(let i=0;i<CONFIG.POOL_PROJECTILES;i++) projPool.push({ _pooled:true }); }
poolInit();
function projAcquire(){ return projPool.length ? projPool.pop() : { _pooled:false }; }
function projRelease(p){ p._pooled = true; p.kind='free'; p.ownerId = 0; p.ownerSide = 'NEUTRAL'; projPool.push(p); }
const spatial = new Map();
function spatialKey(cx,cy){ return `${cx},${cy}`; }
function spatialClear(){ spatial.clear(); }
function spatialInsert(e){
const cell = CONFIG.SPATIAL_CELL;
const minX = Math.floor((e.x - (e.size||0)) / cell);
const maxX = Math.floor((e.x + (e.size||0)) / cell);
const minY = Math.floor((e.y - (e.size||0)) / cell);
const maxY = Math.floor((e.y + (e.size||0)) / cell);
for(let cx=minX; cx<=maxX; cx++){
for(let cy=minY; cy<=maxY; cy++){
const k = spatialKey(cx,cy);
if(!spatial.has(k)) spatial.set(k, []);
spatial.get(k).push(e);
}
}
}
function spatialQueryRegion(x,y,radius){
const cell = CONFIG.SPATIAL_CELL;
const minX = Math.floor((x - radius) / cell);
const maxX = Math.floor((x + radius) / cell);
const minY = Math.floor((y - radius) / cell);
const maxY = Math.floor((y + radius) / cell);
const set = new Set();
for(let cx=minX; cx<=maxX; cx++){
for(let cy=minY; cy<=maxY; cy++){
const k = spatialKey(cx,cy);
const arr = spatial.get(k);
if(arr) for(const e of arr) set.add(e);
}
}
return Array.from(set);
}
// ---------- Spawner ----------
function spawn(obj){
if(entities.length >= CONFIG.MAX_ENTITIES) return null;
const defaults = {
id: nextId++, kind:'unknown', x: Math.random()*W(), y: Math.random()*H(),
vx:0, vy:0, angle: Math.random()*TAU, size:8, hp:100, maxHp:100, side:'NEUTRAL', archetype:'generic', created:performance.now()
};
const e = Object.assign({}, defaults, obj);
entities.push(e);
if(e.side && factions[e.side]) factions[e.side].members.push(e.id);
return e;
}
const spawnActions = {
playerMothership: (x,y) => spawn({ kind:'ship', subtype:'mothership', archetype:'carrier', side:TEAL, playerControl:true, mass:5200, size:30, hp:2500, maxHp:2500, shield:600, maxShield:600, x,y, spawnTimer:0, spawnRate:2.8 }),
enemyMothership: (x,y) => spawn({ kind:'ship', subtype:'mothership', archetype:'aggressive', side:RED, mass:5200, size:30, hp:2600, maxHp:2600, shield:300, maxShield:300, x,y, spawnTimer:0, spawnRate:5.0 }),
fighter: (x,y,side=RED) => spawn({ kind:'ship', subtype:'ship', archetype:'fighter', side, mass:160, size:9, hp:160, maxHp:160, shield:40, maxShield:40, speed:2.2, fireRate:0.45, weapon:'bullet', range:280, damage:22, x,y }),
brawler: (x,y,side=RED) => spawn({ kind:'ship', subtype:'ship', archetype:'brawler', side, mass:300, size:12, hp:360, maxHp:360, speed:1.6, fireRate:0.25, weapon:'ram', range:28, damage:90, x,y }),
sniper: (x,y,side=RED) => spawn({ kind:'ship', subtype:'ship', archetype:'sniper', side, mass:140, size:8, hp:120, maxHp:120, shield:30, maxShield:30, speed:1.9, fireRate:1.2, weapon:'laser', range:620, damage:46, x,y }),
support: (x,y,side=RED) => spawn({ kind:'ship', subtype:'ship', archetype:'support', side, mass:200, size:10, hp:170, maxHp:170, shield:80, maxShield:80, speed:1.6, fireRate:1.0, weapon:'repair', range:160, damage:-20, x,y }),
kamikaze: (x,y,side=RED) => spawn({ kind:'ship', subtype:'ship', archetype:'kamikaze', side, mass:120, size:8, hp:80, maxHp:80, speed:3.0, weapon:'ram', range:18, damage:260, x,y }),
star: (x,y) => spawn({ kind:'celestial', subtype:'star', side:'NEUTRAL', x,y, mass:250000, color:'#ffd166', size:30, fixed:true }),
planet: (x,y) => spawn({ kind:'celestial', subtype:'planet', side:'NEUTRAL', x,y, mass:14000, color:'#7fb', size:18 }),
asteroids: (x,y) => { for(let i=0;i<24;i++) spawn({ kind:'celestial', subtype:'asteroid', side:'NEUTRAL', x: x + rand(160), y: y + rand(160), mass: 20 + Math.random()*160, color:'#b7a', size: rand(4)+3 }); },
shieldGen: (x,y,side=TEAL) => spawn({ kind:'structure', subtype:'shieldGen', side, x,y, size:18, hp:900, maxHp:900, shield:1200, localRadius:220, regen:6 }),
missileBattery: (x,y,side=RED) => spawn({ kind:'structure', subtype:'missileBattery', side, x,y, size:16, hp:600, maxHp:600, reloadTime:3.5 }),
nukeDrop: (x,y,side=RED) => spawn({ kind:'structure', subtype:'nukeDevice', side, x,y, size:14, hp:300, maxHp:300, armed:false, countdown:1.5 }),
capitalCannon: (x,y,side=RED) => spawn({ kind:'structure', subtype:'capitalCannon', side, x,y, size:22, hp:1500, maxHp:1500, reloadTime:5.0, range:800, damage:450 })
};
// ---------- Projectiles ----------
function spawnProjectile(x,y,vx,vy,owner,type='bullet',life=2.0,damage=18){
if (projCount >= CONFIG.MAX_PROJECTILES) return null;
const p = projAcquire();
p.kind = 'proj'; p.subtype = type;
p.x = x; p.y = y; p.vx = vx; p.vy = vy;
p.ownerId = owner ? owner.id : 0; p.ownerSide = owner ? owner.side : 'NEUTRAL';
p.life = life; p.damage = damage; p.size = (type==='laser'?2:2); p.created = performance.now();
entities.push(p); projCount++; return p;
}
// ---------- Removal ----------
function removeEntityByIndex(i){
const e = entities[i];
if (!e) return;
if (e.kind === 'proj'){ projCount = Math.max(0, projCount - 1); if (e._pooled !== undefined) projRelease(e); }
else if (e.side && factions[e.side]) { const idx = factions[e.side].members.indexOf(e.id); if (idx >= 0) factions[e.side].members.splice(idx,1); }
entities.splice(i,1);
}
function removeEntityById(id){ const i = entities.findIndex(e=>e.id===id); if (i>=0) removeEntityByIndex(i); }
function mkFX(type,x,y,color='#fff'){ spawn({ kind:'fx', subtype:type, x,y, t:0, life: type==='bigExpl'?1.2:0.28, color }); }
function explode(x,y,radius=120,damage=500){ const nearby = spatialQueryRegion(x,y,radius); for(const s of nearby){ if (!s || (s.kind!=='ship' && s.kind!=='structure')) continue; const d = Math.hypot(s.x-x,s.y-y); if (d < radius) s.hp -= damage * (1 - d/radius); } mkFX('bigExpl', x, y); }
function destroyShip(s){ explode(s.x,s.y,(s.size||12)*4,(s.maxHp||100)*0.45); const idx = entities.indexOf(s); if (idx>=0) removeEntityByIndex(idx); }
// ---------- Physics & AI ----------
function step(dt){
dt = Math.min(dt, CONFIG.DT_MAX);
spatialClear(); for(const e of entities) spatialInsert(e);
const celestials = entities.filter(e=>e.kind==='celestial');
const ships = entities.filter(e=>e.kind==='ship');
const projs = entities.filter(e=>e.kind==='proj');
// gravity on ships
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 = CONFIG.GRAVITY_BASE * c.mass * (s.mass || 1) / r2;
ax += f * dx * inv / (s.mass || 1); ay += f * dy * inv / (s.mass || 1);
}
s.vx += ax * dt * 60; s.vy += ay * dt * 60;
}
// ships update
for(const s of ships){
s.lastShot = s.lastShot || 0; s.hp = s.hp || s.maxHp || 100;
if (!s.playerControl){ if (!s.target || s.target.hp <= 0) { if (Math.random() < 0.02) s.target = chooseTarget(s); } }
// archetype behaviours
if (!s.playerControl){
switch(s.archetype){
case 'fighter':
if (s.target){ const dx=s.target.x - s.x, dy=s.target.y - s.y, r=Math.hypot(dx,dy)+0.001; s.vx += (dx/r)*0.02; s.vy += (dy/r)*0.02; } else { s.vx += rand(0.0006); s.vy += rand(0.0006); }
break;
case 'brawler':
if (s.target){ const dx=s.target.x - s.x, dy=s.target.y - s.y, r=Math.hypot(dx,dy)+0.001; s.vx += (dx/r)*0.03; s.vy += (dy/r)*0.03; } else { s.vx += rand(0.002); s.vy += rand(0.002); }
break;
case 'sniper':
if (s.target){ const dx=s.target.x - s.x, dy=s.target.y - s.y, r=Math.hypot(dx,dy)+0.001; if (r < (s.range||520)*0.6){ s.vx -= (dx/r)*0.02; s.vy -= (dy/r)*0.02; } else { s.vx += rand(0.0006); s.vy += rand(0.0006); } } else { s.vx *= 0.999; s.vy *= 0.999; }
break;
case 'support':
{
const 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.001; s.vx += (dx/r)*0.015; s.vy += (dy/r)*0.015; if (r < (s.range||160)*0.6 && (performance.now() - (s.lastShot||0) > (s.fireRate*1000 || 1000))){ ally.hp = Math.min(ally.maxHp, ally.hp + Math.abs(s.damage || -20)); s.lastShot = performance.now(); } } else { s.vx += rand(0.0006); s.vy += rand(0.0006); }
}
break;
case 'kamikaze':
if (s.target){ const dx=s.target.x - s.x, dy=s.target.y - s.y, r=Math.hypot(dx,dy)+0.001; s.vx += (dx/r)*0.05; s.vy += (dy/r)*0.05; } else { s.vx += rand(0.01); s.vy += rand(0.01); }
break;
default:
s.vx += rand(0.0005); s.vy += rand(0.0005);
}
}
// firing (non-ram)
if (s.weapon !== 'ram'){
const now = performance.now();
if (s.target && s.weapon !== 'repair' && (now - s.lastShot) > ((s.fireRate || 0.5) * 1000)){
const dx = s.target.x - s.x, dy = s.target.y - s.y, r = Math.hypot(dx,dy);
if (r < (s.range || 300)){
const lead = 0.4;
const tx = s.target.x + (s.target.vx || 0) * lead;
const ty = s.target.y + (s.target.vy || 0) * 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;
spawnProjectile(s.x + (dirx/magd)*s.size, s.y + (diry/magd)*s.size, (dirx/magd)*speed + (s.vx||0), (diry/magd)*speed + (s.vy||0), s, s.weapon, 2, s.damage || 18);
s.lastShot = now;
}
}
}
// mothership spawn
if (s.subtype === 'mothership'){
s.spawnTimer = s.spawnTimer || 0; s.spawnTimer += dt;
const spawnInterval = s.spawnRate || 3.0;
if (s.spawnTimer > spawnInterval){
s.spawnTimer = 0;
const droneTypes = s.archetype === 'aggressive' ? ['brawler','fighter'] : (s.archetype === 'carrier' ? ['fighter','support'] : ['fighter']);
const choice = droneTypes[Math.floor(Math.random()*droneTypes.length)];
spawnActions[choice] && spawnActions[choice](s.x + rand(30), s.y + rand(30), s.side);
}
}
s.x += (s.vx || 0) * dt * 60; s.y += (s.vy || 0) * dt * 60;
if (s.x < -400) s.x = W() + 400;
if (s.x > W() + 400) s.x = -400;
if (s.y < -400) s.y = H() + 400;
if (s.y > H() + 400) s.y = -400;
if (s.hp <= 0) destroyShip(s);
}
// projectiles
for(let i=projs.length-1;i>=0;i--){
const p = projs[i];
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 = CONFIG.GRAVITY_BASE * 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 || 0) * dt * 60; p.y += (p.vy || 0) * dt * 60;
p.life -= dt;
if (p.life <= 0){ const idx = entities.indexOf(p); if (idx>=0) removeEntityByIndex(idx); continue; }
const possible = spatialQueryRegion(p.x, p.y, 80);
for(const hit of possible){
if (!hit || hit.kind !== 'ship') continue;
if (hit.side === p.ownerSide) continue;
const rr = (hit.size || 8) + (p.size || 2);
const dx = hit.x - p.x, dy = hit.y - p.y;
if (dx*dx + dy*dy < rr*rr){
hit.hp -= p.damage || 18; mkFX('hit', p.x, p.y, '#fff');
const idx = entities.indexOf(p); if (idx >= 0) removeEntityByIndex(idx);
if (hit.hp <= 0) destroyShip(hit);
break;
}
}
}
// ship collisions (neighbors)
for(const s of ships){
const neighbors = spatialQueryRegion(s.x, s.y, s.size + 80);
for(const b of neighbors){
if (!b || b.kind !== 'ship' || b.id <= s.id) continue;
const dx = b.x - s.x, dy = b.y - s.y; const r = (s.size||8) + (b.size||8);
if (dx*dx + dy*dy < r*r){
const relx = b.vx - s.vx, rely = b.vy - s.vy;
const impact = Math.min(500, Math.hypot(relx,rely) * ((s.mass||1) + (b.mass||1)) * 0.01);
s.hp -= impact * 0.02; b.hp -= impact * 0.02;
const mag = Math.hypot(dx,dy) || 1; const nx = dx/mag, ny = dy/mag;
s.vx -= nx*0.5; s.vy -= ny*0.5; b.vx += nx*0.5; b.vy += ny*0.5;
mkFX('spark', (s.x + b.x)/2, (s.y + b.y)/2, '#f88');
if (s.hp <= 0) destroyShip(s); if (b.hp <= 0) destroyShip(b);
}
}
}
// fx lifecycle
for(let i = entities.length - 1; i >= 0; i--){
const e = entities[i];
if (e.kind === 'fx'){ e.t = e.t || 0; e.t += dt; if (e.t > e.life) removeEntityByIndex(i); }
}
}
function chooseTarget(source){
const candidates = entities.filter(e => e.kind === 'ship' && e.side !== source.side && e !== source);
if (!candidates.length) return null;
candidates.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 sa = da * (a.archetype === 'sniper' ? 1.3 : 1) - (a.hp || 0) * 0.0005;
const sb = db * (b.archetype === 'sniper' ? 1.3 : 1) - (b.hp || 0) * 0.0005;
return sa - sb;
});
return candidates[0];
}
// ---------- DRAW ----------
function draw(){
ctx.fillStyle = '#010214'; ctx.fillRect(0,0,W(),H());
ctx.fillStyle = 'rgba(255,255,255,0.03)';
for(let i=0;i<120;i++){ const x = (i*123.7 + performance.now()*0.001) % W(); const y = (i*221.3) % H(); ctx.fillRect(x,y,1,1); }
ctx.save();
if (followId){ const f = entities.find(e=>e.id===followId); if (f){ camera.targetX = W()/2 - f.x; camera.targetY = H()/2 - f.y; } }
camera.x += (camera.targetX - camera.x) * camera.lerp; camera.y += (camera.targetY - camera.y) * camera.lerp;
ctx.translate(camera.x, camera.y);
// draw celestials
for(const e of entities){
if (e.kind === 'celestial'){
const r = Math.max(2, e.size);
if (e.subtype === 'star'){ const g = ctx.createRadialGradient(e.x,e.y,0,e.x,e.y,r*7); g.addColorStop(0, hexToRGBA(e.color||'#ffd166',0.55)); g.addColorStop(1, hexToRGBA(e.color||'#ffd166',0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(e.x,e.y,r*7,0,TAU); ctx.fill(); }
ctx.beginPath(); ctx.fillStyle = e.color || '#88c'; ctx.arc(e.x,e.y,r,0,TAU); ctx.fill();
ctx.lineWidth = 1; ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.stroke();
}
}
// draw ships/structures
for(const s of entities){
if (s.kind === 'ship' || s.kind === 'structure'){
ctx.save(); ctx.translate(s.x, s.y);
const ang = Math.atan2(s.vy||0, s.vx||0); ctx.rotate(ang + Math.PI/2);
const col = (s.side === TEAL) ? '#6ee7b7' : ((s.archetype === 'brawler') ? '#ff9b6b' : '#ff6b6b');
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();
if (s.hp < s.maxHp){ 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/s.maxHp)), 5); }
ctx.restore();
}
}
// projectiles
for(const p of entities){ if (p.kind === 'proj'){ ctx.beginPath(); ctx.fillStyle = p.subtype === 'laser' ? 'rgba(160,220,255,0.95)' : 'rgba(255,220,150,0.95)'; ctx.arc(p.x,p.y,p.size||2,0,TAU); ctx.fill(); } }
// fx
for(const fx of entities){ if (fx.kind === 'fx'){ if (fx.subtype === 'bigExpl'){ const s = (fx.t/fx.life)||0; const r = (s*36 + 10); ctx.beginPath(); ctx.fillStyle = `rgba(255,150,50,${1-s})`; ctx.arc(fx.x,fx.y,r,0,TAU); ctx.fill(); } else if (fx.subtype === 'hit'){ const s = (fx.t/fx.life)||0; ctx.beginPath(); ctx.fillStyle = `rgba(255,255,255,${1-s})`; ctx.arc(fx.x,fx.y,3 + s*8, 0, TAU); ctx.fill(); } else if (fx.subtype === 'spark'){ ctx.beginPath(); ctx.fillStyle = 'rgba(255,200,120,0.9)'; ctx.arc(fx.x,fx.y,2,0,TAU); ctx.fill(); } } }
// selection indicator
if (selectedId){ const s = entities.find(e=>e.id===selectedId); if (s){ ctx.save(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.beginPath(); ctx.arc(s.x, s.y, (s.size||8)+6, 0, TAU); ctx.stroke(); ctx.restore(); } }
ctx.restore();
document.getElementById('debug').textContent = `Entities: ${entities.length} — Projectiles: ${projCount} — FPS: ${fps}`;
}
// ---------- Loop ----------
function loop(time){
const rawDt = (time - lastTime) / 1000;
const dt = Math.min(rawDt, CONFIG.DT_MAX);
lastTime = time;
if (running) step(dt);
draw();
frameCount++;
if (time > lastFpsTime + CONFIG.FPS_INTERVAL){ fps = Math.round(frameCount / ((time - lastFpsTime)/1000)); lastFpsTime = time; frameCount = 0; }
requestAnimationFrame(loop);
}
// ---------- Input / Pie Fix ----------
resizeCanvas();
// robust screen->world using canvas rect and camera offset
function screenToWorldFromClient(clientX, clientY){
const rect = canvas.getBoundingClientRect();
const localX = clientX - rect.left;
const localY = clientY - rect.top;
return { x: localX - camera.x, y: localY - camera.y };
}
// Pie menu with fixed open position & STOP propagation on slice mousedown
class PieMenu {
constructor(items){ this.items = items; this.el = null; this.openClientX = 0; this.openClientY = 0; }
show(clientX, clientY){
this.hide();
this.openClientX = clientX; this.openClientY = clientY;
const el = document.createElement('div'); el.className = 'pie-container';
el.style.left = `${clientX}px`; el.style.top = `${clientY}px`; el.style.transform = 'translate(-50%,-50%)';
const N = this.items.length; const angleStep = 360 / N;
for(let i=0;i<N;i++){
const item = this.items[i];
const slice = document.createElement('div'); slice.className = 'pie-slice';
const rot = i * angleStep - 90 - angleStep/2;
slice.style.transform = `rotate(${rot}deg) translate(72px) rotate(${-rot}deg)`;
slice.style.background = hexToRGBA(item.color || '#07202a', 0.72);
slice.style.border = '1px solid rgba(255,255,255,0.04)';
const label = document.createElement('div'); label.className = 'label'; label.textContent = item.label;
slice.appendChild(label);
// IMPORTANT: stop propagation at mousedown/touchstart so global onDocDown doesn't remove pie before we act
const doAction = (ev) => {
try { ev.stopPropagation(); ev.preventDefault && ev.preventDefault(); } catch(e){}
const world = screenToWorldFromClient(this.openClientX, this.openClientY);
if (item.action) item.action(world.x, world.y);
this.hide();
};
slice.addEventListener('mousedown', (ev) => { doAction(ev); }); // immediate
slice.addEventListener('touchstart', (ev) => { doAction(ev); }, {passive:false});
// click fallback (if mousedown didn't run for some reason)
slice.addEventListener('click', (ev) => { ev.stopPropagation(); const world = screenToWorldFromClient(this.openClientX, this.openClientY); if (item.action) item.action(world.x, world.y); this.hide(); });
el.appendChild(slice);
}
document.body.appendChild(el);
this.el = el;
// add a small delay before registering the global close to avoid catching the initial right-click
setTimeout(()=> window.addEventListener('mousedown', onDocDown), 10);
}
hide(){ if (this.el){ this.el.remove(); this.el = null; window.removeEventListener('mousedown', onDocDown); } }
}
function onDocDown(){ playerPie.hide(); enemyPie.hide(); astroPie.hide(); }
const playerPie = new PieMenu([
{ label:'Player Fighter', color:'#6ee7b7', action:(x,y) => spawnActions.fighter(x,y,TEAL) },
{ label:'Player Brawler', color:'#6ee7b7', action:(x,y) => spawnActions.brawler(x,y,TEAL) },
{ label:'Player Sniper', color:'#6ee7b7', action:(x,y) => spawnActions.sniper(x,y,TEAL) },
{ label:'Shield Generator', color:'#a78bfa', action:(x,y) => spawnActions.shieldGen(x,y,TEAL) },
{ label:'Player Mothership', color:'#6ee7b7', action:(x,y) => spawnActions.playerMothership(x,y) }
]);
const enemyPie = new PieMenu([
{ label:'Enemy Fighter', color:'#ff6b6b', action:(x,y) => spawnActions.fighter(x,y,RED) },
{ label:'Enemy Brawler', color:'#ff6b6b', action:(x,y) => spawnActions.brawler(x,y,RED) },
{ label:'Enemy Sniper', color:'#ff6b6b', action:(x,y) => spawnActions.sniper(x,y,RED) },
{ label:'Enemy Mothership', color:'#ff6b6b', action:(x,y) => spawnActions.enemyMothership(x,y) },
{ label:'Chaos Wave', color:'#f8a', action:(x,y) => { for(let i=0;i<16;i++) spawnActions.fighter(x+rand(220), y+rand(220), RED); } }
]);
const astroPie = new PieMenu([
{ label:'Place Star', color:'#ffd166', action:(x,y) => spawnActions.star(x,y) },
{ label:'Place Planet', color:'#7fb', action:(x,y) => spawnActions.planet(x,y) },
{ label:'Asteroid Field', color:'#b7a', action:(x,y) => spawnActions.asteroids(x,y) },
{ label:'Place Shield Gen', color:'#a78bfa', action:(x,y) => spawnActions.shieldGen(x,y) },
{ label:'Place Capital Cannon', color:'#ff9b6b', action:(x,y) => spawnActions.capitalCannon(x,y) }
]);
// ---------- Mouse events ----------
canvas.addEventListener('mousemove', (e) => {
const r = canvas.getBoundingClientRect();
mouse.screenX = e.clientX - r.left; mouse.screenY = e.clientY - r.top;
mouse.x = mouse.screenX; mouse.y = mouse.screenY;
});
canvas.addEventListener('mousedown', (e) => {
const r = canvas.getBoundingClientRect(); const sx = e.clientX - r.left, sy = e.clientY - r.top;
mouse.down = true; mouse.btn = e.button;
if (e.button === 2){
// Right-click: choose pie by modifiers
if (e.ctrlKey || e.metaKey){ enemyPie.show(e.clientX, e.clientY); playerPie.hide(); astroPie.hide(); }
else if (e.altKey){ astroPie.show(e.clientX, e.clientY); playerPie.hide(); enemyPie.hide(); }
else { playerPie.show(e.clientX, e.clientY); enemyPie.hide(); astroPie.hide(); }
return;
}
// left click: select entity
const world = screenToWorldFromClient(e.clientX, e.clientY);
const nearby = spatialQueryRegion(world.x, world.y, 60);
const clicked = nearby.slice().reverse().find(en => {
const rr = (en.kind === 'ship' ? en.size : Math.max(4, en.size || 6));
const dx = en.x - world.x, dy = en.y - world.y;
return dx*dx + dy*dy <= rr*rr;
});
selectedId = clicked ? clicked.id : null;
playerPie.hide(); enemyPie.hide(); astroPie.hide();
});
canvas.addEventListener('mouseup', (e) => { mouse.down = false; mouse.btn = -1; });
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// ---------- Mega panel ----------
document.getElementById('mega').addEventListener('click', (ev) => {
if (ev.target.classList.contains('spawn-btn')){
const action = ev.target.dataset.action;
if (spawnActions[action]){
const cx = (W()/2 - camera.x), cy = (H()/2 - camera.y);
spawnActions[action](cx + rand(120), cy + rand(120));
}
}
});
// ---------- Keyboard ----------
const keys = new Set();
document.addEventListener('keydown', (e) => {
const k = (e.key || '').toLowerCase();
if (k === ' ') keys.add('space'); else keys.add(k);
if (k === 'p') running = !running;
if (k === 'm') document.getElementById('btnMegaToggle').click();
if (k === 'f'){ const ships = entities.filter(s => s.kind === 'ship'); if (ships.length){ const idx = ships.findIndex(s => s.id === followId); followId = ships[(idx + 1) % ships.length].id; } }
});
document.addEventListener('keyup', (e) => { const k = (e.key || '').toLowerCase(); if (k === ' ') keys.delete('space'); else keys.delete(k); });
// player control interval
setInterval(() => {
if (!selectedId) return;
const s = entities.find(e => e.id === selectedId && e.kind === 'ship');
if (!s) return;
if (s.playerControl){
if (keys.has('a')) s.vx -= 0.03;
if (keys.has('d')) s.vx += 0.03;
if (keys.has('w')) s.vy -= 0.03;
if (keys.has('s')) s.vy += 0.03;
if (keys.has('space')){
const now = performance.now();
const interval = (s.fireRate || 0.22) * 1000;
if (!s.lastShot || (now - s.lastShot) > interval){
// spawn projectile towards cursor world position
const rect = canvas.getBoundingClientRect();
const clientX = rect.left + mouse.screenX, clientY = rect.top + mouse.screenY;
const target = screenToWorldFromClient(clientX, clientY);
const dir = Math.atan2(target.y - s.y, target.x - s.x);
const speed = s.weapon === 'laser' ? 8 : 7;
spawnProjectile(s.x + Math.cos(dir)*s.size, s.y + Math.sin(dir)*s.size, Math.cos(dir)*speed + (s.vx||0), Math.sin(dir)*speed + (s.vy||0), s, s.weapon || 'bullet', 2.0, s.damage || 22);
s.lastShot = now;
}
}
}
}, 40);
// ---------- HUD buttons ----------
document.getElementById('btnPause').addEventListener('click', () => running = !running);
document.getElementById('btnCenter').addEventListener('click', () => { followId = null; camera.targetX = 0; camera.targetY = 0; });
document.getElementById('btnClear').addEventListener('click', () => initWorld());
document.getElementById('btnMegaToggle').addEventListener('click', () => {
const mega = document.getElementById('mega'); const isHidden = mega.style.display === 'none' || mega.style.display === '';
mega.style.display = isHidden ? 'block' : 'none';
});
// ---------- Helpers ----------
function assignShipDefaults(){
for (const s of entities.filter(e=>e.kind==='ship')){
if (!s.maxHp) s.maxHp = s.hp || 100; if (!s.hp) s.hp = s.maxHp;
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 = s.fireRate || 0.45; s.range = s.range || 260; s.damage = s.damage || 22; s.maxHp = s.maxHp || 120; }
}
}
}
setInterval(assignShipDefaults, 700);
function prune(){
for (let i = entities.length - 1; i >= 0; i--){
const e = entities[i];
if (e.kind === 'proj'){ if (e.x < -2000 || e.x > W()+2000 || e.y < -2000 || e.y > H()+2000) removeEntityByIndex(i); }
// READ THE REST ON THE PASTEBIN LINc!:!;!
LINE COUNT: 664