← DOOM results

DOOM

Qwen 3.6 35b a3b · 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 35,451 bytes · SHA-256 1681b3ab0263
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DOOMSDAY — Retro FPS</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
#game { display: block; width: 100vw; height: 100vh; image-rendering: pixelated; image-rendering: crisp-edges; }

#hud {
  position: fixed; bottom: 0; left: 0; right: 0;
  height: 70px; background: linear-gradient(to bottom, #333, #111);
  border-top: 3px solid #555; display: flex; align-items: center;
  padding: 0 30px; gap: 30px; z-index: 10;
}
#health-bar { display: flex; align-items: center; gap: 8px; }
#health-label { color: #aaa; font-size: 11px; letter-spacing: 2px; }
.bar-bg { width: 150px; height: 14px; background: #222; border: 1px solid #555; }
.bar-fill { height: 100%; background: linear-gradient(to right, #c00, #f44); transition: width 0.2s; }
#health-value { color: #f44; font-size: 20px; font-weight: bold; min-width: 40px; text-align: right; }
#ammo-bar { display: flex; align-items: center; gap: 8px; }
#ammo-label { color: #aaa; font-size: 11px; letter-spacing: 2px; }
#ammo-value { color: #ff0; font-size: 20px; font-weight: bold; }
#weapon-name { color: #0f0; font-size: 16px; letter-spacing: 3px; margin-left: auto; }
#weapon-icons { display: flex; gap: 6px; }
.wpn-icon { color: #555; font-size: 14px; padding: 4px 8px; border: 1px solid #444; cursor: pointer; }
.wpn-icon.active { color: #0f0; border-color: #0f0; background: rgba(0,255,0,0.1); }

#crosshair {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 24px; height: 24px; z-index: 5; pointer-events: none;
}
#crosshair::before, #crosshair::after {
  content: ''; position: absolute; background: rgba(255,255,255,0.6);
}
#crosshair::before { width: 2px; height: 100%; left: 50%; transform: translateX(-50%); }
#crosshair::after { height: 2px; width: 100%; top: 50%; transform: translateY(-50%); }

#damage-overlay {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  pointer-events: none; z-index: 4; opacity: 0;
  background: radial-gradient(ellipse at center, transparent 40%, rgba(255,0,0,0.5) 100%);
  transition: opacity 0.1s;
}
#damage-overlay.hit { opacity: 1; }

.overlay {
  position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  z-index: 100; background: rgba(0,0,0,0.85); color: #fff;
}
.overlay.hidden { display: none; }
.overlay h1 { font-size: 48px; letter-spacing: 8px; margin-bottom: 20px; }
.overlay p { font-size: 16px; color: #aaa; margin: 6px 0; }
.overlay button {
  margin-top: 30px; padding: 12px 40px; font-size: 18px; font-family: 'Courier New', monospace;
  background: #c00; color: #fff; border: 2px solid #f44; cursor: pointer;
  letter-spacing: 4px; transition: background 0.2s;
}
.overlay button:hover { background: #f44; }
#death-screen h1 { color: #c00; }
#win-screen h1 { color: #0f0; }
</style>
</head>
<body>
<canvas id="game"></canvas>
<canvas id="minimap" width="150" height="150"></canvas>
<div id="hud">
  <div class="hud-section" id="health-section">
    <span class="hud-label">HEALTH</span>
    <div id="health-bar-container"><div id="health-fill"></div></div>
    <span id="health-value" class="hud-value">100</span>
  </div>
  <div class="hud-section" id="ammo-section">
    <span class="hud-label">AMMO</span>
    <span id="ammo-value" class="hud-value">50</span>
  </div>
  <div class="hud-section" id="score-section">
    <span class="hud-label">SCORE</span>
    <span id="score-value" class="hud-value">0</span>
  </div>
  <div class="hud-section" id="weapon-section">
    <span class="hud-label">WEAPON</span>
    <div id="weapon-name">PISTOL</div>
    <div id="weapon-icons">
      <span class="wpn-icon" data-idx="0">1</span>
      <span class="wpn-icon" data-idx="1">2</span>
      <span class="wpn-icon" data-idx="2">3</span>
    </div>
  </div>
</div>
<div id="crosshair"></div>
<div id="damage-overlay"></div>
<div id="death-screen" class="overlay hidden">
  <h1>YOU DIED</h1>
  <p>Score: <span id="death-score">0</span></p>
  <p>Respawning in 3 seconds...</p>
</div><div id="win-screen" class="overlay hidden">
  <h1>MISSION COMPLETE</h1>
  <p>All enemies eliminated!</p>
  <p class="subtitle">Final Score: <span id="win-score">0</span></p>
  <button id="restart-btn">PLAY AGAIN</button>
</div>
<div id="start-screen" class="overlay">
  <h1>DOOMSDAY</h1>
  <p>WASD — Move &nbsp;|&nbsp; Mouse — Look &nbsp;|&nbsp; Click — Shoot</p>
  <p>1/2/3 — Weapons &nbsp;|&nbsp; E — Use Pickup</p>
  <p class="subtitle">Survive the dungeon. Eliminate all enemies.</p>
  <button id="start-btn">START GAME</button>
</div>
<script>
'use strict';

// ── const: MAP_W ──
const MAP_W = 24;

// ── const: MAP_H ──
const MAP_H = 24;

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

// ── const: WORLD_MAP ──
const WORLD_MAP = [
  [1,1,1,1,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,0,0,0,0,1],
  [1,0,0,0,0,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,3,0,0,0,0,0,4,4,4,0,0,0,1],
  [1,0,0,2,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,0,0,0,1],
  [1,0,0,2,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,4,0,0,0,1],
  [1,0,0,0,0,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,0,0,0,0,1],
  [1,0,0,0,0,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,0,0,0,0,1],
  [1,0,0,0,0,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,0,0,0,0,1],
  [1,0,0,0,0,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,0,0,0,0,1],
  [1,0,0,0,0,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,0,0,0,0,1],
  [1,0,0,0,0,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,0,0,0,0,1],
  [1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,1],
  [1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,1],
  [1,0,0,3,3,3,0,0,0,4,4,4,4,0,0,0,0,0,0,3,3,3,0,1],
  [1,0,0,0,0,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,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,1,1,1,1],
];

// ── const: WALL_COLORS ──
const WALL_COLORS = {
  1: { r: 120, g: 120, b: 120 },
  2: { r: 140, g: 60, b: 60 },
  3: { r: 60, g: 100, b: 140 },
  4: { r: 80, g: 140, b: 60 },
};

// ── const: WEAPONS ──
const WEAPONS = [
  { name: 'PISTOL', damage: 20, fireRate: 300, ammoPerShot: 1, maxAmmo: 999, spread: 0, auto: false },
  { name: 'SHOTGUN', damage: 60, fireRate: 700, ammoPerShot: 5, maxAmmo: 200, spread: 0.15, auto: false, pellets: 5 },
  { name: 'PLASMA', damage: 15, fireRate: 100, ammoPerShot: 2, maxAmmo: 500, spread: 0.02, auto: true },
];

// ── const: ENEMY_TYPES ──
const ENEMY_TYPES = {
  grunt: { hp: 50, speed: 0.015, damage: 8, color: '#a44', size: 0.3, attackRange: 6, attackRate: 1000, score: 100 },
  heavy: { hp: 120, speed: 0.008, damage: 15, color: '#a80', size: 0.4, attackRange: 8, attackRate: 1500, score: 200 },
  elite: { hp: 200, speed: 0.02, damage: 25, color: '#a0a', size: 0.35, attackRange: 10, attackRate: 800, score: 500 },
};

// ── const: PICKUP_TYPES ──
const PICKUP_TYPES = {
  health: { color: '#f00', size: 0.2, amount: 25, symbol: '+' },
  ammo: { color: '#ff0', size: 0.15, amount: 20, symbol: 'A' },
  armor: { color: '#00f', size: 0.2, amount: 50, symbol: 'H' },
};

// ── function: createPlayer ──
function createPlayer() {
  return {
    x: 2.5, y: 2.5,
    angle: 0,
    health: 100,
    maxHealth: 100,
    armor: 0,
    ammo: 50,
    weapon: 0,
    lastShot: 0,
    isDead: false,
    bobPhase: 0,
    bobAmount: 0,
  };
}

// ── function: createEnemy ──
function createEnemy(type, x, y) {
  const def = ENEMY_TYPES[type];
  return {
    type, x, y,
    hp: def.hp, maxHp: def.hp,
    speed: def.speed,
    damage: def.damage,
    color: def.color,
    size: def.size,
    attackRange: def.attackRange,
    attackRate: def.attackRate,
    lastAttack: 0,
    alive: true,
    alert: false,
    alertTimer: 0,
    moveTimer: 0,
    targetX: x, targetY: y,
    hitFlash: 0,
  };
}

// ── function: createPickup ──
function createPickup(type, x, y) {
  const def = PICKUP_TYPES[type];
  return {
    type, x, y,
    size: def.size,
    color: def.color,
    amount: def.amount,
    symbol: def.symbol,
    alive: true,
    bobPhase: Math.random() * Math.PI * 2,
  };
}

// ── function: createGameState ──
function createGameState() {
  return {
    player: createPlayer(),
    enemies: [],
    pickups: [],
    running: false,
    won: false,
    deathTimer: 0,
    totalScore: 0,
    kills: 0,
    totalEnemies: 0,
  };
}

// ── function: castRay ──
function castRay(player, angle) {
  const sin = Math.sin(angle);
  const cos = Math.cos(angle);
  const dx = cos;
  const dy = sin;

  let mapX = Math.floor(player.x);
  let mapY = Math.floor(player.y);

  const stepX = cos >= 0 ? 1 : -1;
  const stepY = sin >= 0 ? 1 : -1;

  let tMaxX = cos >= 0
    ? (mapX + 1 - player.x) / cos
    : (player.x - mapX) / (-cos);
  let tMaxY = sin >= 0
    ? (mapY + 1 - player.y) / sin
    : (player.y - mapY) / (-sin);

  const tStepX = stepX >= 0 ? 1 / cos : -1 / cos;
  const tStepY = stepY >= 0 ? 1 / sin : -1 / sin;

  let side = 0;
  let hit = false;
  let dist = 0;

  for (let i = 0; i < 64; i++) {
    if (tMaxX < tMaxY) {
      tMaxX += tStepX;
      mapX += stepX;
      side = 0;
    } else {
      tMaxY += tStepY;
      mapY += stepY;
      side = 1;
    }
    if (mapX < 0 || mapX >= MAP_W || mapY < 0 || mapY >= MAP_H) break;
    if (WORLD_MAP[mapY][mapX] > 0) {
      hit = true;
      break;
    }
  }

  if (side === 0) {
    dist = (mapX - player.x + (1 - stepX) / 2) / cos;
  } else {
    dist = (mapY - player.y + (1 - stepY) / 2) / sin;
  }

  let wallX;
  if (side === 0) {
    wallX = player.y + dist * sin;
  } else {
    wallX = player.x + dist * cos;
  }
  wallX -= Math.floor(wallX);

  return { dist, side, wallX, mapX, mapY, hit };
}

// ── function: renderGame ──
function renderGame(canvas, ctx, player, enemies, pickups, dt) {
  const W = canvas.width;
  const H = canvas.height;
  const FOV = Math.PI / 3;
  const NUM_RAYS = Math.min(W, 640);
  const stripWidth = W / NUM_RAYS;

  // Sky gradient
  const skyGrad = ctx.createLinearGradient(0, 0, 0, H / 2);
  skyGrad.addColorStop(0, '#112');
  skyGrad.addColorStop(1, '#335');
  ctx.fillStyle = skyGrad;
  ctx.fillRect(0, 0, W, H / 2);

  // Floor gradient
  const floorGrad = ctx.createLinearGradient(0, H / 2, 0, H);
  floorGrad.addColorStop(0, '#333');
  floorGrad.addColorStop(1, '#555');
  ctx.fillStyle = floorGrad;
  ctx.fillRect(0, H / 2, W, H / 2);

  // Cast rays
  const zBuffer = new Float32Array(NUM_RAYS);

  for (let i = 0; i < NUM_RAYS; i++) {
    const rayAngle = player.angle - FOV / 2 + (i / NUM_RAYS) * FOV;
    const ray = castRay(player, rayAngle);

    const perpDist = ray.dist * Math.cos(rayAngle - player.angle);
    zBuffer[i] = perpDist;

    if (!ray.hit) continue;

    const lineHeight = H / perpDist;
    const drawStart = (H - lineHeight) / 2;

    const wc = WALL_COLORS[WORLD_MAP[ray.mapY][ray.mapX]];
    const brightness = Math.max(0.15, 1 - perpDist / 16);
    const shade = ray.side === 1 ? 0.7 : 1.0;

    const r = Math.floor(wc.r * brightness * shade);
    const g = Math.floor(wc.g * brightness * shade);
    const b = Math.floor(wc.b * brightness * shade);

    // Add texture-like vertical stripes
    const stripePattern = (ray.wallX * 8) % 1;
    const texShade = stripePattern < 0.1 ? 0.85 : 1;

    ctx.fillStyle = `rgb(${Math.floor(r * texShade)},${Math.floor(g * texShade)},${Math.floor(b * texShade)})`;
    ctx.fillRect(i * stripWidth, drawStart, stripWidth + 1, lineHeight);

    // Wall top/bottom edge highlight
    if (lineHeight > 4) {
      ctx.fillStyle = `rgba(255,255,255,${0.08 * brightness})`;
      ctx.fillRect(i * stripWidth, drawStart, stripWidth + 1, 2);
      ctx.fillStyle = `rgba(0,0,0,${0.2 * brightness})`;
      ctx.fillRect(i * stripWidth, drawStart + lineHeight - 2, stripWidth + 1, 2);
    }
  }

  // Render sprites (enemies + pickups) with z-buffer
  renderSprites(canvas, ctx, player, enemies, pickups, zBuffer, NUM_RAYS, stripWidth, dt);
}

// ── function: renderSprites ──
function renderSprites(canvas, ctx, player, enemies, pickups, zBuffer, numRays, stripWidth, dt) {
  const W = canvas.width;
  const H = canvas.height;
  const FOV = Math.PI / 3;

  const allSprites = [];
  for (const e of enemies) {
    if (!e.alive) continue;
    const dist = Math.sqrt((e.x - player.x) ** 2 + (e.y - player.y) ** 2);
    allSprites.push({ ...e, spriteType: 'enemy', dist });
  }
  for (const p of pickups) {
    if (!p.alive) continue;
    const dist = Math.sqrt((p.x - player.x) ** 2 + (p.y - player.y) ** 2);
    allSprites.push({ ...p, spriteType: 'pickup', dist });
  }

  allSprites.sort((a, b) => b.dist - a.dist);

  for (const sprite of allSprites) {
    const dx = sprite.x - player.x;
    const dy = sprite.y - player.y;

    let spriteAngle = Math.atan2(dy, dx) - player.angle;
    while (spriteAngle < -Math.PI) spriteAngle += 2 * Math.PI;
    while (spriteAngle > Math.PI) spriteAngle -= 2 * Math.PI;

    if (Math.abs(spriteAngle) > FOV * 0.7) continue;

    const perpDist = sprite.dist * Math.cos(spriteAngle);
    if (perpDist < 0.3) continue;

    const screenX = W / 2 + (spriteAngle / (FOV / 2)) * (W / 2);
    const spriteHeight = (H / perpDist) * 0.8;
    const spriteWidth = spriteHeight * 0.6;
    const drawStartY = (H - spriteHeight) / 2;

    if (sprite.spriteType === 'enemy') {
      drawEnemy(ctx, screenX, drawStartY, spriteWidth, spriteHeight, sprite, perpDist, zBuffer, numRays, stripWidth);
    } else if (sprite.spriteType === 'pickup') {
      drawPickup(ctx, screenX, drawStartY, spriteWidth, spriteHeight, sprite, perpDist, zBuffer, numRays, stripWidth);
    }
  }
}

// ── function: drawEnemy ──
function drawEnemy(ctx, x, y, w, h, enemy, dist, zBuffer, numRays, stripWidth) {
  const brightness = Math.max(0.2, 1 - dist / 14);
  const flash = enemy.hitFlash > 0;
  const flashVal = flash ? 255 : Math.floor(160 * brightness);
  const flashVal2 = flash ? 255 : Math.floor(60 * brightness);
  const flashVal3 = flash ? 255 : Math.floor(60 * brightness);

  const startStrip = Math.max(0, Math.floor((x - w / 2) / stripWidth));
  const endStrip = Math.min(numRays - 1, Math.floor((x + w / 2) / stripWidth));
  let visible = false;
  for (let s = startStrip; s <= endStrip; s++) {
    if (zBuffer[s] > dist) { visible = true; break; }
  }
  if (!visible) return;

  const bodyTop = y + h * 0.15;
  const bodyH = h * 0.55;

  // Body with shading
  ctx.fillStyle = `rgb(${flashVal},${flashVal2},${flashVal3})`;
  ctx.fillRect(x - w * 0.3, bodyTop, w * 0.6, bodyH);

  // Darker belly
  ctx.fillStyle = `rgba(0,0,0,${0.15 * brightness})`;
  ctx.fillRect(x - w * 0.15, bodyTop + bodyH * 0.4, w * 0.3, bodyH * 0.3);

  // Head
  const headR = h * 0.25;
  const headY = bodyTop - headR * 0.5;
  ctx.beginPath();
  ctx.arc(x, headY, headR, 0, Math.PI * 2);
  const headColor = flash ? 'rgb(255,255,255)' : `rgb(${Math.floor(180 * brightness)},${Math.floor(140 * brightness)},${Math.floor(100 * brightness)})`;
  ctx.fillStyle = headColor;
  ctx.fill();

  // Eyes
  const eyeSize = headR * 0.2;
  const eyeY = headY - headR * 0.1;
  ctx.fillStyle = flash ? 'rgb(255,0,0)' : `rgb(255,${Math.floor(100 * brightness)},0)`;
  ctx.fillRect(x - headR * 0.4 - eyeSize / 2, eyeY, eyeSize, eyeSize);
  ctx.fillRect(x + headR * 0.4 - eyeSize / 2, eyeY, eyeSize, eyeSize);

  // Legs
  const legH = h * 0.25;
  const legY = bodyTop + bodyH;
  const legColor = flash ? 'rgb(200,200,200)' : `rgb(${Math.floor(80 * brightness)},${Math.floor(80 * brightness)},${Math.floor(80 * brightness)})`;
  ctx.fillStyle = legColor;
  ctx.fillRect(x - w * 0.25, legY, w * 0.12, legH);
  ctx.fillRect(x + w * 0.13, legY, w * 0.12, legH);

  // HP bar
  if (enemy.hp < enemy.maxHp) {
    const barW = w * 0.6;
    const barH = 4;
    const barY = headY - headR - 10;
    ctx.fillStyle = '#300';
    ctx.fillRect(x - barW / 2, barY, barW, barH);
    ctx.fillStyle = '#f00';
    ctx.fillRect(x - barW / 2, barY, barW * (enemy.hp / enemy.maxHp), barH);
  }
}

// ── function: drawPickup ──
function drawPickup(ctx, x, y, w, h, pickup, dist, zBuffer, numRays, stripWidth) {
  const brightness = Math.max(0.3, 1 - dist / 12);
  const bob = Math.sin(pickup.bobPhase) * h * 0.03;

  const startStrip = Math.max(0, Math.floor((x - w / 2) / stripWidth));
  const endStrip = Math.min(numRays - 1, Math.floor((x + w / 2) / stripWidth));
  let visible = false;
  for (let s = startStrip; s <= endStrip; s++) {
    if (zBuffer[s] > dist) { visible = true; break; }
  }
  if (!visible) return;

  const size = w * 0.5;
  const py = y + h * 0.4 + bob;

  // Glow
  ctx.shadowColor = pickup.color;
  ctx.shadowBlur = 10;

  ctx.fillStyle = `rgba(${hexToRgb(pickup.color)},${0.3 * brightness})`;
  ctx.beginPath();
  ctx.arc(x, py, size * 1.2, 0, Math.PI * 2);
  ctx.fill();

  // Core
  ctx.fillStyle = `rgba(${hexToRgb(pickup.color)},${brightness})`;
  ctx.beginPath();
  ctx.arc(x, py, size * 0.5, 0, Math.PI * 2);
  ctx.fill();

  // Symbol
  ctx.shadowBlur = 0;
  ctx.fillStyle = `rgba(255,255,255,${brightness})`;
  ctx.font = `bold ${Math.floor(size * 0.8)}px Courier New`;
  ctx.textAlign = 'center';
  ctx.textBaseline = 'middle';
  ctx.fillText(pickup.symbol, x, py);
}

// ── function: hexToRgb ──
function hexToRgb(hex) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `${r},${g},${b}`;
}

// ── function: updateEnemies ──
function updateEnemies(enemies, player, dt, now) {
  const moveDir = (ex, ey, tx, ty) => {
    const dx = tx - ex;
    const dy = ty - ey;
    const dist = Math.sqrt(dx * dx + dy * dy);
    if (dist < 0.1) return { x: ex, y: ey };
    const nx = dx / dist;
    const ny = dy / dist;
    const step = 0.03;
    const newX = ex + nx * step;
    const newY = ey + ny * step;
    const margin = 0.2;
    if (WORLD_MAP[Math.floor(ey)][Math.floor(ex + nx * step)] === 0 &&
        Math.abs(Math.floor(ex + nx * step) - ex) < margin) {
      // Check wall collision
    }
    if (WORLD_MAP[Math.floor(newY)][Math.floor(newX)] === 0) {
      return { x: newX, y: newY };
    }
    // Try sliding
    if (WORLD_MAP[Math.floor(newY)][Math.floor(ex)] === 0) {
      return { x: ex, y: newY };
    }
    if (WORLD_MAP[Math.floor(ey)][Math.floor(newX)] === 0) {
      return { x: newX, y: ey };
    }
    return { x: ex, y: ey };
  };

  for (const e of enemies) {
    if (!e.alive) continue;

    if (e.hitFlash > 0) e.hitFlash -= dt * 5;

    const dx = player.x - e.x;
    const dy = player.y - e.y;
    const dist = Math.sqrt(dx * dx + dy * dy);

    // Alert check
    if (!e.alert) {
      if (dist < 8) {
        e.alert = true;
        e.alertTimer = now + 5000;
      }
      continue;
    }

    // Lose alert after time
    if (now > e.alertTimer && dist > 10) {
      e.alert = false;
      continue;
    }

    // Move toward player
    e.moveTimer -= dt * 1000;
    if (e.moveTimer <= 0) {
      e.moveTimer = 300 + Math.random() * 400;
      if (dist > 1.5) {
        const result = moveDir(e.x, e.y, player.x, player.y);
        e.x = result.x;
        e.y = result.y;
      }
    }

    // Attack
    if (dist < e.attackRange) {
      e.lastAttack -= dt * 1000;
      if (e.lastAttack <= 0) {
        e.lastAttack = e.attackRate;
        // Check line of sight
        const los = checkLineOfSight(e.x, e.y, player.x, player.y);
        if (los) {
          damagePlayer(e.damage, now);
        }
      }
    }
  }
}

// ── function: checkLineOfSight ──
function checkLineOfSight(x1, y1, x2, y2) {
  const dx = x2 - x1;
  const dy = y2 - y1;
  const dist = Math.sqrt(dx * dx + dy * dy);
  const steps = Math.ceil(dist * 10);
  for (let i = 0; i <= steps; i++) {
    const t = i / steps;
    const mx = Math.floor(x1 + dx * t);
    const my = Math.floor(y1 + dy * t);
    if (mx >= 0 && mx < MAP_W && my >= 0 && my < MAP_H) {
      if (WORLD_MAP[my][mx] > 0) return false;
    }
  }
  return true;
}

// ── let: damageFlashTimer ──
let damageFlashTimer = 0;

// ── function: damagePlayer ──
function damagePlayer(amount, now) {
  damageFlashTimer = now + 200;
  if (game.player.armor > 0) {
    const armorAbsorb = Math.min(game.player.armor, amount * 0.5);
    game.player.armor -= armorAbsorb;
    amount -= armorAbsorb;
  }
  game.player.health -= amount;
  if (game.player.health <= 0) {
    game.player.health = 0;
    game.player.isDead = true;
    game.deathTimer = 3000;
    gameRunning = false;
  }
}

// ── function: fireWeapon ──
function fireWeapon(game) {
  const { player, enemies } = game;
  const weapon = WEAPONS[player.weapon];
  const now = performance.now();

  if (now - player.lastShot < weapon.fireRate) return;
  if (player.ammo < weapon.ammoPerShot) return;

  player.lastShot = now;
  player.ammo -= weapon.ammoPerShot;

  const pellets = weapon.pellets || 1;
  for (let p = 0; p < pellets; p++) {
    const spread = (Math.random() - 0.5) * weapon.spread;
    const rayAngle = player.angle + spread;
    const ray = castRay(player, rayAngle);

    // Check enemy hits
    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);

      let spriteAngle = Math.atan2(dy, dx) - player.angle;
      while (spriteAngle < -Math.PI) spriteAngle += 2 * Math.PI;
      while (spriteAngle > Math.PI) spriteAngle -= 2 * Math.PI;

      const hitWidth = Math.atan2(e.size, dist);
      if (Math.abs(spriteAngle) < hitWidth && dist < ray.dist) {
        const dmg = weapon.damage / pellets;
        e.hp -= dmg;
        e.hitFlash = 1;
        e.alert = true;
        e.alertTimer = now + 10000;
        if (e.hp <= 0) {
          e.alive = false;
          game.kills++;
          game.totalScore += ENEMY_TYPES[e.type].score;
        }
        break;
      }
    }
  }
}

// ── function: updatePickups ──
function updatePickups(game, dt, now) {
  const { player, pickups, enemies } = game;

  for (const p of pickups) {
    if (!p.alive) continue;
    p.bobPhase += dt * 3;

    const dx = player.x - p.x;
    const dy = player.y - p.y;
    const dist = Math.sqrt(dx * dx + dy * dy);

    if (dist < 0.5) {
      if (p.type === 'health' && player.health < player.maxHealth) {
        player.health = Math.min(player.maxHealth, player.health + p.amount);
        p.alive = false;
      } else if (p.type === 'ammo') {
        player.ammo += p.amount;
        p.alive = false;
      } else if (p.type === 'armor' && player.armor < 100) {
        player.armor = Math.min(100, player.armor + p.amount);
        p.alive = false;
      }
    }
  }

  // Respawn pickups occasionally
  if (Math.random() < dt * 0.05) {
    const types = ['health', 'ammo', 'armor'];
    const type = types[Math.floor(Math.random() * types.length)];
    let px, py, attempts = 0;
    do {
      px = 2 + Math.floor(Math.random() * (MAP_W - 4));
      py = 2 + Math.floor(Math.random() * (MAP_H - 4));
      attempts++;
    } while ((WORLD_MAP[py][px] > 0 || attempts < 50) && attempts < 100);
    if (attempts < 100 && WORLD_MAP[py][px] === 0) {
      pickups.push(createPickup(type, px + 0.5, py + 0.5));
    }
  }

  // Spawn enemies periodically
  if (enemies.filter(e => e.alive).length < 8 && Math.random() < dt * 0.1) {
    const types = ['grunt', 'grunt', 'grunt', 'heavy', 'elite'];
    const type = types[Math.floor(Math.random() * types.length)];
    let ex, ey, attempts = 0;
    do {
      ex = 2 + Math.floor(Math.random() * (MAP_W - 4));
      ey = 2 + Math.floor(Math.random() * (MAP_H - 4));
      attempts++;
    } while ((WORLD_MAP[ey][ex] > 0 || attempts < 50) && attempts < 100);
    if (attempts < 100 && WORLD_MAP[ey][ex] === 0) {
      enemies.push(createEnemy(type, ex + 0.5, ey + 0.5));
    }
  }
}

// ── function: checkWinCondition ──
function checkWinCondition(game) {
  const aliveEnemies = game.enemies.filter(e => e.alive).length;
  return aliveEnemies === 0;
}

// ── function: respawnPlayer ──
function respawnPlayer(game) {
  game.player = createPlayer();
  game.player.x = 2.5;
  game.player.y = 2.5;
  game.player.angle = 0;
}

// ── main ──
let game = createGameState();
let keys = {};
let mouseDown = false;
let lastTime = 0;
let gameRunning = false;
let deathRespawnTimer = 0;

const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const minimapCanvas = document.getElementById('minimap');
const mctx = minimapCanvas.getContext('2d');

function resizeCanvas() {
  canvas.width = Math.min(window.innerWidth, 960);
  canvas.height = Math.min(window.innerHeight - 75, 720);
}

function initGame() {
  game = createGameState();
  game.enemies = [];
  game.pickups = [];

  const enemySpawns = [
    { type: 'grunt', x: 7.5, y: 7.5 },
    { type: 'grunt', x: 20.5, y: 7.5 },
    { type: 'heavy', x: 22.5, y: 14.5 },
    { type: 'grunt', x: 7.5, y: 20.5 },
    { type: 'elite', x: 18.5, y: 24.5 },
    { type: 'grunt', x: 12.5, y: 16.5 },
    { type: 'heavy', x: 2.5, y: 16.5 },
    { type: 'grunt', x: 25.5, y: 24.5 },
    { type: 'grunt', x: 4.5, y: 26.5 },
    { type: 'heavy', x: 25.5, y: 4.5 },
  ];
  for (const es of enemySpawns) game.enemies.push(createEnemy(es.type, es.x, es.y));
  game.totalEnemies = game.enemies.length;

  const pickupSpawns = [
    { type: 'health', x: 4.5, y: 4.5 },
    { type: 'ammo', x: 21.5, y: 4.5 },
    { type: 'armor', x: 12.5, y: 12.5 },
    { type: 'health', x: 22.5, y: 22.5 },
    { type: 'ammo', x: 3.5, y: 24.5 },
    { type: 'health', x: 26.5, y: 16.5 },
  ];
  for (const ps of pickupSpawns) game.pickups.push(createPickup(ps.type, ps.x, ps.y));

  game.running = true;
  game.won = false;
  game.deathTimer = 0;
  game.player.isDead = false;
}

function update(dt, now) {
  if (!game.running || game.won) return;

  if (game.player.isDead) {
    game.deathTimer -= dt * 1000;
    document.getElementById('death-screen').classList.remove('hidden');
    document.getElementById('death-score').textContent = game.totalScore;
    if (game.deathTimer <= 0) {
      respawnPlayer(game);
      game.deathTimer = 3000;
    }
    return;
  }

  const player = game.player;
  const moveSpeed = 0.04;
  let moving = false;

  if (keys['KeyW'] || keys['ArrowUp']) {
    const nx = player.x + Math.cos(player.angle) * moveSpeed;
    const ny = player.y + Math.sin(player.angle) * moveSpeed;
    if (WORLD_MAP[Math.floor(player.y)][Math.floor(nx)] === 0) player.x = nx;
    if (WORLD_MAP[Math.floor(ny)][Math.floor(player.x)] === 0) player.y = ny;
    moving = true;
  }
  if (keys['KeyS'] || keys['ArrowDown']) {
    const nx = player.x - Math.cos(player.angle) * moveSpeed;
    const ny = player.y - Math.sin(player.angle) * moveSpeed;
    if (WORLD_MAP[Math.floor(player.y)][Math.floor(nx)] === 0) player.x = nx;
    if (WORLD_MAP[Math.floor(ny)][Math.floor(player.x)] === 0) player.y = ny;
    moving = true;
  }
  if (keys['KeyA'] || keys['ArrowLeft']) {
    const nx = player.x + Math.cos(player.angle - Math.PI / 2) * moveSpeed;
    const ny = player.y + Math.sin(player.angle - Math.PI / 2) * moveSpeed;
    if (WORLD_MAP[Math.floor(player.y)][Math.floor(nx)] === 0) player.x = nx;
    if (WORLD_MAP[Math.floor(ny)][Math.floor(player.x)] === 0) player.y = ny;
    moving = true;
  }
  if (keys['KeyD'] || keys['ArrowRight']) {
    const nx = player.x + Math.cos(player.angle + Math.PI / 2) * moveSpeed;
    const ny = player.y + Math.sin(player.angle + Math.PI / 2) * moveSpeed;
    if (WORLD_MAP[Math.floor(player.y)][Math.floor(nx)] === 0) player.x = nx;
    if (WORLD_MAP[Math.floor(ny)][Math.floor(player.x)] === 0) player.y = ny;
    moving = true;
  }

  if (keys['Digit1']) { player.weapon = 0; keys['Digit1'] = false; }
  if (keys['Digit2']) { player.weapon = 1; keys['Digit2'] = false; }
  if (keys['Digit3']) { player.weapon = 2; keys['Digit3'] = false; }

  if (mouseDown) {
    const weapon = WEAPONS[player.weapon];
    if (weapon.auto || !player._wasMouseDown) fireWeapon(game);
  }
  player._wasMouseDown = mouseDown;

  if (moving) {
    player.bobPhase += dt * 8;
    player.bobAmount = Math.min(1, player.bobAmount + dt * 5);
  } else {
    player.bobAmount = Math.max(0, player.bobAmount - dt * 3);
  }

  updateEnemies(game.enemies, player, dt, now);
  updatePickups(game, dt, now);

  if (checkWinCondition(game)) {
    game.won = true;
    gameRunning = false;
    document.getElementById('win-score').textContent = game.totalScore;
    document.getElementById('win-screen').classList.remove('hidden');
  }

  updateHUD();
}

function render(now) {
  const player = game.player;
  ctx.fillStyle = '#000';
  ctx.fillRect(0, 0, canvas.width, canvas.height);

  const bobY = Math.sin(player.bobPhase) * 5 * player.bobAmount;
  const bobX = Math.cos(player.bobPhase / 2) * 3 * player.bobAmount;

  ctx.save();
  ctx.translate(bobX, bobY);
  renderGame(canvas, ctx, player, game.enemies, game.pickups, 1 / 60);
  ctx.restore();

  const overlay = document.getElementById('damage-overlay');
  if (damageFlashTimer > now) overlay.classList.add('hit');
  else overlay.classList.remove('hit');

  drawGun(ctx, now);
  drawMinimap();
}

function drawGun(ctx, now) {
  const W = canvas.width, H = canvas.height;
  const bobY = Math.sin(game.player.bobPhase) * 3 * game.player.bobAmount;
  const bobX = Math.cos(game.player.bobPhase / 2) * 2 * game.player.bobAmount;
  const gunX = W / 2 + bobX;
  const gunY = H - 10 + bobY;

  if (now - game.player.lastShot < 80) {
    const flashSize = 30 + Math.random() * 20;
    ctx.fillStyle = `rgba(255,${150 + Math.random() * 105},0,${0.8 - (now - game.player.lastShot) / 100})`;
    ctx.beginPath(); ctx.arc(gunX, gunY - 80, flashSize, 0, Math.PI * 2); ctx.fill();
    ctx.fillStyle = `rgba(255,255,200,${0.6 - (now - game.player.lastShot) / 120})`;
    ctx.beginPath(); ctx.arc(gunX, gunY - 80, flashSize * 0.5, 0, Math.PI * 2); ctx.fill();
  }

  ctx.fillStyle = '#444';
  if (game.player.weapon === 0) {
    ctx.fillRect(gunX - 15, gunY - 70, 30, 70);
    ctx.fillStyle = '#333'; ctx.fillRect(gunX - 8, gunY - 75, 16, 10);
  } else if (game.player.weapon === 1) {
    ctx.fillRect(gunX - 20, gunY - 80, 40, 80);
    ctx.fillStyle = '#555'; ctx.fillRect(gunX - 18, gunY - 85, 36, 8);
    ctx.fillStyle = '#333'; ctx.fillRect(gunX - 6, gunY - 90, 12, 15);
  } else {
    ctx.fillRect(gunX - 22, gunY - 75, 44, 75);
    ctx.fillStyle = '#0af'; ctx.fillRect(gunX - 5, gunY - 95, 10, 25);
    ctx.fillStyle = `rgba(0,170,255,${0.3 + Math.sin(now * 0.01) * 0.2})`;
    ctx.beginPath(); ctx.arc(gunX, gunY - 95, 8, 0, Math.PI * 2); ctx.fill();
  }
}

function drawMinimap() {
  const mw = minimapCanvas.width, mh = minimapCanvas.height;
  const scale = mw / MAP_W;
  mctx.fillStyle = 'rgba(0,0,0,0.8)';
  mctx.fillRect(0, 0, mw, mh);

  for (let y = 0; y < MAP_H; y++) {
    for (let x = 0; x < MAP_W; x++) {
      if (WORLD_MAP[y][x] > 0) {
        const c = WALL_COLORS[WORLD_MAP[y][x]];
        mctx.fillStyle = `rgb(${c.r},${c.g},${c.b})`;
        mctx.fillRect(x * scale, y * scale, scale + 0.5, scale + 0.5);
      } else {
        mctx.fillStyle = '#222';
        mctx.fillRect(x * scale, y * scale, scale + 0.5, scale + 0.5);
      }
    }
  }

  const p = game.player;
  mctx.fillStyle = '#0f0';
  mctx.beginPath();
  mctx.arc(p.x * scale, p.y * scale, 3, 0, Math.PI * 2);
  mctx.fill();

  mctx.strokeStyle = '#0f0';
  mctx.lineWidth = 1.5;
  mctx.beginPath();
  mctx.moveTo(p.x * scale, p.y * scale);
  mctx.lineTo((p.x + Math.cos(p.angle) * 1.5) * scale, (p.y + Math.sin(p.angle) * 1.5) * scale);
  mctx.stroke();

  for (const e of game.enemies) {
    if (!e.alive) continue;
    mctx.fillStyle = e.alert ? '#f00' : '#a00';
    mctx.fillRect(e.x * scale - 1.5, e.y * scale - 1.5, 3, 3);
  }

  for (const pk of game.pickups) {
    if (!pk.alive) continue;
    mctx.fillStyle = pk.color;
    mctx.fillRect(pk.x * scale - 1, pk.y * scale - 1, 2, 2);
  }
}

function updateHUD() {
  const player = game.player;
  const healthFill = document.getElementById('health-fill');
  const healthVal = document.getElementById('health-value');
  const ammoVal = document.getElementById('ammo-value');
  const scoreVal = document.getElementById('score-value');
  const weaponName = document.getElementById('weapon-name');

  healthFill.style.width = `${Math.max(0, (player.health / player.maxHealth) * 100)}%`;
  healthVal.textContent = Math.floor(player.health);
  ammoVal.textContent = player.ammo;
  scoreVal.textContent = game.totalScore;
  weaponName.textContent = WEAPONS[player.weapon].name;

  document.querySelectorAll('.wpn-icon').forEach((icon, i) => {
    icon.classList.toggle('active', i === player.weapon);
  });

  healthFill.classList.remove('low', 'critical');
  if (player.health <= 25) healthFill.classList.add('critical');
  else if (player.health <= 50) healthFill.classList.add('low');

  if (player.health > 60) {
    healthFill.style.background = 'linear-gradient(to right, #0a0, #4f4)';
    healthVal.style.color = '#4f4';
  } else if (player.health > 30) {
    healthFill.style.background = 'linear-gradient(to right, #aa0, #ff4)';
    healthVal.style.color = '#ff4';
  } else {
    healthFill.style.background = 'linear-gradient(to right, #c00, #f44)';
    healthVal.style.color = '#f44';
  }
}

function gameLoop(timestamp) {
  if (!lastTime) lastTime = timestamp;
  const dt = Math.min((timestamp - lastTime) / 1000, 0.05);
  lastTime = timestamp;

  if (gameRunning) {
    update(dt, timestamp);
    render(timestamp);
  }

  requestAnimationFrame(gameLoop);
}

document.addEventListener('keydown', (e) => { keys[e.code] = true; });
document.addEventListener('keyup', (e) => { keys[e.code] = false; });
document.addEventListener('mousedown', () => { mouseDown = true; });
document.addEventListener('mouseup', () => { mouseDown = false; });
document.addEventListener('mousemove', (e) => {
  if (!gameRunning) return;
  game.player.angle += e.movementX * 0.002;
});

document.getElementById('start-btn').addEventListener('click', () => {
  document.getElementById('start-screen').classList.add('hidden');
  canvas.requestPointerLock();
  initGame();
  gameRunning = true;
});

document.getElementById('restart-btn').addEventListener('click', () => {
  document.getElementById('win-screen').classList.add('hidden');
  canvas.requestPointerLock();
  initGame();
  gameRunning = true;
});

canvas.addEventListener('click', () => {
  if (gameRunning) canvas.requestPointerLock();
});

window.addEventListener('resize', resizeCanvas);
resizeCanvas();

requestAnimationFrame(gameLoop);
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"f8dca7a8-6006-4d56-834b-b4e1681437d9","tokensIn":3077997,"tokensOut":45720,"tokensTotal":3123717,"turns":76,"toolCalls":76,"failedToolCalls":7,"timestamp":"2026-07-20T22:04:11.264Z"} -->