Space V5.C --> https://pastebin.com/Rea8CA00
Source:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>SPACE v5 - Complete Engine</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;transition:background-color 0.2s}
#hud button:hover{background:#0a2a36}
#hud small{display:block;color:var(--muted);margin-top:6px;font-size:11px;line-height:1.3}
/* New CSS for the pie menus */
.pie-container{
position:fixed;
z-index:120;
user-select:none;
pointer-events:none;
transform:translate(-50%,-50%);
transition: transform .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);
transition:transform 0.1s ease-out;
transform-origin: center center;
}
.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.08) !important;}
#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;transition: all 0.2s;}
.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 - v5</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 Hash Grid ----------
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);
}
// ---------- Entity 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 }),
// New spawn actions for the secret Shift pie menu
rebelShieldGen: (x,y) => spawn({ kind:'structure', subtype:'rebelShieldGen', side:'RED', x,y, size:20, hp:1200, maxHp:1200, shield:1500, localRadius:250, regen:8, color:'#e74c3c' }),
tradersDen: (x,y) => spawn({ kind:'structure', subtype:'tradersDen', side:'NEUTRAL', x,y, size:16, hp:500, maxHp:500, color:'#f1c40f' }),
enclaveSatellite: (x,y) => spawn({ kind:'structure', subtype:'enclaveSatellite', side:'NEUTRAL', x,y, size:12, hp:400, maxHp:400, color:'#bdc3c7' }),
seaPlanet: (x,y) => spawn({ kind:'celestial', subtype:'planet', side:'NEUTRAL', x,y, mass:16000, color:'#3498db', size:20 }),
lushPlanet: (x,y) => spawn({ kind:'celestial', subtype:'planet', side:'NEUTRAL', x,y, mass:14000, color:'#2ecc71', size:18 }),
flamePlanet: (x,y) => spawn({ kind:'celestial', subtype:'planet', side:'NEUTRAL', x,y, mass:18000, color:'#e74c3c', size:22 }),
earthPlanet: (x,y) => spawn({ kind:'celestial', subtype:'planet', side:'NEUTRAL', x,y, mass:15000, color:'#2c3e50', size:19 }),
};
function spawnProjectile(x,y,vx,vy,owner,type='bullet',life=2.0,damage=18){
if (projCount >= CONFIG.MAX_PROJECTILES) return null;
const p = projAcquire();
Object.assign(p, {
kind: 'proj', subtype: type, x, y, vx, vy,
ownerId: owner ? owner.id : 0,
ownerSide: owner ? owner.side : 'NEUTRAL',
life, damage, size: (type==='laser'?2:2), created: performance.now()
});
entities.push(p); projCount++; return p;
}
// ---------- Entity Management & Effects ----------
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 Step ----------
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');
// 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 + 100; 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;
if (!s.playerControl){ if (!s.target || s.target.hp <= 0) { if (Math.random() < 0.02) s.target = chooseTarget(s); } }
// AI behaviors
if (!s.playerControl){
const target = entities.find(e => e.id === s.target?.id);
s.target = target; // Refresh target reference
if (target) {
const dx = target.x - s.x, dy = target.y - s.y, r = Math.hypot(dx,dy) + 0.01;
switch(s.archetype){
case 'fighter': s.vx += (dx/r)*0.02; s.vy += (dy/r)*0.02; break;
case 'brawler': s.vx += (dx/r)*0.03; s.vy += (dy/r)*0.03; break;
case 'sniper': if (r < (s.range||520)*0.6){ s.vx -= (dx/r)*0.02; s.vy -= (dy/r)*0.02; } else { s.vx += (dx/r)*0.01; s.vy += (dy/r)*0.01; } break;
case 'kamikaze': s.vx += (dx/r)*0.05; s.vy += (dy/r)*0.05; 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 adx=ally.x-s.x, ady=ally.y-s.y, ar=Math.hypot(adx,ady)+0.01; s.vx+=(adx/ar)*0.015; s.vy+=(ady/ar)*0.015; if (ar < s.range*0.8 && (performance.now() - s.lastShot > s.fireRate*1000)){ ally.hp = Math.min(ally.maxHp, ally.hp - s.damage); s.lastShot = performance.now(); } }
else { s.vx += rand(0.0006); s.vy += rand(0.0006); }
break;
default: s.vx += rand(0.0005); s.vy += rand(0.0005);
}
} else { s.vx += rand(0.0006); s.vy += rand(0.0006); }
}
// Firing logic
if (s.weapon !== 'ram' && s.weapon !== 'repair'){
const now = performance.now();
if (s.target && (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 = r / (s.weapon === 'laser' ? 15 : 8);
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' ? 15 : 8.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.5, s.damage || 18);
s.lastShot = now;
}
}
}
// Mothership spawning
if (s.subtype === 'mothership'){
s.spawnTimer = (s.spawnTimer || 0) + dt;
if (s.spawnTimer > (s.spawnRate || 3.0)){
s.spawnTimer = 0;
const choice = ['fighter', 'brawler', 'sniper'][Math.floor(Math.random()*3)];
spawnActions[choice](s.x + rand(30), s.y + rand(30), s.side);
}
}
// Update position and check bounds
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 update
for(let i=entities.length-1;i>=0;i--){
const p = entities[i];
if (p?.kind !== 'proj') continue;
p.x += (p.vx || 0) * dt * 60; p.y += (p.vy || 0) * dt * 60;
p.life -= dt;
if (p.life <= 0){ removeEntityByIndex(i); continue; }
const possibleHits = spatialQueryRegion(p.x, p.y, 80);
for(const hit of possibleHits){
if (!hit || hit.kind !== 'ship' || 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);
removeEntityByIndex(i);
if (hit.hp <= 0) destroyShip(hit);
break;
}
}
}
// Ship collisions
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 impact = Math.min(50, Math.hypot(b.vx - s.vx, b.vy - s.vy) * 2);
s.hp -= impact; b.hp -= impact;
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) + 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) => Math.hypot(a.x - source.x, a.y - source.y) - Math.hypot(b.x - source.x, b.y - source.y));
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);
for(const e of entities){
switch(e.kind) {
case '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();
break;
case 'ship':
case 'structure':
ctx.save(); ctx.translate(e.x, e.y);
const ang = Math.atan2(e.vy||0, e.vx||0); ctx.rotate(ang + Math.PI/2);
const col = factions[e.side]?.color || '#999';
// Simple polygon for most ships and structures
ctx.beginPath(); ctx.moveTo(0, -e.size); ctx.lineTo(-e.size*0.7, e.size); ctx.lineTo(e.size*0.7, e.size); ctx.closePath();
ctx.fillStyle = col; ctx.fill();
// Handle unique shapes for specific structures
if (e.subtype === 'rebelShieldGen'){
ctx.beginPath(); ctx.arc(0, 0, e.size, 0, TAU); ctx.strokeStyle = e.color; ctx.lineWidth = 2; ctx.stroke();
} else if (e.subtype === 'capitalCannon'){
ctx.fillStyle = e.color; ctx.beginPath(); ctx.fillRect(-e.size/2, -e.size/2, e.size, e.size); ctx.fill();
} else if (e.subtype === 'tradersDen' || e.subtype === 'enclaveSatellite'){
ctx.beginPath(); ctx.arc(0, 0, e.size, 0, TAU); ctx.fillStyle = e.color; ctx.fill();
}
if (e.hp < e.maxHp){ const hW = e.size * 1.5; ctx.fillStyle = 'rgba(0,0,0,0.6)'; ctx.fillRect(-hW/2, e.size+6, hW, 5); ctx.fillStyle = '#4caf50'; ctx.fillRect(-hW/2, e.size+6, hW * Math.max(0, (e.hp/e.maxHp)), 5); }
ctx.restore();
break;
case 'proj':
ctx.fillStyle = e.subtype === 'laser' ? '#9ef' : '#ffc';
ctx.beginPath();
ctx.arc(e.x,e.y,e.size||2,0,TAU);
ctx.fill();
break;
case 'fx':
const p = (e.t/e.life)||0;
if (e.subtype === 'bigExpl'){ const r = (p*36 + 10); ctx.fillStyle = `rgba(255,150,50,${1-p})`; ctx.beginPath(); ctx.arc(e.x,e.y,r,0,TAU); ctx.fill(); }
else if (e.subtype === 'hit'){ ctx.fillStyle = `rgba(255,255,255,${1-p})`; ctx.beginPath(); ctx.arc(e.x,e.y,3 + p*8, 0, TAU); ctx.fill(); }
break;
}
}
if (selectedId){ const s = entities.find(e=>e.id===selectedId); if (s){ ctx.strokeStyle = '#fff'; ctx.lineWidth = 1/DPR; ctx.beginPath(); ctx.arc(s.x, s.y, (s.size||8)+6, 0, TAU); ctx.stroke(); } }
ctx.restore();
document.getElementById('debug').textContent = `Entities: ${entities.length} - Projectiles: ${projCount} - FPS: ${fps}`;
}
// ---------- Game Loop ----------
function loop(time){
const dt = (time - lastTime) / 1000;
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 & UI ----------
function screenToWorld(screenX, screenY){
return { x: screenX - camera.x, y: screenY - camera.y };
}
// Reworked PieMenu class for the requested behavior
class PieMenu {
constructor(items){ this.items = items; this.el = null; }
show(clientX, clientY){
this.hide();
// Create the pie menu container
this.el = document.createElement('div');
this.el.className = 'pie-container';
// Apply the "stationary shuffle" wobble of 5-10px
const wobbleX = Math.random() * 10 - 5; // -5 to +5
const wobbleY = Math.random() * 10 - 5; // -5 to +5
this.el.style.left = `${clientX + wobbleX}px`;
this.el.style.top = `${clientY + wobbleY}px`;
const N = this.items.length;
const angleStep = TAU / N;
const radius = 100; // Radius from the center
const startOffset = Math.PI / 2; // Start from the top
for(let i=0;i<N;i++){
const item = this.items[i];
const slice = document.createElement('div');
slice.className = 'pie-slice';
const angle = i * angleStep + startOffset;
slice.style.transform = `translate(${Math.cos(angle)*radius}px, ${Math.sin(angle)*radius}px)`;
slice.style.background = hexToRGBA(item.color || '#07202a', 0.8);
slice.innerHTML = `<div class="label">${item.label}</div>`;
const action = (e) => {
e.stopPropagation();
const worldPos = screenToWorld(clientX, clientY);
item.action(worldPos.x, worldPos.y);
this.hide();
};
slice.addEventListener('mousedown', action);
this.el.appendChild(slice);
}
document.body.appendChild(this.el);
setTimeout(()=> document.addEventListener('mousedown', this.hide.bind(this), {once:true}), 10);
}
hide(){ if (this.el){ this.el.remove(); this.el = null; } }
}
const playerPie = new PieMenu([
{ label:'Fighter', color:factions[TEAL].color, action:(x,y) => spawnActions.fighter(x,y,TEAL) },
{ label:'Brawler', color:factions[TEAL].color, action:(x,y) => spawnActions.brawler(x,y,TEAL) },
{ label:'Sniper', color:factions[TEAL].color, action:(x,y) => spawnActions.sniper(x,y,TEAL) },
{ label:'Support', color:factions[TEAL].color, action:(x,y) => spawnActions.support(x,y,TEAL) },
{ label:'Mothership', color:factions[TEAL].color, action:(x,y) => spawnActions.playerMothership(x,y) }
]);
const enemyPie = new PieMenu([
{ label:'Enemy Fighter', color:factions[RED].color, action:(x,y) => spawnActions.fighter(x,y,RED) },
{ label:'Enemy Brawler', color:factions[RED].color, action:(x,y) => spawnActions.brawler(x,y,RED) },
{ label:'Enemy Sniper', color:factions[RED].color, action:(x,y) => spawnActions.sniper(x,y,RED) },
{ label:'Enemy Mother', color:factions[RED].color, action:(x,y) => spawnActions.enemyMothership(x,y) }
]);
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) }
]);
const secretPie = new PieMenu([
{ label:'Capital Cannons', color:'#ff6b6b', action:(x,y) => spawnActions.capitalCannon(x,y,RED) },
{ label:'Rebel Shield Gen', color:'#e74c3c', action:(x,y) => spawnActions.rebelShieldGen(x,y) },
{ label:'Trader\'s Den', color:'#f1c40f', action:(x,y) => spawnActions.tradersDen(x,y) },
{ label:'Enclave Satellite', color:'#bdc3c7', action:(x,y) => spawnActions.enclaveSatellite(x,y) },
{ label:'Earth Planet', color:'#2c3e50', action:(x,y) => spawnActions.earthPlanet(x,y) }
]);
canvas.addEventListener('mousemove', (e) => { mouse.screenX = e.clientX; mouse.screenY = e.clientY; });
canvas.addEventListener('mousedown', (e) => {
// Check for right-click button (button === 2)
if (e.button === 2){
e.preventDefault();
// Check key combinations to show the correct menu
if (e.shiftKey) secretPie.show(e.clientX, e.clientY);
else if (e.ctrlKey || e.metaKey) enemyPie.show(e.clientX, e.clientY);
else if (e.altKey) astroPie.show(e.clientX, e.clientY);
else playerPie.show(e.clientX, e.clientY);
return;
}
const world = screenToWorld(e.clientX, e.clientY);
const clicked = spatialQueryRegion(world.x, world.y, 60).slice().reverse().find(en => en.kind==='ship' && Math.hypot(en.x-world.x, en.y-world.y) < en.size);
selectedId = clicked ? clicked.id : null;
});
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
const keys = new Set();
document.addEventListener('keydown', (e) => {
const k = e.key.toLowerCase();
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) => { keys.delete(e.key.toLowerCase()); });
// Player control interval
setInterval(() => {
if (!running || !selectedId) return;
const s = entities.find(e => e.id === selectedId && e.playerControl);
if (!s) return;
if (keys.has('a')) s.vx -= 0.08; if (keys.has('d')) s.vx += 0.08;
if (keys.has('w')) s.vy -= 0.08; if (keys.has('s')) s.vy += 0.08;
if (keys.has(' ')){
const now = performance.now();
if (!s.lastShot || (now - s.lastShot) > (s.fireRate || 0.22) * 1000){
const target = screenToWorld(mouse.screenX, mouse.screenY);
const dir = Math.atan2(target.y - s.y, target.x - s.x); // Fixed target.y - s.y, target.x - s.x
const speed = 12;
spawnProjectile(s.x, s.y, Math.cos(dir)*speed + s.vx, Math.sin(dir)*speed + s.vy, s, 'bullet', 2.5, 35);
s.lastShot = now;
}
}
}, 16);
// UI Buttons
document.getElementById('btnPause').onclick = () => running = !running;
document.getElementById('btnCenter').onclick = () => { followId = null; camera.targetX = 0; camera.targetY = 0; };
document.getElementById('btnClear').onclick = () => { entities = []; projCount = 0; initFactions(); initGame(); };
document.getElementById('btnMegaToggle').onclick = () => { const m = document.getElementById('mega'); m.style.display = m.style.display==='block'?'none':'block'; };
document.getElementById('mega').onclick = (e) => {
if(e.target.classList.contains('spawn-btn')){
const action = e.target.dataset.action;
if(spawnActions[action]){
const worldCenter = screenToWorld(W()/2, H()/2);
spawnActions[action](worldCenter.x + rand(120), worldCenter.y + rand(120));
}
}
};
// ---------- Init ----------
function initGame() {
const worldCenter = screenToWorld(W()/2, H()/2);
spawnActions.star(worldCenter.x, worldCenter.y);
spawnActions.playerMothership(worldCenter.x - 400, worldCenter.y);
spawnActions.enemyMothership(worldCenter.x + 400, worldCenter.y);
}
initFactions();
poolInit();
initGame();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
LINE COUNT: 621
PyroflamePC
https://console.firebase.google.com/u/0/project/spaceverse-o4bm8/