Counter-Strike esports manager: career mode + LIVE tactical top-down simulation
- Full manager sim: 28 orgs / 168 players, calendar, transfers, youth scouting, loans, training, board confidence - Simulation Mode with real-time micro-engine (live.js): A* pathfinding on map geometry, raycast ballistics (spread/falloff/armor/headshots), smokes/flashes/HE that matter, plant/defuse/retake AI, clutch detection - Canvas renderer (viz.js): follow/radar cameras, HP bars, CS2-style kill feed, callouts, synthesized Web-Audio SFX, speed x1-x3, AUTO-SPECTATE hands-free series playback - Round results flow from the firefight back into the career engine (score, economy, box score, MVP) - Playwright + Node VM test suites included (test/, plus external pwtest harness)
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
/* ============================================================
|
||||
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;i<n;i++)d[i]=(Math.random()*2-1)*(1-i/n);
|
||||
const src=ac.createBufferSource();src.buffer=buf;
|
||||
const f=ac.createBiquadFilter();f.type=type;f.frequency.value=freq;f.Q.value=q;
|
||||
const g=ac.createGain();
|
||||
g.gain.setValueAtTime(vol,ac.currentTime);
|
||||
g.gain.exponentialRampToValueAtTime(0.001,ac.currentTime+dur);
|
||||
src.connect(f);f.connect(g);g.connect(ac.destination);
|
||||
src.start();
|
||||
},
|
||||
tone(freq,dur,vol,type,slide){
|
||||
vol=vol||0.12;type=type||'square';
|
||||
const ac=this.ac();if(!ac)return;
|
||||
const o=ac.createOscillator(),g=ac.createGain();
|
||||
o.type=type;o.frequency.setValueAtTime(freq,ac.currentTime);
|
||||
if(slide)o.frequency.exponentialRampToValueAtTime(slide,ac.currentTime+dur);
|
||||
g.gain.setValueAtTime(vol,ac.currentTime);
|
||||
g.gain.exponentialRampToValueAtTime(0.001,ac.currentTime+dur);
|
||||
o.connect(g);g.connect(ac.destination);o.start();o.stop(ac.currentTime+dur);
|
||||
},
|
||||
sShot(big){if(this.sndOn)this._noise(big?0.15:0.08,big?850:1700,0.8,0.3);},
|
||||
sHit(){if(this.sndOn)this.tone(300,0.05,0.09,'square');},
|
||||
sDeath(){if(this.sndOn)this.tone(90,0.13,0.22,'sine',50);},
|
||||
sPlant(){if(this.sndOn){this.tone(1250,0.06,0.1);setTimeout(()=>this.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<aA?'#4d7fb5':'rgba(90,100,120,0.25)';
|
||||
ctx.fillRect(14+i*11,8,8,10);
|
||||
ctx.fillStyle=i<aB?'#c79a4b':'rgba(90,100,120,0.25)';
|
||||
ctx.fillRect(W-22-i*11,8,8,10);
|
||||
}
|
||||
const shown=st.planted?st.bombT:st.timeLeft;
|
||||
ctx.fillStyle=st.planted?'#ffab4d':shown<15?'#ff7a5c':'#dfe8f5';
|
||||
ctx.font='bold 13px ui-monospace,monospace';ctx.textAlign='center';ctx.textBaseline='middle';
|
||||
ctx.fillText(Math.floor(Math.max(shown,0)/60)+':'+String(Math.floor(Math.max(shown,0)%60)).padStart(2,'0'),W/2,13);
|
||||
// kill feed
|
||||
ctx.textAlign='right';ctx.textBaseline='middle';
|
||||
st.feed.forEach((f,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();
|
||||
Reference in New Issue
Block a user