DOOM
ling 3.0 tiny:free · full

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 31,826 bytes · SHA-256 a0ca1a208990
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RETRO SHOOTER</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
overflow: hidden;
font-family: monospace;
}
canvas {
image-rendering: pixelated;
image-rendering: crisp-edges;
border: 3px solid #2a2a2a;
}
#gameContainer {
position: relative;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas" width="800" height="600"></canvas>
</div>
<script>
// ============================================
// RETRO SHOOTER - A Retro FPS Game
// ============================================
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const W = 800, H = 600;
// ---- Audio ----
const AudioCtx = window.AudioContext || window.webkitAudioContext;
let audioCtx;
function initAudio() {
if (!audioCtx) audioCtx = new AudioCtx();
}
function playSound(type) {
if (!audioCtx) return;
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(800, now);
osc.frequency.exponentialRampToValueAtTime(200, now + 0.08);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.08);
osc.start(now);
osc.stop(now + 0.08);
break;
case 'hit':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(200, now);
osc.frequency.exponentialRampToValueAtTime(80, now + 0.1);
gain.gain.setValueAtTime(0.12, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
osc.start(now);
osc.stop(now + 0.1);
break;
case 'pickup':
osc.type = 'sine';
osc.frequency.setValueAtTime(600, now);
osc.frequency.exponentialRampToValueAtTime(1200, now + 0.15);
gain.gain.setValueAtTime(0.06, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
osc.start(now);
osc.stop(now + 0.15);
break;
case 'death':
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(400, now);
osc.frequency.exponentialRampToValueAtTime(50, now + 0.4);
gain.gain.setValueAtTime(0.15, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.4);
osc.start(now);
osc.stop(now + 0.4);
break;
case 'win':
osc.type = 'sine';
osc.frequency.setValueAtTime(400, now);
osc.frequency.setValueAtTime(500, now + 0.1);
osc.frequency.setValueAtTime(600, now + 0.2);
osc.frequency.setValueAtTime(800, now + 0.3);
gain.gain.setValueAtTime(0.08, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
osc.start(now);
osc.stop(now + 0.5);
break;
case 'respawn':
osc.type = 'triangle';
osc.frequency.setValueAtTime(300, now);
osc.frequency.setValueAtTime(600, now + 0.1);
gain.gain.setValueAtTime(0.06, now);
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
osc.start(now);
osc.stop(now + 0.2);
break;
}
}
// ---- Game State ----
let state = 'MENU'; // MENU, PLAYING, PAUSED, DEATH, WIN
let player, enemies, bullets, pickups, particles, powerups;
let score, level, gameTime, deaths, kills;
let screenShake, shakeIntensity;
let spawnTimer, spawnInterval;
let levelEndShown, levelEndTimer;
let gameOverTimer, gameOverAlpha;
let highScore = parseInt(localStorage.getItem('retroShooterHigh') || '0');
let keys = {};
let mouse = { x: W/2, y: H/2, down: false };
let camX = 0, camY = 0;
let comboTimer, comboCount;
let lastTime = 0;
let flashAlpha = 0;
let flashColor = '#ff0000';
// ---- Weapons ----
const WEAPONS = [
{ name: 'Pistol', shots: 1, spread: 0, rate: 280, damage: 12, mag: 10, color: '#00ff41', bulletColor: '#00ff41', icon: '⚔' },
{ name: 'Shotgun', shots: 1, spread: 4, rate: 220, damage: 8, mag: 6, color: '#ff4400', bulletColor: '#ff4400', icon: '🔫' },
{ name: 'Minigun', shots: 1, spread: 0, rate: 45, damage: 6, mag: 60, color: '#00ccff', bulletColor: '#00ccff', icon: '🪓' },
];
// ---- Level Data ----
const LEVELS = [
{ name: 'Training Ground', enemyCount: 8, spawnEvery: 2000, bossHP: 80 },
{ name: 'Corridor', enemyCount: 15, spawnEvery: 1600, bossHP: 120 },
{ name: 'War Zone', enemyCount: 22, spawnEvery: 1200, bossHP: 160 },
{ name: 'The Pit', enemyCount: 30, spawnEvery: 1000, bossHP: 200 },
{ name: 'Nexus Prime', enemyCount: 40, spawnEvery: 800, bossHP: 300 },
];
// ---- Player ----
function createPlayer() {
return {
x: 100, y: 300,
w: 20, h: 28,
speed: 3.5,
health: 100, maxHealth: 100,
ammo: 0, currentWeapon: 0,
bullets: [],
invincible: 0,
respawnTimer: 0,
dead: false,
lastShot: 0,
alive: true,
};
}
function createEnemy(x, y, type, level) {
const config = {
zombie: { hp: 20, speed: 1.2, size: 14, color: '#884422', score: 100 },
mutant: { hp: 12, speed: 2.5, size: 10, color: '#8844cc', score: 150 },
slime: { hp: 8, speed: 0.7, size: 16, color: '#44cc44', score: 80 },
boss: { hp: 300, speed: 0.8, size: 36, color: '#ff2200', score: 1000 },
};
const c = config[type] || config.zombie;
return {
x, y,
type,
hp: c.hp,
maxHp: c.hp,
speed: c.speed + Math.random() * 0.5,
size: c.size,
color: c.color,
score: c.score,
dx: 0, dy: 0,
shootTimer: 0,
patrolDir: Math.random() * Math.PI * 2,
patrolTimer: 0,
alive: true,
hitFlash: 0,
};
}
function createBullet(x, y, angle, damage, color, speed, isEnemy = false) {
return { x, y, angle, damage, color, speed, isEnemy, life: 60, trail: [] };
}
function createPickup(x, y, type) {
return { x, y, type, pulse: 0, alive: true };
}
function createParticle(x, y, color, speed, life) {
return {
x, y,
vx: (Math.random() - 0.5) * speed,
vy: (Math.random() - 0.5) * speed,
color,
life,
maxLife: life,
size: Math.random() * 3 + 1,
};
}
function createPowerUp(x, y, type) {
return { x, y, type, pulse: 0, alive: true };
}
// ---- Input ----
document.addEventListener('keydown', e => {
keys[e.key.toLowerCase()] = true;
keys[e.code] = true;
if (e.key === 'Escape' && state === 'PLAYING') {
state = 'PAUSED';
} else if (e.key === 'Escape' && state === 'PAUSED') {
state = 'PLAYING';
}
if (e.key === '1') switchWeapon(0);
if (e.key === '2') switchWeapon(1);
if (e.key === '3') switchWeapon(2);
});
document.addEventListener('keyup', e => {
keys[e.key.toLowerCase()] = false;
keys[e.code] = false;
});
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
mouse.x = (e.clientX - rect.left) * (W / rect.width);
mouse.y = (e.clientY - rect.top) * (H / rect.height);
});
canvas.addEventListener('mousedown', e => {
mouse.down = true;
initAudio();
});
canvas.addEventListener('mouseup', e => { mouse.down = false; });
canvas.addEventListener('contextmenu', e => e.preventDefault());
// ---- Weapon Switch ----
function switchWeapon(idx) {
if (player.currentWeapon !== idx) {
player.currentWeapon = idx;
player.ammo = WEAPONS[idx].mag;
playSound('pickup');
}
}
// ---- Camera ----
function updateCamera() {
camX += (player.x - W/2 - camX) * 0.1;
camY += (player.y - H/2 - camY) * 0.1;
}
// ---- Spawning ----
function spawnEnemy() {
const lvl = LEVELS[Math.min(level - 1, LEVELS.length - 1)];
const count = lvl.enemyCount;
const spawnEvery = lvl.spawnEvery;
const typeRoll = Math.random();
let type = 'zombie';
if (typeRoll < 0.08) type = 'boss';
else if (typeRoll < 0.2) type = 'mutant';
else if (typeRoll < 0.35) type = 'slime';
const side = Math.floor(Math.random() * 4);
let x, y;
if (side === 0) { x = player.x + 200 + Math.random() * 300; y = -20; }
else if (side === 1) { x = -20; y = 200 + Math.random() * 300; }
else if (side === 2) { x = player.x + 200 + Math.random() * 300; y = H + 20; }
else { x = -20; y = 200 + Math.random() * 300; }
const enemy = createEnemy(x, y, type, level);
enemies.push(enemy);
}
// ---- Game Init ----
function initGame() {
player = createPlayer();
enemies = [];
bullets = [];
pickups = [];
particles = [];
powerups = [];
score = 0;
level = 1;
gameTime = 0;
deaths = 0;
kills = 0;
comboCount = 0;
comboTimer = 0;
screenShake = 0;
shakeIntensity = 0;
spawnTimer = 0;
spawnInterval = 2000;
levelEndShown = false;
levelEndTimer = 0;
gameOverTimer = 0;
gameOverAlpha = 0;
flashAlpha = 0;
camX = 0;
camY = 0;
}
function startLevel() {
initGame();
const lvl = LEVELS[Math.min(level - 1, LEVELS.length - 1)];
player.x = 100;
player.y = 300;
player.ammo = WEAPONS[player.currentWeapon].mag;
// Spawn initial enemies
for (let i = 0; i < lvl.enemyCount; i++) {
setTimeout(() => {
const side = Math.floor(Math.random() * 4);
let x, y;
if (side === 0) { x = 100 + Math.random() * 200; y = -10; }
else if (side === 1) { x = -10; y = 200 + Math.random() * 200; }
else if (side === 2) { x = 100 + Math.random() * 200; y = H + 10; }
else { x = -10; y = 200 + Math.random() * 200; }
enemies.push(createEnemy(x, y, 'zombie', level));
}, i * 400);
}
state = 'PLAYING';
}
// ---- Update ----
function update(dt) {
if (state !== 'PLAYING') return;
gameTime += dt;
// ---- Player Movement ----
if (!player.dead && player.alive) {
let dx = 0, dy = 0;
if (keys['w'] || keys['arrowup']) dy -= 1;
if (keys['s'] || keys['arrowdown']) dy += 1;
if (keys['a'] || keys['arrowleft']) dx -= 1;
if (keys['d'] || keys['arrowright']) dx += 1;
const speed = player.speed * (keys['shift'] ? 1.5 : 1);
const len = Math.sqrt(dx*dx + dy*dy);
if (len > 0) {
dx /= len; dy /= len;
player.x += dx * speed * dt;
player.y += dy * speed * dt;
}
// Clamp to world bounds
player.x = Math.max(0, Math.min(W - player.w, player.x));
player.y = Math.max(0, Math.min(H - player.h, player.y));
// Invincibility
if (player.invincible > 0) player.invincible -= dt;
// Shooting
const weapon = WEAPONS[player.currentWeapon];
const now = performance.now();
if ((keys['z'] || mouse.down) && now - player.lastShot > weapon.rate) {
player.lastShot = now;
const spreadAngle = (Math.random() - 0.5) * weapon.spread * Math.PI / 180;
const angle = Math.atan2(mouse.y - player.y, mouse.x - player.x) + spreadAngle;
const bulletSpeed = weapon.rate > 100 ? 14 : 10;
bullets.push(createBullet(
player.x + player.w/2, player.y + player.h/2,
angle, weapon.damage, weapon.bulletColor, bulletSpeed
));
if (weapon.rate > 100 && weapon.mag > 50) {
// Quick burst for minigun
for (let i = 0; i < 3; i++) {
const spreadAngle = (Math.random() - 0.5) * 0.3;
bullets.push(createBullet(
player.x + player.w/2, player.y + player.h/2,
angle + spreadAngle, weapon.damage * 0.8, weapon.bulletColor, 12
));
}
}
}
}
// ---- Camera ----
updateCamera();
// ---- Spawn Enemies ----
spawnTimer += dt * 1000;
if (spawnTimer >= spawnInterval) {
spawnTimer = 0;
spawnEnemy();
// Also spawn ammo pickups occasionally
if (Math.random() < 0.15) {
pickups.push(createPickup(
player.x + 200 + Math.random() * 200,
player.y + 50 + Math.random() * 200,
'ammo'
));
}
if (Math.random() < 0.1) {
pickups.push(createPickup(
player.x + 200 + Math.random() * 200,
player.y + 50 + Math.random() * 200,
'health'
));
}
}
// ---- Update Bullets ----
for (let i = bullets.length - 1; i >= 0; i--) {
const b = bullets[i];
b.x += Math.cos(b.angle) * b.speed * 60 * dt;
b.y += Math.sin(b.angle) * b.speed * 60 * dt;
b.life--;
// Trail
b.trail.push({ x: b.x, y: b.y, life: 8 });
if (b.trail.length > 6) b.trail.shift();
if (b.life <= 0) { bullets.splice(i, 1); continue; }
// Collisions with enemies
for (let j = enemies.length - 1; j >= 0; j--) {
const e = enemies[j];
if (!e.alive) continue;
const dist = Math.hypot(b.x - e.x, b.y - e.y);
if (dist < e.size + 4) {
e.hp -= b.damage;
e.hitFlash = 0.2;
// Hit particles
for (let k = 0; k < 3; k++) {
particles.push(createParticle(b.x, b.y, '#ffffff', 60, 10));
}
playSound('hit');
if (e.hp <= 0) {
e.alive = false;
score += e.score;
kills++;
comboCount++;
comboTimer = 1.5;
// Death particles
for (let k = 0; k < 12; k++) {
particles.push(createParticle(e.x, e.y, e.color, 100, 25));
}
playSound('hit');
if (e.type === 'boss') {
// Special boss death
for (let k = 0; k < 20; k++) {
particles.push(createParticle(e.x, e.y, '#ff0000', 150, 30));
}
flashAlpha = 1;
flashColor = '#ff0000';
}
bullets.splice(i, 1);
break;
}
}
}
// Collisions with pickups
for (let j = pickups.length - 1; j >= 0; j--) {
const p = pickups[j];
if (!p.alive) continue;
const dist = Math.hypot(b.x - p.x, b.y - p.y);
if (dist < 16) {
if (p.type === 'ammo') {
player.ammo += 30;
playSound('pickup');
particles.push(createParticle(p.x, p.y, '#ffaa00', 50, 15));
} else if (p.type === 'health') {
player.health = Math.min(player.maxHealth, player.health + 25);
playSound('pickup');
particles.push(createParticle(p.x, p.y, '#44ff44', 50, 15));
}
pickups.splice(j, 1);
}
}
}
// ---- Update Enemies ----
for (let i = enemies.length - 1; i >= 0; i--) {
const e = enemies[i];
if (!e.alive) { enemies.splice(i, 1); continue; }
// HP reduction
if (e.hitFlash > 0) e.hitFlash -= dt;
// Boss behavior
if (e.type === 'boss') {
e.patrolTimer += dt;
if (e.patrolTimer > 1.5) {
e.patrolTimer = 0;
e.patrolDir = (Math.random() - 0.5) * Math.PI * 2;
}
const targetX = player.x + (Math.random() - 0.5) * 100;
const targetY = player.y + (Math.random() - 0.5) * 100;
e.dx = targetX - e.x;
e.dy = targetY - e.y;
const dist = Math.hypot(e.dx, e.dy);
if (dist > 0) {
e.x += (e.dx / dist) * e.speed * 60 * dt;
e.y += (e.dy / dist) * e.speed * 60 * dt;
}
} else {
// Patrol or chase
e.patrolTimer += dt;
if (e.patrolTimer > 2.0) {
e.patrolTimer = 0;
e.patrolDir += (Math.random() - 0.5) * Math.PI;
}
e.x += Math.cos(e.patrolDir) * e.speed * 60 * dt;
e.y += Math.sin(e.patrolDir) * e.speed * 60 * dt;
// Move toward player when close enough
const distToPlayer = Math.hypot(e.x - player.x, e.y - player.y);
if (distToPlayer < 150) {
e.dx = player.x - e.x;
e.dy = player.y - e.y;
const dist = Math.hypot(e.dx, e.dy);
if (dist > 0) {
e.x += (e.dx / dist) * e.speed * 60 * dt;
e.y += (e.dy / dist) * e.speed * 60 * dt;
}
}
}
// Enemy shooting (boss and mutants)
if (e.type === 'boss') {
e.shootTimer += dt;
if (e.shootTimer > 2.0) {
e.shootTimer = 0;
const angle = Math.atan2(player.y - e.y, player.x - e.x);
bullets.push(createBullet(
e.x - Math.cos(angle) * 16, e.y - Math.sin(angle) * 16,
angle + (Math.random() - 0.5) * 0.2, 8, '#ff4400', 8
));
}
} else if (e.type === 'mutant') {
e.shootTimer += dt;
if (e.shootTimer > 1.5) {
e.shootTimer = 0;
const angle = Math.atan2(player.y - e.y, player.x - e.x);
bullets.push(createBullet(
e.x - Math.cos(angle) * 16, e.y - Math.sin(angle) * 16,
angle + (Math.random() - 0.5) * 0.15, 6, '#ff4400', 8
));
}
}
// Player collision
if (!player.dead && player.alive) {
const dist = Math.hypot(e.x - player.x, e.y - player.y);
if (dist < e.size + player.w/2) {
if (player.invincible <= 0) {
player.health -= 25;
player.invincible = 0.5;
screenShake = 5;
shakeIntensity = 5;
playSound('hit');
for (let k = 0; k < 5; k++) {
particles.push(createParticle(player.x, player.y, '#ff0000', 80, 12));
}
if (player.health <= 0) {
player.health = 0;
player.dead = true;
player.respawnTimer = 3;
player.alive = false;
deaths++;
playSound('death');
}
}
}
}
// Remove dead enemies
if (!e.alive) enemies.splice(i, 1);
}
// ---- Update Particles ----
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.x += p.vx * dt * 60;
p.y += p.vy * dt * 60;
p.vx *= 0.95;
p.vy *= 0.95;
p.life -= dt;
if (p.life <= 0) particles.splice(i, 1);
}
// ---- Update PowerUps ----
for (let i = powerups.length - 1; i >= 0; i--) {
const p = powerups[i];
p.pulse += dt * 3;
p.life -= dt;
if (p.life <= 0) powerups.splice(i, 1);
}
// ---- Combo Timer ----
if (comboTimer > 0) {
comboTimer -= dt;
if (comboTimer <= 0) comboCount = 0;
}
// ---- Level Check ----
if (player.health > 0 && player.alive) {
if (!levelEndShown && player.x > W - 300 && player.y > H - 300) {
levelEndShown = true;
levelEndTimer = 2;
playSound('win');
}
}
if (levelEndShown) {
levelEndTimer -= dt;
if (levelEndTimer <= 0) {
nextLevel();
}
}
// ---- Respawn ----
if (player.dead) {
player.respawnTimer -= dt;
if (player.respawnTimer <= 0) {
respawnPlayer();
}
}
// ---- Screen Shake ----
if (screenShake > 0) {
screenShake -= dt;
shakeIntensity *= 0.95;
}
// ---- Flash ----
if (flashAlpha > 0) {
flashAlpha -= dt * 2;
if (flashAlpha < 0) flashAlpha = 0;
}
// ---- Update Player ----
if (player.dead && player.alive) {
player.respawnTimer -= dt;
if (player.respawnTimer <= 0) respawnPlayer();
}
// ---- Check Win ----
if (player.health <= 0 && player.dead) {
// Already respawned
}
}
function respawnPlayer() {
player.x = 100;
player.y = 300;
player.health = 100;
player.invincible = 3;
player.dead = false;
player.alive = true;
player.ammo = WEAPONS[player.currentWeapon].mag;
spawnTimer = 0;
// Respawn particles
for (let i = 0; i < 20; i++) {
particles.push(createParticle(
player.x + 10 + Math.random() * 5, player.y + 10 + Math.random() * 5,
'#00ff41', 100, 15
));
}
playSound('respawn');
}
function nextLevel() {
level++;
startLevel();
}
// ---- Render ----
function render() {
ctx.save();
// Screen shake
if (screenShake > 0) {
const sx = (Math.random() - 0.5) * shakeIntensity * 2;
const sy = (Math.random() - 0.5) * shakeIntensity * 2;
ctx.translate(sx, sy);
}
// Background
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, W, H);
// Grid
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
ctx.lineWidth = 1;
for (let x = 0; x < W; x += 32) {
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, H);
ctx.stroke();
}
for (let y = 0; y < H; y += 32) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(W, y);
ctx.stroke();
}
// Particles (behind entities)
for (const p of particles) {
const alpha = p.life / p.maxLife;
ctx.globalAlpha = alpha;
ctx.fillStyle = p.color;
ctx.fillRect(p.x - p.size/2, p.y - p.size/2, p.size, p.size);
}
ctx.globalAlpha = 1;
// Only render game world if playing
if (state === 'PLAYING' || state === 'DEATH' || state === 'WIN') {
// ---- Draw pickups ----
for (const p of pickups) {
const pulse = Math.sin(p.pulse) * 0.3 + 0.7;
ctx.globalAlpha = pulse;
if (p.type === 'ammo') {
// Ammo box
ctx.fillStyle = '#ffaa00';
ctx.fillRect(p.x - 10, p.y - 10, 20, 20);
ctx.fillStyle = '#ff4400';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText('30', p.x, p.y + 3);
} else {
// Health can
ctx.fillStyle = '#44ff44';
ctx.fillRect(p.x - 10, p.y - 10, 20, 20);
ctx.fillStyle = '#00ff44';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText('+25', p.x, p.y + 3);
}
ctx.globalAlpha = 1;
}
// ---- Draw enemies ----
for (const e of enemies) {
if (!e.alive) continue;
ctx.fillStyle = e.color;
ctx.fillRect(e.x - e.size/2, e.y - e.size/2, e.size, e.size);
// Eyes for mutants
if (e.type === 'mutant') {
ctx.fillStyle = '#fff';
ctx.fillRect(e.x - 3, e.y - e.size/2 + 2, 3, 3);
ctx.fillRect(e.x + 1, e.y - e.size/2 + 2, 3, 3);
ctx.fillStyle = '#000';
ctx.fillRect(e.x - 2, e.y - e.size/2 + 3, 2, 2);
ctx.fillRect(e.x + 2, e.y - e.size/2 + 3, 2, 2);
}
// Boss eyes
if (e.type === 'boss') {
ctx.fillStyle = '#ff0000';
ctx.fillRect(e.x - 5, e.y - e.size/2 + 2, 5, 5);
ctx.fillRect(e.x + 2, e.y - e.size/2 + 2, 5, 5);
}
// HP bar for bosses
if (e.type === 'boss') {
const barW = 30;
ctx.fillStyle = '#333';
ctx.fillRect(e.x - barW/2, e.y - e.size/2 - 6, barW, 3);
ctx.fillStyle = '#ff0000';
ctx.fillRect(e.x - barW/2, e.y - e.size/2 - 6, barW * (e.hp / e.maxHp), 3);
}
// Damage text
if (e.hitFlash > 0) {
ctx.fillStyle = '#fff';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
ctx.fillText('!', e.x, e.y - e.size/2 - 2);
}
// Name tag
ctx.fillStyle = '#fff';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
ctx.fillText(e.type.toUpperCase(), e.x, e.y - e.size/2 - 8);
}
// ---- Draw player ----
if (player.alive || player.dead) {
if (player.dead && player.respawnTimer > 2) {
// Dead player - blinking
ctx.globalAlpha = 0.5;
ctx.fillStyle = '#ff0000';
ctx.fillRect(player.x - player.w/2, player.y - player.h/2, player.w, player.h);
ctx.globalAlpha = 1;
} else if (player.invincible > 0 && Math.floor(player.invincible * 10) % 2 === 0) {
// Invincible - blinking
ctx.globalAlpha = 0.4;
ctx.fillStyle = '#00ff41';
ctx.fillRect(player.x - player.w/2, player.y - player.h/2, player.w, player.h);
ctx.globalAlpha = 1;
} else {
// Player body
ctx.fillStyle = '#00ff41';
ctx.fillRect(player.x - player.w/2, player.y - player.h/2, player.w, player.h);
// Head
ctx.fillStyle = '#00ff41';
ctx.fillRect(player.x - 5, player.y - player.h/2 - 6, 10, 8);
// Weapon
const w = WEAPONS[player.currentWeapon];
ctx.fillStyle = w.color;
ctx.fillRect(player.x - 2, player.y - 2, 4, 10);
// Legs
ctx.fillStyle = '#004400';
if (keys['w'] || keys['arrowup']) {
ctx.fillRect(player.x - 5, player.y + 12, 4, 6);
ctx.fillRect(player.x + 1, player.y + 12, 4, 6);
} else {
ctx.fillRect(player.x - 5, player.y + 12, 4, 6);
ctx.fillRect(player.x + 1, player.y + 12, 4, 6);
}
}
}
// ---- Draw bullets ----
for (const b of bullets) {
ctx.fillStyle = b.color;
ctx.fillRect(b.x - 2, b.y - 2, 4, 4);
// Trail
for (const t of b.trail) {
ctx.globalAlpha = t.life / 8 * 0.3;
ctx.fillStyle = b.color;
ctx.fillRect(t.x - 1, t.y - 1, 2, 2);
}
ctx.globalAlpha = 1;
}
// ---- Draw power-ups ----
for (const p of powerups) {
const pulse = Math.sin(p.pulse) * 0.3 + 0.7;
ctx.globalAlpha = pulse;
const colors = { health: '#44ff44', shield: '#00ccff', ammo: '#ffaa00' };
ctx.fillStyle = colors[p.type] || '#ffffff';
ctx.fillRect(p.x - 10, p.y - 10, 20, 20);
ctx.fillStyle = '#000';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
ctx.fillText(p.type === 'health' ? '+' : p.type === 'shield' ? '!' : '×', p.x, p.y + 3);
ctx.globalAlpha = 1;
}
// ---- Draw HUD ----
drawHUD();
// ---- Level Name ----
ctx.fillStyle = '#ffffff';
ctx.font = '14px monospace';
ctx.textAlign = 'left';
ctx.fillText('LEVEL ' + level + ': ' + LEVELS[Math.min(level - 1, LEVELS.length - 1)].name, 10, 16);
// ---- Boss indicator ----
if (levelEndShown) {
ctx.fillStyle = '#ffaa00';
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.fillText('PRESS SPACE TO CONTINUE', W/2, H - 20);
}
}
// ---- Menu Screen ----
if (state === 'MENU') {
drawMenu();
}
// ---- Pause Screen ----
if (state === 'PAUSED') {
drawPaused();
}
// ---- Death Screen ----
if (state === 'DEATH') {
drawDeath();
}
// ---- Win Screen ----
if (state === 'WIN') {
drawWin();
}
// ---- Flash effect ----
if (flashAlpha > 0) {
ctx.globalAlpha = flashAlpha;
ctx.fillStyle = flashColor;
ctx.fillRect(0, 0, W, H);
ctx.globalAlpha = 1;
}
ctx.restore();
}
function drawHUD() {
// Health bar
const barW = 160;
const barH = 16;
const barX = 10;
const barY = 10;
ctx.fillStyle = '#222';
ctx.fillRect(barX, barY, barW, barH);
const hpRatio = player.health / player.maxHealth;
ctx.fillStyle = hpRatio > 0.5 ? '#00ff41' : hpRatio > 0.25 ? '#ffaa00' : '#ff0000';
ctx.fillRect(barX, barY, barW * hpRatio, barH);
ctx.strokeStyle = '#555';
ctx.lineWidth = 1;
ctx.strokeRect(barX, barY, barW, barH);
// Health text
ctx.fillStyle = '#fff';
ctx.font = '10px monospace';
ctx.textAlign = 'left';
ctx.fillText('HP ' + Math.ceil(player.health) + '/' + player.maxHealth, barX + 2, barY + 12);
// Ammo
const weapon = WEAPONS[player.currentWeapon];
ctx.fillStyle = weapon.color;
ctx.font = '10px monospace';
ctx.fillText(weapon.name + ': ' + Math.ceil(player.ammo) + '/' + weapon.mag, barX, barY + 26);
// Weapon indicator
ctx.fillStyle = '#888';
ctx.font = '8px monospace';
ctx.fillText('[1:' + weapon.name + ' 2:' + WEAPONS[1].name + ' 3:' + WEAPONS[2].name + ']', barX, barY + 38);
// Score
ctx.fillStyle = '#fff';
ctx.textAlign = 'right';
ctx.fillText('SCORE: ' + score, W - 10, 16);
ctx.fillText('KILLS: ' + kills, W - 10, 28);
ctx.fillText('LEVEL: ' + level, W - 10, 40);
// Combo
if (comboCount > 1) {
ctx.fillStyle = '#ffaa00';
ctx.font = '10px monospace';
ctx.textAlign = 'center';
ctx.fillText('COMBO x' + comboCount + '!', W/2, 20);
}
// Level progress bar
const lvl = LEVELS[Math.min(level - 1, LEVELS.length - 1)];
const prog = (level - 1) / (LEVELS.length - 1);
const progX = W/2 - 30;
const progY = 52;
ctx.fillStyle = '#333';
ctx.fillRect(progX, progY, 60, 4);
ctx.fillStyle = '#00ff41';
ctx.fillRect(progX, progY, 60 * prog, 4);
ctx.fillStyle = '#fff';
ctx.font = '8px monospace';
ctx.textAlign = 'center';
ctx.fillText('PROGRESS', W/2, progY + 14);
}
function drawMenu() {
// Dark background
ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, W, H);
// Title
ctx.fillStyle = '#00ff41';
ctx.font = 'bold 42px monospace';
ctx.textAlign = 'center';
ctx.fillText('RETRO SHOOTER', W/2, 130);
// Subtitle
ctx.fillStyle = '#ff0066';
ctx.font = '14px monospace';
ctx.fillText('A Retro FPS Experience', W/2, 165);
// Start button
const btnX = W/2 - 100;
const btnY = 220;
const btnW = 200;
const btnH = 48;
ctx.fillStyle = '#ff4400';
ctx.fillRect(btnX, btnY, btnW, btnH);
ctx.fillStyle = '#fff';
ctx.font = '16px monospace';
ctx.textAlign = 'center';
ctx.fillText('PRESS ENTER TO START', W/2, btnY + 22);
// Instructions
ctx.fillStyle = '#888';
ctx.font = '10px monospace';
ctx.fillText('WASD: Move Mouse: Aim Click: Shoot', W/2, 340);
ctx.fillText('1/2/3: Switch Weapons ESC: Pause', W/2, 360);
ctx.fillText('3 Weapons: Pistol | Shotgun | Minigun', W/2, 380);
ctx.fillText('Collect Ammo & Health pickups!', W/2, 400);
ctx.fillText('Defeat the boss to complete each level!', W/2, 420);
// High Score
ctx.fillStyle = '#ffaa00';
ctx.font = '12px monospace';
ctx.fillText('HIGH SCORE: ' + highScore, W/2, 480);
// Controls summary
ctx.fillStyle = '#666';
ctx.font = '9px monospace';
ctx.fillText('Retro Pixel Art | Web Audio Powered', W/2, 540);
}
function drawPaused() {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#fff';
ctx.font = 'bold 36px monospace';
ctx.textAlign = 'center';
ctx.fillText('PAUSED', W/2, H/2 - 20);
ctx.font = '14px monospace';
ctx.fillText('Press ESC to resume', W/2, H/2 + 20);
}
function drawDeath() {
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#ff0000';
ctx.font = 'bold 48px monospace';
ctx.textAlign = 'center';
ctx.fillText('DEAD', W/2, H/2 - 20);
ctx.font = '14px monospace';
ctx.fillStyle = '#fff';
ctx.fillText('Respawning in 3 seconds...', W/2, H/2 + 20);
}
function drawWin() {
ctx.fillStyle = 'rgba(0,0,0,0.7)';
ctx.fillRect(0, 0, W, H);
ctx.fillStyle = '#ffaa00';
ctx.font = 'bold 48px monospace';
ctx.textAlign = 'center';
ctx.fillText('LEVEL COMPLETE!', W/2, H/2 - 20);
ctx.font = '14px monospace';
ctx.fillStyle = '#fff';
ctx.fillText('Score: ' + score + ' Kills: ' + kills, W/2, H/2 + 30);
ctx.fillText('Press ENTER for next level', W/2, H/2 + 60);
}
// ---- Game Loop ----
function gameLoop(timestamp) {
const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
lastTime = timestamp;
if (state === 'PLAYING') {
update(dt);
}
// Always render
render();
requestAnimationFrame(gameLoop);
}
// ---- Start ----
initGame();
lastTime = performance.now();
requestAnimationFrame(gameLoop);
// ---- Keyboard enter ----
document.addEventListener('keydown', e => {
if (e.key === 'Enter') {
if (state === 'MENU') {
startLevel();
} else if (state === 'DEATH') {
respawnPlayer();
} else if (state === 'WIN') {
level++;
startLevel();
}
}
if (e.key === ' ') {
if (state === 'DEATH' || state === 'WIN') {
// Trigger respawn / level advance
if (state === 'DEATH') respawnPlayer();
if (state === 'WIN') {
level++;
startLevel();
}
}
}
});
// ---- Handle window resize ----
function resizeCanvas() {
const container = document.getElementById('gameContainer');
const scale = Math.min(window.innerWidth / W, window.innerHeight / H);
canvas.style.width = (W * scale) + 'px';
canvas.style.height = (H * scale) + 'px';
}
resizeCanvas();
window.addEventListener('resize', resizeCanvas);
</script>
</body>
</html>
<!-- agent-meta {"model":"inclusionai/ling-3.0-tiny:free","provider":"openrouter","persona":"full","sessionId":"6332a0b3-f658-476b-8318-7a5a473aba1f","tokensIn":180688,"tokensOut":21784,"tokensTotal":202472,"cost":0,"turns":8,"toolCalls":7,"failedToolCalls":0,"timestamp":"2026-08-09T02:31:41.659Z"} -->