Blueprint Zombies, Build a COD Zombies Game in UE5
JVDesignStudio - https://jvdesignstudio.co.uk/workshops/unreal-zombie-survivor.html

Offline reference pack for this workshop.
This is a companion copy of the build steps, so you can follow along without
the website. Open the workshop online for the interactive screenshots, videos
and the copy buttons.

What you build: Blueprint Zombies, Build a COD Zombies Game in UE5

--------------------------------------------------------------------
STEP-BY-STEP BUILD
--------------------------------------------------------------------

## On Spawn Zombie',x:4,y:34,w:150,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Random Integer in Range',x:176,y:8,w:170,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2i',type:'int',label:'Class'}]}, {id:'n3',type:'func',title:'Set Zombie Class',x:176,y:98,w:170,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'int',label:'Class'}],rp:[{id:'r3e',type:'exec',label:''}]}, {id:'n4',type:'vset',title:'Set Max Health',x:364,y:98,w:150,lp:[{id:'l4e',type:'exec',label:''}],vf:{hint:'Brute HP (e.g. 200)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2i',t:'l3c'},{f:'r3e',t:'l4e'}], v:[{id:'n4',ok:v=>parseInt(v)>=150&&parseInt(v)'✅ Zombie variants live! Brutes spawn with '+v['n4']+' HP.'}, /* 13 ZOMBIE ANIMATIONS */{ defs:[ {id:'n1',type:'event',title:'Update Animation',x:4,y:34,w:160,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Velocity',x:186,y:10,w:155,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2v',type:'vec3',label:'Velocity'}]}, {id:'n3',type:'vset',title:'Set Walk Speed',x:186,y:100,w:155,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'vec3',label:'In'}],vf:{hint:'Blend space top speed (e.g. 400)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2v',t:'l3v'}], v:[{id:'n3',ok:v=>parseInt(v)>=100&&parseInt(v)'✅ Anim Blueprint wired! Walk blend tops out at '+v['n3']+'.'}, /* 14 BLOOD & HIT EFFECTS */{ defs:[ {id:'n1',type:'custom',title:'On Zombie Hit',x:4,y:26,w:150,rp:[{id:'r1e',type:'exec',label:''},{id:'r1h',type:'bool',label:'Headshot'}]}, {id:'n2',type:'branch',title:'Branch',sub:'Headshot?',x:176,y:8,w:124,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Cond'}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'func',title:'Spawn Emitter: Headshot',x:322,y:0,w:190,lp:[{id:'l3e',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Spawn Emitter: Body Hit',x:322,y:88,w:190,lp:[{id:'l4e',type:'exec',label:''}]}, ], w:[{f:'r1e',t:'l2e'},{f:'r1h',t:'l2c'},{f:'r2t',t:'l3e'},{f:'r2f',t:'l4e'}], v:[], msg:()=>'✅ Blood FX wired! Headshots get their own burst.'}, /* 15 HIT & ASSIST POINTS */{ defs:[ {id:'n1',type:'custom',title:'On Zombie Damaged',x:4,y:32,w:160,lp:[],rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'Did it kill?',x:186,y:8,w:130,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'math',title:'Add Kill Points',x:338,y:0,w:160,lp:[{id:'l3e',type:'exec',label:''}]}, {id:'n4',type:'math',title:'Add Hit Points',x:338,y:88,w:160,lp:[{id:'l4e',type:'exec',label:''}],vf:{hint:'Points per non-killing hit (e.g. 10)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r2f',t:'l4e'}], v:[{id:'n4',ok:v=>parseInt(v)>=5&&parseInt(v)'✅ Assist points live! '+v['n4']+' points for a hit that does not kill.'}, /* 16 POWER-UPS */{ defs:[ {id:'n1',type:'custom',title:'On Brute Killed',x:4,y:32,w:155,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Spawn Actor: Power-Up',x:181,y:8,w:180,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2a',type:'actor',label:'Return'}]}, {id:'n3',type:'func',title:'Set Life Span',x:181,y:100,w:180,lp:[{id:'l3e',type:'exec',label:''},{id:'l3a',type:'actor',label:'Target'}],vf:{hint:'Seconds before it fades (e.g. 30)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2a',t:'l3a'}], v:[{id:'n3',ok:v=>parseInt(v)>=5&&parseInt(v)'✅ Brutes drop power-ups that fade after '+v['n3']+'s.'}, /* 17 MAP ZONES & DOORS */{ defs:[ {id:'n1',type:'custom',title:'On Door Interact',x:4,y:32,w:155,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'Points >= Cost?',x:181,y:8,w:140,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'func',title:'Open Zone Door',x:343,y:0,w:170,lp:[{id:'l3e',type:'exec',label:''}],vf:{hint:'Door cost in points (e.g. 1500)',val:''}}, {id:'n4',type:'func',title:'Show "Not Enough Points"',x:343,y:96,w:170,lp:[{id:'l4e',type:'exec',label:''}]}, ], w:[{f:'r1e',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r2f',t:'l4e'}], v:[{id:'n3',ok:v=>parseInt(v)>=250&&parseInt(v)'✅ Zone doors open for '+v['n3']+' points.'}, /* 18 FLASHLIGHT & DARK MAP */{ defs:[ {id:'n1',type:'event',title:'IA_Flashlight (Started)',x:4,y:30,w:175,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Toggle Visibility',sub:'SpotLight',x:201,y:8,w:160,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''}]}, {id:'n3',type:'func',title:'Set Outer Cone Angle',x:201,y:100,w:160,lp:[{id:'l3e',type:'exec',label:''}],vf:{hint:'Cone angle in degrees (e.g. 32)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'}], v:[{id:'n3',ok:v=>parseInt(v)>=10&&parseInt(v)'? Flashlight toggles with a '+v['n3']+'° cone.'}, /* 19 DYNAMIC AUDIO */{ defs:[ {id:'n1',type:'event',title:'Event Tick',x:4,y:34,w:130,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Zombies In Range',x:156,y:8,w:170,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2i',type:'int',label:'Count'}]}, {id:'n3',type:'func',title:'Map Range Clamped',x:156,y:100,w:170,lp:[{id:'l3e',type:'exec',label:''},{id:'l3i',type:'int',label:'In'}],rp:[{id:'r3f',type:'float',label:'Out'}]}, {id:'n4',type:'vset',title:'Set Music Intensity',x:348,y:100,w:165,lp:[{id:'l4f',type:'float',label:'In'}],vf:{hint:'Zombie count for max intensity (e.g. 12)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2i',t:'l3i'},{f:'r3f',t:'l4f'}], v:[{id:'n4',ok:v=>parseInt(v)>=4&&parseInt(v)'✅ Music ramps up, peaking at '+v['n4']+' nearby zombies.'}, /* 20 PAUSE & RESTART MENU */{ defs:[ {id:'n1',type:'event',title:'IA_Pause (Started)',x:4,y:30,w:160,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Set Game Paused',sub:'Paused = true',x:186,y:6,w:155,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''}]}, {id:'n3',type:'func',title:'Create Widget',x:186,y:98,w:155,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3o',type:'object',label:'Return'}],vf:{hint:'Widget class (e.g. WBP_PauseMenu)',val:''}}, {id:'n4',type:'func',title:'Add to Viewport',x:363,y:98,w:150,lp:[{id:'l4e',type:'exec',label:''},{id:'l4o',type:'object',label:'Target'}]}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r3e',t:'l4e'},{f:'r3o',t:'l4o'}], v:[{id:'n3',ok:v=>v.toLowerCase().includes('wbp')||v.toLowerCase().includes('pause')}], msg:v=>'✅ Pause menu wired using '+v['n3']+'.'}, /* 21 PACK-A-PUNCH (gold) */{ defs:[ {id:'n1',type:'custom',title:'On Pack-a-Punch Use',x:4,y:28,w:170,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'Points >= Cost?',x:196,y:6,w:140,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'math',title:'Multiply Weapon Damage',x:348,y:0,w:168,lp:[{id:'l3e',type:'exec',label:''}],vf:{hint:'Pack-a-Punch cost (e.g. 2500)',val:''}}, {id:'n4',type:'func',title:'Show "Need More Points"',x:348,y:96,w:168,lp:[{id:'l4e',type:'exec',label:''}]}, ], w:[{f:'r1e',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r2f',t:'l4e'}], v:[{id:'n3',ok:v=>parseInt(v)>=1000&&parseInt(v)'✅ Pack-a-Punch online at '+v['n3']+' points.'}, /* 22 BOSS ROUND */{ defs:[ {id:'n1',type:'custom',title:'On Round Start',x:4,y:34,w:155,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Is Boss Round (Round % 10)',x:181,y:10,w:195,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2b',type:'bool',label:'Result'}]}, {id:'n3',type:'branch',title:'Branch',x:181,y:104,w:120,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'bool',label:'Cond'}],rp:[{id:'r3t',type:'exec',label:'True'}]}, {id:'n4',type:'func',title:'Spawn Boss',x:323,y:104,w:155,lp:[{id:'l4e',type:'exec',label:''}],vf:{hint:'Boss HP (e.g. 5000)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2b',t:'l3c'},{f:'r3t',t:'l4e'}], v:[{id:'n4',ok:v=>parseInt(v)>=1000&&parseInt(v)'✅ Boss arrives every 10th round with '+v['n4']+' HP.'}, /* 23 LEADERBOARD & BEST ROUND */{ defs:[ {id:'n1',type:'custom',title:'On Game Over',x:4,y:32,w:150,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'Round > Best?',x:176,y:8,w:140,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2t',type:'exec',label:'True'}]}, {id:'n3',type:'vset',title:'Set Best Round',x:338,y:8,w:165,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3e',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Save Game to Slot',x:338,y:98,w:165,lp:[{id:'l4e',type:'exec',label:''}],vf:{hint:'Save slot name (e.g. ZombieSave)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3e',t:'l4e'}], v:[{id:'n4',ok:v=>v.toLowerCase().includes('save')||v.toLowerCase().includes('zombie')}], msg:v=>'✅ Best round persists to slot "'+v['n4']+'".'}, /* 24 CAMERA SHAKE & RUMBLE */{ defs:[ {id:'n1',type:'custom',title:'On Weapon Fired',x:4,y:32,w:155,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Play World Camera Shake',x:181,y:8,w:195,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''}],vf:{hint:'Shake scale (e.g. 0.6)',val:''}}, {id:'n3',type:'func',title:'Client Play Force Feedback',x:181,y:104,w:195,lp:[{id:'l3e',type:'exec',label:''}]}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'}], v:[{id:'n2',ok:v=>parseFloat(v)>=0.1&&parseFloat(v)'✅ Camera shake at scale '+v['n2']+', plus controller rumble.'}, /* 25 ZOMBIE SWARM BEHAVIOUR */{ defs:[ {id:'n1',type:'event',title:'Event Tick',sub:'Zombie AI',x:4,y:30,w:140,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Nearby Zombies',x:166,y:8,w:175,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2v',type:'vec3',label:'Avoid Dir'}]}, {id:'n3',type:'func',title:'Add Movement Input',x:166,y:100,w:175,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'vec3',label:'World Dir'}],vf:{hint:'Separation radius in cm (e.g. 60)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2v',t:'l3v'}], v:[{id:'n3',ok:v=>parseInt(v)>=40&&parseInt(v)'✅ Swarm separation at '+v['n3']+'cm, no more zombie stacking.'}, /* 26 PROCEDURAL SPAWN POINTS */{ defs:[ {id:'n1',type:'custom',title:'On Wave Start',x:4,y:32,w:150,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get All Actors of Class',sub:'BP_SpawnPoint',x:176,y:6,w:180,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2a',type:'actor',label:'Out Actors'}]}, {id:'n3',type:'func',title:'Filter: Out of Player View',x:176,y:104,w:180,lp:[{id:'l3e',type:'exec',label:''},{id:'l3a',type:'actor',label:'In'}],rp:[{id:'r3e',type:'exec',label:''}],vf:{hint:'Min distance from player, cm (e.g. 1500)',val:''}}, {id:'n4',type:'func',title:'Spawn Zombie',x:368,y:104,w:145,lp:[{id:'l4e',type:'exec',label:''}]}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2a',t:'l3a'},{f:'r3e',t:'l4e'}], v:[{id:'n3',ok:v=>parseInt(v)>=500&&parseInt(v)'✅ Zombies spawn out of sight, at least '+v['n3']+'cm away.'}, /* 27 MOBILE OPTIMISATION */{ defs:[ {id:'n1',type:'event',title:'Event BeginPlay',x:4,y:34,w:155,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Is Mobile Platform',x:181,y:10,w:165,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2b',type:'bool',label:'Result'}]}, {id:'n3',type:'branch',title:'Branch',x:181,y:104,w:120,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'bool',label:'Cond'}],rp:[{id:'r3t',type:'exec',label:'True'}]}, {id:'n4',type:'vset',title:'Cap Niagara Particles',x:323,y:104,w:180,lp:[{id:'l4e',type:'exec',label:''}],vf:{hint:'Particle cap on mobile (e.g. 200)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2b',t:'l3c'},{f:'r3t',t:'l4e'}], v:[{id:'n4',ok:v=>parseInt(v)>=50&&parseInt(v)'✅ Mobile preset on, Niagara particles capped at '+v['n4']+'.'}, /* 28 POST PROCESS & ATMOSPHERE */{ defs:[ {id:'n1',type:'event',title:'Event BeginPlay',x:4,y:34,w:155,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Post Process Volume',x:181,y:8,w:190,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2o',type:'object',label:'Volume'}]}, {id:'n3',type:'func',title:'Set Vignette Intensity',x:181,y:104,w:190,lp:[{id:'l3e',type:'exec',label:''},{id:'l3o',type:'object',label:'Target'}],vf:{hint:'Vignette intensity (e.g. 0.45)',val:''}}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2o',t:'l3o'}], v:[{id:'n3',ok:v=>parseFloat(v)>=0.2&&parseFloat(v)'✅ Atmosphere set, vignette at '+v['n3']+'.'}, /* 29 PACKAGE & TEST */{ defs:[ {id:'n1',type:'event',title:'Event BeginPlay',sub:'Test Build',x:4,y:30,w:155,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Validate Game Systems',x:181,y:8,w:180,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2b',type:'bool',label:'All OK'}]}, {id:'n3',type:'branch',title:'Branch',x:181,y:104,w:120,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'bool',label:'Cond'}],rp:[{id:'r3t',type:'exec',label:'True'}]}, {id:'n4',type:'func',title:'Print "Ready to Package"',x:323,y:104,w:190,lp:[{id:'l4e',type:'exec',label:''}]}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2b',t:'l3c'},{f:'r3t',t:'l4e'}], v:[], msg:()=>'✅ Every system reports green. Ready to package.'}, /* 30 SHARE YOUR ZOMBIES MAP */{ defs:[ {id:'n1',type:'custom',title:'On Publish',x:4,y:36,w:145,rp:[{id:'r1e',type:'exec',label:''}]}, {id:'n2',type:'vset',title:'Set Map Name',x:171,y:12,w:180,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''}],vf:{hint:'Name your map, then Apply',val:''}}, {id:'n3',type:'func',title:'Launch!',x:171,y:108,w:180,lp:[{id:'l3e',type:'exec',label:''}]}, ], w:[{f:'r1e',t:'l2e'},{f:'r2e',t:'l3e'}], v:[{id:'n2',ok:v=>v.trim().length>=3}], msg:v=>'🎮 "'+v['n2']+'" is ready to share. Outstanding work!'}, ]; function mkPZ(defs,rW,rV,onDone,isGold=false){ const uid=++UID; const hasW=rW.length>0,hasV=rV.length>0; const html=` 🔧 Drag output pin → input pin${hasV?' → Click fields to type':''} ${hasW?`Connect the required wires`:''} ${hasV?`Type values in the fields`:''}`; return{uid,html,hasReq:hasW||hasV,init(cb){ const el=document.getElementById('bpcw-'+uid);if(!el)return; const bp=new BPC(el,defs,rW,rV,v=>{cb&&cb(v);},isGold);bp.uid=uid;return bp; }}; } /* ------------------------------------------- WEAPON DEFINITIONS ------------------------------------------- */ const WEAPONS={ pistol: {name:'Pistol', dmg:25, spread:.06, ammo:12, mag:12, fire:.42, bullets:1, col:'#aaa', cost:0, icon:'🔫'}, smg: {name:'SMG', dmg:14, spread:.13, ammo:30, mag:30, fire:.11, bullets:1, col:'#4af', cost:500, icon:'⚡'}, shotgun: {name:'Shotgun', dmg:20, spread:.28, ammo:6, mag:6, fire:.68, bullets:5, col:'#c84', cost:750, icon:'🔫'}, ak: {name:'AK-47', dmg:22, spread:.09, ammo:30, mag:30, fire:.14, bullets:1, col:'#8c4', cost:1200, icon:'🔫'}, sniper: {name:'Sniper', dmg:90, spread:.01, ammo:5, mag:5, fire:.9, bullets:1, col:'#2ef', cost:1500, icon:'🔫'}, lmg: {name:'LMG', dmg:18, spread:.11, ammo:75, mag:75, fire:.09, bullets:1, col:'#f82', cost:2000, icon:'🔫'}, rocket: {name:'RPG', dmg:120,spread:.04, ammo:3, mag:3, fire:1.2, bullets:1, col:'#f44', cost:0, icon:'🔫', explosive:true}, wonder: {name:'Wonder Gun', dmg:50, spread:.02, ammo:20, mag:20, fire:.2, bullets:3, col:'#f5f', cost:0, icon:'✨', homing:true}, }; // Wall buy layout: which weapon at which position const WALL_BUYS=[ {wpn:'smg', x:.14, y:.5 }, // left corridor centre {wpn:'shotgun',x:.86, y:.5 }, // right corridor centre {wpn:'ak', x:.5, y:.14}, // top corridor centre {wpn:'sniper', x:.5, y:.86}, // bottom corridor centre {wpn:'lmg', x:.19, y:.19}, // top-left corner (reachable via left/top corridors) ]; const BOX_POOL=['shotgun','ak','sniper','lmg','rocket','wonder','smg']; const BOX_COST=950; /* ------------------------------------------- GAME ENGINE ------------------------------------------- */ const canvas=document.getElementById('gc'); const ctx=canvas.getContext('2d'); let CW=650,CH=650; function resizeGame(){ const wrap=document.getElementById('gameWrap'); if(!wrap)return; const availW=wrap.clientWidth||wrap.offsetWidth||400; const availH=wrap.clientHeight||wrap.offsetHeight||0; const s=Math.max(availH>120?Math.min(availW-12,availH-12):availW-12,320); CW=s;CH=s; canvas.width=CW;canvas.height=CH; canvas.style.width=CW+'px';canvas.style.height=CH+'px'; if(typeof G!=='undefined'){ G.boxAnim.x=CW*.5;G.boxAnim.y=CH*.5; if(G.f_barricades&&G.barricades.length)buildBars(); } } window.addEventListener('resize',()=>requestAnimationFrame(resizeGame)); const G={ phase:'title', p:{x:200,y:200,r:11,hp:100,maxHp:100,angle:0, wpn:'pistol',ammo:12,reloading:false,reloadT:0}, points:0,kills:0,round:1,bestRound:0, zombies:[],bullets:[],particles:[],walls:[], barricades:[], lastShot:0,shakeT:0,shakeAmt:0, roundActive:false,zombiesLeft:0, interRound:false,interTimer:0, boxAnim:{active:false,timer:0,x:0,y:0,idx:0,result:null}, promptBuy:{active:false,buyIdx:-1,label:''}, keys:{},mx:200,my:200, // Feature flags f_arena:false,f_player:false,f_move:false,f_shoot:false, f_zombies:false,f_rounds:false,f_points:false, f_wallbuy:false,f_box:false,f_barricades:false, f_fullGame:false, }; function curWpn(){return WEAPONS[G.p.wpn];} /* Input */ window.addEventListener('keydown',e=>{ G.keys[e.code]=true; if(['Space','ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.code))e.preventDefault(); if(e.code==='KeyQ')tryBuy(); if(e.code==='KeyE')tryBuy(); if(e.code==='KeyR'&&(G.phase==='dead'))startGame(); if(e.code==='Escape')closeFS(); }); window.addEventListener('keyup',e=>G.keys[e.code]=false); canvas.addEventListener('mousemove',e=>{const r=canvas.getBoundingClientRect(),sc=CW/r.width;G.mx=(e.clientX-r.left)*sc;G.my=(e.clientY-r.top)*sc;}); canvas.addEventListener('mousedown',e=>{if(!e.button)G.keys['LMB']=true;}); canvas.addEventListener('mouseup', e=>{if(!e.button)G.keys['LMB']=false;}); canvas.addEventListener('click',()=>{if(G.phase==='dead')startGame();}); /* D-pad bindings */ function bindCtrl(prefix,fs){ const map={'up':'ArrowUp','down':'ArrowDown','left':'ArrowLeft','right':'ArrowRight'}; ['up','down','left','right'].forEach(d=>{ const el=document.getElementById(prefix+d);if(!el)return; const code=map[d]; el.addEventListener('touchstart',e=>{e.preventDefault();G.keys[code]=true;el.classList.add('held');},{passive:false}); el.addEventListener('touchend', e=>{e.preventDefault();G.keys[code]=false;el.classList.remove('held');},{passive:false}); el.addEventListener('mousedown',()=>{G.keys[code]=true;el.classList.add('held');}); el.addEventListener('mouseup', ()=>{G.keys[code]=false;el.classList.remove('held');}); el.addEventListener('mouseleave',()=>{G.keys[code]=false;el.classList.remove('held');}); }); const fire=document.getElementById(prefix+'fire'); const buy =document.getElementById(prefix+'buy'); if(fire){ fire.addEventListener('touchstart',e=>{e.preventDefault();G.keys['LMB']=true;fire.classList.add('held');},{passive:false}); fire.addEventListener('touchend', e=>{e.preventDefault();G.keys['LMB']=false;fire.classList.remove('held');},{passive:false}); fire.addEventListener('mousedown',()=>{G.keys['LMB']=true;fire.classList.add('held');}); fire.addEventListener('mouseup', ()=>{G.keys['LMB']=false;fire.classList.remove('held');}); } if(buy){ buy.addEventListener('touchstart',e=>{e.preventDefault();tryBuy();},{passive:false}); buy.addEventListener('click',tryBuy); } } bindCtrl('mb-',false); bindCtrl('fs-',true); function tryBuy(){ // Barricade repair, free, always available when near a damaged barricade if(G.f_barricades&&G.promptBuy.repairIdx>=0){ const b=G.barricades[G.promptBuy.repairIdx]; if(b){ const wasDestroyed=b.destroyed; b.hp=wasDestroyed?35:Math.min(100,b.hp+40); b.destroyed=false; spawnPtcl(b.x+b.w/2,b.y+b.h/2,'#4ade80',6); sout('🔧 Barricade '+(wasDestroyed?'rebuilt':'repaired')+'! ('+Math.round(b.hp)+'hp)'); return; } } if(!G.f_wallbuy&&!G.f_box)return; const px=G.p.x/CW,py=G.p.y/CH; // Check wall buys if(G.f_wallbuy){ for(let i=0;i=refillCost){G.points-=refillCost;G.p.ammo=w.mag;G.p.reloading=false;sout('✅ Ammo refilled! -'+refillCost+' pts');updateHUD();} else sout('❌ Not enough points! Need '+refillCost); } else if(G.points>=w.cost){ G.points-=w.cost;G.p.wpn=wb.wpn;G.p.ammo=w.mag;G.p.reloading=false; sout('💰 Bought '+w.name+'! -'+w.cost+' pts');updateHUD(); } else { sout('❌ Need '+w.cost+' pts for '+w.name); } return; } } } // Check mystery box if(G.f_box){ const bx=G.boxAnim.x/CW,by=G.boxAnim.y/CH; const bdist=Math.hypot(px-bx,py-by)*CW; if(bdist=BOX_COST){ G.points-=BOX_COST; G.boxAnim.active=true;G.boxAnim.timer=90;G.boxAnim.idx=0;G.boxAnim.result=BOX_POOL[Math.floor(Math.random()*BOX_POOL.length)]; sout('🎰 Mystery Box spinning! -'+BOX_COST+' pts');updateHUD(); } else { sout('❌ Mystery Box costs '+BOX_COST+' pts!'); } } } } function startGame(){ G.phase='playing'; G.p.x=CW/2;G.p.y=CH/2;G.p.hp=G.p.maxHp; G.p.wpn='pistol';G.p.ammo=WEAPONS.pistol.mag;G.p.reloading=false; G.zombies=[];G.bullets=[];G.particles=[]; G.round=1;G.kills=0;G.points=0; G.lastShot=0;G.interRound=false;G.roundActive=false; G.boxAnim={active:false,timer:0,x:CW*.5,y:CH*.5,idx:0,result:null}; G.walls=[];buildBars(); // always build map geometry spawnRound();updateHUD();sout('🎮 Round 1! WASD=move · Click=shoot · E near planks=repair barricade · E near wall sign=buy weapon'); } /* MAP GEOMETRY Outer boundary = full canvas edge (drawn as thick walls) 4 doorways cut into the outer wall, each has a barricade the player can defend Inner safe zone = central room 30%×70% of map Corridors connect outer wall gaps to the inner room Solid walls block movement outside corridors Wall buys sit on corridor walls Mystery box is in the centre room */ function getMap(){ const W=CW,H=CH,T=Math.round(W*.032); // wall thickness (thinner = more playable space) const iL=Math.round(W*.22),iR=Math.round(W*.78); // inner room wider: 22%×78% const iT=Math.round(H*.22),iB=Math.round(H*.78); // inner room taller: 22%×78% const gHalf=Math.round(W*.115); // wider doorways (11.5% each side = 23% opening) const cx=W/2,cy=H/2; // Static solid walls (player + zombie collision, never destroyed) // Outer top wall: two segments either side of top-centre gap const walls=[ // Top outer wall, left segment {x:0, y:0, w:cx-gHalf, h:T}, // Top outer wall, right segment {x:cx+gHalf, y:0, w:W-(cx+gHalf), h:T}, // Bottom outer wall, left segment {x:0, y:H-T, w:cx-gHalf, h:T}, // Bottom outer wall, right segment {x:cx+gHalf, y:H-T, w:W-(cx+gHalf), h:T}, // Left outer wall, top segment {x:0,y:0, w:T,h:cy-gHalf}, // Left outer wall, bottom segment {x:0,y:cy+gHalf,w:T,h:H-(cy+gHalf)}, // Right outer wall, top segment {x:W-T,y:0, w:T,h:cy-gHalf}, // Right outer wall, bottom segment {x:W-T,y:cy+gHalf,w:T,h:H-(cy+gHalf)}, // Inner room, top wall, left segment {x:iL,y:iT,w:cx-gHalf-iL,h:T}, // Inner room, top wall, right segment {x:cx+gHalf,y:iT,w:iR-(cx+gHalf),h:T}, // Inner room, bottom wall, left segment {x:iL,y:iB,w:cx-gHalf-iL,h:T}, // Inner room, bottom wall, right segment {x:cx+gHalf,y:iB,w:iR-(cx+gHalf),h:T}, // Inner room, left wall, top segment {x:iL,y:iT,w:T,h:cy-gHalf-iT}, // Inner room, left wall, bottom segment {x:iL,y:cy+gHalf,w:T,h:iB-(cy+gHalf)}, // Inner room, right wall, top segment {x:iR,y:iT,w:T,h:cy-gHalf-iT}, // Inner room, right wall, bottom segment {x:iR,y:cy+gHalf,w:T,h:iB-(cy+gHalf)}, // Corridor side walls (close off areas that aren't corridors) // Top-left corner filler (seals NW quadrant diagonals) {x:T, y:T, w:iL-T, h:iT-T}, // Top-right corner filler {x:iR+T,y:T, w:W-T-(iR+T),h:iT-T}, // Bottom-left corner filler {x:T, y:iB+T,w:iL-T, h:H-T-(iB+T)}, // Bottom-right corner filler {x:iR+T,y:iB+T,w:W-T-(iR+T),h:H-T-(iB+T)}, ]; // Doorways: 4 gaps in outer wall, each gets a barricade // Top door: cx-gHalf .. cx+gHalf, y=0..T // Bottom door: same x, y=H-T..H // Left door: x=0..T, cy-gHalf..cy+gHalf // Right door: x=W-T..W, same y const bT=Math.round(T*1.6); // barricade thickness (slightly thicker than wall) const barricades=[ // Top doorway barricade {x:cx-gHalf, y:0, w:gHalf*2, h:bT, hp:100, door:'top'}, // Bottom doorway barricade {x:cx-gHalf, y:H-bT,w:gHalf*2, h:bT, hp:100, door:'bottom'}, // Left doorway barricade {x:0, y:cy-gHalf, w:bT, h:gHalf*2, hp:100, door:'left'}, // Right doorway barricade {x:W-bT,y:cy-gHalf, w:bT, h:gHalf*2, hp:100, door:'right'}, // Inner room top door barricade {x:cx-gHalf,y:iT, w:gHalf*2, h:bT, hp:100, door:'inner-top'}, // Inner room bottom door barricade {x:cx-gHalf,y:iB-bT,w:gHalf*2, h:bT, hp:100, door:'inner-bottom'}, // Inner room left door barricade {x:iL, y:cy-gHalf,w:bT, h:gHalf*2, hp:100, door:'inner-left'}, // Inner room right door barricade {x:iR-bT,y:cy-gHalf,w:bT, h:gHalf*2, hp:100, door:'inner-right'}, ]; return {walls,barricades,iL,iR,iT,iB,T,gHalf,cx,cy}; } function buildBars(){ const m=getMap(); G.walls=m.walls; G.barricades=m.barricades; G._map=m; } function spawnRound(){ G.roundActive=true; const n=Math.min(4+G.round*2,30); G.zombiesLeft=n; const cx=CW/2,cy=CH/2; // Spawn just outside each of the 4 outer doorways so zombies funnel through const spawnPts=[ ()=>({x:cx+(Math.random()-.5)*CW*.12, y:-18}), // top door ()=>({x:cx+(Math.random()-.5)*CW*.12, y:CH+18}), // bottom door ()=>({x:-18, y:cy+(Math.random()-.5)*CH*.12}), // left door ()=>({x:CW+18, y:cy+(Math.random()-.5)*CH*.12}), // right door ]; for(let i=0;i3&&Math.random()5&&Math.random()0)G.shakeT-=dt; if(G.phase!=='playing')return; const p=G.p,w=curWpn(); // Movement if(G.f_player||G.f_move){ let mx=0,my=0; if(G.keys['KeyW']||G.keys['ArrowUp']) my=-1; if(G.keys['KeyS']||G.keys['ArrowDown']) my=1; if(G.keys['KeyA']||G.keys['ArrowLeft']) mx=-1; if(G.keys['KeyD']||G.keys['ArrowRight'])mx=1; const len=Math.hypot(mx,my)||1; if(mx||my){ const spd=3.2*dt; let nx=p.x+mx/len*spd,ny=p.y+my/len*spd; if(!hitBar(nx,p.y,p.r))p.x=nx; if(!hitBar(p.x,ny,p.r))p.y=ny; p.x=Math.max(p.r+2,Math.min(CW-p.r-2,p.x)); p.y=Math.max(p.r+2,Math.min(CH-p.r-2,p.y)); } } p.angle=Math.atan2(G.my-p.y,G.mx-p.x); // Reload if(p.reloading){p.reloadT-=dt;if(p.reloadT=w.fire&&p.ammo>0&&!p.reloading){ G.lastShot=0;p.ammo--; if(p.ammo1?(i/(w.bullets-1)-.5)*w.spread:0; const ang=p.angle+ao+(Math.random()-.5)*w.spread*.1; G.bullets.push({x:p.x+Math.cos(p.angle)*13,y:p.y+Math.sin(p.angle)*13, vx:Math.cos(ang)*10,vy:Math.sin(ang)*10, dmg:w.dmg,life:55,col:w.col, explosive:w.explosive||false,homing:w.homing||false}); } G.shakeT=3;G.shakeAmt=1.8; } } // Bullets for(let i=G.bullets.length-1;i>=0;i--){ const b=G.bullets[i]; if(b.homing&&G.zombies.length){ let nx=G.zombies[0].x,ny=G.zombies[0].y,nd=Infinity; G.zombies.forEach(z=>{const d=Math.hypot(b.x-z.x,b.y-z.y);if(dCW+10||b.yCH+10){G.bullets.splice(i,1);continue;} let stopped=false; // Stop on static walls if(G.walls){for(const w2 of G.walls){if(b.x>w2.x&&b.xw2.y&&b.y=0;i--){ const z=G.zombies[i];if(z.hit>0)z.hit-=dt; const dx=p.x-z.x,dy=p.y-z.y,dist=Math.hypot(dx,dy); if(dist>.5){ let nx=z.x+dx/dist*z.spd*dt,ny=z.y+dy/dist*z.spd*dt; if(!hitBarZombie(nx,z.y,z.r-2))z.x=nx; if(!hitBarZombie(z.x,ny,z.r-2))z.y=ny; } // Damage barricades (only inner ones slow zombies; outer ones face zombie paths) for(let bi=G.barricades.length-1;bi>=0;bi--){ const bar=G.barricades[bi]; if(z.x+z.r>bar.x&&z.x-z.rbar.y&&z.y-z.r=0;bi--){ const b=G.bullets[bi]; const bd=Math.hypot(b.x-z.x,b.y-z.y); if(bd{const d=Math.hypot(b.x-z2.x,b.y-z2.y);if(d=0;i--){const p2=G.particles[i];p2.x+=p2.vx;p2.y+=p2.vy;p2.life-=dt;p2.vx*=.86;p2.vy*=.86;if(p2.lifeG.bestRound){G.bestRound=G.round;localStorage.setItem('bz_best',G.bestRound);} sout('✅ Round '+G.round+' complete! Next round in 3...'); } if(G.interRound){G.interTimer-=dt;if(G.interTimerG.bestRound){G.bestRound=G.round;localStorage.setItem('bz_best',G.bestRound);} sout('💀 Died on Round '+G.round+'. '+G.kills+' kills. Click to restart.'); } // Proximity prompts, barricade repair, wall buy, mystery box G.promptBuy={active:false,buyIdx:-1,label:'',repairIdx:-1}; // Barricade repair check (highest priority, always available) if(G.f_barricades){ const REPAIR_RANGE=CW*.1; for(let i=0;ib.x&&x-rb.y&&y-rb.x&&x-rb.y&&y-rb.x&&x-rb.y&&y-r0){ctx.translate((Math.random()-.5)*G.shakeAmt,(Math.random()-.5)*G.shakeAmt);} ctx.clearRect(-8,-8,CW+16,CH+16); if(!G.f_arena){drawTitle();ctx.restore();return;} const W=CW,H=CH; const m=G._map||getMap(); // use cached map or compute // -- Background (unreachable corners = very dark) -- ctx.fillStyle='#0a0804';ctx.fillRect(0,0,W,H); // -- Corridor floors -- const corridorCol='#1a1710'; ctx.fillStyle=corridorCol; // Top corridor ctx.fillRect(W/2-m.gHalf,m.T,m.gHalf*2,m.iT-m.T); // Bottom corridor ctx.fillRect(W/2-m.gHalf,m.iB+m.T,m.gHalf*2,H-m.T-(m.iB+m.T)); // Left corridor ctx.fillRect(m.T,H/2-m.gHalf,m.iL-m.T,m.gHalf*2); // Right corridor ctx.fillRect(m.iR+m.T,H/2-m.gHalf,W-m.T-(m.iR+m.T),m.gHalf*2); // -- Inner room floor -- const innerGr=ctx.createLinearGradient(m.iL,m.iT,m.iR,m.iB); innerGr.addColorStop(0,'#201e16');innerGr.addColorStop(1,'#18160e'); ctx.fillStyle=innerGr;ctx.fillRect(m.iL,m.iT,m.iR-m.iL,m.iB-m.iT); // -- Floor tile grid (corridors + inner room only) -- ctx.strokeStyle='rgba(255,255,255,.028)';ctx.lineWidth=1; const tSize=Math.round(W/18); for(let gx=0;gx{ // Wall body ctx.fillStyle='#2a2520';ctx.fillRect(w2.x,w2.y,w2.w,w2.h); // Brick texture lines ctx.strokeStyle='rgba(0,0,0,.45)';ctx.lineWidth=1; const bw=Math.round(tSize*.7),bh=Math.round(m.T*.45); if(w2.w>=w2.h){ // Horizontal wall, draw horizontal brick rows for(let by=w2.y;by=w2.h){ctx.fillRect(w2.x,w2.y,w2.w,2);} else{ctx.fillRect(w2.x,w2.y,2,w2.h);} }); } // Barricades (boarded-up doorways) if(G.f_barricades){ G.barricades.forEach(b=>{ const pct=b.hp/100; const horiz=b.w>=b.h; // Destroyed barricade = rubble on floor (still visible, player can rebuild) if(b.destroyed){ ctx.fillStyle='rgba(100,70,30,.35)';ctx.fillRect(b.x,b.y,b.w,b.h); ctx.strokeStyle='rgba(245,200,66,.4)';ctx.lineWidth=1;ctx.setLineDash([3,3]); ctx.strokeRect(b.x+1,b.y+1,b.w-2,b.h-2);ctx.setLineDash([]); ctx.fillStyle='rgba(245,200,66,.55)';ctx.font=`bold ${W*.024}px JetBrains Mono,monospace`; ctx.textAlign='center';ctx.fillText('[E] Rebuild',b.x+b.w/2,b.y+b.h/2+4); return; } // Wood plank base ctx.fillStyle=`rgba(${Math.round(120+35*pct)},${Math.round(70+20*pct)},${Math.round(30+10*pct)},${.7+pct*.25})`; ctx.fillRect(b.x,b.y,b.w,b.h); // Plank lines ctx.strokeStyle=`rgba(0,0,0,${.4+pct*.2})`;ctx.lineWidth=1.5; if(horiz){ const planks=3;const ph=b.h/planks; for(let pi=0;pi.6?'#4ade80':pct>.3?'#f5c842':'#f87171'; if(horiz){ctx.fillRect(b.x,b.y-6,barLen*pct,4);} else{ctx.fillRect(b.x-6,b.y,4,barLen*pct);} // Glow when healthy if(pct>.8){ ctx.strokeStyle=`rgba(74,222,128,${(pct-.8)*1.5})`;ctx.lineWidth=1.5; ctx.strokeRect(b.x,b.y,b.w,b.h); } }); } // Wall buys if(G.f_wallbuy){ WALL_BUYS.forEach((wb,i)=>{ const wx=wb.x*W,wy=wb.y*H; const w2=WEAPONS[wb.wpn]; const near=G.promptBuy.buyIdx===i; ctx.fillStyle=near?'rgba(245,200,66,.22)':'rgba(245,200,66,.07)'; ctx.strokeStyle=near?'rgba(245,200,66,.8)':'rgba(245,200,66,.25)'; ctx.lineWidth=near?2:1; // Wall plate ctx.beginPath();ctx.roundRect(wx-22,wy-18,44,36,5);ctx.fill();ctx.stroke(); // Weapon icon + cost ctx.fillStyle=near?'#f5c842':'rgba(245,200,66,.65)'; ctx.font=`bold ${W*.032}px JetBrains Mono,monospace`;ctx.textAlign='center'; ctx.fillText(w2.icon,wx,wy-3); ctx.font=`${W*.024}px JetBrains Mono,monospace`; ctx.fillText(w2.cost+'pts',wx,wy+12); }); } // Mystery Box if(G.f_box){ const bx=G.boxAnim.x,by=G.boxAnim.y; const spinning=G.boxAnim.active; const dispWpn=spinning?WEAPONS[BOX_POOL[G.boxAnim.idx]]:null; // Glow const grd=ctx.createRadialGradient(bx,by,0,bx,by,40); grd.addColorStop(0,'rgba(245,200,66,.22)');grd.addColorStop(1,'rgba(245,200,66,0)'); ctx.fillStyle=grd;ctx.beginPath();ctx.arc(bx,by,40,0,Math.PI*2);ctx.fill(); // Box body ctx.fillStyle=spinning?'rgba(245,200,66,.45)':'rgba(80,60,10,.9)'; ctx.strokeStyle=spinning?'#f5c842':'rgba(245,200,66,.5)'; ctx.lineWidth=spinning?2.5:1.5; ctx.beginPath();ctx.roundRect(bx-18,by-18,36,36,5);ctx.fill();ctx.stroke(); // Question mark or weapon icon ctx.fillStyle='#f5c842';ctx.font=`bold ${W*.055}px Bebas Neue,sans-serif`;ctx.textAlign='center'; ctx.fillText(spinning?dispWpn.icon:'?',bx,by+10); // Cost label ctx.font=`${W*.023}px JetBrains Mono,monospace`;ctx.fillStyle='rgba(245,200,66,.7)'; ctx.fillText(BOX_COST+'pts',bx,by+30); // Spin ring if(spinning){ const prog=1-(G.boxAnim.timer/90); ctx.strokeStyle='#f5c842';ctx.lineWidth=2.5; ctx.beginPath();ctx.arc(bx,by,24,-Math.PI/2,-Math.PI/2+prog*Math.PI*2);ctx.stroke(); } } // Particles G.particles.forEach(p2=>{ const a=Math.min(1,p2.life/14);ctx.globalAlpha=a; ctx.fillStyle=p2.col;ctx.beginPath();ctx.arc(p2.x,p2.y,2,0,Math.PI*2);ctx.fill(); });ctx.globalAlpha=1; // Bullets G.bullets.forEach(b=>{ ctx.fillStyle=b.col;ctx.beginPath();ctx.arc(b.x,b.y,b.explosive?4:2.5,0,Math.PI*2);ctx.fill(); if(b.explosive){ctx.strokeStyle='rgba(255,130,0,.4)';ctx.lineWidth=1;ctx.beginPath();ctx.arc(b.x,b.y,8,0,Math.PI*2);ctx.stroke();} ctx.strokeStyle='rgba(255,255,255,.2)';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(b.x,b.y);ctx.lineTo(b.x-b.vx*2,b.y-b.vy*2);ctx.stroke(); }); // Zombies G.zombies.forEach(z=>{ const hit=z.hit>0; ctx.fillStyle='rgba(0,0,0,.26)';ctx.beginPath();ctx.ellipse(z.x,z.y+z.r*.65,z.r*.72,z.r*.25,0,0,Math.PI*2);ctx.fill(); ctx.fillStyle=hit?'#fff':(z.runner?'#a3e635':z.big?'#f87171':'#4ade80'); ctx.beginPath();ctx.arc(z.x,z.y,z.r,0,Math.PI*2);ctx.fill(); ctx.strokeStyle=z.runner?'#65a30d':z.big?'#b91c1c':'#16a34a';ctx.lineWidth=1.5;ctx.stroke(); // Eyes ctx.fillStyle='#0a1a0a'; ctx.beginPath();ctx.arc(z.x-z.r*.3,z.y-z.r*.15,z.r*.17,0,Math.PI*2);ctx.fill(); ctx.beginPath();ctx.arc(z.x+z.r*.3,z.y-z.r*.15,z.r*.17,0,Math.PI*2);ctx.fill(); ctx.strokeStyle='#0a1a0a';ctx.lineWidth=1.5;ctx.beginPath();ctx.arc(z.x,z.y+z.r*.1,z.r*.32,.2,Math.PI-.2);ctx.stroke(); // HP bar const pct=z.hp/z.maxHp; ctx.fillStyle='rgba(0,0,0,.5)';ctx.fillRect(z.x-z.r,z.y-z.r-6,z.r*2,3); ctx.fillStyle=pct>.5?'#4ade80':'#f87171';ctx.fillRect(z.x-z.r,z.y-z.r-6,z.r*2*pct,3); // Type label if(z.big||z.runner){ ctx.fillStyle=z.big?'#f87171':'#a3e635';ctx.font=`bold ${W*.024}px JetBrains Mono,monospace`;ctx.textAlign='center'; ctx.fillText(z.big?'BRUTE':'RUNNER',z.x,z.y-z.r-10); } }); // Player if(G.f_player){ const{x,y,angle,r,hp,maxHp,reloading,ammo}=G.p; ctx.fillStyle='rgba(0,0,0,.26)';ctx.beginPath();ctx.ellipse(x,y+r*.58,r*.68,r*.24,0,0,Math.PI*2);ctx.fill(); ctx.fillStyle='#e8eaf0';ctx.beginPath();ctx.arc(x,y,r,0,Math.PI*2);ctx.fill(); ctx.strokeStyle='rgba(255,255,255,.32)';ctx.lineWidth=1.5;ctx.stroke(); if(G.f_shoot){ const wx=x+Math.cos(angle)*15,wy=y+Math.sin(angle)*15; ctx.strokeStyle=curWpn().col;ctx.lineWidth=3;ctx.lineCap='round'; ctx.beginPath();ctx.moveTo(x,y);ctx.lineTo(wx,wy);ctx.stroke();ctx.lineCap='butt'; } // HP bar const pct=hp/maxHp; ctx.fillStyle='rgba(0,0,0,.6)';ctx.fillRect(x-13,y-r-8,26,4); ctx.fillStyle=pct>.5?'#4ade80':pct>.25?'#f5c842':'#f87171';ctx.fillRect(x-13,y-r-8,26*pct,4); // Weapon + ammo top-left ctx.fillStyle='rgba(0,0,0,.55)';ctx.fillRect(4,4,78,32); ctx.fillStyle='#f5c842';ctx.font=`bold ${W*.03}px JetBrains Mono,monospace`;ctx.textAlign='left'; ctx.fillText(curWpn().icon+' '+curWpn().name,8,18); if(reloading){ ctx.fillStyle='#f87171';ctx.fillText('RELOADING...',8,30); ctx.strokeStyle='#f5c842';ctx.lineWidth=1.5; ctx.beginPath();ctx.arc(x,y,r+5,-Math.PI/2,-Math.PI/2+(1-G.p.reloadT/55)*Math.PI*2);ctx.stroke(); } else { const w2=curWpn(); for(let i=0;i=0; ctx.font=`bold ${W*.026}px JetBrains Mono,monospace`;ctx.textAlign='center'; const tw=ctx.measureText(lbl).width+22; ctx.fillStyle=isRepair?'rgba(74,222,128,.88)':'rgba(245,200,66,.88)'; ctx.beginPath();ctx.roundRect(W/2-tw/2,H-40,tw,22,4);ctx.fill(); ctx.fillStyle='#0a1a0a'; ctx.fillText(lbl,W/2,H-24); } } // Round banner (inter-round) if(G.interRound&&G.interTimer>130){ const a=Math.min(1,(200-G.interTimer)/30); ctx.fillStyle=`rgba(0,0,0,${a*.7})`;ctx.fillRect(0,H/2-24,W,46); ctx.fillStyle=`rgba(74,222,128,${a})`;ctx.font=`bold ${W*.062}px Bebas Neue,sans-serif`;ctx.textAlign='center'; ctx.fillText('ROUND '+G.round+' COMPLETE!',W/2,-4+H/2); ctx.fillStyle=`rgba(200,255,200,${a*.55})`;ctx.font=`${W*.028}px JetBrains Mono,monospace`; ctx.fillText('ROUND '+(G.round+1)+' INCOMING...',W/2,H/2+18); } if(G.phase==='dead')drawDead(); ctx.restore(); } function drawTitle(){ ctx.fillStyle='rgba(0,0,0,.88)';ctx.fillRect(0,0,CW,CH); ctx.fillStyle='#4ade80';ctx.font=`bold ${CW*.075}px Bebas Neue,sans-serif`;ctx.textAlign='center';ctx.fillText('BLUEPRINT ZOMBIES',CW/2,CH*.28); ctx.fillStyle='rgba(245,200,66,.8)';ctx.font=`${CW*.028}px JetBrains Mono,monospace`; ctx.fillText('Wall Buys · Mystery Box · Points · Rounds',CW/2,CH*.41); ctx.fillStyle='rgba(255,255,255,.38)';ctx.font=`${CW*.024}px JetBrains Mono,monospace`; ctx.fillText('barricades · brutes · runners · explosive weapons',CW/2,CH*.49); ctx.fillStyle='rgba(74,222,128,.62)';ctx.font=`bold ${CW*.033}px Nunito,sans-serif`;ctx.fillText('Complete Step 1 to begin',CW/2,CH*.65); ctx.fillStyle='rgba(255,255,255,.2)';ctx.font=`${CW*.024}px JetBrains Mono,monospace`;ctx.fillText('or hit Quick Play',CW/2,CH*.74);ctx.textAlign='left'; } function drawDead(){ ctx.fillStyle='rgba(0,0,0,.8)';ctx.fillRect(0,0,CW,CH); ctx.fillStyle='#f87171';ctx.font=`bold ${CW*.09}px Bebas Neue,sans-serif`;ctx.textAlign='center';ctx.fillText('YOU DIED',CW/2,CH*.3); ctx.fillStyle='#f5c842';ctx.font=`${CW*.038}px Bebas Neue,sans-serif`;ctx.fillText('Round '+G.round+' · '+G.kills+' kills · $'+G.points,CW/2,CH*.44); ctx.fillStyle='rgba(255,255,255,.55)';ctx.font=`${CW*.028}px JetBrains Mono,monospace`;ctx.fillText('Best: Round '+G.bestRound,CW/2,CH*.54); ctx.fillStyle='rgba(74,222,128,.72)';ctx.font=`bold ${CW*.034}px Nunito,sans-serif`;ctx.fillText('Tap or press R to restart',CW/2,CH*.7);ctx.textAlign='left'; } function updateHUD(){ const s=(v)=>String(v); document.getElementById('hudRound').textContent=s(G.round); document.getElementById('hudPts').textContent='$'+s(G.points); document.getElementById('hudKills').textContent=s(G.kills); document.getElementById('hudAmmo').textContent=s(G.p.ammo)+'/'+s(curWpn().mag); document.getElementById('hudHP').textContent=s(Math.max(0,Math.round(G.p.hp))); document.getElementById('fsRound').textContent=s(G.round); document.getElementById('fsPts').textContent='$'+s(G.points); document.getElementById('fsKills').textContent=s(G.kills); document.getElementById('fsAmmo').textContent=s(G.p.ammo)+'/'+s(curWpn().mag); document.getElementById('fsHP').textContent=s(Math.max(0,Math.round(G.p.hp))); document.getElementById('fsWpn').textContent=curWpn().icon+' '+curWpn().name; updateCard(); } function updateCard(){ document.getElementById('heroRound').textContent='Round '+G.round; document.getElementById('hw-pts').textContent='$'+G.points; document.getElementById('hw-wpn').textContent=curWpn().icon+' '+curWpn().name; document.getElementById('hw-wb').textContent=G.f_wallbuy?'Active ?':'Off'; document.getElementById('hw-mb').textContent=G.f_box?'Active ?':'Off'; document.getElementById('hw-best').textContent='Round '+G.bestRound; } function sout(t){document.getElementById('obar').textContent=t;document.getElementById('liveStatus').textContent=t.slice(0,40);} /* FULLSCREEN */ const fsCanvas=document.getElementById('gcFS'); const fsCtx=fsCanvas?fsCanvas.getContext('2d'):null; let fsOpen=false; function openFS(){ document.getElementById('fsOverlay').classList.add('open'); fsOpen=true;requestAnimationFrame(sizeFS); if((G.phase==='title'||G.phase==='dead')&&G.f_fullGame)startGame(); if(fsCanvas){ fsCanvas.addEventListener('mousemove',e=>{const r=fsCanvas.getBoundingClientRect(),sc=CW/r.width;G.mx=(e.clientX-r.left)*sc;G.my=(e.clientY-r.top)*sc;}); fsCanvas.addEventListener('mousedown',e=>{if(!e.button)G.keys['LMB']=true;}); fsCanvas.addEventListener('mouseup', e=>{if(!e.button)G.keys['LMB']=false;}); fsCanvas.addEventListener('click',()=>{if(G.phase==='dead')startGame();}); } } function closeFS(){document.getElementById('fsOverlay').classList.remove('open');fsOpen=false;} function sizeFS(){ if(!fsCanvas)return; const avH=window.innerHeight-72;const s=Math.min(window.innerWidth,avH); fsCanvas.style.width=s+'px';fsCanvas.style.height=s+'px';fsCanvas.width=CW;fsCanvas.height=CH; } window.addEventListener('resize',sizeFS); function syncFS(){ if(!fsOpen||!fsCtx||!fsCanvas)return; fsCtx.clearRect(0,0,fsCanvas.width,fsCanvas.height); fsCtx.drawImage(canvas,0,0,fsCanvas.width,fsCanvas.height); document.getElementById('fsFoot').textContent=document.getElementById('obar').textContent; } document.addEventListener('keydown',e=>{if(e.key==='Escape')closeFS();}); /* QUICK PLAY */ function quickPlay(){ G.f_arena=true;G.f_player=true;G.f_move=true;G.f_shoot=true; G.f_zombies=true;G.f_rounds=true;G.f_points=true; G.f_wallbuy=true;G.f_box=true;G.f_barricades=true;G.f_fullGame=true; G.bestRound=parseInt(localStorage.getItem('bz_best')||'0'); startGame();openFS(); sout('⚡ Quick Play! WASD=move · Click/fire=shoot · [E]/BUY=wall buy or box · Survive the rounds!'); } /* SAVE & SHARE */ const SAVE_KEY='bz_progress'; function saveProgress(){localStorage.setItem(SAVE_KEY,JSON.stringify({done:[...doneSet],step:cur,v:1}));} function loadProgress(){ try{ const d=JSON.parse(localStorage.getItem(SAVE_KEY)||'{}'); if(d.done)d.done.forEach(i=>doneSet.add(i)); if(typeof d.step==='number')cur=Math.min(d.step,STEPS.length-1); doneSet.forEach(i=>{const s=STEPS[i];if(s?.onApply)s.onApply({});}); }catch(e){} } function saveShare(){ saveProgress(); const url=window.location.href.split('#')[0]+'#s='+cur+'&d='+[...doneSet].join(','); navigator.clipboard?.writeText(url).then(()=>{ const t=document.getElementById('shareToast'); t.textContent='✅ Link copied! Step '+(cur+1)+', '+doneSet.size+'/'+STEPS.length+' done'; t.classList.add('show');setTimeout(()=>t.classList.remove('show'),2800); }).catch(()=>prompt('Copy link:',url)); } function parseHash(){ try{ const h=window.location.hash.slice(1);if(!h)return; const p=Object.fromEntries(h.split('&').map(x=>x.split('='))); if(p.d)p.d.split(',').filter(Boolean).forEach(i=>doneSet.add(parseInt(i))); if(p.s)cur=Math.min(parseInt(p.s)||0,STEPS.length-1); doneSet.forEach(i=>{const s=STEPS[i];if(s?.onApply)s.onApply({});}); }catch(e){} } /* ------------------------------------------- STEP DEFINITIONS ------------------------------------------- */ function nav(i){return`← BackNext →`;} const STEPS=[ /* 01 */ {tag:'STEP 01 · SETUP
- Connect Event BeginPlay ? Setup Map ? Spawn Player. This is the first wire in any Blueprint, it confirms the engine is running and builds your survival arena.

## Event BeginPlay',x:10,y:30,w:148,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Setup Survival Map',sub:'Level Init',x:180,y:10,w:165,lp:[{id:'l2',type:'exec',label:''}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'func',title:'Spawn Player',sub:'SurvivorPawn',x:368,y:10,w:152,lp:[{id:'l3',type:'exec',label:''}]}, ],[{f:'r1',t:'l2'},{f:'r2',t:'l3'}],[]), boxes:[{t:'box-c',tx:'White arrow pins = exec wires. They control the order nodes run. BeginPlay fires once when the game starts, then flows right.'},{t:'box-u',tx:'Add a NavMesh Bounds Volume covering the full arena. Press P in editor to see the green walkable area. Zombies use this to pathfind around obstacles.'}], onApply(){G.f_arena=true;G.phase='title';sout('✅ Arena built! Complete more steps to enable gameplay.');} }, /* 02 */ {tag:'STEP 02 · CHARACTER
- Set up SurvivorCharacter. Wire BeginPlay ? Set Camera Height. Type a camera height value, this sets the overhead view angle.

## Event BeginPlay',x:10,y:34,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Set Camera Height',sub:'Spring Arm Z',x:178,y:12,w:165,lp:[{id:'l2e',type:'exec',label:''},{id:'l2v',type:'float',label:'Height'}],rp:[{id:'r2',type:'exec',label:''}],vf:{hint:'Height 500-1000 (e.g. 750)',val:''}}, {id:'n3',type:'func',title:'Lock Camera Pitch',sub:'-90 overhead',x:366,y:12,w:162,lp:[{id:'l3',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2',t:'l3'}],[{id:'n2',ok:v=>parseFloat(v)>=500&&parseFloat(v)<=1000}]), boxes:[{t:'box-c',tx:'Spring Arm pitch -90° = straight-down overhead camera. Disable "Use Pawn Control Rotation" so the camera never rotates with the character.'},{t:'box-t',tx:'Add a Capsule Component (radius 34, half-height 88) and a Skeletal Mesh. The capsule does all physics, the mesh is visual only.'}], onApply(){G.f_player=true;G.p.x=CW/2;G.p.y=CH/2;if(G.phase==='title')G.phase='playing';sout('✅ Survivor spawned! Camera locked overhead.');} }, /* 03 */ {tag:'STEP 03 · INPUT
- Wire Enhanced Input for 8-direction movement. Connect IA_Move (Triggered) ? Break Vector2D ? Add Movement Input for both X and Y axes.

## IA_Move (Triggered)',sub:'Enhanced Input',x:4,y:22,w:168,rp:[{id:'r1e',type:'exec',label:''},{id:'r1v',type:'vec3',label:'Value'}]}, {id:'n2',type:'func',title:'Break Vector2D',x:196,y:8,w:135,lp:[{id:'l2v',type:'vec3',label:''}],rp:[{id:'r2x',type:'float',label:'X'},{id:'r2y',type:'float',label:'Y'}]}, {id:'n3',type:'func',title:'Add Mvmt X',x:355,y:4,w:145,lp:[{id:'l3e',type:'exec',label:''},{id:'l3s',type:'float',label:'Scale'}],rp:[{id:'r3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Add Mvmt Y',x:355,y:78,w:145,lp:[{id:'l4e',type:'exec',label:''},{id:'l4s',type:'float',label:'Scale'}]}, ],[{f:'r1e',t:'l3e'},{f:'r1v',t:'l2v'},{f:'r2x',t:'l3s'},{f:'r2y',t:'l4s'}],[]), boxes:[{t:'box-c',tx:'Axis2D returns both X and Y in one action. Break Vector2D unpacks them. X feeds right/left movement, Y feeds forward/back.'},{t:'box-t',tx:'Set Max Walk Speed to 380 in Character Movement Component. Fast enough to dodge, not fast enough to trivially escape a horde.'}], onApply(){G.f_move=true;sout('✅ WASD active! WASD or the D-pad to move.');} }, /* 04 */ {tag:'STEP 04 · SHOOTING
- Wire mouse aim using Get Hit Result Under Cursor. Then IA_Shoot ? Get Muzzle Socket Location ? Spawn Bullet. Click the arena to fire.

## IA_Shoot',sub:'Triggered',x:4,y:30,w:130,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Get Socket Location',sub:'Muzzle',x:156,y:8,w:162,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2l',type:'vec3',label:'Location'}]}, {id:'n3',type:'func',title:'Spawn Bullet Actor',x:340,y:8,w:155,lp:[{id:'l3e',type:'exec',label:''},{id:'l3l',type:'vec3',label:'Location'}],rp:[{id:'r3',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2l',t:'l3l'}],[]), boxes:[{t:'box-c',tx:'BP_Bullet: Projectile Movement Component, initial speed 2000. OnComponentHit: apply damage to overlapping zombies via Apply Point Damage.'},{t:'box-t',tx:'Muzzle socket: add a socket to the weapon mesh in the Skeleton editor at the barrel tip. Every bullet and muzzle flash spawns from exactly here.'}], onApply(){G.f_shoot=true;sout('✅ Shooting live! Click or tap the fire button.');} }, /* 05 */ {tag:'STEP 05 · AI
- Create ZombieCharacter BP with an AI Controller. Wire BeginPlay ? Run Behavior Tree (BT_Zombie) to start the chase.

## Event BeginPlay',x:8,y:30,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Run Behavior Tree',sub:'BT_Zombie',x:175,y:8,w:168,lp:[{id:'l2e',type:'exec',label:''},{id:'l2bt',type:'object',label:'BT Asset'}],rp:[{id:'r2',type:'exec',label:''}]}, {id:'n3',type:'vget',title:'BT_Zombie Asset',x:8,y:90,w:152,rp:[{id:'r3',type:'object',label:'BTree Ref'}]}, ],[{f:'r1',t:'l2e'},{f:'r3',t:'l2bt'}],[]), boxes:[{t:'box-ai',tx:'Behavior Tree: Selector → BTService_FindPlayer (updates PlayerLocation Blackboard key each tick) → BTTask_MoveTo(PlayerLocation). Minimal but terrifying at scale.'},{t:'box-u',tx:'Three zombie types in the same BT, differentiate by data: a ZombieType variable drives the speed and HP multipliers set at spawn time.'}], onApply(){G.f_zombies=true;if(G.f_player)spawnRound();sout('✅ Zombies spawning! They\'re coming for you.');} }, /* 06 */ {tag:'STEP 06 · ROUNDS
- Build the round manager. Wire OnRoundClear → Add to Round Count → Set Timer → Spawn Next Round. Type the inter-round delay.

## On Round Clear',x:4,y:34,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'math',title:'Round Count + 1',x:172,y:10,w:148,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2v',type:'int',label:'New Round'}]}, {id:'n3',type:'func',title:'Set Timer',sub:'Next Round',x:344,y:10,w:138,lp:[{id:'l3e',type:'exec',label:''},{id:'l3t',type:'float',label:'Delay'}],rp:[{id:'r3',type:'exec',label:''}],vf:{hint:'Delay secs (e.g. 4)',val:''}}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'}],[{id:'n3',ok:v=>parseFloat(v)>=1&&parseFloat(v)<=10}]), boxes:[{t:'box-c',tx:'Spawn formula: zombies = 4 + (round × 2). Round 1 = 6. Round 5 = 14. Round 10 = 24. Capped at 30 to keep it playable.'},{t:'box-t',tx:'Runners appear from Round 6 onward (20% chance). Brutes from Round 4 (15%). Weight both toward the end to keep early rounds learnable.'}], onApply(v){G.f_rounds=true;sout('✅ Round system active! '+v['n3']+'s between rounds. Survive!');} }, /* 07 */ {tag:'STEP 07 · ECONOMY
- Wire the point system. OnZombieKill ? Add Points (headshot = double). Connect a headshot check Branch and type the base kill reward.

## On Zombie Kill',x:4,y:34,w:145,rp:[{id:'r1e',type:'exec',label:''},{id:'r1h',type:'bool',label:'Headshot'}]}, {id:'n2',type:'branch',title:'Branch',sub:'Headshot?',x:172,y:10,w:118,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Cond'}],rp:[{id:'r2t',type:'exec',label:'True'},{id:'r2f',type:'exec',label:'False'}]}, {id:'n3',type:'math',title:'Add Points × 2',x:316,y:4,w:138,lp:[{id:'l3e',type:'exec',label:''},{id:'l3v',type:'int',label:'Base pts'}],vf:{hint:'Base pts per kill (e.g. 100)',val:''}}, {id:'n4',type:'math',title:'Add Points × 1',x:316,y:84,w:138,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1e',t:'l2e'},{f:'r1h',t:'l2c'},{f:'r2t',t:'l3e'},{f:'r2f',t:'l4e'}],[{id:'n3',ok:v=>parseInt(v)>=50&&parseInt(v)<=500}]), boxes:[{t:'box-c',tx:'Points are the currency for wall buys and the Mystery Box. Killing a zombie = base points. Headshot = 2× base. Hitting (not killing) = 10pts.'},{t:'box-t',tx:'Display points top-right in UMG with a pop-up number that flies off the kill position (+100, +200 HEADSHOT!), makes the economy feel exciting.'}], onApply(v){G.f_points=true;sout('✅ Points system live! Kill zombies to earn cash. '+v['n3']+' pts per kill.');} }, /* 08, GOLD STEP */ {tag:'STEP 08 🔫 WALL BUY
- Place weapon purchase stations on the walls. Wire OnPlayerNearby ? Show Cost Prompt ? IA_Interact ? Deduct Points ? Give Weapon. Choose which weapon to place on your first wall.

## IA_Interact',sub:'E Key / Buy Btn',x:4,y:34,w:162,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'Has enough pts?',x:188,y:10,w:130,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:'Cond'}],rp:[{id:'r2t',type:'exec',label:'True'}]}, {id:'n3',type:'box',title:'Deduct Cost',sub:'Points Variable',x:342,y:10,w:138,lp:[{id:'l3e',type:'exec',label:''},{id:'l3c',type:'int',label:'Cost'}],rp:[{id:'r3e',type:'exec',label:''},{id:'r3v',type:'int',label:'Remaining'}],vf:{hint:'Wall buy cost (e.g. 500)',val:''}}, {id:'n4',type:'func',title:'Give Weapon BP',x:342,y:95,w:150,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3e',t:'l4e'}],[{id:'n3',ok:v=>parseInt(v)>=100&&parseInt(v)<=5000}],true), boxes:[ {t:'box-gold',tx:'5 wall buy stations: SMG 500pts · Shotgun 750pts · AK-47 1200pts · Sniper 1500pts · LMG 2000pts. Revisiting the same gun refills ammo at 40% of the buy cost.'}, {t:'box-t',tx:'Walk up to a wall station and press E or the BUY button. The game already shows the prompt and colour highlight, try it with any weapon once points are active!'}, ], onApply(v){G.f_wallbuy=true;sout('✅ Wall Buys active! Walk to a weapon icon and press E or BUY. Cost set: '+v['n3']+' pts.');} }, /* 09, GOLD STEP */ {tag:'STEP 09 🎰 MYSTERY BOX
- Place the Mystery Box, a glowing chest that gives a random weapon for 950 points. Wire IA_Interact ? Deduct 950pts ? Spin Animation ? Give Random Weapon.

## IA_Interact',sub:'At Box Position',x:4,y:34,w:162,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'branch',title:'Branch',sub:'pts = 950?',x:188,y:10,w:120,lp:[{id:'l2e',type:'exec',label:''},{id:'l2c',type:'bool',label:''}],rp:[{id:'r2t',type:'exec',label:'True'}]}, {id:'n3',type:'box',title:'Play Box Spin',sub:'Timeline Anim',x:332,y:10,w:148,lp:[{id:'l3e',type:'exec',label:''}],rp:[{id:'r3',type:'exec',label:'On End'}]}, {id:'n4',type:'box',title:'Give Random Weapon',x:332,y:80,w:165,lp:[{id:'l4e',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2t',t:'l3e'},{f:'r3',t:'l4e'}],[]), boxes:[ {t:'box-gold',tx:'Box pool: Shotgun · AK-47 · Sniper · LMG · RPG (explosive) · Wonder Gun (homing, 3-burst) · SMG. Any weapon, could be amazing, could be a downgrade!'}, {t:'box-t',tx:'The Wonder Gun shoots 3 homing bullets that chase the nearest zombie. RPG has only 3 ammo but each shot does 120 damage in a radius. Walk up to the spinning yellow box and press E/BUY!'}, ], onApply(){G.f_box=true;sout('✅ Mystery Box active! Walk to the gold box and press E/BUY. 950pts per spin. Good luck!');} }, /* 10 */ {tag:'STEP 10 · ENVIRONMENT
- Place destructible barricades around the arena. Zombies smash through them over time. Wire IA_Repair ? Check Nearby Barricade ? Add 30 HP (clamped to 100).

## IA_Repair',sub:'E near barricade',x:4,y:30,w:155,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'func',title:'Find Nearest Barricade',x:182,y:8,w:168,lp:[{id:'l2e',type:'exec',label:''}],rp:[{id:'r2e',type:'exec',label:''},{id:'r2b',type:'actor',label:'Barricade'}]}, {id:'n3',type:'math',title:'Clamp HP + 30',sub:'Max 100',x:374,y:8,w:148,lp:[{id:'l3e',type:'exec',label:''},{id:'l3t',type:'actor',label:'Target'}],rp:[{id:'r3',type:'exec',label:''}]}, ],[{f:'r1',t:'l2e'},{f:'r2e',t:'l3e'},{f:'r2b',t:'l3t'}],[]), boxes:[{t:'box-c',tx:'Barricades slow zombies and absorb damage instead of the player. Place them in chokepoints. Repairing costs nothing, just time and positioning.'},{t:'box-t',tx:'Chaos Destruction on barricades: set the mesh as a Geometry Collection. At zero HP trigger the destruction simulation, satisfying splinter-and-collapse effect.'}], onApply(){G.f_barricades=true;buildBars();sout('✅ Barricades placed! Use them for cover. Zombies will break through.');} }, /* 11 */ {tag:'STEP 11 · HUD
- Create WBP_ZombiesHUD. Wire BeginPlay ? Create Widget ? Add to Viewport. Bind: Points, Round, HP bar, Ammo pips, round-clear banner.

## Event BeginPlay',x:4,y:30,w:145,rp:[{id:'r1',type:'exec',label:''}]}, {id:'n2',type:'macro',title:'Sequence',x:172,y:8,w:88,lp:[{id:'l2',type:'exec',label:'In'}],rp:[{id:'r2a',type:'exec',label:'HUD'},{id:'r2b',type:'exec',label:'Player'},{id:'r2c',type:'exec',label:'Waves'},{id:'r2d',type:'exec',label:'Economy'}]}, {id:'n3',type:'func',title:'Create & Show HUD',x:285,y:8,w:148,lp:[{id:'l3',type:'exec',label:''}]}, {id:'n4',type:'func',title:'Enable Player',x:285,y:50,w:148,lp:[{id:'l4',type:'exec',label:''}]}, {id:'n5',type:'func',title:'Start Round 1',x:285,y:92,w:148,lp:[{id:'l5',type:'exec',label:''}]}, {id:'n6',type:'func',title:'Init Points',x:285,y:134,w:148,lp:[{id:'l6',type:'exec',label:''}]}, ],[{f:'r1',t:'l2'},{f:'r2a',t:'l3'},{f:'r2b',t:'l4'},{f:'r2c',t:'l5'},{f:'r2d',t:'l6'}],[]), boxes:[ {t:'box-c',tx:'Round banner: WBP widget animates in for 2s between rounds. Scale punch animation on the round number + colour flash green?white.'}, {t:'box-t',tx:'WASD/D-pad = move · Click/fire = shoot · E/BUY = buy weapon or spin box · Fullscreen for the best experience. Survive as long as you can!'}, ], onApply(){ G.f_arena=true;G.f_player=true;G.f_move=true;G.f_shoot=true; G.f_zombies=true;G.f_rounds=true;G.f_points=true; G.f_wallbuy=true;G.f_box=true;G.f_barricades=true;G.f_fullGame=true; G.bestRound=parseInt(localStorage.getItem('bz_best')||'0'); buildBars();startGame();openFS(); sout('🎮 ALL SYSTEMS LIVE! Survive the rounds. Buy from walls. Spin the box. Good luck!'); } }, // Steps 12-30: content ...[ {tag:'STEP 12 · AI
- Three zombie classes, Shambler (default), Runner (fast, fragile), Brute (tank, high reward). Spawn ratios shift each round.

## Zombie Animations
- Wire the Animation Blueprint for walk, attack, and death. Drive Walk Speed from the Character Movement Component velocity.

## Blood & Hit Effects
- Spawn Niagara hit sparks and blood particles on bullet impact. Persistent blood decals mark the battlefield.

## Hit & Assist Points
- Reward players for every bullet that connects, not just kills. Hitting a zombie = 10pts. Melee hit = 30pts. Kill = 100pts base.

## Power-ups
- Four power-ups drop from Brutes: Max Ammo, Double Points, Insta-Kill, and Carpenter (repairs all barricades).

## Map Zones & Doors
- Add locked door zones. Pay points to unlock new areas of the map, each area has new wall buys and hiding spots.

## Flashlight & Dark Map
- Set the map to night. Each player has a Spot Light flashlight attached to their weapon, casting a cone of light ahead.

## Dynamic Audio
- Wire zombie groans, weapon sounds, and a dynamic music layer that intensifies with zombie count.

## Pause & Restart Menu
- Wire Escape ? Pause Game ? Show WBP_PauseMenu (Resume / Restart / Main Menu). Mobile: always-visible pause button top-right.

## Weapon Upgrades (Pack-a-Punch)
- Add a Pack-a-Punch machine, for 2500pts it upgrades the player\'s current weapon: +50% damage, +25% fire rate, gold visual effect.

## Boss Round (Round 10)
- Every 10 rounds spawn a massive Boss zombie with 3 attack phases. Round 10 gets significantly more dramatic.

## Leaderboard & Best Round
- Save the player\'s best round and top 5 kill counts. Show a stylised Game Over screen with all stats.

## Camera Shake & Rumble
- Three camera shakes: GunShake (small, fast), ExplosionShake (big, 0.5s), LowHPShake (slow heartbeat pulse below 25% HP).

## Zombie Swarm Behaviour
- Add flocking so zombies spread out rather than stacking. Use separation forces to keep them from clustering into a single point.

## Procedural Spawn Points
- Randomise zombie spawn windows each round. Spawners appear at windows and doors, zombies crawl through before entering the map.

## Mobile Optimisation
- Scale performance for mobile: disable Lumen, reduce particles, use HISM for props, cap active decals, enforce 60fps.

## Post Process & Atmosphere
- Set a Post Process Volume: film grain, slight vignette, colour grade (desaturate + green tint for horror), motion blur on fast movement.

## Package & Test
- Package for Windows or HTML5. Test all features: wall buys, mystery box, round escalation, boss round, mobile controls.

## Share Your Zombies Map
- Upload to itch.io, add screenshots, write a devlog, and share your round record with the community!
