← DOOM results

DOOM

thinkingcap qwen3.6 27b · composer

Initial view of DOOM

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 36,656 bytes · SHA-256 b721bc4179ce
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOMED DEPTHS - Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #000; overflow: hidden; font-family: 'Courier New', monospace; user-select: none; }
#game-container { position: relative; width: 100vw; height: 100vh; display: flex; justify-content: center; align-items: center; }
#gameCanvas { display: block; image-rendering: pixelated; background: #000; }
#minimap { position: absolute; top: 10px; right: 10px; border: 2px solid #555; opacity: 0.8; }
/* ── css: hud ── */
#hud { position: absolute; bottom: 0; left: 0; right: 0; height: 80px; background: linear-gradient(to top, rgba(0,0,0,0.9), transparent); pointer-events: none; display: flex; align-items: center; justify-content: space-between; padding: 10px 20px; }
#health-bar { width: 200px; height: 24px; background: #333; border: 2px solid #666; border-radius: 4px; overflow: hidden; }
#health-fill { height: 100%; background: linear-gradient(to right, #f00, #0f0); transition: width 0.3s; width: 100%; }
#health-text { color: #fff; font-size: 24px; margin-left: 10px; text-shadow: 2px 2px #000; }
#weapon-info { display: flex; flex-direction: column; align-items: flex-end; }
#weapon-name { color: #ff0; font-size: 18px; text-shadow: 2px 2px #000; }
#ammo-count { color: #fff; font-size: 28px; text-shadow: 2px 2px #000; }
#crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 20px; height: 20px; pointer-events: none; }
#crosshair::before, #crosshair::after { content: ''; position: absolute; background: rgba(255,255,255,0.7); }
#crosshair::before { width: 2px; height: 100%; left: 50%; transform: translateX(-50%); }
#crosshair::after { height: 2px; width: 100%; top: 50%; transform: translateY(-50%); }
/* ── css: overlay ── */
#overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.85); display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 10; cursor: pointer; }
#overlay.hidden { display: none; }
#overlay-title { color: #f00; font-size: 48px; text-shadow: 3px 3px #600, 0 0 20px #f00; margin-bottom: 20px; letter-spacing: 4px; }
#overlay-msg { color: #ccc; font-size: 20px; animation: pulse 1.5s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
</style>
</head>
<body>
<div id="game-container">
  <canvas id="gameCanvas"></canvas>
  <div id="hud">
    <div id="health-bar"><div id="health-fill"></div></div>
    <span id="health-text">100</span>
    <div id="weapon-info">
      <span id="weapon-name">PISTOL</span>
      <span id="ammo-count">∞</span>
    </div>
    <div id="crosshair"></div>
  <div id="kills-display" style="color:#f44;font-size:16px;text-shadow:2px 2px #000;">KILLS: <span id="kill-count">0</span>/<span id="total-enemies">13</span></div></div>
  <div id="overlay" class="hidden">
    <h1 id="overlay-title">DOOMED DEPTHS</h1>
    <p id="overlay-msg">Click to start</p>
  </div>
  <canvas id="minimap"></canvas>
</div>
<script>
'use strict';

// ── const: SCREEN_W ──
const SCREEN_W = 640;

// ── const: SCREEN_H ──
const SCREEN_H = 360;

// ── const: MINIMAP_SIZE ──
const MINIMAP_SIZE = 128;

// ── const: TILE ──
const TILE = 1;

// ── const: FOV ──
const FOV = Math.PI / 3;

// ── const: HALF_FOV ──
const HALF_FOV = FOV / 2;

// ── const: NUM_RAYS ──
const NUM_RAYS = SCREEN_W;

// ── const: MAX_DEPTH ──
const MAX_DEPTH = 20;

// ── const: MOVE_SPEED ──
const MOVE_SPEED = 3.5;

// ── const: ROT_SPEED ──
const ROT_SPEED = 2.5;

// ── const: WALL_COLORS ──
// Wall colors by type
const WALL_COLORS = {
  1: '#8B4513', // brown brick
  2: '#696969', // gray stone  
  3: '#556B2F', // dark green
  4: '#800020', // burgundy
};

// ── const: MAP ──
// Map: 0=empty, 1-4=walls, 9=exit
const MAP = [
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,2,2,2,0,0,0,3,3,0,0,0,4,4,4,0,0,1],
  [1,0,0,2,0,0,0,0,0,0,3,0,0,0,0,0,4,0,0,1],
  [1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,0,0,0,0,2,2,0,0,3,3,0,0,0,0,0,0,1],
  [1,0,0,0,0,0,0,2,0,0,0,0,3,0,0,4,4,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,0,0,0,1],
  [1,0,0,3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,3,0,0,0,4,4,0,0,2,2,0,0,0,0,0,0,1],
  [1,0,0,0,0,0,0,4,0,0,0,0,2,0,0,3,3,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,1],
  [1,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,4,0,2,0,0,0,3,3,0,0,4,4,0,0,9,0,1],
  [1,0,0,4,0,0,0,0,0,0,3,0,0,4,0,0,0,0,0,1],
  [1,0,0,4,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
  [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];

// ── const: MAP_H ──
const MAP_H = MAP.length;

// ── const: MAP_W ──
const MAP_W = MAP[0].length;

// ── const: WEAPONS ──
const WEAPONS = {
  pistol: { name: 'PISTOL', damage: 15, fireRate: 400, ammoUse: 1, range: 8, spread: 0.02, color: '#ff0' },
  shotgun: { name: 'SHOTGUN', damage: 10, fireRate: 800, ammoUse: 2, range: 6, spread: 0.08, pellets: 5, color: '#f80' },
  plasma: { name: 'PLASMA', damage: 30, fireRate: 150, ammoUse: 1, range: 12, spread: 0.01, color: '#0ff' }
};

// ── const: MAX_AMMO ──
const MAX_AMMO = { pistol: Infinity, shotgun: 20, plasma: 40 };

// ── function: castRay ──
function castRay(px, py, angle) {
  const sinA = Math.sin(angle);
  const cosA = Math.cos(angle);
  
  // DDA algorithm
  let mapX = Math.floor(px);
  let mapY = Math.floor(py);
  
  const deltaDistX = Math.abs(1 / (cosA || 0.0001));
  const deltaDistY = Math.abs(1 / (sinA || 0.0001));
  
  let stepX, stepY, sideDistX, sideDistY;
  
  if (cosA < 0) {
    stepX = -1;
    sideDistX = (px - mapX) * deltaDistX;
  } else {
    stepX = 1;
    sideDistX = (mapX + 1 - px) * deltaDistX;
  }
  
  if (sinA < 0) {
    stepY = -1;
    sideDistY = (py - mapY) * deltaDistY;
  } else {
    stepY = 1;
    sideDistY = (mapY + 1 - py) * deltaDistY;
  }
  
  let hit = false;
  let side = 0; // 0=NS, 1=EW
  let dist = 0;
  let wallType = 0;
  
  while (!hit) {
    if (sideDistX < sideDistY) {
      sideDistX += deltaDistX;
      mapX += stepX;
      side = 0;
    } else {
      sideDistY += deltaDistY;
      mapY += stepY;
      side = 1;
    }
    
    if (mapY >= 0 && mapY < MAP_H && mapX >= 0 && mapX < MAP_W) {
      if (MAP[mapY][mapX] > 0) {
        hit = true;
        wallType = MAP[mapY][mapX];
      }
    } else {
      hit = true;
      dist = MAX_DEPTH;
    }
  }
  
  if (side === 0) {
    dist = sideDistX - deltaDistX;
  } else {
    dist = sideDistY - deltaDistY;
  }
  
  // Calculate exact hit position for texture coordinate
  let wallX;
  if (side === 0) {
    wallX = py + dist * sinA;
  } else {
    wallX = px + dist * cosA;
  }
  wallX -= Math.floor(wallX);
  
  return { dist, side, wallType, wallX };
}

// ── function: getMapCell ──
function getMapCell(x, y) {
  const mx = Math.floor(x);
  const my = Math.floor(y);
  if (my < 0 || my >= MAP_H || mx < 0 || mx >= MAP_W) return 1;
  return MAP[my][mx];
}

// ── function: isWalkable ──
function isWalkable(x, y) {
  // Add small margin for player radius
  const r = 0.2;
  return getMapCell(x - r, y - r) === 0 &&
         getMapCell(x + r, y - r) === 0 &&
         getMapCell(x - r, y + r) === 0 &&
         getMapCell(x + r, y + r) === 0;
}

// ── function: distanceBetween ──
function distanceBetween(x1, y1, x2, y2) {
  return Math.sqrt((x2-x1)**2 + (y2-y1)**2);
}

// ── function: createPlayer ──
function createPlayer() {
  return {
    x: 1.5, y: 1.5,
    angle: 0,
    health: 100,
    maxHealth: 100,
    weapon: 'pistol',
    ammo: { pistol: Infinity, shotgun: 8, plasma: 12 },
    lastShot: 0,
    shooting: false,
    shootTimer: 0,
    bobPhase: 0,
    alive: true
  };
}

// ── function: createEnemy ──
function createEnemy(x, y, type) {
  return {
    x, y, type,
    health: type === 'imp' ? 40 : type === 'demon' ? 80 : 25,
    maxHealth: type === 'imp' ? 40 : type === 'demon' ? 80 : 25,
    speed: type === 'imp' ? 1.5 : type === 'demon' ? 2.5 : 3,
    damage: type === 'imp' ? 8 : type === 'demon' ? 15 : 5,
    attackRange: type === 'imp' ? 4 : 1.5,
    attackCooldown: type === 'imp' ? 1000 : type === 'demon' ? 600 : 300,
    lastAttack: 0,
    alertDist: type === 'imp' ? 8 : 6,
    alive: true,
    hitFlash: 0,
    color: type === 'imp' ? '#a52a2a' : type === 'demon' ? '#8b0000' : '#4a0',
    size: type === 'demon' ? 0.6 : 0.4
  };
}

// ── function: createPickup ──
function createPickup(x, y, type) {
  return { x, y, type, collected: false };
}

// ── function: renderScene ──
function renderScene(ctx, player) {
  const imgData = ctx.createImageData(SCREEN_W, SCREEN_H);
  const data = imgData.data;
  
  // Sky gradient (top half) and floor (bottom half)
  for (let y = 0; y < SCREEN_H; y++) {
    for (let x = 0; x < SCREEN_W; x++) {
      const idx = (y * SCREEN_W + x) * 4;
      if (y < SCREEN_H / 2) {
        // Sky - dark gradient
        const t = y / (SCREEN_H / 2);
        data[idx] = Math.floor(10 + t * 30);
        data[idx+1] = Math.floor(10 + t * 20);
        data[idx+2] = Math.floor(40 + t * 50);
      } else {
        // Floor - dark gradient
        const t = (y - SCREEN_H / 2) / (SCREEN_H / 2);
        data[idx] = Math.floor(30 + t * 20);
        data[idx+1] = Math.floor(25 + t * 15);
        data[idx+2] = Math.floor(20 + t * 10);
      }
      data[idx+3] = 255;
    }
  }
  
  // Cast rays and draw walls
  const zBuffer = new Float64Array(SCREEN_W);
  
  for (let col = 0; col < NUM_RAYS; col++) {
    const rayAngle = player.angle - HALF_FOV + (col / NUM_RAYS) * FOV;
    const ray = castRay(player.x, player.y, rayAngle);
    
    // Fix fisheye
    const perpDist = ray.dist * Math.cos(rayAngle - player.angle);
    zBuffer[col] = perpDist;
    
    if (ray.wallType === 0) continue;
    
    const lineHeight = SCREEN_H / perpDist;
    let drawStart = Math.floor(SCREEN_H / 2 - lineHeight / 2);
    let drawEnd = Math.floor(SCREEN_H / 2 + lineHeight / 2);
    
    // Get wall color with shading
    const baseColor = WALL_COLORS[ray.wallType] || '#888';
    const r = parseInt(baseColor.slice(1,3), 16);
    const g = parseInt(baseColor.slice(3,5), 16);
    const b = parseInt(baseColor.slice(5,7), 16);
    
    // Distance shading and side shading
    let shade = Math.max(0.15, 1 - perpDist / MAX_DEPTH);
    if (ray.side === 1) shade *= 0.7;
    
    for (let row = drawStart; row < drawEnd && row < SCREEN_H; row++) {
      if (row < 0) continue;
      const idx = (row * SCREEN_W + col) * 4;
      
      // Add stripe pattern for texture effect
      let texShade = shade;
      const wallTexY = ((row - drawStart) / lineHeight);
      if (Math.floor(wallTexY * 8) % 2 === 0 && ray.wallX > 0.1 && ray.wallX < 0.9) {
        texShade *= 0.9;
      }
      
      data[idx] = Math.min(255, r * texShade);
      data[idx+1] = Math.min(255, g * texShade);
      data[idx+2] = Math.min(255, b * texShade);
    }
  }
  
  ctx.putImageData(imgData, 0, 0);
  return zBuffer;
}

// ── function: drawSprite ──
function drawSprite(ctx, sprite, player, zBuffer) {
  if (!sprite.alive && sprite.type !== 'pickup') return;
  
  const dx = sprite.x - player.x;
  const dy = sprite.y - player.y;
  const dist = Math.sqrt(dx*dx + dy*dy);
  
  // Transform sprite relative to player direction
  const invDet = 1.0 / (Math.cos(player.angle) * Math.cos(player.angle) + Math.sin(player.angle) * Math.sin(player.angle));
  const transformX = Math.cos(player.angle) * dy - Math.sin(player.angle) * dx;
  const transformY = Math.sin(player.angle) * dx + Math.cos(player.angle) * dy;
  
  if (transformY <= 0.1) return; // Behind player
  
  const spriteScreenX = Math.floor((SCREEN_W / 2) * (1 + transformX / transformY));
  const spriteHeight = Math.abs(SCREEN_H / transformY);
  const spriteWidth = spriteHeight * 0.7;
  
  const drawStartX = Math.floor(spriteScreenX - spriteWidth / 2);
  const drawEndX = Math.floor(spriteScreenX + spriteWidth / 2);
  const drawStartY = Math.floor(SCREEN_H / 2 - spriteHeight / 2);
  const drawEndY = Math.floor(SCREEN_H / 2 + spriteHeight / 2);
  
  // Check z-buffer for visibility
  let visible = false;
  for (let x = Math.max(0, drawStartX); x < Math.min(SCREEN_W, drawEndX); x++) {
    if (x >= 0 && x < SCREEN_W && transformY < zBuffer[x]) {
      visible = true;
      break;
    }
  }
  
  if (!visible) return;
  
  const shade = Math.max(0.2, 1 - dist / MAX_DEPTH);
  
  ctx.save();
  
  for (let x = Math.max(0, drawStartX); x < Math.min(SCREEN_W, drawEndX); x++) {
    if (x >= SCREEN_W || transformY >= zBuffer[x]) continue;
    
    const texU = (x - drawStartX) / (drawEndX - drawStartX);
    
    // Draw sprite body as a simple shape
    for (let y = Math.max(0, drawStartY); y < Math.min(SCREEN_H, drawEndY); y++) {
      const texV = (y - drawStartY) / (drawEndY - drawStartY);
      
      // Simple sprite rendering: body + head
      let isBody = false;
      if (sprite.type === 'pickup') {
        // Diamond shape for pickups
        const cx = Math.abs(texU - 0.5) * 2;
        const cy = Math.abs(texV - 0.5) * 2;
        isBody = (cx + cy < 1);
      } else {
        // Enemy body: wider at bottom, head on top
        const bodyWidth = 0.3 + texV * 0.2;
        if (texV > 0.3 && Math.abs(texU - 0.5) < bodyWidth) isBody = true;
        if (texV <= 0.35 && Math.abs(texU - 0.5) < 0.18) isBody = true; // head
      }
      
      if (!isBody) continue;
      
      let color;
      if (sprite.type === 'pickup') {
        if (sprite.type === 'health' || sprite.subtype === 'health') {
          color = `rgb(${Math.floor(0*shade)},${Math.floor(255*shade)},${Math.floor(0*shade)})`;
        } else {
          color = `rgb(${Math.floor(255*shade)},${Math.floor(200*shade)},${Math.floor(0*shade)})`;
        }
      } else if (sprite.hitFlash > 0) {
        color = '#fff';
      } else {
        const r = parseInt(sprite.color.slice(1,3), 16);
        const g = parseInt(sprite.color.slice(3,5), 16);
        const b = parseInt(sprite.color.slice(5,7), 16);
        color = `rgb(${Math.floor(r*shade)},${Math.floor(g*shade)},${Math.floor(b*shade)})`;
      }
      
      ctx.fillStyle = color;
      ctx.fillRect(x, y, 1, 1);
    }
    
    // Draw eyes for enemies
    if (sprite.type !== 'pickup') {
      const eyeY = Math.floor(SCREEN_H / 2 - spriteHeight * 0.35);
      const eyeSize = Math.max(1, spriteWidth * 0.08);
      ctx.fillStyle = `rgba(255,${sprite.hitFlash > 0 ? 0 : 100},0,${shade})`;
      ctx.fillRect(spriteScreenX - Math.floor(spriteWidth*0.1), eyeY, eyeSize, eyeSize);
      ctx.fillRect(spriteScreenX + Math.floor(spriteWidth*0.05), eyeY, eyeSize, eyeSize);
    }
  }
  
  // Health bar above enemy
  if (sprite.type !== 'pickup' && sprite.health < sprite.maxHealth) {
    const barW = spriteWidth * 0.8;
    const barH = Math.max(2, spriteHeight * 0.03);
    const barX = spriteScreenX - barW / 2;
    const barY = drawStartY - barH - 4;
    
    ctx.fillStyle = '#300';
    ctx.fillRect(barX, barY, barW, barH);
    ctx.fillStyle = '#f00';
    ctx.fillRect(barX, barY, barW * (sprite.health / sprite.maxHealth), barH);
  }
  
  ctx.restore();
}

// ── function: drawWeapon ──
function drawWeapon(ctx, player) {
  const bobOffset = Math.sin(player.bobPhase) * 5;
  const weaponData = WEAPONS[player.weapon];
  
  // Weapon position at bottom center
  const wx = SCREEN_W / 2 + bobOffset;
  const wy = SCREEN_H - 10 + Math.abs(bobOffset);
  
  ctx.save();
  
  if (player.shootTimer > 0) {
    // Muzzle flash
    const flashSize = 30 * (player.shootTimer / WEAPONS[player.weapon].fireRate);
    ctx.fillStyle = weaponData.color;
    ctx.globalAlpha = player.shootTimer / WEAPONS[player.weapon].fireRate;
    ctx.beginPath();
    ctx.arc(wx, wy - 80, flashSize, 0, Math.PI * 2);
    ctx.fill();
    ctx.globalAlpha = 1;
    
    // Bright center of flash
    ctx.fillStyle = '#fff';
    ctx.beginPath();
    ctx.arc(wx, wy - 80, flashSize * 0.4, 0, Math.PI * 2);
    ctx.fill();
  }
  
  // Draw weapon body
  if (player.weapon === 'pistol') {
    ctx.fillStyle = '#666';
    ctx.fillRect(wx - 8, wy - 70, 16, 50);
    ctx.fillStyle = '#444';
    ctx.fillRect(wx - 12, wy - 30, 24, 30);
  } else if (player.weapon === 'shotgun') {
    ctx.fillStyle = '#8B4513';
    ctx.fillRect(wx - 6, wy - 90, 12, 70);
    ctx.fillStyle = '#555';
    ctx.fillRect(wx - 10, wy - 30, 20, 30);
    // Double barrel look
    ctx.fillStyle = '#8B4513';
    ctx.fillRect(wx - 10, wy - 90, 6, 70);
    ctx.fillRect(wx + 4, wy - 90, 6, 70);
  } else if (player.weapon === 'plasma') {
    ctx.fillStyle = '#333';
    ctx.fillRect(wx - 12, wy - 80, 24, 55);
    // Glowing parts
    ctx.fillStyle = weaponData.color;
    ctx.globalAlpha = 0.7 + Math.sin(Date.now() * 0.01) * 0.3;
    ctx.fillRect(wx - 6, wy - 85, 12, 10);
    ctx.fillRect(wx - 4, wy - 50, 8, 20);
    ctx.globalAlpha = 1;
  }
  
  ctx.restore();
}

// ── function: drawMinimap ──
function drawMinimap(ctx, player, enemies) {
  const mctx = ctx;
  const cellSize = MINIMAP_SIZE / MAP_W;
  
  mctx.fillStyle = 'rgba(0,0,0,0.7)';
  mctx.fillRect(0, 0, MINIMAP_SIZE, MINIMAP_SIZE);
  
  // Draw map
  for (let y = 0; y < MAP_H; y++) {
    for (let x = 0; x < MAP_W; x++) {
      if (MAP[y][x] > 0) {
        mctx.fillStyle = WALL_COLORS[MAP[y][x]] || '#888';
        mctx.fillRect(x * cellSize, y * cellSize, cellSize, cellSize);
      }
    }
  }
  
  // Draw enemies
  for (const e of enemies) {
    if (!e.alive) continue;
    mctx.fillStyle = '#f00';
    mctx.fillRect(e.x * cellSize - 1, e.y * cellSize - 1, 3, 3);
  }
  
  // Draw player
  mctx.fillStyle = '#0f0';
  mctx.beginPath();
  mctx.arc(player.x * cellSize, player.y * cellSize, 2, 0, Math.PI * 2);
  mctx.fill();
  
  // Player direction indicator
  mctx.strokeStyle = '#0f0';
  mctx.lineWidth = 1;
  mctx.beginPath();
  mctx.moveTo(player.x * cellSize, player.y * cellSize);
  mctx.lineTo(
    (player.x + Math.cos(player.angle) * 0.5) * cellSize,
    (player.y + Math.sin(player.angle) * 0.5) * cellSize
  );
  mctx.stroke();
}

// ── function: updateEnemies ──
function updateEnemies(enemies, player, dt) {
  const now = Date.now();
  
  for (const e of enemies) {
    if (!e.alive) continue;
    
    const dist = distanceBetween(player.x, player.y, e.x, e.y);
    
    // Alert when player is close enough
    if (dist < e.alertDist && player.alive) {
      // Move toward player
      const angle = Math.atan2(player.y - e.y, player.x - e.x);
      const newX = e.x + Math.cos(angle) * e.speed * dt;
      const newY = e.y + Math.sin(angle) * e.speed * dt;
      
      if (isWalkable(newX, e.y)) e.x = newX;
      if (isWalkable(e.x, newY)) e.y = newY;
      
      // Attack when in range
      if (dist < e.attackRange && now - e.lastAttack > e.attackCooldown) {
        player.health -= e.damage;
        e.lastAttack = now;
        return e.damage; // damage taken
      }
    }
    
    // Hit flash decay
    if (e.hitFlash > 0) e.hitFlash -= dt * 3;
  }
  
  return 0;
}

// ── function: checkPickups ──
function checkPickups(player, pickups) {
  for (const p of pickups) {
    if (p.collected) continue;
    
    const dist = distanceBetween(player.x, player.y, p.x, p.y);
    if (dist < 0.5) {
      p.collected = true;
      
      // Play pickup sound
      playSound('pickup');
      
      if (p.type === 'health') {
        player.health = Math.min(player.maxHealth, player.health + 25);
      } else if (p.type === 'shotgun_ammo') {
        player.ammo.shotgun = Math.min(MAX_AMMO.shotgun, player.ammo.shotgun + 8);
      } else if (p.type === 'plasma_ammo') {
        player.ammo.plasma = Math.min(MAX_AMMO.plasma, player.ammo.plasma + 12);
      } else if (p.type === 'shotgun_weapon') {
        player.weapon = 'shotgun';
        player.ammo.shotgun = Math.min(MAX_AMMO.shotgun, player.ammo.shotgun + 8);
      } else if (p.type === 'plasma_weapon') {
        player.weapon = 'plasma';
        player.ammo.plasma = Math.min(MAX_AMMO.plasma, player.ammo.plasma + 12);
      }
    }
  }
}

// ── function: checkWinCondition ──
function checkWinCondition(player, enemies) {
  // Win: all enemies dead AND reach exit tile (9)
  const allDead = enemies.every(e => !e.alive);
  if (!allDead) return false;
  
  const cell = getMapCell(player.x, player.y);
  return cell === 9;
}

// ── function: respawnPlayer ──
function respawnPlayer(player) {
  player.health = player.maxHealth;
  player.alive = true;
  player.x = 1.5;
  player.y = 1.5;
  player.angle = 0;
  // Keep weapons and ammo
}

// ── function: handleInput ──
function handleInput(player, dt) {
  const moveX = Math.cos(player.angle);
  const moveY = Math.sin(player.angle);
  const strafeX = Math.cos(player.angle + Math.PI/2);
  const strafeY = Math.sin(player.angle + Math.PI/2);
  
  let moved = false;
  
  if (keys['KeyW'] || keys['ArrowUp']) {
    const nx = player.x + moveX * MOVE_SPEED * dt;
    const ny = player.y + moveY * MOVE_SPEED * dt;
    if (isWalkable(nx, player.y)) player.x = nx;
    if (isWalkable(player.x, ny)) player.y = ny;
    moved = true;
  }
  if (keys['KeyS'] || keys['ArrowDown']) {
    const nx = player.x - moveX * MOVE_SPEED * dt;
    const ny = player.y - moveY * MOVE_SPEED * dt;
    if (isWalkable(nx, player.y)) player.x = nx;
    if (isWalkable(player.x, ny)) player.y = ny;
    moved = true;
  }
  if (keys['KeyA']) {
    const nx = player.x - strafeX * MOVE_SPEED * dt;
    const ny = player.y - strafeY * MOVE_SPEED * dt;
    if (isWalkable(nx, player.y)) player.x = nx;
    if (isWalkable(player.x, ny)) player.y = ny;
    moved = true;
  }
  if (keys['KeyD']) {
    const nx = player.x + strafeX * MOVE_SPEED * dt;
    const ny = player.y + strafeY * MOVE_SPEED * dt;
    if (isWalkable(nx, player.y)) player.x = nx;
    if (isWalkable(player.x, ny)) player.y = ny;
    moved = true;
  }
  
  // Rotation
  if (keys['ArrowLeft']) player.angle -= ROT_SPEED * dt;
  if (keys['ArrowRight']) player.angle += ROT_SPEED * dt;
  
  // Head bob
  if (moved) {
    player.bobPhase += dt * 10;
  } else {
    player.bobPhase *= 0.9;
  }
}

// ── function: handleMouseInput ──
function handleMouseInput(player, mouse) {
  document.addEventListener('mousemove', (e) => {
    if (document.pointerLockElement && gameRunning) {
      player.angle += e.movementX * 0.003;
    }
  });
  
  document.addEventListener('mousedown', (e) => {
    if (!gameRunning) return;
    if (!player.alive) return;
    
    // Try to lock pointer on first click
    if (!document.pointerLockElement) {
      document.getElementById('gameCanvas').requestPointerLock();
      return;
    }
    
    shootWeapon(player, enemies);
  });
}

// ── function: showOverlay ──
function showOverlay(title, msg) {
  const overlay = document.getElementById('overlay');
  const titleEl = document.getElementById('overlay-title');
  const msgEl = document.getElementById('overlay-msg');
  
  if (titleEl) titleEl.textContent = title;
  if (msgEl) msgEl.textContent = msg;
  overlay.classList.remove('hidden');
}

// ── function: hideOverlay ──
function hideOverlay() {
  document.getElementById('overlay').classList.add('hidden');
}

// ── function: updateHUD ──
function updateHUD(player) {
  const healthFill = document.getElementById('health-fill');
  const healthText = document.getElementById('health-text');
  const weaponName = document.getElementById('weapon-name');
  const ammoCount = document.getElementById('ammo-count');
  const killCount = document.getElementById('kill-count');
  
  if (healthFill && healthText && weaponName && ammoCount) {
    const pct = Math.max(0, player.health / player.maxHealth * 100);
    healthFill.style.width = pct + '%';
    healthText.textContent = Math.ceil(player.health);
    
    // Health bar color based on level
    if (pct > 60) {
      healthFill.style.background = 'linear-gradient(to right, #0a0, #0f0)';
    } else if (pct > 30) {
      healthFill.style.background = 'linear-gradient(to right, #aa0, #ff0)';
    } else {
      healthFill.style.background = 'linear-gradient(to right, #a00, #f00)';
    }
    
    weaponName.textContent = WEAPONS[player.weapon].name;
    
    if (player.weapon === 'pistol') {
      ammoCount.textContent = '∞';
    } else {
      const amt = player.ammo[player.weapon];
      ammoCount.textContent = amt;
      ammoCount.style.color = amt <= 3 ? '#f44' : '#fff';
    }
  }
  
  if (killCount) {
    killCount.textContent = enemies.filter(e => !e.alive).length;
  }
}

// ── let: audioCtx ──
// Simple sound synthesis for weapon effects
let audioCtx = null;

// ── function: playSound ──
function playSound(type) {
  try {
    if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    
    const osc = audioCtx.createOscillator();
    const gain = audioCtx.createGain();
    osc.connect(gain);
    gain.connect(audioCtx.destination);
    
    const now = audioCtx.currentTime;
    
    if (type === 'pistol') {
      osc.type = 'square';
      osc.frequency.setValueAtTime(200, now);
      osc.frequency.exponentialRampToValueAtTime(50, now + 0.1);
      gain.gain.setValueAtTime(0.15, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1);
      osc.start(now);
      osc.stop(now + 0.1);
    } else if (type === 'shotgun') {
      // Noise-like burst using multiple oscillators
      for (let i = 0; i < 3; i++) {
        const o = audioCtx.createOscillator();
        const g = audioCtx.createGain();
        o.connect(g);
        g.connect(audioCtx.destination);
        o.type = 'sawtooth';
        o.frequency.setValueAtTime(100 + Math.random()*50, now);
        o.frequency.exponentialRampToValueAtTime(30, now + 0.2);
        g.gain.setValueAtTime(0.12, now);
        g.gain.exponentialRampToValueAtTime(0.001, now + 0.2);
        o.start(now);
        o.stop(now + 0.2);
      }
    } else if (type === 'plasma') {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(800, now);
      osc.frequency.exponentialRampToValueAtTime(200, now + 0.15);
      gain.gain.setValueAtTime(0.1, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
      osc.start(now);
      osc.stop(now + 0.15);
    } else if (type === 'hit') {
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(300, now);
      osc.frequency.exponentialRampToValueAtTime(100, 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);
    } else if (type === 'pickup') {
      osc.type = 'sine';
      osc.frequency.setValueAtTime(400, now);
      osc.frequency.linearRampToValueAtTime(600, now + 0.1);
      gain.gain.setValueAtTime(0.08, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.15);
      osc.start(now);
      osc.stop(now + 0.15);
    } else if (type === 'death') {
      osc.type = 'sawtooth';
      osc.frequency.setValueAtTime(200, now);
      osc.frequency.exponentialRampToValueAtTime(30, now + 0.5);
      gain.gain.setValueAtTime(0.15, now);
      gain.gain.exponentialRampToValueAtTime(0.001, now + 0.5);
      osc.start(now);
      osc.stop(now + 0.5);
    }
  } catch(e) { /* Audio not available */ }
}

// ── function: shootWeapon ──
function shootWeapon(player, enemies) {
  const now = Date.now();
  const weapon = WEAPONS[player.weapon];
  
  if (now - player.lastShot < weapon.fireRate) return false;
  if (player.ammo[player.weapon] <= 0 && player.weapon !== 'pistol') return false;
  
  player.lastShot = now;
  player.shootTimer = 150;
  
  // Play weapon sound
  playSound(player.weapon);
  
  if (player.weapon !== 'pistol') {
    player.ammo[player.weapon] -= weapon.ammoUse;
  }
  
  const pellets = weapon.pellets || 1;
  let hitAny = false;
  
  for (let p = 0; p < pellets; p++) {
    const spreadAngle = player.angle + (Math.random() - 0.5) * weapon.spread * 2;
    
    let closestHit = null;
    let closestDist = Infinity;
    
    for (const e of enemies) {
      if (!e.alive) continue;
      
      const dx = e.x - player.x;
      const dy = e.y - player.y;
      const dist = Math.sqrt(dx*dx + dy*dy);
      
      if (dist > weapon.range) continue;
      
      const angleToEnemy = Math.atan2(dy, dx);
      let angleDiff = spreadAngle - angleToEnemy;
      
      while (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
      while (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;
      
      const hitWidth = e.size / dist;
      if (Math.abs(angleDiff) < hitWidth && dist < closestDist) {
        const ray = castRay(player.x, player.y, angleToEnemy);
        if (ray.dist > dist * 0.95) {
          closestHit = e;
          closestDist = dist;
        }
      }
    }
    
    if (closestHit) {
      const dmgMult = 1 - (closestDist / weapon.range) * 0.3;
      closestHit.health -= Math.floor(weapon.damage * dmgMult);
      closestHit.hitFlash = 1;
      
      if (closestHit.health <= 0) {
        closestHit.alive = false;
      }
      hitAny = true;
    }
  }
  
  return hitAny;
}

// ── function: drawDamageOverlay ──
function drawDamageOverlay(ctx, damage) {
  if (damage <= 0) return;
  const alpha = Math.min(0.6, damage / 50);
  ctx.fillStyle = `rgba(255, 0, 0, ${alpha})`;
  ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
}

// ── function: drawVignette ──
function drawVignette(ctx) {
  const gradient = ctx.createRadialGradient(SCREEN_W/2, SCREEN_H/2, SCREEN_H*0.3, SCREEN_W/2, SCREEN_H/2, SCREEN_H*0.8);
  gradient.addColorStop(0, 'rgba(0,0,0,0)');
  gradient.addColorStop(1, 'rgba(0,0,0,0.5)');
  ctx.fillStyle = gradient;
  ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
}

// ── let: weaponNotifyTimer ──
// Weapon switch notification
let weaponNotifyTimer = 0;

// ── let: weaponNotifyText ──
let weaponNotifyText = '';

// ── function: notifyWeapon ──
function notifyWeapon(name) {
  weaponNotifyText = name;
  weaponNotifyTimer = 120;
}

// ── function: drawNotifications ──
function drawNotifications(ctx) {
  if (weaponNotifyTimer > 0) {
    const alpha = Math.min(1, weaponNotifyTimer / 30);
    ctx.fillStyle = `rgba(255, 255, 0, ${alpha})`;
    ctx.font = 'bold 24px Courier New';
    ctx.textAlign = 'center';
    ctx.fillText('>>> ' + weaponNotifyText + ' <<<', SCREEN_W/2, SCREEN_H * 0.3);
    weaponNotifyTimer--;
  }
}

// ── let: prevWeapon ──
// Track previous weapon for notifications
let prevWeapon = 'pistol';

// ── function: initInput ──
function initInput(keys, mouse) {
  document.addEventListener('keydown', (e) => {
    keys[e.code] = true;
    
    if (e.code === 'Digit1') {
      if (player.weapon !== 'pistol') { player.weapon = 'pistol'; notifyWeapon(WEAPONS.pistol.name); }
    }
    if (e.code === 'Digit2') {
      if (player.weapon !== 'shotgun') { player.weapon = 'shotgun'; notifyWeapon(WEAPONS.shotgun.name); }
    }
    if (e.code === 'Digit3') {
      if (player.weapon !== 'plasma') { player.weapon = 'plasma'; notifyWeapon(WEAPONS.plasma.name); }
    }
    
    if (['Space', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.code)) {
      e.preventDefault();
    }
  });
  
  document.addEventListener('keyup', (e) => {
    keys[e.code] = false;
  });
}

// ── main ──
// Setup canvases
const canvas = document.getElementById('gameCanvas');
canvas.width = SCREEN_W;
canvas.height = SCREEN_H;
canvas.style.width = '100vw';
canvas.style.height = 'auto';
canvas.style.maxHeight = 'calc(100vh - 80px)';

const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;

// Minimap
const minimapCanvas = document.getElementById('minimap');
minimapCanvas.width = MINIMAP_SIZE;
minimapCanvas.height = MINIMAP_SIZE;
const mctx = minimapCanvas.getContext('2d');

// Game state
let player = createPlayer();
let enemies = [];
let pickups = [];
let keys = {};
let mouse = { x: 0, y: 0 };
let gameRunning = false;
let gameOver = false;
let damageFlash = 0;

const totalEl = document.getElementById('total-enemies');
if (totalEl) totalEl.textContent = '13';

function spawnEnemies() {
  const positions = [
    {x:5.5,y:3.5,type:'soldier'},
    {x:10.5,y:2.5,type:'imp'},
    {x:15.5,y:4.5,type:'soldier'},
    {x:8.5,y:7.5,type:'demon'},
    {x:3.5,y:9.5,type:'imp'},
    {x:12.5,y:8.5,type:'soldier'},
    {x:6.5,y:11.5,type:'imp'},
    {x:14.5,y:10.5,type:'demon'},
    {x:9.5,y:13.5,type:'soldier'},
    {x:4.5,y:15.5,type:'imp'},
    {x:16.5,y:14.5,type:'soldier'},
    {x:10.5,y:16.5,type:'demon'},
    {x:7.5,y:17.5,type:'imp'},
  ];
  enemies = positions.map(p => createEnemy(p.x, p.y, p.type));
}

function spawnPickups() {
  const positions = [
    {x:3.5,y:2.5,type:'health'},
    {x:8.5,y:4.5,type:'shotgun_ammo'},
    {x:14.5,y:2.5,type:'plasma_ammo'},
    {x:6.5,y:7.5,type:'health'},
    {x:10.5,y:9.5,type:'shotgun_weapon'},
    {x:3.5,y:12.5,type:'plasma_ammo'},
    {x:15.5,y:8.5,type:'health'},
    {x:8.5,y:14.5,type:'shotgun_ammo'},
    {x:12.5,y:16.5,type:'plasma_weapon'},
    {x:5.5,y:17.5,type:'health'},
  ];
  pickups = positions.map(p => createPickup(p.x, p.y, p.type));
}

function initGame() {
  player = createPlayer();
  spawnEnemies();
  spawnPickups();
  damageFlash = 0;
  gameOver = false;
  gameRunning = true;
  hideOverlay();
  
  canvas.addEventListener('click', () => {
    if (!document.pointerLockElement && gameRunning) {
      canvas.requestPointerLock();
    }
  });
}

initInput(keys, mouse);
showOverlay('DOOMED DEPTHS', 'Click to Start');

document.getElementById('overlay').addEventListener('click', () => {
  if (!gameRunning || gameOver) initGame();
});

let lastTime = performance.now();

function gameLoop(timestamp) {
  requestAnimationFrame(gameLoop);
  
  const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
  lastTime = timestamp;
  
  if (!gameRunning || gameOver) return;
  
  handleInput(player, dt);
  
  if (player.shootTimer > 0) player.shootTimer -= dt * 1000;
  if (damageFlash > 0) damageFlash -= dt * 30;
  
  const beforeKills = enemies.filter(e => !e.alive).length;
  
  const dmgTaken = updateEnemies(enemies, player, dt);
  if (dmgTaken > 0) {
    damageFlash = Math.max(damageFlash, dmgTaken);
    playSound('hit');
  }
  
  checkPickups(player, pickups);
  
  const afterKills = enemies.filter(e => !e.alive).length;
  if (afterKills > beforeKills) playSound('death');
  
  if (player.health <= 0 && player.alive) {
    player.alive = false;
    playSound('death');
    setTimeout(() => {
      respawnPlayer(player);
      spawnEnemies();
      spawnPickups();
      damageFlash = 0;
    }, 2000);
  }
  
  if (checkWinCondition(player, enemies)) {
    gameOver = true;
    gameRunning = false;
    showOverlay('VICTORY!', 'All demons slain! Click to play again');
  }
  
  // === RENDER ===
  ctx.clearRect(0, 0, SCREEN_W, SCREEN_H);
  
  if (player.alive) {
    const zBuffer = renderScene(ctx, player);
    
    const allSprites = [
      ...enemies.map(e => ({...e, dist: distanceBetween(player.x, player.y, e.x, e.y)})),
      ...pickups.filter(p => !p.collected).map(p => ({...p, type: 'pickup', subtype: p.type, dist: distanceBetween(player.x, player.y, p.x, p.y)}))
    ];
    allSprites.sort((a, b) => b.dist - a.dist);
    
    for (const sprite of allSprites) {
      drawSprite(ctx, sprite, player, zBuffer);
    }
    
    drawWeapon(ctx, player);
  } else {
    ctx.fillStyle = 'rgba(139, 0, 0, 0.6)';
    ctx.fillRect(0, 0, SCREEN_W, SCREEN_H);
    ctx.fillStyle = '#fff';
    ctx.font = 'bold 36px Courier New';
    ctx.textAlign = 'center';
    ctx.fillText('YOU DIED', SCREEN_W/2, SCREEN_H/2 - 10);
    ctx.font = '18px Courier New';
    ctx.fillText('Respawning...', SCREEN_W/2, SCREEN_H/2 + 25);
  }
  
  drawDamageOverlay(ctx, damageFlash);
  drawVignette(ctx);
  drawNotifications(ctx);
  updateHUD(player);
  drawMinimap(mctx, player, enemies);
}

requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"e1fda245-5e32-42f1-8c1a-a9ce291b7f2a","tokensIn":943421,"tokensOut":22930,"tokensTotal":966351,"turns":45,"toolCalls":44,"failedToolCalls":3,"timestamp":"2026-07-18T05:13:56.149Z"} -->