Mini Minecraft — Fly & Trap
by ShadowLegend171847 lines131.4 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mini Minecraft — Fly & Trap</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&family=VT323&display=swap" rel="stylesheet">
<style>
:root{--ink:#f4f4f4;--panel:#3a3a44;--line:#15151a;--sel:#ffffff;}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{margin:0;height:100%;}
body{background:#243244;color:var(--ink);font-family:"VT323",monospace;
display:flex;flex-direction:column;align-items:center;min-height:100%;
padding:8px;gap:8px;overflow:hidden;touch-action:manipulation;image-rendering:pixelated;}
#wrap{width:100%;max-width:980px;display:flex;flex-direction:column;gap:8px;}
header{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
.title{font-family:"Press Start 2P",monospace;font-size:clamp(10px,2.7vw,16px);
line-height:1.4;color:#eaffea;text-shadow:2px 2px 0 #2a4d1e;}
.title b{color:#8fe05a;}
.spacer{flex:1;}
.stat{display:flex;align-items:center;gap:6px;background:var(--panel);
border:3px solid var(--line);border-bottom-color:#000;border-right-color:#000;
padding:4px 9px;line-height:1;white-space:nowrap;}
.stat .lbl{font-family:"Press Start 2P",monospace;font-size:9px;color:#c8c8c8;}
.stat .val{font-family:"Press Start 2P",monospace;font-size:12px;color:#fff;}
#hearts{display:block;image-rendering:pixelated;}
.stage{position:relative;width:100%;overflow:hidden;border:4px solid var(--line);
box-shadow:0 10px 0 #000,0 12px 30px rgba(0,0,0,.5);}
canvas#cv{display:block;width:100%;height:auto;image-rendering:pixelated;touch-action:none;}
.overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;
justify-content:center;gap:15px;text-align:center;background:rgba(10,14,20,.86);padding:22px;}
.overlay.hidden{display:none;}
.overlay h2{margin:0;font-family:"Press Start 2P",monospace;font-size:clamp(14px,4.4vw,26px);
line-height:1.5;color:#8fe05a;text-shadow:2px 2px 0 #24401a;}
.overlay p{margin:0;max-width:40ch;line-height:1.35;font-size:clamp(16px,4.2vw,21px);color:#dcdcdc;}
.overlay p b{color:#ffe066;}
.mc-btn{font-family:"Press Start 2P",monospace;font-size:clamp(11px,3vw,15px);cursor:pointer;
color:#122a0c;padding:14px 22px;background:#7fbf4f;border:3px solid #000;
border-top-color:#c6f29a;border-left-color:#c6f29a;box-shadow:0 5px 0 #2f5720;}
.mc-btn:active{transform:translateY(4px);box-shadow:0 1px 0 #2f5720;}
.banner{position:absolute;top:12px;left:50%;transform:translateX(-50%);
background:rgba(10,14,20,.9);border:3px solid #000;color:#8fe05a;padding:6px 14px;
font-family:"Press Start 2P",monospace;font-size:clamp(9px,2.6vw,12px);
pointer-events:none;opacity:0;transition:opacity .25s;}
.banner.show{opacity:1;}
.modal{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
background:rgba(6,10,16,.82);z-index:5;padding:16px;}
.modal.hidden{display:none;}
.kp{background:#1a2230;border:4px solid #000;border-top-color:#3a4a60;border-left-color:#3a4a60;
padding:16px;width:min(300px,90%);text-align:center;box-shadow:0 10px 0 #000;}
.kp-title{font-family:"Press Start 2P",monospace;font-size:11px;color:#37e1ff;margin-bottom:10px;line-height:1.4;}
.kp-display{font-family:"Press Start 2P",monospace;font-size:18px;color:#eaffff;letter-spacing:2px;
background:#0c1420;border:3px solid #000;padding:12px 6px;margin-bottom:6px;min-height:44px;}
.kp-msg{font-size:16px;color:#ff8a7a;min-height:20px;margin-bottom:8px;}
.kp-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;}
.kp-grid button{font-family:"Press Start 2P",monospace;font-size:14px;padding:12px 0;cursor:pointer;
color:#fff;background:#39424c;border:3px solid #000;border-top-color:#5b6570;border-left-color:#5b6570;}
.kp-grid button:active{transform:translateY(2px);}
.kp-grid button.ok{color:#0c2a12;background:#7fbf4f;border-top-color:#c6f29a;border-left-color:#c6f29a;}
.kp-grid button.clr{color:#2a1810;background:#e0a24d;border-top-color:#ffd89a;border-left-color:#ffd89a;}
.kp-row{display:flex;gap:6px;margin-top:8px;}
.kp-alt{flex:1;font-family:"Press Start 2P",monospace;font-size:9px;padding:11px 0;cursor:pointer;
color:#fff;background:#3a3a44;border:3px solid #000;border-top-color:#5b5b66;border-left-color:#5b5b66;}
.kp-alt:active{transform:translateY(2px);}
.dock{display:flex;gap:6px;width:100%;align-items:stretch;}
.hotbar{display:grid;grid-template-columns:repeat(14,1fr);gap:2px;flex:1;}
.slot{position:relative;background:#4a4a52;cursor:pointer;color:var(--ink);
border:3px solid #000;border-top-color:#6b6b76;border-left-color:#6b6b76;
padding:4px 2px 2px;display:flex;flex-direction:column;align-items:center;gap:1px;font-family:inherit;}
.slot:active{transform:translateY(3px);}
.slot.sel{border-color:var(--sel);border-top-color:#fff;border-left-color:#fff;box-shadow:0 0 0 2px #fff inset;}
.slot .num{position:absolute;top:1px;left:3px;font-family:"Press Start 2P",monospace;font-size:7px;color:#cfcfcf;}
.slot .ic{width:100%;max-width:32px;height:auto;aspect-ratio:1/1;image-rendering:pixelated;}
.slot .nm{font-size:13px;line-height:1;}
.start{flex:0 0 auto;cursor:pointer;font-family:"Press Start 2P",monospace;font-size:9px;
padding:4px 8px;line-height:1.4;color:#0c1830;background:#6aa8e2;border:3px solid #000;
border-top-color:#bfe0ff;border-left-color:#bfe0ff;box-shadow:0 4px 0 #2f5a8a;
display:flex;flex-direction:column;align-items:center;justify-content:center;gap:2px;}
.start:active{transform:translateY(3px);box-shadow:0 1px 0 #2f5a8a;}
.start:disabled{opacity:.45;cursor:default;box-shadow:0 4px 0 #2f5a8a;}
.start:disabled{filter:grayscale(.7);opacity:.55;cursor:default;box-shadow:none;}
.iconbtn{background:var(--panel);border:3px solid #000;border-top-color:#5b5b66;
border-left-color:#5b5b66;color:#fff;width:44px;cursor:pointer;font-size:20px;}
.iconbtn:active{transform:translateY(3px);}
.hint{text-align:center;font-size:18px;color:#bfe6a8;min-height:20px;}
:focus-visible{outline:3px solid #8fd0ff;outline-offset:2px;}
@media(max-width:540px){.slot .nm{display:none;}.slot{padding:3px 1px;}.slot .ic{max-width:26px;}.start{flex-basis:74px;}}
</style>
</head>
<body>
<div id="wrap">
<header>
<div class="title">MINI MINECRAFT<br><b>ZOMBIE SURVIVAL</b></div>
<div class="spacer"></div>
<div class="stat"><span class="lbl">NIGHT</span><span class="val" id="nightNum">1</span></div>
<div class="stat"><span class="lbl">YOU</span><canvas id="phearts" width="80" height="14"></canvas></div>
<div class="stat"><span class="lbl">BODY</span><canvas id="body" width="46" height="16"></canvas></div>
<div class="stat" id="armorStat" style="display:none"><span class="lbl" style="color:#37e1ff">ARMOR</span><span style="color:#6eff7a;font-size:9px">LOCKED ON</span></div>
<div class="stat"><span class="lbl">BASE</span><canvas id="hearts" width="150" height="14"></canvas></div>
<div class="stat"><span class="lbl">KILLS</span><span class="val" id="killNum">0</span></div>
<button class="start" id="startBtn">START<br>NIGHT</button>
<button class="iconbtn" id="muteBtn" aria-label="Sound">🔊</button>
</header>
<div class="stage">
<canvas id="cv" width="900" height="560"></canvas>
<div class="banner" id="banner"></div>
<div class="overlay" id="overlay">
<h2 id="ovTitle">MINI MINECRAFT<br>ZOMBIE SURVIVAL</h2>
<p id="ovText">Survive the zombie horde! Use the <b>arrow pad</b> to move and jump, place your <b>base</b> and traps from the hotbar, then hit <b>START NIGHT</b>. Zombies attack your base <b>and you</b> — jump to escape and build walls to stay alive!</p>
<button class="mc-btn" id="ovBtn">Start Game</button>
</div>
<div class="modal hidden" id="keypad">
<div class="kp">
<div class="kp-title" id="kpTitle">SET DOOR CODE</div>
<div class="kp-display" id="kpDisplay">– – – –</div>
<div class="kp-msg" id="kpMsg"></div>
<div class="kp-grid" id="kpGrid"></div>
<div class="kp-row">
<button class="kp-alt" id="kpSkip">NO LOCK</button>
<button class="kp-alt" id="kpClose">CANCEL</button>
</div>
</div>
</div>
</div>
<div class="dock">
<div class="hotbar" id="hotbar"></div>
</div>
<div class="hint" id="hint">Arrows = move, up = jump. Tap world/PLACE = drop item. Tap a door to open/close.</div>
</div>
<script>
(()=>{
"use strict";
const cv=document.getElementById('cv'), ctx=cv.getContext('2d');
const W=900,H=560; ctx.imageSmoothingEnabled=false;
const B=45; // block size
const COLS=44, WORLD_W=COLS*B; // 1980
const GROUND=430; // ground surface y
const CAM_MAX=WORLD_W-W; // 1080
let HOUSE_C1=21, HOUSE_C2=23, HOUSE_L=HOUSE_C1*B, HOUSE_R=(HOUSE_C2+1)*B, HOUSE_CX=(HOUSE_L+HOUSE_R)/2, parkX=HOUSE_R+42;
function setBase(centerCol){ const c=Math.max(2,Math.min(COLS-3,centerCol));
HOUSE_C1=c-1; HOUSE_C2=c+1; HOUSE_L=HOUSE_C1*B; HOUSE_R=(HOUSE_C2+1)*B; HOUSE_CX=(HOUSE_L+HOUSE_R)/2;
parkX = Math.max(34, Math.min(WORLD_W-34, (HOUSE_R+90<WORLD_W)?HOUSE_R+42:HOUSE_L-42)); }
// ---- items ----
const ITEMS={
spikes:{name:'Spikes', type:'ground', dps:52, hp:110, ic:'spikes', desc:'Plasma spikes — zap zombies that walk over them. Wear down over time.'},
base: {name:'Base', type:'base', ic:'base', desc:'Your home base — fly to a spot and tap to set it down. Defend it!'},
elevator:{name:'Lift', type:'elevator', ic:'elevator', desc:'Futuristic elevator that rides up and down — hop on for a lift to the top!'},
armory:{name:'Armory', type:'armory', ic:'armory', desc:'Metal weapon crate — place it, tap it to grab a weapon (blaster, laser, rocket, knives), then FIRE at night!'},
carpet:{name:'Carpet', type:'furn', ic:'carpet', desc:'A cozy rug for the floor.'},
chair: {name:'Chair', type:'furn', ic:'chair', desc:'A comfy chair.'},
table: {name:'Table', type:'furn', ic:'table', desc:'A sturdy table.'},
bookshelf:{name:'Bookshelf', type:'furn', ic:'bookshelf', desc:'A shelf full of books.'},
passage:{name:'Iron Block', type:'furn', ic:'passage', desc:'A solid iron block — you cannot walk through it.'},
idoor: {name:'Door', type:'furn', ic:'idoor', desc:'A door — tap it to open or close. Closed doors block you.'},
bed: {name:'Bed', type:'furn', ic:'bed', desc:'A comfy bed.'},
sofa: {name:'Sofa', type:'furn', ic:'sofa', desc:'A soft couch.'},
tv: {name:'TV', type:'furn', ic:'tv', desc:'A flatscreen TV.'},
fridge:{name:'Fridge', type:'furn', ic:'fridge', desc:'Keeps your food cold.'},
stove: {name:'Stove', type:'furn', ic:'stove', desc:'Cook up a meal.'},
sink: {name:'Sink', type:'furn', ic:'sink', desc:'A kitchen/bath sink.'},
toilet:{name:'Toilet', type:'furn', ic:'toilet', desc:'The bathroom essential.'},
bath: {name:'Bathtub', type:'furn', ic:'bath', desc:'A relaxing bathtub.'},
lamp: {name:'Lamp', type:'furn', ic:'lamp', desc:'A floor lamp.'},
plant: {name:'Plant', type:'furn', ic:'plant', desc:'A leafy houseplant.'},
desk: {name:'Desk', type:'furn', ic:'desk', desc:'A desk with a computer.'},
painting:{name:'Painting', type:'furn', ic:'painting', desc:'Wall art.'},
rug: {name:'Rug', type:'furn', ic:'rug', desc:'A round patterned rug.'},
remove:{name:'Remove', type:'furn', ic:'remove', desc:'Take furniture back.'},
soldier:{name:'Soldier', type:'ally', ic:'soldier', desc:'Spawn a futuristic soldier that hunts and blasts zombies for you.'},
heli: {name:'Heli', type:'heli', ic:'heli', desc:'Attack helicopter — patrols the sky and machine-guns zombies below.'},
tank: {name:'Tank', type:'tank', ic:'tank', desc:'Place it, walk up and tap to get in. Drive with arrows, FIRE to blast zombies, UP to get out.'},
lava: {name:'Lava', type:'ground', dps:34, burn:1.3, hp:240, ic:'lava', desc:'Plasma pit — burns zombies and keeps them on fire. Very long-lasting.'},
web: {name:'Cobweb', type:'ground', slow:.72, hp:150, ic:'web', desc:'Stasis field — slows zombies way down. They break out of it over time.'},
laser: {name:'Laser Turret', type:'device', laser:true, fire:.6, dmg:44, range:9*B, hp:150, ic:'laser', desc:'Zaps zombies with instant laser beams from far away.'},
cactus:{name:'Cactus', type:'solid', hp:280, thorn:34, ic:'cactus', desc:'Energy spike pylon. Blocks zombies and zaps ones that attack it.'},
cobble:{name:'Cobble', type:'solid', hp:800, ic:'cobble', desc:'Reinforced alloy wall. The toughest, strongest barrier around.'},
door: {name:'Blast Door', type:'door', hp:1600, thorn:42, ic:'door', desc:'Electrified blast door. Set a secret code to open. Shocks zombies that attack it!'},
vault: {name:'Vault Door', type:'door', hp:30000, ic:'vault', desc:'Ultra-reinforced vault door. No shock — but so strong it takes about 10 minutes of pounding to break.'},
tnt: {name:'TNT', type:'tnt', dmg:115, rad:108, ic:'tnt', desc:'Proximity mine — arms and detonates when zombies get close. Big blast!'},
dirt: {name:'Dirt', type:'fill', ic:'dirt', desc:'Fill a pit back in — tap a hole to patch the ground.'},
};
const BAR=['base','spikes','lava','web','laser','cactus','cobble','door','vault','tnt','dirt','elevator','armory','pick'];
const UNITS={
blaster: {name:'Blaster Trooper', hp:80, spd:120, range:230, cd:0.5, dmg:30, shot:'bolt', col:'#8fe9ff'},
laser: {name:'Laser Rifleman', hp:90, spd:110, range:300, cd:0.32, dmg:26, shot:'laser', col:'#7cffb0'},
rocket: {name:'Rocket Trooper', hp:100, spd:80, range:340, cd:1.9, dmg:120,shot:'rocket', col:'#ff9a2a'},
knife: {name:'Knife Thrower', hp:70, spd:150, range:150, cd:0.5, dmg:26, shot:'knife', col:'#d8dde3'},
heavy: {name:'Armored Heavy', hp:260, spd:70, range:120, cd:0.7, dmg:44, shot:'bolt', col:'#ffd24a', armored:true},
};
let sel='spikes';
// ---- zombies ----
const ZT={
zombie:{hp:130,spd:40,house:16,block:26,pdmg:14,r:20},
baby: {hp:60, spd:80,house:10,block:16,pdmg:10,r:14},
brute: {hp:430,spd:26,house:34,block:48,pdmg:26,r:27},
missiler:{hp:120,spd:28,house:14,block:22,pdmg:12,r:20,missile:true},
};
let blocks,player,cam,zombies,shots,parts,floats,blasts,beams,dug,queue,spawnT,tanks,inTank,tankShells,tankCd,helis,heliBullets,inHeli,bombs,bombCd,missiles,allies,allyShots,armors,inArmor,elevators,playerShots,blasterCd,crates;
let phase,night,houseHP,houseMax,kills,muted,shake,tPrev,dn,clouds,flashes,basePlaced,baseT,playerHP,playerMax,inside,interior,intSavedX;
const ROOM_L=36, ROOM_R=W-36, IFLOOR=486, ITOP=96, ICELL=44;
const keys={};
function reset(){
blocks={}; dug={}; zombies=[];shots=[];parts=[];floats=[];blasts=[];beams=[];queue=[];spawnT=0;
tanks=[]; inTank=null; tankShells=[]; tankCd=0; helis=[]; heliBullets=[]; inHeli=null; bombs=[]; bombCd=0; missiles=[]; allies=[]; allyShots=[]; armors=[]; inArmor=null; elevators=[]; playerShots=[]; blasterCd=0; crates=[];
phase='day'; basePlaced=false; baseT=0; setBase(Math.floor(COLS/2)); night=1; houseMax=200; houseHP=houseMax; kills=0; muted=false;
playerMax=100; playerHP=playerMax; shake=0; dn=0; flashes=[];
player={x:WORLD_W/2,y:GROUND,vx:0,vy:0,face:-1,anim:0,step:0,onGround:true,hurt:0,armored:false,
injuries:{larm:0,rarm:0,lleg:0,rleg:0}, biteCd:0, wrap:0, hitCd:0, weapon:'blaster'};
cam=clamp(player.x-W/2,0,CAM_MAX);
clouds=[]; for(let i=0;i<10;i++)clouds.push({x:Math.random()*WORLD_W,y:30+Math.random()*140,s:.7+Math.random()*1.1});
sel='base'; dpadPtrs.clear(); armoryOpen=false; inside=false; interior=[]; intSavedX=0;
syncHUD(); buildBar(); refreshBar();
}
const $=id=>document.getElementById(id);
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
function syncHUD(){ $('nightNum').textContent=night; $('killNum').textContent=kills; drawHearts(); drawPHearts(); drawBody();
$('armorStat').style.display = (player&&player.armored)?'':'none'; }
const bdc=$('body').getContext('2d');
function drawBody(){ bdc.clearRect(0,0,46,16); const inj=(player&&player.injuries)||{};
const col=st=> st===2?'#e8e8e8' : st===1?'#e23b3b' : '#6ee27a';
bdc.fillStyle='#c98f6a'; bdc.fillRect(20,1,6,5); // head
bdc.fillStyle='#2aa7a0'; bdc.fillRect(19,6,8,6); // torso
bdc.fillStyle=col(inj.larm); bdc.fillRect(14,6,4,6); // arms
bdc.fillStyle=col(inj.rarm); bdc.fillRect(28,6,4,6);
bdc.fillStyle=col(inj.lleg); bdc.fillRect(19,12,3,4); // legs
bdc.fillStyle=col(inj.rleg); bdc.fillRect(24,12,3,4); }
// hearts
const hc=$('hearts').getContext('2d'), phc=$('phearts').getContext('2d');
const HP=[".XX.XX.","XXXXXXX","XXXXXXX",".XXXXX.","..XXX..","...X..."];
function pixHeart(g,px,mode){ const s=2;
for(let y=0;y<HP.length;y++)for(let x=0;x<7;x++){ if(HP[y][x]!=='X')continue;
let c = mode==='empty'?'#4a2020':(mode==='half'&&x>=4)?'#4a2020':'#e23b3b';
g.fillStyle=c; g.fillRect(px+x*s,y*s,s,s);
if(c==='#e23b3b'&&y<2){g.fillStyle='#ff8a8a';g.fillRect(px+x*s,y*s,s,s);} } }
function drawHearts(){ hc.clearRect(0,0,150,14);
for(let i=0;i<10;i++){ const rem=houseHP-i*20; pixHeart(hc,i*15, rem>=20?'full':rem>=10?'half':'empty'); } }
function drawPHearts(){ phc.clearRect(0,0,80,14);
for(let i=0;i<5;i++){ const rem=playerHP-i*20; pixHeart(phc,i*15, rem>=20?'full':rem>=10?'half':'empty'); } }
// hotbar
function buildBar(){ const box=$('hotbar'); box.innerHTML=''; const bar = inside?IBAR:BAR;
if(inside){ box.style.display='flex'; box.style.overflowX='auto'; box.style.gridTemplateColumns='none'; }
else { box.style.display='grid'; box.style.overflowX=''; box.style.gridTemplateColumns=''; }
bar.forEach((k,i)=>{ const b=document.createElement('button'); b.className='slot'; b.dataset.k=k;
if(inside) b.style.flex='0 0 52px';
const nm = k==='pick'?'Break':k==='armory'?armorySlotName():ITEMS[k].name;
b.innerHTML=`<span class="num">${i+1}</span><canvas class="ic" width="60" height="60"></canvas><span class="nm">${nm}</span>`;
b.onclick=()=>selItem(k); box.appendChild(b);
drawItemIcon(b.querySelector('canvas').getContext('2d'),k,60); }); }
function armorySlotName(){ return 'Wpn: '+WPN[player.weapon||'blaster'].name; }
function selItem(k){
if(k==='armory' && sel==='armory'){ openArmory(); return; } // tap again = open weapon picker
sel=k; refreshBar();
const hint = k==='pick' ? 'Laser pickaxe — break a block, dig the ground, or tap your base to pick it up.'
: k==='armory' ? 'Armory — tap again to switch weapon. Day: place a crate. Night: FIRE ('+WPN[player.weapon||'blaster'].name+').'
: ITEMS[k].name+' — '+ITEMS[k].desc;
$('hint').textContent = hint; blip(700,.04); }
function refreshBar(){ [...document.querySelectorAll('.slot')].forEach(b=>b.classList.toggle('sel',b.dataset.k===sel));
$('startBtn').disabled = phase!=='day' || !basePlaced; }
// ---- placement ----
const kkey=(c,l)=>c+':'+l;
function inHouse(c){ return c>=HOUSE_C1 && c<=HOUSE_C2; }
function solidInCol(c){
const b0=blocks[kkey(c,0)], b1=blocks[kkey(c,1)];
if(b0){ const t=ITEMS[b0.k].type; if(t==='solid'||(t==='door'&&!b0.open)) return b0; }
if(b1 && ITEMS[b1.k].type==='solid') return b1;
return null;
}
function solidRects(){ const rects=[];
for(const key in blocks){ const b=blocks[key], it=ITEMS[b.k];
if(it.type==='solid') rects.push({x:b.c*B, y:GROUND-(b.l+1)*B, w:B, h:B});
else if(it.type==='door' && (b.openT||0)<0.5) rects.push({x:b.c*B, y:GROUND-2*B, w:B, h:2*B}); }
return rects; }
function pbox(px,py,pw,ph,r){ return px-pw<r.x+r.w && px+pw>r.x && py-ph<r.y+r.h && py>r.y; }
function placeItem(k,c,l){
if(k==='pick')return null;
if(c<0||c>=COLS)return null; if(inHouse(c)){flash(c,l,'#e2663b');return null;}
const it=ITEMS[k]; if(it.type!=='solid') l=0;
if(blocks[kkey(c,l)]){ flash(c,l,'#e2663b'); return null; }
if(it.type==='door' && blocks[kkey(c,1)]){ flash(c,l,'#e2663b'); return null; } // door needs 2 blocks tall
if(l===1 && blocks[kkey(c,0)] && ITEMS[blocks[kkey(c,0)].k].type==='door'){ flash(c,l,'#e2663b'); return null; }
const b=blocks[kkey(c,l)]={k,c,l,hp:it.hp||0,max:it.hp||0,cd:0,fuse:-1,anim:0,open:false,pass:null,openT:0};
puff(c*B+B/2, GROUND-l*B-B/2, '#8fe05a',9); blip(560,.05,'square');
return b;
}
function breakAt(c,wy){
let best=null,bd=1e9;
for(let l=0;l<5;l++){ const b=blocks[kkey(c,l)]; if(!b)continue;
const cy=GROUND-l*B-B/2, d=Math.abs(cy-wy); if(d<bd){bd=d;best=b;} }
// ground traps sit near surface
const g=blocks[kkey(c,0)]; if(g&&ITEMS[g.k].type!=='solid'){ best=g; }
if(best){ delete blocks[kkey(best.c,best.l)]; puff(best.c*B+B/2,GROUND-best.l*B-8,'#caa',9); blip(240,.07,'sawtooth'); }
}
function flash(c,l,col){ flashes.push({c,l,col,t:.3}); blip(150,.06); }
// ---- controls (on-canvas arrow pad + place button) ----
const DP={x:96,y:H-80}, PB={x:W-96,y:H-84}, PR=52, BB={x:W-158,y:H-150}, BBr=30, BOMB_CD=20, BND={x:W-158,y:H-150}, BNDr=30;
const dpadPtrs=new Map(); const dpad={up:0,down:0,left:0,right:0};
function screenPt(e){ const r=cv.getBoundingClientRect();
return {x:(e.clientX-r.left)/r.width*W, y:(e.clientY-r.top)/r.height*H}; }
const dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y);
function inDpad(p){ return Math.abs(p.x-DP.x)<86 && Math.abs(p.y-DP.y)<86; }
function dpadDirs(p){ const dx=p.x-DP.x, dy=p.y-DP.y, dz=14, res=[];
if(dx<-dz)res.push('left'); else if(dx>dz)res.push('right');
if(dy<-dz)res.push('up'); else if(dy>dz)res.push('down');
return res; }
function useDoor(d){
if(d.open){ d.open=false; puff(d.c*B+B/2,GROUND-B,'#caa',6); blip(300,.09,'square'); return; }
if(d.pass){ openKeypad('enter',d); return; } // locked → ask for code
d.open=true; puff(d.c*B+B/2,GROUND-B,'#8fe05a',6); blip(540,.09,'square');
}
function doorInCol(c){ const b=blocks[kkey(c,0)]; return b&&ITEMS[b.k].type==='door'?b:null; }
function digCol(c){ if(c<0||c>=COLS||inHouse(c)||dug[c])return;
dug[c]=true;
for(let i=0;i<12;i++)parts.push({x:c*B+B/2+(Math.random()-.5)*B,y:GROUND+4,
vx:(Math.random()-.5)*120,vy:-70-Math.random()*90,life:.55,max:.55,grav:340,
col:Math.random()<.5?'#3f342b':'#5b4d42',sz:2+Math.random()*3});
blip(200,.13,'sawtooth'); }
function fillCol(c){ if(c<0||c>=COLS||!dug[c])return; dug[c]=false;
for(let i=0;i<12;i++)parts.push({x:c*B+B/2+(Math.random()-.5)*B,y:GROUND+4,
vx:(Math.random()-.5)*90,vy:-40-Math.random()*60,life:.5,max:.5,grav:300,
col:Math.random()<.5?'#5b4d42':'#443528',sz:2+Math.random()*3});
blip(360,.1,'square'); }
function hasBlockCol(c){ for(let l=0;l<5;l++) if(blocks[kkey(c,l)]) return true; return false; }
function dropBase(c){ setBase(c); basePlaced=true; baseT=0; // start fold-out animation
banner('BASE PLACED! BUILD YOUR DEFENSES',1.8); refreshBar(); blip(560,.12,'triangle');
if(inTank && inTank.fromBase) inTank=null; // the base's tank moves with the base
tanks=tanks.filter(t=>!t.fromBase);
tanks.push({x:parkX,vx:0,hp:400,max:400,face:1,cd:0,fromBase:true});
helis=helis.filter(h=>!h.fromBase); // helicopter on top of the base
helis.push({x:HOUSE_CX, y:GROUND-2*B-30, dir:1, fireCd:0, anim:Math.random()*6, flash:0, fromBase:true, home:HOUSE_CX, range:150});
if(inArmor && inArmor.fromBase) inArmor=null; // power armor beside the base (super strong)
armors=armors.filter(a=>!a.fromBase);
const armorX=Math.max(30,Math.min(WORLD_W-30, parkX>HOUSE_CX ? HOUSE_L-42 : HOUSE_R+42));
if(!player.armored) armors.push({x:armorX,y:GROUND,vx:0,vy:0,hp:1000,max:1000,face:parkX>HOUSE_CX?1:-1,cd:0,step:0,onGround:true,fromBase:true});
allies=allies.filter(a=>!a.fromBase); // two guards spawn with the base
spawnAlly(HOUSE_CX-55,true); spawnAlly(HOUSE_CX+55,true);
for(let i=0;i<16;i++)parts.push({x:HOUSE_CX+(Math.random()-.5)*90,y:GROUND-10,vx:(Math.random()-.5)*120,vy:-60-Math.random()*80,life:.6,max:.6,grav:280,col:'#37e1ff',sz:2+Math.random()*3}); }
function breakBase(){ basePlaced=false; houseHP=houseMax;
if(inTank && inTank.fromBase) inTank=null;
tanks=tanks.filter(t=>!t.fromBase); // tank disappears with the base
helis=helis.filter(h=>!h.fromBase); // helicopter disappears with the base
if(inArmor && inArmor.fromBase) inArmor=null;
armors=armors.filter(a=>!a.fromBase); // power armor disappears with the base
allies=allies.filter(a=>!a.fromBase); // guards disappear with the base
for(let i=0;i<20;i++)parts.push({x:HOUSE_CX+(Math.random()-.5)*100,y:GROUND-40,vx:(Math.random()-.5)*160,vy:-40-Math.random()*100,life:.6,max:.6,grav:300,col:Math.random()<.5?'#59636f':'#37e1ff',sz:2+Math.random()*3});
banner('BASE PICKED UP',1.3); blip(240,.12,'sawtooth'); refreshBar(); }
function placeTank(c){ const x=c*B+B/2;
tanks.push({x,vx:0,hp:400,max:400,face:1,cd:0});
for(let i=0;i<12;i++)parts.push({x,y:GROUND-8,vx:(Math.random()-.5)*120,vy:-40-Math.random()*60,life:.5,max:.5,grav:280,col:Math.random()<.5?'#3a424c':'#4a545f',sz:2+Math.random()*3});
blip(300,.1,'square'); }
function enterTank(t){ inTank=t; tankCd=0.35; banner('IN TANK — FIRE to shoot, UP to get out',1.8); blip(200,.12,'square'); }
function exitTank(){ if(!inTank)return; const t=inTank; player.x=clamp(t.x+(t.face>0?-42:42),12,WORLD_W-12); player.y=GROUND; player.vy=0; inTank=null; tankCd=0.35; blip(180,.1,'square'); }
function destroyTank(t){ boom(t.x,GROUND-18,55,72); const i=tanks.indexOf(t); if(i>=0)tanks.splice(i,1);
if(inTank===t){ player.x=t.x; player.y=GROUND; inTank=null; tankCd=0.35; } banner('TANK DESTROYED!',1.4); }
function fireTank(){ const t=inTank; if(!t||t.cd>0)return; t.cd=0.55;
const dir=t.face, mx=t.x+dir*34, my=GROUND-34;
tankShells.push({x:mx,y:my,dir,life:1.3,dead:false});
for(let i=0;i<6;i++)parts.push({x:mx,y:my,vx:dir*(60+Math.random()*120),vy:-20-Math.random()*40,life:.2,max:.2,col:Math.random()<.5?'#ffd24a':'#ff7a2a',sz:2+Math.random()*2});
shake=Math.max(shake,5); blip(110,.16,'sawtooth',.07); }
function enterHeli(h){ inHeli=h; tankCd=0.35; h.vx=0; h.vy=0; banner('PILOTING HELI — FIRE to shoot, EXIT to get out',1.8); blip(220,.12,'square'); }
function exitHeli(){ if(!inHeli)return; const h=inHeli; if(h.home!==undefined)h.x=h.home; h.y=GROUND-2*B-30; h.vx=0; h.vy=0;
player.x=h.x; player.y=GROUND; player.vy=0; inHeli=null; tankCd=0.35; blip(180,.1,'square'); }
function exitVehicle(){ if(inTank)exitTank(); else if(inHeli)exitHeli(); }
function equipArmor(a){ player.armored=true; const i=armors.indexOf(a); if(i>=0)armors.splice(i,1); syncHUD();
banner('POWER ARMOR ON FOR GOOD — it can never be removed!',2.2); blip(200,.14,'square');
for(let k=0;k<16;k++)parts.push({x:player.x,y:GROUND-22,vx:(Math.random()-.5)*130,vy:-40-Math.random()*90,life:.55,max:.55,col:'#37e1ff',sz:2+Math.random()*3}); }
function fireHeli(){ const h=inHeli; if(!h||h.fireCd>0)return; h.fireCd=0.07; h.flash=0.05;
const f=h.dir||1, mx=h.x+f*18, my=h.y+14, dx=f*0.62, dy=0.78, len=Math.hypot(dx,dy), sp=560;
heliBullets.push({x:mx, y:my, vx:dx/len*sp+(Math.random()-.5)*20, vy:dy/len*sp});
blip(1400,.02,'square',.03); }
function dropBomb(){ if(!inHeli||bombCd>0)return; bombCd=BOMB_CD;
bombs.push({x:inHeli.x, y:inHeli.y+16, vy:70, spin:0});
blip(150,.12,'sawtooth',.06); }
function fireMissile(z){ const h=inHeli; if(!h)return;
const sx=z.x, sy=GROUND-ZT[z.t].r-4, a=Math.atan2(h.y-sy,h.x-sx);
missiles.push({x:sx,y:sy,vx:Math.cos(a)*230,vy:Math.sin(a)*230,life:4.5});
blip(240,.14,'sawtooth',.05); shake=Math.max(shake,2);
for(let i=0;i<5;i++)parts.push({x:sx,y:sy,vx:(Math.random()-.5)*40,vy:20+Math.random()*30,life:.3,max:.3,col:'#ccc',sz:2}); }
function spawnAlly(x,fromBase,type){ const u=UNITS[type||'blaster'];
allies.push({x:Math.max(12,Math.min(WORLD_W-12,x)), t:type||'blaster', hp:u.hp, max:u.hp, face:1, cd:Math.random()*u.cd, step:0, flash:0, fromBase:!!fromBase});
for(let i=0;i<8;i++)parts.push({x,y:GROUND-10,vx:(Math.random()-.5)*80,vy:-30-Math.random()*40,life:.4,max:.4,col:u.col,sz:2});
blip(520,.08,'square'); }
function placeElevator(c){ const x=c*B+B/2;
elevators.push({x, y:GROUND-6, top:GROUND-4*B, bottom:GROUND-6, dir:-1, speed:72, wait:0});
for(let i=0;i<6;i++)parts.push({x,y:GROUND-10,vx:(Math.random()-.5)*60,vy:-20-Math.random()*30,life:.3,max:.3,col:'#37e1ff',sz:2});
blip(440,.08,'square'); }
const LIMBS={larm:'LEFT ARM', rarm:'RIGHT ARM', lleg:'LEFT LEG', rleg:'RIGHT LEG'};
function hurtLimbs(){ return Object.keys(LIMBS).filter(k=>player.injuries[k]===1); } // still need a bandage
function deadLimbs(){ return Object.keys(LIMBS).filter(k=>player.injuries[k]>0); } // ruined for good
function biteLimb(){ if(player.biteCd>0 || player.armored)return; // armor stops bites
const ok=Object.keys(LIMBS).filter(k=>!player.injuries[k]); if(!ok.length)return;
const k=ok[Math.floor(Math.random()*ok.length)];
player.injuries[k]=1; player.biteCd=3.2; shake=Math.max(shake,7);
banner('BITTEN! '+LIMBS[k]+' RUINED — BANDAGE IT IN THE MORNING',2.4); syncHUD();
blip(90,.22,'sawtooth',.07);
for(let i=0;i<12;i++)parts.push({x:player.x,y:player.y-20,vx:(Math.random()-.5)*140,vy:-30-Math.random()*90,life:.5,max:.5,grav:300,col:'#c0392b',sz:2+Math.random()*2}); }
function bandageLimb(){ const h=hurtLimbs(); if(!h.length)return;
const k=h[0]; player.injuries[k]=2; player.wrap=1.2; // patched, but never works again
banner(LIMBS[k]+' BANDAGED — but it stays unusable',1.8); blip(760,.12,'triangle'); syncHUD();
for(let i=0;i<10;i++)parts.push({x:player.x,y:player.y-20,vx:(Math.random()-.5)*90,vy:-25-Math.random()*60,life:.5,max:.5,grav:250,col:'#ffffff',sz:2+Math.random()*2}); }
const IBAR=['carpet','rug','chair','table','sofa','bed','tv','desk','bookshelf','lamp','plant','fridge','stove','sink','toilet','bath','painting','idoor','passage','remove'];
const WPN={ blaster:{dmg:46,cd:0.14,col:'#8fe9ff',kind:'bolt',sp:760,name:'BLASTER'},
laser:{dmg:30,cd:0.1,col:'#7cffb0',kind:'bolt',sp:830,name:'LASER RIFLE'},
rocket:{dmg:120,cd:0.85,col:'#ff9a2a',kind:'rocket',sp:560,name:'ROCKET LAUNCHER'},
knife:{dmg:42,cd:0.26,col:'#d8dde3',kind:'knife',sp:620,name:'THROWING KNIVES'} };
const WCYCLE=['blaster','laser','rocket','knife'];
function placeCrate(c){ crates.push({x:c*B+B/2});
for(let i=0;i<6;i++)parts.push({x:c*B+B/2,y:GROUND-10,vx:(Math.random()-.5)*60,vy:-20-Math.random()*30,life:.3,max:.3,col:'#f4c020',sz:2});
blip(360,.08,'square'); }
let armoryOpen=false;
const AP={x:W/2-232,y:132,w:464,h:214};
function apSlot(i){ const sw=100,gap=10; return {x:AP.x+22+i*(sw+gap), y:AP.y+58, w:sw, h:112}; }
function openArmory(){ armoryOpen=true; blip(620,.1,'square'); }
function equipWeapon(k){ player.weapon=k; armoryOpen=false;
banner('EQUIPPED: '+WPN[k].name,1.4); blip(760,.1,'square'); syncArmorySlot(); }
function syncArmorySlot(){ const b=[...document.querySelectorAll('.slot')].find(x=>x.dataset.k==='armory');
if(!b)return; const nm=b.querySelector('.nm'); if(nm)nm.textContent='Wpn: '+WPN[player.weapon||'blaster'].name;
const cv=b.querySelector('canvas'); if(cv)drawItemIcon(cv.getContext('2d'),'armory',60); }
function grabWeapon(){ openArmory(); }
function drawArmoryPanel(){
ctx.fillStyle='rgba(6,10,16,.82)'; ctx.fillRect(0,0,W,H);
ctx.fillStyle='#141a23'; ctx.fillRect(AP.x,AP.y,AP.w,AP.h);
ctx.strokeStyle='#37e1ff'; ctx.lineWidth=3; ctx.strokeRect(AP.x,AP.y,AP.w,AP.h);
ctx.fillStyle='#f4c020'; ctx.fillRect(AP.x,AP.y,AP.w,5);
ctx.fillStyle='#1c2229'; for(let sx=AP.x+4;sx<AP.x+AP.w-4;sx+=12) ctx.fillRect(sx,AP.y,6,5);
ctx.fillStyle='#37e1ff'; ctx.font='12px "Press Start 2P",monospace'; ctx.textAlign='center';
ctx.fillText('ARMORY — PICK A WEAPON',W/2,AP.y+34);
for(let i=0;i<WCYCLE.length;i++){ const k=WCYCLE[i], r=apSlot(i), cur=(player.weapon===k);
ctx.fillStyle= cur?'rgba(55,225,255,.16)':'rgba(255,255,255,.045)'; ctx.fillRect(r.x,r.y,r.w,r.h);
ctx.strokeStyle= cur?'#37e1ff':'#3a424c'; ctx.lineWidth=2; ctx.strokeRect(r.x,r.y,r.w,r.h);
ctx.save(); ctx.translate(r.x+r.w/2,r.y+38); ctx.scale(1.15,1.15); drawWpnArt(ctx,k); ctx.restore();
ctx.fillStyle= cur?'#6eff7a':'#cfd8e3'; ctx.font='7px "Press Start 2P",monospace';
const words=WPN[k].name.split(' ');
words.forEach((wd,j)=>ctx.fillText(wd, r.x+r.w/2, r.y+76+j*11));
ctx.fillStyle='#8fa0b4'; ctx.fillText('DMG '+WPN[k].dmg, r.x+r.w/2, r.y+r.h-9);
if(cur){ ctx.fillStyle='#6eff7a'; ctx.font='7px "Press Start 2P",monospace'; ctx.fillText('EQUIPPED', r.x+r.w/2, r.y+14); }
}
ctx.fillStyle='#cfd8e3'; ctx.font='8px "Press Start 2P",monospace';
ctx.fillText('TAP A WEAPON TO TAKE IT • TAP OUTSIDE TO CLOSE', W/2, AP.y+AP.h-6);
}
function fireWeapon(tx,ty){ if(blasterCd>0)return;
const arms=(player.injuries.larm?1:0)+(player.injuries.rarm?1:0);
if(arms>=2){ banner('BOTH ARMS BITTEN — CAN\u2019T SHOOT!',1.2); blip(120,.1,'square'); blasterCd=0.6; return; }
const w=WPN[player.weapon||'blaster'];
blasterCd = w.cd*(arms?2.2:1); // one bad arm = slower
const px=player.x, py=player.y-24, a=Math.atan2(ty-py,tx-px);
playerShots.push({x:px,y:py,vx:Math.cos(a)*w.sp,vy:Math.sin(a)*w.sp,dist:0,kind:w.kind,dmg:w.dmg,col:w.col,spin:0});
if(tx>px)player.face=1; else if(tx<px)player.face=-1;
blip(w.kind==='rocket'?240:w.kind==='knife'?900:1300,.03,'square',.04); }
let noBuildT=0;
function noBuild(){ if(noBuildT>0)return; noBuildT=1.2;
banner('NO BUILDING AT NIGHT — SURVIVE TILL MORNING!',1.2); blip(150,.1,'square'); }
function placeUnderPlayer(){ const c=Math.floor(player.x/B);
if(inTank){ fireTank(); return; }
if(inHeli){ fireHeli(); return; }
if(sel==='armory'){ if(phase==='night') fireWeapon(player.x+player.face*800, player.y-24); else placeCrate(Math.floor(player.x/B)); return; }
if(sel!=='pick' && phase==='night'){ noBuild(); return; } // no building during the night
if(sel==='elevator'){ placeElevator(c); return; }
if(sel==='base'){ if(phase==='day') dropBase(c); return; }
if(sel==='dirt'){ fillCol(c); return; }
const dr=doorInCol(c); if(dr && sel!=='pick'){ useDoor(dr); return; }
if(sel==='pick'){ if(basePlaced && phase==='day' && inHouse(c)){ breakBase(); return; } if(hasBlockCol(c)) breakAt(c,GROUND-10); else digCol(c); return; }
let l=0; if(ITEMS[sel].type==='solid'){ while(blocks[kkey(c,l)]&&l<4)l++; }
const nb=placeItem(sel,c,l); if(nb&&nb.k==='door') openKeypad('set',nb); }
function worldTap(p){ const wx=p.x+cam, wy=p.y, c=Math.floor(wx/B);
if(inTank){ fireTank(); return; }
if(inHeli){ fireHeli(); return; }
for(const t of tanks){ if(Math.abs(t.x-wx)<38 && Math.abs(t.x-player.x)<80){ enterTank(t); return; } } // tap a tank to get in
for(const a of armors){ if(Math.abs(a.x-wx)<34 && Math.abs(a.x-player.x)<80){ equipArmor(a); return; } } // tap the armor to put it on
for(const h of helis){ if(Math.abs(h.x-wx)<40 && Math.abs(wy-h.y)<40 && Math.abs(h.x-player.x)<70){ enterHeli(h); return; } } // tap a heli to fly it
if(sel==='armory'){
for(const cr of crates){ if(Math.abs(cr.x-wx)<34 && Math.abs(cr.x-player.x)<75){ grabWeapon(); return; } } // grab a weapon from the crate
if(phase==='night'){ fireWeapon(wx,wy); return; }
placeCrate(c); return; } // day: set down a crate
if(sel!=='pick' && phase==='night'){ const dr0=doorInCol(c); if(dr0){ useDoor(dr0); return; } noBuild(); return; }
if(sel==='elevator'){ placeElevator(c); return; }
if(sel==='base'){ if(phase==='day') dropBase(c); return; }
if(sel==='dirt'){ fillCol(c); return; }
if(sel==='pick'){ // pick up base, break blocks, or dig the ground
if(basePlaced && phase==='day' && inHouse(c)){ breakBase(); return; }
if(wy<GROUND && hasBlockCol(c)) breakAt(c,wy); else digCol(c);
return; }
if(wy>GROUND+40) return;
const dr=doorInCol(c); if(dr){ useDoor(dr); return; }
let l=0; if(ITEMS[sel].type==='solid') l=clamp(Math.floor((GROUND-wy)/B),0,4);
const nb=placeItem(sel,c,l); if(nb&&nb.k==='door') openKeypad('set',nb); }
cv.addEventListener('pointerdown',e=>{ e.preventDefault(); cv.setPointerCapture(e.pointerId);
const p=screenPt(e);
if(inside){ // ---- inside the base ----
if(p.x>14 && p.x<124 && p.y>60 && p.y<96){ exitBase(); return; } // EXIT
if(inDpad(p)){ dpadPtrs.set(e.pointerId, dpadDirs(p)); return; }
if(dist(p,PB)<PR+8){ placeFurniture(player.x); return; } // PLACE button
placeFurniture(p.x); return; // tap room to place
}
if(nearBaseDoor() && p.x>W/2-70 && p.x<W/2+70 && p.y>62 && p.y<98){ enterBase(); return; } // ENTER button
if(armoryOpen){ // weapon picker is open
for(let i=0;i<WCYCLE.length;i++){ const r=apSlot(i);
if(p.x>r.x&&p.x<r.x+r.w&&p.y>r.y&&p.y<r.y+r.h){ equipWeapon(WCYCLE[i]); return; } }
if(!(p.x>AP.x&&p.x<AP.x+AP.w&&p.y>AP.y&&p.y<AP.y+AP.h)) armoryOpen=false; // tap outside closes
return; }
if((inTank||inHeli) && p.x>14 && p.x<110 && p.y>60 && p.y<96){ exitVehicle(); return; } // EXIT button
if(phase==='day' && !inTank && !inHeli && hurtLimbs().length && dist(p,BND)<BNDr+8){ bandageLimb(); return; } // BANDAGE
if(inHeli && dist(p,BB)<BBr+6){ dropBomb(); return; } // BOMB button
if(inDpad(p)){ dpadPtrs.set(e.pointerId, dpadDirs(p)); }
else if(dist(p,PB)<PR+8){ placeUnderPlayer(); }
else worldTap(p);
});
cv.addEventListener('pointermove',e=>{ if(dpadPtrs.has(e.pointerId)) dpadPtrs.set(e.pointerId, dpadDirs(screenPt(e))); });
function endPtr(e){ dpadPtrs.delete(e.pointerId); }
cv.addEventListener('pointerup',endPtr); cv.addEventListener('pointercancel',endPtr);
// ---- night waves ----
function startNight(){ if(phase!=='day'||inside)return;
if(!basePlaced){ banner('PLACE YOUR BASE FIRST!',1.4); blip(160,.1,'square'); return; }
phase='night';
const n=night, count=5+Math.round(n*2.2), q=[];
for(let i=0;i<count;i++){ const babyC=Math.min(.14+n*.05,.5), bruteC=Math.min(.03+n*.045,.4), missC=Math.min(.05+n*.02,.2), r=Math.random();
q.push(r<bruteC?'brute':r<bruteC+babyC?'baby':r<bruteC+babyC+missC?'missiler':'zombie'); }
queue=q.sort(()=>Math.random()-.5); spawnT=.6;
refreshBar(); banner('NIGHT '+n+' — ZOMBIES RISE'); blip(110,.3,'sawtooth');
}
$('startBtn').onclick=startNight;
function spawn(t){ const b=ZT[t], hp=b.hp*(1+(night-1)*0.12), side=Math.random()<.5?-1:1;
const x= side<0? -30 : WORLD_W+30, dir= side<0? 1 : -1;
zombies.push({t,x,dir,hp,max:hp,step:0,wob:Math.random()*6.28,hurt:0,slowT:0,slowA:0,burn:0}); }
// ---- combat ----
function shoot(x,y,dir,dmg){ shots.push({x,y,dir,dmg,dead:false}); blip(880,.03,'square'); }
function fireLaser(x,y,tgt,dmg){
const ty=GROUND-ZT[tgt.t].r;
beams.push({x1:x,y1:y,x2:tgt.x,y2:ty,life:.12,max:.12});
hurt(tgt,dmg);
for(let i=0;i<6;i++)parts.push({x:tgt.x,y:ty,vx:(Math.random()-.5)*160,vy:(Math.random()-.7)*140,life:.3,max:.3,col:i%2?'#ff5a5a':'#ffd0d0',sz:2+Math.random()*2});
blip(1500,.05,'sawtooth',.045);
}
function boom(x,y,dmg,rad){ blasts.push({x,y,r:6,max:rad,t:0}); shake=Math.max(shake,9);
for(const z of zombies){ const d=Math.abs(z.x-x)+Math.abs(GROUND-y)*.2; if(d<rad+ZT[z.t].r) hurt(z,dmg*(1-Math.min(1,d/(rad+40)))); }
for(let i=0;i<20;i++)parts.push({x,y,vx:(Math.random()-.5)*300,vy:(Math.random()-.7)*260,life:.55,max:.55,grav:200,col:i%2?'#ffb14d':'#ff5a2a',sz:3+Math.random()*3});
blip(80,.24,'sawtooth'); }
function hurt(z,dmg){ z.hp-=dmg; z.hurt=.12; blood(z.x,GROUND-ZT[z.t].r,3);
if(z.hp<=0&&!z.dead){ z.dead=true; kills++; floats.push({x:z.x,y:GROUND-ZT[z.t].r*1.6,txt:'+1',t:0,col:'#8fe05a'});
blood(z.x,GROUND-ZT[z.t].r,14); syncHUD(); } }
function blood(x,y,n){ for(let i=0;i<n;i++)parts.push({x,y,vx:(Math.random()-.5)*160,vy:(Math.random()-.8)*160,life:.5,max:.5,grav:300,col:Math.random()<.3?'#4c7f4c':'#2f7d2f',sz:2+Math.random()*3}); }
function puff(x,y,col,n){ for(let i=0;i<n;i++)parts.push({x,y,vx:(Math.random()-.5)*130,vy:(Math.random()-.5)*130,life:.4,max:.4,col,sz:2+Math.random()*3}); }
// ---- update ----
function update(dt){
shake=Math.max(0,shake-dt*18);
if(sel==='base' && !basePlaced) setBase(Math.floor(player.x/B)); // ghost base follows you
if(basePlaced && baseT<1) baseT=Math.min(1,baseT+dt*0.8); // fold-out deploy (slow)
dn += ((phase==='night'?1:0)-dn)*Math.min(1,dt*1.5);
flashes=flashes.filter(f=>(f.t-=dt)>0);
// elevators ride up and down
for(const el of elevators){
if(el.wait>0){ el.wait-=dt; continue; } // pause at the top/bottom
el.y += el.dir*el.speed*dt;
if(el.y<=el.top){ el.y=el.top; el.dir=1; el.wait=2.4; }
else if(el.y>=el.bottom){ el.y=el.bottom; el.dir=-1; el.wait=2.4; } }
// player: walk + jump (grounded, no flying)
player.anim+=dt;
if(player.hurt>0)player.hurt-=dt;
if(player.biteCd>0)player.biteCd-=dt;
if(player.hitCd>0)player.hitCd-=dt;
if(noBuildT>0)noBuildT-=dt;
if(player.wrap>0)player.wrap-=dt;
if(phase==='night'){ const open=hurtLimbs().length; // untreated bites bleed
if(open && playerHP>0){ playerHP=Math.max(0,playerHP-open*1.6*dt); syncHUD();
if(Math.random()<dt*3)parts.push({x:player.x+(Math.random()-.5)*10,y:player.y-14,vx:0,vy:40,life:.4,max:.4,grav:200,col:'#c0392b',sz:2});
if(playerHP<=0) gameOver('player'); } }
dpad.up=dpad.down=dpad.left=dpad.right=0;
for(const dirs of dpadPtrs.values()) for(const d of dirs) dpad[d]=1;
const R=keys.right||dpad.right, L=keys.left||dpad.left, U=keys.up||dpad.up, Dn=keys.down||dpad.down;
if(inTank){
const t=inTank; t.cd=Math.max(0,t.cd-dt); if(tankCd>0)tankCd-=dt;
const drive=180, dvx=((R?1:0)-(L?1:0))*drive;
if(dvx!==0){ t.vx += (dvx-t.vx)*Math.min(1,dt*7); } // driving
else { t.vx=0; if(Math.abs(t.x-parkX)<26) t.x=parkX; } // released = lock / snap into the bay
t.x=clamp(t.x+t.vx*dt,B,WORLD_W-B);
if(t.vx>5)t.face=1; else if(t.vx<-5)t.face=-1;
player.x=t.x; player.y=GROUND; player.vx=0; player.vy=0; player.onGround=true;
if(U && tankCd<=0) exitTank();
for(const z of zombies){ if(z.dead||z.pit)continue; if(Math.abs(z.x-t.x)<32){ hurt(z,50*dt); z.x += (z.x>t.x?1:-1)*40*dt; } } // crush
} else if(inHeli){ // PILOT the helicopter
const h=inHeli; if(tankCd>0)tankCd-=dt; if(h.flash>0)h.flash-=dt; h.fireCd-=dt; h.anim+=dt;
const SP=235, ix=(R?1:0)-(L?1:0), iy=(Dn?1:0)-(U?1:0);
h.vx=(h.vx||0)+(ix*SP-(h.vx||0))*Math.min(1,dt*6);
h.vy=(h.vy||0)+(iy*SP-(h.vy||0))*Math.min(1,dt*6);
h.x=clamp(h.x+h.vx*dt,20,WORLD_W-20);
h.y=clamp(h.y+h.vy*dt,40,GROUND-30);
if(h.vx>4)h.dir=1; else if(h.vx<-4)h.dir=-1;
player.x=h.x; player.y=GROUND; player.vx=0; player.vy=0; player.onGround=true;
} else if(phase==='night'){ // NIGHT: grounded survival, collide with blocks
const legs=(player.injuries.lleg?1:0)+(player.injuries.rleg?1:0);
const walk=legs>=2?70:legs?135:205, tvx=((R?1:0)-(L?1:0))*walk;
player.vx += (tvx-player.vx)*Math.min(1,dt*12);
const pw=9, ph=40, rects=solidRects();
player.x=clamp(player.x+player.vx*dt,12,WORLD_W-12);
for(const r of rects){ if(pbox(player.x,player.y,pw,ph,r)){
if(player.vx>0) player.x=r.x-pw; else if(player.vx<0) player.x=r.x+r.w+pw; player.vx=0; } }
player.vy += 950*dt; player.y += player.vy*dt; player.onGround=false;
if(player.y>=GROUND){ player.y=GROUND; player.vy=0; player.onGround=true; }
for(const r of rects){ if(pbox(player.x,player.y,pw,ph,r)){
if(player.vy>0){ player.y=r.y; player.vy=0; player.onGround=true; }
else if(player.vy<0){ player.y=r.y+r.h+ph; player.vy=0; } } }
for(const el of elevators){ const w=B*0.5+4; // ride the lift platform
if(Math.abs(player.x-el.x)<w && player.vy>=-1 && player.y>=el.y-4 && player.y<=el.y+16){ player.y=el.y; player.vy=0; player.onGround=true; } }
if(U && player.onGround){ // jump (bad legs = weak/none)
if(legs>=2){ if(!muted&&Math.random()<.1)blip(120,.06,'square',.03); }
else { player.vy=legs?-280:-400; player.onGround=false; if(!muted)blip(300,.08,'square',.04); } }
} else { // DAY: fly freely and pass through blocks
const SP=280, ix=(R?1:0)-(L?1:0), iy=(Dn?1:0)-(U?1:0);
player.vx += (ix*SP-player.vx)*Math.min(1,dt*9);
player.vy += (iy*SP-player.vy)*Math.min(1,dt*9);
player.x=clamp(player.x+player.vx*dt,12,WORLD_W-12);
player.y=clamp(player.y+player.vy*dt,34,GROUND);
player.onGround = player.y>=GROUND-1;
}
if(player.vx<-6)player.face=-1; else if(player.vx>6)player.face=1;
if(player.onGround && Math.abs(player.vx)>40) player.step=(player.step||0)+dt*8;
cam += (clamp(player.x-W/2,0,CAM_MAX)-cam)*Math.min(1,dt*6);
for(const cl of clouds){ cl.x+=6*dt; if(cl.x>WORLD_W+80)cl.x=-80; }
// devices + tnt + block anim
for(const key in blocks){ const b=blocks[key], it=ITEMS[b.k]; b.anim+=dt;
if(it.type==='door'){ const tg=b.open?1:0, r=dt*3.4; // animate open/close
if(b.openT===undefined)b.openT=tg;
if(b.openT<tg)b.openT=Math.min(tg,b.openT+r); else if(b.openT>tg)b.openT=Math.max(tg,b.openT-r); }
if(phase!=='night')continue;
if(it.type==='device'){ b.cd-=dt; if(b.cd<=0){
let tgt=null,best=1e9; const bx=b.c*B+B/2;
for(const z of zombies){ if(z.dead)continue; const d=Math.abs(z.x-bx);
if(d<it.range && d<best){best=d;tgt=z;} }
if(tgt){ b.cd=it.fire;
if(it.laser){ fireLaser(bx,GROUND-26,tgt,it.dmg); }
else { shoot(bx,GROUND-14,tgt.x>bx?1:-1,it.dmg); } } } }
else if(it.type==='tnt'){ const bx=b.c*B+B/2;
if(b.fuse<0){ for(const z of zombies){ if(!z.dead&&Math.abs(z.x-bx)<1.6*B){ b.fuse=.5; blip(200,.1,'square'); break; } } }
else { b.fuse-=dt; if(b.fuse<=0){ boom(bx,GROUND-B/2,it.dmg,it.rad); delete blocks[key]; } } }
}
// spawns
if(phase==='night'){ if(queue.length){ spawnT-=dt; if(spawnT<=0){ spawn(queue.shift()); spawnT=Math.max(.4,1.3-night*.05);} }
else if(zombies.length===0) nightCleared(); }
// shots
for(const s of shots){ s.x+=s.dir*720*dt; if(s.x<-40||s.x>WORLD_W+40){s.dead=true;continue;}
for(const z of zombies){ if(z.dead)continue; if(Math.abs(z.x-s.x)<ZT[z.t].r+6){ hurt(z,s.dmg);
parts.push({x:s.x,y:s.y,vx:s.dir*60,vy:-30,life:.15,max:.15,col:'#fff',sz:2}); s.dead=true; break; } } }
shots=shots.filter(s=>!s.dead);
// zombies
for(const z of zombies){ if(z.dead)continue;
const zt=ZT[z.t];
if(z.pit){ // pit sequence: fall -> stuck -> climb out
if(z.pit==='fall'){ z.fallY+=170*dt; if(z.fallY>=44){ z.fallY=44; z.pit='stuck'; z.stuckT=z.t==='brute'?1.9:z.t==='baby'?0.8:1.3; } }
else if(z.pit==='stuck'){ z.struggle=(z.struggle||0)+dt; z.stuckT-=dt;
if(Math.random()<dt*6)parts.push({x:z.x+(Math.random()-.5)*16,y:GROUND+2,vx:(Math.random()-.5)*40,vy:-30,life:.3,max:.3,col:'#4a3a2c',sz:2});
if(z.stuckT<=0) z.pit='climb'; }
else { z.fallY-=90*dt; // climbing back out
if(z.fallY<=0){ z.fallY=0; z.pit=null; const c2=Math.floor(z.x/B);
z.x = z.dir>0 ? (c2+1)*B+zt.r : c2*B-zt.r; puff(z.x,GROUND-4,'#5b4d42',6); } }
continue;
}
z.hurt=Math.max(0,z.hurt-dt); if(z.slowT>0)z.slowT-=dt;
const col=Math.floor(z.x/B);
if(dug[col] && !inHouse(col)){ z.pit='fall'; z.fallY=0; z.struggle=0; // fall into the hole
puff(z.x,GROUND+6,'#2a2320',8); blip(200,.14,'sawtooth'); continue; }
// ground effects at current column
z.slowA=z.slowT>0?z.slowA:0;
const g=blocks[kkey(col,0)];
if(g){ const it=ITEMS[g.k];
if(it.type==='ground'){ if(it.dps)hurt(z,it.dps*dt);
if(it.slow){ z.slowT=Math.max(z.slowT,.1); z.slowA=it.slow; }
if(it.burn){ z.burn=it.burn; }
if(g.k==='lava'&&Math.random()<dt*10)parts.push({x:z.x,y:GROUND-6,vx:0,vy:-40,life:.4,max:.4,col:'#ff7a2a',sz:2});
}
// zombies trample and break traps / dispensers they walk over
if((it.type==='ground'||it.type==='device') && g.max){
g.hp-=zt.block*dt;
if(Math.random()<dt*7)parts.push({x:z.x+(Math.random()-.5)*B*.6,y:GROUND-8,vx:0,vy:-30,life:.3,max:.3,col:'#caa',sz:2});
if(g.hp<=0){ puff(g.c*B+B/2,GROUND-10,'#caa',9); delete blocks[kkey(g.c,0)]; blip(150,.09,'sawtooth'); }
}
}
if(z.burn>0){ z.burn-=dt; hurt(z,18*dt);
if(Math.random()<dt*14)parts.push({x:z.x+(Math.random()-.5)*14,y:GROUND-zt.r,vx:0,vy:-50,life:.4,max:.4,col:Math.random()<.5?'#ff7a2a':'#ffd24a',sz:2}); }
if(z.dead)continue;
// missile zombie: fire rockets at the helicopter you're piloting
if(zt.missile && inHeli){ z.mcd=(z.mcd||1.2)-dt; if(z.mcd<=0){ z.mcd=2.6; fireMissile(z); } }
// melee: zombies can only hurt you on the ground (or your tank) — never the helicopter
if(!inHeli){ const tgtX = inTank?inTank.x:player.x;
if(Math.abs(z.x-tgtX)<zt.r+16 && (inTank || player.y>GROUND-26)){
z.step+=dt*6; if(Math.random()<dt*4)shake=Math.max(shake,2);
if(inTank){ inTank.hp-=zt.pdmg*dt*1.1; if(inTank.hp<=0) destroyTank(inTank); }
else if(player.hitCd<=0){ playerHP=Math.max(0,playerHP-zt.pdmg*(player.armored?0.25:1)); player.hurt=0.2; player.hitCd=1.4; biteLimb(); if(playerHP<=0) gameOver('player'); }
syncHUD(); continue;
} }
if(player.armored && !inTank && !inHeli && Math.abs(z.x-player.x)<zt.r+16 && player.y>GROUND-30){ hurt(z,44*dt); } // armor crushes zombies
// block / house ahead
const leadC=Math.floor((z.x+z.dir*zt.r)/B);
const sol=solidInCol(leadC);
let atHouse = z.dir>0 ? (z.x+zt.r>=HOUSE_L && leadC>=HOUSE_C1) : (z.x-zt.r<=HOUSE_R && leadC<=HOUSE_C2);
if(sol && !inHouse(leadC)){
sol.hp-=zt.block*dt; z.step+=dt*7;
if(ITEMS[sol.k].thorn){ hurt(z,ITEMS[sol.k].thorn*dt);
if(Math.random()<dt*12){ const el=sol.k==='door';
parts.push({x:z.x,y:GROUND-zt.r-Math.random()*zt.r, vx:(Math.random()-.5)*120, vy:-40-Math.random()*60,
life:.3,max:.3, col: el?(Math.random()<.5?'#37e1ff':'#eaffff'):'#3f7d2f', sz:2+Math.random()*2}); }
if(sol.k==='door' && Math.random()<dt*5) blip(1200,.03,'square'); }
if(sol.hp<=0){ puff(sol.c*B+B/2,GROUND-sol.l*B-B/2,'#caa',10); delete blocks[kkey(sol.c,sol.l)]; blip(140,.1,'sawtooth'); }
} else if(atHouse){
houseHP-=zt.house*dt; z.step+=dt*6; if(Math.random()<dt*4)shake=Math.max(shake,3);
syncHUD(); if(houseHP<=0){ houseHP=0; gameOver(); }
} else {
const eff=zt.spd*(z.slowT>0?1-z.slowA:1); z.x+=z.dir*eff*dt; z.step+=eff*dt*.16;
}
}
zombies=zombies.filter(z=>!z.dead);
for(const p of parts){ p.x+=p.vx*dt;p.y+=p.vy*dt; if(p.grav)p.vy+=p.grav*dt; p.life-=dt; }
parts=parts.filter(p=>p.life>0);
if(parts.length>260) parts.splice(0,parts.length-260); // hard cap keeps frame time stable
for(const f of floats){f.t+=dt;f.y-=28*dt;} floats=floats.filter(f=>f.t<1);
for(const b of blasts){b.t+=dt;b.r=b.max*(b.t/.28);} blasts=blasts.filter(b=>b.t<.28);
for(const bm of beams) bm.life-=dt; beams=beams.filter(bm=>bm.life>0);
for(const s of tankShells){ s.x+=s.dir*560*dt; s.life-=dt;
let hit=false; for(const z of zombies){ if(z.dead||z.pit)continue; if(Math.abs(z.x-s.x)<ZT[z.t].r+8){ hit=true; break; } }
if(hit||s.life<=0||s.x<0||s.x>WORLD_W){ boom(s.x,GROUND-20,95,84); s.dead=true; } }
tankShells=tankShells.filter(s=>!s.dead);
// helicopters idle (rotor spinning) unless you're piloting one — the piloted one is handled above
for(const h of helis){ if(h!==inHeli){ h.anim+=dt; if(h.flash>0)h.flash-=dt; } }
for(const b of heliBullets){ b.x+=b.vx*dt; b.y+=b.vy*dt; let hit=false;
for(const z of zombies){ if(z.dead||z.pit)continue; if(Math.abs(z.x-b.x)<ZT[z.t].r+3 && b.y>GROUND-ZT[z.t].r*2 && b.y<GROUND+6){ hurt(z,20); hit=true; break; } }
if(hit||b.y>GROUND+6){ if(!hit)parts.push({x:b.x,y:GROUND,vx:0,vy:-30,life:.2,max:.2,col:'#caa',sz:2}); b.dead=true; } }
heliBullets=heliBullets.filter(b=>!b.dead);
if(heliBullets.length>90) heliBullets.splice(0,heliBullets.length-90);
if(bombCd>0)bombCd-=dt;
for(const bm of bombs){ bm.vy+=680*dt; bm.y+=bm.vy*dt; bm.spin+=dt*8; let hit=false;
for(const z of zombies){ if(z.dead||z.pit)continue; if(Math.abs(z.x-bm.x)<ZT[z.t].r+8 && bm.y>GROUND-ZT[z.t].r*2){ hit=true; break; } }
if(hit||bm.y>=GROUND){ boom(bm.x,GROUND-14,240,155); shake=Math.max(shake,16); // powerful bomb
for(let i=0;i<14;i++)parts.push({x:bm.x,y:GROUND-10,vx:(Math.random()-.5)*360,vy:-60-Math.random()*180,life:.7,max:.7,grav:260,col:i%2?'#ffb14d':'#ff5a2a',sz:3+Math.random()*4});
bm.dead=true; } }
bombs=bombs.filter(b=>!b.dead);
for(const m of missiles){ m.life-=dt;
m.x+=m.vx*dt; m.y+=m.vy*dt;
if(Math.random()<dt*40)parts.push({x:m.x,y:m.y,vx:0,vy:0,life:.3,max:.3,col:Math.random()<.5?'#ffb14d':'#aaa',sz:2});
if(inHeli && Math.hypot(m.x-inHeli.x,m.y-inHeli.y)<22){ boom(m.x,m.y,0,42); shake=Math.max(shake,9);
playerHP=Math.max(0,playerHP-16); player.hurt=0.14; syncHUD(); if(playerHP<=0) gameOver('player'); m.dead=true; }
else if(m.life<=0 || m.y>GROUND+10 || m.x<-30 || m.x>WORLD_W+30) m.dead=true; }
missiles=missiles.filter(m=>!m.dead);
// allied soldiers
for(const al of allies){ const u=UNITS[al.t||'blaster']; al.cd-=dt; if(al.flash>0)al.flash-=dt;
let tgt=null,best=1e9; for(const z of zombies){ if(z.dead||z.pit)continue; const d=Math.abs(z.x-al.x); if(d<best){best=d;tgt=z;} }
if(tgt){ al.face = tgt.x>al.x?1:-1;
if(best>u.range){ al.x=clamp(al.x+al.face*u.spd*dt,12,WORLD_W-12); al.step+=dt*9; }
else if(al.cd<=0){ al.cd=u.cd; al.flash=0.06;
allyShots.push({x:al.x+al.face*13, y:GROUND-24, dir:al.face, dmg:u.dmg, kind:u.shot, col:u.col, dist:0, vy:0});
blip(u.shot==='rocket'?260:u.shot==='knife'?900:1050, u.shot==='rocket'?.14:.03, u.shot==='rocket'?'sawtooth':'square', .03); }
} else { // no zombies — wander around
if(al.wanderT===undefined||al.wanderT<=0){ al.pdir=Math.random()<.5?1:-1; al.wanderT=1.2+Math.random()*2.6; }
al.wanderT-=dt; al.face=al.pdir; al.x=clamp(al.x+al.pdir*Math.min(u.spd,80)*dt,12,WORLD_W-12); al.step+=dt*7;
if(al.x<=12||al.x>=WORLD_W-12) al.pdir*=-1;
}
for(const z of zombies){ if(z.dead||z.pit)continue; if(Math.abs(z.x-al.x)<ZT[z.t].r+12){ al.hp-=ZT[z.t].pdmg*dt*(u.armored?0.4:1); } }
if(al.hp<=0){ al.dead=true; puff(al.x,GROUND-20,'#8aa',12); blip(180,.14,'sawtooth'); }
}
allies=allies.filter(a=>!a.dead);
for(const s of allyShots){ const sp=s.kind==='rocket'?420:s.kind==='knife'?520:600; s.x+=s.dir*sp*dt; s.dist+=sp*dt;
if(s.kind==='knife'){ s.spin=(s.spin||0)+dt*22; }
let hit=false;
for(const z of zombies){ if(z.dead||z.pit)continue; if(Math.abs(z.x-s.x)<ZT[z.t].r+5){ hit=true;
if(s.kind==='rocket'){ boom(s.x,GROUND-20,s.dmg,80); } else hurt(z,s.dmg); break; } }
if(hit||s.dist>Math.max(360,660)||s.x<0||s.x>WORLD_W){ if(s.kind==='rocket'&&hit){} s.dead=true; } }
allyShots=allyShots.filter(s=>!s.dead);
if(allyShots.length>90) allyShots.splice(0,allyShots.length-90);
if(blasterCd>0)blasterCd-=dt;
for(const s of playerShots){ s.x+=s.vx*dt; s.y+=s.vy*dt; s.dist+=Math.hypot(s.vx,s.vy)*dt; if(s.kind==='knife')s.spin=(s.spin||0)+dt*24; let hit=false;
for(const z of zombies){ if(z.dead||z.pit)continue; if(Math.abs(z.x-s.x)<ZT[z.t].r+4 && s.y>GROUND-ZT[z.t].r*2 && s.y<GROUND+6){ hit=true;
if(s.kind==='rocket'){ boom(s.x,s.y,s.dmg||120,80); } else { hurt(z,s.dmg||46); parts.push({x:s.x,y:s.y,vx:0,vy:0,life:.14,max:.14,col:'#cfefff',sz:3}); } break; } }
if(hit||s.dist>1100||s.x<0||s.x>WORLD_W||s.y<0||s.y>GROUND+8){ s.dead=true; } }
playerShots=playerShots.filter(s=>!s.dead);
if(playerShots.length>70) playerShots.splice(0,playerShots.length-70);
}
function nightCleared(){ night++; phase='day'; playerHP=playerMax; heliBullets=[]; missiles=[]; allyShots=[]; for(const a of allies)a.hp=a.max; banner('YOU SURVIVED! BUILD MORE',1.7);
blip(660,.1,'triangle'); setTimeout(()=>blip(880,.13,'triangle'),110); syncHUD(); refreshBar(); }
function gameOver(cause){ phase='over'; zombies=[];shots=[]; helis=[]; heliBullets=[]; missiles=[];
$('ovTitle').innerHTML='GAME OVER';
const how = cause==='player' ? 'The zombies got you!' : 'The zombies destroyed your base!';
$('ovText').innerHTML=`${how} You survived <b>${night-1}</b> night${night-1===1?'':'s'} and defeated <b>${kills}</b> zombies.`;
$('ovBtn').textContent='Play Again'; $('overlay').classList.remove('hidden'); refreshBar(); }
let bT=null; function banner(t,s=1.3){ const b=$('banner'); b.textContent=t; b.classList.add('show');
clearTimeout(bT); bT=setTimeout(()=>b.classList.remove('show'),s*1000); }
// =============== RENDER ===============
function lerpC(a,b,t){ return `rgb(${a[0]+(b[0]-a[0])*t|0},${a[1]+(b[1]-a[1])*t|0},${a[2]+(b[2]-a[2])*t|0})`; }
function render(){
const sx=(Math.random()-.5)*shake, sy=(Math.random()-.5)*shake;
// sky
ctx.fillStyle=lerpC([158,118,86],[34,24,18],dn); ctx.fillRect(0,0,W,H);
ctx.save(); ctx.translate(sx,sy);
// celestial + parallax clouds
ctx.save(); ctx.translate(-cam*0.4,0);
// stars at night
if(dn>.2){ ctx.globalAlpha=dn; ctx.fillStyle='#fff';
for(let i=0;i<40;i++){ const x=(i*137)%WORLD_W, y=(i*89)%260; ctx.fillRect(x,y,2,2);} ctx.globalAlpha=1; }
// sun / moon
const skyX=W*0.5+cam*0.4-cam*0.4; // fixed-ish
ctx.fillStyle=dn<.5?'#ffe870':'#e8eef7';
const cyc=180 - dn*40; ctx.fillRect(700,60+dn*10,44,44);
if(dn>.5){ ctx.fillStyle=lerpC([158,118,86],[34,24,18],dn); ctx.fillRect(712,64,30,30); ctx.fillRect(700,60,44,44);
ctx.fillStyle='#e8eef7'; ctx.fillRect(700,60,44,44); ctx.fillStyle=lerpC([158,118,86],[34,24,18],dn); ctx.fillRect(720,66,20,20);}
// clouds
ctx.globalAlpha=0.4; ctx.fillStyle= dn>.4?'#3a352f':'#6b6258';
for(const cl of clouds){ const x=cl.x,y=cl.y,s=cl.s;
ctx.fillRect(x,y,44*s,16*s);ctx.fillRect(x+16*s,y-10*s,30*s,16*s);ctx.fillRect(x+30*s,y,30*s,14*s);} ctx.globalAlpha=1;
ctx.restore();
drawBackdrop();
drawSmoke();
// ---- world (camera) ----
ctx.save(); ctx.translate(-cam,0);
drawGround();
if(basePlaced){ drawHouse(baseT); }
else if(sel==='base'){ ctx.globalAlpha=0.45; drawHouse(1); ctx.globalAlpha=1;
ctx.strokeStyle='rgba(55,225,255,.7)'; ctx.lineWidth=2; ctx.setLineDash([6,6]);
ctx.strokeRect(HOUSE_L-2,GROUND-B*2-2,HOUSE_R-HOUSE_L+4,B*2+4); ctx.setLineDash([]); }
// blocks / traps
for(const key in blocks) drawPlacement(blocks[key]);
// build-mode grid hint near player column
if(phase==='day'){ const c=Math.floor(player.x/B); ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=2;
ctx.strokeRect(c*B+1,GROUND-B+1,B-2,B-2); }
for(const f of flashes){ ctx.globalAlpha=f.t*2.5; ctx.fillStyle=f.col;
ctx.fillRect(f.c*B,GROUND-(f.l+1)*B,B,B); ctx.globalAlpha=1; }
for(const b of blasts){ ctx.globalAlpha=1-b.t/.28; ctx.fillStyle='#ffb14d';
ctx.fillRect(b.x-b.r,b.y-b.r,b.r*2,b.r*2); ctx.globalAlpha=(1-b.t/.28)*.6; ctx.fillStyle='#ff5a2a';
ctx.fillRect(b.x-b.r*.6,b.y-b.r*.6,b.r*1.2,b.r*1.2); ctx.globalAlpha=1; }
for(const s of shots){ ctx.fillStyle='#6a4a2a'; ctx.fillRect(s.x-8*s.dir,s.y-1,12,3);
ctx.fillStyle='#c8c8c8'; ctx.fillRect(s.x+(s.dir>0?2:-6),s.y-2,4,5); }
for(const el of elevators) drawElevator(el);
for(const cr of crates) drawCrate(cr);
for(const z of zombies) drawZombie(z);
for(const al of allies) drawAlly(al);
for(const s of allyShots){ const y=s.y||GROUND-24;
if(s.kind==='rocket'){ ctx.save(); ctx.translate(s.x,y); if(s.dir<0)ctx.scale(-1,1);
ctx.fillStyle='#ffb14d'; ctx.fillRect(-12,-1,5,2); ctx.fillStyle='#c0392b'; ctx.fillRect(-7,-3,13,6); ctx.fillStyle='#e8e8e8'; ctx.fillRect(4,-3,4,6); ctx.restore(); }
else if(s.kind==='knife'){ ctx.save(); ctx.translate(s.x,y); ctx.rotate(s.spin||0);
ctx.fillStyle='#d8dde3'; ctx.fillRect(-5,-1,10,2); ctx.fillStyle='#8a5a2b'; ctx.fillRect(-5,-1,3,2); ctx.restore(); }
else { ctx.strokeStyle=s.col||'#7cffb0'; ctx.lineWidth=3; ctx.beginPath(); ctx.moveTo(s.x-s.dir*12,y); ctx.lineTo(s.x,y); ctx.stroke(); } }
for(const s of playerShots){ const a=Math.atan2(s.vy,s.vx);
if(s.kind==='rocket'){ ctx.save(); ctx.translate(s.x,s.y); ctx.rotate(a); ctx.fillStyle='#ffb14d'; ctx.fillRect(-12,-1,5,2); ctx.fillStyle='#c0392b'; ctx.fillRect(-7,-3,13,6); ctx.fillStyle='#e8e8e8'; ctx.fillRect(4,-3,4,6); ctx.restore(); }
else if(s.kind==='knife'){ ctx.save(); ctx.translate(s.x,s.y); ctx.rotate(s.spin||0); ctx.fillStyle='#d8dde3'; ctx.fillRect(-6,-1,12,2); ctx.fillStyle='#8a5a2b'; ctx.fillRect(-6,-1,3,2); ctx.restore(); }
else { ctx.strokeStyle=s.col||'#8fe9ff'; ctx.lineWidth=3; ctx.beginPath(); ctx.moveTo(s.x,s.y); ctx.lineTo(s.x-Math.cos(a)*15,s.y-Math.sin(a)*15); ctx.stroke(); ctx.fillStyle='#fff'; ctx.fillRect(s.x-1.5,s.y-1.5,3,3); } }
for(const t of tanks) drawTank(t);
for(const a of armors) drawArmor(a);
for(const b of heliBullets){ ctx.strokeStyle='#ffe066'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(b.x,b.y); ctx.lineTo(b.x-b.vx*0.018,b.y-b.vy*0.018); ctx.stroke(); }
for(const h of helis) drawHeli(h);
for(const bm of bombs){ ctx.save(); ctx.translate(bm.x,bm.y); ctx.rotate(Math.sin(bm.spin)*0.3);
ctx.fillStyle='#2b333c'; ctx.beginPath(); ctx.ellipse(0,0,5,8,0,0,7); ctx.fill();
ctx.fillStyle='#4a545f'; ctx.fillRect(-5,-9,10,3);
ctx.fillStyle='#ff5a2a'; ctx.fillRect(-1,-13,2,4); ctx.restore(); }
for(const m of missiles){ const a=Math.atan2(m.vy,m.vx); ctx.save(); ctx.translate(m.x,m.y); ctx.rotate(a);
ctx.fillStyle='#ffb14d'; ctx.fillRect(-12,-1,5,2); // exhaust
ctx.fillStyle='#c0392b'; ctx.fillRect(-7,-3,14,6); ctx.fillStyle='#e8e8e8'; ctx.fillRect(4,-3,5,6); // body + tip
ctx.fillStyle='#8a1f18'; ctx.fillRect(-7,-4,3,8); ctx.restore(); }
if(inHeli){ const f=inHeli.dir||1, mx=inHeli.x+f*18, my=inHeli.y+14, dyg=GROUND-my; // predicted impact on the ground
const ix=mx+dyg*(f*0.62/0.78);
ctx.fillStyle='rgba(255,58,58,.25)'; ctx.beginPath(); ctx.arc(ix,GROUND-4,9,0,7); ctx.fill();
ctx.strokeStyle='#ff3a3a'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(ix,GROUND-4,9,0,7); ctx.stroke();
ctx.fillStyle='#ff3a3a'; ctx.fillRect(ix-1,GROUND-11,2,6); ctx.fillRect(ix-4,GROUND-5,8,2); }
for(const s of tankShells){ ctx.fillStyle='#ffd24a'; ctx.fillRect(s.x-5,s.y-3,10,6);
ctx.fillStyle='#ff7a2a'; ctx.fillRect(s.x-(s.dir>0?8:-4),s.y-2,5,4);
ctx.fillStyle='#fff2b0'; ctx.fillRect(s.x-1,s.y-1,2,2); }
if(!inTank && !inHeli) drawPlayer();
for(const bm of beams){ const a=bm.life/bm.max;
ctx.globalAlpha=a*0.6; ctx.strokeStyle='#ff3a3a'; ctx.lineWidth=7; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(bm.x1,bm.y1); ctx.lineTo(bm.x2,bm.y2); ctx.stroke();
ctx.globalAlpha=a; ctx.strokeStyle='#fff'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(bm.x1,bm.y1); ctx.lineTo(bm.x2,bm.y2); ctx.stroke();
ctx.globalAlpha=1; }
for(const p of parts){ ctx.globalAlpha=Math.max(0,p.life/p.max); ctx.fillStyle=p.col;
ctx.fillRect(p.x-p.sz,p.y-p.sz,p.sz*2,p.sz*2);} ctx.globalAlpha=1;
ctx.textAlign='center';
for(const f of floats){ ctx.globalAlpha=1-f.t; ctx.fillStyle=f.col; ctx.font='20px "Press Start 2P",monospace';
ctx.fillText(f.txt,f.x,f.y);} ctx.globalAlpha=1;
ctx.restore();
// night darkening vignette
if(dn>.05){ ctx.fillStyle='rgba(6,10,26,'+(dn*0.32)+')'; ctx.fillRect(0,0,W,H); }
drawControls();
if(nearBaseDoor()){ const bp=(Math.sin(performance.now()/300)+1)/2;
ctx.fillStyle='rgba(20,26,36,.9)'; ctx.fillRect(W/2-70,62,140,36);
ctx.strokeStyle='rgba(55,225,255,'+(0.5+bp*0.5)+')'; ctx.lineWidth=2; ctx.strokeRect(W/2-70,62,140,36);
ctx.fillStyle='#37e1ff'; ctx.font='11px "Press Start 2P",monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('ENTER BASE',W/2,81); ctx.textBaseline='alphabetic'; }
if(armoryOpen) drawArmoryPanel();
if(!basePlaced){ promptBox(sel==='base'?'TAP TO SET DOWN YOUR BASE':'SELECT THE BASE & PLACE IT'); }
else if(inTank){ promptBox('EXIT to get out FIRE to shoot'); }
else if(inHeli){ promptBox('FLY with arrows • FIRE • EXIT'); }
else { let shown=false;
for(const cr of crates){ if(Math.abs(cr.x-player.x)<70){ promptBox('TAP ARMORY TO OPEN ('+WPN[player.weapon||'blaster'].name+')'); shown=true; break; } }
if(!shown && phase==='day' && hurtLimbs().length){ promptBox('TAP THE BANDAGE TO PATCH YOUR '+LIMBS[hurtLimbs()[0]]); shown=true; }
if(!shown) for(const t of tanks){ if(Math.abs(t.x-player.x)<80){ promptBox('TAP THE TANK TO GET IN'); shown=true; break; } }
if(!shown && !player.armored) for(const a of armors){ if(Math.abs(a.x-player.x)<80){ promptBox('TAP THE ARMOR TO PUT IT ON'); shown=true; break; } }
if(!shown) for(const h of helis){ if(Math.abs(h.x-player.x)<70){ promptBox('TAP THE HELI TO FLY IT'); break; } } }
ctx.restore();
}
function grnd(c){ const s=Math.sin(c*127.1)*43758.5453; return s-Math.floor(s); } // stable 0..1 per column
function bgHash(i){ const s=Math.sin(i*57.31)*43758.5453; return s-Math.floor(s); }
function drawBgSoldier(x,gy,t,seed){
ctx.fillStyle = dn>.4 ? '#26313f' : '#2c3a4c';
ctx.fillRect(x,gy-14,5,14); ctx.fillRect(x-1,gy-18,7,5); // body + helmet
ctx.fillRect(x+4,gy-13,9,2); // rifle
ctx.fillRect(x,gy-4,2,4); ctx.fillRect(x+3,gy-4,2,4); // legs
ctx.fillStyle = dn>.4 ? '#37e1ff' : '#5fd0ff'; ctx.fillRect(x+4,gy-16,2,2); // visor
}
function drawBgZombie(x,gy,t,seed){
const bob=Math.sin(t*4+seed)*1;
ctx.fillStyle = dn>.4 ? '#1f3326' : '#2b4a33';
ctx.fillRect(x,gy-13+bob,5,13); ctx.fillRect(x-1,gy-17+bob,6,5); // body + head
ctx.fillRect(x-4,gy-12+bob,4,2); // arm reaching
ctx.fillRect(x,gy-3,2,3); ctx.fillRect(x+3,gy-3,2,3);
ctx.fillStyle='#ff4a3d'; ctx.fillRect(x-1,gy-15+bob,1,1); // red eye
}
const SKY_TOP=GROUND-210, SKY_OFFS=260;
let skyCache={day:null,night:null};
function bakeSkyline(){
const w=Math.ceil(CAM_MAX*0.22+W)+SKY_OFFS+160, h=GROUND-SKY_TOP+10;
for(const mode of ['day','night']){
const oc=document.createElement('canvas'); oc.width=w; oc.height=h;
const g=oc.getContext('2d'); g.translate(SKY_OFFS,-SKY_TOP);
const isNight=(mode==='night');
const body=isNight?'#0f1622':'#1b2634', dark=isNight?'#080d15':'#101823';
const step=112, i1=Math.ceil((w-SKY_OFFS)/step)+2;
for(let i=-3;i<i1;i++){
const bx=i*step+bgHash(i)*30, bw=48+bgHash(i+5)*40, bh=60+bgHash(i*2+1)*110;
for(let sx=bx; sx<bx+bw; sx+=6){
let sh=bh-bgHash(sx*0.7+i)*30;
if(bgHash(sx*1.9+i)>0.86) sh*=0.35+0.3*bgHash(sx+i);
const stop=GROUND-sh;
g.fillStyle=body; g.fillRect(sx,stop,5,sh);
if(bgHash(sx*3.1+i)>0.82){ g.fillStyle=dark; g.fillRect(sx+2,stop-9,1,11); }
}
for(let k=0;k<3;k++) if(bgHash(i*7+k+1)>0.55){
const hx=bx+4+bgHash(i+k*3)*(bw-16), hy=GROUND-bh*0.28-bgHash(i*2+k)*bh*0.5,
hw=6+bgHash(i+k)*9, hh=6+bgHash(i+k*2)*9;
g.fillStyle=dark; g.fillRect(hx,hy,hw,hh); }
g.fillStyle=dark; g.fillRect(bx+bw*0.32,GROUND-bh*0.55,3,bh*0.45);
for(let wy=GROUND-bh*0.6; wy<GROUND-8; wy+=15) for(let wx=bx+5; wx<bx+bw-6; wx+=12){
if(bgHash(wx*1.3+wy)>0.7){ g.fillStyle=bgHash(wx+wy)>.5?'#37e1ff':'#ffb24a';
g.globalAlpha=0.35+0.4*bgHash(wx*2+wy); g.fillRect(wx,wy,3,4); g.globalAlpha=1; } }
g.fillStyle=body; g.fillRect(bx-5,GROUND-7,bw+10,7);
g.fillStyle=dark; g.fillRect(bx-7,GROUND-4,7,4); g.fillRect(bx+bw,GROUND-5,7,5);
}
skyCache[mode]=oc;
}
}
function drawBackdrop(){
const t=performance.now()/1000;
if(!skyCache.day) bakeSkyline();
ctx.drawImage(dn>.4?skyCache.night:skyCache.day, Math.round(-cam*0.22-SKY_OFFS), SKY_TOP);
// mid-distance battle silhouettes: soldiers vs zombies
ctx.save(); ctx.translate(-cam*0.4,0); ctx.globalAlpha=0.68;
const gstep=155, g0=Math.floor(cam*0.4/gstep)-1, g1=Math.ceil((cam*0.4+W)/gstep)+1;
for(let i=g0;i<g1;i++){ const gx=i*gstep+bgHash(i+11)*70;
drawBgSoldier(gx,GROUND,t,i); drawBgZombie(gx+27,GROUND,t,i);
if(bgHash(i+3)>.5) drawBgZombie(gx+41,GROUND,t,i+7);
if((Math.sin(t*3+i)+1)/2>0.6){ ctx.strokeStyle='#ff5a4a'; ctx.lineWidth=2; // laser fire
ctx.beginPath(); ctx.moveTo(gx+8,GROUND-10); ctx.lineTo(gx+30,GROUND-9); ctx.stroke(); }
const ex=(Math.sin(t*1.3+i*2)+1)/2; // explosion flash
if(ex>0.9){ ctx.fillStyle='rgba(255,140,40,'+((1-(ex-0.9)/0.1)*0.5)+')';
ctx.beginPath(); ctx.arc(gx+52,GROUND-6,8+(ex-0.9)*130,0,7); ctx.fill(); }
}
ctx.globalAlpha=1; ctx.restore();
}
let smogG={day:null,night:null};
function drawSmoke(){
const t=performance.now()/1000, key=dn>.4?'night':'day';
if(!smogG[key]){ const g=ctx.createLinearGradient(0,GROUND-280,0,GROUND);
g.addColorStop(0,'rgba(80,66,54,0)');
g.addColorStop(1, key==='night'?'rgba(36,30,26,0.55)':'rgba(92,76,60,0.42)');
smogG[key]=g; }
ctx.fillStyle=smogG[key]; ctx.fillRect(0,0,W,GROUND);
// smoke plumes rising from the ruined horizon
ctx.save(); ctx.translate(-cam*0.3,0);
const step=260, p0=Math.floor(cam*0.3/step)-1, p1=Math.ceil((cam*0.3+W)/step)+1;
const smokeCol=dn>.4?'50,44,40':'110,96,82';
for(let i=p0;i<p1;i++){ const px=i*step+bgHash(i+21)*170, drift=Math.sin(t*0.5+i)*14;
for(let k=0;k<6;k++){ const ky=GROUND-8-k*24, wob=Math.sin(t*0.7+i+k*0.6)*(6+k*3)+drift*(k/6);
ctx.fillStyle='rgba('+smokeCol+','+(0.22-k*0.03)+')';
ctx.beginPath(); ctx.arc(px+wob,ky,8+k*4,0,7); ctx.fill(); }
}
ctx.restore();
}
let groundCache=null;
function bakeGround(){
const oc=document.createElement('canvas'); oc.width=WORLD_W; oc.height=H-GROUND;
const g=oc.getContext('2d'); g.translate(0,-GROUND);
for(let c=0;c<COLS;c++){ const x=c*B;
const r1=grnd(c), r2=grnd(c+37), r3=grnd(c*2+9);
g.fillStyle=(c%2)?'#3f342b':'#392f27'; g.fillRect(x,GROUND,B,16); // charred surface
g.fillStyle='#2a2320'; g.fillRect(x,GROUND,B,4); // burnt crust
g.fillStyle='#5b4d42'; g.fillRect(x+3+r1*(B-12),GROUND+6,4,3); // ash
g.fillStyle='#1e1916'; g.fillRect(x+2+r2*(B-10),GROUND+9,5,3); // scorch
g.fillStyle=(c%2)?'#4a3a2c':'#443528'; g.fillRect(x,GROUND+16,B,60); // torn dirt
g.strokeStyle='rgba(0,0,0,.35)'; g.lineWidth=2;
g.beginPath(); g.moveTo(x+4+r1*10,GROUND+18); g.lineTo(x+14+r2*16,GROUND+52); g.stroke();
if(r1>.55){ g.fillStyle='#7a7a7a'; g.fillRect(x+5+r2*16,GROUND+24+r3*18,11,8);
g.fillStyle='#5c5c5c'; g.fillRect(x+5+r2*16,GROUND+30+r3*18,11,2); }
if(r2>.6){ g.fillStyle='#8a8a8a'; g.fillRect(x+B-18+r1*6,GROUND+40+r1*16,8,6); }
g.fillStyle='#2e241b'; g.fillRect(x+8,GROUND+58,5,5); g.fillRect(x+B-16,GROUND+66,4,4);
g.fillStyle=(c%2)?'#3f3f3f':'#3a3a3a'; g.fillRect(x,GROUND+76,B,H-GROUND-76); // bedrock
g.fillStyle='#2b2b2b'; g.fillRect(x+6,GROUND+92,8,5); g.fillRect(x+B-18,GROUND+108,8,5);
g.strokeStyle='rgba(0,0,0,.28)'; g.beginPath(); g.moveTo(x+r3*B,GROUND+80); g.lineTo(x+r1*B,H); g.stroke();
g.strokeStyle='rgba(0,0,0,.12)'; g.strokeRect(x,GROUND,B,H-GROUND);
if(r3>.72){ g.fillStyle='#231c17'; g.beginPath(); g.ellipse(x+B/2,GROUND+3,B*0.42,7,0,0,7); g.fill(); }
}
groundCache=oc;
}
function drawGround(){
if(!groundCache) bakeGround();
ctx.drawImage(groundCache,0,GROUND); // whole terrain in one blit
const c0=Math.floor(cam/B)-1, c1=Math.ceil((cam+W)/B)+1;
for(let c=Math.max(0,c0);c<Math.min(COLS,c1);c++){ if(!dug[c])continue;
const x=c*B, floor=GROUND+76; // pits drawn over the baked ground
ctx.fillStyle='#0d0b0a'; ctx.fillRect(x,GROUND,B,floor-GROUND);
ctx.fillStyle='#1a1512'; ctx.fillRect(x,GROUND+4,4,floor-GROUND-4); ctx.fillRect(x+B-4,GROUND+4,4,floor-GROUND-4);
ctx.fillStyle='#2a2320'; ctx.fillRect(x,GROUND,B,4);
ctx.fillStyle='#3f342b'; ctx.fillRect(x-2,GROUND-3,7,6); ctx.fillRect(x+B-5,GROUND-2,7,5);
ctx.fillStyle=(c%2)?'#3f3f3f':'#3a3a3a'; ctx.fillRect(x,floor,B,H-floor);
ctx.fillStyle='#4a4038'; ctx.fillRect(x,floor,B,3);
ctx.fillStyle='#2b2b2b'; ctx.fillRect(x+6,floor+14,8,5); ctx.fillRect(x+B-18,floor+30,8,5);
}
}
function drawPark(){ const px=parkX, pw=66, topY=GROUND-30;
ctx.fillStyle='#2b333c'; ctx.fillRect(px-pw/2,GROUND-6,pw,8); // metal pad
ctx.fillStyle='#3a424c'; ctx.fillRect(px-pw/2,GROUND-6,pw,3);
ctx.fillStyle='#f4c020'; ctx.fillRect(px-pw/2,GROUND-8,pw,3); // hazard border
ctx.fillStyle='#1c2229'; for(let sx=px-pw/2;sx<px+pw/2;sx+=9) ctx.fillRect(sx,GROUND-8,4,3);
ctx.strokeStyle='rgba(55,225,255,.55)'; ctx.lineWidth=2; // corner brackets = parking bay
ctx.beginPath();
ctx.moveTo(px-pw/2,topY+9); ctx.lineTo(px-pw/2,topY); ctx.lineTo(px-pw/2+9,topY);
ctx.moveTo(px+pw/2,topY+9); ctx.lineTo(px+pw/2,topY); ctx.lineTo(px+pw/2-9,topY);
ctx.stroke();
ctx.fillStyle='rgba(55,225,255,.5)'; ctx.font='bold 11px "Press Start 2P",monospace'; ctx.textAlign='center';
ctx.fillText('P',px,topY+2); }
function drawHouse(rev){ rev = rev===undefined?1:rev;
const x=HOUSE_L, w=HOUSE_R-HOUSE_L, top=GROUND-B*2, h=GROUND-top, cx=x+w/2;
const t=performance.now()/1000, pl=(Math.sin(t*3)+1)/2;
drawPark();
const anim = rev<1;
if(anim){ const e=1-Math.pow(1-rev,3); // ease-out
const sx=(B/w)+(1-B/w)*e, sy=(B/h)+(1-B/h)*e; // unfold outward from a single block
ctx.save(); ctx.translate(cx,GROUND); ctx.scale(sx,sy); ctx.translate(-cx,-GROUND); }
// main body: metal plating
ctx.fillStyle='#39424c'; ctx.fillRect(x,top,w,h);
ctx.fillStyle='#2c333b'; for(let yy=top+16;yy<GROUND;yy+=16) ctx.fillRect(x,yy,w,2); // panel seams
ctx.fillStyle='#2b333c'; ctx.fillRect(x,top,7,h); ctx.fillRect(x+w-7,top,7,h); // support pylons
ctx.fillStyle='#4a545f'; ctx.fillRect(x+2,top,2,h); ctx.fillRect(x+w-5,top,2,h);
ctx.fillStyle='#5a6672'; ctx.fillRect(x,top,w,3); // top bevel
// roof cap + hazard ledge
ctx.fillStyle='#2b333c'; ctx.fillRect(x-8,top-10,w+16,12);
ctx.fillStyle='#4a545f'; ctx.fillRect(x-8,top-10,w+16,3);
ctx.fillStyle='#f4c020'; ctx.fillRect(x-8,top-2,w+16,3);
ctx.fillStyle='#1c2229'; for(let sx=x-8;sx<x+w+8;sx+=10) ctx.fillRect(sx,top-2,5,3);
// rooftop beacon
ctx.fillStyle='#2b333c'; ctx.fillRect(cx-6,top-22,12,12);
ctx.fillStyle='#37e1ff'; ctx.fillRect(cx-3,top-30,6,10);
ctx.fillStyle='rgba(55,225,255,'+(0.4+pl*0.5)+')'; ctx.fillRect(cx-5,top-34,10,8);
// radar dish (sweeps back and forth)
const rd=Math.sin(t*2)*6;
ctx.strokeStyle='#8a97a4'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(x+15,top-10); ctx.lineTo(x+15+rd,top-20); ctx.stroke();
ctx.fillStyle='#c8d2dc'; ctx.fillRect(x+13+rd,top-23,6,4);
// energy viewports
const glow = dn>.4 ? '#ffd24a' : '#37e1ff';
const win=(wx,wy)=>{
ctx.fillStyle='rgba(55,225,255,'+(0.2+pl*0.3)+')'; ctx.fillRect(wx-2,wy-2,20,16);
ctx.fillStyle=glow; ctx.fillRect(wx,wy,16,12);
ctx.strokeStyle='#1c2229'; ctx.lineWidth=2; ctx.strokeRect(wx,wy,16,12);
};
win(x+12,top+14); win(x+w-28,top+14); // top row
win(x+12,top+34); win(x+w-28,top+34); // second row
// blast-door entrance
const dtp=GROUND-46, dh=46, dw=30, dx=cx-dw/2;
ctx.fillStyle='#1c2229'; ctx.fillRect(dx-2,dtp,dw+4,dh);
ctx.fillStyle='#6b7684'; ctx.fillRect(dx,dtp,dw/2-1,dh); ctx.fillRect(cx+1,dtp,dw/2-1,dh);
ctx.fillStyle='#8492a2'; ctx.fillRect(dx,dtp,dw,3);
ctx.fillStyle='#4b545f'; for(let yy=dtp+11;yy<GROUND-4;yy+=12) ctx.fillRect(dx,yy,dw,2);
ctx.fillStyle='rgba(55,225,255,'+(0.35+pl*0.4)+')'; ctx.fillRect(cx-3,dtp,6,dh);
ctx.fillStyle='#37e1ff'; ctx.fillRect(cx-1,dtp,2,dh);
ctx.fillStyle='#f4c020'; ctx.fillRect(dx,dtp-5,dw,4);
ctx.fillStyle='#1c2229'; for(let sx=dx;sx<dx+dw;sx+=8) ctx.fillRect(sx,dtp-5,4,4);
// faint energy-shield shimmer over the base
ctx.globalAlpha=0.12+pl*0.08; ctx.strokeStyle='#37e1ff'; ctx.lineWidth=3;
ctx.beginPath(); ctx.arc(cx,GROUND,h*1.15,Math.PI*1.16,Math.PI*1.84); ctx.stroke();
ctx.globalAlpha=1;
if(anim) ctx.restore();
}
function drawPlacement(b){
const it=ITEMS[b.k], x=b.c*B, ytop=GROUND-(b.l+1)*B;
if(it.type==='ground'){
if(b.k==='spikes'){ const pl=(Math.sin(b.anim*8)+1)/2, h=16+pl*4;
ctx.fillStyle='#2b333c'; ctx.fillRect(x,GROUND-8,B,8); // metal emitter plate
ctx.fillStyle='#3f4a56'; ctx.fillRect(x,GROUND-8,B,2);
ctx.fillStyle='#1b2129'; ctx.fillRect(x,GROUND-2,B,2);
ctx.fillStyle='rgba(55,225,255,'+(0.3+pl*0.5)+')'; // emitter lights
for(let i=0;i<5;i++){ const px=x+5+i*(B-10)/4; ctx.fillRect(px-1,GROUND-7,3,3); }
for(let i=0;i<5;i++){ const px=x+5+i*(B-10)/4;
ctx.fillStyle='rgba(55,225,255,'+(0.25+pl*0.35)+')'; // glow
ctx.beginPath(); ctx.moveTo(px-4,GROUND-8); ctx.lineTo(px+1,GROUND-8-h-3); ctx.lineTo(px+6,GROUND-8); ctx.fill();
ctx.fillStyle='#37e1ff'; // plasma blade
ctx.beginPath(); ctx.moveTo(px-2,GROUND-8); ctx.lineTo(px+1,GROUND-8-h); ctx.lineTo(px+4,GROUND-8); ctx.fill();
ctx.fillStyle='#eaffff'; // white-hot tip
ctx.beginPath(); ctx.moveTo(px,GROUND-8-h+5); ctx.lineTo(px+1,GROUND-8-h); ctx.lineTo(px+2,GROUND-8-h+5); ctx.fill(); }
}
else if(b.k==='lava'){ const g=(Math.sin(b.anim*6)+1)/2, g2=(Math.sin(b.anim*9+1.5)+1)/2;
ctx.fillStyle='#2b333c'; ctx.fillRect(x,GROUND-12,B,12); // metal rim
ctx.fillStyle='#3f4a56'; ctx.fillRect(x,GROUND-12,B,2);
ctx.fillStyle='#c0330a'; ctx.fillRect(x+3,GROUND-10,B-6,8); // molten core
ctx.fillStyle=g>.5?'#ff7a1a':'#ff5a0a'; ctx.fillRect(x+3,GROUND-9,B-6,5);
ctx.fillStyle='#ffb02a'; ctx.fillRect(x+4,GROUND-8,B-8,3);
ctx.fillStyle='#ffe066'; ctx.fillRect(x+5+g*(B-14),GROUND-8,5,2); // moving hotspots
ctx.fillStyle='#fff2b0'; ctx.fillRect(x+6+g2*(B-16),GROUND-7,3,1);
ctx.fillStyle='rgba(255,120,30,'+(0.15+g*0.2)+')'; ctx.fillRect(x+2,GROUND-16,B-4,6); // heat glow
ctx.fillStyle='#f4c020'; ctx.fillRect(x+1,GROUND-12,4,2); ctx.fillRect(x+B-5,GROUND-12,4,2); } // hazard ticks
else if(b.k==='web'){ const pl=(Math.sin(b.anim*6)+1)/2;
const yt=GROUND-B+3, yb=GROUND-3, xl=x+3, xr=x+B-3, mx=x+B/2, my=GROUND-B/2;
ctx.fillStyle='rgba(80,140,255,'+(0.1+pl*0.12)+')'; ctx.fillRect(xl,yt,xr-xl,yb-yt); // field
ctx.strokeStyle='rgba(130,175,255,'+(0.55+pl*0.35)+')'; ctx.lineWidth=2; // energy strands
ctx.beginPath(); ctx.rect(xl,yt,xr-xl,yb-yt);
ctx.moveTo(xl,yt); ctx.lineTo(xr,yb); ctx.moveTo(xr,yt); ctx.lineTo(xl,yb);
ctx.moveTo(mx,yt); ctx.lineTo(mx,yb); ctx.moveTo(xl,my); ctx.lineTo(xr,my); ctx.stroke();
ctx.fillStyle='rgba(80,140,255,'+(0.3+pl*0.4)+')'; ctx.fillRect(mx-4,my-4,8,8); // core glow
ctx.fillStyle='#dbe8ff'; ctx.fillRect(mx-2,my-2,4,4);
ctx.fillStyle='#2b333c'; ctx.fillRect(xl-2,yb-4,5,7); ctx.fillRect(xr-3,yb-4,5,7); // emitter posts
ctx.fillStyle='#5a8cff'; ctx.fillRect(xl-1,yb-3,3,2); ctx.fillRect(xr-2,yb-3,3,2); }
}
else if(it.type==='device'){
if(b.k==='laser'){ const g=(Math.sin(b.anim*6)+1)/2; // laser turret
ctx.fillStyle='#2b333c'; ctx.fillRect(x+7,GROUND-16,B-14,16); // base
ctx.fillStyle='#464f5a'; ctx.fillRect(x+11,GROUND-30,B-22,16); // head
ctx.fillStyle='#5b6570'; ctx.fillRect(x+11,GROUND-30,B-22,3);
ctx.fillStyle='#1c2229'; ctx.fillRect(x+B-16,GROUND-26,12,7); // emitter barrel
ctx.fillStyle=g>.5?'#ff5a5a':'#ff2a2a'; ctx.fillRect(x+B-6,GROUND-25,4,5); // glowing tip
ctx.fillStyle='rgba(255,60,60,'+(0.3+g*0.4)+')'; ctx.fillRect(x+B-8,GROUND-27,8,9);
ctx.fillStyle='#ff3a3a'; ctx.fillRect(x+15,GROUND-26,4,4); } // status lens
else { // arrow dispenser
ctx.fillStyle='#4a4a4a'; ctx.fillRect(x+6,GROUND-30,B-12,30);
ctx.fillStyle='#2a2a2a'; ctx.fillRect(x+12,GROUND-24,B-24,18);
ctx.fillStyle='#111'; ctx.fillRect(x+16,GROUND-20,B-32,10);
ctx.fillStyle='#6a6a6a'; ctx.fillRect(x+6,GROUND-30,B-12,3); } }
else if(it.type==='tnt'){ const armed=b.fuse>=0, g=(Math.sin(b.anim*(armed?24:6))+1)/2;
const cxx=x+B/2, cyy=ytop+B*.46;
ctx.fillStyle='#39424c'; ctx.fillRect(x+2,ytop+2,B-4,B-4); // metal casing
ctx.fillStyle='#5a6672'; ctx.fillRect(x+2,ytop+2,B-4,3);
ctx.fillStyle='#20262c'; ctx.fillRect(x+2,ytop+B-5,B-4,3);
ctx.fillStyle='#f4c020'; ctx.fillRect(x+2,ytop+B-13,B-4,7); // hazard stripe
ctx.fillStyle='#1c2229'; for(let sx=x+2;sx<x+B-2;sx+=9) ctx.fillRect(sx,ytop+B-13,5,7);
ctx.fillStyle='rgba(255,80,40,'+(0.3+g*0.4)+')'; ctx.fillRect(cxx-10,cyy-10,20,20); // core glow
ctx.fillStyle=armed?(g>.5?'#ff7a2a':'#ffd24a'):'#c0330a'; ctx.fillRect(cxx-7,cyy-7,14,14);
ctx.fillStyle='#ff5a2a'; ctx.fillRect(cxx-5,cyy-5,10,10);
ctx.fillStyle='#fff2b0'; ctx.fillRect(cxx-2,cyy-2,4,4);
ctx.fillStyle='#8a97a4'; // corner bolts
ctx.fillRect(x+4,ytop+4,3,3); ctx.fillRect(x+B-7,ytop+4,3,3);
ctx.fillRect(x+4,ytop+B-7,3,3); ctx.fillRect(x+B-7,ytop+B-7,3,3);
ctx.fillStyle=armed?(g>.5?'#ff3a3a':'#5a1010'):'#3a3a3a'; ctx.fillRect(cxx-3,ytop-8,6,6); // arming light
if(armed){ ctx.fillStyle='rgba(255,60,60,'+(0.5*g)+')'; ctx.fillRect(cxx-6,ytop-10,12,10); shakeHint(cxx,ytop); } }
else if(it.type==='solid'){
if(b.k==='cactus'){ const pl=(Math.sin(b.anim*5)+1)/2;
ctx.fillStyle='#2b333c'; ctx.fillRect(x+10,ytop,B-20,B); // metal core column
ctx.fillStyle='#3f4a56'; ctx.fillRect(x+12,ytop,4,B); // left highlight
ctx.fillStyle='#1b2129'; ctx.fillRect(x+B-14,ytop,4,B); // right shade
ctx.fillStyle='rgba(110,255,120,'+(0.4+pl*0.5)+')'; ctx.fillRect(x+B/2-3,ytop,6,B); // energy core
ctx.fillStyle='#c6ffb0'; ctx.fillRect(x+B/2-1,ytop,2,B);
ctx.fillStyle='#6eff7a'; // glowing energy blades
for(let yy=ytop+6;yy<ytop+B-4;yy+=12){
ctx.beginPath(); ctx.moveTo(x+10,yy); ctx.lineTo(x-2,yy+3); ctx.lineTo(x+10,yy+6); ctx.fill();
ctx.beginPath(); ctx.moveTo(x+B-10,yy); ctx.lineTo(x+B+2,yy+3); ctx.lineTo(x+B-10,yy+6); ctx.fill(); }
ctx.fillStyle='rgba(110,255,120,'+(0.3+pl*0.4)+')'; // blade glow tips
for(let yy=ytop+6;yy<ytop+B-4;yy+=12){ ctx.fillRect(x-3,yy+1,4,4); ctx.fillRect(x+B-1,yy+1,4,4); }
ctx.fillStyle='#8a97a4'; ctx.fillRect(x+9,ytop+4,3,B-8); ctx.fillRect(x+B-12,ytop+4,3,B-8); } // emitter rails
else { // reinforced futuristic alloy block
const pl=(Math.sin(b.anim*4)+1)/2;
ctx.fillStyle='#3a424c'; ctx.fillRect(x,ytop,B,B); // base metal
ctx.fillStyle='#5a6672'; ctx.fillRect(x,ytop,B,3); ctx.fillRect(x,ytop,3,B); // top/left bevel
ctx.fillStyle='#20262c'; ctx.fillRect(x,ytop+B-3,B,3); ctx.fillRect(x+B-3,ytop,3,B); // bottom/right shade
ctx.fillStyle='#464f5a'; ctx.fillRect(x+6,ytop+6,B-12,B-12); // inner panel
ctx.strokeStyle='#2c333b'; ctx.lineWidth=2; // X reinforcement
ctx.beginPath(); ctx.moveTo(x+7,ytop+7); ctx.lineTo(x+B-7,ytop+B-7);
ctx.moveTo(x+B-7,ytop+7); ctx.lineTo(x+7,ytop+B-7); ctx.stroke();
ctx.fillStyle='#8a97a4'; // corner bolts
ctx.fillRect(x+4,ytop+4,4,4); ctx.fillRect(x+B-8,ytop+4,4,4);
ctx.fillRect(x+4,ytop+B-8,4,4); ctx.fillRect(x+B-8,ytop+B-8,4,4);
const cxx=x+B/2, cyy=ytop+B/2; // glowing core
ctx.fillStyle='rgba(55,225,255,'+(0.22+pl*0.4)+')'; ctx.fillRect(cxx-8,cyy-8,16,16);
ctx.fillStyle='#37e1ff'; ctx.fillRect(cxx-4,cyy-4,8,8);
ctx.fillStyle='#eaffff'; ctx.fillRect(cxx-2,cyy-2,4,4); }
if(b.max){ hpbar(x+B/2, ytop-6, B*.8, b.hp/b.max, '#7fbf3f'); }
}
else if(it.type==='door' && b.k==='vault'){ const dtop=GROUND-2*B, hgt=2*B, cx=x+B/2, cyD=dtop+hgt/2, pulse=(Math.sin(b.anim*4)+1)/2;
const p=b.openT||0, hw=16, hh=hgt/2+(hw-hgt/2)*p; // half height: hgt/2 closed -> hw open
const topB=dtop+hh, botT=dtop+hgt-hh;
ctx.fillStyle='#181d24'; ctx.fillRect(x,dtop,6,hgt); ctx.fillRect(x+B-6,dtop,6,hgt); // frame posts
if(p>0.01){ ctx.fillStyle='rgba(110,255,120,'+(0.16+pulse*0.16)+')'; ctx.fillRect(x+6,topB,B-12,botT-topB); } // passage
const slab=(sy,sh)=>{ ctx.fillStyle='#59636f'; ctx.fillRect(x+5,sy,B-10,sh); // a door half
ctx.fillStyle='#6b7783'; ctx.fillRect(x+5,sy,B-10,3);
ctx.fillStyle='#3f4954'; ctx.fillRect(x+5,sy+sh-3,B-10,3);
ctx.fillStyle='#aab6c2'; for(let by=sy+6; by<sy+sh-4; by+=15){ ctx.fillRect(x+8,by,3,3); ctx.fillRect(x+B-11,by,3,3); } };
slab(dtop,hh); slab(botT,hh); // top + bottom halves
ctx.fillStyle='#f4c020'; ctx.fillRect(x+6,dtop+hgt-8,B-12,4); // hazard strip on base
ctx.fillStyle='#181d24'; for(let sx=x+7;sx<x+B-8;sx+=9) ctx.fillRect(sx,dtop+hgt-8,4,4);
ctx.strokeStyle='#8a97a4'; ctx.lineWidth=3; // valve half-rings
ctx.beginPath(); ctx.arc(cx,topB,11,0,Math.PI); ctx.stroke();
ctx.beginPath(); ctx.arc(cx,botT,11,Math.PI,Math.PI*2); ctx.stroke();
if(p<0.5){ ctx.save(); ctx.globalAlpha=(0.5-p)*2; ctx.translate(cx,cyD); ctx.rotate(b.anim*0.5); // spinning wheel (closed)
ctx.strokeStyle='#6b7783'; ctx.lineWidth=4;
for(let s=0;s<4;s++){ ctx.rotate(Math.PI/2); ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(0,-B*0.29); ctx.stroke(); }
ctx.restore(); ctx.globalAlpha=1; }
ctx.fillStyle='rgba(110,255,120,'+(0.3+pulse*0.4)+')'; ctx.beginPath(); ctx.arc(cx,topB,7,0,7); ctx.fill(); ctx.beginPath(); ctx.arc(cx,botT,7,0,7); ctx.fill();
ctx.fillStyle='#6eff7a'; ctx.beginPath(); ctx.arc(cx,topB,3,0,7); ctx.fill(); ctx.beginPath(); ctx.arc(cx,botT,3,0,7); ctx.fill();
if(p>0.02){ ctx.fillStyle='#6eff7a'; ctx.fillRect(x+6,topB-2,B-12,2); ctx.fillRect(x+6,botT,B-12,2); } // split edge lights
if(b.pass && p<0.3){ ctx.globalAlpha=1-p/0.3; ctx.fillStyle='#0c1420'; ctx.fillRect(cx-8,dtop+8,16,11); // keypad/lock
ctx.fillStyle='#6eff7a'; for(let iy=0;iy<2;iy++)for(let ix=0;ix<3;ix++) ctx.fillRect(cx-6+ix*5,dtop+10+iy*5,3,3); ctx.globalAlpha=1; }
if(b.max&&b.hp<b.max) hpbar(cx,dtop-6,B*.85,b.hp/b.max,'#6eff7a');
}
else if(it.type==='door'){ const dtop=GROUND-2*B, hgt=2*B, cx=x+B/2, pulse=(Math.sin(b.anim*4)+1)/2;
const p=b.openT||0, hwOpen=9, halfW=(B/2-5)+(hwOpen-(B/2-5))*p; // halves slide toward the sides
const inL=x+5+halfW, inR=x+B-5-halfW;
ctx.fillStyle='#1c2229'; ctx.fillRect(x,dtop,5,hgt); ctx.fillRect(x+B-5,dtop,5,hgt); // frame posts
ctx.fillStyle='#39424c'; ctx.fillRect(x+3,dtop,2,hgt); ctx.fillRect(x+B-5,dtop,2,hgt);
if(inR>inL+1){ ctx.fillStyle='rgba(55,225,255,'+(0.12+pulse*0.16)+')'; ctx.fillRect(inL,dtop,inR-inL,hgt); // open passage
ctx.fillStyle='#37e1ff'; ctx.fillRect(inL-1,dtop,2,hgt); ctx.fillRect(inR-1,dtop,2,hgt); }
const panel=(px,pw)=>{
ctx.fillStyle='#6b7684'; ctx.fillRect(px,dtop,pw,hgt);
ctx.fillStyle='#8492a2'; ctx.fillRect(px,dtop,pw,4);
ctx.fillStyle='#505a66'; ctx.fillRect(px,dtop+hgt-4,pw,4);
ctx.fillStyle='#4b545f'; for(let yy=dtop+15;yy<dtop+hgt-8;yy+=17) ctx.fillRect(px,yy,pw,2);
ctx.fillStyle='#aab6c2';
ctx.fillRect(px+2,dtop+4,3,3); ctx.fillRect(px+pw-5,dtop+4,3,3);
ctx.fillRect(px+2,dtop+hgt-7,3,3); ctx.fillRect(px+pw-5,dtop+hgt-7,3,3); };
panel(x+5,halfW); panel(inR,halfW);
ctx.fillStyle='#f4c020'; ctx.fillRect(x+5,dtop+hgt-19,halfW,8); ctx.fillRect(inR,dtop+hgt-19,halfW,8); // hazard band
ctx.fillStyle='#1c2229';
for(let sx=x+6;sx<x+5+halfW-2;sx+=10) ctx.fillRect(sx,dtop+hgt-19,4,8);
for(let sx=inR+1;sx<inR+halfW-2;sx+=10) ctx.fillRect(sx,dtop+hgt-19,4,8);
if(p<0.5){ ctx.globalAlpha=(0.5-p)*2; // central energy seam (closing)
ctx.fillStyle='rgba(55,225,255,'+(0.3+pulse*0.4)+')'; ctx.fillRect(cx-4,dtop,8,hgt);
ctx.fillStyle='#37e1ff'; ctx.fillRect(cx-1,dtop,2,hgt);
ctx.fillStyle='#eaffff'; ctx.fillRect(cx-1,dtop+hgt*0.32,2,hgt*0.18); ctx.globalAlpha=1; }
ctx.fillStyle= p>0.5?'#6effa0':'#ff4a4a'; ctx.fillRect(cx-3,dtop+6,6,6); // status light
ctx.fillStyle= p>0.5?'rgba(110,255,160,'+(0.4*pulse)+')':'rgba(255,74,74,'+(0.4*pulse)+')'; ctx.fillRect(cx-6,dtop+3,12,12);
if(b.pass && p<0.4){ ctx.globalAlpha=1-p/0.4; ctx.fillStyle='#0c1420'; ctx.fillRect(cx-8,dtop+hgt*0.5-8,16,16); // keypad
ctx.fillStyle='#37e1ff'; for(let iy=0;iy<3;iy++)for(let ix=0;ix<3;ix++) ctx.fillRect(cx-6+ix*5,dtop+hgt*0.5-6+iy*5,3,3); ctx.globalAlpha=1; }
if(b.max&&b.hp<b.max) hpbar(cx,dtop-6,B*.85,b.hp/b.max,'#37e1ff');
}
if((it.type==='ground'||it.type==='device') && b.max && b.hp<b.max){
hpbar(x+B/2, GROUND-34, B*.8, b.hp/b.max, '#e0b24d'); }
}
function shakeHint(x,y){ ctx.globalAlpha=.3; ctx.fillStyle='#ff0'; ctx.fillRect(x-2,y-2,4,4); ctx.globalAlpha=1; }
function hpbar(cx,y,w,f,col){ f=clamp(f,0,1); ctx.fillStyle='#111'; ctx.fillRect(cx-w/2-1,y-1,w+2,5);
ctx.fillStyle='#3a1414'; ctx.fillRect(cx-w/2,y,w,3); ctx.fillStyle=col; ctx.fillRect(cx-w/2,y,w*f,3); }
function P(x,y,w,h,c){ ctx.fillStyle=c; ctx.fillRect(x,y,w,h); }
function drawHeli(h){
const x=Math.round(h.x), y=Math.round(h.y), f=h.dir;
ctx.save(); ctx.translate(x,y);
// tail boom (opposite facing)
ctx.fillStyle='#3a424c'; ctx.fillRect(f>0?-30:6,-3,24,5);
ctx.fillStyle='#2b333c'; ctx.fillRect(f>0?-34:26,-7,8,10); // tail fin
const trot=Math.sin(h.anim*30)*7; // tail rotor
ctx.strokeStyle='#8a97a4'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(f>0?-30:30,-2-trot); ctx.lineTo(f>0?-30:30,-2+trot); ctx.stroke();
// body
ctx.fillStyle='#4a545f'; ctx.fillRect(-12,-8,24,14);
ctx.fillStyle='#5a6672'; ctx.fillRect(-12,-8,24,3);
// cockpit window
ctx.fillStyle='#37e1ff'; ctx.fillRect(f>0?4:-10,-6,8,7);
// skids
ctx.fillStyle='#2b333c'; ctx.fillRect(-12,7,24,2); ctx.fillRect(-9,6,2,3); ctx.fillRect(7,6,2,3);
// front machine gun angled diagonally down-forward
ctx.strokeStyle='#1c2229'; ctx.lineWidth=5; ctx.lineCap='round';
const bx=f*8, by=2, tx=f*18, ty=14;
ctx.beginPath(); ctx.moveTo(bx,by); ctx.lineTo(tx,ty); ctx.stroke(); ctx.lineCap='butt';
ctx.fillStyle='#3a424c'; ctx.fillRect(tx-2,ty-2,4,4);
if(h.flash>0){ ctx.fillStyle='#ffe066'; ctx.beginPath(); ctx.arc(tx+f*2,ty+3,4,0,7); ctx.fill();
ctx.fillStyle='#ff9a2a'; ctx.beginPath(); ctx.arc(tx+f*4,ty+4,2,0,7); ctx.fill(); }
// mast + spinning main rotor (motion-blur blades)
ctx.fillStyle='#2b333c'; ctx.fillRect(-2,-12,4,4);
const rb=(Math.sin(h.anim*40)+1)/2;
ctx.fillStyle='rgba(200,210,220,'+(0.5+rb*0.4)+')'; ctx.fillRect(-30,-13,60,2);
ctx.fillStyle='rgba(200,210,220,'+(0.9-rb*0.4)+')'; ctx.fillRect(-16*rb-6,-13,12+32*rb,2);
ctx.restore();
}
function drawArmor(a){ const f=a.face||1, u=2.6, x=Math.round(a.x), y=Math.round(a.y), occ=(inArmor===a);
const sw=a.onGround?Math.sin(a.step||0)*3*u:2*u;
ctx.save(); ctx.translate(x,y);
ctx.globalAlpha=.3; ctx.fillStyle='#000'; ctx.beginPath(); ctx.ellipse(0,1,9*u,2.4*u,0,0,7); ctx.fill(); ctx.globalAlpha=1;
P(-4.6*u-sw*0.15,-7*u,4*u,7*u,'#3a424c'); P(0.6*u+sw*0.15,-7*u,4*u,7*u,'#3a424c'); // legs
P(-4.6*u,-1.8*u,4.2*u,2*u,'#1c2229'); P(0.6*u,-1.8*u,4.2*u,2*u,'#1c2229'); // feet
P(-5.6*u,-15*u,11.2*u,9*u,'#4a545f'); P(-5.6*u,-15*u,11.2*u,2.2*u,'#5a6672'); // torso
P(-2.2*u,-13*u,4.4*u,4.4*u,'#37e1ff'); P(-1*u,-12*u,2*u,2.4*u,'#eaffff'); // chest core
P(-7.6*u,-15.5*u,3*u,4.5*u,'#3a424c'); P(4.6*u,-15.5*u,3*u,4.5*u,'#3a424c'); // shoulders
P(f>0?5*u:-11*u,-13.2*u,6*u,3.6*u,'#2b333c'); P(f>0?10*u:-11*u,-13.4*u,2*u,4*u,'#1c2229'); // arm cannon
P(f>0?11.4*u:-11.4*u,-12.8*u,1.6*u,2.8*u, a.cd>0.3?'#ff7a2a':'#37e1ff'); // muzzle glow
P(-3.1*u,-20.4*u,6.2*u,5.4*u,'#3a424c'); P(-3.1*u,-20.4*u,6.2*u,1.5*u,'#5a6672'); // helmet
P(f>0?-0.6*u:-2.6*u,-18.6*u,3.2*u,1.8*u,'#37e1ff'); // visor
ctx.restore();
hpbar(x,GROUND-58,46,a.hp/a.max,'#7fbf3f');
}
function drawTank(t){
const x=Math.round(t.x), f=t.face, occ=(inTank===t);
ctx.save(); ctx.translate(x,GROUND);
ctx.globalAlpha=.3; ctx.fillStyle='#000'; ctx.fillRect(-34,-2,68,4); ctx.globalAlpha=1; // shadow
ctx.fillStyle='#1c2229'; ctx.fillRect(-32,-13,64,13); // tread
ctx.fillStyle='#3a424c'; for(let wx=-28;wx<30;wx+=9){ ctx.beginPath(); ctx.arc(wx,-6,4,0,7); ctx.fill(); }
ctx.fillStyle='#0f141a'; for(let wx=-31;wx<32;wx+=6) ctx.fillRect(wx,-13,3,3);
ctx.fillStyle='#3a424c'; ctx.fillRect(-30,-27,60,15); // hull
ctx.fillStyle='#4a545f'; ctx.fillRect(-30,-27,60,3);
ctx.fillStyle='#2b333c'; ctx.fillRect(-30,-14,60,2);
ctx.fillStyle='#4a545f'; ctx.fillRect(-14,-39,28,14); // turret
ctx.fillStyle='#5a6672'; ctx.fillRect(-14,-39,28,3);
ctx.fillStyle='#2b333c'; ctx.fillRect(f>0?12:-36,-35,24,6); // barrel
ctx.fillStyle=t.cd>0.3?'#ff7a2a':'#37e1ff'; ctx.fillRect(f>0?34:-36,-35,2,6); // muzzle
if(occ){ ctx.fillStyle='#c98f6a'; ctx.fillRect(-4,-46,8,7); ctx.fillStyle='#37e1ff'; ctx.fillRect(f>0?1:-3,-44,2,2); } // driver head
else { ctx.fillStyle='#37e1ff'; ctx.fillRect(-3,-42,6,4); } // open hatch light
ctx.restore();
hpbar(x,GROUND-50,54,t.hp/t.max,'#7fbf3f');
}
function drawPlayer(){
const f=player.face, u=2.2, px=Math.round(player.x), py=Math.round(player.y);
const flash=player.hurt>0;
const sw = player.onGround ? Math.sin(player.step||0)*3*u : 2*u; // leg swing (spread when jumping)
const skin=flash?'#fff':'#c98f6a', shirt=flash?'#fff':'#2aa7a0', shirtHi=flash?'#fff':'#37c3ba', pants=flash?'#fff':'#3b4a7a';
const inj=player.injuries||{}, fresh=player.wrap>0;
const limbCol=(st,base)=> st===2?'#e8e8e8' : st===1?'#7a2b2b' : base;
const legL=limbCol(inj.lleg,pants), legR=limbCol(inj.rleg,pants);
const armL=limbCol(inj.larm,shirt), armR=limbCol(inj.rarm,shirt);
ctx.save(); ctx.translate(px,py);
ctx.globalAlpha=.28; ctx.fillStyle='#000'; ctx.beginPath(); ctx.ellipse(0,1,7*u,2.2*u,0,0,7); ctx.fill(); ctx.globalAlpha=1; // shadow
P(-3*u,-7*u,2.6*u,7*u,legL); P(0.4*u,-7*u,2.6*u,7*u,legR); // legs
P(-3*u-sw*0.4,-1.4*u,2.8*u,1.6*u,'#222'); P(0.4*u+sw*0.4,-1.4*u,2.8*u,1.6*u,'#222'); // shoes
P(-3.2*u,-13*u,6.4*u,6.4*u,shirt); P(-3.2*u,-13*u,6.4*u,1.4*u,shirtHi); // body
P(-4.6*u,-12.6*u,1.9*u,5*u,armL); P(2.7*u,-12.6*u,1.9*u,5*u,armR); // arms
{ const mark=(st,mx,my)=>{ if(st===1){ P(mx,my,1*u,1*u,'#4a1414'); P(mx,my+1.8*u,1*u,1*u,'#4a1414'); }
else if(st===2){ P(mx-0.6*u,my-0.4*u,2.4*u,0.8*u,'#f2b6b6'); P(mx-0.6*u,my+1.6*u,2.4*u,0.8*u,'#f2b6b6'); } };
mark(inj.larm,-4.4*u,-11.4*u); mark(inj.rarm,3.5*u,-11.4*u);
mark(inj.lleg,-2.6*u,-5.4*u); mark(inj.rleg,1.2*u,-5.4*u); }
P(-4.6*u,-8*u,1.9*u,1.6*u,skin); P(2.7*u,-8*u,1.9*u,1.6*u,skin); // hands
P(-3.4*u,-19.4*u,6.8*u,6.4*u,skin); // head
P(-3.4*u,-19.4*u,6.8*u,1.8*u,'#5a3d28'); P(-3.4*u,-19.4*u,6.8*u,.9*u,'#4a3320'); // hair
if(!flash){ const ex=f>0?0.6:-2.2;
P((ex)*u,-17*u,1.3*u,1.5*u,'#fff'); P((ex+1.4)*u,-17*u,1.3*u,1.5*u,'#fff');
P((ex+0.4)*u,-17*u,0.7*u,1.5*u,'#3a5db0'); P((ex+1.8)*u,-17*u,0.7*u,1.5*u,'#3a5db0');
P(-1.6*u,-15.2*u,3.2*u,1*u,'#7a5238'); }
if(player.armored){ // power-armor plating
P(-4.6*u,-13.6*u,9.2*u,2.6*u,'#4a545f'); P(-4.6*u,-13.6*u,9.2*u,1*u,'#5a6672'); // shoulder yoke
P(-3.7*u,-11.2*u,7.4*u,5.2*u,'#3a4a5c'); P(-3.7*u,-11.2*u,7.4*u,1.2*u,'#4f6478'); // chest plate
P(-1.7*u,-9.8*u,3.4*u,3.4*u,'#37e1ff'); P(-0.6*u,-8.8*u,1.4*u,1.6*u,'#eaffff'); // core
P(-4.4*u,-12.6*u,1.5*u,4.6*u,'#3a4a5c'); P(2.9*u,-12.6*u,1.5*u,4.6*u,'#3a4a5c'); // arm guards
P(-3.7*u,-20*u,7.4*u,2.2*u,'#4a545f'); P(f>0?0.1*u:-2.3*u,-17.2*u,2.2*u,1.6*u,'#37e1ff'); } // helmet + visor
ctx.restore();
}
function drawAlly(al){ const f=al.face||1, u=2.0, x=Math.round(al.x);
const U=UNITS[al.t||'blaster'], acc=U.col, heavy=U.armored, kind=U.shot;
const body=heavy?'#4a545f':'#3a4a5c', bodyHi=heavy?'#5a6672':'#4f6478';
const sw=Math.sin(al.step||0)*3*u;
ctx.save(); ctx.translate(x,GROUND);
ctx.globalAlpha=.28; ctx.fillStyle='#000'; ctx.beginPath(); ctx.ellipse(0,1,7*u,2*u,0,0,7); ctx.fill(); ctx.globalAlpha=1;
P(-3*u-sw*0.3,-7*u,2.6*u,7*u,'#2c3a4c'); P(0.4*u+sw*0.3,-7*u,2.6*u,7*u,'#2c3a4c'); // legs
P(-3*u-sw*0.3,-1.4*u,2.8*u,1.6*u,'#1c2229'); P(0.4*u+sw*0.3,-1.4*u,2.8*u,1.6*u,'#1c2229');
P(heavy?-4*u:-3.4*u,-13*u,heavy?8*u:6.8*u,6.4*u,body); P(heavy?-4*u:-3.4*u,-13*u,heavy?8*u:6.8*u,1.6*u,bodyHi); // torso
P(-1.4*u,-11.4*u,2.8*u,1.4*u,acc); // unit-color badge
P(f>0?2.4*u:-4.4*u,-11.5*u,2*u,4.6*u,body); // arm
// weapon by type
if(kind==='rocket'){ P(f>0?1.5*u:-10*u,-11.2*u,9*u,3.4*u,'#2b333c'); P(f>0?10.5*u:-11*u,-11.6*u,2*u,4.2*u,'#1c2229'); P(f>0?1*u:9*u,-13.4*u,2*u,2*u,'#111'); }
else if(kind==='knife'){ P(f>0?2*u:-6*u,-11*u,4*u,1.6*u,'#d8dde3'); P(f>0?2*u:-3*u,-11*u,1.4*u,1.6*u,'#8a5a2b'); }
else { P(f>0?2*u:-9*u,-10.5*u,7*u,2.2*u,'#1c2229'); P(f>0?9*u:-9*u,-11*u,2*u,3*u,'#111'); } // rifle
if(al.flash>0){ ctx.fillStyle=acc; ctx.fillRect(f>0?11*u:-13*u,-10.8*u,3*u,2.2*u); }
P(-3.2*u,-19*u,6.4*u,6*u,'#c98f6a'); // face
P(-3.6*u,-20.4*u,7.2*u,3*u,'#2c3a4c'); P(-3.6*u,-20.4*u,7.2*u,1*u,acc); // helmet w/ unit stripe
P(f>0?0.2*u:-2.2*u,-17*u,2.2*u,1.6*u,'#37e1ff'); // visor
if(heavy){ P(-4.6*u,-13.4*u,1.6*u,3*u,'#5a6672'); P(3*u,-13.4*u,1.6*u,3*u,'#5a6672'); } // shoulder pads
ctx.restore();
if(al.hp<al.max) hpbar(x,GROUND-46,heavy?36:30,al.hp/al.max,acc);
}
function drawCrate(cr){ const x=cr.x;
ctx.fillStyle='#3a424c'; ctx.fillRect(x-16,GROUND-22,32,22); ctx.fillStyle='#4a545f'; ctx.fillRect(x-16,GROUND-22,32,3);
ctx.fillStyle='#2b333c'; ctx.fillRect(x-16,GROUND-22,3,22); ctx.fillRect(x+13,GROUND-22,3,22);
ctx.fillStyle='#1c2229'; ctx.fillRect(x-16,GROUND-12,32,2); // latch line
ctx.fillStyle='#f4c020'; ctx.fillRect(x-16,GROUND-24,32,2); // hazard trim
ctx.fillStyle='#1c2229'; for(let sx=x-14;sx<x+14;sx+=6)ctx.fillRect(sx,GROUND-24,3,2);
// weapons poking out of the box
ctx.fillStyle='#8fe9ff'; ctx.fillRect(x-13,GROUND-30,11,3); // blaster
ctx.fillStyle='#ff9a2a'; ctx.fillRect(x+2,GROUND-33,13,4); ctx.fillStyle='#c0392b'; ctx.fillRect(x+12,GROUND-34,3,6); // rocket
ctx.fillStyle='#d8dde3'; ctx.fillRect(x-5,GROUND-35,3,11); ctx.fillRect(x,GROUND-34,3,10); // knives
ctx.fillStyle='#37e1ff'; ctx.font='bold 6px "Press Start 2P",monospace'; ctx.textAlign='center'; ctx.fillText('ARMORY',x,GROUND-3);
}
function drawElevator(el){ const x=el.x, w=B*0.5+4;
ctx.fillStyle='#2b333c'; ctx.fillRect(x-w-3,el.top-6,3,GROUND-el.top+6); ctx.fillRect(x+w,el.top-6,3,GROUND-el.top+6); // rails
ctx.fillStyle='rgba(55,225,255,.35)'; ctx.fillRect(x-w-3,el.top-6,3,GROUND-el.top+6); ctx.fillRect(x+w,el.top-6,3,GROUND-el.top+6);
ctx.fillStyle='#3a424c'; ctx.fillRect(x-w-4,el.top-8,2*w+8,6); // top housing
ctx.fillStyle='#5a6672'; ctx.fillRect(x-w-4,el.top-8,2*w+8,2);
ctx.strokeStyle='#4a545f'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(x,el.top-4); ctx.lineTo(x,el.y); ctx.stroke(); // cable
ctx.fillStyle='#4a545f'; ctx.fillRect(x-w,el.y,2*w,9); // platform
ctx.fillStyle='#5a6672'; ctx.fillRect(x-w,el.y,2*w,2);
ctx.fillStyle='#1c2229'; ctx.fillRect(x-w,el.y+9,2*w,2);
ctx.fillStyle='#37e1ff'; for(let sx=x-w+3;sx<x+w-2;sx+=8) ctx.fillRect(sx,el.y+6,4,2); // glowing floor strip
}
function drawZombie(z){
const t=ZT[z.t], u=t.r/10, cy=GROUND+(z.fallY||0);
let cx=Math.round(z.x);
if(z.pit==='stuck') cx+=Math.round(Math.sin(z.struggle*22)*2); // struggle wobble
const bob=Math.sin(z.step*3+z.wob)*2, sw=Math.sin(z.step*3)*3*u, d=z.dir;
const skin=z.hurt>0?'#fff':'#4c7f4c', shirt=z.hurt>0?'#fff':'#2f7d7d', pants='#464673';
ctx.save(); ctx.translate(cx,cy+bob); ctx.scale(d,1); // face travel dir
ctx.globalAlpha=.3; P(-8*u,-2,16*u,3,'#000'); ctx.globalAlpha=1;
P(-4*u,-6*u+0,4*u,6*u,pants); P(0.2*u,-6*u,4*u,6*u,pants);
P(-4*u,-1*u+sw,4*u,2*u,'#333'); P(0.2*u,-1*u-sw,4*u,2*u,'#333');
P(-5*u,-13*u,10*u,8*u,shirt); P(-5*u,-13*u,10*u,2*u,z.hurt>0?'#fff':'#3f9a9a');
// arms forward (in travel dir = +x after scale)
P(4*u,-12*u,5*u,3*u,shirt); P(8*u,-12*u,3*u,3*u,skin);
P(4*u,-9*u,5*u,3*u,shirt); P(8*u,-9*u,3*u,3*u,skin);
P(-5*u,-20*u,10*u,8*u,skin); P(-5*u,-20*u,10*u,2*u,z.hurt>0?'#fff':'#5a9160');
if(z.hurt<=0){ P(-4*u,-18*u,3*u,2.5*u,'#20402a'); P(1*u,-18*u,3*u,2.5*u,'#20402a');
P(-4*u,-19*u,8*u,1*u,'#33513a'); P(-2*u,-14.5*u,4*u,1.5*u,'#20402a'); }
if(z.t==='brute'){ P(-6*u,-21*u,12*u,3*u,'#c8c8c8'); P(-7*u,-13*u,2*u,7*u,'#b0b0b0'); P(5*u,-13*u,2*u,7*u,'#b0b0b0'); }
if(z.t==='missiler'){ P(-2*u,-20*u,12*u,4*u,'#3a3a3a'); P(8*u,-20*u,3*u,4*u,'#1c1c1c'); P(9*u,-19.5*u,2*u,1*u,'#ff5a2a'); P(-3*u,-17*u,3*u,3*u,'#8a1f18'); } // rocket launcher
ctx.restore();
if(z.slowT>0){ ctx.globalAlpha=.45; ctx.fillStyle='#bfeeff'; ctx.fillRect(cx-t.r*.8,GROUND-t.r*2,t.r*1.6,t.r*2); ctx.globalAlpha=1; }
if(!z.pit) hpbar(cx,GROUND-t.r*2-6,t.r*1.7,z.hp/z.max,'#d0342c');
ctx.globalAlpha=1;
}
function promptBox(txt){ const bp=(Math.sin(performance.now()/300)+1)/2;
ctx.fillStyle='rgba(10,14,20,.8)'; ctx.fillRect(W/2-200,18,400,40);
ctx.strokeStyle='rgba(55,225,255,'+(0.5+bp*0.5)+')'; ctx.lineWidth=2; ctx.strokeRect(W/2-200,18,400,40);
ctx.fillStyle='#37e1ff'; ctx.font='12px "Press Start 2P",monospace'; ctx.textAlign='center'; ctx.fillText(txt,W/2,44); }
// ---- base interior ----
function nearBaseDoor(){ return basePlaced && phase==='day' && !inside && !inTank && !inHeli && Math.abs(player.x-HOUSE_CX)<(HOUSE_R-HOUSE_L)/2+18; }
function enterBase(){ inside=true; intSavedX=player.x; player.x=(ROOM_L+ROOM_R)/2; player.y=IFLOOR; player.vx=0; player.vy=0;
sel='carpet'; buildBar(); refreshBar(); banner('INSIDE THE BASE',1.4); blip(500,.1,'square'); }
function exitBase(){ inside=false; player.x=intSavedX; player.y=GROUND; player.vx=0; player.vy=0;
sel='base'; buildBar(); refreshBar(); blip(400,.1,'square'); }
function placeFurniture(x){
const cells=Math.floor((ROOM_R-ROOM_L)/ICELL);
let ci=Math.floor((x-ROOM_L)/ICELL); ci=Math.max(0,Math.min(cells-1,ci));
const cx=ROOM_L+ci*ICELL+ICELL/2;
for(const f of interior){ if(f.t==='idoor' && f.ci===ci){ f.open=!f.open; blip(f.open?620:300,.1,'square'); return; } } // tap a door to toggle
if(sel==='remove'){ const i=interior.findIndex(f=>f.ci===ci); if(i>=0){ interior.splice(i,1); blip(240,.1,'square'); } return; }
if(interior.some(f=>f.ci===ci)){ banner('SPOT TAKEN!',0.9); blip(150,.08,'square'); return; } // one item per cell
interior.push(sel==='idoor'?{t:'idoor',x:cx,ci,open:false,openT:0}:{t:sel,x:cx,ci}); blip(560,.08,'square'); }
function updateInterior(dt){
player.anim+=dt;
dpad.up=dpad.down=dpad.left=dpad.right=0;
for(const dirs of dpadPtrs.values()) for(const d of dirs) dpad[d]=1;
const R=keys.right||dpad.right, L=keys.left||dpad.left;
const tvx=((R?1:0)-(L?1:0))*180;
player.vx += (tvx-player.vx)*Math.min(1,dt*12);
player.x=clamp(player.x+player.vx*dt,ROOM_L+12,ROOM_R-12);
for(const f of interior){
if(f.t==='idoor'){ f.openT=(f.openT||0)+((f.open?1:0)-(f.openT||0))*Math.min(1,dt*10);
if(!f.open || f.openT<0.5){ const hw=14, pw=10;
if(Math.abs(player.x-f.x)<hw+pw){ if(player.vx>0) player.x=f.x-hw-pw; else if(player.vx<0) player.x=f.x+hw+pw; player.vx=0; } } }
else if(f.t==='passage'){ const hw=16, pw=10; // iron blocks are solid
if(Math.abs(player.x-f.x)<hw+pw){ if(player.vx>0) player.x=f.x-hw-pw; else if(player.vx<0) player.x=f.x+hw+pw; player.vx=0; } }
}
player.y=IFLOOR; player.onGround=true;
if(player.vx<-6)player.face=-1; else if(player.vx>6)player.face=1;
if(Math.abs(player.vx)>40) player.step=(player.step||0)+dt*8;
}
function drawFurn(f){ const x=f.x, g=ctx;
if(f.t==='carpet'){ g.fillStyle='#7a2f3a'; g.fillRect(x-34,IFLOOR-3,68,6); g.fillStyle='#a8414f'; g.fillRect(x-34,IFLOOR-3,68,2);
g.fillStyle='#e8c46a'; for(let sx=x-30;sx<x+30;sx+=10)g.fillRect(sx,IFLOOR-2,4,3); return; }
if(f.t==='rug'){ g.fillStyle='#3a6ea5'; g.beginPath(); g.ellipse(x,IFLOOR-1,30,8,0,0,7); g.fill();
g.fillStyle='#2a5480'; g.beginPath(); g.ellipse(x,IFLOOR-1,20,5,0,0,7); g.fill();
g.fillStyle='#e8c46a'; g.beginPath(); g.ellipse(x,IFLOOR-1,10,3,0,0,7); g.fill(); return; }
if(f.t==='bed'){ g.fillStyle='#5a4a6a'; g.fillRect(x-26,IFLOOR-18,52,18); g.fillStyle='#3f3350'; g.fillRect(x-26,IFLOOR-4,52,4);
g.fillStyle='#d8dde8'; g.fillRect(x-26,IFLOOR-24,52,8); g.fillStyle='#8fb0d8'; g.fillRect(x-24,IFLOOR-22,32,5);
g.fillStyle='#fff'; g.fillRect(x+8,IFLOOR-24,14,7); g.fillStyle='#4a3d5a'; g.fillRect(x+22,IFLOOR-32,5,32); return; }
if(f.t==='sofa'){ g.fillStyle='#8a5a4a'; g.fillRect(x-26,IFLOOR-16,52,16);
g.fillStyle='#a06a58'; g.fillRect(x-26,IFLOOR-28,6,28); g.fillRect(x+20,IFLOOR-28,6,28); g.fillRect(x-20,IFLOOR-30,40,14);
g.fillStyle='#b57a66'; g.fillRect(x-18,IFLOOR-16,16,6); g.fillRect(x+2,IFLOOR-16,16,6); return; }
if(f.t==='tv'){ g.fillStyle='#3a2f27'; g.fillRect(x-20,IFLOOR-8,40,8);
g.fillStyle='#111'; g.fillRect(x-22,IFLOOR-40,44,30); g.fillStyle='#1a2634'; g.fillRect(x-19,IFLOOR-37,38,24);
g.globalAlpha=.5; g.fillStyle='#37e1ff'; g.fillRect(x-19,IFLOOR-37,38,24); g.globalAlpha=1;
g.fillStyle='#0d1a26'; g.fillRect(x-14,IFLOOR-33,10,7); return; }
if(f.t==='fridge'){ g.fillStyle='#d8dde3'; g.fillRect(x-14,IFLOOR-52,28,52); g.fillStyle='#eef1f4'; g.fillRect(x-14,IFLOOR-52,28,3);
g.fillStyle='#b8bdc3'; g.fillRect(x-14,IFLOOR-24,28,2); g.fillStyle='#8a8f96'; g.fillRect(x+8,IFLOOR-46,3,14); g.fillRect(x+8,IFLOOR-20,3,14); return; }
if(f.t==='stove'){ g.fillStyle='#2b2f34'; g.fillRect(x-16,IFLOOR-30,32,30); g.fillStyle='#3a3f45'; g.fillRect(x-16,IFLOOR-30,32,6);
g.fillStyle='#111'; g.beginPath(); g.arc(x-8,IFLOOR-27,3,0,7); g.arc(x+8,IFLOOR-27,3,0,7); g.fill();
g.fillStyle='#1a2634'; g.fillRect(x-12,IFLOOR-22,24,16); g.globalAlpha=.55; g.fillStyle='#ff9a2a'; g.fillRect(x-12,IFLOOR-22,24,16); g.globalAlpha=1; return; }
if(f.t==='sink'){ g.fillStyle='#8a6a4a'; g.fillRect(x-18,IFLOOR-22,36,22); g.fillStyle='#cfd8e3'; g.fillRect(x-12,IFLOOR-24,24,8);
g.fillStyle='#8a8f96'; g.fillRect(x-1,IFLOOR-32,2,8); g.fillRect(x-1,IFLOOR-32,8,2); return; }
if(f.t==='toilet'){ g.fillStyle='#e8ebee'; g.fillRect(x-8,IFLOOR-16,16,16); g.beginPath(); g.ellipse(x,IFLOOR-16,11,5,0,0,7); g.fill();
g.fillStyle='#e8ebee'; g.fillRect(x-9,IFLOOR-30,18,14); g.fillStyle='#cfd4d9'; g.fillRect(x-9,IFLOOR-30,18,3); return; }
if(f.t==='bath'){ g.fillStyle='#e8ebee'; g.fillRect(x-26,IFLOOR-16,52,16); g.fillStyle='#cfd4d9'; g.fillRect(x-26,IFLOOR-16,52,3);
g.fillStyle='#bfe0ff'; g.fillRect(x-22,IFLOOR-13,44,7); g.fillStyle='#8a8f96'; g.fillRect(x+22,IFLOOR-24,3,10); return; }
if(f.t==='lamp'){ g.fillStyle='#3a3f45'; g.fillRect(x-1,IFLOOR-44,3,44); g.fillStyle='#2b2f34'; g.fillRect(x-6,IFLOOR-2,12,3);
g.globalAlpha=.4; g.fillStyle='#ffe08a'; g.beginPath(); g.arc(x,IFLOOR-44,16,0,7); g.fill(); g.globalAlpha=1;
g.fillStyle='#ffe08a'; g.beginPath(); g.moveTo(x-10,IFLOOR-44); g.lineTo(x+10,IFLOOR-44); g.lineTo(x+6,IFLOOR-56); g.lineTo(x-6,IFLOOR-56); g.closePath(); g.fill(); return; }
if(f.t==='plant'){ g.fillStyle='#a05a34'; g.fillRect(x-8,IFLOOR-12,16,12); g.fillStyle='#b8683f'; g.fillRect(x-8,IFLOOR-12,16,3);
g.fillStyle='#2f7a3a'; for(const d of [[0,-30,10],[-8,-22,8],[8,-22,8],[-4,-34,7],[5,-33,7]]){ g.beginPath(); g.ellipse(x+d[0],IFLOOR-12+d[1],d[2],d[2]*1.4,0,0,7); g.fill(); } return; }
if(f.t==='desk'){ g.fillStyle='#6b4a2f'; g.fillRect(x-22,IFLOOR-18,44,5); g.fillRect(x-20,IFLOOR-13,4,13); g.fillRect(x+16,IFLOOR-13,4,13);
g.fillStyle='#111'; g.fillRect(x-10,IFLOOR-34,22,16); g.globalAlpha=.5; g.fillStyle='#37e1ff'; g.fillRect(x-8,IFLOOR-32,18,12); g.globalAlpha=1;
g.fillStyle='#2b2f34'; g.fillRect(x-4,IFLOOR-18,8,2); return; }
if(f.t==='painting'){ const py=ITOP+42; g.fillStyle='#6b4a2f'; g.fillRect(x-16,py-14,32,28); g.fillStyle='#c9a56a'; g.fillRect(x-16,py-14,32,3);
g.fillStyle='#4a7ba5'; g.fillRect(x-13,py-11,26,22); g.fillStyle='#6ea05a'; g.fillRect(x-13,py+3,26,8); g.fillStyle='#ffe08a'; g.beginPath(); g.arc(x+7,py-4,4,0,7); g.fill(); return; }
if(f.t==='chair'){ g.fillStyle='#6b4a2f'; g.fillRect(x-10,IFLOOR-16,4,16); g.fillRect(x+6,IFLOOR-16,4,16); g.fillRect(x-10,IFLOOR-30,4,16);
g.fillStyle='#8a5a34'; g.fillRect(x-12,IFLOOR-18,24,5); g.fillRect(x-12,IFLOOR-32,4,16); return; }
if(f.t==='table'){ g.fillStyle='#8a5a34'; g.fillRect(x-22,IFLOOR-22,44,6); g.fillStyle='#6b4a2f'; g.fillRect(x-20,IFLOOR-16,4,16); g.fillRect(x+16,IFLOOR-16,4,16); return; }
if(f.t==='bookshelf'){ g.fillStyle='#5a3d28'; g.fillRect(x-18,IFLOOR-56,36,56); g.fillStyle='#3f2a1a'; g.fillRect(x-18,IFLOOR-56,36,3);
for(let sy=IFLOOR-50;sy<IFLOOR-6;sy+=17){ g.fillStyle='#3f2a1a'; g.fillRect(x-16,sy+13,32,3);
const cols=['#c0392b','#2a7ab0','#4caf50','#e0a83a','#8e44ad'];
for(let bx=x-15,j=0;bx<x+14;bx+=5,j++){ g.fillStyle=cols[(j+Math.floor(sy))%cols.length]; g.fillRect(bx,sy,4,13); } } return; }
if(f.t==='idoor'){ const bt=IFLOOR-58, dh=58, slide=(f.openT||0)*24;
g.fillStyle='#3a2f22'; g.fillRect(x-16,bt-3,32,3); g.fillRect(x-16,bt-3,3,dh+3); g.fillRect(x+13,bt-3,3,dh+3); // frame
g.save(); g.beginPath(); g.rect(x-13,bt,26,dh); g.clip(); // door panel slides into frame
const dx=x-13-slide;
g.fillStyle='#7a5230'; g.fillRect(dx,bt,26,dh); g.fillStyle='#8a5f38'; g.fillRect(dx,bt,26,3);
g.fillStyle='#5f3f22'; g.fillRect(dx+3,bt+5,20,22); g.fillRect(dx+3,bt+30,20,22); // panels
g.fillStyle='#d9b25a'; g.fillRect(dx+20,bt+28,3,4); // handle
g.restore(); return; }
if(f.t==='passage'){ const bt=IFLOOR-48, bw=32;
g.fillStyle='#8a8f96'; g.fillRect(x-16,bt,bw,48); // iron body
g.fillStyle='#a7adb4'; g.fillRect(x-16,bt,bw,4); // top highlight
g.fillStyle='#6b7178'; g.fillRect(x-16,IFLOOR-7,bw,7); // shaded base
g.fillStyle='#5c6167'; g.fillRect(x-16,bt,3,48); g.fillRect(x+13,bt,3,48); // side edges
g.fillStyle='#767c83'; g.fillRect(x-10,bt+17,20,3); g.fillRect(x-10,bt+31,20,3); // panel seams
g.fillStyle='#c9ced3'; for(const ry of [bt+7,IFLOOR-13]) for(const rx of [x-11,x+8]) g.fillRect(rx,ry,3,3); // rivets
return; }
}
function renderInterior(){
const sx=(Math.random()-.5)*shake, sy=(Math.random()-.5)*shake;
ctx.save(); ctx.translate(sx,sy);
// walls
ctx.fillStyle='#2a2f38'; ctx.fillRect(0,0,W,H);
ctx.fillStyle='#39424c'; ctx.fillRect(ROOM_L-24,ITOP-24,ROOM_R-ROOM_L+48,IFLOOR-ITOP+24); // room wall
ctx.fillStyle='#454f5a'; ctx.fillRect(ROOM_L-24,ITOP-24,ROOM_R-ROOM_L+48,10); // ceiling trim
for(let wx=ROOM_L-24;wx<ROOM_R+24;wx+=56){ ctx.fillStyle='#333b44'; ctx.fillRect(wx,ITOP-14,2,IFLOOR-ITOP+14); } // wall panels
// window with the smoggy sky outside
ctx.fillStyle='#1a2634'; ctx.fillRect(ROOM_R-140,ITOP+20,96,64); ctx.strokeStyle='#5a6672'; ctx.lineWidth=4; ctx.strokeRect(ROOM_R-140,ITOP+20,96,64);
ctx.fillStyle='#37e1ff'; ctx.globalAlpha=.25; ctx.fillRect(ROOM_R-140,ITOP+20,96,64); ctx.globalAlpha=1;
ctx.strokeStyle='#5a6672'; ctx.beginPath(); ctx.moveTo(ROOM_R-92,ITOP+20); ctx.lineTo(ROOM_R-92,ITOP+84); ctx.moveTo(ROOM_R-140,ITOP+52); ctx.lineTo(ROOM_R-44,ITOP+52); ctx.stroke();
// wall light
ctx.fillStyle='#ffe08a'; ctx.globalAlpha=.6; ctx.beginPath(); ctx.arc(ROOM_L+70,ITOP+30,26,0,7); ctx.fill(); ctx.globalAlpha=1;
ctx.fillStyle='#cfd8e3'; ctx.fillRect(ROOM_L+64,ITOP+8,12,8);
// floor
ctx.fillStyle='#3a2f27'; ctx.fillRect(0,IFLOOR,W,H-IFLOOR);
ctx.fillStyle='#4a3a2c'; ctx.fillRect(0,IFLOOR,W,6);
ctx.fillStyle='#31281f'; for(let fx=0;fx<W;fx+=40) ctx.fillRect(fx,IFLOOR+6,2,H-IFLOOR);
// placement grid cells
const cells=Math.floor((ROOM_R-ROOM_L)/ICELL);
ctx.strokeStyle='rgba(55,225,255,.14)'; ctx.lineWidth=1;
for(let i=0;i<=cells;i++){ const gx=ROOM_L+i*ICELL; ctx.beginPath(); ctx.moveTo(gx,IFLOOR-2); ctx.lineTo(gx,IFLOOR+9); ctx.stroke(); }
for(const f of interior){ ctx.fillStyle='rgba(55,225,255,.06)'; ctx.fillRect(f.x-ICELL/2+1,IFLOOR-2,ICELL-2,4); } // occupied cell tint
// furniture (carpets first so they sit under things)
for(const f of interior) if(f.t==='carpet') drawFurn(f);
for(const f of interior) if(f.t!=='carpet') drawFurn(f);
drawPlayer();
drawControls();
// EXIT button
ctx.fillStyle='rgba(60,70,80,.85)'; ctx.fillRect(14,60,110,36); ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=2; ctx.strokeRect(14,60,110,36);
ctx.fillStyle='#fff'; ctx.font='11px "Press Start 2P",monospace'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('EXIT',69,79); ctx.textBaseline='alphabetic';
promptBox('DECORATE — TAP TO PLACE • TAP A DOOR TO OPEN/CLOSE');
ctx.restore();
}
// on-screen controls
function drawControls(){
ctx.globalAlpha=.92;
// directional arrow pad
const S=38;
const arrow=(cx,cy,dir,active)=>{
ctx.fillStyle= active?'rgba(55,225,255,.75)':'rgba(20,26,36,.6)';
ctx.fillRect(cx-18,cy-18,36,36);
ctx.strokeStyle='rgba(255,255,255,.4)'; ctx.lineWidth=2; ctx.strokeRect(cx-18,cy-18,36,36);
ctx.fillStyle= active?'#04121a':'rgba(240,245,255,.92)'; ctx.beginPath();
if(dir==='up'){ ctx.moveTo(cx,cy-9); ctx.lineTo(cx-9,cy+6); ctx.lineTo(cx+9,cy+6); }
if(dir==='down'){ ctx.moveTo(cx,cy+9); ctx.lineTo(cx-9,cy-6); ctx.lineTo(cx+9,cy-6); }
if(dir==='left'){ ctx.moveTo(cx-9,cy); ctx.lineTo(cx+6,cy-9); ctx.lineTo(cx+6,cy+9); }
if(dir==='right'){ ctx.moveTo(cx+9,cy); ctx.lineTo(cx-6,cy-9); ctx.lineTo(cx-6,cy+9); }
ctx.fill();
};
arrow(DP.x, DP.y-S, 'up', dpad.up);
arrow(DP.x, DP.y+S, 'down', dpad.down);
arrow(DP.x-S, DP.y, 'left', dpad.left);
arrow(DP.x+S, DP.y, 'right', dpad.right);
ctx.fillStyle='rgba(255,255,255,.6)'; ctx.font='9px "Press Start 2P",monospace'; ctx.textAlign='center';
ctx.fillText(inTank?'DRIVE':inHeli?'FLY':(phase==='night'?'MOVE':'FLY'),DP.x,DP.y+S+30);
// place / fire button
if(inTank||inHeli){
ctx.fillStyle='rgba(200,60,40,.82)'; circle(PB.x,PB.y,PR);
ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(PB.x,PB.y,PR,0,7); ctx.stroke();
ctx.fillStyle='#ffd24a'; ctx.fillRect(PB.x-14,PB.y-9,20,8); ctx.fillStyle='#ff7a2a'; ctx.fillRect(PB.x+6,PB.y-8,8,6);
ctx.fillStyle='#fff'; ctx.font='11px "Press Start 2P",monospace'; ctx.textAlign='center'; ctx.fillText('FIRE',PB.x,PB.y+PR-6);
// EXIT button (top-left)
ctx.fillStyle='rgba(60,70,80,.85)'; ctx.fillRect(14,60,96,36);
ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=2; ctx.strokeRect(14,60,96,36);
ctx.fillStyle='#fff'; ctx.font='11px "Press Start 2P",monospace'; ctx.textBaseline='middle'; ctx.fillText('EXIT',62,79); ctx.textBaseline='alphabetic';
if(inHeli){ const ready=bombCd<=0;
ctx.fillStyle= ready?'rgba(230,150,40,.85)':'rgba(60,55,50,.8)'; circle(BB.x,BB.y,BBr);
ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(BB.x,BB.y,BBr,0,7); ctx.stroke();
if(ready){
ctx.fillStyle='#1c2229'; ctx.beginPath(); ctx.arc(BB.x,BB.y+2,8,0,7); ctx.fill(); // bomb body
ctx.fillStyle='#ff5a2a'; ctx.fillRect(BB.x-1,BB.y-10,2,4); // fuse spark
ctx.fillStyle='#fff'; ctx.font='8px "Press Start 2P",monospace'; ctx.textAlign='center'; ctx.fillText('BOMB',BB.x,BB.y+BBr-3);
} else {
ctx.strokeStyle='#ffb14d'; ctx.lineWidth=3; ctx.beginPath(); // recharge sweep
ctx.arc(BB.x,BB.y,BBr-4,-Math.PI/2,-Math.PI/2+(1-bombCd/BOMB_CD)*Math.PI*2); ctx.stroke();
ctx.fillStyle='#fff'; ctx.font='13px "Press Start 2P",monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(Math.ceil(bombCd), BB.x, BB.y); ctx.textBaseline='alphabetic';
} }
} else {
const locked = (phase==='night' && sel!=='pick' && sel!=='armory');
ctx.fillStyle= locked?'rgba(70,70,78,.7)':sel==='pick'?'rgba(120,60,40,.75)':sel==='armory'?'rgba(40,110,150,.82)':'rgba(60,120,50,.75)'; circle(PB.x,PB.y,PR);
ctx.strokeStyle='rgba(255,255,255,.5)'; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(PB.x,PB.y,PR,0,7); ctx.stroke();
ctx.save(); ctx.translate(PB.x,PB.y-4); ctx.globalAlpha=locked?0.4:1; drawItemIcon(ctx, sel, 34, true); ctx.globalAlpha=1; ctx.restore();
ctx.fillStyle='#fff'; ctx.font='9px "Press Start 2P",monospace'; ctx.textAlign='center';
ctx.fillText(locked?'LOCKED':sel==='pick'?'BREAK':sel==='armory'?(phase==='night'?'FIRE':'CRATE'):'PLACE',PB.x,PB.y+PR-6);
if(phase==='day' && hurtLimbs().length){ const pulse=(Math.sin(performance.now()/260)+1)/2;
ctx.fillStyle='rgba(235,235,235,.92)'; circle(BND.x,BND.y,BNDr);
ctx.strokeStyle='rgba(255,120,120,'+(0.55+pulse*0.45)+')'; ctx.lineWidth=3; ctx.beginPath(); ctx.arc(BND.x,BND.y,BNDr,0,7); ctx.stroke();
ctx.save(); ctx.translate(BND.x,BND.y); ctx.rotate(-0.5); // band-aid
ctx.fillStyle='#f0c08a'; ctx.fillRect(-17,-6,34,12); ctx.fillStyle='#e0a868'; ctx.fillRect(-17,-6,34,3);
ctx.fillStyle='#fff'; ctx.fillRect(-7,-6,14,12);
ctx.fillStyle='#d9b07c'; for(let bx=-5;bx<6;bx+=4)for(let by=-4;by<5;by+=4) ctx.fillRect(bx,by,2,2);
ctx.restore();
ctx.fillStyle='#fff'; ctx.font='8px "Press Start 2P",monospace'; ctx.textAlign='center';
ctx.fillText('BANDAGE ('+hurtLimbs().length+')',BND.x,BND.y+BNDr+12); }
}
ctx.globalAlpha=1;
}
function circle(x,y,r){ ctx.beginPath(); ctx.arc(x,y,r,0,7); ctx.fill(); }
// item icons (shared by hotbar + place btn)
function drawWpnArt(g,w){ // weapon art, centered at origin, ~60px design space
const at=(x,y,ww,hh,c)=>{g.fillStyle=c;g.fillRect(x,y,ww,hh);};
if(w==='blaster'){ at(-16,-4,22,8,'#3a424c'); at(-16,-4,22,2,'#5a6672'); at(6,-3,11,5,'#2b333c'); at(16,-2,5,3,'#8fe9ff'); at(-12,4,5,8,'#2b333c'); }
else if(w==='laser'){ at(-16,-4,24,7,'#2c3a4c'); at(-16,-4,24,2,'#3f5266'); at(8,-3,12,4,'#1c2229'); at(19,-2,4,3,'#7cffb0'); at(-12,3,5,8,'#2c3a4c'); }
else if(w==='rocket'){ at(-16,-5,26,10,'#2b333c'); at(-16,-5,26,2,'#3a424c'); at(10,-6,10,12,'#c0392b'); at(16,-8,3,4,'#ff9a2a'); at(-16,0,6,3,'#ff9a2a'); }
else { at(-3,-16,3,20,'#d8dde3'); at(-3,-16,3,5,'#8a5a2b'); at(6,-12,3,16,'#d8dde3'); at(6,-12,3,4,'#8a5a2b'); at(-12,-10,3,14,'#d8dde3'); }
}
function drawItemIcon(g,k,size,centered){
const s=size/60; g.save(); if(!centered)g.translate(30,30); g.scale(s,s);
const at=(x,y,w,h,c)=>{g.fillStyle=c;g.fillRect(x,y,w,h);};
if(k==='pick'){
at(-2,-12,5,30,'#39424c'); at(-2,-12,2,30,'#5a6672'); // handle
at(-4,-13,9,4,'#20262c');
at(-8,-22,16,9,'#2b333c'); at(-8,-22,16,2,'#4a545f'); // metal head mount
g.fillStyle='rgba(55,225,255,.35)'; // energy blade glow
g.beginPath(); g.moveTo(-8,-18); g.lineTo(-24,-24); g.lineTo(-8,-12); g.fill();
g.beginPath(); g.moveTo(8,-18); g.lineTo(24,-24); g.lineTo(8,-12); g.fill();
g.fillStyle='#37e1ff'; // blade core
g.beginPath(); g.moveTo(-8,-18); g.lineTo(-22,-22); g.lineTo(-8,-14); g.fill();
g.beginPath(); g.moveTo(8,-18); g.lineTo(22,-22); g.lineTo(8,-14); g.fill();
at(-23,-23,4,3,'#eaffff'); at(19,-23,4,3,'#eaffff'); // white-hot tips
at(-3,-20,6,5,'#37e1ff'); at(-1,-19,2,3,'#eaffff'); } // core node
else if(k==='tank'){ at(-16,-2,32,7,'#1c2229');
g.fillStyle='#333'; for(let wx=-13;wx<14;wx+=6){ g.beginPath(); g.arc(wx,1,3,0,7); g.fill(); }
at(-15,-13,30,11,'#3a424c'); at(-15,-13,30,2,'#4a545f');
at(-8,-21,16,9,'#4a545f'); at(6,-19,15,4,'#2b333c'); at(19,-18,3,3,'#37e1ff'); }
else if(k==='heli'){ g.fillStyle='#c8d2dc'; g.fillRect(-20,-13,40,2); // rotor
at(-2,-13,4,4,'#2b333c'); at(-11,-9,22,11,'#4a545f'); at(-11,-9,22,2,'#5a6672');
at(3,-7,7,6,'#37e1ff'); at(10,-5,15,4,'#3a424c'); at(23,-9,3,7,'#2b333c');
at(-11,4,22,2,'#2b333c'); at(-2,2,4,7,'#1c2229'); }
else if(k==='soldier'){ at(-6,-20,12,3,'#2c3a4c'); at(-6,-18,12,7,'#c98f6a'); at(-3,-16,3,2,'#37e1ff'); // helmet + face + visor
at(-7,-11,14,12,'#3a4a5c'); at(-7,-11,14,2,'#4f6478'); // armor
at(4,-8,15,3,'#1c2229'); at(17,-9,3,4,'#111'); // rifle
at(-6,1,4,9,'#2c3a4c'); at(2,1,4,9,'#2c3a4c'); } // legs
else if(k==='carpet'){ at(-20,2,40,7,'#7a2f3a'); at(-20,2,40,2,'#a8414f'); for(let sx=-16;sx<16;sx+=8)at(sx,4,4,3,'#e8c46a'); }
else if(k==='chair'){ at(-10,-2,4,14,'#6b4a2f'); at(6,-2,4,14,'#6b4a2f'); at(-12,-4,24,5,'#8a5a34'); at(-12,-18,4,16,'#8a5a34'); }
else if(k==='table'){ at(-18,-6,36,6,'#8a5a34'); at(-15,0,4,14,'#6b4a2f'); at(11,0,4,14,'#6b4a2f'); }
else if(k==='bookshelf'){ at(-14,-18,28,32,'#5a3d28'); at(-12,-14,24,3,'#3f2a1a'); at(-12,-2,24,3,'#3f2a1a');
at(-11,-13,4,10,'#c0392b'); at(-6,-13,4,10,'#2a7ab0'); at(-1,-13,4,10,'#4caf50'); at(4,-13,4,10,'#e0a83a'); }
else if(k==='rug'){ g.fillStyle='#3a6ea5'; g.beginPath(); g.ellipse(0,4,20,8,0,0,7); g.fill(); g.fillStyle='#e8c46a'; g.beginPath(); g.ellipse(0,4,8,4,0,0,7); g.fill(); }
else if(k==='bed'){ at(-18,2,36,8,'#5a4a6a'); at(-18,-4,36,6,'#d8dde8'); at(10,-4,8,5,'#fff'); at(14,-9,4,15,'#4a3d5a'); }
else if(k==='sofa'){ at(-16,-2,32,10,'#8a5a4a'); at(-16,-10,6,16,'#a06a58'); at(10,-10,6,16,'#a06a58'); at(-12,-12,24,8,'#a06a58'); }
else if(k==='tv'){ at(-16,-12,32,20,'#111'); g.globalAlpha=.6; at(-13,-9,26,14,'#37e1ff'); g.globalAlpha=1; at(-6,10,12,4,'#3a2f27'); }
else if(k==='fridge'){ at(-10,-16,20,32,'#d8dde3'); at(-10,-1,20,2,'#b8bdc3'); at(6,-12,2,8,'#8a8f96'); }
else if(k==='stove'){ at(-14,-11,28,23,'#2b2f34'); g.fillStyle='#111'; g.beginPath(); g.arc(-6,-6,3,0,7); g.arc(6,-6,3,0,7); g.fill(); g.globalAlpha=.6; at(-10,0,20,10,'#ff9a2a'); g.globalAlpha=1; }
else if(k==='sink'){ at(-16,-2,32,14,'#8a6a4a'); at(-10,-4,20,6,'#cfd8e3'); at(-1,-12,2,8,'#8a8f96'); }
else if(k==='toilet'){ at(-7,-4,14,14,'#e8ebee'); at(-8,-18,16,12,'#e8ebee'); }
else if(k==='bath'){ at(-18,-4,36,14,'#e8ebee'); at(-14,-1,28,7,'#bfe0ff'); at(15,-12,3,8,'#8a8f96'); }
else if(k==='lamp'){ at(-1,-14,2,28,'#3a3f45'); g.fillStyle='#ffe08a'; g.beginPath(); g.moveTo(-10,-14); g.lineTo(10,-14); g.lineTo(6,-25); g.lineTo(-6,-25); g.closePath(); g.fill(); }
else if(k==='plant'){ at(-7,3,14,10,'#a05a34'); g.fillStyle='#2f7a3a'; for(const d of [[0,-10],[-6,-3],[6,-3]]){ g.beginPath(); g.ellipse(d[0],d[1]-2,6,9,0,0,7); g.fill(); } }
else if(k==='desk'){ at(-18,-1,36,4,'#6b4a2f'); at(-16,3,3,12,'#6b4a2f'); at(13,3,3,12,'#6b4a2f'); at(-9,-16,20,14,'#111'); g.globalAlpha=.5; at(-7,-14,16,10,'#37e1ff'); g.globalAlpha=1; }
else if(k==='painting'){ at(-14,-12,28,24,'#6b4a2f'); at(-11,-9,22,18,'#4a7ba5'); at(-11,3,22,6,'#6ea05a'); g.fillStyle='#ffe08a'; g.beginPath(); g.arc(6,-3,3,0,7); g.fill(); }
else if(k==='idoor'){ at(-13,-17,26,34,'#3a2f22'); at(-10,-14,20,28,'#7a5230'); at(-10,-14,20,3,'#8a5f38');
at(-8,-11,16,10,'#5f3f22'); at(-8,1,16,10,'#5f3f22'); at(6,-2,2,4,'#d9b25a'); }
else if(k==='passage'){ at(-13,-14,26,28,'#8a8f96'); at(-13,-14,26,3,'#a7adb4'); at(-13,11,26,3,'#6b7178');
at(-13,-14,3,28,'#5c6167'); at(10,-14,3,28,'#5c6167');
at(-9,-10,3,3,'#c9ced3'); at(6,-10,3,3,'#c9ced3'); at(-9,7,3,3,'#c9ced3'); at(6,7,3,3,'#c9ced3'); }
else if(k==='remove'){ g.strokeStyle='#e05a5a'; g.lineWidth=5; g.beginPath(); g.moveTo(-12,-12); g.lineTo(12,12); g.moveTo(12,-12); g.lineTo(-12,12); g.stroke(); }
else if(k==='armory'){ drawWpnArt(g,(player&&player.weapon)||'blaster'); }
else if(k==='elevator'){ at(-14,-18,3,36,'#2b333c'); at(11,-18,3,36,'#2b333c'); // rails
at(-12,-2,24,6,'#4a545f'); at(-12,-2,24,2,'#5a6672'); at(-12,4,24,2,'#37e1ff'); // platform
at(-3,-16,6,3,'#37e1ff'); at(-3,11,6,3,'#37e1ff'); } // up/down glow
else if(k==='base'){ at(-16,-6,32,22,'#39424c'); at(-16,-6,32,3,'#5a6672');
at(-16,-6,4,22,'#2b333c'); at(12,-6,4,22,'#2b333c');
at(-6,3,12,13,'#1c2229'); at(-1,3,2,13,'#37e1ff');
at(-3,-15,6,9,'#2b333c'); at(-2,-19,4,5,'#37e1ff');
at(-12,-2,6,5,'#37e1ff'); at(6,-2,6,5,'#37e1ff'); }
else if(k==='spikes'){ at(-20,10,40,8,'#2b333c'); at(-20,10,40,2,'#3f4a56');
g.fillStyle='#37e1ff';
for(let i=0;i<4;i++){ const x=-17+i*11; g.beginPath();g.moveTo(x,10);g.lineTo(x+4,-15);g.lineTo(x+8,10);g.fill(); }
g.fillStyle='#eaffff'; for(let i=0;i<4;i++){ const x=-17+i*11; g.fillRect(x+3,-13,2,6); } }
else if(k==='lava'){ at(-20,-4,40,20,'#2b333c'); at(-20,-4,40,3,'#3f4a56');
at(-16,0,32,12,'#c0330a'); at(-16,1,32,8,'#ff5a0a'); at(-14,3,28,4,'#ffb02a'); at(-6,3,8,3,'#ffe066');
at(-19,-4,4,2,'#f4c020'); at(15,-4,4,2,'#f4c020'); }
else if(k==='web'){ g.fillStyle='rgba(80,140,255,.18)'; g.fillRect(-18,-18,36,36);
g.strokeStyle='#9cc0ff'; g.lineWidth=2; g.strokeRect(-18,-18,36,36);
g.beginPath();g.moveTo(-18,-18);g.lineTo(18,18);g.moveTo(18,-18);g.lineTo(-18,18);g.moveTo(0,-18);g.lineTo(0,18);g.moveTo(-18,0);g.lineTo(18,0);g.stroke();
g.fillStyle='#dbe8ff'; g.fillRect(-3,-3,6,6); }
else if(k==='arrow'){ at(-16,-14,32,30,'#4a4a4a'); at(-10,-8,20,18,'#111'); at(16,-4,10,5,'#c8c8c8'); }
else if(k==='laser'){ at(-14,2,28,14,'#2b333c'); at(-10,-14,20,16,'#464f5a'); at(-10,-14,20,3,'#5b6570');
at(8,-10,10,7,'#1c2229'); at(16,-9,5,5,'#ff3a3a'); at(-6,-10,4,4,'#ff5a5a'); at(20,-8,8,2,'#ff3a3a'); }
else if(k==='cactus'){ at(-8,-20,16,40,'#2b333c'); at(-2,-20,4,40,'#6eff7a');
g.fillStyle='#6eff7a';
for(let y=-16;y<18;y+=10){ g.beginPath(); g.moveTo(-8,y); g.lineTo(-18,y+3); g.lineTo(-8,y+6); g.fill();
g.beginPath(); g.moveTo(8,y); g.lineTo(18,y+3); g.lineTo(8,y+6); g.fill(); }
at(-9,-18,3,36,'#8a97a4'); at(6,-18,3,36,'#8a97a4'); }
else if(k==='cobble'){ at(-20,-20,40,40,'#3a424c'); at(-20,-20,40,3,'#5a6672'); at(-20,-20,3,40,'#5a6672');
at(-20,17,40,3,'#20262c'); at(17,-20,3,40,'#20262c'); at(-13,-13,26,26,'#464f5a');
at(-18,-18,4,4,'#8a97a4'); at(14,-18,4,4,'#8a97a4'); at(-18,14,4,4,'#8a97a4'); at(14,14,4,4,'#8a97a4');
at(-6,-6,12,12,'#37e1ff'); at(-2,-2,4,4,'#eaffff'); }
else if(k==='door'){ at(-14,-22,28,44,'#1c2229'); // frame
at(-11,-20,22,40,'#6b7684'); at(-11,-20,22,3,'#8492a2');
at(-11,-6,22,2,'#4b545f'); at(-11,8,22,2,'#4b545f'); // plating seams
at(-1,-20,2,40,'#37e1ff'); // energy seam
at(-3,-17,6,5,'#ff4a4a'); // locked light
at(-11,13,22,5,'#f4c020'); // hazard band
at(-9,13,3,5,'#1c2229'); at(-2,13,3,5,'#1c2229'); at(5,13,3,5,'#1c2229'); }
else if(k==='tnt'){ at(-18,-18,36,36,'#39424c'); at(-18,-18,36,3,'#5a6672');
at(-18,8,36,7,'#f4c020'); g.fillStyle='#1c2229'; for(let sx=-18;sx<18;sx+=8) g.fillRect(sx,8,4,7);
at(-9,-11,18,18,'rgba(255,80,40,.5)'); at(-6,-8,12,12,'#ff5a2a'); at(-2,-4,4,4,'#fff2b0');
at(-16,-16,3,3,'#8a97a4'); at(13,-16,3,3,'#8a97a4'); at(-16,13,3,3,'#8a97a4'); at(13,13,3,3,'#8a97a4');
at(-2,-25,4,5,'#ff3a3a'); }
else if(k==='dirt'){ at(-18,-16,36,32,'#4a3a2c'); at(-18,-16,36,5,'#5b4d42');
at(-12,-4,6,6,'#2e241b'); at(6,3,6,6,'#2e241b'); at(-4,-10,5,5,'#7a7a7a'); at(9,-8,4,4,'#5b4d42'); }
else if(k==='vault'){ at(-16,-20,32,40,'#59636f'); at(-16,-20,32,3,'#6b7783');
g.strokeStyle='#8a97a4'; g.lineWidth=3; g.beginPath(); g.arc(0,0,12,0,7); g.stroke();
g.strokeStyle='#6b7783'; g.lineWidth=3; g.beginPath(); g.moveTo(0,-12); g.lineTo(0,12); g.moveTo(-12,0); g.lineTo(12,0); g.stroke();
g.fillStyle='#6eff7a'; g.beginPath(); g.arc(0,0,4,0,7); g.fill();
at(-14,-17,3,3,'#aab6c2'); at(11,-17,3,3,'#aab6c2'); at(-14,14,3,3,'#aab6c2'); at(11,14,3,3,'#aab6c2'); }
g.restore();
}
function loop(t){ const dt=Math.min(1/30,(t-tPrev)/1000)||0; tPrev=t;
if(inside){ updateInterior(dt); renderInterior(); requestAnimationFrame(loop); return; }
if(phase!=='over')update(dt); render(); requestAnimationFrame(loop); }
// sound
let AC=null, blipBudget=0, blipWin=0;
function blip(f,d,type='sine',v=.05){ if(muted)return;
const now=performance.now();
if(now-blipWin>100){ blipWin=now; blipBudget=0; } // reset every 100ms
if(blipBudget>=3) return; // cap voices — stops rapid fire stuttering
blipBudget++;
try{ AC=AC||new(window.AudioContext||window.webkitAudioContext)(); const o=AC.createOscillator(),g=AC.createGain();
o.type=type;o.frequency.value=f;o.connect(g);g.connect(AC.destination);
g.gain.setValueAtTime(v,AC.currentTime);g.gain.exponentialRampToValueAtTime(.0001,AC.currentTime+d);
o.start();o.stop(AC.currentTime+d);}catch(e){} }
$('muteBtn').onclick=()=>{ muted=!muted; $('muteBtn').textContent=muted?'🔇':'🔊'; };
// ---- door keypad ----
const kp={open:false,mode:'set',door:null,input:''};
function buildKeypad(){ const g=$('kpGrid'); g.innerHTML='';
['1','2','3','4','5','6','7','8','9','CLR','0','OK'].forEach(t=>{ const b=document.createElement('button');
b.textContent=t; if(t==='OK')b.className='ok'; if(t==='CLR')b.className='clr';
b.onclick=()=>{ t==='OK'?kpOk():t==='CLR'?kpClear():kpPress(t); }; g.appendChild(b); }); }
function openKeypad(mode,door){ kp.open=true; kp.mode=mode; kp.door=door; kp.input='';
$('kpTitle').textContent=mode==='set'?'SET DOOR CODE':'ENTER CODE';
$('kpMsg').textContent=mode==='set'?'Pick a secret code':'';
$('kpSkip').style.display=mode==='set'?'':'none';
updateKp(); $('keypad').classList.remove('hidden'); }
function closeKeypad(){ kp.open=false; kp.door=null; $('keypad').classList.add('hidden'); }
function kpPress(d){ if(kp.input.length<6){ kp.input+=d; updateKp(); blip(720,.03,'square'); } }
function kpClear(){ kp.input=''; updateKp(); }
function kpBack(){ kp.input=kp.input.slice(0,-1); updateKp(); }
function updateKp(){ const n=Math.max(4,kp.input.length); let s='';
for(let i=0;i<n;i++) s+=(kp.input[i]||'–')+(i<n-1?' ':''); $('kpDisplay').textContent=s; }
function kpOk(){ const d=kp.door; if(!d){closeKeypad();return;}
if(kp.mode==='set'){ d.pass=kp.input.length?kp.input:null; d.open=false;
banner(d.pass?'DOOR LOCKED 🔒':'DOOR UNLOCKED',1.2); blip(540,.1,'triangle'); closeKeypad(); }
else { if(kp.input===d.pass){ d.open=true; puff(d.c*B+B/2,GROUND-B,'#6effa0',10); blip(660,.1,'triangle');
banner('ACCESS GRANTED',1); closeKeypad(); }
else { $('kpMsg').textContent='WRONG CODE!'; kp.input=''; updateKp(); blip(150,.15,'sawtooth'); shake=Math.max(shake,4); } } }
$('kpSkip').onclick=()=>{ if(kp.door){ kp.door.pass=null; kp.door.open=false; } banner('DOOR UNLOCKED',1); closeKeypad(); };
$('kpClose').onclick=()=>closeKeypad();
buildKeypad();
// overlay + keys
$('ovBtn').onclick=()=>{ if(phase==='over')reset(); $('overlay').classList.add('hidden'); if(AC&&AC.state==='suspended')AC.resume(); };
window.addEventListener('keydown',e=>{ const k=e.key.toLowerCase();
if(kp.open){ if(e.key>='0'&&e.key<='9')kpPress(e.key);
else if(e.key==='Backspace')kpBack(); else if(e.key==='Enter')kpOk(); else if(e.key==='Escape')closeKeypad();
e.preventDefault(); return; }
if(k==='arrowleft'||k==='a')keys.left=1; else if(k==='arrowright'||k==='d')keys.right=1;
else if(k==='arrowup'||k==='w')keys.up=1; else if(k==='arrowdown'||k==='s')keys.down=1;
else if(e.key>='1'&&e.key<='9')selItem(BAR[+e.key-1]);
else if(e.key==='0')selItem(BAR[9]);
else if(e.key==='-')selItem(BAR[10]);
else if(e.key==='=')selItem(BAR[11]);
else if(e.key==='[')selItem(BAR[12]);
else if(e.key===']')selItem(BAR[13]);
else if(k==='e'||k===' '){e.preventDefault();placeUnderPlayer();}
else if(k==='n')startNight();
});
window.addEventListener('keyup',e=>{ const k=e.key.toLowerCase();
if(k==='arrowleft'||k==='a')keys.left=0; else if(k==='arrowright'||k==='d')keys.right=0;
else if(k==='arrowup'||k==='w')keys.up=0; else if(k==='arrowdown'||k==='s')keys.down=0; });
reset(); requestAnimationFrame(loop);
})();
</script>
</body>
</html>
Game Source: Mini Minecraft — Fly & Trap
Creator: ShadowLegend17
Libraries: none
Complexity: complex (1847 lines, 131.4 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: mini-minecraft-fly-amp-trap-shadowlegend17" to link back to the original. Then publish at arcadelab.ai/publish.