DOOM
Tiel Coder 35B A3B GGUF MTP · full

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 <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 <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"} -->