|
<!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> |