← DOOM results

DOOM

Tiel Coder 35B A3B GGUF MTP · full

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,243 bytes · SHA-256 0d9d0d32d0a7
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EMBER HALLS — Retro FPS</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { width:100%; height:100%; background:#000; overflow:hidden; font-family:'Courier New', monospace; }
  #wrap { position:fixed; inset:0; display:flex; align-items:center; justify-content:center; }
  #game { position:relative; width:100%; height:100%; background:#000; image-rendering:pixelated; }
  canvas { display:block; width:100%; height:100%; image-rendering:pixelated; image-rendering:crisp-edges; }

  /* HUD */
  #hud { position:absolute; inset:0; pointer-events:none; color:#e8c060; text-shadow:2px 2px 0 #000; }
  .hud-row { position:absolute; display:flex; gap:18px; font-weight:bold; }
  #hud-top { top:8px; left:0; right:0; justify-content:center; font-size:18px; letter-spacing:2px; }
  #hud-bottom { bottom:8px; left:0; right:0; justify-content:space-between; padding:0 12px; align-items:flex-end; }
  .hud-box { background:rgba(20,10,5,0.55); border:2px solid #6b4a1a; padding:3px 8px; border-radius:4px; }
  .hud-box .lab { font-size:9px; color:#b8945a; letter-spacing:2px; display:block; }
  .hud-box .val { font-size:20px; color:#f0d890; }
  .hud-box .val.low { color:#e04030; }
  #health .val { color:#7fe060; }
  #health .val.low { color:#e04030; }

  /* Crosshair */
  #crosshair { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:26px; height:26px; }
  #crosshair::before,#crosshair::after { content:''; position:absolute; background:rgba(255,255,255,0.75); }
  #crosshair::before { left:50%; top:0; width:2px; height:100%; transform:translateX(-50%); }
  #crosshair::after { top:50%; left:0; height:2px; width:100%; transform:translateY(-50%); }

  /* Damage flash */
  #damage { position:absolute; inset:0; background:radial-gradient(ellipse at center, transparent 40%, rgba(200,0,0,0.55) 100%); opacity:0; transition:opacity .12s; pointer-events:none; }

  /* Vignette */
  #vignette { position:absolute; inset:0; pointer-events:none; background:radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.6) 100%); }

  /* Weapon viewmodel */
  #weapon { position:absolute; bottom:-2px; left:50%; transform:translateX(-50%); width:420px; height:360px; pointer-events:none; }

  /* Overlays */
  .overlay { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
    background:rgba(10,4,2,0.9); color:#f0d890; text-align:center; z-index:20; }
  .overlay.hidden { display:none; }
  .title { font-size:64px; letter-spacing:6px; color:#e04020; text-shadow:4px 4px 0 #5a1206, 0 0 24px rgba(224,64,32,0.6); margin-bottom:6px; }
  .subtitle { font-size:20px; color:#c9a24a; letter-spacing:3px; margin-bottom:28px; }
  .panel { background:rgba(30,15,6,0.7); border:2px solid #6b4a1a; padding:22px 34px; border-radius:6px; max-width:560px; line-height:1.7; }
  .panel h2 { color:#e0c070; letter-spacing:3px; margin-bottom:12px; font-size:22px; }
  .panel .keys { color:#e8c060; }
  .btn { margin-top:24px; pointer-events:auto; cursor:pointer; background:#e04020; color:#fff; border:3px solid #7a1a0c;
    padding:14px 34px; font-family:inherit; font-size:22px; font-weight:bold; letter-spacing:3px; border-radius:4px;
    text-shadow:2px 2px 0 #5a1206; transition:transform .06s, background .15s; }
  .btn:hover { background:#ff5530; transform:scale(1.05); }
  .btn:active { transform:scale(0.97); }
  .win .title { color:#7fe060; text-shadow:4px 4px 0 #14540c, 0 0 24px rgba(127,224,96,0.5); }
  .lose .title { color:#e04020; }
  .stat { color:#c9a24a; font-size:16px; margin:4px 0; }
  #msg { position:absolute; top:70px; left:50%; transform:translateX(-50%); color:#fff; font-size:20px;
    letter-spacing:2px; text-shadow:2px 2px 0 #000; opacity:0; transition:opacity .3s; pointer-events:none; z-index:15; }
  .flash { animation:flash .4s ease-out; }
  @keyframes flash { from{filter:brightness(2.4);} to{filter:brightness(1);} }
  #minimap { position:absolute; top:12px; right:12px; border:2px solid #6b4a1a; background:rgba(0,0,0,0.55); opacity:0.85; }
</style>
</head>
<body>
<div id="wrap">
  <div id="game">
    <canvas id="screen"></canvas>
    <canvas id="minimap" width="140" height="140"></canvas>
    <div id="hud">
      <div id="hud-top">
        <span class="hud-box">SCORE <span class="val" id="score">0</span></span>
      </div>
      <div id="hud-bottom">
        <div id="health" class="hud-box"><span class="lab">HEALTH</span><span class="val" id="healthVal">100</span></div>
        <div id="weaponName" class="hud-box" style="text-align:center;"><span class="lab">WEAPON</span><span class="val" id="weaponVal">PISTOL</span></div>
        <div id="ammo" class="hud-box"><span class="lab">AMMO</span><span class="val" id="ammoVal">--</span></div>
        <div id="enemies" class="hud-box"><span class="lab">HOSTILES</span><span class="val" id="enemyVal">0</span></div>
      </div>
    </div>
    <div id="crosshair"></div>
    <div id="damage"></div>
    <div id="vignette"></div>
    <div id="msg"></div>
    <canvas id="weapon" width="420" height="360"></canvas>

    <div id="menu" class="overlay">
      <div class="title">EMBER HALLS</div>
      <div class="subtitle">— BLOW THE FOUNTAIN TO RUBBLE —</div>
      <div class="panel">
        <h2>THE MISSION</h2>
        <p>Survive the cursed halls. Hunt down every hostile, then reach the glowing <span class="keys">FOUNTAIN</span> at the center and destroy it with the <span class="keys">SHOTGUN</span>.</p>
        <h2 style="margin-top:16px;">CONTROLS</h2>
        <p><span class="keys">W A S D</span> — Move &nbsp; <span class="keys">Mouse</span> — Look</p>
        <p><span class="keys">Left Click</span> / <span class="keys">Space</span> — Fire</p>
        <p><span class="keys">1 2 3</span> — Switch Pistol / Shotgun / Rifle</p>
        <p><span class="keys">E</span> — Pick up &nbsp; <span class="keys">Esc</span> — Release mouse</p>
        <p style="color:#e0a040; margin-top:10px;">Pick up <span class="keys">HEALTH</span> and <span class="keys">AMMO</span>. Keep health above 0!</p>
      </div>
      <button class="btn" id="startBtn">ENTER THE HALLS</button>
    </div>

    <div id="deathScreen" class="overlay hidden lose">
      <div class="title">YOU DIED</div>
      <div class="subtitle">The halls claim another soul.</div>
      <div class="panel">
        <div class="stat">Final Score: <span class="keys" id="deathScore">0</span></div>
        <div class="stat">Hostiles Slain: <span class="keys" id="deathKills">0</span></div>
      </div>
      <button class="btn" id="respawnBtn">RESPAWN</button>
    </div>

    <div id="winScreen" class="overlay hidden win">
      <div class="title">VICTORY</div>
      <div class="subtitle">The fountain is destroyed. The halls are silent.</div>
      <div class="panel">
        <div class="stat">Final Score: <span class="keys" id="winScore">0</span></div>
        <div class="stat">Hostiles Slain: <span class="keys" id="winKills">0</span></div>
        <div class="stat">Health Remaining: <span class="keys" id="winHealth">0</span></div>
      </div>
      <button class="btn" id="winBtn">PLAY AGAIN</button>
    </div>
  </div>
</div>

<script>
"use strict";
(function(){
const canvas = document.getElementById('screen');
const ctx = canvas.getContext('2d');
const wpCanvas = document.getElementById('weapon');
const wctx = wpCanvas.getContext('2d');
const mini = document.getElementById('minimap');
const mctx = mini.getContext('2d');

// ---- Map ----
// 1..4 wall types, 0 floor. Fountain at center = 9
const 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,3,0,0,0,4,4,4,0,0,0,0,1],
  [1,0,0,2,0,0,0,0,0,0,0,0,3,0,0,0,0,0,4,0,0,0,0,1],
  [1,0,0,2,0,0,0,0,0,0,0,0,3,0,0,0,0,0,4,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,3,3,0,0,0,0,3,3,0,0,0,0,0,0,0,0,0,1],
  [1,0,0,0,0,0,3,0,0,0,0,0,0,3,0,0,0,2,2,2,0,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,2,0,0,0,0,0,1],
  [1,0,0,0,0,0,0,0,0,0,9,9,0,0,0,0,0,2,0,0,0,0,0,1],
  [1,0,0,0,0,0,3,0,0,0,0,0,0,3,0,0,0,2,0,0,0,0,0,1],
  [1,0,0,0,0,0,3,3,0,0,0,0,3,3,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,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,1],
  [1,0,0,4,0,0,0,0,0,3,0,0,3,0,0,0,0,0,2,0,0,0,0,1],
  [1,0,0,4,4,4,0,0,0,3,0,0,3,0,0,0,0,0,2,0,0,0,0,1],
  [1,0,0,0,0,0,0,0,0,3,3,3,3,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,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
];
const MAP_W = MAP[0].length, MAP_H = MAP.length;
function isWall(x,y){ if(x<0||y<0||x>=MAP_W||y>=MAP_H) return true; return MAP[y][x]!==0; }
function cellAt(x,y){ if(x<0||y<0||x>=MAP_W||y>=MAP_H) return 1; return MAP[y][x]; }

// Fountain center cell
const FOUNT_X = 10.5, FOUNT_Y = 10.5;

// ---- Game State ----
let W=0,H=0;
const state = {
  running:false, over:false, won:false,
  player:{ x:2.5, y:2.5, dir:0, health:100, dead:false },
  weapons:[
    { key:'pistol', name:'PISTOL',  ammo:Infinity, max:Infinity, dmg:25, rate:220, spread:0.02, pellets:1, auto:false, sound:'pistol', recoil:0.06 },
    { key:'shotgun',name:'SHOTGUN', ammo:32, max:32,  dmg:18, rate:620, spread:0.13, pellets:6, auto:false, sound:'shotgun', recoil:0.14 },
    { key:'rifle',  name:'RIFLE',   ammo:120, max:120, dmg:15, rate:110, spread:0.04, pellets:1, auto:true,  sound:'rifle',  recoil:0.04 }
  ],
  current:0,
  lastFire:0,
  enemies:[],
  pickups:[],
  score:0, kills:0,
  fireAnim:0, hurtAnim:0, bobT:0, moving:false
};
window.__GAME = state;

const keys = {};
let pointerLocked=false;

// ---- Resize ----
function resize(){
  const g = document.getElementById('game');
  W = Math.floor(g.clientWidth); H = Math.floor(g.clientHeight);
  canvas.width = W; canvas.height = H;
}
window.addEventListener('resize', resize);

// ---- Audio (WebAudio, generated) ----
let actx=null;
function initAudio(){ if(!actx){ try{ actx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } }
function beep(freq, dur, type='square', vol=0.15, slideTo=null){
  if(!actx) return;
  const t=actx.currentTime;
  const o=actx.createOscillator(), g=actx.createGain();
  o.type=type; o.frequency.setValueAtTime(freq,t);
  if(slideTo) o.frequency.exponentialRampToValueAtTime(slideTo, t+dur);
  g.gain.setValueAtTime(vol,t);
  g.gain.exponentialRampToValueAtTime(0.0001, t+dur);
  o.connect(g); g.connect(actx.destination);
  o.start(t); o.stop(t+dur);
}
function noiseBurst(dur, vol=0.2){
  if(!actx) return;
  const t=actx.currentTime;
  const buf=actx.createBuffer(1, actx.sampleRate*dur, actx.sampleRate);
  const d=buf.getChannelData(0);
  for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,2);
  const src=actx.createBufferSource(); src.buffer=buf;
  const g=actx.createGain(); g.gain.value=vol;
  const fl=actx.createBiquadFilter(); fl.type='lowpass'; fl.frequency.value=1200;
  src.connect(fl); fl.connect(g); g.connect(actx.destination);
  src.start();
}
const sfx={
  pistol:()=>{ beep(660,0.08,'square',0.12,220); noiseBurst(0.08,0.15); },
  shotgun:()=>{ noiseBurst(0.22,0.3); beep(180,0.15,'sawtooth',0.15,70); },
  rifle:()=>{ beep(900,0.05,'square',0.09,400); },
  hurt:()=>{ beep(140,0.18,'sawtooth',0.18,60); },
  enemyDie:()=>{ beep(300,0.25,'sawtooth',0.16,60); noiseBurst(0.15,0.12); },
  enemyHit:()=>{ beep(220,0.06,'square',0.08,120); },
  pickup:()=>{ beep(600,0.07,'sine',0.14,900); },
  shootEmpty:()=>{ beep(300,0.05,'square',0.08); },
  fire:()=>{ beep(1200,0.03,'square',0.05,600); },
  win:()=>{ [523,659,784,1047].forEach((f,i)=>setTimeout(()=>beep(f,0.25,'square',0.16),i*130)); },
  death:()=>{ [400,300,200,120].forEach((f,i)=>setTimeout(()=>beep(f,0.3,'sawtooth',0.16),i*160)); }
};

// ---- Enemy factory ----
function spawnEnemies(){
  state.enemies = [];
  const spots = [[6,6],[17,6],[6,17],[17,17],[11,4],[11,17],[4,11],[19,11],[11,19],[3,3]];
  spots.forEach((s,i)=>{
    state.enemies.push({
      x:s[0]+0.5, y:s[1]+0.5, hp:60, alive:true,
      speed:0.018 + i*0.001, hurt:0, attackCd:0,
      state:'idle', animT:Math.random()*6, alert:0, deadT:0
    });
  });
}

function spawnPickups(){
  state.pickups = [
    { x:2.5, y:5.5, type:'health', amount:25, taken:false },
    { x:20.5, y:3.5, type:'health', amount:25, taken:false },
    { x:11.5, y:2.5, type:'ammo', amount:40, taken:false },
    { x:3.5, y:20.5, type:'ammo', amount:60, taken:false },
    { x:20.5, y:20.5, type:'health', amount:50, taken:false },
    { x:11.5, y:21.5, type:'ammo', amount:60, taken:false },
    { x:8.5, y:11.5, type:'ammo', amount:30, taken:false },
    { x:15.5, y:12.5, type:'health', amount:30, taken:false }
  ];
}

// ---- Input ----
window.addEventListener('keydown', e=>{
  keys[e.code]=true;
  if(!state.running) return;
  if(e.code==='Digit1') selectWeapon(0);
  if(e.code==='Digit2') selectWeapon(1);
  if(e.code==='Digit3') selectWeapon(2);
  if(e.code==='KeyE') tryPickup();
  if(e.code==='Space'){ e.preventDefault(); fire(); }
});
window.addEventListener('keyup', e=>{ keys[e.code]=false; });

canvas.addEventListener('click', ()=>{
  if(state.running && !pointerLocked){ canvas.requestPointerLock(); }
  else if(state.running && pointerLocked){ fire(); }
});
document.addEventListener('pointerlockchange', ()=>{
  pointerLocked = (document.pointerLockElement===canvas);
});
document.addEventListener('mousemove', e=>{
  if(pointerLocked && state.running){
    state.player.dir += e.movementX * 0.0025;
  }
});

function selectWeapon(i){
  if(state.over||!state.running) return;
  state.current=i; state.fireAnim=6;
  updateHUD();
}

// ---- Firing ----
function fire(){
  if(state.over||!state.running) return;
  const now=performance.now();
  const w=state.weapons[state.current];
  if(now - state.lastFire < w.rate) return;
  if(w.ammo<=0){ sfx.shootEmpty(); return; }
  state.lastFire=now;
  if(w.ammo!==Infinity){ w.ammo--; }
  state.fireAnim=Math.max(state.fireAnim, w.key==='shotgun'?14:w.key==='rifle'?5:8);
  sfx[w.sound]();
  // Raycast hit detection for each pellet
  for(let p=0;p<w.pellets;p++){
    const spread=(Math.random()-0.5)*w.spread*2;
    const ang=state.player.dir+spread;
    const hitEnemy = raycastEnemies(ang);
    // check wall first along this ray for cover
    const wallDist = castRay(state.player.x, state.player.y, ang);
    if(hitEnemy && hitEnemy.dist < wallDist && hitEnemy.enemy.alive){
      damageEnemy(hitEnemy.enemy, w.dmg);
    }
  }
  // muzzle flash light
  state.muzzleFlash = 0.25;
  if(w.ammo<=0 && w.ammo!==Infinity) updateHUD();
}

function damageEnemy(e, dmg){
  if(!e.alive) return;
  e.hp -= dmg; e.hurt=0.15; e.alert=1;
  sfx.enemyHit();
  if(e.hp<=0){ e.alive=false; e.deadT=performance.now(); state.kills++; state.score+=100;
    sfx.enemyDie(); updateHUD();
    // chance to drop health
    if(Math.random()<0.35){
      state.pickups.push({ x:e.x, y:e.y, type:'health', amount:15, taken:false });
    }
  }
}

// ---- Raycast (DDA) for walls, returns dist ----
function castRay(px,py,ang){
  const dx=Math.cos(ang), dy=Math.sin(ang);
  let mapX=Math.floor(px), mapY=Math.floor(py);
  const deltaX=Math.abs(1/dx), deltaY=Math.abs(1/dy);
  let stepX,stepY,leftDist,topDist;
  if(dx<0){ stepX=-1; leftDist=(px-mapX)*deltaX; } else { stepX=1; leftDist=(mapX+1-px)*deltaX; }
  if(dy<0){ stepY=-1; topDist=(py-mapY)*deltaY; } else { stepY=1; topDist=(mapY+1-py)*deltaY; }
  let side=0, hit=false, tile=1;
  for(let i=0;i<64 && !hit;i++){
    if(leftDist<topDist){ leftDist+=deltaX; mapX+=stepX; side=0; }
    else { topDist+=deltaY; mapY+=stepY; side=1; }
    if(isWall(mapX,mapY)){ hit=true; tile=cellAt(mapX,mapY); }
  }
  let dist = side===0 ? (leftDist-deltaX) : (topDist-deltaY);
  return dist||0.0001;
}
function wallTileAt(px,py,ang){
  const dx=Math.cos(ang), dy=Math.sin(ang);
  let mapX=Math.floor(px), mapY=Math.floor(py);
  const deltaX=Math.abs(1/dx), deltaY=Math.abs(1/dy);
  let stepX,stepY,leftDist,topDist,tile=1;
  if(dx<0){ stepX=-1; leftDist=(px-mapX)*deltaX; } else { stepX=1; leftDist=(mapX+1-px)*deltaX; }
  if(dy<0){ stepY=-1; topDist=(py-mapY)*deltaY; } else { stepY=1; topDist=(mapY+1-py)*deltaY; }
  let side=0,hit=false;
  for(let i=0;i<64 && !hit;i++){
    if(leftDist<topDist){ leftDist+=deltaX; mapX+=stepX; side=0; }
    else { topDist+=deltaY; mapY+=stepY; side=1; }
    if(isWall(mapX,mapY)){ hit=true; tile=cellAt(mapX,mapY); }
  }
  return { side, tile };
}

// Raycast for nearest enemy along angle (approx with stepping)
function raycastEnemies(ang){
  const dx=Math.cos(ang), dy=Math.sin(ang);
  let nearest=null, bestDist=64;
  for(const e of state.enemies){
    if(!e.alive) continue;
    const ex=e.x-state.player.x, ey=e.y-state.player.y;
    const dist=Math.hypot(ex,ey);
    let ea=Math.atan2(ey,ex);
    let diff=ea-state.player.dir;
    while(diff>Math.PI) diff-=2*Math.PI;
    while(diff<-Math.PI) diff+=2*Math.PI;
    const angDiff=Math.abs(diff);
    // enemy angular radius scales with inverse distance
    const radius=Math.atan2(0.35,dist);
    if(angDiff<radius && dist<bestDist){
      // ensure not blocked by wall
      const wallDist=castRay(state.player.x, state.player.y, ea);
      if(wallDist>dist-0.1){ nearest=e; bestDist=dist; }
    }
  }
  return nearest? { enemy:nearest, dist:bestDist } : null;
}

// ---- Pickups ----
function tryPickup(){
  for(const p of state.pickups){
    if(p.taken) continue;
    const d=Math.hypot(p.x-state.player.x, p.y-state.player.y);
    if(d<0.7){
      p.taken=true;
      if(p.type==='health'){
        state.player.health=Math.min(100, state.player.health+p.amount);
        showMsg('+'+p.amount+' HEALTH');
      } else {
        const w=state.weapons[1];
        w.ammo=Math.min(w.max, w.ammo+p.amount);
        showMsg('+'+p.amount+' AMMO');
      }
      sfx.pickup(); updateHUD();
    }
  }
}

// ---- Enemy AI ----
function updateEnemies(dt){
  for(const e of state.enemies){
    if(!e.alive){ if(e.deadT===0) e.deadT=performance.now(); continue; }
    if(e.hurt>0) e.hurt-=dt;
    const ex=state.player.x-e.x, ey=state.player.y-e.y;
    const dist=Math.hypot(ex,ey);
    const seeLine = hasLineOfSight(e.x,e.y,state.player.x,state.player.y);
    if(seeLine) e.alert=1;
    if(e.alert){
      // move toward player if far
      if(dist>0.9){
        const ang=Math.atan2(ey,ex);
        const nx=e.x+Math.cos(ang)*e.speed*dt*60;
        const ny=e.y+Math.sin(ang)*e.speed*dt*60;
        if(!isWall(Math.floor(nx),Math.floor(e.y))){ e.x=nx; }
        if(!isWall(Math.floor(e.x),Math.floor(ny))){ e.y=ny; }
      }
      // attack
      if(e.attackCd>0) e.attackCd-=dt;
      if(dist<1.2 && e.attackCd<=0){
        e.attackCd=1.1;
        playerDamage(12 + Math.random()*8);
      } else if(dist<6 && e.attackCd<=0 && seeLine){
        // ranged attack (fires from distance)
        e.attackCd=1.6;
        rangedAttack(e);
      }
    }
    e.animT += dt*4;
  }
}
function rangedAttack(e){
  // projectile: check line of sight, then damage player
  if(hasLineOfSight(e.x,e.y,state.player.x,state.player.y)){
    playerDamage(6 + Math.random()*6);
    sfx.fire();
  }
}
function hasLineOfSight(x0,y0,x1,y1){
  const steps=Math.hypot(x1-x0,y1-y0)*10;
  for(let i=0;i<steps;i++){
    const t=i/steps;
    const x=x0+(x1-x0)*t, y=y0+(y1-y0)*t;
    if(isWall(Math.floor(x),Math.floor(y))) return false;
  }
  return true;
}

function playerDamage(dmg){
  if(state.player.dead) return;
  state.player.health -= dmg;
  state.hurtAnim=0.25;
  sfx.hurt();
  flashDamage();
  if(state.player.health<=0){ state.player.health=0; die(); }
  updateHUD();
}

// ---- Render ----
function render(){
  // Ceiling + floor gradient
  const cg = ctx.createLinearGradient(0,0,0,H/2);
  cg.addColorStop(0,'#2a2030'); cg.addColorStop(1,'#4a3a4a');
  ctx.fillStyle=cg; ctx.fillRect(0,0,W,H/2);
  const fg = ctx.createLinearGradient(0,H/2,0,H);
  fg.addColorStop(0,'#5a4a3a'); fg.addColorStop(1,'#3a2a20');
  ctx.fillStyle=fg; ctx.fillRect(0,H/2,W,H/2);

  const px=state.player.x, py=state.player.y, pd=state.player.dir;
  const fov=Math.PI/3;
  const zbuffer=new Float32Array(W);

  for(let x=0;x<W;x++){
    const camX = 2*x/W - 1;
    const rayAng = pd + Math.atan(camX*Math.tan(fov/2));
    const dx=Math.cos(rayAng), dy=Math.sin(rayAng);
    // DDA
    let mapX=Math.floor(px), mapY=Math.floor(py);
    const deltaX=Math.abs(1/dx), deltaY=Math.abs(1/dy);
    let stepX,stepY,leftDist,topDist,side=0,tile=1,hit=false;
    if(dx<0){ stepX=-1; leftDist=(px-mapX)*deltaX; } else { stepX=1; leftDist=(mapX+1-px)*deltaX; }
    if(dy<0){ stepY=-1; topDist=(py-mapY)*deltaY; } else { stepY=1; topDist=(mapY+1-py)*deltaY; }
    for(let i=0;i<64 && !hit;i++){
      if(leftDist<topDist){ leftDist+=deltaX; mapX+=stepX; side=0; }
      else { topDist+=deltaY; mapY+=stepY; side=1; }
      if(isWall(mapX,mapY)){ hit=true; tile=cellAt(mapX,mapY); }
    }
    let perp = side===0 ? (leftDist-deltaX) : (topDist-deltaY);
    if(perp<0.0001) perp=0.0001;
    zbuffer[x]=perp;
    const lineH = Math.min(H*4, H/perp);
    const drawStart = (H-lineH)/2;
    // shading by distance & side, and tile color
    let shade = 1 - Math.min(0.85, perp/16);
    if(side===1) shade*=0.7;
    let base;
    switch(tile){
      case 1: base=[120,90,70]; break;
      case 2: base=[110,70,70]; break;
      case 3: base=[80,90,110]; break;
      case 4: base=[90,110,70]; break;
      case 9: base=[180,140,60]; break; // fountain
      default: base=[120,90,70];
    }
    const r=Math.floor(base[0]*shade), g=Math.floor(base[1]*shade), b=Math.floor(base[2]*shade);
    ctx.fillStyle=`rgb(${r},${g},${b})`;
    ctx.fillRect(x,drawStart,1,lineH);
  }

  // ---- Sprites (enemies + pickups + fountain detail) ----
  const sprites=[];
  for(const e of state.enemies){
    sprites.push({ x:e.x, y:e.y, draw:drawEnemy, ref:e });
  }
  for(const p of state.pickups){
    if(p.taken) continue;
    sprites.push({ x:p.x, y:p.y, draw:drawPickup, ref:p });
  }
  // sort far to near
  sprites.sort((a,b)=> Math.hypot(b.x-px,b.y-py) - Math.hypot(a.x-px,a.y-py));
  for(const s of sprites){
    drawSprite(s, px, py, pd, fov, zbuffer);
  }

  drawWeapon();
  drawFountainHud();
}

function drawSprite(s, px, py, dir, fov, zbuffer){
  const dx=s.x-px, dy=s.y-py;
  let ang=Math.atan2(dy,dx)-dir;
  while(ang>Math.PI) ang-=2*Math.PI;
  while(ang<-Math.PI) ang+=2*Math.PI;
  const dist=Math.hypot(dx,dy);
  if(Math.abs(ang)>fov/2+0.2 || dist<0.2) return;
  const tanFov=Math.tan(fov/2);
  const camX = Math.tan(ang)/tanFov;
  const spriteX = (0.5 + camX*0.5) * W;
  const screenX = spriteX;
  const scale = H/dist;
  const col=Math.floor(screenX);
  if(col>=0 && col<W && zbuffer[col]>dist) return;

  s.draw(s, scale, screenX, dist);
}

function drawEnemy(e, scale, screenX, dist){
  const shade = 1 - Math.min(0.7, dist/14);
  const h = scale*0.7;
  const w = scale*0.4;
  const x = screenX - w/2;
  const y = (H - h)/2 + (e.alive?0: h*0.55);
  const flash = e.hurt>0;
  // body
  let bodyC = flash ? '#fff' : '#8a2a20';
  let darkC   = flash ? '#ccc' : '#5a1612';
  ctx.save();
  if(!e.alive){ ctx.globalAlpha=0.85; }
  // shadow
  ctx.fillStyle='rgba(0,0,0,0.4)';
  ctx.fillRect(x+0.05*w, y+h*0.9, w*0.9, h*0.1);
  // legs
  ctx.fillStyle=darkC;
  const legSpread = e.alive ? Math.sin(e.animT)*w*0.12 : 0;
  ctx.fillRect(x+w*0.2-legSpread, y+h*0.6, w*0.22, h*0.4);
  ctx.fillRect(x+w*0.6+legSpread, y+h*0.6, w*0.22, h*0.4);
  // torso
  ctx.fillStyle=bodyC;
  ctx.fillRect(x+w*0.12, y+h*0.28, w*0.76, h*0.34);
  // arms
  ctx.fillRect(x, y+h*0.3, w*0.14, h*0.28);
  ctx.fillRect(x+w*0.86, y+h*0.3, w*0.14, h*0.28);
  // head
  ctx.fillStyle = flash ? '#fff' : '#c96a4a';
  ctx.beginPath();
  ctx.arc(screenX, y+h*0.16, w*0.22, 0, Math.PI*2);
  ctx.fill();
  // eyes (glowing)
  if(e.alive){
    ctx.fillStyle = flash ? '#f00' : '#ffdd30';
    ctx.fillRect(screenX-w*0.1, y+h*0.13, w*0.07, h*0.05);
    ctx.fillRect(screenX+w*0.03, y+h*0.13, w*0.07, h*0.05);
  }
  // health bar
  if(e.alive && e.hp<60){
    ctx.fillStyle='#000'; ctx.fillRect(x, y+h*0.05, w, h*0.03);
    ctx.fillStyle='#3f0'; ctx.fillRect(x, y+h*0.05, w*(e.hp/60), h*0.03);
  }
  ctx.restore();
}

function drawPickup(p, scale, screenX, dist){
  const shade = 1 - Math.min(0.6, dist/10);
  const size = scale*0.35;
  const y = (H)/2 + scale*0.15;
  const bob = Math.sin(performance.now()/300 + p.x)*size*0.08;
  ctx.save();
  ctx.globalAlpha = Math.min(1, shade+0.2);
  const cx = screenX, cy = y - size/2 + bob;
  if(p.type==='health'){
    ctx.fillStyle='#d02020';
    ctx.fillRect(cx-size*0.5, cy-size*0.2, size, size*0.4);
    ctx.fillStyle='#fff';
    ctx.fillRect(cx-size*0.08, cy-size*0.25, size*0.16, size*0.5);
    ctx.fillRect(cx-size*0.25, cy-size*0.08, size*0.5, size*0.16);
  } else {
    ctx.fillStyle='#d0a020';
    ctx.fillRect(cx-size*0.4, cy-size*0.3, size*0.8, size*0.6);
    ctx.fillStyle='#8a6a10';
    ctx.fillRect(cx-size*0.4, cy-size*0.3, size*0.8, size*0.1);
    ctx.fillStyle='#5a4008';
    ctx.fillRect(cx-size*0.15, cy-size*0.1, size*0.3, size*0.4);
  }
  // glow
  ctx.globalAlpha=0.25; ctx.fillStyle=p.type==='health'?'#f44':'#ff4';
  ctx.beginPath(); ctx.arc(cx,cy,size*0.6,0,Math.PI*2); ctx.fill();
  ctx.restore();
}

// ---- Fountain interaction hint ----
function drawFountainHud(){
  // check if standing near fountain & have shotgun
  const d=Math.hypot(state.player.x-FOUNT_X, state.player.y-FOUNT_Y);
  if(d<1.4){
    let haveShotgun = state.weapons[1].ammo>0 || state.weapons[1].ammo===Infinity;
    if(!haveShotgun) haveShotgun = true; // shotgun always available as weapon choice
    // message
    const el=document.getElementById('msg');
    // handled in update via msg timer
  }
}

// ---- Weapon viewmodel ----
function drawWeapon(){
  wctx.clearRect(0,0,420,360);
  const w=state.weapons[state.current];
  const cx=210;
  const baseY=340;
  // bob
  const bobY = Math.sin(state.bobT)*8 * (state.moving?1:0.2);
  const bobX = Math.cos(state.bobT*0.5)*5 * (state.moving?1:0.2);
  const recoil = state.fireAnim;
  const y = baseY + bobY + recoil*6;
  wctx.save();
  wctx.translate(cx+bobX, y);
  // muzzle flash
  if(recoil>4){
    wctx.save();
    const f = recoil>10?40:22;
    const g=wctx.createRadialGradient(0,-10,2,0,-10,f);
    g.addColorStop(0,'#fff'); g.addColorStop(0.4,'#ffd060'); g.addColorStop(1,'rgba(255,120,0,0)');
    wctx.fillStyle=g;
    wctx.beginPath();
    for(let i=0;i<8;i++){
      const a=(i/8)*Math.PI*2;
      wctx.moveTo(0,-10);
      wctx.lineTo(Math.cos(a)*f, -10+Math.sin(a)*f);
    }
    wctx.fill();
    wctx.restore();
  }
  if(w.key==='pistol'){
    // grip/barrel
    wctx.fillStyle='#333'; wctx.fillRect(-30,-140,60,150);
    wctx.fillStyle='#222'; wctx.fillRect(-22,-170,44,40);
    wctx.fillStyle='#1a1a1a'; wctx.fillRect(-16,-180,32,20);
    wctx.fillStyle='#444'; wctx.fillRect(-24,-150,48,8);
  } else if(w.key==='shotgun'){
    // long barrel
    wctx.fillStyle='#5a3a20'; wctx.fillRect(-26,-230,52,240);
    wctx.fillStyle='#333'; wctx.fillRect(-20,-260,40,60);
    wctx.fillStyle='#2a2a2a'; wctx.fillRect(-14,-280,28,30);
    wctx.fillStyle='#6b4a28'; wctx.fillRect(-26,-120,52,40);
    wctx.fillStyle='#3a2a18'; wctx.fillRect(-22,-90,44,20);
  } else {
    // rifle - slimmer, longer
    wctx.fillStyle='#2a2a2a'; wctx.fillRect(-22,-250,44,260);
    wctx.fillStyle='#111'; wctx.fillRect(-16,-270,32,30);
    wctx.fillStyle='#3a3a3a'; wctx.fillRect(-18,-150,36,12);
    wctx.fillStyle='#1a1a1a'; wctx.fillRect(-10,-200,20,60);
    // red dot
    wctx.fillStyle='#f00'; wctx.fillRect(-3,-266,6,6);
  }
  wctx.restore();
}

// ---- Minimap ----
function drawMinimap(){
  mctx.clearRect(0,0,140,140);
  const s=140/MAP_W;
  for(let y=0;y<MAP_H;y++){
    for(let x=0;x<MAP_W;x++){
      const t=MAP[y][x];
      if(t===0) continue;
      mctx.fillStyle = t===9 ? '#ffd040' : (t===2?'#a66644':(t===3?'#66668a':'#886644'));
      mctx.fillRect(x*s,y*s,Math.ceil(s),Math.ceil(s));
    }
  }
  // pickups
  for(const p of state.pickups){
    if(p.taken) continue;
    mctx.fillStyle = p.type==='health'?'#0f0':'#ff0';
    mctx.fillRect(p.x*s-2,p.y*s-2,4,4);
  }
  // enemies
  for(const e of state.enemies){
    if(!e.alive) continue;
    mctx.fillStyle = e.alert?'#f40':'#f00';
    mctx.fillRect(e.x*s-2,e.y*s-2,4,4);
  }
  // player
  mctx.fillStyle='#4f4';
  mctx.beginPath(); mctx.arc(state.player.x*s,state.player.y*s,3,0,Math.PI*2); mctx.fill();
  mctx.strokeStyle='#4f4'; mctx.beginPath();
  mctx.moveTo(state.player.x*s,state.player.y*s);
  mctx.lineTo(state.player.x*s+Math.cos(state.player.dir)*10, state.player.y*s+Math.sin(state.player.dir)*10);
  mctx.stroke();
}

// ---- HUD ----
function updateHUD(){
  const w=state.weapons[state.current];
  document.getElementById('ammoVal').textContent = w.ammo===Infinity?'∞':w.ammo;
  document.getElementById('ammoVal').className='val'+(w.ammo===0?' low':'');
  document.getElementById('weaponVal').textContent=w.name;
  document.getElementById('healthVal').textContent=Math.max(0,Math.round(state.player.health));
  document.getElementById('healthVal').className='val'+(state.player.health<=30?' low':'');
  document.getElementById('score').textContent=state.score;
  const alive=state.enemies.filter(e=>e.alive).length;
  document.getElementById('enemyVal').textContent=alive;
}

let msgTimer=null;
function showMsg(t){
  const el=document.getElementById('msg');
  el.textContent=t; el.style.opacity='1';
  clearTimeout(msgTimer);
  msgTimer=setTimeout(()=>{ el.style.opacity='0'; }, 1400);
}

function flashDamage(){
  const el=document.getElementById('damage');
  el.style.opacity='1';
  setTimeout(()=>{ el.style.opacity='0'; }, 120);
}

// ---- Win / Death ----
function die(){
  if(state.over||state.player.dead) return;
  state.player.dead=true; state.over=true;
  sfx.death();
  document.exitPointerLock();
  document.getElementById('deathScore').textContent=state.score;
  document.getElementById('deathKills').textContent=state.kills;
  document.getElementById('deathScreen').classList.remove('hidden');
  updateHUD();
}

function winGame(){
  if(state.over||state.won) return;
  state.won=true; state.over=true;
  state.score += Math.round(state.player.health)*10 + 500;
  sfx.win();
  document.exitPointerLock();
  document.getElementById('winScore').textContent=state.score;
  document.getElementById('winKills').textContent=state.kills;
  document.getElementById('winHealth').textContent=Math.round(state.player.health);
  document.getElementById('winScreen').classList.remove('hidden');
}

function destroyFountain(){
  const d=Math.hypot(state.player.x-FOUNT_X, state.player.y-FOUNT_Y);
  if(d<1.4 && state.weapons[1].ammo>0){
    state.weapons[1].ammo--;
    // big explosion
    if(actx){ noiseBurst(0.6,0.4); beep(80,0.6,'sawtooth',0.3,20); }
    state.score+=500;
    updateHUD();
    showMsg('FOUNTAIN HIT!');
    if(state.weapons[1].ammo<=0) updateHUD();
    if(state.weapons[1].ammo>0){
      // keep going; but let's require shotgun ammo per blast — but simpler: instant win when near with ammo
    }
    // Win when fountain destroyed (shotgun used near it)
    winGame();
  } else if(d<1.4){
    showMsg('NEED AMMO TO DESTROY IT');
  }
}

// ---- Movement ----
function updatePlayer(dt){
  const p=state.player;
  if(p.dead) return;
  const speed=0.055*dt*60;
  let moved=false;
  let mx=0, my=0;
  const dx=Math.cos(p.dir), dy=Math.sin(p.dir);
  const lx=-dy, ly=dx;
  if(keys['KeyW']||keys['ArrowUp']){ mx+=dx; my+=dy; moved=true; }
  if(keys['KeyS']||keys['ArrowDown']){ mx-=dx; my-=dy; moved=true; }
  if(keys['KeyA']||keys['ArrowLeft']){ mx+=lx; my+=ly; moved=true; }
  if(keys['KeyD']||keys['ArrowRight']){ mx-=lx; my-=ly; moved=true; }
  // strafe also with Q/E? no, E is pickup. Use A/D strafe.
  if(mx||my){
    const len=Math.hypot(mx,my); mx/=len; my/=len;
    const nx=p.x+mx*speed, ny=p.y+my*speed;
    const pad=0.18;
    if(!isWall(Math.floor(nx+Math.sign(mx)*pad),Math.floor(p.y))){ p.x=nx; }
    if(!isWall(Math.floor(p.x),Math.floor(ny+Math.sign(my)*pad))){ p.y=ny; }
  }
  state.moving=moved;
  if(moved) state.bobT += dt*12;

  // auto fire for auto weapon
  if(keys['MouseLeft'] || (keys['Space'] && state.weapons[state.current].auto)){
    // MouseLeft tracked via key? we use click; handle continuous via mouse button
  }
  // continuous fire handled separately
  // fountain
  const dF=Math.hypot(p.x-FOUNT_X,p.y-FOUNT_Y);
  if(dF<1.4 && state.weapons[1].ammo>0){
    const el=document.getElementById('msg');
    // subtle hint via already-set messages
  }
}

// continuous holding fire
let mouseDown=false;
document.addEventListener('mousedown', e=>{ if(e.button===0){ mouseDown=true; } });
window.addEventListener('mouseup', e=>{ if(e.button===0){ mouseDown=false; } });

// ---- Game loop ----
let last=0;
function loop(t){
  const dt=Math.min(0.05,(t-last)/1000)||0;
  last=t;
  if(state.running && !state.over){
    updatePlayer(dt);
    updateEnemies(dt);
    // continuous fire
    if(mouseDown && state.weapons[state.current].auto) fire();
    // hurt anim decay
    if(state.hurtAnim>0) state.hurtAnim-=dt;
    if(state.fireAnim>0) state.fireAnim-=dt*1.5;
    if(state.muzzleFlash>0) state.muzzleFlash-=dt;
    // fountain destroy when pressing E near it and carrying shotgun ammo
    if(keys['KeyE']) destroyFountain();
    updateHUD();
    render();
    drawMinimap();
    // damage vignette intensity
    if(state.hurtAnim>0){
      document.getElementById('damage').style.opacity = String(Math.min(0.8, state.hurtAnim*3));
    }
  }
  requestAnimationFrame(loop);
}

// ---- Game control ----
function startGame(){
  initAudio();
  resize();
  state.running=true; state.over=false; state.won=false;
  state.player.x=2.5; state.player.y=2.5; state.player.dir=0; state.player.health=100; state.player.dead=false;
  state.weapons[0].ammo=Infinity; state.weapons[1].ammo=32; state.weapons[2].ammo=120;
  state.current=0; state.score=0; state.kills=0;
  spawnEnemies(); spawnPickups();
  document.getElementById('menu').classList.add('hidden');
  document.getElementById('deathScreen').classList.add('hidden');
  document.getElementById('winScreen').classList.add('hidden');
  updateHUD();
  canvas.requestPointerLock();
  showMsg('DESTROY THE FOUNTAIN');
}

document.getElementById('startBtn').onclick=startGame;
document.getElementById('respawnBtn').onclick=startGame;
document.getElementById('winBtn').onclick=startGame;

resize();
requestAnimationFrame(loop);
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"7d6f5a19-1fb9-4a33-810e-3ab15f8ef349","tokensIn":1646255,"tokensOut":31422,"tokensTotal":1677677,"cost":0,"turns":37,"toolCalls":36,"failedToolCalls":2,"timestamp":"2026-09-04T18:01:38.847Z"} -->