Skip to content

Instantly share code, notes, and snippets.

@yossiovadia
Last active March 12, 2026 15:41
Show Gist options
  • Select an option

  • Save yossiovadia/beb9d0a08c6b4a125d8f248ab150e7ad to your computer and use it in GitHub Desktop.

Select an option

Save yossiovadia/beb9d0a08c6b4a125d8f248ab150e7ad to your computer and use it in GitHub Desktop.
Model quality comparison: Opus 4.6 vs Qwen 3.5 — neon space shooter (same prompt, single shot)

Model Quality Comparison: Opus 4.6 vs Qwen 3.5 (35B, local Ollama)

Same prompt, single shot, no follow-ups.

This demonstrates why routing a follow-up query to a cheaper model — even with full context — produces meaningfully worse results than keeping it on the original model.

Results

Claude Opus 4.6 Qwen 3.5 (35B-A3B, Q4_K_M, local)
Time ~15s 356s (~6 min)
Output 495 lines / 18KB 935 lines / 29KB
Playable? ✅ Fully functional ❌ Only welcome screen works
Enemies 5 types with distinct patterns Nothing spawns
Particles Explosions, thrusters, impacts None visible
Sound Web Audio procedural SFX Silent
Game feel Smooth, polished, fun Broken

Play in browser

The prompt (identical for both)

Build a complete, single-file HTML game: a neon-themed space shooter.

Requirements:
- Canvas-based rendering, 60fps game loop
- Player ship with WASD movement + mouse aim/shoot
- 5 distinct enemy types with different movement patterns (zigzag, homing, circular, spawner, boss)
- Wave system: 10 waves with increasing difficulty, boss every 5th wave
- Power-up system: shield, rapid fire, spread shot, speed boost — dropped by enemies
- Particle system for explosions, thruster trails, bullet impacts (at least 3 different effects)
- Screen shake on hits, flash effects on damage
- HUD: score, wave number, health bar, active power-ups with duration timers
- Neon glow aesthetic: use CSS filters and shadow effects, dark background
- Game over screen with final score and restart button
- Sound effects using Web Audio API (procedurally generated, no external files)
- Smooth difficulty curve: enemy speed, spawn rate, and health scale per wave

Output ONLY the complete HTML file. No explanations, no markdown fences, just the raw HTML from <!DOCTYPE html> to </html>.

Context

This comparison supports PR #1459 — Conversational Routing Momentum on the vLLM Semantic Router project. The argument: once you route a conversation to a strong model, downgrading mid-conversation to a cheaper model degrades quality — even if the cheaper model receives the full conversation context. More output ≠ better output.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>NEON STORM — Space Shooter</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#000;overflow:hidden;font-family:'Courier New',monospace;cursor:crosshair}
canvas{display:block}
#hud{position:fixed;top:0;left:0;right:0;padding:12px 20px;display:flex;justify-content:space-between;align-items:center;pointer-events:none;z-index:10}
#hud>div{display:flex;gap:18px;align-items:center}
.hud-item{color:#0ff;font-size:14px;text-shadow:0 0 8px #0ff,0 0 16px #08f}
.hud-item span{color:#fff}
#health-bar-bg{width:160px;height:10px;background:rgba(255,255,255,.1);border:1px solid #0f8;border-radius:5px;overflow:hidden}
#health-bar{width:100%;height:100%;background:linear-gradient(90deg,#0f8,#0ff);transition:width .2s;box-shadow:0 0 8px #0f8}
#powerups{display:flex;gap:6px}
.pu-icon{width:28px;height:28px;border-radius:4px;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:bold;position:relative;border:1px solid}
.pu-icon .timer{position:absolute;bottom:-12px;font-size:9px;color:#fff}
#game-over{position:fixed;top:0;left:0;right:0;bottom:0;display:none;flex-direction:column;align-items:center;justify-content:center;background:rgba(0,0,0,.85);z-index:20}
#game-over h1{font-size:64px;color:#f05;text-shadow:0 0 30px #f05,0 0 60px #a03;margin-bottom:10px}
#game-over .score{font-size:28px;color:#0ff;text-shadow:0 0 15px #0ff;margin-bottom:8px}
#game-over .wave{font-size:18px;color:#ff0;text-shadow:0 0 10px #ff0;margin-bottom:30px}
#game-over button{padding:14px 40px;font-size:20px;font-family:inherit;background:transparent;color:#0ff;border:2px solid #0ff;cursor:pointer;text-shadow:0 0 8px #0ff;box-shadow:0 0 15px rgba(0,255,255,.3);transition:all .2s}
#game-over button:hover{background:rgba(0,255,255,.15);box-shadow:0 0 30px rgba(0,255,255,.5)}
#start-screen{position:fixed;top:0;left:0;right:0;bottom:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#000;z-index:25}
#start-screen h1{font-size:56px;color:#0ff;text-shadow:0 0 30px #0ff,0 0 60px #08f;margin-bottom:8px;letter-spacing:6px}
#start-screen .sub{font-size:16px;color:#f0f;text-shadow:0 0 10px #f0f;margin-bottom:40px}
#start-screen .controls{color:#888;font-size:13px;margin-bottom:30px;text-align:center;line-height:1.8}
#start-screen button{padding:14px 50px;font-size:22px;font-family:inherit;background:transparent;color:#0ff;border:2px solid #0ff;cursor:pointer;text-shadow:0 0 8px #0ff;box-shadow:0 0 20px rgba(0,255,255,.3);transition:all .2s;animation:pulse 2s infinite}
#start-screen button:hover{background:rgba(0,255,255,.15)}
@keyframes pulse{0%,100%{box-shadow:0 0 20px rgba(0,255,255,.3)}50%{box-shadow:0 0 40px rgba(0,255,255,.6)}}
</style>
</head>
<body>
<div id="start-screen">
<h1>NEON STORM</h1>
<div class="sub">⚡ SPACE SHOOTER ⚡</div>
<div class="controls">WASD — move &nbsp;|&nbsp; MOUSE — aim &nbsp;|&nbsp; CLICK — shoot<br>Destroy enemies • Collect power-ups • Survive 10 waves</div>
<button onclick="startGame()">▶ START</button>
</div>
<div id="hud">
<div>
<div class="hud-item">SCORE <span id="score">0</span></div>
<div class="hud-item">WAVE <span id="wave">1</span></div>
</div>
<div>
<div id="powerups"></div>
<div id="health-bar-bg"><div id="health-bar"></div></div>
</div>
</div>
<canvas id="c"></canvas>
<div id="game-over">
<h1>GAME OVER</h1>
<div class="score">SCORE: <span id="final-score">0</span></div>
<div class="wave">Reached Wave <span id="final-wave">1</span></div>
<button onclick="startGame()">▶ PLAY AGAIN</button>
</div>
<script>
const canvas=document.getElementById('c'),ctx=canvas.getContext('2d');
let W,H;function resize(){W=canvas.width=innerWidth;H=canvas.height=innerHeight}
resize();addEventListener('resize',resize);
// Audio
const actx=new(window.AudioContext||window.webkitAudioContext)();
function playSound(freq,dur,type='square',vol=.08){
const o=actx.createOscillator(),g=actx.createGain();
o.type=type;o.frequency.setValueAtTime(freq,actx.currentTime);
o.frequency.exponentialRampToValueAtTime(freq*.3,actx.currentTime+dur);
g.gain.setValueAtTime(vol,actx.currentTime);
g.gain.exponentialRampToValueAtTime(.001,actx.currentTime+dur);
o.connect(g);g.connect(actx.destination);o.start();o.stop(actx.currentTime+dur);
}
function shootSfx(){playSound(800,.08,'square',.06)}
function hitSfx(){playSound(200,.15,'sawtooth',.1)}
function explosionSfx(){playSound(80,.4,'sawtooth',.12)}
function powerupSfx(){playSound(600,.2,'sine',.1);setTimeout(()=>playSound(900,.15,'sine',.08),100)}
function bossWarnSfx(){playSound(120,.6,'square',.1);setTimeout(()=>playSound(150,.5,'square',.08),300)}
// State
let keys={},mouseX=0,mouseY=0,mouseDown=false,gameRunning=false;
let player,bullets,enemies,particles,powerups,stars;
let score,wave,enemiesRemaining,waveDelay,shakeX,shakeY,shakeDur;
let activePU={};
addEventListener('keydown',e=>{keys[e.key.toLowerCase()]=true});
addEventListener('keyup',e=>{keys[e.key.toLowerCase()]=false});
addEventListener('mousemove',e=>{mouseX=e.clientX;mouseY=e.clientY});
addEventListener('mousedown',()=>{mouseDown=true});
addEventListener('mouseup',()=>{mouseDown=false});
function startGame(){
document.getElementById('start-screen').style.display='none';
document.getElementById('game-over').style.display='none';
actx.resume();
player={x:W/2,y:H/2,r:14,hp:100,maxHp:100,speed:4,angle:0,shootCd:0,invincible:0};
bullets=[];enemies=[];particles=[];powerups=[];
score=0;wave=0;enemiesRemaining=0;waveDelay=60;shakeX=0;shakeY=0;shakeDur=0;
activePU={shield:0,rapid:0,spread:0,speed:0};
initStars();
gameRunning=true;
requestAnimationFrame(loop);
}
function initStars(){
stars=[];for(let i=0;i<120;i++)stars.push({x:Math.random()*W,y:Math.random()*H,s:Math.random()*2+.5,sp:Math.random()*1+.3});
}
// Particles
function emit(x,y,count,color,spd=3,life=30,size=3){
for(let i=0;i<count;i++){
const a=Math.random()*Math.PI*2,v=Math.random()*spd+1;
particles.push({x,y,vx:Math.cos(a)*v,vy:Math.sin(a)*v,life,maxLife:life,color,size:Math.random()*size+1});
}
}
function thrusterParticle(){
const a=player.angle+Math.PI+(.5-Math.random())*.6;
particles.push({x:player.x-Math.cos(player.angle)*16,y:player.y-Math.sin(player.angle)*16,
vx:Math.cos(a)*2+(.5-Math.random()),vy:Math.sin(a)*2+(.5-Math.random()),
life:15,maxLife:15,color:'#0af',size:Math.random()*3+1});
}
// Shake
function shake(dur,mag=4){shakeDur=dur;shakeX=(Math.random()-.5)*mag;shakeY=(Math.random()-.5)*mag}
// Spawn wave
function spawnWave(){
wave++;
document.getElementById('wave').textContent=wave;
const isBoss=wave%5===0;
if(isBoss){
bossWarnSfx();
enemies.push(makeBoss());
enemiesRemaining=1;
// Also add some minions
for(let i=0;i<wave;i++)setTimeout(()=>enemies.push(makeEnemy(Math.floor(Math.random()*4))),i*400);
enemiesRemaining+=wave;
}else{
const count=4+wave*2;
enemiesRemaining=count;
for(let i=0;i<count;i++){
const type=Math.floor(Math.random()*4);
setTimeout(()=>enemies.push(makeEnemy(type)),i*300);
}
}
}
function makeEnemy(type){
const side=Math.floor(Math.random()*4);
let x,y;
if(side===0){x=Math.random()*W;y=-30}
else if(side===1){x=W+30;y=Math.random()*H}
else if(side===2){x=Math.random()*W;y=H+30}
else{x=-30;y=Math.random()*H}
const spd=1.5+wave*.15;
const hp=2+Math.floor(wave*.5);
const colors=['#f0f','#ff0','#0f0','#f80'];
// type 0=zigzag, 1=homing, 2=circular, 3=spawner
return{x,y,r:12,hp,maxHp:hp,speed:spd,type,color:colors[type],angle:0,timer:0,boss:false};
}
function makeBoss(){
return{x:W/2,y:-60,r:40,hp:30+wave*5,maxHp:30+wave*5,speed:.8,type:4,color:'#f05',angle:0,timer:0,boss:true};
}
// Update
function update(){
// Player movement
let dx=0,dy=0;
const sp=activePU.speed>0?player.speed*1.6:player.speed;
if(keys.w||keys.arrowup)dy-=sp;
if(keys.s||keys.arrowdown)dy+=sp;
if(keys.a||keys.arrowleft)dx-=sp;
if(keys.d||keys.arrowright)dx+=sp;
if(dx&&dy){dx*=.707;dy*=.707}
player.x=Math.max(player.r,Math.min(W-player.r,player.x+dx));
player.y=Math.max(player.r,Math.min(H-player.r,player.y+dy));
player.angle=Math.atan2(mouseY-player.y,mouseX-player.x);
if(player.invincible>0)player.invincible--;
// Thruster
if(dx||dy)thrusterParticle();
// Shooting
const firerate=activePU.rapid>0?4:12;
if(player.shootCd>0)player.shootCd--;
if(mouseDown&&player.shootCd<=0){
shootSfx();
player.shootCd=firerate;
if(activePU.spread>0){
for(let i=-2;i<=2;i++){
const a=player.angle+i*.15;
bullets.push({x:player.x+Math.cos(a)*20,y:player.y+Math.sin(a)*20,vx:Math.cos(a)*10,vy:Math.sin(a)*10,life:50,friendly:true});
}
}else{
bullets.push({x:player.x+Math.cos(player.angle)*20,y:player.y+Math.sin(player.angle)*20,
vx:Math.cos(player.angle)*10,vy:Math.sin(player.angle)*10,life:50,friendly:true});
}
}
// Bullets
for(let i=bullets.length-1;i>=0;i--){
const b=bullets[i];
b.x+=b.vx;b.y+=b.vy;b.life--;
if(b.life<=0||b.x<-20||b.x>W+20||b.y<-20||b.y>H+20){bullets.splice(i,1);continue}
if(b.friendly){
for(let j=enemies.length-1;j>=0;j--){
const e=enemies[j];
if(dist(b,e)<e.r+4){
e.hp--;
hitSfx();
emit(b.x,b.y,5,e.color,2,15,2);
bullets.splice(i,1);
if(e.hp<=0){
explosionSfx();
emit(e.x,e.y,e.boss?40:15,e.color,e.boss?6:4,e.boss?40:25,e.boss?5:3);
score+=e.boss?500:100;
document.getElementById('score').textContent=score;
shake(e.boss?15:6,e.boss?8:4);
// Drop powerup (25% chance)
if(Math.random()<.25||e.boss){
const types=['shield','rapid','spread','speed'];
const pt=types[Math.floor(Math.random()*types.length)];
powerups.push({x:e.x,y:e.y,type:pt,life:300});
}
enemies.splice(j,1);
enemiesRemaining--;
}
break;
}
}
}else{
// Enemy bullet hits player
if(dist(b,player)<player.r+3&&player.invincible<=0){
if(activePU.shield>0){
activePU.shield=0;
shake(8,5);
emit(player.x,player.y,15,'#0ff',3,20);
}else{
player.hp-=10;
shake(10,6);
emit(player.x,player.y,8,'#f00',2,15);
player.invincible=30;
}
hitSfx();
bullets.splice(i,1);
}
}
}
// Enemies
for(const e of enemies){
e.timer++;
const toPlayer=Math.atan2(player.y-e.y,player.x-e.x);
switch(e.type){
case 0: // zigzag
e.x+=Math.cos(toPlayer)*e.speed;
e.y+=Math.sin(toPlayer)*e.speed+Math.sin(e.timer*.1)*3;
break;
case 1: // homing
e.x+=Math.cos(toPlayer)*e.speed*1.2;
e.y+=Math.sin(toPlayer)*e.speed*1.2;
break;
case 2: // circular
e.angle+=.03;
e.x+=Math.cos(toPlayer)*e.speed*.6+Math.cos(e.angle)*2;
e.y+=Math.sin(toPlayer)*e.speed*.6+Math.sin(e.angle)*2;
break;
case 3: // spawner - moves slowly, shoots
e.x+=Math.cos(toPlayer)*e.speed*.5;
e.y+=Math.sin(toPlayer)*e.speed*.5;
if(e.timer%80===0){
const ba=toPlayer;
bullets.push({x:e.x,y:e.y,vx:Math.cos(ba)*5,vy:Math.sin(ba)*5,life:80,friendly:false});
}
break;
case 4: // boss
if(e.y<120)e.y+=1;
else{
e.x+=Math.sin(e.timer*.02)*2;
// Boss shoots spread
if(e.timer%40===0){
for(let i=-3;i<=3;i++){
const ba=toPlayer+i*.2;
bullets.push({x:e.x,y:e.y,vx:Math.cos(ba)*4,vy:Math.sin(ba)*4,life:100,friendly:false});
}
}
}
break;
}
// Collision with player
if(dist(e,player)<e.r+player.r&&player.invincible<=0){
if(activePU.shield>0){
activePU.shield=0;
shake(8,5);
emit(player.x,player.y,15,'#0ff',3,20);
}else{
player.hp-=20;
shake(12,8);
emit(player.x,player.y,12,'#f00',3,20);
player.invincible=45;
}
hitSfx();
}
}
// Powerups
for(let i=powerups.length-1;i>=0;i--){
const p=powerups[i];
p.life--;
if(p.life<=0){powerups.splice(i,1);continue}
if(dist(p,player)<player.r+12){
powerupSfx();
activePU[p.type]=600; // 10 seconds
if(p.type==='shield')emit(player.x,player.y,20,'#0ff',3,25);
powerups.splice(i,1);
}
}
// PU timers
for(const k in activePU)if(activePU[k]>0)activePU[k]--;
updatePUDisplay();
// Particles
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.x+=p.vx;p.y+=p.vy;p.vx*=.96;p.vy*=.96;p.life--;
if(p.life<=0)particles.splice(i,1);
}
// Stars
for(const s of stars){s.y+=s.sp;if(s.y>H){s.y=0;s.x=Math.random()*W}}
// Health bar
const hpPct=Math.max(0,player.hp/player.maxHp*100);
document.getElementById('health-bar').style.width=hpPct+'%';
if(hpPct<30)document.getElementById('health-bar').style.background='#f00';
else if(hpPct<60)document.getElementById('health-bar').style.background='#ff0';
else document.getElementById('health-bar').style.background='linear-gradient(90deg,#0f8,#0ff)';
// Shake decay
if(shakeDur>0){shakeDur--;shakeX*=.8;shakeY*=.8}else{shakeX=0;shakeY=0}
// Wave logic
if(enemiesRemaining<=0&&enemies.length===0){
waveDelay--;
if(waveDelay<=0){
if(wave>=10){
// Win!
score+=2000;
gameOver();return;
}
spawnWave();
waveDelay=90;
}
}
// Death
if(player.hp<=0){gameOver();return}
}
function gameOver(){
gameRunning=false;
document.getElementById('final-score').textContent=score;
document.getElementById('final-wave').textContent=wave;
document.getElementById('game-over').style.display='flex';
}
function updatePUDisplay(){
const el=document.getElementById('powerups');
const icons={shield:{sym:'🛡',col:'#0ff'},rapid:{sym:'⚡',col:'#ff0'},spread:{sym:'✦',col:'#f0f'},speed:{sym:'»',col:'#0f0'}};
let html='';
for(const k in activePU){
if(activePU[k]>0){
const ic=icons[k];
const sec=Math.ceil(activePU[k]/60);
html+=`<div class="pu-icon" style="background:${ic.col}22;border-color:${ic.col};color:${ic.col};text-shadow:0 0 6px ${ic.col}">${ic.sym}<span class="timer">${sec}s</span></div>`;
}
}
el.innerHTML=html;
}
// Draw
function draw(){
ctx.save();
ctx.translate(shakeX,shakeY);
ctx.fillStyle='#0a0a12';
ctx.fillRect(-10,-10,W+20,H+20);
// Stars
for(const s of stars){ctx.fillStyle=`rgba(255,255,255,${s.s/3})`;ctx.fillRect(s.x,s.y,s.s,s.s)}
// Particles
for(const p of particles){
const alpha=p.life/p.maxLife;
ctx.globalAlpha=alpha;
ctx.fillStyle=p.color;
ctx.shadowColor=p.color;ctx.shadowBlur=8;
ctx.beginPath();ctx.arc(p.x,p.y,p.size*alpha,0,Math.PI*2);ctx.fill();
}
ctx.globalAlpha=1;ctx.shadowBlur=0;
// Powerups on ground
for(const p of powerups){
const icons={shield:'#0ff',rapid:'#ff0',spread:'#f0f',speed:'#0f0'};
const col=icons[p.type];
const pulse=1+Math.sin(Date.now()*.005)*.2;
ctx.shadowColor=col;ctx.shadowBlur=15;
ctx.strokeStyle=col;ctx.lineWidth=2;
ctx.beginPath();
// Diamond shape
ctx.moveTo(p.x,p.y-10*pulse);ctx.lineTo(p.x+8*pulse,p.y);
ctx.lineTo(p.x,p.y+10*pulse);ctx.lineTo(p.x-8*pulse,p.y);ctx.closePath();
ctx.stroke();
ctx.fillStyle=col+'44';ctx.fill();
}
ctx.shadowBlur=0;
// Bullets
for(const b of bullets){
ctx.shadowColor=b.friendly?'#0ff':'#f55';ctx.shadowBlur=10;
ctx.fillStyle=b.friendly?'#0ff':'#f55';
ctx.beginPath();ctx.arc(b.x,b.y,b.friendly?3:4,0,Math.PI*2);ctx.fill();
}
ctx.shadowBlur=0;
// Enemies
for(const e of enemies){
ctx.shadowColor=e.color;ctx.shadowBlur=12;
ctx.strokeStyle=e.color;ctx.lineWidth=2;ctx.fillStyle=e.color+'33';
if(e.boss){
// Boss: large hexagon
ctx.beginPath();
for(let i=0;i<6;i++){const a=i*Math.PI/3+e.timer*.01;ctx.lineTo(e.x+Math.cos(a)*e.r,e.y+Math.sin(a)*e.r)}
ctx.closePath();ctx.fill();ctx.stroke();
// HP bar
ctx.fillStyle='#333';ctx.fillRect(e.x-30,e.y-e.r-12,60,6);
ctx.fillStyle=e.color;ctx.fillRect(e.x-30,e.y-e.r-12,60*(e.hp/e.maxHp),6);
}else{
// Regular: triangle/diamond shapes
ctx.beginPath();
const sides=e.type===0?3:e.type===1?4:e.type===2?5:6;
for(let i=0;i<sides;i++){const a=i*Math.PI*2/sides-Math.PI/2;ctx.lineTo(e.x+Math.cos(a)*e.r,e.y+Math.sin(a)*e.r)}
ctx.closePath();ctx.fill();ctx.stroke();
}
}
ctx.shadowBlur=0;
// Player
if(player.invincible>0&&player.invincible%4<2){/* blink */}else{
ctx.save();
ctx.translate(player.x,player.y);
ctx.rotate(player.angle);
// Ship shape
ctx.shadowColor='#0ff';ctx.shadowBlur=15;
ctx.strokeStyle='#0ff';ctx.lineWidth=2;
ctx.fillStyle='rgba(0,255,255,.15)';
ctx.beginPath();
ctx.moveTo(18,0);ctx.lineTo(-12,-10);ctx.lineTo(-6,0);ctx.lineTo(-12,10);ctx.closePath();
ctx.fill();ctx.stroke();
// Shield visual
if(activePU.shield>0){
ctx.strokeStyle='rgba(0,255,255,.4)';ctx.lineWidth=2;
ctx.shadowColor='#0ff';ctx.shadowBlur=20;
ctx.beginPath();ctx.arc(0,0,22,0,Math.PI*2);ctx.stroke();
}
ctx.restore();
}
ctx.shadowBlur=0;
// Crosshair
ctx.strokeStyle='rgba(255,255,255,.4)';ctx.lineWidth=1;
ctx.beginPath();ctx.arc(mouseX,mouseY,12,0,Math.PI*2);ctx.stroke();
ctx.beginPath();ctx.moveTo(mouseX-16,mouseY);ctx.lineTo(mouseX-8,mouseY);ctx.stroke();
ctx.beginPath();ctx.moveTo(mouseX+8,mouseY);ctx.lineTo(mouseX+16,mouseY);ctx.stroke();
ctx.beginPath();ctx.moveTo(mouseX,mouseY-16);ctx.lineTo(mouseX,mouseY-8);ctx.stroke();
ctx.beginPath();ctx.moveTo(mouseX,mouseY+8);ctx.lineTo(mouseX,mouseY+16);ctx.stroke();
ctx.restore();
}
function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y)}
function loop(){
if(!gameRunning)return;
update();draw();
requestAnimationFrame(loop);
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Neon Void: Space Shooter</title>
<style>
body {
margin: 0;
overflow: hidden;
background-color: #050505;
font-family: 'Courier New', Courier, monospace;
color: #fff;
user-select: none;
}
canvas {
display: block;
cursor: crosshair;
}
#ui-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 20px;
box-sizing: border-box;
}
.hud-panel {
text-shadow: 0 0 5px currentColor;
font-weight: bold;
font-size: 18px;
}
#score-display { color: #0ff; }
#wave-display { color: #f0f; }
#powerup-display { color: #ff0; margin-top: 5px; display: flex; gap: 10px; }
.p-badge { background: rgba(255,255,0,0.2); padding: 2px 8px; border-radius: 4px; border: 1px solid #ff0; }
#health-container {
width: 200px;
height: 20px;
border: 2px solid #f00;
position: relative;
background: rgba(0,0,0,0.5);
}
#health-bar {
height: 100%;
background-color: #f00;
width: 100%;
transition: width 0.2s;
box-shadow: 0 0 10px #f00;
}
#screens {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
z-index: 10;
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.hidden { display: none !important; }
h1 {
font-size: 60px;
margin: 0;
color: #0ff;
text-shadow: 0 0 20px #0ff, 0 0 40px #0ff;
letter-spacing: 5px;
}
button {
background: transparent;
color: #f0f;
border: 2px solid #f0f;
padding: 15px 40px;
font-size: 24px;
font-family: inherit;
cursor: pointer;
text-transform: uppercase;
box-shadow: 0 0 10px #f0f;
transition: all 0.2s;
}
button:hover {
background: #f0f;
color: #000;
box-shadow: 0 0 20px #f0f;
}
#damage-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: red;
opacity: 0;
pointer-events: none;
transition: opacity 0.1s;
}
@keyframes flash {
0% { opacity: 0.5; }
100% { opacity: 0; }
}
</style>
</head>
<body>
<div id="damage-overlay"></div>
<canvas id="gameCanvas"></canvas>
<div id="ui-layer">
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
<div>
<div id="score-display" class="hud-panel">SCORE: 0</div>
<div id="wave-display" class="hud-panel">WAVE 1</div>
</div>
<div style="text-align: right;">
<div id="health-container"><div id="health-bar"></div></div>
<div id="powerup-display"></div>
</div>
</div>
</div>
<div id="screens">
<div id="start-screen">
<h1>NEON VOID</h1>
<p>WASD to Move | Mouse to Aim & Shoot</p>
<button id="start-btn">Initialize System</button>
</div>
<div id="game-over-screen" class="hidden">
<h1>SYSTEM FAILURE</h1>
<p id="final-score">SCORE: 0</p>
<button id="restart-btn">Reboot</button>
</div>
<div id="wave-screen" class="hidden">
<h1 id="wave-title">WAVE 1</h1>
</div>
</div>
<script>
/**
* AUDIO SYSTEM (Web Audio API)
* Procedurally generated sounds
*/
const AudioSys = (() => {
let ctx = null;
let masterGain = null;
const init = () => {
if (!ctx) {
ctx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = ctx.createGain();
masterGain.gain.value = 0.3;
masterGain.connect(ctx.destination);
}
if (ctx.state === 'suspended') ctx.resume();
};
const playTone = (type, freq, duration, vol = 1) => {
if (!ctx) return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
gain.gain.setValueAtTime(vol, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(ctx.currentTime + duration);
};
const playNoise = (duration) => {
if (!ctx) return;
const bufferSize = ctx.sampleRate * duration;
const buffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
const noise = ctx.createBufferSource();
noise.buffer = buffer;
const filter = ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 1000;
const gain = ctx.createGain();
gain.gain.setValueAtTime(0.5, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + duration);
noise.connect(filter);
filter.connect(gain);
gain.connect(masterGain);
noise.start();
};
return {
init,
shoot: () => { playTone('sawtooth', 800, 0.1, 0.5); playTone('square', 200, 0.1, 0.3); },
enemyExplode: () => { playNoise(0.3); },
playerHit: () => { playTone('square', 150, 0.2, 0.8); playNoise(0.2); },
powerUp: () => {
if (!ctx) return;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(400, ctx.currentTime);
osc.frequency.linearRampToValueAtTime(1200, ctx.currentTime + 0.2);
gain.gain.setValueAtTime(0.5, ctx.currentTime);
gain.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.3);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(ctx.currentTime + 0.3);
}
};
})();
/**
* GAME CONSTANTS & UTILS
*/
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
let width, height;
const resize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
};
window.addEventListener('resize', resize);
resize();
// Helper functions
const randomRange = (min, max) => Math.random() * (max - min) + min;
const checkCollision = (c1, c2) => {
const dx = c1.x - c2.x;
const dy = c1.y - c2.y;
const dist = Math.sqrt(dx * dx + dy * dy);
return dist < c1.radius + c2.radius;
};
/**
* INPUT HANDLER
*/
const Input = {
keys: {},
mouse: { x: 0, y: 0, down: false },
init() {
window.addEventListener('keydown', e => this.keys[e.key.toLowerCase()] = true);
window.addEventListener('keyup', e => this.keys[e.key.toLowerCase()] = false);
window.addEventListener('mousemove', e => {
this.mouse.x = e.clientX;
this.mouse.y = e.clientY;
});
window.addEventListener('mousedown', () => this.mouse.down = true);
window.addEventListener('mouseup', () => this.mouse.down = false);
}
};
Input.init();
/**
* ENTITIES
*/
class Entity {
constructor(x, y, radius, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.vx = 0;
this.vy = 0;
this.markedForDeletion = false;
}
update(dt) {
this.x += this.vx * dt;
this.y += this.vy * dt;
}
draw(ctx) {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
ctx.fillStyle = this.color;
ctx.fill();
}
}
class Particle extends Entity {
constructor(x, y, color, speed, life) {
super(x, y, randomRange(1, 3), color);
const angle = randomRange(0, Math.PI * 2);
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.life = life;
this.maxLife = life;
}
update(dt) {
super.update(dt);
this.life -= dt;
if (this.life <= 0) this.markedForDeletion = true;
}
draw(ctx) {
ctx.globalAlpha = this.life / this.maxLife;
super.draw(ctx);
ctx.globalAlpha = 1;
}
}
class Bullet extends Entity {
constructor(x, y, angle, isEnemy) {
super(x, y, 4, isEnemy ? '#f0f' : '#0ff');
this.speed = isEnemy ? 300 : 800;
this.vx = Math.cos(angle) * this.speed;
this.vy = Math.sin(angle) * this.speed;
this.isEnemy = isEnemy;
}
draw(ctx) {
ctx.shadowBlur = 10;
ctx.shadowColor = this.color;
super.draw(ctx);
ctx.shadowBlur = 0;
}
}
class PowerUp extends Entity {
constructor(x, y, type) {
super(x, y, 15, '#fff');
this.type = type; // shield, rapid, spread, speed
this.vx = 0;
this.vy = 0;
this.floatOffset = 0;
}
update(dt) {
this.floatOffset += dt * 2;
this.y += Math.sin(this.floatOffset) * 0.5;
}
draw(ctx) {
ctx.shadowBlur = 15;
ctx.shadowColor = this.color;
ctx.fillStyle = this.type === 'shield' ? '#0ff' : this.type === 'rapid' ? '#ff0' : this.type === 'spread' ? '#f0f' : '#0f0';
ctx.beginPath();
const size = this.radius;
ctx.moveTo(this.x, this.y - size);
ctx.lineTo(this.x + size * 0.8, this.y + size * 0.5);
ctx.lineTo(this.x - size * 0.8, this.y + size * 0.5);
ctx.fill();
ctx.shadowBlur = 0;
}
}
/**
* GAME OBJECTS
*/
class Player extends Entity {
constructor() {
super(width/2, height/2, 15, '#fff');
this.speed = 400;
this.health = 100;
this.lastShot = 0;
this.fireRate = 150;
this.angle = 0;
this.powerups = {
rapid: { active: false, timer: 0 },
shield: { active: false, timer: 0 },
spread: { active: false, timer: 0 },
speed: { active: false, timer: 0 }
};
}
update(dt) {
// Movement
let moveX = 0;
let moveY = 0;
if (Input.keys['w']) moveY = -1;
if (Input.keys['s']) moveY = 1;
if (Input.keys['a']) moveX = -1;
if (Input.keys['d']) moveX = 1;
if (moveX !== 0 || moveY !== 0) {
const len = Math.sqrt(moveX*moveX + moveY*moveY);
moveX /= len;
moveY /= len;
let currentSpeed = this.speed;
if (this.powerups.speed.active) currentSpeed *= 1.5;
this.vx = moveX * currentSpeed;
this.vy = moveY * currentSpeed;
} else {
this.vx = 0;
this.vy = 0;
}
this.x += this.vx * dt;
this.y += this.vy * dt;
// Bounds
this.x = Math.max(this.radius, Math.min(width - this.radius, this.x));
this.y = Math.max(this.radius, Math.min(height - this.radius, this.y));
// Aiming
this.angle = Math.atan2(Input.mouse.y - this.y, Input.mouse.x - this.x);
// Shooting
if (Input.mouse.down) {
const now = Date.now();
let rate = this.fireRate;
if (this.powerups.rapid.active) rate = 50;
if (now - this.lastShot > rate) {
this.shoot();
this.lastShot = now;
}
}
// Thruster trail
if (this.vx !== 0 || this.vy !== 0) {
const tailX = this.x - this.vx * dt * 0.1;
const tailY = this.y - this.vy * dt * 0.1;
particles.push(new Particle(tailX, tailY, '#0ff', 2, 0.3));
}
// Powerups
for (let p in this.powerups) {
if (this.powerups[p].active) {
this.powerups[p].timer -= dt;
if (this.powerups[p].timer <= 0) {
this.powerups[p].active = false;
}
}
}
}
shoot() {
AudioSys.shoot();
const spread = this.powerups.spread.active ? 0.3 : 0;
let shots = this.powerups.spread.active ? 3 : 1;
for(let i=0; i<shots; i++) {
let offset = shots === 1 ? 0 : (i - (shots-1)/2);
let a = this.angle + (offset * spread);
bullets.push(new Bullet(this.x, this.y, a, false));
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
// Shield effect
if (this.powerups.shield.active) {
ctx.beginPath();
ctx.arc(0, 0, this.radius + 5, 0, Math.PI*2);
ctx.strokeStyle = `rgba(0, 255, 255, ${0.5 + Math.sin(Date.now()/100)*0.2})`;
ctx.lineWidth = 2;
ctx.stroke();
}
// Ship Body
ctx.shadowBlur = 15;
ctx.shadowColor = this.powerups.shield.active ? '#fff' : '#0ff';
ctx.fillStyle = this.powerups.shield.active ? '#fff' : '#000';
ctx.beginPath();
ctx.moveTo(15, 0);
ctx.lineTo(-10, 10);
ctx.lineTo(-5, 0);
ctx.lineTo(-10, -10);
ctx.closePath();
ctx.fill();
// Cockpit
ctx.fillStyle = '#f00';
ctx.beginPath();
ctx.arc(-2, 0, 4, 0, Math.PI*2);
ctx.fill();
ctx.restore();
ctx.shadowBlur = 0;
}
}
class Enemy extends Entity {
constructor(x, y, type, wave) {
let color = '#fff';
let r = 10;
let hp = 1;
switch(type) {
case 'zigzag': color='#f0f'; r=10; hp=1; break;
case 'homing': color='#f00'; r=12; hp=2; break;
case 'orbiter': color='#ff0'; r=15; hp=3; break;
case 'dropper': color='#aa0000'; r=12; hp=5; break;
case 'tank': color='#8800ff'; r=20; hp=10; break;
case 'boss': color='#ffaa00'; r=50; hp=200; break;
}
super(x, y, r, color);
this.type = type;
this.hp = hp;
this.maxHp = hp;
this.wave = wave;
this.timer = 0;
// Movement patterns
this.baseSpeed = type === 'boss' ? 50 : (type === 'orbiter' ? 0 : (100 + wave * 20));
this.angle = type === 'orbiter' ? 0 : randomRange(0, Math.PI*2);
// Boss behavior
if(type === 'boss') {
this.x = width / 2;
this.y = -100;
this.angle = 0; // for figure 8
}
}
update(dt, player) {
this.timer += dt;
switch (this.type) {
case 'zigzag':
this.y += this.baseSpeed * dt;
this.x += Math.sin(this.timer * 3) * 50;
break;
case 'homing':
const angleToPlayer = Math.atan2(player.y - this.y, player.x - this.x);
this.vx = Math.cos(angleToPlayer) * (this.baseSpeed + 50); // Fast
this.vy = Math.sin(angleToPlayer) * (this.baseSpeed + 50);
this.x += this.vx * dt;
this.y += this.vy * dt;
break;
case 'orbiter':
const center = {x: width/2, y: height/2 + 100};
const dist = Math.sqrt((this.x - center.x)**2 + (this.y - center.y)**2);
this.angle += 1 * dt;
this.x = center.x + Math.cos(this.angle) * 150;
this.y = center.y + Math.sin(this.angle) * 150;
break;
case 'dropper':
this.y += (this.baseSpeed * 0.5) * dt;
if (this.timer % 2 < 0.02) { // Fire rate
// Drop bullets downwards or towards player
if(Math.random() > 0.5) {
bullets.push(new Bullet(this.x, this.y, Math.PI/2, true));
}
}
break;
case 'boss':
const moveTime = this.timer * 2;
this.x = (width/2) + Math.cos(moveTime) * (width * 0.3);
this.y = (height/4) + Math.sin(moveTime * 0.5) * (height * 0.15);
if (moveTime % 30 < 0.02) {
// Burst fire
for(let i=-1; i<=1; i++) {
bullets.push(new Bullet(this.x, this.y, (i*0.5), true));
}
}
break;
}
// Bounds check for non-boss
if (this.y > height + 50) this.markedForDeletion = true;
// Movement integration for non-pattern types
if (this.type === 'homing') {
this.x += this.vx * dt;
this.y += this.vy * dt;
}
}
draw(ctx) {
ctx.save();
ctx.translate(this.x, this.y);
if (this.type === 'orbiter' || this.type === 'boss') {
// Rotation effect
ctx.rotate(this.timer * 5);
}
ctx.shadowBlur = 10;
ctx.shadowColor = this.color;
ctx.fillStyle = '#000';
ctx.strokeStyle = this.color;
ctx.lineWidth = 2;
ctx.beginPath();
if(this.type === 'boss') {
// Boss Shape
ctx.rect(-30, -30, 60, 60);
} else if (this.type === 'tank') {
ctx.rect(-15, -15, 30, 30);
} else {
// Standard Diamond
ctx.moveTo(0, -this.radius);
ctx.lineTo(this.radius, 0);
ctx.lineTo(0, this.radius);
ctx.lineTo(-this.radius, 0);
ctx.closePath();
}
ctx.fill();
ctx.stroke();
// HP bar for tough enemies
if (this.hp < this.maxHp) {
ctx.fillStyle = '#f00';
ctx.fillRect(-10, this.radius + 5, 20 * (this.hp/this.maxHp), 3);
}
ctx.restore();
}
}
/**
* GAME STATE
*/
const Game = {
state: 'MENU', // MENU, PLAYING, GAMEOVER
lastTime: 0,
score: 0,
wave: 1,
enemies: [],
bullets: [],
particles: [],
powerups: [],
player: null,
// Wave Logic
spawnTimer: 0,
spawnRate: 1000,
enemiesToSpawn: 0,
waveActive: false,
// Effects
shake: 0,
flash: 0,
init() {
this.player = new Player();
this.startWave(1);
this.loop = this.loop.bind(this);
requestAnimationFrame(this.loop);
},
startWave(n) {
this.wave = n;
this.enemiesToSpawn = 3 + n * 2; // More enemies each wave
this.spawnRate = Math.max(200, 1500 - n * 100);
this.waveActive = true;
const title = document.getElementById('wave-title');
const screen = document.getElementById('wave-screen');
title.innerText = `WAVE ${n}`;
screen.classList.remove('hidden');
setTimeout(() => screen.classList.add('hidden'), 2000);
},
start() {
this.score = 0;
this.player = new Player();
this.bullets = [];
this.enemies = [];
this.particles = [];
this.powerups = [];
this.wave = 0;
document.getElementById('screens').classList.add('hidden');
AudioSys.init();
this.startWave(1);
},
gameOver() {
this.state = 'GAMEOVER';
document.getElementById('final-score').innerText = `SCORE: ${this.score}`;
document.getElementById('screens').classList.remove('hidden');
document.getElementById('start-screen').classList.add('hidden');
document.getElementById('game-over-screen').classList.remove('hidden');
},
spawnEnemy() {
const types = ['zigzag', 'homing', 'orbiter', 'dropper'];
let type = types[Math.floor(Math.random() * Math.min(types.length, 2 + Math.floor(this.wave/3)))];
// Boss every 5th wave
if (this.wave % 5 === 0 && this.enemies.length === 0) {
type = 'boss';
this.enemiesToSpawn = 1;
this.spawnRate = 2000;
}
const x = randomRange(50, width - 50);
const y = -50;
this.enemies.push(new Enemy(x, y, type, this.wave));
},
spawnPowerup(x, y) {
if (Math.random() > 0.15) return; // 15% chance
const types = ['rapid', 'spread', 'shield', 'speed'];
const type = types[Math.floor(Math.random() * types.length)];
this.powerups.push(new PowerUp(x, y, type));
},
shakeScreen(amount) {
this.shake = amount;
},
update(dt) {
if (this.state !== 'PLAYING') return;
// Spawning
this.spawnTimer += dt * 1000;
if (this.spawnTimer > this.spawnRate && this.enemiesToSpawn > 0) {
this.spawnEnemy();
this.spawnTimer = 0;
this.enemiesToSpawn--;
}
// Wave Completion
if (this.enemiesToSpawn === 0 && this.enemies.length === 0 && this.waveActive) {
this.waveActive = false;
setTimeout(() => {
if (this.wave < 10) this.startWave(this.wave + 1);
else this.gameOver();
}, 2000);
}
this.player.update(dt);
// Update Entities
[this.bullets, this.enemies, this.particles, this.powerups].forEach(arr => {
arr.forEach(e => e.update(dt));
});
// Collision: Bullet vs Enemy
this.bullets.forEach(b => {
if (b.isEnemy) return; // Skip enemy bullets here
this.enemies.forEach(e => {
if (checkCollision(b, e)) {
b.markedForDeletion = true;
e.hp -= 1;
// Spark particles
for(let i=0; i<3; i++) particles.push(new Particle(b.x, b.y, '#fff', 5, 0.1));
if (e.hp <= 0) {
this.score += (e.type === 'boss' ? 1000 : 100);
this.shakeScreen(5);
AudioSys.enemyExplode();
// Explosion
for(let i=0; i<10; i++) particles.push(new Particle(e.x, e.y, e.color, 10, 0.6));
this.spawnPowerup(e.x, e.y);
e.markedForDeletion = true;
}
}
});
});
// Collision: Player vs Enemy
if (this.player.powerups.shield.active) {
// Shield damage enemies
this.enemies.forEach(e => {
if (checkCollision(this.player, e)) {
e.hp -= 1;
e.markedForDeletion = true; // Destroy enemy on impact
for(let i=0; i<5; i++) particles.push(new Particle(e.x, e.y, '#f00', 3, 0.3));
}
});
} else {
this.enemies.forEach(e => {
if (checkCollision(this.player, e)) {
this.takeDamage(20);
e.markedForDeletion = true;
for(let i=0; i<10; i++) particles.push(new Particle(e.x, e.y, '#f00', 8, 0.5));
}
});
}
// Collision: Player vs Powerup
this.powerups.forEach(p => {
if (checkCollision(this.player, p)) {
p.markedForDeletion = true;
AudioSys.powerUp();
this.player.powerups[p.type].active = true;
this.player.powerups[p.type].timer = 5; // 5 seconds
this.score += 50;
}
});
// Collision: Enemy Bullets vs Player
this.bullets.forEach(b => {
if (!b.isEnemy) return;
if (checkCollision(this.player, b)) {
b.markedForDeletion = true;
this.takeDamage(10);
this.shakeScreen(3);
}
});
// Cleanup
[this.bullets, this.enemies, this.particles, this.powerups].forEach(arr => {
this.player.powerups.shield.active && arr === this.enemies ? arr : // Skip deletion if shielded (optional)
arr.splice(0, arr.length, ...arr.filter(e => !e.markedForDeletion));
});
if (!this.player.powerups.shield.active) {
this.enemies = this.enemies.filter(e => !e.markedForDeletion);
}
// Fade Shake
if (this.shake > 0) this.shake -= dt * 20;
if (this.shake < 0) this.shake = 0;
// Flash effect
if (this.flash > 0) this.flash -= dt;
},
takeDamage(amount) {
this.player.health -= amount;
AudioSys.playerHit();
this.shakeScreen(10);
this.flash = 0.2;
// Explosion at player
for(let i=0; i<20; i++) particles.push(new Particle(this.player.x, this.player.y, '#f00', 10, 0.8));
if (this.player.health <= 0) {
this.gameOver();
}
updateHUD();
},
draw() {
// Background
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, width, height);
// Stars (Simple static background)
ctx.fillStyle = '#fff';
if (Math.random() > 0.9) ctx.fillRect(randomRange(0, width), randomRange(0, height), 1, 1);
// Shake Translation
ctx.save();
if (this.shake > 0) {
ctx.translate(randomRange(-this.shake, this.shake), randomRange(-this.shake, this.shake));
}
// Draw Entities
this.powerups.forEach(p => p.draw(ctx));
this.particles.forEach(p => p.draw(ctx));
this.enemies.forEach(e => e.draw(ctx));
this.bullets.forEach(b => b.draw(ctx));
this.player.draw(ctx);
ctx.restore();
// Damage Overlay
const overlay = document.getElementById('damage-overlay');
if (this.flash > 0) {
overlay.style.opacity = this.flash;
} else {
overlay.style.opacity = 0;
}
// Flash effect on canvas context could also be added here
this.updateHUD();
},
updateHUD() {
if (!this.player) return;
document.getElementById('score-display').innerText = `SCORE: ${this.score}`;
document.getElementById('wave-display').innerText = `WAVE ${this.wave}`;
const hpPercent = Math.max(0, (this.player.health / 100) * 100);
const bar = document.getElementById('health-bar');
bar.style.width = `${hpPercent}%`;
if (hpPercent < 30) bar.style.backgroundColor = '#f00';
else bar.style.backgroundColor = '#f50';
// Powerups
const puContainer = document.getElementById('powerup-display');
let html = '';
if (this.player.powerups.rapid.active) html += `<div class="p-badge">RAPID</div>`;
if (this.player.powerups.spread.active) html += `<div class="p-badge">SPREAD</div>`;
if (this.player.powerups.shield.active) html += `<div class="p-badge">SHIELD</div>`;
if (this.player.powerups.speed.active) html += `<div class="p-badge">SPEED</div>`;
puContainer.innerHTML = html;
},
loop(timestamp) {
const dt = (timestamp - this.lastTime) / 1000;
this.lastTime = timestamp;
if (this.state === 'PLAYING') {
this.update(dt);
this.draw();
}
if (this.state === 'PLAYING' || this.particles.length > 0 || this.shake > 0) {
requestAnimationFrame(this.loop);
}
}
};
// Event Listeners
document.getElementById('start-btn').addEventListener('click', () => {
document.getElementById('start-screen').classList.add('hidden');
Game.start();
});
document.getElementById('restart-btn').addEventListener('click', () => {
document.getElementById('screens').classList.add('hidden');
Game.start();
});
// Initial Draw
ctx.fillStyle = '#050505';
ctx.fillRect(0, 0, width, height);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment