/* ============================================================ LIVE — real-time tactical micro-simulation (top-down CS) A* pathfinding on real geometry, raycast bullets with spread/ falloff/armor/headshots, utility that matters (smoke blocks LOS, flash blinds, HE/molly damage), plant/defuse/timer. The round outcome comes OUT of this fight and is fed back into the career engine by SimSession.finishLiveRound(). ============================================================ */ 'use strict'; const LIVE=(()=>{ function VZ(){return (typeof window!=='undefined')?window.VIZ:null;} /* ---------------- maps ---------------- */ const MAPS={ 'Dust II':{ pal:{floor:'#182234',wall:'#2c3c5a',crate:'#3b4f73'}, /* lanes separated by real walls: long | mid | tunnels, joined only at doors / catwalk-stairs / site mouths */ floors:[[12,66,14,10], // T spawn [26,60,32,8], // outside long (low) [58,58,4,6], // doors [62,58,36,8], // along long bottom [96,16,10,42], // long A (vertical) [96,6,26,20], // A site [56,30,40,6], // catwalk (mid->short) [88,25,8,5], // short stairs [46,36,9,30], // mid [40,62,8,6], // tspawn->mid connector [55,18,41,7], // top-mid (CT side) [20,46,18,8], // lower tunnel [16,22,10,24], // upper tunnel [24,54,8,6], // tspawn->tunnels [6,6,28,20], // B site [30,6,72,8], // CT rotate corridor [110,6,14,14]], // CT spawn crates:[[101,10],[106,17],[113,12],[119,21],[13,11],[21,16],[27,9], [51,45],[86,32],[100,44],[120,32]], tSpawn:[18,71],ctSpawn:[117,11], sites:{A:[108,15],B:[18,15]}, chokes:{A:[[99,26]],B:[[20,27]]}, holds:{A:[[104,10,'r'],[114,21,'d'],[98,11,'l'],[103,23,'dr']], B:[[12,9,'l'],[24,19,'d'],[8,20,'dl'],[30,10,'ld']], mid:[[60,22,'d'],[49,40,'u']]}, flashes:{A:[104,18],B:[17,16]}, }, 'Mirage':{ pal:{floor:'#1b2030',wall:'#37304a',crate:'#4a4266'}, floors:[[10,64,16,12],[28,58,30,12],[58,54,10,10],[68,56,34,10], [100,16,14,42],[96,6,26,20],[52,26,46,8],[44,30,12,36], [58,16,40,10],[16,42,20,10],[14,18,12,26],[6,6,28,20], [32,6,68,8],[42,62,10,6],[110,6,14,14]], crates:[[102,11],[107,17],[112,13],[99,21],[13,11],[21,15],[27,10], [60,30],[47,50],[104,42],[86,60]], tSpawn:[17,71],ctSpawn:[117,11], sites:{A:[109,15],B:[19,15]}, chokes:{A:[[99,27]],B:[[20,27]]}, holds:{A:[[107,10,'r'],[114,21,'d'],[97,13,'l'],[102,23,'dr']], B:[[11,9,'l'],[25,17,'d'],[7,19,'dl'],[31,11,'ld']], mid:[[63,21,'d'],[49,33,'u']]}, flashes:{A:[104,17],B:[16,15]}, }, 'Inferno':{ pal:{floor:'#20191b',wall:'#4a3038',crate:'#6b4630'}, floors:[[10,62,16,12],[28,58,26,10],[54,52,14,10],[68,50,16,10], [84,48,20,12],[96,44,24,16],[88,10,14,36],[90,6,28,18], [46,30,12,26],[58,24,34,8],[16,40,14,22],[14,16,12,26], [6,6,26,20],[32,6,60,8],[110,24,12,22],[40,60,10,6]], crates:[[95,11],[101,17],[107,12],[93,20],[12,11],[20,15],[26,10], [52,33],[62,27],[100,51],[116,52]], tSpawn:[17,69],ctSpawn:[116,11], sites:{A:[104,14],B:[106,53]}, chokes:{A:[[92,23]],B:[[92,53]]}, holds:{A:[[101,9,'r'],[111,20,'d'],[92,12,'l'],[98,20,'dr']], B:[[112,47,'d'],[99,57,'l'],[114,57,'dl'],[96,46,'lu']], mid:[[62,27,'d'],[50,34,'u']]}, flashes:{A:[98,16],B:[100,55]}, }, 'Nuke':{ pal:{floor:'#161e2c',wall:'#33415c',crate:'#465a80'}, floors:[[12,34,18,12],[30,30,18,20],[48,26,44,28],[92,30,26,20], [56,10,24,16],[56,54,24,16],[100,10,20,16],[100,54,20,16], [30,12,26,10],[30,58,26,10],[12,12,18,14],[12,54,18,14]], crates:[[62,14],[71,14],[62,60],[71,60],[52,33],[84,45],[104,16], [110,61],[36,35]], tSpawn:[20,40],ctSpawn:[112,40], sites:{A:[110,17],B:[110,62]}, chokes:{A:[[96,18]],B:[[96,62]]}, holds:{A:[[106,12,'r'],[116,22,'d'],[98,14,'l']], B:[[106,58,'r'],[116,52,'u'],[98,65,'l']], mid:[[88,38,'l'],[52,40,'r']]}, flashes:{A:[106,18],B:[106,62]}, }, 'Ancient':{ pal:{floor:'#141d22',wall:'#2c4a44',crate:'#3d6459'}, floors:[[12,36,16,10],[28,30,18,22],[46,26,40,28],[86,30,24,20], [58,8,22,18],[58,54,22,18],[98,8,22,18],[98,54,22,18], [28,10,28,10],[28,60,28,10]], crates:[[64,12],[73,12],[64,60],[73,60],[50,31],[82,45],[102,12], [108,62],[34,35]], tSpawn:[19,41],ctSpawn:[111,41], sites:{A:[109,16],B:[109,62]}, chokes:{A:[[94,17]],B:[[94,62]]}, holds:{A:[[105,11,'r'],[115,21,'d'],[97,13,'l']], B:[[105,58,'r'],[115,51,'u'],[97,66,'l']], mid:[[82,39,'l'],[50,40,'r']]}, flashes:{A:[104,17],B:[104,61]}, }, }; MAPS['Anubis']=JSON.parse(JSON.stringify(MAPS['Mirage'])); MAPS['Train']=JSON.parse(JSON.stringify(MAPS['Dust II'])); MAPS['Overpass']=JSON.parse(JSON.stringify(MAPS['Ancient'])); MAPS['Vertigo']=JSON.parse(JSON.stringify(MAPS['Nuke'])); /* ---------------- grid / nav ---------------- */ const GW=130,GH=80; function buildGrid(L){ const g=new Uint8Array(GW*GH); L.floors.forEach(([x,y,w,h])=>{ for(let yy=Math.max(0,y|0);yy{ for(let yy=(cy-2)|0;yy<=((cy+2)|0);yy++) for(let xx=(cx-2)|0;xx<=((cx+2)|0);xx++) if(xx>=0&&yy>=0&&xx=0&&y>=0&&xMath.abs(i%W-tx)+Math.abs(((i/W)|0)-ty); let guard=0; while(open.length&&guard++<9000){ let bi=0; for(let i=1;i0.12&&s.life<0.9){ const ex=x-s.x,ey=y-s.y; if(ex*ex+ey*ey{ if(!walkable(g,sp[0],sp[1])){const s=snap(g,sp[0],sp[1]);sp[0]=s.x;sp[1]=s.y;} const targets=[L.sites.A,L.sites.B] .concat(L.chokes.A||[],L.chokes.B||[]); targets.forEach(tg=>{ if(walkable(g,tg[0],tg[1])&&astar(g,sp[0],sp[1],tg[0],tg[1]))return; const put=(x,y)=>{ [[0,0],[1,0],[-1,0],[0,1],[0,-1]].forEach(([dx,dy])=>{ const nx=x+dx,ny=y+dy; if(nx>=1&&ny>=1&&nxx?1:-1;put(x,y);} while(y!==ty){y+=ty>y?1:-1;put(x,y);} tg[0]=tx+0.5;tg[1]=ty+0.5; L.carved=L.carved||[]; L.carved.push([Math.min(sp[0],tx)-1,Math.min(sp[1],ty)-1, Math.abs(tx-sp[0])+3,Math.abs(ty-sp[1])+3]); }); }); } /* ---------------- weapons ---------------- */ const WEAPONS={ awp:{name:'AWP ⭐',dmg:115,rpm:45,rng:60,spread:0.015,move:0.7,big:true}, rifle:{name:'Rifle',dmg:37,rpm:600,rng:38,spread:0.042,move:0.9}, smg:{name:'SMG',dmg:28,rpm:780,rng:24,spread:0.06,move:1.0}, pistol:{name:'Pistol',dmg:32,rpm:400,rng:20,spread:0.05,move:1.0}, }; /* ---------------- round construction ---------------- */ function makeRound(cfg){ const L=MAPS[cfg.map]||MAPS['Mirage']; L.w=L.w||130;L.h=L.h||80; const g=buildGrid(L); const tKey=cfg.ctSide==='a'?'b':'a'; const mk=(pid,side)=>{ const p=cfg.players[pid],sk=cfg.skills[pid]; const isCT=side===cfg.ctSide; const sp=isCT?L.ctSpawn:L.tSpawn; const p0=snap(g,sp[0]+(Math.random()-0.5)*6,sp[1]+(Math.random()-0.5)*6); return{pid,nick:p.nick,role:p.role,side,isCT, x:p0.x,y:p0.y, hp:100,armor:cfg.buys[side]==='full'?100:cfg.buys[side]==='force'?60:0, dead:false,ang:isCT?-Math.PI/2:Math.PI/2, spd:(0.82+sk.spd*0.0035), react:U.clamp(0.46-sk.aim*0.0026-sk.clutch*0.0007,0.13,0.5)*(cfg.timeoutSide===side?0.82:1), acc:U.clamp(sk.aim/100,0.3,1), weapon:(cfg.buys[side]==='full') ?(p.role==='AWP'?WEAPONS.awp:WEAPONS.rifle) :(cfg.buys[side]==='force'?WEAPONS.smg:WEAPONS.pistol), cool:0,blind:0,target:null,lastSeenT:-9, path:null,pathI:0,goal:null,fake:false,alertT:-9, holdNear:null,kills:0,dmg:0,bob:Math.random()*6.28,moving:false, reactT:null}; }; const agents=[]; cfg.lineups.a.forEach(pid=>agents.push(mk(pid,'a'))); cfg.lineups.b.forEach(pid=>agents.push(mk(pid,'b'))); // CT setup: split holds (2-2-1 default) const cts=agents.filter(a=>a.isCT); const split=Math.random()<0.75?[2,2,1]:Math.random()<0.5?[3,1,1]:[1,3,1]; let ci=0; ['A','B','mid'].forEach((setName,si)=>{ const hs=L.holds[setName]; for(let i=0;i!a.isCT); const nFake=atkTac==='fake'?2:0; atks.forEach((a,i)=>{ a.fake=iutilPlan.push({t:7,type:'smoke',x,y})); }else{ L.chokes[site].forEach(([x,y],i)=>utilPlan.push({t:5+i*1.6,type:'smoke',x,y})); const fl=L.flashes[site]; utilPlan.push({t:8,type:'flash',x:fl[0],y:fl[1]}); } return {cfg,L,g,agents,site,tKey,t:0,timeLeft:115, planted:false,plantP:0,bombPos:null,bombT:0, bombCarrier:atks[Math.floor(Math.random()*atks.length)], defuseP:0,defuser:null, smokes:[],fires:[],nades:[],fx:[],corpses:[],feed:[], callout:null,calloutT:-9, winner:null,reason:null,killsLog:[], pain:0,shake:0,ended:false,endT:0, firstBlood:false,clutchFlag:false, utilPlan,utilDone:new Set(),lastFlashAt:-9}; } /* ---------------- utility ---------------- */ function throwNade(st,type,x,y){st.nades.push({type,x,y,fuse:type==='flash'?0.7:type==='he'?1.0:1.1,total:1.1,progress:0,z:0});} function detonate(st,n){ if(n.type==='smoke'){st.smokes.push({x:n.x,y:n.y,r:12.5,life:0});} else if(n.type==='flash'){ st.fx.push({type:'flashPop',t0:st.t,dur:0.09}); st.agents.forEach(a=>{ if(a.dead)return; const d=Math.hypot(a.x-n.x,a.y-n.y); if(d<16&&!raycast(st.g,n.x,n.y,(a.x-n.x)/d,(a.y-n.y)/d,d,null).hit) a.blind=Math.max(a.blind,U.clamp(2.5-d*0.1,0.4,2.5)); }); const vz=VZ();if(vz)vz.sFlash(); }else if(n.type==='he'){ st.fx.push({type:'boom',x:n.x,y:n.y,t0:st.t,dur:0.35}); st.shake=Math.min(st.shake+1.7,3); const vz=VZ();if(vz)vz.sBoom(); st.agents.forEach(a=>{ if(a.dead)return; const d=Math.hypot(a.x-n.x,a.y-n.y); if(d<11)a.hp-=Math.max(0,55*(1-d/11)); }); } } /* ---------------- helpers ---------------- */ function visAnyEnemy(st,a){ for(const e of st.agents){ if(e.dead||e.isCT===a.isCT)continue; if(los(st.g,a,e,st.smokes))return true; } return false; } function los(g,a,b,smokes){ const dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy); if(d<0.01)return true; const r=raycast(g,a.x,a.y,dx/d,dy/d,d,smokes); return !r.hit&&!r.smoke; } function hotSite(st){ let ht=-9,hx=0,hy=0; const A=st.L.sites.A,B=st.L.sites.B; st.agents.forEach(m=>{ if(m.lastSeenT>ht){ht=m.lastSeenT;hx=m.seenX;hy=m.seenY;} }); if(ht<=0)return null; // contact older than 25s stops drawing rotations if(st.t-ht>25)return null; return Math.hypot(hx-A[0],hy-A[1])=a.path.length|| Math.hypot(last.x-to2.x,last.y-to2.y)>2.5; if(needNew){ a.path=astar(g,a.x,a.y,to2.x,to2.y)||[{x:to2.x,y:to2.y}]; a.pathI=0; } let tgt=a.path[Math.min(a.pathI,a.path.length-1)]; if(Math.hypot(tgt.x-a.x,tgt.y-a.y)<0.9&&a.pathI0.12){ const sp=a.spd*3*dt; const nx=a.x+dx/d*sp,ny=a.y+dy/d*sp; let moved=false; if(walkable(g,nx,ny)){a.x=nx;a.y=ny;moved=true;} else if(walkable(g,nx,a.y)){a.x=nx;moved=true;} // slide X else if(walkable(g,a.x,ny)){a.y=ny;moved=true;} // slide Y if(moved){a._stuck=0;a.ang=Math.atan2(dy,dx);a.moving=true;} else{ a.moving=false; a._stuck=(a._stuck||0)+1; if(a._stuck>5){ a._stuck=0;a.path=null;a.via=null; for(let rr=3;rr<=10;rr+=3){ const s=snap(g,a.x+(Math.random()-0.5)*rr*2,a.y+(Math.random()-0.5)*rr*2); if(Math.hypot(s.x-a.x,s.y-a.y)>2){a.via={x:s.x,y:s.y};break;} } } } }else{a.moving=false;a._stuck=0;} } /* ---------------- combat ---------------- */ function shoot(st,a,vis){ a.shots=(a.shots||0)+1; const d=Math.hypot(vis.x-a.x,vis.y-a.y); const w=a.weapon; const spread=w.spread*(1+(a.moving?0.85:0))*(1.35-Math.min(d/w.rng,1)*0.6)/(0.45+a.acc*0.8); const ang=a.ang+(Math.random()-0.5)*spread*5.5; const maxD=Math.min(w.rng*1.7,d+15); const hit=raycast(st.g,a.x,a.y,Math.cos(ang),Math.sin(ang),maxD,st.smokes); let victim=null,hitAt={x:hit.x,y:hit.y}; if(!hit.hit&&!hit.smoke){ let bt=1e9; for(const e of st.agents){ if(e.dead||e.isCT===a.isCT)continue; const rx=e.x-a.x,ry=e.y-a.y; const tt=rx*Math.cos(ang)+ry*Math.sin(ang); if(tt<0||tt>hit.d)continue; const px=a.x+Math.cos(ang)*tt,py=a.y+Math.sin(ang)*tt; const dd=Math.hypot(px-e.x,py-e.y); if(dd<1.25&&tt0){victim.armor=Math.max(0,victim.armor-dmg*0.5);dmg*=0.68;} dmg=Math.min(dmg,victim.hp); victim.hp-=dmg;a.dmg+=dmg; st.fx.push({type:'bloodHit',x:victim.x,y:victim.y,t0:st.t,dur:0.1}); const vz=VZ();if(vz)vz.sHit(); if(victim.hp<=0)kill(st,victim,a,w.name+(hs?' ⌖':'')); }else{ st.fx.push({type:hit.hit?'spark':'puff',x:hitAt.x,y:hitAt.y,t0:st.t,dur:0.13}); } const vz=VZ();if(vz)vz.sShot(w.big); } function kill(st,v,killer,wpn){ if(v.dead)return; v.dead=true;v.hp=0; st.corpses.push({x:v.x,y:v.y,ang:v.ang,ct:v.isCT,grow:0}); st.shake=Math.min(st.shake+0.9,2.4); const vz=VZ();if(vz)vz.sDeath(); const wct=killer?killer.isCT:!v.isCT; if(killer){ killer.kills++; st.killsLog.push({k:killer.pid,v:v.pid,wpn:wpn||''}); st.feed.unshift({t0:st.t,killer:killer.nick,kCT:killer.isCT,wpn:wpn||'',victim:v.nick,vCT:v.isCT}); if(st.feed.length>5)st.feed.pop(); } if(v.side===st.cfg.userSide)st.pain=1; if(!st.firstBlood){st.firstBlood=true;callout(st,'FIRST BLOOD!','#ff5c5c');} const aliveCT=st.agents.filter(x=>!x.dead&&x.isCT).length; const aliveT=st.agents.filter(x=>!x.dead&&!x.isCT).length; if(aliveCT===1&&aliveT>=2&&!st.clutchFlag&&wct){st.clutchFlag=true;callout(st,'🧊 CLUTCH TIME!','#ffd34d');} if(aliveT===1&&aliveCT>=2&&!st.clutchFlag&&!wct){st.clutchFlag=true;callout(st,'🧊 CLUTCH TIME!','#ffd34d');} const tKey=st.cfg.ctSide==='a'?'b':'a'; if(aliveCT===0){ if(st.planted)endRound(st,tKey,'bomb'); else endRound(st,tKey,'elim'); }else if(aliveT===0&&!st.planted){ endRound(st,st.cfg.ctSide,'elim'); } if(v===st.defuser){st.defuser=null;st.defuseP=0;} if(v===st.bombCarrier&&!st.planted){ const ts=st.agents.filter(x=>!x.dead&&!x.isCT); if(ts.length)st.bombCarrier=ts[0]; } } function callout(st,txt,color){st.callout={txt,color};st.calloutT=st.t;} /* ---------------- AI brains ---------------- */ function tBrain(st,a,dt){ // late-round urgency: commit everyone left to the chosen site if(!st.planted&&!a.fake&&st.timeLeft<78&&!a._rushed){ a._rushed=true; const tg=st.L.sites[st.site];const gp=snap(st.g,tg[0],tg[1]); a.goal={x:gp.x,y:gp.y};a.path=null;a.via=null; } if(st.planted){ // post-plant: watch entrances near bomb const entr=st.L.chokes[st.site][0]; a.moving=false; if(Math.hypot(a.x-st.bombPos.x,a.y-st.bombPos.y)>9)moveAlong(st,a,{x:st.bombPos.x,y:st.bombPos.y},dt); else a.ang=Math.atan2(entr[1]-a.y,entr[0]-a.x)+(a.bob%1-0.5)*0.5; return; } const siteC=st.L.sites[st.site]; if(a.fake){ const o=st.site==='A'?'B':'A'; const ch=st.L.chokes[o][0]; if(Math.hypot(a.x-ch[0],a.y-ch[1])>6)moveAlong(st,a,{x:ch[0],y:ch[1]},dt); else if(st.t>16){a.fake=false;const tg=st.L.sites[st.site];a.goal={x:tg[0],y:tg[1]};} return; } const atSite=Math.hypot(a.x-siteC[0],a.y-siteC[1])<9.5; if(atSite){ let blocker=null,bd=1e9; st.agents.forEach(e=>{ if(e.dead||!e.isCT)return; const dd=Math.hypot(e.x-a.x,e.y-a.y); if(dd4.5; const enemiesNear=blocker&&!desperate; if(st.bombCarrier===a&&!enemiesNear){ a.moving=false; st.plantP+=dt/3.2; if(Math.floor(st.plantP*8)!==a._lastBeep){a._lastBeep=Math.floor(st.plantP*8);} if(st.plantP>=1){ st.planted=true;st.bombPos={x:a.x,y:a.y};st.bombT=40;st.plantP=1; callout(st,'💣 BOMB PLANTED','#ffab4d'); const vz=VZ();if(vz)vz.sPlant(); } return; } if(blocker&&st.timeLeft<70){ // entry fight: collapse onto the defender moveAlong(st,a,{x:blocker.x,y:blocker.y},dt); return; } // support: crowd around site watching chokes a.moving=false; a.ang=Math.atan2(st.L.chokes[st.site][0][1]-a.y,st.L.chokes[st.site][0][0]-a.x)+(a.bob%1-0.5)*0.6; return; } moveAlong(st,a,a.goal,dt); } function ctBrain(st,a,dt){ if(st.planted){ if(st.defuser&&st.defuser!==a){} const d=Math.hypot(a.x-st.bombPos.x,a.y-st.bombPos.y); if(d>2.3)moveAlong(st,a,st.bombPos,dt); else{ a.moving=false; if(visAnyEnemy(st,a))return; // cover first } return; } const hot=hotSite(st); const mine=a.holdNear||(a.holdNear=holdSideOf(st,a)); if(hot&&hot!==mine){ moveAlong(st,a,{x:st.L.sites[hot][0],y:st.L.sites[hot][1]},dt); return; } if(a.hold&&st.planted===false&&st.timeLeft<58){ // aggressive CTs collapse toward the hotter choke to break the stalemate const target=(hot||st.siteGuess||(st.siteGuess=Math.random()<0.5?'A':'B')); const ch=st.L.chokes[target][0]; if(Math.hypot(a.x-ch[0],a.y-ch[1])>6){moveAlong(st,a,{x:ch[0],y:ch[1]},dt);return;} } if(a.hold){ a.moving=false; const fa=a.hold[2]; const base=fa==='r'?0:fa==='l'?Math.PI:fa==='u'?-Math.PI/2:fa==='d'?Math.PI/2: fa==='dr'?Math.PI/4:fa==='dl'?Math.PI*0.75:fa==='lu'?-Math.PI*0.75:-Math.PI/4; a.ang=base+Math.sin(st.t*0.85+a.bob)*0.07; } } /* ---------------- main step ---------------- */ function step(st,dt){ if(st.ended){st.endT+=dt;decayFx(st);return;} st.timeLeft-=dt; if(st.timeLeft<=0&&!st.planted){endRound(st,st.cfg.ctSide,'time');decayFx(st);return;} st.t+=dt; // scheduled attacker utility st.utilPlan.forEach(u=>{ const key=''+u.t+'_'+u.type; if(st.t>=u.t&&!st.utilDone.has(key)){ st.utilDone.add(key); throwNade(st,u.type,u.x,u.y); } }); // nades st.nades=st.nades.filter(n=>{ n.fuse-=dt;n.progress+=dt/n.total;n.z=Math.sin(Math.min(n.progress,1)*Math.PI)*3; if(n.fuse<=0){detonate(st,n);return false;} return true; }); st.smokes.forEach(s=>s.life+=dt/17); st.smokes=st.smokes.filter(s=>s.life<1); st.fires.forEach(f=>{ f.life+=dt/5; st.agents.forEach(a=>{ if(!a.dead&&Math.hypot(a.x-f.x,a.y-f.y)f.life<1); st.agents.forEach(a=>{ if(a.dead)return; a.cool-=dt; if(a.blind>0)a.blind-=dt; if(a.hp<=0){kill(st,a,null,'🔥');return;} // perception let vis=null; if(a.blind<=0){ if(st.t-(a._vt||-9)>=0.12){ let visD=1e9; for(const e of st.agents){ if(e.dead||e.isCT===a.isCT)continue; const d=Math.hypot(e.x-a.x,e.y-a.y); if(d>Math.min(62,a.weapon.rng*1.5+8))continue; const da=Math.atan2(e.y-a.y,e.x-a.x); const diff=Math.abs(((da-a.ang)+Math.PI*3)%(Math.PI*2)-Math.PI); if(d>3.5&&diff>1.2)continue; if(los(st.g,a,e,st.smokes)&&d{ if(m!==a&&!m.dead&&m.isCT===a.isCT&&Math.hypot(m.x-a.x,m.y-a.y)<26)m.alertT=st.t; }); a.reactT=(a.reactT===null||a.reactT===undefined)?a.react*(0.7+Math.random()*0.6):a.reactT-dt; a.ang=Math.atan2(vis.y-a.y,vis.x-a.x)+(Math.random()-0.5)*0.05; if(a.reactT<=0&&a.cool<=0){shoot(st,a,vis);a.cool=60/a.weapon.rpm;} // attackers keep pushing while trading (entry discipline by tactic) if(!a.isCT&&!st.planted){ const gd=a.goal?Math.hypot(a.goal.x-a.x,a.goal.y-a.y):99; const push=st.cfg.tactic[st.tKey]==='aggr'?1:0.45; if(gd>7&&Math.random()1.3)){a.target=null;a.reactT=null;} if(a.isCT)ctBrain(st,a,dt);else tBrain(st,a,dt); }); // defuse if(st.planted){ st.bombT-=dt; if(st.bombT<=0){explodeBomb(st);return;} if(st.defuser){ const du=st.defuser; if(du.dead||Math.hypot(du.x-st.bombPos.x,du.y-st.bombPos.y)>2.4){st.defuser=null;st.defuseP=0;} else{ st.defuseP+=dt/9; if(st.defuseP>=1){endRound(st,st.cfg.ctSide,'defuse');return;} } }else{ for(const c of st.agents){ if(c.dead||!c.isCT)continue; if(Math.hypot(c.x-st.bombPos.x,c.y-st.bombPos.y)<2.5&&!visAnyEnemy(st,c)){ st.defuser=c;c.moving=false;break; } } } } // gentle separation const al=st.agents.filter(a=>!a.dead); for(let i=0;i0.01&&d<1.4){ const px=dx/d*(1.4-d)*0.5,py=dy/d*(1.4-d)*0.5; if(walkable(st.g,A.x-px,A.y-py)){A.x-=px;A.y-=py;} if(walkable(st.g,B.x+px,B.y+py)){B.x+=px;B.y+=py;} } } decayFx(st); } function decayFx(st){ st.fx=st.fx.filter(f=>(st.t-f.t0){ if(!a.dead&&Math.hypot(a.x-st.bombPos.x,a.y-st.bombPos.y)<10)a.hp-=200; }); // mark deaths without recursion issues st.agents.filter(a=>!a.dead&&a.hp<=0).forEach(a=>kill(st,a,null,'💥 C4')); endRound(st,st.tKey,'bomb'); } function endRound(st,winnerKey,reason){ if(st.ended)return; st.ended=true;st.winner=winnerKey;st.reason=reason;st.endT=0; const won=winnerKey===st.cfg.userSide; const vz=VZ();if(vz)vz.sWinSting(won); } return {MAPS,buildGrid,astar,raycast,losClear,makeRound,step,WEAPONS, ensureConnectedForTest:(L,g)=>{if(!L._conn){ensureConnected(L,g);L._conn=true;}}}; })(); if(typeof window!=='undefined')window.LIVE=LIVE;