Files
counterstrikemanager/js/util.js
T
deepseek 4c7fe1db4c 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)
2026-08-23 07:00:47 +00:00

102 lines
4.7 KiB
JavaScript

'use strict';
/* ================= util ================= */
const U = {
rnd:(a,b)=>a+Math.random()*(b-a),
ri:(a,b)=>Math.floor(a+Math.random()*(b-a+1)),
pick:a=>a[Math.floor(Math.random()*a.length)],
pickW:(items,wfn)=>{ // weighted pick
let tot=0; const ws=items.map(it=>{const w=Math.max(0.0001,wfn(it)); tot+=w; return w;});
let r=Math.random()*tot;
for(let i=0;i<items.length;i++){ r-=ws[i]; if(r<=0) return items[i]; }
return items[items.length-1];
},
chance:p=>Math.random()<p,
gauss:(m=0,s=1)=>{let u=0,v=0;while(!u)u=Math.random();while(!v)v=Math.random();return m+s*Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);},
clamp:(v,a,b)=>Math.max(a,Math.min(b,v)),
lerp:(a,b,t)=>a+(b-a)*t,
sum:arr=>arr.reduce((s,x)=>s+x,0),
avg:arr=>arr.length?U.sum(arr)/arr.length:0,
shuffle:a=>{for(let i=a.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[a[i],a[j]]=[a[j],a[i]];}return a;},
esc:s=>String(s??'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])),
uid:()=> 'x'+Math.random().toString(36).slice(2,9)+Date.now().toString(36).slice(-4),
fmtM(n){
if(n===Infinity||n===-Infinity) return '∞';
const neg=n<0; n=Math.abs(Math.round(n));
let s;
if(n>=1e9) s=(n/1e9).toFixed(2)+'B';
else if(n>=1e6) s=(n/1e6).toFixed(n>=1e7?1:2)+'M';
else if(n>=1e3) s=(n/1e3).toFixed(0)+'K';
else s=String(n);
return (neg?'-$':'$')+s;
},
fmtNum(n){return Math.round(n).toLocaleString('en-US');},
/* day 0 = Mon 5 Jan 2026 */
dateFromDay(d){const dt=new Date(Date.UTC(2026,0,5)); dt.setUTCDate(dt.getUTCDate()+d); return dt;},
fmtDate(d){const dt=U.dateFromDay(d);
return dt.toLocaleDateString('en-GB',{day:'numeric',month:'short',year:'numeric',timeZone:'UTC'});},
fmtDateShort(d){const dt=U.dateFromDay(d);return dt.toLocaleDateString('en-GB',{day:'numeric',month:'short',timeZone:'UTC'});},
dow(d){return U.dateFromDay(d).getUTCDay();}, // 0 sun .. 6 sat
weekOfYear(d){return Math.floor((d+4)/7);},
logistic(x){return 1/(1+Math.exp(-x));},
};
/* role weights for overall rating (attrs: aim,pos,clutch,util,iq,move,comm) */
const ROLE_W={
IGL: {iq:.32,comm:.22,util:.14,pos:.12,aim:.08,clutch:.04,move:.08},
AWP: {aim:.38,pos:.18,iq:.13,clutch:.10,move:.10,comm:.07,util:.04},
Entry: {aim:.33,move:.20,pos:.12,clutch:.09,iq:.10,util:.08,comm:.08},
Rifler:{aim:.26,iq:.16,pos:.16,move:.12,clutch:.10,util:.10,comm:.10},
Support:{util:.28,iq:.20,comm:.17,pos:.15,aim:.10,move:.06,clutch:.04},
Lurker:{pos:.26,iq:.22,aim:.16,clutch:.14,move:.10,util:.06,comm:.06},
};
const ROLES=Object.keys(ROLE_W);
function playerOverall(p){
const w=ROLE_W[p.role]||ROLE_W.Rifler; let s=0;
for(const k in w) s+=p.attrs[k]*w[k];
return s;
}
/* market value & wage model. attrs 40..99 scale */
function playerValue(p,G){
const ovr=playerOverall(p), potGap=p.potential-ovr;
const ageF = p.age<=19?1.45 : p.age<=22?1.35 : p.age<=25?1.2 : p.age<=28?1.0 : p.age<=31?0.72 : 0.42;
let v = Math.pow(Math.max(ovr-40,1),2.75) * 14 * ageF;
v *= (1 + Math.max(potGap,0)*0.022);
v *= (0.75 + p.form*0.05); // form 0..10
v *= (0.85 + p.morale/100*0.3);
const yrsLeft = Math.max(0,(p.contractUntil-G.day)/364);
if(yrsLeft<0.5 && G.players[p.teamId]!==undefined) v*=0.65; // expiring contract discount
if(p.injuryDays>7) v*=0.85;
return Math.round(v/1000)*1000;
}
function wageDemand(p,G){
const ovr=playerOverall(p);
let base = Math.pow(Math.max(ovr-40,1),2.6)*0.75; // monthly usd
base *= (0.8 + p.form*0.045);
base *= (1 + (p.potential-ovr)*0.008);
const yrsLeft=Math.max(0,(p.contractUntil-G.day)/364);
if(yrsLeft<0.5) base*=1.25;
base *= G.diff.wageMul;
return Math.round(base/500)*500;
}
const NATIONS={ // flag emoji
UA:'🇺🇦',RU:'🇷🇺',DK:'🇩🇰',LV:'🇱🇻',IL:'🇮🇱',DE:'🇩🇪',FR:'🇫🇷',GB:'🇬🇧',PL:'🇵🇱',CZ:'🇨🇿',
SE:'🇸🇪',NO:'🇳🇴',FI:'🇫🇮',EE:'🇪🇪',LT:'🇱🇹',NL:'🇳🇱',BG:'🇧🇬',RO:'🇷🇴',RS:'🇷🇸',
US:'🇺🇸',CA:'🇨🇦',BR:'🇧🇷',AR:'🇦🇷',UY:'🇺🇾',MX:'🇲🇽',
TR:'🇹🇷',KZ:'🇰🇿',MN:'🇲🇳',CN:'🇨🇳',KR:'🇰🇷',JP:'🇯🇵',
AU:'🇦🇺',NZ:'🇳🇿',ES:'🇪🇸',PT:'🇵🇹',IT:'🇮🇹',BA:'🇧🇦',MK:'🇲🇰',GR:'🇬🇷',SA:'🇸🇦',MA:'🇲🇦',ID:'🇮🇩'
};
const FLAG=n=>NATIONS[n]||'🏳️';
function faceFor(m){ // morale emoji
if(m>=80)return '😄'; if(m>=60)return '🙂'; if(m>=40)return '😐'; if(m>=20)return '🙁'; return '😡';
}
function formStars(f){ // f 0..10 → stars display
const s=Math.round(f/2); return '★'.repeat(s)+'☆'.repeat(5-s);
}
function ovrClass(o){return o>=90?'s90':o>=80?'s80':'';}