'use strict'; /* ================= match engine ================= */ /* Shared round-resolution core used by both quick-sim and the interactive Simulation mode. */ const WEAPONS={ pistol:['Glock','USP-S','P250','Tec-9'], smg:['MAC-10','MP9','UMP-45'], rifle:['AK-47','M4A1-S','Galil AR','FAMAS'], awp:'AWP', }; const TACTICS=[ {id:'aggr', name:'⚑ Fast Execute', desc:'Hit fast, win raw duels. Beats deception.', beats:'fake'}, {id:'default',name:'🧭 Default', desc:'Balanced structured CS.', beats:null}, {id:'patient',name:'🐒 Patient Play', desc:'Slow, trade-heavy. Punishes aggression.', beats:'aggr'}, {id:'fake', name:'🎭 Fake & Rotate', desc:'Deceive and punish rotations. Beats patience.',beats:'patient'}, {id:'stack', name:'🧱 Site Stack', desc:'All-in one site. High risk, high reward.', beats:null}, ]; function tacticEdge(a,b){ // small RPS edges if(!a||!b)return 0; const t=TACTICS.find(x=>x.id===a), o=b; if(t.beats===o)return 0.06; const ot=TACTICS.find(x=>x.id===o); if(ot&&ot.beats===a)return -0.06; return 0; } const MatchEngine={ lineupOf(teamId){ let t=Game.team(teamId); if(!t.lineup||t.lineup.length<5||(t.lineup.some(pid=>Game.p(pid).injuryDays>0)&&t.lineup.filter(pid=>Game.p(pid).injuryDays===0).length<5)){ Game.autoLineup(t); } return t.lineup.slice(0,5); }, playerWeight(p){ return Math.pow(playerOverall(p)/60,3)*(0.8+p.form*0.04)*(p.role==='AWP'?1.12:1); }, mapStrength(teamId,map){ const t=Game.team(teamId); return Game.teamStrength(t,{prepped:true})+(t.mapAff[map]||0); }, preview(m){ const A=Game.team(m.a),B=Game.team(m.b); const la=this.lineupOf(m.a),lb=this.lineupOf(m.b); const sA=U.avg(la.map(pid=>playerOverall(Game.p(pid)))),sB=U.avg(lb.map(pid=>playerOverall(Game.p(pid)))); return { A,B,sA,sB, strA:this.mapStrength(m.a,'Mirage'),strB:this.mapStrength(m.b,'Mirage'), pWin:U.logistic((sA-sB)*0.09), rankA:Game.rankOf(A.id)||'β€”',rankB:Game.rankOf(B.id)||'β€”', }; }, /* quick veto: returns array of maps to play (up to bo maps) */ planMaps(aId,bId,bo){ const nPlay=bo; const pool=ACTIVE_MAPS.map(map=>{ const d=this.mapStrength(aId,map)-this.mapStrength(bId,map); return {map,d}; }); const avail=new Set(pool.map(x=>x.map)); const bans=U.clamp(ACTIVE_MAPS.length-nPlay,0,4); let turn=U.chance(.5)?0:1; for(let i=0;i{if(!avail.has(x.map))return; const v=side===0?-x.d:x.d; // ban worst own map if(!best||v>best.v)best={map:x.map,v}; }); avail.delete(best.map); } // order remaining maps by alternating preference (picks then deciders) const seq=[]; while(avail.size){ const side=((seq.length%2===0?turn:1-turn))%2; let best=null; pool.forEach(x=>{if(!avail.has(x.map))return; const v=side===0?x.d:-x.d; if(!best||v>best.v)best={map:x.map,v}; }); avail.delete(best.map);seq.push(best.map); } return seq.slice(0,nPlay); }, /* ---------- ROUND CORE ---------- */ newMapState(aId,bId,map,bo,ctx){ const aCt=U.chance(0.5); return { aId,bId,map,bo, sa:0,sb:0,round:0, aStartCT:aCt, fund:{a:4200,b:4200}, lossStreak:{a:0,b:0}, momentum:0, // + favors A timeouts:{a:2,b:2}, winScore:13, otSets:0, hist:{a:[],b:[]}, // tactic history this map log:[], box:{}, // pid -> {k,d,adr,rating,clutch} aLineup:this.lineupOf(aId), bLineup:this.lineupOf(bId), finished:false, }; }, sideOf(st,teamKey,roundNo){ // is this team CT this round? const firstHalfCT=teamKey==='a'?st.aStartCT:!st.aStartCT; const swapped=roundNo>=12&&st.winScore===13; // halftime only in regulation return swapped?!firstHalfCT:firstHalfCT; }, autoBuy(st,key){ const f=st.fund[key]; const opp=key==='a'?'b':'a'; if(st.round===0)return 'pistol'; if(f>=11500)return 'full'; if(f>=6500&&st.fund[opp]<11500)return 'force'; if(f>=6500)return U.chance(.45)?'force':'eco'; return 'eco'; }, autoTactic(st,key){ const t=Game.team(key==='a'?st.aId:st.bId); const coach=Game.staffOf(t,'Coach'); // counter user tendencies if smart staff const opp=key==='a'?'b':'a'; const h=st.hist[opp]; let choice; if(h.length>=3&&(coach&&coach.skill>70)&&U.chance(coach.skill/180)){ const counts={};h.forEach(x=>counts[x]=(counts[x]||0)+1); const likely=Object.entries(counts).sort((x,y)=>y[1]-x[1])[0][0]; const counter={aggr:'patient',patient:'fake',fake:'aggr',default:'aggr',stack:'default'}[likely]; choice=counter||'default'; }else{ choice=U.pick(['aggr','default','patient','default','fake']); } return choice; }, resolveRound(st,opt={}){ const r=st.round; const keyA='a',keyB='b'; let buyA=opt.buyA??this.autoBuy(st,'a'); let buyB=opt.buyB??this.autoBuy(st,'b'); if(r===0){buyA='pistol';buyB='pistol';} let tacA=opt.tacA??this.autoTactic(st,'a'); let tacB=opt.tacB??this.autoTactic(st,'b'); st.hist.a.push(tacA);st.hist.b.push(tacB); const strA=this.mapStrength(st.aId,st.map),strB=this.mapStrength(st.bId,st.map); let p=strA-strB; // base diff in points (~ -20..20) // side & map bias const mapObj=MAPS.find(x=>x.name===st.map); const biasEdge=(this.sideOf(st,'a',r)?mapObj.bias:-mapObj.bias)*0.22; p+=biasEdge; // economy const buyVal={full:3,force:2,eco:1,pistol:1.4}; p+=(buyVal[buyA]-buyVal[buyB])*11; if(buyA==='eco'&&buyB==='full')p-=6; // save rounds are brutal if(buyB==='eco'&&buyA==='full')p+=6; // tactics p+=tacticEdge(tacA,tacB)*100*0.9; // momentum p+=st.momentum*0.32; // timeout calm-down if(opt.usedTimeoutA)p+=2.2; if(opt.usedTimeoutB)p-=2.2; // convert to probability let pw=U.logistic(p*0.058); pw=U.clamp(pw,0.05,0.95); const aWins=U.chance(pw); const winKey=aWins?'a':'b', loseKey=aWins?'b':'a'; const events=[]; const wLineup=winKey==='a'?st.aLineup:st.bLineup; const lLineup=winKey==='a'?st.bLineup:st.aLineup; // survivors flavor: how many died on each side const survWin=U.pickW([1,2,3,4,5],v=>v===5?2:v); const survLose=U.pickW([0,1,2,3],v=>v===3?1.2:v); let clutchHappened=false; if(survWin<=2&&survLose===1){ // potential 1vX clutch for the LAST loser-survivor const lp=lLineup[U.ri(0,lLineup.length-1)]; const cp=Game.p(lp); const pClutch=U.clamp(0.05+cp.attrs.clutch*0.0032+(cp.form-5)*0.01,0.02,0.5); if(U.chance(pClutch)){ clutchHappened=true; st.box[lp]=st.box[lp]||{k:0,d:0,adr:0,clutch:0}; st.box[lp].clutch++; events.push({type:'clutch',txt:`🧊 CLUTCH! ${cp.nick} wins the 1v${survWin}!`}); } } if(clutchHappened){/* round flips to loser side */} const finalWin=clutchHappened?loseKey:winKey; /* --- stats: kills exactly balance deaths --- winners died: 5-survWin ; losers died: 5-survLose (after possible clutch flip, survivors counts describe the ORIGINAL winner side, so recompute for final sides) */ const wArr=finalWin==='a'?st.aLineup:st.bLineup; const lArr=finalWin==='a'?st.bLineup:st.aLineup; // how many died per side of the FINAL round outcome const winDead=U.pickW([1,2,3,4],v=>v===4?1.6:v); // 1..4 winners died const loseDead=U.clamp(5-(clutchHappened?1:survLose),1,5); // at least the clutch survivor lives const boxOf=pid=>{st.box[pid]=st.box[pid]||{k:0,d:0,adr:0,_dmg:0,clutch:0};return st.box[pid];}; const giveKills=(lineup,nKills)=>{ if(nKills<=0)return; const ws=lineup.map(pid=>this.playerWeight(Game.p(pid))); const tot=U.sum(ws)||1; let left=nKills; lineup.forEach((pid,i)=>{ const k=Math.min(left,Math.max(0,Math.round(nKills*ws[i]/tot*(U.rnd(.55,1.45))))); left-=k;const b=boxOf(pid);b.k+=k;b._dmg+=k*U.rnd(90,160); }); while(left>0){const pid=U.pick(lineup);const b=boxOf(pid);b.k++;b._dmg+=U.rnd(90,160);left--;} }; giveKills(wArr,loseDead); // winners killed the losers who died giveKills(lArr,winDead); // losers' kills = winners who died U.shuffle(wArr.slice()).slice(0,winDead).forEach(pid=>boxOf(pid).d++); U.shuffle(lArr.slice()).slice(0,loseDead).forEach(pid=>boxOf(pid).d++); // event text if(!opt.silent){ const wpnPool=WEAPONS.pistol.concat(WEAPONS.smg); if(U.chance(0.75)){ const killer=Game.p(U.pick(wArr)),victim=Game.p(U.pick(lArr)); const fullBuy=(finalWin==='a'?buyA:buyB)==='full'; const wpn=killer.role==='AWP'&&fullBuy?WEAPONS.awp:(fullBuy?U.pick(WEAPONS.rifle):U.pick(wpnPool)); events.push({type:'kill',txt:`${killer.nick} [${wpn}] ${victim.nick}`}); } if(U.chance(0.05)){ const ace=Game.p(U.pick(wArr)); events.push({type:'info',txt:`πŸ’₯ ${ace.nick} with a MULTI-KILL!`}); } } // economy update const rewardWin=3250+(U.chance(0.4)?800:0); const ls=st.lossStreak; ls[loseKey]=Math.min(ls[loseKey]+1,4);ls[winKey]=0; const rewardLose=1400+500*(ls[loseKey]-1); const spendA={full:11800,force:6800,eco:1200,pistol:800}[buyA]; const spendB={full:11800,force:6800,eco:1200,pistol:800}[buyB]; st.fund.a=U.clamp(st.fund.a-spendA+(finalWin==='a'?rewardWin:rewardLose),0,16000*5); st.fund.b=U.clamp(st.fund.b-spendB+(finalWin==='b'?rewardWin:rewardLose),0,16000*5); // cap funds realistically st.fund.a=Math.min(st.fund.a,52000);st.fund.b=Math.min(st.fund.b,52000); // score & momentum if(finalWin==='a')st.sa++;else st.sb++; st.momentum=U.clamp(st.momentum+(finalWin==='a'?1.4:-1.4)* (clutchHappened?1.8:1),-8,8); if(opt.usedTimeoutA)st.momentum*=0.7; if(opt.usedTimeoutB)st.momentum*=0.7; st.round++; // win condition incl. OT checkpoints (MR12 β†’ 13; OT to 16, 19, 22…) if(st.winScore===13){ if(st.sa>=13||st.sb>=13)st.finished=true; else if(st.sa===12&&st.sb===12){st.winScore=16;if(!opt.silent)st.log.push('⏱ OVERTIME β€” first to 16!');} }else{ const hi=Math.max(st.sa,st.sb); if(hi>=st.winScore)st.finished=true; else if(st.sa===st.sb&&st.sa===st.winScore-1){st.winScore+=3;if(!opt.silent)st.log.push(`⏱ ${st.sa}-${st.sb} β€” another OT set! First to ${st.winScore}`);} } return {finalWin,aWins,buyA,buyB,tacA,tacB,events,pw,clutchHappened, score:[st.sa,st.sb]}; }, /* ---------- full map sim ---------- */ simMap(st,opt={}){ let guard=0; while(!st.finished&&guard++<300){ this.resolveRound(st,{silent:!opt.verbose}); } // finalize box ratings Object.entries(st.box).forEach(([pid,b])=>{ b.adr=U.clamp(Math.round((b._dmg||b.k*90)/Math.max(st.round,1)),10,180); b.rating=U.clamp(0.92+(b.k-b.d)/40+(b.adr-72)/280+b.clutch*0.05,0.25,1.9); delete b._dmg; }); return st; }, /* ---------- whole fixture ---------- */ simulate(m,ev){ const A=Game.team(m.a),B=Game.team(m.b); const la=this.lineupOf(m.a),lb=this.lineupOf(m.b); if(A.lineup.join()!==la.join())A.lineup=la; if(B.lineup.join()!==lb.join())B.lineup=lb; const bo=m.bo||3, need=Math.ceil(bo/2); const mapSeq=this.planMaps(m.a,m.b,bo); let sa=0,sb=0; const maps=[],box={}; for(const map of mapSeq){ if(sa>=need||sb>=need)break; const st=this.newMapState(m.a,m.b,map,bo,{}); this.simMap(st,{}); const aw=st.sa>st.sb; if(aw)sa++;else sb++; maps.push({map,score:`${aw?st.sa:st.sb}-${aw?st.sb:st.sa}`,winner:aw?m.a:m.b}); Object.entries(st.box).forEach(([pid,b])=>{ box[pid]=box[pid]||{k:0,d:0,adr:0,ratingSum:0,clutch:0,mapsN:0,mvp:false,aces:0}; box[pid].k+=b.k;box[pid].d+=b.d;box[pid].adr+=b.adr;box[pid].ratingSum+=b.rating; box[pid].clutch+=b.clutch;box[pid].mapsN++; box[pid].aces+=b.clutch>=2?1:0; }); } // MVP = best rated on winning side const winId=sa>sb?m.a:m.b; let mvpPid=null,best=-1; (winId===m.a?la:lb).forEach(pid=>{ const b=box[pid];const r=b?b.ratingSum/Math.max(b.mapsN,1):0.8; if(r>best){best=r;mvpPid=pid;} }); if(box[mvpPid])box[mvpPid].mvp=true; const res={ winner:winId,score:bo===1?(sa?`${maps[0].score}`:`${maps[0].score}`):`${sa}-${sb}`, maps,box,mvp:mvpPid, }; if(ev)m.evId=ev.id; Game.finishMatch(m,res); return res; }, }; 'use strict'; /* ================= INTERACTIVE SESSION (Simulation Mode) ================= */ const SimSession={ s:null, start(m,ev){ const userId=m.a===G.playerTeamId?'a':'b'; this.s={ m,ev,phase:'veto', userId, aId:m.a,bId:m.b,bo:m.bo||3, veto:{pool:new Set(ACTIVE_MAPS),seq:[],step:0, order:U.chance(.5)?['u','ai']:['ai','u'],log:['πŸ—³ Map VETO begins…']}, mapStates:[],results:[],cur:null,curIdx:-1, awaiting:null,lastRound:null, scoreA:0,scoreB:0,userBuy:null,userTac:null,pendingTimeout:false,roundResolved:false, }; // if AI moves first in veto, resolve its moves while(!this.vetoDone()&&!this.myTurn())this.aiVetoAction(); return this.s; }, oppId(){return this.s.userId==='a'?this.s.bId:this.s.aId;}, myTurn(){return this.s.veto.order[this.s.veto.step%2]==='u';}, winsNeeded(){return Math.ceil(this.s.bo/2);}, // series wins needed totalVetoSteps(){return this.s.bo===1?4:U.clamp(ACTIVE_MAPS.length-this.s.bo,0,4);}, picksCount(){return this.s.bo===1?0:2;}, vetoDone(){return this.s.veto.step>=this.totalVetoSteps()+this.picksCount();}, vetoAction(map){ const v=this.s.veto,S=this.s; if(this.vetoDone()||!v.pool.has(map))return; const isBan=v.step{ const mine=MatchEngine.mapStrength(aiId,mp); const opp=MatchEngine.mapStrength(S.userId==='a'?S.aId:S.bId,mp); const val=isBan?(opp-mine):(mine-opp); if(!best||val>best.val)best={map:mp,val}; }); if(!best)return; if(isBan)v.pool.delete(best.map);else{v.seq.push(best.map);v.pool.delete(best.map);} v.log.push(`${isBan?'🚫 Ban':'βœ… Pick'} β€” ${Game.team(aiId).tag}: ${best.map}`); v.step++; }, beginMap(idx){ const S=this.s; const map=S.veto.seq[idx]; S.curIdx=idx; S.cur=MatchEngine.newMapState(S.aId,S.bId,map,S.bo,{}); S.phase='round'; S.awaiting='buy'; S.userBuy=null;S.userTac=null;S.pendingTimeout=false;S.roundResolved=false; return S.cur; }, buyOptions(){ const c=this.s.cur,k=this.s.userId,f=c.fund[k]; const opts=[]; if(c.round===0)return [{id:'pistol',name:'πŸ”« Pistol Round',desc:'Default pistols + utility',cost:800}]; if(f>=11800)opts.push({id:'full',name:'πŸ›’ Full Buy',desc:'Rifles, armor, full utility',cost:11800}); if(f>=6800)opts.push({id:'force',name:'πŸ”¨ Force Buy',desc:'Mix of guns, partial armor',cost:6800}); opts.push({id:'eco',name:'πŸ’° Eco / Save',desc:'Bank cash for a later full round',cost:1200}); return opts; }, chooseBuy(opt){ const S=this.s;if(S.phase!=='round'||S.awaiting!=='buy')return; S.userBuy=opt;S.awaiting='tactic'; if(typeof UI!=='undefined'&&UI.renderSim)UI.renderSim(); }, chooseTactic(tid){ const S=this.s;if(S.awaiting!=='tactic')return; S.userTac=tid;this.playRound(); // playRound owns rendering (may start the viz) }, callTimeout(){ const S=this.s,c=S.cur,k=S.userId; if(c.timeouts[k]<=0||S.roundResolved)return false; c.timeouts[k]--;S.pendingTimeout=true; c.log.push(`πŸŽ™ ${Game.team(k==='a'?S.aId:S.bId).tag} burns a tactical timeout.`); return true; }, playRound(){ const S=this.s,c=S.cur; if(S.awaiting!=='tactic')return; const opt={}; if(S.userId==='a'){opt.buyA=S.userBuy;opt.tacA=S.userTac;opt.usedTimeoutA=S.pendingTimeout;} else{opt.buyB=S.userBuy;opt.tacB=S.userTac;opt.usedTimeoutB=S.pendingTimeout;} S.pendingTimeout=false; /* LIVE tactical simulation: the round is decided by an actual top-down firefight, then its result is fed back into the career engine (score/economy/box stats). */ if(typeof LIVE!=='undefined'&&typeof VIZ!=='undefined'&&VIZ.ok){ try{ const cfg=this.buildLiveCfg(S,c,opt); S.awaiting='anim'; UI.renderSim(); VIZ.play(cfg,(st)=>{ if(st)this.applyLiveResult(S,c,st,cfg,opt); this._finishLive(); }); return; }catch(e){G.ui.vizErr=(e&&e.message)+' @ '+((e&&e.stack||'').split('\n')[1]||'?');} } /* fallback: math model */ const out=MatchEngine.resolveRound(c,opt); S.lastRound=out; out.events.forEach(e=>c.log.push(e.txt)); const wTag=Game.team(out.finalWin==='a'?S.aId:S.bId).tag; const tagA=Game.team(S.aId).tag,tagB=Game.team(S.bId).tag; c.log.push(`R${c.round} ${tagA} ${out.buyA.toUpperCase()} vs ${out.buyB.toUpperCase()} β†’ ${wTag} (${c.sa}-${c.sb})`); S.roundResolved=true; S.awaiting='next'; if(c.finished)this.endMap(); if(typeof UI!=='undefined'&&UI.renderSim)UI.renderSim(); }, buildLiveCfg(S,c,opt){ const r=c.round; let buyA=opt.buyA??MatchEngine.autoBuy(c,'a'); let buyB=opt.buyB??MatchEngine.autoBuy(c,'b'); if(r===0){buyA='pistol';buyB='pistol';} const tacA=opt.tacA??MatchEngine.autoTactic(c,'a'); const tacB=opt.tacB??MatchEngine.autoTactic(c,'b'); c.hist.a.push(tacA);c.hist.b.push(tacB); const ctSide=MatchEngine.sideOf(c,'a',r)?'a':'b'; const players={},skills={}; c.aLineup.concat(c.bLineup).forEach(pid=>{ const p=Game.p(pid); players[pid]={nick:p.nick,role:p.role}; skills[pid]={ aim:p.attrs.aim+(p.form-5)*2, spd:p.attrs.move, clutch:p.attrs.clutch, }; }); const tA=Game.team(S.aId),tB=Game.team(S.bId); return {map:c.map,ctSide,lineups:{a:c.aLineup.slice(),b:c.bLineup.slice()}, players,skills,buys:{a:buyA,b:buyB},tactic:{a:tacA,b:tacB}, tags:{a:tA.tag,b:tB.tag},colors:{a:tA.color,b:tB.color}, userSide:S.userId,scoreAfter:[c.sa,c.sb], timeoutSide:opt.usedTimeoutA?'a':opt.usedTimeoutB?'b':null}; }, applyLiveResult(S,c,st,cfg,opt){ const finalWin=st.winner; /* real fight stats β†’ box score */ st.agents.forEach(a=>{ c.box[a.pid]=c.box[a.pid]||{k:0,d:0,adr:0,_dmg:0,clutch:0}; const b=c.box[a.pid]; b.k+=a.kills;b._dmg+=Math.round(a.dmg); if(a.dead)b.d++; }); let clutchPid=null; if(st.clutchFlag){ let best=null; st.agents.forEach(x=>{if(x.side===finalWin&&!x.dead&&(!best||x.kills>best.kills))best=x;}); if(best){c.box[best.pid].clutch++;clutchPid=best.pid; c.log.push(`🧊 CLUTCH! ${Game.p(best.pid).nick} seals the round!`);} } st.feed.slice().reverse().forEach(f=>c.log.push(`πŸ”« ${f.killer} [${f.wpn}] ${f.victim}`)); /* economy β€” same model as resolveRound */ const rewardWin=3250+(U.chance(0.4)?800:0); const winKey=finalWin,loseKey=finalWin==='a'?'b':'a'; const ls=c.lossStreak; ls[loseKey]=Math.min(ls[loseKey]+1,4);ls[winKey]=0; const rewardLose=1400+500*(ls[loseKey]-1); const spend={full:11800,force:6800,eco:1200,pistol:800}; c.fund.a=U.clamp(c.fund.a-spend[cfg.buys.a]+(finalWin==='a'?rewardWin:rewardLose),0,52000); c.fund.b=U.clamp(c.fund.b-spend[cfg.buys.b]+(finalWin==='b'?rewardWin:rewardLose),0,52000); /* score & momentum & win condition */ if(finalWin==='a')c.sa++;else c.sb++; c.momentum=U.clamp(c.momentum+(finalWin==='a'?1.4:-1.4)*(st.clutchFlag?1.8:1),-8,8); if(opt.usedTimeoutA)c.momentum*=0.7; if(opt.usedTimeoutB)c.momentum*=0.7; c.round++; if(c.winScore===13){ if(c.sa>=13||c.sb>=13)c.finished=true; else if(c.sa===12&&c.sb===12){c.winScore=16;c.log.push('⏱ OVERTIME β€” first to 16!');} }else{ const hi=Math.max(c.sa,c.sb); if(hi>=c.winScore)c.finished=true; else if(c.sa===c.sb&&c.sa===c.winScore-1){c.winScore+=3;c.log.push(`⏱ ${c.sa}-${c.sb} β€” another OT set! First to ${c.winScore}`);} } const wTag=Game.team(finalWin==='a'?S.aId:S.bId).tag; const tagA=Game.team(S.aId).tag,tagB=Game.team(S.bId).tag; const reasonTxt={elim:'',bomb:' πŸ’£',defuse:' βœ‚οΈ',time:' ⏱'}[st.reason]||''; c.log.push(`R${c.round} ${tagA} ${cfg.buys.a.toUpperCase()} vs ${cfg.buys.b.toUpperCase()} β†’ ${wTag}${reasonTxt} (${c.sa}-${c.sb})`); S.lastRound={finalWin,aWins:finalWin==='a',buyA:cfg.buys.a,buyB:cfg.buys.b, tacA:cfg.tactic.a,tacB:cfg.tactic.b,events:[],pw:0.5, clutchHappened:st.clutchFlag,score:[c.sa,c.sb]}; S.roundResolved=true; }, _finishLive(){ const S=this.s,c=S.cur; S.awaiting='next'; if(c.finished)this.endMap(); UI.renderSim(); }, nextRound(){ const S=this.s; if(S.awaiting!=='next')return; const c=S.cur; S.userBuy=null;S.userTac=null;S.roundResolved=false; S.awaiting='buy'; if(typeof UI!=='undefined'&&UI.renderSim)UI.renderSim(); }, simRest(){ const S=this.s,c=S.cur; if(S.awaiting!=='next')return; let guard=0; while(!c.finished&&guard++<400)MatchEngine.resolveRound(c,{silent:true}); // fold quick-simmed rounds into box stats Object.entries(c.box).forEach(([pid,b])=>{ b.adr=U.clamp(Math.round((b._dmg||b.k*90)/Math.max(c.round,1)),10,180); b.rating=U.clamp(0.92+(b.k-b.d)/40+(b.adr-72)/280+b.clutch*0.05,0.25,1.9); delete b._dmg; }); this.endMap(); }, endMap(){ const S=this.s,c=S.cur;if(!c)return; const aWon=c.sa>c.sb; const winKey=aWon?'a':'b'; // finalize this map's ratings Object.entries(c.box).forEach(([pid,b])=>{ b.adr=U.clamp(Math.round((b._dmg!==undefined?b._dmg:b.k*90)/Math.max(c.round,1)),10,180); b.rating=U.clamp(0.92+(b.k-b.d)/40+(b.adr-72)/280+b.clutch*0.05,0.25,1.9); delete b._dmg; }); S.results.push({map:c.map,sa:c.sa,sb:c.sb,winner:winKey}); if(winKey==='a')S.scoreA++;else S.scoreB++; // accumulate series box S.boxAcc=S.boxAcc||{}; Object.entries(c.box).forEach(([pid,b])=>{ S.boxAcc[pid]=S.boxAcc[pid]||{k:0,d:0,adr:0,ratingSum:0,clutch:0,mapsN:0,mvp:false,aces:0}; const t=S.boxAcc[pid]; t.k+=b.k;t.d+=b.d;t.adr+=b.adr;t.ratingSum+=b.rating; t.clutch+=b.clutch;t.mapsN++;t.aces+=b.clutch>=2?1:0; }); const need=Math.ceil(S.bo/2); const logLine=`${Game.team(winKey==='a'?S.aId:S.bId).tag} take ${c.map} ${c.sa}-${c.sb} (series ${S.scoreA}-${S.scoreB})`; S.veto.log.push(logLine); if(S.scoreA>=need||S.scoreB>=need){ // series over β†’ finalize real fixture const winId=winKey==='a'?S.aId:S.bId; let mvpPid=null,best=-1; const winLineup=winKey==='a'?c.aLineup:c.bLineup; winLineup.forEach(pid=>{ const b=S.boxAcc[pid];const r=b?b.ratingSum/Math.max(b.mapsN,1):0.8; if(r>best){best=r;mvpPid=pid;} }); if(S.boxAcc[mvpPid])S.boxAcc[mvpPid].mvp=true; const maps=S.results.map(r=>({map:r.map, score:`${winKey===r.winner?Math.max(r.sa,r.sb):Math.min(r.sa,r.sb)}-${winKey===r.winner?Math.min(r.sa,r.sb):Math.max(r.sa,r.sb)}`, winner:winKey==='a'?(r.winner==='a'?S.aId:S.bId):(r.winner==='a'?S.aId:S.bId)})); const res={winner:winId,score:`${S.scoreA}-${S.scoreB}`,maps, box:S.boxAcc,mvp:mvpPid}; if(S.ev)S.m.evId=S.ev.id; Game.finishMatch(S.m,res); S.m.mvp=mvpPid; S.phase='done';S.cur=null; }else{ S.phase='intermission';S.cur=null; } if(typeof UI!=='undefined'&&UI.renderSim)UI.renderSim(); }, };