homage to the old era,
cheers to lost designs
today I wanted things to be different I wasn’t sure how or what but I also remembered why I wanted it to be that way. I thought a lot about when things were made, and I focused really hard on what I wanted to see, and now you can see it too… create this HTML file and play what I see in my mind.
…/// FEED INITIALIZING///…
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8”>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0”>
<title>VOIDERS: Ultimate</title>
<style>
body {
margin: 0;
padding: 0;
background-color: #111;
color: #fff;
font-family: ‘Courier New’, Courier, monospace;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
user-select: none;
cursor: none;
}
#game-container {
position: relative;
box-shadow: 0 0 20px rgba(255, 255, 255, 0.15);
border: 2px solid #333;
cursor: none;
}
canvas {
display: block;
background-color: #000;
image-rendering: pixelated;
}
#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: 10px;
box-sizing: border-box;
}
.hud-row {
display: flex;
justify-content: space-between;
width: 100%;
text-transform: uppercase;
font-weight: bold;
font-size: 16px;
letter-spacing: 2px;
text-shadow: 2px 2px 0 #000;
}
#boss-warning {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
color: #f00;
font-size: 40px;
font-weight: bold;
letter-spacing: 5px;
text-shadow: 3px 3px 0 #500;
display: none;
animation: blink 0.2s infinite;
width: 100%;
text-align: center;
}
@keyframes blink {
0% { opacity: 1; }
50% { opacity: 0; }
100% { opacity: 1; }
}
#start-screen, #game-over-screen {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0, 0, 0, 0.95);
padding: 30px;
border: 2px solid white;
pointer-events: auto;
display: flex;
flex-direction: column;
gap: 15px;
min-width: 280px;
cursor: default;
}
.hidden { display: none !important; }
h1 { margin: 0; font-size: 32px; letter-spacing: 4px; }
p { margin: 5px 0; font-size: 14px; line-height: 1.5; color: #ccc; }
.powerup-legend {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
text-align: left;
font-size: 12px;
margin: 10px 0;
border-top: 1px solid #333;
padding-top: 10px;
}
.p-item span { font-weight: bold; color: #fff; }
button {
background: #000;
color: #fff;
border: 2px solid #fff;
padding: 12px 20px;
font-family: inherit;
font-size: 16px;
cursor: pointer;
text-transform: uppercase;
transition: all 0.1s;
margin-top: 10px;
}
button:hover { background: #fff; color: #000; }
</style>
</head>
<body>
<div id=”game-container”>
<canvas id=”gameCanvas” width=”480” height=”640”></canvas>
<div id=”ui-layer”>
<div class=”hud-row”>
<span id=”score-display”>1UP 00</span>
<span id=”highscore-display”>HI-SCORE 00</span>
</div>
<div id=”boss-warning”>WARNING</div>
<div class=”hud-row” style=”margin-top: auto;”>
<span id=”lives-display”>LIVES: 1</span>
<span id=”powerup-display”></span>
</div>
</div>
<div id=”start-screen”>
<h1>VOID ULTIMATE</h1>
<p>ARROWS or MOUSE to Fly<br>SPACE or CLICK to Shoot</p>
<div class=”powerup-legend”>
<div class=”p-item”><span>[S]</span> SPEED UP</div>
<div class=”p-item”><span>[T]</span> SLOW TIME</div>
<div class=”p-item”><span>[L]</span> LASER BEAM</div>
<div class=”p-item”><span>[R]</span> RAPID FIRE</div>
<div class=”p-item”><span>[W]</span> WIDE BURST</div>
<div class=”p-item”><span>[M]</span> MIRROR</div>
<div class=”p-item”><span>[O]</span> SHIELD</div>
<div class=”p-item”><span>[B]</span> SMART BOMB</div>
<div class=”p-item” style=”color:#afa”><span>[1UP]</span> EXTRA LIFE</div>
<div class=”p-item” style=”color:#f44”><span>[XXX]</span> ANTI-LIFE</div>
</div>
<p style=”margin-top:10px; color:#f88; font-size:12px;”>SUPPLY DROPS EVERY 30s</p>
<button id=”start-btn”>START MISSION</button>
</div>
<div id=”game-over-screen” class=”hidden”>
<h1>MISSION FAILED</h1>
<p id=”final-score”>SCORE: 0</p>
<button id=”restart-btn”>RETRY</button>
</div>
</div>
<script>
/**
* VOID GALAGA: ULTIMATE EDITION
* Final Additions: Burst Shot, Mirror Reflect, Random Drops
*/
const CANVAS_WIDTH = 480;
const CANVAS_HEIGHT = 640;
const PIXEL_SCALE = 2;
const BASE_PLAYER_SPEED = 5;
const BASE_COOLDOWN = 18;
const EXTRA_LIFE_SCORES = [50000, 150000, 300000, 500000];
const POWERUPS = {
SPEED: { label: ‘S’, color: ‘#fff’, duration: 600 },
SLOW: { label: ‘T’, color: ‘#fff’, duration: 400 },
LASER: { label: ‘L’, color: ‘#fff’, duration: 400 },
RAPID: { label: ‘R’, color: ‘#fff’, duration: 300 },
BURST: { label: ‘W’, color: ‘#d0f’, duration: 400 }, // New: Wide Burst
MIRROR:{ label: ‘M’, color: ‘#0ff’, duration: 500 }, // New: Mirror
SHIELD:{ label: ‘O’, color: ‘#fff’, duration: 0 },
BOMB: { label: ‘B’, color: ‘#fff’, duration: 0 },
LIFE: { label: ‘1UP’, color: ‘#afa’, duration: 0 },
DEATH: { label: ‘XXX’, color: ‘#f00’, duration: 0 }
};
const SPRITES = {
player: [[0,0,0,0,1,0,0,0,0],[0,0,0,1,1,1,0,0,0],[0,0,0,1,2,1,0,0,0],[0,0,1,1,2,1,1,0,0],[0,1,1,1,1,1,1,1,0],[1,1,1,1,1,1,1,1,1],[1,0,1,0,1,0,1,0,1],[1,0,1,0,0,0,1,0,1]],
box: [[1,1,1,1,1,1,1,1,1],[1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,1],[1,0,0,0,0,0,0,0,1],[1,1,1,1,1,1,1,1,1]],
zako: [[0,0,1,0,0,0,1,0,0],[0,1,1,1,0,1,1,1,0],[1,1,1,1,1,1,1,1,1],[1,1,0,1,1,1,0,1,1],[1,1,1,1,1,1,1,1,1],[0,1,0,0,0,0,0,1,0],[0,1,0,0,0,0,0,1,0],[0,0,1,1,0,1,1,0,0]],
goei: [[0,0,1,0,0,0,1,0,0],[1,1,1,1,0,1,1,1,1],[1,1,1,1,1,1,1,1,1],[1,1,1,0,0,0,1,1,1],[1,1,1,1,1,1,1,1,1],[0,1,1,1,0,1,1,1,0],[0,0,1,0,0,0,1,0,0],[0,1,0,0,0,0,0,1,0]],
boss: [[0,0,0,1,1,1,0,0,0],[0,0,1,1,1,1,1,0,0],[0,1,1,1,1,1,1,1,0],[1,1,0,1,1,1,0,1,1],[1,1,1,1,1,1,1,1,1],[1,0,1,1,1,1,1,0,1],[1,0,1,0,0,0,1,0,1],[0,1,1,0,0,0,1,1,0]],
titan: [[0,0,0,0,1,1,1,1,1,0,0,0,0],[0,0,1,1,1,1,1,1,1,1,1,0,0],[0,1,1,2,1,2,1,2,1,2,1,1,0],[1,1,1,1,1,1,1,1,1,1,1,1,1],[1,1,0,1,1,1,2,1,1,1,0,1,1],[1,1,0,1,0,1,1,1,0,1,0,1,1],[0,1,1,1,0,0,1,0,0,1,1,1,0],[0,0,1,0,0,0,1,0,0,0,1,0,0]],
hive_core: [[0,0,1,1,1,1,1,0,0],[0,1,1,2,1,2,1,1,0],[1,1,1,1,2,1,1,1,1],[1,2,1,2,2,2,1,2,1],[1,1,1,1,2,1,1,1,1],[0,1,1,2,1,2,1,1,0],[0,0,1,1,1,1,1,0,0]],
hive_bit: [[0,1,0],[1,2,1],[0,1,0]],
snake_head: [[0,0,1,1,1,0,0],[0,1,1,2,1,1,0],[1,1,1,1,1,1,1],[1,0,1,1,1,0,1],[1,0,0,1,0,0,1]],
snake_body: [[0,1,1,1,0],[1,1,2,1,1],[1,1,1,1,1],[0,1,1,1,0]]
};
const ENEMY_TYPES = {
zako: { sprite: ‘zako’, score: 100, hp: 1, speed: 2 },
goei: { sprite: ‘goei’, score: 200, hp: 1, speed: 2.5 },
boss: { sprite: ‘boss’, score: 1000, hp: 3, speed: 2 },
titan: { sprite: ‘titan’, score: 5000, hp: 60, speed: 1 },
hive: { sprite: ‘hive_core’, score: 3000, hp: 40, speed: 1 },
snake: { sprite: ‘snake_head’, score: 4000, hp: 15, speed: 3 }
};
// --- Audio ---
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
if (audioCtx.state === ‘suspended’) audioCtx.resume();
const now = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.connect(gain);
gain.connect(audioCtx.destination);
switch(type) {
case ‘shoot’:
osc.type = ‘square’;
osc.frequency.setValueAtTime(880, now);
osc.frequency.exponentialRampToValueAtTime(110, now + 0.1);
gain.gain.setValueAtTime(0.05, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
osc.start(now); osc.stop(now + 0.1);
break;
case ‘explosion’:
osc.type = ‘sawtooth’;
osc.frequency.setValueAtTime(150, now);
osc.frequency.exponentialRampToValueAtTime(10, now + 0.4);
gain.gain.setValueAtTime(0.2, now);
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.4);
osc.start(now); osc.stop(now + 0.4);
break;
case ‘alert’:
osc.type = ‘square’;
osc.frequency.setValueAtTime(400, now);
osc.frequency.linearRampToValueAtTime(600, now + 0.1);
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0, now + 0.5);
osc.start(now); osc.stop(now + 0.5);
break;
case ‘reflect’:
osc.type = ‘sine’;
osc.frequency.setValueAtTime(800, now);
osc.frequency.linearRampToValueAtTime(1200, now + 0.1);
gain.gain.setValueAtTime(0.1, now);
gain.gain.linearRampToValueAtTime(0, now + 0.1);
osc.start(now); osc.stop(now + 0.1);
break;
}
}
// --- Game State ---
const canvas = document.getElementById(’gameCanvas’);
const ctx = canvas.getContext(’2d’);
let gameState = ‘start’;
let keys = {};
let mouseX = CANVAS_WIDTH/2, mouseY = CANVAS_HEIGHT - 50;
let useMouse = false;
let clickPending = false;
let score = 0;
let highscore = parseInt(localStorage.getItem(’galagaOmniHigh’)) || 0;
let lives = 1;
let nextLifeThresholdIndex = 0;
let wave = 1;
let frameCount = 0;
let attackTimer = 0;
let randomDropTimer = 1200; // ~20 seconds
let player = null;
let enemies = [];
let bullets = [];
let enemyBullets = [];
let particles = [];
let stars = [];
let powerUps = [];
let activeEffects = [];
// --- Classes ---
class Star {
constructor() {
this.x = Math.random() * CANVAS_WIDTH;
this.y = Math.random() * CANVAS_HEIGHT;
this.speed = (Math.random() * 0.5) + 0.2;
this.size = Math.random() < 0.05 ? 2 : 1;
}
update(speedMod) {
this.y += this.speed * speedMod;
if (this.y > CANVAS_HEIGHT) {
this.y = 0;
this.x = Math.random() * CANVAS_WIDTH;
}
}
draw() {
ctx.fillStyle = ‘#555’;
ctx.fillRect(this.x, this.y, this.size, this.size);
}
}
class Particle {
constructor(x, y, speed, life, color=’#fff’) {
this.x = x; this.y = y;
const angle = Math.random() * Math.PI * 2;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.life = life;
this.maxLife = life;
this.size = Math.random()*3 + 1;
this.color = color;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.life--;
}
draw() {
ctx.globalAlpha = this.life / this.maxLife;
ctx.fillStyle = this.color;
ctx.fillRect(this.x, this.y, this.size, this.size);
ctx.globalAlpha = 1.0;
}
}
class Entity {
constructor(x, y, spriteKey) {
this.x = x;
this.y = y;
this.spriteKey = spriteKey;
this.width = SPRITES[spriteKey][0].length * PIXEL_SCALE;
this.height = SPRITES[spriteKey].length * PIXEL_SCALE;
this.markedForDeletion = false;
}
drawSprite(color = ‘#fff’) {
const sprite = SPRITES[this.spriteKey];
for (let r = 0; r < sprite.length; r++) {
for (let c = 0; c < sprite[r].length; c++) {
const val = sprite[r][c];
if (val === 1) {
ctx.fillStyle = color;
ctx.fillRect(this.x + c*PIXEL_SCALE, this.y + r*PIXEL_SCALE, PIXEL_SCALE, PIXEL_SCALE);
} else if (val === 2) {
ctx.fillStyle = ‘#000’;
ctx.fillRect(this.x + c*PIXEL_SCALE, this.y + r*PIXEL_SCALE, PIXEL_SCALE, PIXEL_SCALE);
}
}
}
}
getBounds() { return { x: this.x, y: this.y, w: this.width, h: this.height }; }
}
class Player extends Entity {
constructor() {
super(CANVAS_WIDTH/2 - 9, CANVAS_HEIGHT - 60, ‘player’);
this.cooldown = 0;
this.invulnerable = 120;
this.speedMod = 1;
this.fireRateMod = 1;
this.hasLaser = false;
this.hasShield = false;
this.hasBurst = false;
this.hasMirror = false;
}
update() {
if (this.invulnerable > 0) this.invulnerable--;
let dx = 0, dy = 0;
const speed = BASE_PLAYER_SPEED * this.speedMod;
if (useMouse) {
this.x += (mouseX - this.width/2 - this.x) * 0.15 * this.speedMod;
this.y += (mouseY - this.height/2 - this.y) * 0.15 * this.speedMod;
} else {
if (keys[’ArrowUp’]) dy = -speed;
if (keys[’ArrowDown’]) dy = speed;
if (keys[’ArrowLeft’]) dx = -speed;
if (keys[’ArrowRight’]) dx = speed;
this.x += dx;
this.y += dy;
}
this.x = Math.max(0, Math.min(CANVAS_WIDTH - this.width, this.x));
this.y = Math.max(0, Math.min(CANVAS_HEIGHT - this.height, this.y));
const isShooting = keys[’ ‘] || clickPending;
if (isShooting && this.cooldown <= 0) {
const bType = this.hasLaser ? ‘laser’ : ‘normal’;
const bx = this.x + this.width/2 - (this.hasLaser ? 1 : 2);
if (this.hasBurst) {
// Burst Fire: 3 angles
bullets.push(new Bullet(bx, this.y, false, ‘normal’, 0));
bullets.push(new Bullet(bx, this.y, false, ‘normal’, -2));
bullets.push(new Bullet(bx, this.y, false, ‘normal’, 2));
} else {
bullets.push(new Bullet(bx, this.y, false, bType));
}
playSound(’shoot’);
this.cooldown = BASE_COOLDOWN * this.fireRateMod;
clickPending = false;
}
if (this.cooldown > 0) this.cooldown--;
}
draw() {
if (this.invulnerable > 0 && Math.floor(frameCount/5)%2===0) return;
this.drawSprite();
// Shield Visual
if (this.hasShield) {
ctx.strokeStyle = ‘#fff’; ctx.lineWidth = 2; ctx.beginPath();
ctx.arc(this.x + this.width/2, this.y + this.height/2, 22 + Math.sin(frameCount/5)*2, 0, Math.PI*2);
ctx.stroke();
}
// Mirror Visual
if (this.hasMirror) {
ctx.strokeStyle = ‘#0ff’; ctx.lineWidth = 2; ctx.beginPath();
ctx.arc(this.x + this.width/2, this.y + this.height/2, 26 + Math.cos(frameCount/5)*2, 0, Math.PI*2);
ctx.stroke();
}
}
}
class Bullet {
constructor(x, y, isEnemy, type=’normal’, vx=0) {
this.x = x;
this.y = y;
this.isEnemy = isEnemy;
this.type = type;
this.width = type === ‘laser’ ? 4 : 4;
this.height = type === ‘laser’ ? 25 : 8;
this.markedForDeletion = false;
this.piercing = (type === ‘laser’);
this.vx = vx;
this.vyOverride = null; // For reflected bullets
}
update(timeScale = 1) {
let speedY = this.isEnemy ? 3 : (this.type === ‘laser’ ? 12 : 8);
if (this.vyOverride !== null) {
this.y += this.vyOverride * timeScale;
} else {
this.y += (this.isEnemy ? speedY : -speedY) * timeScale;
}
this.x += this.vx * timeScale;
if (this.y < -50 || this.y > CANVAS_HEIGHT + 50 || this.x < -50 || this.x > CANVAS_WIDTH + 50) {
this.markedForDeletion = true;
}
}
draw() {
// Reflected bullets are cyan
ctx.fillStyle = this.vyOverride !== null ? ‘#0ff’ : (this.isEnemy ? ‘#f88’ : (this.type === ‘laser’ ? ‘#aaf’ : ‘#fff’));
ctx.fillRect(this.x, this.y, this.width, this.height);
}
getBounds() { return {x:this.x, y:this.y, w:this.width, h:this.height}; }
}
class Enemy extends Entity {
constructor(type, gridX, gridY, delay) {
super(0, -50, ENEMY_TYPES[type].sprite);
this.data = ENEMY_TYPES[type];
this.hp = this.data.hp;
this.scoreVal = this.data.score;
this.targetX = 40 + gridX * 40;
this.targetY = 50 + gridY * 35;
this.state = ‘entering’;
this.tick = -delay;
this.pathMode = Math.floor(Math.random()*2);
this.startX = this.pathMode === 0 ? (gridX < 5 ? -50 : CANVAS_WIDTH+50) : CANVAS_WIDTH/2;
this.isBoss = false;
}
update(timeScale) {
this.tick += 1 * timeScale;
if (this.state === ‘entering’) {
if (this.tick < 0) return;
const t = this.tick / 120;
if (t >= 1) {
this.state = ‘idle’;
this.x = this.targetX;
this.y = this.targetY;
return;
}
if (this.pathMode === 0) {
this.x = this.startX + (this.targetX - this.startX) * t;
this.y = 100 + Math.sin(t*Math.PI)*100 + (this.targetY - 100)*t;
} else {
this.x = (CANVAS_WIDTH/2) + Math.sin(t * Math.PI * 4) * 100;
this.y = -50 + t * (this.targetY + 50);
}
}
else if (this.state === ‘idle’) {
this.x = this.targetX + Math.sin(frameCount/40)*10;
this.y = this.targetY + Math.cos(frameCount/30)*5;
}
else if (this.state === ‘diving’) {
this.y += this.data.speed * timeScale;
if (player) {
const dx = player.x - this.x;
if (Math.abs(dx) > 2) this.x += Math.sign(dx) * 1 * timeScale;
}
if (Math.random() < 0.02 * timeScale) enemyBullets.push(new Bullet(this.x+this.width/2, this.y+this.height, true));
if (this.y > CANVAS_HEIGHT) { this.y = -50; this.state = ‘returning’; }
}
else if (this.state === ‘returning’) {
const dx = this.targetX - this.x;
const dy = this.targetY - this.y;
this.x += dx * 0.04 * timeScale;
this.y += dy * 0.04 * timeScale;
if (Math.abs(dx)<2 && Math.abs(dy)<2) this.state = ‘idle’;
}
}
startDive() { if (this.state === ‘idle’) this.state = ‘diving’; }
takeDamage(amt = 1) {
this.hp -= amt;
if (this.hp <= 0) {
this.die();
} else {
createExplosion(this.x + this.width/2, this.y + this.height/2, 2);
}
}
die() {
this.markedForDeletion = true;
playSound(’explosion’);
createExplosion(this.x + this.width/2, this.y + this.height/2, 8);
score += this.scoreVal;
checkExtraLife();
if (Math.random() < 0.15) {
let keys = Object.keys(POWERUPS).filter(k => k !== ‘DEATH’); // Death drop logic handled separately
if ((wave + 1) % 5 === 0) keys = keys.filter(k => k !== ‘BOMB’); // No bomb before boss
const type = keys[Math.floor(Math.random() * keys.length)];
powerUps.push(new PowerUp(this.x, this.y, type));
}
}
}
// --- Boss Classes ---
class TitanBoss extends Enemy {
constructor(targetXOverride) {
super(’titan’, 5, 2, 0);
this.targetX = targetXOverride !== undefined ? targetXOverride : CANVAS_WIDTH/2 - 13*PIXEL_SCALE/2;
this.targetY = 80;
this.hp = 60;
this.shieldUp = false;
this.isBoss = true;
this.attackCycle = 0;
}
update(timeScale) {
super.update(timeScale);
if (this.state === ‘idle’) {
this.attackCycle++;
if (this.attackCycle % 300 === 0) this.shieldUp = !this.shieldUp;
if (this.attackCycle % 120 === 0 && !this.shieldUp) {
for (let i=-2; i<=2; i++) {
let b = new Bullet(this.x + this.width/2, this.y + this.height, true);
enemyBullets.push(b);
enemyBullets.push(new Bullet(this.x + this.width/2 + i*10, this.y + this.height, true));
}
}
}
}
takeDamage(amt) {
if (this.shieldUp) return;
super.takeDamage(amt);
}
drawSprite() {
super.drawSprite(this.shieldUp ? ‘#aaf’ : ‘#f44’);
if (this.shieldUp) {
ctx.strokeStyle = ‘#aaf’; ctx.beginPath();
ctx.arc(this.x+this.width/2, this.y+this.height/2, 30, 0, Math.PI*2); ctx.stroke();
}
}
}
class HiveBoss extends Enemy {
constructor(targetXOverride) {
super(’hive’, 5, 2, 0);
this.targetX = targetXOverride !== undefined ? targetXOverride : CANVAS_WIDTH/2 - 20;
this.targetY = 100;
this.hp = 50;
this.isBoss = true;
this.spawnTimer = 0;
}
update(timeScale) {
super.update(timeScale);
if (this.state === ‘idle’) {
this.spawnTimer++;
const droneCount = enemies.filter(e => e instanceof HiveDrone).length;
if (this.spawnTimer > 60 && droneCount < 16) {
this.spawnTimer = 0;
enemies.push(new HiveDrone(this));
}
}
}
}
class HiveDrone extends Enemy {
constructor(parent) {
super(’zako’, 0, 0, 0);
this.parent = parent;
this.angle = Math.random() * Math.PI * 2;
this.orbitDist = 60;
this.state = ‘orbiting’;
this.hp = 2;
this.scoreVal = 50;
this.x = parent.x;
this.y = parent.y;
}
update(timeScale) {
if (this.parent.markedForDeletion) { this.die(); return; }
if (this.state === ‘orbiting’) {
this.angle += 0.05 * timeScale;
this.x = this.parent.x + Math.cos(this.angle) * this.orbitDist;
this.y = this.parent.y + Math.sin(this.angle) * this.orbitDist;
if (Math.random() < 0.01) this.state = ‘diving’;
} else { super.update(timeScale); }
}
}
class SnakeSegment extends Enemy {
constructor(type, leader, isHead, centerXOverride) {
super(type === ‘head’ ? ‘snake’ : ‘zako’, 0, 0, 0);
this.spriteKey = type === ‘head’ ? ‘snake_head’ : ‘snake_body’;
this.leader = leader;
this.isHead = isHead;
this.history = [];
this.hp = isHead ? 20 : 8;
this.scoreVal = isHead ? 3000 : 200;
this.width = SPRITES[this.spriteKey][0].length * PIXEL_SCALE;
this.height = SPRITES[this.spriteKey].length * PIXEL_SCALE;
this.centerX = centerXOverride !== undefined ? centerXOverride : CANVAS_WIDTH/2;
this.x = this.centerX; this.y = 100;
if (isHead) this.t = 0;
}
update(timeScale) {
this.history.push({x: this.x, y: this.y});
if (this.history.length > 10) this.history.shift();
if (this.isHead) {
this.t += 0.05 * timeScale;
this.x = this.centerX + Math.sin(this.t) * 100;
this.y = 100 + Math.sin(this.t * 2) * 30;
if (Math.random() < 0.03) enemyBullets.push(new Bullet(this.x+this.width/2, this.y+this.height, true));
} else {
if (this.leader && !this.leader.markedForDeletion) {
if (this.leader.history.length >= 8) {
const pos = this.leader.history[this.leader.history.length - 8];
this.x = pos.x; this.y = pos.y;
}
} else { this.die(); }
}
}
die() {
super.die();
if (!this.isHead) {
let debris = new Enemy(’goei’, 0,0,0);
debris.x = this.x; debris.y = this.y; debris.state = ‘diving’; enemies.push(debris);
}
}
}
class PowerUp extends Entity {
constructor(x, y, typeKey) {
super(x, y, ‘box’);
this.type = typeKey;
this.label = POWERUPS[typeKey].label;
this.vy = 1.5;
}
draw() {
const pData = POWERUPS[this.type];
let boxColor = ‘#fff’;
if (this.type === ‘DEATH’ || this.type === ‘LIFE’) boxColor = pData.color;
if (this.type === ‘BURST’ || this.type === ‘MIRROR’) boxColor = pData.color;
this.drawSprite(boxColor);
ctx.fillStyle = ‘#000’;
ctx.font = ‘bold 12px monospace’;
ctx.fillText(this.label, this.x + 4, this.y + 13);
}
update() {
this.y += this.vy;
if (this.y > CANVAS_HEIGHT) this.markedForDeletion = true;
}
}
// --- Logic ---
function createExplosion(x, y, count) {
for (let i=0; i<count; i++) particles.push(new Particle(x, y, Math.random()*2, 30 + Math.random()*20, ‘#fff’));
}
function spawnWave() {
attackTimer = 150;
enemies = [];
const isMultiBoss = score >= 100000;
const bossCount = isMultiBoss ? 2 : 1;
if (wave % 15 === 5) {
showBossWarning(isMultiBoss ? “DOUBLE TITAN” : “TITAN APPROACHING”);
for(let i=0; i<bossCount; i++) {
const tx = (CANVAS_WIDTH * (i+1)/(bossCount+1)) - (26/2);
enemies.push(new TitanBoss(tx));
}
} else if (wave % 15 === 10) {
showBossWarning(isMultiBoss ? “HIVE SWARM” : “HIVE DETECTED”);
for(let i=0; i<bossCount; i++) {
const tx = (CANVAS_WIDTH * (i+1)/(bossCount+1)) - 20;
enemies.push(new HiveBoss(tx));
}
} else if (wave % 15 === 0) {
showBossWarning(isMultiBoss ? “TWIN SERPENTS” : “SERPENT INBOUND”);
for(let i=0; i<bossCount; i++) {
const cx = (CANVAS_WIDTH * (i+1)/(bossCount+1));
const head = new SnakeSegment(’head’, null, true, cx);
enemies.push(head);
let prev = head;
for(let j=0; j<6; j++) {
let seg = new SnakeSegment(’body’, prev, false, cx);
enemies.push(seg);
prev = seg;
}
}
} else {
const rows = 5;
const cols = 8;
let count = 0;
for (let r=0; r<rows; r++) {
for (let c=0; c<cols; c++) {
if (r===0 && (c<2 || c>5)) continue;
const type = r===0 ? ‘boss’ : (r===1 ? ‘goei’ : ‘zako’);
enemies.push(new Enemy(type, c + 1, r, count*5));
count++;
}
}
}
}
function showBossWarning(text) {
const el = document.getElementById(’boss-warning’);
el.textContent = text;
el.style.display = ‘block’;
playSound(’alert’);
setTimeout(() => { el.style.display = ‘none’; }, 3000);
}
function activatePowerUp(type) {
if (type === ‘DEATH’) { die(); return; }
const p = POWERUPS[type];
playSound(’shoot’);
if (p.duration > 0) {
const existing = activeEffects.find(e => e.type === type);
if (existing) existing.timer = p.duration;
else {
activeEffects.push({ type: type, timer: p.duration });
applyEffect(type, true);
}
} else {
if (type === ‘SHIELD’) player.hasShield = true;
if (type === ‘LIFE’) lives++;
if (type === ‘BOMB’) {
playSound(’explosion’);
ctx.fillStyle = ‘#fff’; ctx.fillRect(0,0,CANVAS_WIDTH, CANVAS_HEIGHT);
enemies.forEach(e => { e.hp = 0; e.takeDamage(100); });
enemyBullets = [];
}
}
updatePowerUpUI();
}
function applyEffect(type, active) {
if (!player) return;
if (type === ‘SPEED’) player.speedMod = active ? 1.6 : 1;
if (type === ‘RAPID’) player.fireRateMod = active ? 0.25 : 1;
if (type === ‘LASER’) player.hasLaser = active;
if (type === ‘BURST’) player.hasBurst = active;
if (type === ‘MIRROR’) player.hasMirror = active;
}
function updatePowerUpUI() {
const display = document.getElementById(’powerup-display’);
const actives = activeEffects.map(e => POWERUPS[e.type].label).join(’ ‘);
const shield = player && player.hasShield ? ‘[SHIELD]’ : ‘’;
display.textContent = `${actives} ${shield}`;
}
function checkExtraLife() {
if (nextLifeThresholdIndex < EXTRA_LIFE_SCORES.length && score >= EXTRA_LIFE_SCORES[nextLifeThresholdIndex]) {
lives++;
playSound(’shoot’);
nextLifeThresholdIndex++;
}
}
function rectIntersect(r1, r2) {
return !(r2.x > r1.x + r1.w || r2.x + r2.w < r1.x || r2.y > r1.y + r1.h || r2.y + r2.h < r1.y);
}
// --- Main Loop ---
function die() {
playSound(’explosion’);
lives--;
for(let i=0; i<40; i++) particles.push(new Particle(player.x+player.width/2, player.y+player.height/2, 6, 60, ‘#fff’));
gameState = ‘death_anim’;
let deathTimer = 100;
const animLoop = () => {
if (gameState !== ‘death_anim’) return;
ctx.fillStyle = ‘#000’; ctx.fillRect(0,0,CANVAS_WIDTH, CANVAS_HEIGHT);
particles = particles.filter(p => p.life > 0);
particles.forEach(p => { p.update(); p.draw(); });
deathTimer--;
if (deathTimer <= 0) {
if (lives > 0) { resetPlayer(); gameState = ‘playing’; loop(); }
else showGameOver();
} else requestAnimationFrame(animLoop);
};
animLoop();
}
function resetPlayer() {
player = new Player();
activeEffects = [];
bullets = [];
enemyBullets = [];
}
function resetGame() {
score = 0; lives = 1; wave = 1;
nextLifeThresholdIndex = 0;
enemies = []; activeEffects = [];
randomDropTimer = 1200;
highscore = parseInt(localStorage.getItem(’galagaOmniHigh’)) || 0;
resetPlayer();
spawnWave();
gameState = ‘playing’;
document.getElementById(’start-screen’).classList.add(’hidden’);
document.getElementById(’game-over-screen’).classList.add(’hidden’);
document.getElementById(’game-container’).style.cursor = ‘none’;
loop();
}
function showGameOver() {
gameState = ‘gameover’;
document.getElementById(’game-container’).style.cursor = ‘default’;
if (score > highscore) {
highscore = score;
localStorage.setItem(’galagaOmniHigh’, highscore);
}
document.getElementById(’highscore-display’).textContent = `HI-SCORE ${highscore}`;
document.getElementById(’final-score’).textContent = `SCORE: ${score}`;
document.getElementById(’game-over-screen’).classList.remove(’hidden’);
}
function loop() {
if (gameState !== ‘playing’) return;
frameCount++;
ctx.fillStyle = ‘#000’; ctx.fillRect(0,0,CANVAS_WIDTH, CANVAS_HEIGHT);
const slowEffect = activeEffects.find(e => e.type === ‘SLOW’);
const timeScale = slowEffect ? 0.3 : 1.0;
// Random PowerUp Drops
randomDropTimer--;
if (randomDropTimer <= 0) {
// Drop any powerup (except Death usually, unless you want chaos)
let keys = Object.keys(POWERUPS).filter(k => k !== ‘DEATH’);
if ((wave + 1) % 5 === 0) keys = keys.filter(k => k !== ‘BOMB’);
const type = keys[Math.floor(Math.random() * keys.length)];
// Also add a small chance for DEATH box to drop randomly!
if (Math.random() < 0.05) powerUps.push(new PowerUp(Math.random()*(CANVAS_WIDTH-20), -20, ‘DEATH’));
else powerUps.push(new PowerUp(Math.random()*(CANVAS_WIDTH-20), -20, type));
randomDropTimer = 1200 + Math.random() * 600; // Reset timer (20-30s)
}
if (stars.length < 50) stars.push(new Star());
stars.forEach(s => { s.update(player.speedMod > 1 ? 2 : 1); s.draw(); });
player.update();
activeEffects = activeEffects.filter(e => {
e.timer--;
if (e.timer <= 0) { applyEffect(e.type, false); updatePowerUpUI(); return false; }
return true;
});
bullets = bullets.filter(b => !b.markedForDeletion);
bullets.forEach(b => b.update());
enemyBullets = enemyBullets.filter(b => !b.markedForDeletion);
enemyBullets.forEach(b => b.update(timeScale));
enemies = enemies.filter(e => !e.markedForDeletion);
enemies.forEach(e => e.update(timeScale));
if (enemies.length === 0) { wave++; spawnWave(); }
const isBossLevel = (wave % 5 === 0);
if (!isBossLevel && !enemies.some(e => e.state === ‘entering’)) {
attackTimer -= timeScale;
if (attackTimer <= 0) {
const idle = enemies.filter(e => e.state === ‘idle’);
if (idle.length > 0) idle[Math.floor(Math.random()*idle.length)].startDive();
attackTimer = Math.max(20, 120 - wave*5);
}
}
powerUps = powerUps.filter(p => !p.markedForDeletion);
powerUps.forEach(p => p.update());
particles = particles.filter(p => p.life > 0);
particles.forEach(p => { p.update(); p.draw(); });
const pRect = player.getBounds();
powerUps.forEach(p => { if (rectIntersect(pRect, p.getBounds())) { p.markedForDeletion=true; activatePowerUp(p.type); }});
bullets.forEach(b => {
const bRect = b.getBounds();
enemies.forEach(e => {
if (rectIntersect(bRect, e.getBounds())) {
if (!b.piercing) b.markedForDeletion = true;
e.takeDamage();
}
});
});
let hit = false;
enemies.forEach(e => { if (rectIntersect(pRect, e.getBounds())) { e.takeDamage(10); hit=true; } });
// Enemy Bullet Collisions (Reflect check)
enemyBullets.forEach(b => {
if (rectIntersect(pRect, b.getBounds())) {
if (player.hasMirror && b.isEnemy) {
// Reflect!
b.isEnemy = false;
b.vyOverride = -5; // Fly UP
b.vx = (Math.random() - 0.5) * 4; // Deflect randomly
playSound(’reflect’);
createExplosion(b.x, b.y, 2); // Spark effect
} else {
b.markedForDeletion = true;
hit = true;
}
}
});
if (hit && player.invulnerable <= 0) {
if (player.hasShield) {
player.hasShield = false; player.invulnerable = 60; playSound(’shoot’); updatePowerUpUI();
createExplosion(player.x, player.y, 10);
} else { die(); return; }
}
player.draw();
enemies.forEach(e => e.drawSprite());
bullets.forEach(b => b.draw());
enemyBullets.forEach(b => b.draw());
powerUps.forEach(p => p.draw());
document.getElementById(’score-display’).textContent = `SCORE ${score}`;
document.getElementById(’highscore-display’).textContent = `HI-SCORE ${highscore}`;
document.getElementById(’lives-display’).textContent = `LIVES ${lives}`;
requestAnimationFrame(loop);
}
window.addEventListener(’keydown’, e => keys[e.key] = true);
window.addEventListener(’keyup’, e => keys[e.key] = false);
canvas.addEventListener(’mousemove’, e => {
const r = canvas.getBoundingClientRect();
mouseX = e.clientX - r.left;
mouseY = e.clientY - r.top;
useMouse = true;
});
canvas.addEventListener(’mousedown’, () => { clickPending = true; useMouse = true; });
canvas.addEventListener(’mouseup’, () => clickPending = false);
document.getElementById(’start-btn’).addEventListener(’click’, resetGame);
document.getElementById(’restart-btn’).addEventListener(’click’, resetGame);
</script>
</body>
</html>
