/* ============================================================ VIZ โ€” canvas renderer + host loop for LIVE tactical rounds Simulation logic lives in live.js; this file draws the world, plays synthesized audio, and manages cameras/speed. ============================================================ */ 'use strict'; const VIZ={ ok:false, R:null, // {st, cv, ctx, onDone, lastTs} cam:'follow', speed:1, sndOn:true, snd:null, init(){try{const c=document.createElement('canvas');this.ok=!!(c.getContext&&c.getContext('2d'));}catch(e){this.ok=false;}return this.ok;}, setCam(m){this.cam=m;}, cycleSpeed(){this.speed=this.speed===1?2:this.speed===2?3:1;return this.speed;}, /* ---------- synthesized audio ---------- */ ac(){ if(!this.sndOn)return null; if(!this.snd){try{this.snd=new (window.AudioContext||window.webkitAudioContext)();}catch(e){return null;}} if(this.snd.state==='suspended')this.snd.resume(); return this.snd; }, _noise(dur,freq,q,vol,type){ type=type||'bandpass'; const ac=this.ac();if(!ac)return; const n=Math.max(64,Math.floor(ac.sampleRate*dur)); const buf=ac.createBuffer(1,n,ac.sampleRate),d=buf.getChannelData(0); for(let i=0;ithis.tone(1250,0.06,0.1),95);}}, sBoom(){if(this.sndOn)this._noise(0.6,210,0.6,0.45,'lowpass');}, sFlash(){if(this.sndOn)this.tone(2400,0.25,0.12,'sine',3400);}, sBeep(fast){if(this.sndOn)this.tone(fast?1600:1150,0.05,0.08);}, sWinSting(good){ if(!this.sndOn)return; (good?[520,660,880]:[420,330,247]).forEach((f,i)=>setTimeout(()=>this.tone(f,0.16,0.13,'triangle'),i*110)); }, /* ---------- host loop ---------- */ timescale(){return this.speed===1?7:this.speed===2?14:21;}, play(cfg,onDone){ if(!this.ok||typeof LIVE==='undefined'){onDone(null);return;} this.stop(); const cv=document.getElementById('simCanvas'); if(!cv){onDone(null);return;} let st; try{st=LIVE.makeRound(cfg);} catch(e){console.warn('live makeRound failed',e);onDone(null);return;} this.R={st,cv,ctx:cv.getContext('2d'),onDone,lastTs:performance.now(),_nextBeep:0}; const loop=(ts)=>{ const R=this.R;if(!R)return; let dt=(ts-R.lastTs)/1000;R.lastTs=ts; dt=Math.min(dt,0.06); const gdt=dt*this.timescale(); let remain=gdt; while(remain>0&&!R.st.ended){ LIVE.step(R.st,Math.min(remain,0.05)); remain-=0.05; } if(R.st.ended)R.st.endT+=gdt; this.draw(R.st); if(R.st.planted&&!R.st.ended){ const iv=R.st.bombT<10?260:520; if(ts>R._nextBeep){R._nextBeep=ts+iv;this.sBeep(R.st.bombT<10);} } if(R.st.ended&&R.st.endT>=1.15){ const st=R.st;this.stop(); try{onDone(st);}catch(e){console.warn('viz onDone error',e);} return; } R.raf=requestAnimationFrame(loop); }; this.R.raf=requestAnimationFrame(loop); }, skip(){ const R=this.R;if(!R)return; if(R.raf)cancelAnimationFrame(R.raf); let guard=0; while(!R.st.ended&&guard++<6000)LIVE.step(R.st,0.05); this.draw(R.st); setTimeout(()=>{if(this.R===R){const cb=R.onDone,st=R.st;this.stop();cb(st);}},120); }, stop(){if(this.R&&this.R.raf)cancelAnimationFrame(this.R.raf);this.R=null;}, /* ---------- camera ---------- */ camFor(st,W,H){ let scale=1,ox=0,oy=0; if(this.cam==='follow'){ const alive=st.agents.filter(a=>!a.dead); const cx=alive.reduce((s,a)=>s+a.x,0)/Math.max(alive.length,1); const cy=alive.reduce((s,a)=>s+a.y,0)/Math.max(alive.length,1); scale=2.1; ox=W/2-this._sx(st.L,U.clamp(cx,24,st.L.w-24),W,H)*scale; oy=H/2-this._sy(st.L,U.clamp(cy,18,st.L.h-18),W,H)*scale; ox=U.clamp(ox,W-W*scale+6,6);oy=U.clamp(oy,H-H*scale+6,6); } return{scale:scale,ox:ox,oy:oy}; }, _sx(L,x,W,H){return 8+x/L.w*(W-16);}, _sy(L,y,W,H){return 8+y/L.h*(H-16);}, /* ---------- main draw ---------- */ draw(st){ const R=this.R;if(!R)return; const ctx=R.ctx,cv=R.cv,W=cv.width,H=cv.height,L=st.L,P=L.pal; ctx.fillStyle='#0a0f18';ctx.fillRect(0,0,W,H); const cm=this.camFor(st,W,H),scale=cm.scale; ctx.save(); ctx.translate(cm.ox,cm.oy);ctx.scale(scale,scale); if(st.shake>0.05)ctx.translate((Math.random()-0.5)*st.shake*2,(Math.random()-0.5)*st.shake*2); const SX=x=>this._sx(L,x,W,H),SY=y=>this._sy(L,y,W,H); // ground ctx.fillStyle='#0d1420'; ctx.fillRect(-200,-200,W/scale+400,H/scale+400); // walkable floors ctx.fillStyle=P.floor; L.floors.forEach(function(f){ const x=f[0],y=f[1],w=f[2],h=f[3]; ctx.fillRect(SX(x),SY(y),SX(x+w)-SX(x),SY(y+h)-SY(y)); }); ctx.strokeStyle='rgba(255,255,255,0.03)';ctx.lineWidth=0.4; for(let gx=0;gx<=L.w;gx+=10){ctx.beginPath();ctx.moveTo(SX(gx),SY(0));ctx.lineTo(SX(gx),SY(L.h));ctx.stroke();} for(let gy=0;gy<=L.h;gy+=10){ctx.beginPath();ctx.moveTo(SX(0),SY(gy));ctx.lineTo(SX(L.w),SY(gy));ctx.stroke();} // site zones ['A','B'].forEach(function(k){ const s=L.sites[k]; ctx.strokeStyle='rgba(255,150,60,0.3)';ctx.lineWidth=1; ctx.strokeRect(SX(s[0]-12),SY(s[1]-7),SX(s[0]+12)-SX(s[0]-12),SY(s[1]+7)-SY(s[1]-7)); ctx.fillStyle='rgba(255,170,80,0.22)'; ctx.font='bold 11px system-ui';ctx.textAlign='center';ctx.textBaseline='middle'; ctx.fillText(k,SX(s[0]),SY(s[1])); }); // crates (L.crates||[]).forEach(function(c){ const cx=c[0],cy=c[1],s=4; ctx.fillStyle=P.crate; ctx.fillRect(SX(cx-s/2),SY(cy-s/2),SX(cx+s/2)-SX(cx-s/2),SY(cy+s/2)-SY(cy-s/2)); ctx.strokeStyle='rgba(0,0,0,0.45)'; ctx.strokeRect(SX(cx-s/2),SY(cy-s/2),SX(cx+s/2)-SX(cx-s/2),SY(cy+s/2)-SY(cy-s/2)); }); // fires st.fires.forEach(function(f){ const flick=0.75+0.25*Math.sin(performance.now()/55+f.x*7); const x=SX(f.x),y=SY(f.y); const r=(f.r*flick)/scale*9; const g=ctx.createRadialGradient(x,y,0,x,y,r); g.addColorStop(0,'rgba(255,150,40,0.85)');g.addColorStop(1,'rgba(255,60,10,0)'); ctx.fillStyle=g;ctx.beginPath();ctx.arc(x,y,r,0,7);ctx.fill(); }); // smokes st.smokes.forEach(function(s){ const grow=Math.min(s.life*5,1); const fade=s.life>0.75?(1-(s.life-0.75)/0.25):1; const x=SX(s.x),y=SY(s.y),r=s.r/scale*9*Math.max(grow,0.3); const g=ctx.createRadialGradient(x,y,r*0.25,x,y,r); g.addColorStop(0,'rgba(185,195,210,'+(0.85*fade)+')'); g.addColorStop(1,'rgba(185,195,210,'+(0.28*fade)+')'); ctx.fillStyle=g;ctx.beginPath();ctx.arc(x,y,r,0,7);ctx.fill(); }); // nades in flight st.nades.forEach(function(n){ ctx.fillStyle='rgba(0,0,0,0.35)'; ctx.beginPath();ctx.ellipse(SX(n.x),SY(n.y),1.3,0.7,0,0,7);ctx.fill(); ctx.fillStyle=n.type==='smoke'?'#9aa7b8':n.type==='flash'?'#f5f5da':'#5c8a4a'; ctx.beginPath();ctx.arc(SX(n.x),SY(n.y)-n.z*1.6,1.5,0,7);ctx.fill(); }); // corpses & pools st.corpses.forEach(function(c){ c.grow=Math.min((c.grow||0)+0.01,1); ctx.fillStyle='rgba(140,20,25,'+(0.55*c.grow)+')'; ctx.beginPath();ctx.ellipse(SX(c.x),SY(c.y),5*c.grow,3.6*c.grow,c.ang,0,7);ctx.fill(); ctx.save();ctx.translate(SX(c.x),SY(c.y));ctx.rotate(c.ang); ctx.globalAlpha=0.9; ctx.fillStyle=c.ct?'#3c5a7d':'#7d6437'; ctx.fillRect(-3.4,-1.6,6.8,3.2); ctx.beginPath();ctx.arc(3.2,0,1.5,0,7); ctx.fillStyle=c.ct?'#5b83ad':'#a3834c';ctx.fill(); ctx.restore();ctx.globalAlpha=1; }); // agents st.agents.forEach(a=>{ if(a.dead)return; const x=SX(a.x),y=SY(a.y); const bob=a.moving?Math.sin(performance.now()/65+a.bob)*0.4:0; ctx.save();ctx.translate(x,y+bob*0.3); ctx.fillStyle='rgba(0,0,0,0.38)'; ctx.beginPath();ctx.ellipse(0.7,1.5,3.2,2.1,0,0,7);ctx.fill(); ctx.rotate(a.ang); ctx.fillStyle='#10151f'; ctx.fillRect(2.1,-0.5,a.role==='AWP'?5.2:3.6,1.05); ctx.fillStyle=a.isCT?'#4d7fb5':'#c79a4b'; ctx.beginPath();ctx.ellipse(0,0,2.7,2,0,0,7);ctx.fill(); ctx.strokeStyle='rgba(0,0,0,0.55)';ctx.lineWidth=0.5;ctx.stroke(); ctx.fillStyle=a.isCT?'#8db8e4':'#e8c887'; ctx.beginPath();ctx.arc(0.4,0,1.25,0,7);ctx.fill(); ctx.restore(); if(a.blind>0){ ctx.strokeStyle='rgba(255,255,240,'+Math.min(a.blind,1)+')';ctx.lineWidth=1; ctx.beginPath();ctx.arc(x,y,4.8,0,7);ctx.stroke(); } ctx.fillStyle='rgba(0,0,0,0.55)';ctx.fillRect(x-6,y-8.4,12,1.9); ctx.fillStyle=a.hp>60?'#43d17c':a.hp>30?'#e6b23c':'#e05252'; ctx.fillRect(x-6,y-8.4,12*U.clamp(a.hp/100,0,1),1.9); ctx.font='7px system-ui';ctx.textAlign='center';ctx.textBaseline='top'; ctx.fillStyle='rgba(220,230,245,0.88)'; ctx.fillText(a.nick.slice(0,9),x,y+5.2); if(!st.planted&&st.bombCarrier===a&&st.plantP>0)VIZ.bar(ctx,x,y-11,st.plantP,'#ffab4d'); if(st.defuser===a&&st.defuseP>0)VIZ.bar(ctx,x,y-11,st.defuseP,'#63a7e6'); if(st.bombCarrier===a&&!st.planted){ ctx.font='bold 7px system-ui';ctx.fillStyle='#ffab4d'; ctx.fillText('C4',x,y-13.8); } }); // planted bomb if(st.planted){ const bx=SX(st.bombPos.x),by=SY(st.bombPos.y); const iv=st.bombT<10?130:320; const pulse=0.5+0.5*Math.sin(performance.now()/iv); ctx.strokeStyle='rgba(255,130,40,'+(0.4+pulse*0.55)+')';ctx.lineWidth=1.3; ctx.beginPath();ctx.arc(bx,by,5+pulse*4,0,7);ctx.stroke(); ctx.fillStyle='#ff8c28';ctx.fillRect(bx-2.3,by-2.3,4.6,4.6); if(st.defuseP>0){ ctx.strokeStyle='#63a7e6';ctx.lineWidth=1.2; ctx.beginPath();ctx.arc(bx,by,8,-Math.PI/2,-Math.PI/2+U.clamp(st.defuseP,0,1)*Math.PI*2);ctx.stroke(); } } // transient fx st.fx.forEach(f=>{ const p=U.clamp((st.t-f.t0)/f.dur,0,1); if(f.type==='tracer'){ ctx.strokeStyle='rgba(255,235,170,'+(1-p)+')';ctx.lineWidth=0.8; ctx.beginPath();ctx.moveTo(SX(f.x1),SY(f.y1));ctx.lineTo(SX(f.x2),SY(f.y2));ctx.stroke(); }else if(f.type==='muzzle'){ ctx.fillStyle='rgba(255,230,150,'+(1-p)+')'; ctx.beginPath();ctx.arc(SX(f.x),SY(f.y),1.5-p,0,7);ctx.fill(); }else if(f.type==='bloodHit'){ ctx.fillStyle='rgba(190,40,40,'+(1-p)+')'; ctx.beginPath();ctx.arc(SX(f.x),SY(f.y),1.6+p,0,7);ctx.fill(); }else if(f.type==='spark'){ ctx.fillStyle='rgba(255,220,140,'+(1-p)+')'; ctx.beginPath();ctx.arc(SX(f.x),SY(f.y),1+p*1.6,0,7);ctx.fill(); }else if(f.type==='puff'){ ctx.fillStyle='rgba(180,180,180,'+((1-p)*0.5)+')'; ctx.beginPath();ctx.arc(SX(f.x),SY(f.y),1+p*2.2,0,7);ctx.fill(); }else if(f.type==='boom'){ const r=(3+p*13)/scale*5; ctx.strokeStyle='rgba(255,180,80,'+(1-p)+')';ctx.lineWidth=1.4; ctx.beginPath();ctx.arc(SX(f.x),SY(f.y),r,0,7);ctx.stroke(); ctx.fillStyle='rgba(255,200,90,'+((1-p)*0.45)+')'; ctx.beginPath();ctx.arc(SX(f.x),SY(f.y),r*0.6,0,7);ctx.fill(); }else if(f.type==='flashPop'){ ctx.fillStyle='rgba(255,255,255,'+((1-p)*0.92)+')'; ctx.fillRect(0,0,W,H); } }); ctx.restore(); /* ===== screen-space overlays ===== */ if(st.pain>0.02){ const g=ctx.createRadialGradient(W/2,H/2,H*0.32,W/2,H/2,H*0.72); g.addColorStop(0,'rgba(255,30,30,0)'); g.addColorStop(1,'rgba(255,25,25,'+(st.pain*0.36)+')'); ctx.fillStyle=g;ctx.fillRect(0,0,W,H); } // HUD top bar ctx.fillStyle='rgba(8,12,20,0.78)';ctx.fillRect(0,0,W,26); const aA=st.agents.filter(a=>!a.dead&&a.side==='a').length; const aB=st.agents.filter(a=>!a.dead&&a.side==='b').length; for(let i=0;i<5;i++){ ctx.fillStyle=i{ const age=st.t-f.t0; const slide=(1-Math.min(age/0.05,1))*150; const alpha=age>3.2?Math.max(1-(age-3.2)/0.6,0):1; const y=40+i*19; ctx.font='bold 10px system-ui'; const kw=ctx.measureText(f.killer).width; ctx.font='9px ui-monospace,monospace'; const ww=ctx.measureText(' '+f.wpn+' ').width; ctx.font='bold 10px system-ui'; const vw=ctx.measureText(f.victim).width; const bw=kw+ww+vw+18; ctx.fillStyle='rgba(8,12,20,'+(0.72*alpha)+')'; ctx.beginPath();ctx.roundRect(W-bw-slide,y-8,bw,16,4);ctx.fill(); ctx.globalAlpha=alpha; ctx.fillStyle=f.kCT?'#8db8e4':'#e8c887'; ctx.fillText(f.killer,W-vw-ww-9-slide,y); ctx.font='9px ui-monospace,monospace';ctx.fillStyle='rgba(200,210,225,0.85)'; ctx.fillText(' '+f.wpn+' ',W-vw-9-slide,y); ctx.font='bold 10px system-ui'; ctx.fillStyle=f.vCT?'#8db8e4':'#e8c887'; ctx.fillText(f.victim,W-9-slide,y); ctx.globalAlpha=1; }); // callout if(st.callout!==undefined&&st.callout!==null){ const p=(st.t-st.calloutT)/0.55; if(p<1){ const pop=p<0.2?(0.2-p)*5:0; const alpha=p>0.65?Math.max(1-(p-0.65)/0.35,0):1; ctx.save();ctx.globalAlpha=Math.max(alpha,0); ctx.translate(W/2,62-pop*6);ctx.scale(1+pop,1+pop); ctx.font='italic bold 21px system-ui';ctx.textAlign='center';ctx.textBaseline='middle'; ctx.lineWidth=4;ctx.strokeStyle='rgba(5,8,14,0.85)'; ctx.strokeText(st.callout.txt,0,0); ctx.fillStyle=st.callout.color||'#fff'; ctx.fillText(st.callout.txt,0,0); ctx.restore(); }else st.callout=null; } // end banner if(st.ended){ const reasonTxt={elim:'ELIMINATION',bomb:'THE BOMB DETONATED ๐Ÿ’ฅ', defuse:'BOMB DEFUSED',time:'TIME UP โ€” DEFENSE HOLDS'}[st.reason]||''; const wt=st.cfg.tags[st.winner]||'?'; ctx.fillStyle='rgba(6,10,18,0.74)';ctx.fillRect(0,H/2-34,W,68); ctx.fillStyle=st.cfg.colors[st.winner]||'#fff';ctx.font='bold 20px system-ui'; ctx.textAlign='center';ctx.textBaseline='middle'; ctx.fillText(wt+' WIN THE ROUND',W/2,H/2-9); ctx.fillStyle='rgba(255,255,255,0.8)';ctx.font='12px system-ui'; ctx.fillText(reasonTxt+' ยท '+st.cfg.scoreAfter[0]+' : '+st.cfg.scoreAfter[1],W/2,H/2+14); } }, bar(ctx,x,y,p,color){ ctx.fillStyle='rgba(0,0,0,0.6)';ctx.fillRect(x-8,y-1.5,16,2.6); ctx.fillStyle=color;ctx.fillRect(x-8,y-1.5,16*U.clamp(p,0,1),2.6); }, /* static pre-round preview */ preview(map,roster,ctSide){ if(!this.ok||this.R)return; const cv=document.getElementById('simCanvas');if(!cv)return; const fake={map:map,ctSide:ctSide,lineups:{a:[],b:[]},players:{},skills:{}, buys:{a:'full',b:'full'},tactic:{a:'default',b:'default'}, tags:{},colors:{},userSide:'a',scoreAfter:[0,0]}; roster.forEach(r=>{ fake.players[r.pid]={nick:r.nick,role:r.role}; fake.skills[r.pid]={aim:60,spd:60,clutch:60}; if(r.side===ctSide)fake.lineups.a.push(r.pid); else fake.lineups.b.push(r.pid); }); try{ const st=LIVE.makeRound(fake); const keep=this.R;this.R={st:st,cv:cv,ctx:cv.getContext('2d')}; this.draw(st);this.R=keep; }catch(e){} }, }; if(typeof window!=='undefined')VIZ.init();