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:
deepseek
2026-08-23 07:00:47 +00:00
commit 4c7fe1db4c
15 changed files with 4722 additions and 0 deletions
+995
View File
@@ -0,0 +1,995 @@
'use strict';
/* ================= UI layer ================= */
const $=id=>document.getElementById(id);
const UI={
route:'dashboard',
/* ---------- shell ---------- */
show(){
$('titleScreen').style.display='none';
$('newGameScreen').style.display='none';
$('gameShell').style.display='grid';
this.go('dashboard');
},
go(route){
this.route=route;
document.querySelectorAll('.nav-btn').forEach(b=>b.classList.toggle('active',b.dataset.route===route));
const R={dashboard:this.rDash,squad:this.rSquad,training:this.rTraining,transfers:this.rTransfers,
tournaments:this.rTours,matches:this.rMatches,staff:this.rStaff,finance:this.rFinance,
rankings:this.rRankings,inbox:this.rInbox};
(R[route]||this.rDash).call(this);
this.updateTopbar();
const v=$('view');if(v)v.scrollTop=0;
},
updateTopbar(){
if(!G)return;
const me=Game.me();
$('orgBadge').innerHTML=`<span class="dot" style="background:${me.color}"></span><div>${U.esc(me.name)}<small>${me.tag} · #${Game.rankOf(me.id)||'—'} WORLD</small></div>`;
$('tbMoney').textContent=U.fmtM(G.money);
$('tbMoney').className=G.money<0?'bad':'good';
const net=(Game.lastWeeklyMerch||0)-(Game.lastWeeklyWages||0);
$('tbNet').textContent=(net>=0?'+':'')+U.fmtM(net);
$('tbNetIc').textContent=net>=0?'📈':'📉';
$('tbNet').style.color=net>=0?'var(--good)':'var(--bad)';
$('tbFans').textContent=U.fmtNum(G.fans);
$('tbRank').textContent='#'+(Game.rankOf(me.id)||'—');
$('tbBoard').textContent=Math.round(G.board)+'%';
$('tbBoard').style.color=G.board>50?'var(--good)':G.board>25?'var(--warn)':'var(--bad)';
$('tbDate').textContent=U.fmtDate(G.day)+' · S'+G.season;
$('tbNextMatch').textContent=this.nextFixtureInfo()||'no match scheduled';
const unread=G.news.filter(n=>!n.read).length;
const pill=$('inboxCount');pill.style.display=unread?'':'none';pill.textContent=unread;
},
nextFixtureInfo(){
for(const ev of G.calendar){
if(ev.status!=='live')continue;
for(const rnd of ev.rounds)for(const m of rnd.matches)
if((m.a===G.playerTeamId||m.b===G.playerTeamId)&&!m.winner&&m.a&&m.b)
return `${ev.name}: vs ${Game.team(m.a===G.playerTeamId?m.b:m.a).tag} · ${U.fmtDateShort(m.day)}`;
}
const next=G.calendar.find(e=>e.status!=='done'&&e.registered&&e.startDay>G.day);
return next?`next: ${next.name} (${U.fmtDateShort(next.startDay)})`:'';
},
toast(msg,cls=''){
const t=document.createElement('div');t.className='toast '+cls;t.innerHTML=msg;
$('toastRoot').appendChild(t);setTimeout(()=>t.remove(),4200);
},
modal(html,wide){
this.closeModal();
const bg=document.createElement('div');bg.className='modal-bg';bg.id='modalBg';
bg.innerHTML=`<div class="modal" style="${wide?'width:min(980px,96vw)':''}">${html}</div>`;
bg.addEventListener('click',e=>{if(e.target===bg)bg.remove();});
$('modalRoot').appendChild(bg);return bg;
},
closeModal(){const m=$('modalBg');if(m)m.remove();},
/* ---------- advance ---------- */
advance(){
const fx=Game.advanceWeek();
this.updateTopbar();
if(fx)this.openMatchModal(fx);
else{
this.refreshRoute();
this.toast(`📅 ${U.fmtDate(G.day)}`);
}
},
refreshRoute(){this.go(this.route);},
continueAfterMatch(){
this.closeModal();
$('simOverlay').style.display='none';
const fx=Game.remainingAdvance()>0?Game.resumeAdvance():null;
if(fx)this.openMatchModal(fx);
else this.refreshRoute();
},
/* ================= DASHBOARD ================= */
rDash(){
const me=Game.me();
const rank=Game.rankOf(me.id);
const liveEv=G.calendar.find(e=>e.status==='live'&&e.teams.includes(me.id));
const upc=G.calendar.filter(e=>e.status!=='done'&&e.startDay>G.day-10).slice(0,5);
const recent=[];
G.calendar.forEach(e=>e.rounds.forEach(r=>r.matches.forEach(m=>{
if(m.a&&m.b&&(m.a===me.id||m.b===me.id)&&m.winner)recent.push({m,e});
})));
recent.sort((a,b)=>b.m.day-a.m.day);
const obj=Game.objectives();
let html=`<h1 style="font-size:22px">Club Dashboard</h1>
<div class="stat-tiles mt">
<div class="tile"><div class="t-label">Balance</div><div class="t-val ${G.money>=0?'good':'bad'}">${U.fmtM(G.money)}</div></div>
<div class="tile"><div class="t-label">World Rank</div><div class="t-val">#${rank}</div></div>
<div class="tile"><div class="t-label">Fans</div><div class="t-val">${U.fmtNum(G.fans)}</div></div>
<div class="tile"><div class="t-label">Reputation</div><div class="t-val">${Math.round(G.rep)}</div></div>
<div class="tile"><div class="t-label">Board</div><div class="t-val" style="color:${G.board>50?'var(--good)':G.board>25?'var(--warn)':'var(--bad)'}">${Math.round(G.board)}%</div></div>
<div class="tile"><div class="t-label">Chemistry</div><div class="t-val">${Math.round(me.fam)}%</div></div>
</div>
<div class="card mt"><h3>🎯 Season ${G.season} Objectives</h3>
<div class="grid" style="grid-template-columns:repeat(auto-fit,minmax(230px,1fr))">
<div><div class="kv"><span>World ranking</span><b class="${obj.rankOk?'good':'warn'}">#${obj.rank} · target TOP ${obj.targetRank}</b></div>
<div class="bar"><i style="width:${U.clamp(obj.targetRank/Math.max(rank,1)*100,4,100)}%;background:${obj.rankOk?'var(--good)':'var(--warn)'}"></i></div></div>
<div><div class="kv"><span>Finances</span><b class="${obj.moneyOk?'good':'bad'}">${obj.moneyOk?'solvent ⚖️':'deficit ⚠️'}</b></div>
<div class="bar"><i style="width:${U.clamp((G.money+200000)/1700000*100,2,100)}%;background:${obj.moneyOk?'var(--good)':'var(--bad)'}"></i></div></div>
<div><div class="kv"><span>Board confidence</span><b>${obj.board}%</b></div>
<div class="bar"><i style="width:${obj.board}%;background:${G.board>50?'var(--good)':G.board>25?'var(--warn)':'var(--bad)'}"></i></div></div>
</div></div>
<div class="card-grid mt">`;
html+=`<div class="card"><h3>${liveEv?'🔴 LIVE — '+U.esc(liveEv.name):'📅 Upcoming'}</h3>`;
if(liveEv){
html+=this.bracketMini(liveEv)+
`<button class="btn btn-accent btn-sm mt" onclick="UI.eventDetail('${liveEv.id}')">Open bracket</button>`;
}else{
const nxt=G.calendar.find(e=>e.registered&&e.status!=='done'&&e.startDay>G.day);
html+=nxt?`<p>${U.esc(nxt.name)} starts <b>${U.fmtDate(nxt.startDay)}</b>.</p>
<p class="muted small">Tier ${nxt.tier} · ${U.fmtM(nxt.prize)} to the winner · ${nxt.slots} teams${nxt.major?' · MAJOR 🏆':''}</p>`
:`<p class="muted">No event registered.</p><button class="btn btn-accent btn-sm" onclick="UI.go('tournaments')">Browse Tournaments →</button>`;
html+=`<h3 class="mt">Calendar ahead</h3>`+upc.map(e=>
`<div class="kv"><span>${U.esc(e.name)}${e.registered?' <span class="cy">✓</span>':''}</span><span class="muted small">${U.fmtDateShort(e.startDay)} · T${e.tier}</span></div>`).join('');
}
html+=`</div>`;
const sorted=Game.teamPlayers(me).sort((a,b)=>playerOverall(b)-playerOverall(a));
const trophies=Object.entries(G.achievements).filter(([k])=>k.startsWith('trophy_'));
const expiring=Game.teamPlayers(me)
.filter(p=>p.contractUntil>G.day&&p.contractUntil-G.day<=150)
.sort((a,b)=>a.contractUntil-b.contractUntil).slice(0,5);
html+=`<div class="card"><h3>🏆 Trophy Cabinet</h3>${
G.achievements.grandSlam?'<p class="gold">💎 GRAND SLAM CHAMPION</p>':''}${
trophies.length?trophies.map(([k,v])=>`<div class="kv"><span>🥆 ${U.esc(v.ev)}</span><span class="tiny muted">${U.fmtDateShort(v.day)}</span></div>`).join('')
:'<p class="muted small">Empty shelf. The first trophy tastes the sweetest.</p>'
}<h3 class="mt">⏳ Expiring Contracts</h3>${
expiring.map(p=>{const left=p.contractUntil-G.day;
return `<div class="kv clickable" onclick="UI.playerModal('${p.id}')"><span>${p.nick} <span class="tiny muted">${playerOverall(p).toFixed(0)}</span></span><span class="${left<45?'bad':left<90?'warn':'muted'} tiny">${left}d</span></div>`;}).join('')
||'<p class="good tiny">All contracts secure ✔</p>'
}</div>`;
html+=`<div class="card"><h3>🏅 Career Milestones</h3>${Game.MILESTONES.map(m=>{
const got=G.milestones.includes(m.id);
return `<div class="kv" title="${U.esc(m.desc)}"><span>${got?'✅':'🔒'} ${m.name}</span><span class="tiny ${got?'gold':'muted'}">${got?'unlocked':U.esc(m.desc)}</span></div>`;
}).join('')}</div>`;
html+=`<div class="card"><h3>Squad Snapshot</h3><table class="tbl"><tr><th></th><th>Player</th><th>Form</th><th>Morale</th><th>Fatigue</th></tr>${
sorted.slice(0,6).map(p=>`<tr class="clickable" onclick="UI.playerModal('${p.id}')">
<td><span class="ovr ${ovrClass(playerOverall(p))}">${playerOverall(p).toFixed(0)}</span></td>
<td><span class="nick">${p.nick}</span> ${FLAG(p.nat)}<br><span class="tiny muted">${p.role}</span></td>
<td class="stars tiny">${formStars(p.form)}</td>
<td>${faceFor(p.morale)}</td>
<td><div class="bar fatigue"><i style="width:${p.fatigue}%"></i></div></td></tr>`).join('')
}</table></div>`;
html+=`<div class="card"><h3>Recent Results</h3>${
recent.slice(0,6).map(({m,e})=>{
const won=m.winner===me.id;
return `<div class="kv clickable" onclick="UI.boxScoreModal('${e.id}','${m.id}')"><span>${won?'✅':'❌'} vs ${Game.team(m.a===me.id?m.b:m.a).name}</span><span class="${won?'good':'bad'}"><b>${m.score}</b> <span class="muted tiny">${U.esc(e.name)}</span></span></div>`;
}).join('')||'<p class="muted">No matches played yet.</p>'
}</div></div>`;
html+=`<div class="card mt"><h3>Latest News</h3>${
G.news.slice(0,6).map(newsRow).join('')}</div>`;
$('view').innerHTML=html;
},
bracketMini(ev){
let html='';
ev.rounds.forEach(rnd=>{
rnd.matches.forEach(m=>{
if(!m.a||!m.b)return;
const mine=m.a===G.playerTeamId||m.b===G.playerTeamId;
html+=`<div class="kv" style="${mine?'font-weight:800':''}">
<span>${Game.team(m.a).tag} vs ${Game.team(m.b).tag}${mine?' ⭐':''}</span>
<span>${m.winner?`${m.score} <span class="muted tiny">${rnd.name}</span>`:`<span class="cy tiny">${U.fmtDateShort(m.day)}</span>`}</span></div>`;
});
});
return html||'<p class="muted">Bracket forming…</p>';
},
/* ================= SQUAD ================= */
rSquad(){
const me=Game.me();
const ps=Game.teamPlayers(me).sort((a,b)=>playerOverall(b)-playerOverall(a));
const rows=ps.map(p=>{
const starter=me.lineup.includes(p.id)?'⭐':'';
const left=p.contractUntil-G.day;
const ctr=left<45?'bad':left<120?'warn':'muted';
return `<tr class="clickable" onclick="UI.playerModal('${p.id}')">
<td><span class="ovr ${ovrClass(playerOverall(p))}">${playerOverall(p).toFixed(0)}</span></td>
<td>${starter}<span class="nick">${p.nick}</span> ${FLAG(p.nat)}${p.listedPrice!==null?' 🔁':''}<br><span class="tiny muted">${U.esc(p.name)}, ${p.age}</span></td>
<td><span class="role-tag">${p.role}</span></td>
<td class="stars tiny">${formStars(p.form)}</td>
<td>${faceFor(p.morale)} <span class="tiny muted">${Math.round(p.morale)}</span></td>
<td><div class="bar fatigue"><i style="width:${p.fatigue}%"></i></div>${p.injuryDays?`<span class="bad tiny">🏥${p.injuryDays}d</span>`:''}</td>
<td class="num">${U.fmtM(p.salary)}</td>
<td class="num">${U.fmtM(playerValue(p,G))}</td>
<td class="${ctr} tiny">${left<=0?'EXPIRED':left+'d'}</td></tr>`;
}).join('');
$('view').innerHTML=`<h1 style="font-size:22px">Squad Management</h1>
<div class="card mt"><h3>Starting Lineup</h3>
<div class="wrap">${me.lineup.map((pid,i)=>{const p=Game.p(pid);return `<span class="tagchip">${i+1}. <b>${p.nick}</b> ${playerOverall(p).toFixed(0)}${p.injuryDays?' 🏥':''}</span>`;}).join('')}
<button class="btn btn-ghost btn-sm" onclick="Game.autoLineup(Game.me());Game.checkPromisesAfterLineup();UI.toast('Best available five selected','good');UI.refreshRoute()">⚙ Auto-pick best</button>
<button class="btn btn-accent btn-sm" onclick="UI.scoutModal()">🔍 Scout prospects (${U.fmtM(Game.SCOUT_COST)})</button></div>
</div>
<div class="card mt"><table class="tbl">
<tr><th>OVR</th><th>Player</th><th>Role</th><th>Form</th><th>Morale</th><th>Fatigue</th><th class="num">Wage/mo</th><th class="num">Value</th><th>Contract</th></tr>
${rows}</table></div>
<p class="tiny muted mt">Roster cap: 8 players (${ps.length}/8).</p>`;
},
playerModal(pid){
const p=Game.p(pid);
const mine=p.teamId===G.playerTeamId;
const ovr=playerOverall(p);
const inLineup=mine&&Game.me().lineup.includes(pid);
const left=p.contractUntil-G.day;
const attrs=[['Aim',p.attrs.aim],['Positioning',p.attrs.pos],['Clutch',p.attrs.clutch],['Utility',p.attrs.util],['Gamesense',p.attrs.iq],['Movement',p.attrs.move],['Communication',p.attrs.comm]];
const s=p.seasonStats,r=s.maps?s.ratingSum/s.maps:0;
const bar=(label,val,max=100)=>`<div class="kv"><span>${label}</span><span style="min-width:130px"><div class="bar"><i style="width:${U.clamp(val/max*100,2,100)}%"></i></div></span></div>`;
const html=`<button class="close-x" onclick="UI.closeModal()">×</button>
<div class="flex"><span class="ovr ${ovrClass(ovr)}" style="min-width:52px;height:52px;font-size:24px">${ovr.toFixed(0)}</span>
<div><h2>${p.nick} ${FLAG(p.nat)}</h2><div class="muted">${U.esc(p.name)} · ${p.age}y · ${p.role}${p.potential>ovr+4&&mine?` · <span class="cy">POT ${p.potential}</span>`:''}</div></div></div>
<div class="card-grid mt">
<div class="card"><h3>Attributes</h3>${attrs.map(([l,vv])=>bar(l,vv)).join('')}
<div class="kv mt"><span>Progress to potential</span><span style="min-width:130px"><div class="bar"><i style="width:${U.clamp(ovr/Math.max(p.potential,1)*100,5,100)}%;background:${ovr>=p.potential-2?'var(--gold)':'var(--accent2)'}"></i></div></span></div>
<div class="mt small muted">Form ${formStars(p.form)} · Morale ${faceFor(p.morale)} ${Math.round(p.morale)} · Fatigue ${Math.round(p.fatigue)}%${p.injuryDays?` · <span class="bad">Injured ${p.injuryDays}d</span>`:''}</div>
<div class="tiny muted mt">Professionalism ${p.personality.prof} · Temperament ${p.personality.temper} · Ambition ${p.personality.ambition}</div></div>
<div class="card"><h3>Season Stats</h3>
<div class="stat-tiles">
<div class="tile"><div class="t-label">Maps</div><div class="t-val">${s.maps}</div></div>
<div class="tile"><div class="t-label">Rating</div><div class="t-val">${r?r.toFixed(2):'—'}</div></div>
<div class="tile"><div class="t-label">K/D</div><div class="t-val">${s.maps&&s.d?(s.k/s.d).toFixed(2):'—'}</div></div>
<div class="tile"><div class="t-label">ADR</div><div class="t-val">${s.maps?Math.round(s.adr):'—'}</div></div>
<div class="tile"><div class="t-label">MVPs</div><div class="t-val">${s.mvp}</div></div>
<div class="tile"><div class="t-label">Clutches</div><div class="t-val">${s.aces}</div></div>
</div>
<div class="mt kv"><span>Contract</span><span>${mine?(left>0?left+' days left':`expired ${-left}d ago`):U.fmtDate(p.contractUntil)}</span></div>
<div class="kv"><span>Value ≈</span><b>${U.fmtM(playerValue(p,G))}</b></div>
</div>
</div>
${mine?`<div class="flex wrap mt">
<button class="btn btn-accent btn-sm" onclick="UI.talkModal('${pid}')">💬 Talk</button>
${inLineup
?`<button class="btn btn-ghost btn-sm" onclick="Game.me().lineup=Game.me().lineup.filter(x=>x!=='${pid}');Game.checkPromisesAfterLineup();UI.closeModal();UI.refreshRoute()">Bench</button>`
:`<button class="btn btn-good btn-sm" onclick="UI.addToLineup('${pid}')">Add to Lineup</button>`}
<button class="btn btn-primary btn-sm" onclick="UI.negotiateModal('${pid}','renew')">🖊 Renew Contract</button>
${p.listedPrice!==null?`<button class="btn btn-ghost btn-sm" onclick="Game.unlistPlayer('${pid}');UI.toast('Removed from transfer list');UI.closeModal();UI.refreshRoute()">Unlist</button>`
:`<button class="btn btn-ghost btn-sm" onclick="UI.closeModal();UI.listModal('${pid}')">🔁 Transfer List</button>`}
${!inLineup&&Game.me().roster.length>5?`<button class="btn btn-ghost btn-sm" onclick="UI.closeModal();UI.loanModal('${pid}')">📤 Loan Out</button>`:''}
<button class="btn btn-danger btn-sm" onclick="UI.confirmRelease('${pid}')">Release</button>
</div>`:
`<div class="flex wrap mt">
${p.teamId?`<button class="btn btn-primary btn-sm" onclick="UI.negotiateModal('${pid}','buy')">💰 Make Transfer Offer</button>`
:`<button class="btn btn-good btn-sm" onclick="UI.negotiateModal('${pid}','fa')">✍ Sign Free Agent</button>`}
<span class="muted small">Wage demand ≈ ${U.fmtM(wageDemand(p,G))}/mo${p.listedPrice!==null?` · asking ${U.fmtM(p.listedPrice)}`:''}</span>
</div>`}`;
this.modal(html,true);
},
addToLineup(pid){
const me=Game.me();
if(me.lineup.length>=5){this.toast('Lineup is full — bench someone first.','warn');return;}
if(!me.lineup.includes(pid))me.lineup.push(pid);
Game.checkPromisesAfterLineup();
this.closeModal();this.refreshRoute();
},
confirmRelease(pid){
const p=Game.p(pid);
if(confirm(`Release ${p.nick}? A severance payment applies.`)){
const r=Game.releasePlayer(pid);
this.toast(r.ok?`${p.nick} released.`:r.msg,r.ok?'good':'bad');
this.closeModal();this.refreshRoute();
}
},
listModal(pid){
const p=Game.p(pid);
const val=playerValue(p,G);
const html=`<h2>Transfer List — ${p.nick}</h2><p class="muted small">Market value: ${U.fmtM(val)}. Listed players attract bids from other clubs (see Inbox / Transfers → Bids).</p>
<label class="small muted">Asking price</label>
<input id="listPrice" type="number" step="50000" value="${val}" style="width:100%">
<div class="flex mt"><button class="btn btn-accent" onclick="Game.listPlayer('${pid}',parseInt(document.getElementById('listPrice').value)||${val});UI.toast('${p.nick} listed for transfer','good');UI.closeModal();UI.refreshRoute()">Confirm listing</button>
<button class="btn btn-ghost" onclick="UI.closeModal()">Cancel</button></div>`;
this.modal(html);
},
scoutModal(){
const r=Game.scoutYouth();
if(r.ok===false){UI.toast(r.msg,'warn');return;}
this.updateTopbar();
const card=p=>{
const ovr=playerOverall(p);
const attrs=[['AIM',p.attrs.aim],['POS',p.attrs.pos],['CLU',p.attrs.clutch],['UTL',p.attrs.util],['IQ',p.attrs.iq],['MOV',p.attrs.move],['COM',p.attrs.comm]];
return `<div class="offer-card"><div class="flex spread">
<div><b>${FLAG(p.nat)} ${p.nick}</b> <span class="tiny muted">${p.age}y · ${p.role} · ${NATIONS[p.nat]||p.nat}</span><br>
<span class="ovr ${ovrClass(ovr)}">${ovr.toFixed(0)}</span> <span class="tiny gold">POT ${p.potential}★</span></div>
<button class="btn btn-primary btn-sm" onclick="const rr=Game.signProspect('${p.id}');if(rr&&rr.ok===false){UI.toast(rr.msg,'warn');}else{UI.toast('${p.nick} joins the academy!','good');UI.closeModal();UI.refreshRoute();}">Sign (3y)</button></div>
<div class="tiny muted mt" style="letter-spacing:1px">${attrs.map(([l,v])=>`${l} ${v.toFixed(0)}`).join(' · ')}</div>
</div>`;
};
this.modal(`<h2>🔍 Scouting Camp — $80K</h2>
<p class="muted small">Three unattached youngsters caught our scouts' eyes. Sign one — the others return to obscurity. Potential scales with club reputation.</p>
<div class="mt">${r.list.map(card).join('')}</div>
<div class="mt"><button class="btn btn-ghost btn-sm" onclick="UI.closeModal()">Walk away</button></div>`);
},
loanModal(pid){ const p=Game.p(pid);
const clubs=Object.values(G.teams).filter(t=>!t.isPlayer&&t.id!==p.teamId)
.sort((a,b)=>a.ratingPts-b.ratingPts).slice(0,8);
const html=`<h2>Development Loan — ${p.nick}</h2>
<p class="muted small">Loaning a young player out for 90 days earns a small fee and accelerates his growth through playing time. He returns automatically afterwards.</p>
<div class="mt">${clubs.map(t=>`
<div class="offer-card flex spread">
<div><b style="color:${t.color}">${U.esc(t.name)}</b> <span class="tiny muted">Tier ${t.tier} · #${Game.rankOf(t.id)} · roster ${t.roster.length}/8</span></div>
<button class="btn btn-primary btn-sm" onclick="const r=Game.sendOnLoan('${pid}','${t.id}');if(r&&r.ok===false){UI.toast(r.msg,'warn');}else{UI.toast('${p.nick} loaned to ${U.esc(t.tag)}','good');}UI.closeModal();UI.refreshRoute()">Loan</button>
</div>`).join('')}</div>
<div class="flex mt"><button class="btn btn-ghost" onclick="UI.closeModal()">Cancel</button></div>`;
this.modal(html);
},
/* ---------- talk module ---------- */
talkModal(pid){
const p=Game.p(pid);
const topics=[['praise','👏 Praise his form'],['motivate','🔥 Motivate him'],['role','🧩 Discuss role'],
['discipline','⚠️ Demand professionalism'],['promise','🤝 Promise a starting spot'],['contract','💵 Hint at a raise']];
const tones=['supportive','neutral','demanding'];
const html=`<button class="close-x" onclick="UI.closeModal()">×</button>
<h2>Talk to ${p.nick}</h2>
<p class="muted small">Morale ${faceFor(p.morale)} ${Math.round(p.morale)} · Motivation ${Math.round(p.motivation)} · Form ${formStars(p.form)}. One meaningful talk per ~10 days.</p>
<h3>Topic</h3><div class="tac-grid" id="talkTopics">${
topics.map(([id,l])=>`<button class="choice" data-t="${id}" onclick="document.querySelectorAll('#talkTopics .choice').forEach(b=>b.classList.remove('on'));this.classList.add('on')"><b>${l}</b></button>`).join('')
}</div>
<h3>Tone</h3><div class="seg" id="talkTones">${tones.map((t,i)=>`<button class="${i===1?'on':''}" data-t="${t}" onclick="document.querySelectorAll('#talkTones button').forEach(b=>b.classList.remove('on'));this.classList.add('on')">${t[0].toUpperCase()+t.slice(1)}</button>`).join('')}</div>
<div class="flex mt"><button class="btn btn-accent" id="doTalk">Have the conversation</button>
<button class="btn btn-ghost" onclick="UI.closeModal()">Not now</button></div>
<div id="talkResult" class="mt"></div>`;
this.modal(html);
$('doTalk').onclick=()=>{
const topic=document.querySelector('#talkTopics .on')?.dataset.t;
const tone=document.querySelector('#talkTones .on')?.dataset.t||'neutral';
if(!topic){$('talkResult').innerHTML='<p class="bad">Pick a topic first.</p>';return;}
const res=Game.applyTalk(pid,topic,tone);
$('talkResult').innerHTML=`<div class="offer-card"><p>${U.esc(res.msg)}</p>
${res.dM!==undefined?`<p class="tiny muted">Morale ${res.dM>=0?'+':''}${res.dM} · Motivation ${res.dMo>=0?'+':''}${res.dMo}</p>`:''}
<button class="btn btn-ghost btn-sm mt" onclick="UI.closeModal();UI.refreshRoute()">Done</button></div>`;
this.updateTopbar();
};
},
/* ---------- negotiation ---------- */
negotiateModal(pid,kind){
const p=Game.p(pid);
const demand=wageDemand(p,G);
const defSalary=Math.max(kind==='renew'?p.salary:0,Math.round(demand/1000)*1000);
const title=kind==='renew'?`Renew — ${p.nick}`:(kind==='fa'?`Sign free agent ${p.nick}`:`Bid for ${p.nick} (${U.esc(Game.team(p.teamId)?.tag||'')})`);
const feeField=kind==='buy'?`<label class="small muted">Transfer fee (value ≈ ${U.fmtM(playerValue(p,G))})<input id="negFee" type="number" step="50000" value="${playerValue(p,G)}"></label>`:'';
const html=`<button class="close-x" onclick="UI.closeModal()">×</button>
<h2>${title}</h2>
<p class="muted small">His camp wants ≈ <b>${U.fmtM(demand)}</b>/mo. Current club: ${p.teamId?U.esc(Game.team(p.teamId).name):'free agent'} · morale ${faceFor(p.morale)} · OVR ${playerOverall(p).toFixed(0)}</p>
<div class="form-grid">
${feeField}
<label class="small muted">Monthly wage<input id="negSalary" type="number" step="500" value="${defSalary}"></label>
<label class="small muted">Years<input id="negYears" type="number" min="1" max="5" value="2"></label>
<label class="small muted">Signing bonus<input id="negBonus" type="number" step="25000" value="0"></label>
</div>
<div class="flex mt"><button class="btn btn-accent" id="negGo">Submit offer</button>
<button class="btn btn-ghost" onclick="UI.closeModal()">Cancel</button></div>
<div id="negResult" class="mt"></div>`;
this.modal(html);
$('negGo').onclick=()=>{
const salary=parseInt($('negSalary').value)||demand;
const years=U.clamp(parseInt($('negYears').value)||2,1,5);
const bonus=parseInt($('negBonus').value)||0;
let res;
if(kind==='renew')res=Game.renewContract(pid,salary,years,bonus);
else if(kind==='fa')res=Game.signFreeAgent(pid,salary,years,bonus);
else{
const fee=parseInt($('negFee')?.value)||playerValue(p,G);
res=Game.makeTransferOffer(pid,fee,salary,years);
}
if(res.ok){this.toast(res.msg,'good');this.closeModal();this.refreshRoute();}
else $('negResult').innerHTML=`<div class="offer-card">❌ ${U.esc(res.msg)}
<div class="mt"><button class="btn btn-ghost btn-sm" onclick="UI.closeModal()">Close</button></div></div>`;
};
},
/* ================= TRAINING ================= */
rTraining(){
const me=Game.me();
const coach=Game.staffOf(me,'Coach'),physio=Game.staffOf(me,'Physio');
const rowHtml=Game.teamPlayers(me).map(p=>{
const focusSel=TRAINING_FOCUS.map(f=>`<option ${p.training.focus===f?'selected':''}>${f}</option>`).join('');
const intSel=[[0,'Light'],[1,'Normal'],[2,'Hard']].map(([v,l])=>`<option value="${v}" ${p.training.intensity===v?'selected':''}>${l}</option>`).join('');
return `<tr>
<td><b>${p.nick}</b> <span class="tiny muted">${p.role}</span></td>
<td><span class="ovr" style="min-width:28px;height:28px;font-size:13px">${playerOverall(p).toFixed(0)}</span> <span class="tiny cy">→ ${p.potential}</span></td>
<td><select onchange="Game.setTraining('${p.id}',this.value);UI.refreshRoute()">${focusSel}</select></td>
<td><select onchange="Game.setTraining('${p.id}',undefined,parseInt(this.value));UI.refreshRoute()">${intSel}</select></td>
<td><div class="bar fatigue"><i style="width:${p.fatigue}%"></i></div></td>
<td>${p.injuryDays?`<span class="bad">🏥 ${p.injuryDays}d</span>`:'<span class="good">✔</span>'}</td></tr>`;
}).join('');
const bootcamping=(G.flags.bootcampUntil||0)>G.day;
$('view').innerHTML=`<h1 style="font-size:22px">Training Center</h1>
<div class="card-grid mt">
<div class="card"><h3>Weekly Plan</h3><p class="muted small">Focus each player's week. Hard intensity grows skills fastest but piles up fatigue — injury risk rises above 86%. Rest recovers body and mind. Media weeks grow your fanbase.</p></div>
<div class="card"><h3>Support Effects</h3>
<div class="kv"><span>Coach</span><span>${coach?`<b>${coach.nick}</b> (+${Math.round(coach.skill-55)}% growth)`:'<span class="bad">vacant</span>'}</span></div>
<div class="kv"><span>Physio</span><span>${physio?`<b>${physio.nick}</b> (faster recovery)`:'<span class="bad">vacant</span>'}</span></div>
<div class="kv"><span>Chemistry</span><span>${Math.round(me.fam)}%</span></div>
<button class="btn btn-accent btn-sm mt" onclick="UI.doBootcamp()" ${bootcamping?'disabled':''}>🏕 Bootcamp week ($20K)</button>
${bootcamping?'<p class="tiny good mt">Bootcamp in progress…</p>':''}
</div>
</div>
<div class="card mt"><table class="tbl"><tr><th>Player</th><th>Level</th><th>Focus</th><th>Intensity</th><th>Fatigue</th><th>Status</th></tr>${rowHtml}</table></div>`;
},
doBootcamp(){
const r=Game.bootcamp();
if(r==='nomoney')this.toast('Not enough money.','bad');
else if(r===false)this.toast('Already bootcamping.','warn');
else this.toast('Bootcamp booked — chemistry rising.','good');
this.refreshRoute();
},
/* ================= TRANSFERS ================= */
_marketFilters:{q:'',role:'',maxAge:99,minOvr:0,sortBy:'ovr'},
rTransfers(){
const f=this._marketFilters;
const tab=G.ui.marketTab||'market';
const win=Game.inTransferWindow();
const d=((G.day-1)%364)+1;
let winTxt;
if(d>=150&&d<=230)winTxt=`window closes in ${230-d}d`;
else if(d>=300)winTxt=`winter window · ${365-d}d left`;
else winTxt=`opens in ${150-d}d`;
let body='';
if(tab==='offers'){
body=G.pendingOffers.map((b,i)=>{
const p=Game.p(b.pid),from=Game.team(b.from);
return `<div class="offer-card"><div class="flex spread wrap">
<div><b>${p.nick}</b> <span class="tiny muted">${p.role} · ${playerOverall(p).toFixed(0)} OVR · value ${U.fmtM(playerValue(p,G))}</span><br>
<span class="small">${U.esc(from.name)} offers <b class="gold">${U.fmtM(b.fee)}</b> + ${U.fmtM(b.wage)}/mo wages</span></div>
<div class="flex"><button class="btn btn-good btn-sm" onclick="Game.respondBid(${i},true);UI.toast('Sale completed 💰','good');UI.refreshRoute()">Accept</button>
<button class="btn btn-danger btn-sm" onclick="Game.respondBid(${i},false);UI.refreshRoute()">Reject</button></div></div></div>`;
}).join('')||'<p class="muted">No incoming bids right now.</p>';
}else{
let list=Object.values(G.players).filter(p=>{
if(p.loanFrom)return false;
const isFA=p.teamId===null;
const isListed=p.listedPrice!==null&&p.teamId!==G.playerTeamId;
if(!isFA&&!isListed)return false;
if(f.role&&p.role!==f.role)return false;
if(p.age>f.maxAge)return false;
if(playerOverall(p)<f.minOvr)return false;
if(f.q&&!(p.nick.toLowerCase().includes(f.q.toLowerCase())||p.name.toLowerCase().includes(f.q.toLowerCase())))return false;
return true;
});
const sorters={ovr:(a,b)=>playerOverall(b)-playerOverall(a),val:(a,b)=>playerValue(b,G)-playerValue(a,G),age:(a,b)=>a.age-b.age,pot:(a,b)=>b.potential-a.potential};
list.sort(sorters[f.sortBy]);
body=list.slice(0,60).map(p=>{
const isFA=p.teamId===null;
return `<tr class="clickable" onclick="UI.playerModal('${p.id}')">
<td><span class="ovr ${ovrClass(playerOverall(p))}">${playerOverall(p).toFixed(0)}</span></td>
<td><b>${p.nick}</b> ${FLAG(p.nat)}<br><span class="tiny muted">${U.esc(p.name)}, ${p.age}</span></td>
<td><span class="role-tag">${p.role}</span></td>
<td class="num tiny cy">${p.potential}</td>
<td class="num">${U.fmtM(playerValue(p,G))}</td>
<td class="num">${isFA?'<span class="good">FREE AGENT</span>':U.esc(Game.team(p.teamId)?.tag||'—')}</td>
<td class="num tiny muted">${isFA?U.fmtM(wageDemand(p,G))+'/mo':(p.listedPrice?U.fmtM(p.listedPrice):'—')}</td></tr>`;
}).join('')||'<tr><td colspan="7" class="muted">Nobody matches those filters.</td></tr>';
}
$('view').innerHTML=`<h1 style="font-size:22px">Transfer Center <span class="tiny ${win?'good':'muted'}" style="font-weight:400">· ${win?'🟢 transfer window open':'⚪ closed'} (${winTxt})</span></h1>
<div class="flex mt mb wrap">
<span class="seg">
<button class="${tab==='market'?'on':''}" onclick="G.ui.marketTab='market';UI.refreshRoute()">Market</button>
<button class="${tab==='offers'?'on':''}" onclick="G.ui.marketTab='offers';UI.refreshRoute()">Incoming Bids${G.pendingOffers.length?` <span class="pill">${G.pendingOffers.length}</span>`:''}</button>
</span>
${tab==='market'?`
<input placeholder="Search…" value="${U.esc(f.q)}" oninput="UI._marketFilters.q=this.value;clearTimeout(UI._qT);UI._qT=setTimeout(()=>UI.refreshRoute(),300)">
<select onchange="UI._marketFilters.role=this.value;UI.refreshRoute()">
<option value="">All roles</option>${ROLES.map(r=>`<option ${f.role===r?'selected':''}>${r}</option>`).join('')}</select>
<select onchange="UI._marketFilters.minOvr=parseInt(this.value);UI.refreshRoute()">
${[0,70,75,80,85].map(v=>`<option value="${v}" ${f.minOvr===v?'selected':''}>${v?('OVR ≥ '+v):'Any OVR'}</option>`).join('')}</select>
<select onchange="UI._marketFilters.maxAge=parseInt(this.value);UI.refreshRoute()">
${[[99,'Any age'],[20,'≤20'],[23,'≤23'],[26,'≤26'],[30,'≤30']].map(([v,l])=>`<option value="${v}" ${f.maxAge===v?'selected':''}>${l}</option>`).join('')}</select>
<select onchange="UI._marketFilters.sortBy=this.value;UI.refreshRoute()">
${[['ovr','Sort: OVR'],['pot','Sort: Potential'],['val','Sort: Value'],['age','Sort: Age']].map(([v,l])=>`<option value="${v}" ${f.sortBy===v?'selected':''}>${l}</option>`).join('')}</select>`:''}
</div>
${tab==='market'?`<div class="card"><table class="tbl">
<tr><th>OVR</th><th>Player</th><th>Role</th><th class="num">Pot</th><th class="num">Value</th><th>Club</th><th class="num">Price</th></tr>
${body}</table></div>`:`<div>${body}</div>`}`;
},
/* ================= TOURNAMENTS ================= */
rTours(){
const me=Game.me();
const evs=G.calendar.filter(e=>e.startDay>G.day-40).sort((a,b)=>a.startDay-b.startDay);
const groups={open:[],locked:[],live:[],done:[]};
evs.forEach(e=>groups[e.status]?.push(e));
const card=e=>{
const regd=e.registered,inField=e.teams.includes(me.id);
return `<div class="card" style="border-left:4px solid ${e.major?'var(--gold)':e.tier===1?'var(--accent)':'var(--line)'}">
<div class="flex spread"><h3 style="margin:0">${U.esc(e.name)} ${e.major?'🏆':''}${e.gs?' <span class="cy" title="Grand Slam event">💎</span>':''}</h3>
<span class="tagchip">Tier ${e.tier}</span></div>
<div class="kv"><span>Starts</span><b>${U.fmtDate(e.startDay)}</b></div>
<div class="kv"><span>Winner prize</span><b class="gold">${U.fmtM(e.prize)}</b></div>
<div class="kv"><span>Points</span><b>${e.pts}</b></div>
<div class="kv"><span>Teams</span><b>${e.teams.length||e.slots}</b></div>
${e.status==='open'?(regd?'<p class="good small mt">✓ Registered — field locks soon</p>'
:`<button class="btn btn-accent btn-sm mt" onclick="UI.registerEvent('${e.id}')">Register team</button>`)
:e.status==='live'?`<p class="cy small mt">● LIVE${inField?' — you are competing!':''}</p><button class="btn btn-ghost btn-sm mt" onclick="UI.eventDetail('${e.id}')">View bracket</button>`
:`<p class="muted small mt">Champion: <b>${e.champion?U.esc(Game.team(e.champion).name):'—'}</b></p><button class="btn btn-ghost btn-sm mt" onclick="UI.eventDetail('${e.id}')">Results</button>`}
</div>`;
};
$('view').innerHTML=`<h1 style="font-size:22px">Tournament Circuit</h1>
${G.achievements.grandSlam?'<p class="gold">💎 GRAND SLAM CHAMPION 💎</p>':`<p class="muted small">Win ${GS_REQUIRED} of the 💎 elite events consecutively within a year for the Grand Slam (${U.fmtM(GS_BONUS)} bonus).</p>`}
${groups.live.length?`<h3 class="mt">Happening now</h3><div class="card-grid">${groups.live.map(card).join('')}</div>`:''}
<h3 class="mt">Upcoming & open</h3>
<div class="card-grid">${groups.open.concat(groups.locked).map(card).join('')||'<p class="muted">Nothing open right now.</p>'}</div>
<h3 class="mt">Recently finished</h3><div class="card-grid">${groups.done.slice(-4).reverse().map(card).join('')||'<p class="muted">—</p>'}</div>`;
},
registerEvent(id){
const r=Game.registerEvent(id);
if(r&&!r.ok)this.toast(r.msg,'warn');
else this.toast('Registered ✔','good');
this.refreshRoute();
},
eventDetail(id){
const ev=G.calendar.find(e=>e.id===id);if(!ev)return;
let html=`<button class="close-x" onclick="UI.closeModal()">×</button>
<h2>${U.esc(ev.name)} ${ev.major?'🏆':''}</h2>
<p class="muted small">${U.fmtDate(ev.startDay)} · Tier ${ev.tier} · ${U.fmtM(ev.prize)} winner prize · ${ev.pts} pts</p>`;
if(ev.champion)html+=`<p>🥇 Champion: <b>${U.esc(Game.team(ev.champion).name)}</b></p>`;
html+='<div class="mt">';
ev.rounds.forEach(rnd=>{
html+=`<h3>${rnd.name} <span class="tiny muted">${U.fmtDateShort(rnd.day)}</span></h3>`;
rnd.matches.forEach(m=>{
if(!m.a||!m.b){html+=`<div class="kv muted"><span>TBD vs TBD</span></div>`;return;}
const A=Game.team(m.a),B=Game.team(m.b);
const mine=m.a===G.playerTeamId||m.b===G.playerTeamId;
const winIsA=m.winner===m.a;
html+=`<div class="kv" style="${mine?'font-weight:800;background:rgba(255,122,26,.06)':''}">
<span>${winIsA?'<b>':''}${A.tag}${winIsA?'</b>':''} vs ${m.winner&&!winIsA?'<b>':''}${B.tag}${m.winner&&!winIsA?'</b>':''} ${mine?'⭐':''}</span>
<span>${m.winner?`<b>${m.score}</b> <span class="tiny muted">${(m.maps||[]).map(x=>`${x.map} ${x.score}`).join(' · ')}</span>`:`<span class="cy tiny">${U.fmtDateShort(m.day)} · BO${m.bo}</span>`}</span></div>`;
});
});
html+='</div>';
const pending=Game.findUserFixtureToday();
html+=`<div class="flex mt">${pending&&ev.status==='live'?`<button class="btn btn-accent" onclick="UI.closeModal();UI.openMatchModal(Game.findUserFixtureToday())">▶ Play today's match</button>`:''}
<button class="btn btn-ghost" onclick="UI.closeModal()">Close</button></div>`;
this.modal(html,true);
},
/* ================= MATCHES ================= */
rMatches(){
const me=Game.me();
const past=[];const future=[];
G.calendar.forEach(e=>e.rounds.forEach(r=>r.matches.forEach(m=>{
if(!m.a||!m.b)return;
if(m.a===me.id||m.b===me.id)(m.winner?past:future).push({m,e});
})));
past.sort((a,b)=>b.m.day-a.m.day);
const opp=m=>Game.team(m.a===me.id?m.b:m.a);
$('view').innerHTML=`<h1 style="font-size:22px">Matches</h1>
<div class="card mt"><h3>Upcoming</h3>${
future.sort((a,b)=>a.m.day-b.m.day).map(({m,e})=>`<div class="kv"><span>vs <b>${U.esc(opp(m).name)}</b> <span class="tiny muted">BO${m.bo} · ${U.esc(e.name)}</span></span>
<span class="cy tiny">${U.fmtDate(m.day)}</span></div>`).join('')||'<p class="muted">Register for events to schedule matches.</p>'}</div>
<div class="card mt"><h3>History</h3>
<table class="tbl"><tr><th>Date</th><th>Event</th><th>Opponent</th><th>Result</th><th>Score</th></tr>
${past.map(({m,e})=>{
const won=m.winner===me.id;
return `<tr class="clickable" onclick="UI.boxScoreModal('${e.id}','${m.id}')">
<td class="tiny muted">${U.fmtDateShort(m.day)}</td><td class="tiny">${U.esc(e.name)}</td>
<td>${U.esc(opp(m).tag)}</td><td class="${won?'good':'bad'}">${won?'WIN':'LOSS'}</td><td><b>${m.score}</b></td></tr>`;
}).join('')}</table>
${past.length?'':'<p class="muted">No matches yet.</p>'}</div>`;
},
boxScoreModal(evId,mid){
const ev=G.calendar.find(e=>e.id===evId);if(!ev)return;
let m=null;
for(const r of ev.rounds)for(const x of r.matches)if(x.id===mid)m=x;
if(!m||!m.winner)return;
this.boxScoreInline(m,ev);
},
boxScoreInline(m,ev,opts={}){
const la=MatchEngine.lineupOf(m.a),lb=MatchEngine.lineupOf(m.b);
const tbl=(tid,lineup)=>`<h3>${U.esc(Game.team(tid).name)} ${m.winner===tid?'🏆':''}</h3><table class="tbl">
<tr><th>Player</th><th class="num">K</th><th class="num">D</th><th class="num">ADR</th><th class="num">Rating</th></tr>
${lineup.map(pid=>{const b=m.box[pid];if(!b)return'';const p=Game.p(pid);
const rt=(b.ratingSum||1)/Math.max(b.mapsN||1,1);
return `<tr ${m.mvp===pid?'style="color:var(--gold)"':''}><td>${m.mvp===pid?'🏅 ':''}${p.nick}</td>
<td class="num">${b.k}</td><td class="num">${b.d}</td><td class="num">${Math.round(b.adr/Math.max(b.mapsN||1,1))}</td><td class="num"><b>${rt.toFixed(2)}</b></td></tr>`;
}).join('')}</table>`;
const scoreParts=String(m.score).split('-');
this.modal(`<button class="close-x" onclick="UI.closeModal()">×</button>
<h2>${U.esc(Game.team(m.a).tag)} ${U.esc(scoreParts[0]||'')} : ${U.esc(scoreParts[1]||'')} ${U.esc(Game.team(m.b).tag)}</h2>
<p class="muted small">${ev?U.esc(ev.name||'')+' · ':''}${(m.maps||[]).map(x=>`${x.map} <b>${x.score}</b>`).join(' · ')}</p>
<div class="grid mt" style="grid-template-columns:1fr 1fr">${tbl(m.a,la)}${tbl(m.b,lb)}</div>
<div class="flex mt wrap">
${opts.continueBtn?`<button class="btn btn-primary" onclick="UI.closeModal();UI.continueAfterMatch()">Continue →</button>`:''}
<button class="btn btn-ghost" onclick="UI.closeModal()">Close</button></div>`,true);
},
/* ================= STAFF ================= */
rStaff(){
const me=Game.me();
const ROLES_STAFF=['Coach','Analyst','Psychologist','Physio','Media Manager','Finance Director'];
const cur=ROLES_STAFF.map(role=>{
const s=Game.staffOf(me,role);
return `<div class="card"><h3>${role}</h3>${s?
`<div class="flex spread"><div><b>${s.nick}</b> <span class="muted small">${U.esc(s.name)}</span><br>
<span class="stars">${'★'.repeat(Math.round(s.skill/20))}</span> <span class="muted tiny">${s.skill} skill · ${U.fmtM(s.salary)}/mo</span></div>
<button class="btn btn-danger btn-sm" onclick="UI.fireStaff('${s.id}','${role}')">Fire</button></div>`
:'<p class="muted">Vacant</p>'}</div>`;
}).join('');
const pool=Object.values(G.staff).filter(s=>!s.teamId).sort((a,b)=>b.skill-a.skill);
$('view').innerHTML=`<h1 style="font-size:22px">Backroom Staff</h1>
<p class="muted small">Coaches accelerate training, analysts sharpen prep, psychologists stabilize morale, physios cut fatigue, media managers grow fans, finance directors find savings & better sponsors.</p>
<div class="card-grid mt">${cur}</div>
<div class="card mt"><h3>Available Staff</h3><table class="tbl">
<tr><th>Name</th><th>Role</th><th>Skill</th><th class="num">Salary/mo</th><th></th></tr>
${pool.map(s=>`<tr><td><b>${s.nick}</b> <span class="tiny muted">${U.esc(s.name)}</span></td>
<td>${s.role}</td><td class="stars tiny">${'★'.repeat(Math.round(s.skill/20))} ${s.skill}</td>
<td class="num">${U.fmtM(s.salary)}</td>
<td><button class="btn btn-good btn-sm" onclick="UI.hireStaff('${s.id}')">Hire</button></td></tr>`).join('')||'<tr><td colspan="5" class="muted">Market is empty — check back later.</td></tr>'}</table></div>`;
},
hireStaff(sid){
const s=G.staff[sid];const me=Game.me();
if(G.money<s.salary*2){this.toast('Need at least 2 months of their salary banked.','bad');return;}
me.staff[s.role]=sid;s.teamId=me.id;
this.toast(`${s.nick} joins as your ${s.role}!`,'good');
this.refreshRoute();
},
fireStaff(sid,role){
const s=G.staff[sid];const me=Game.me();
if(me.staff[role]===sid){delete me.staff[role];s.teamId=null;
G.money-=s.salary; // one month severance
this.toast(`${s.nick} dismissed.`,'warn');this.refreshRoute();}
},
/* ================= FINANCE ================= */
rFinance(){
const me=Game.me();
const wagesMo=me.roster.reduce((s,pid)=>s+G.players[pid].salary,0)+Object.values(me.staff).reduce((s,sid)=>s+(G.staff[sid]?.salary||0),0);
const merchMo=Math.round((Game.lastWeeklyMerch||0)*4.33);
const sponsMo=G.sponsors.reduce((s,x)=>s+x.monthly,0);
const ops=me.tier===4?30000:45000;
const net=merchMo+sponsMo-wagesMo-ops;
$('view').innerHTML=`<h1 style="font-size:22px">Finances</h1>
<div class="stat-tiles mt">
<div class="tile"><div class="t-label">Balance</div><div class="t-val ${G.money>=0?'good':'bad'}">${U.fmtM(G.money)}</div></div>
<div class="tile"><div class="t-label">Monthly Net</div><div class="t-val" style="color:${net>=0?'var(--good)':'var(--bad)'}">${net>=0?'+':''}${U.fmtM(net)}</div></div>
<div class="tile"><div class="t-label">Wages+Staff</div><div class="t-val">-${U.fmtM(wagesMo)}</div></div>
<div class="tile"><div class="t-label">Ops Cost</div><div class="t-val">-${U.fmtM(ops)}</div></div>
<div class="tile"><div class="t-label">Merch/Stream</div><div class="t-val good">+${U.fmtM(merchMo)}</div></div>
<div class="tile"><div class="t-label">Sponsors</div><div class="t-val good">+${U.fmtM(sponsMo)}</div></div>
</div>
<div class="card-grid mt">
<div class="card"><h3>Active Partnerships (${G.sponsors.length}/3)</h3>${
G.sponsors.map(s=>`<div class="kv"><span><b>${U.esc(s.brand)}</b> <span class="tiny muted">${U.esc(s.type)}</span></span><span class="good">${U.fmtM(s.monthly)}/mo · ${Math.ceil(Math.max(0,(s.until-G.day))/28)}mo left</span></div>`).join('')||'<p class="muted">None yet.</p>'}</div>
<div class="card"><h3>Pending Offers</h3>${
G.sponsorOffers.map(o=>`<div class="offer-card"><div class="flex spread"><div><b>${U.esc(o.brand)}</b> <span class="tiny muted">${U.esc(o.type)}</span><br><span class="small">${U.fmtM(o.monthly)}/mo × ${o.months} months</span></div>
<div class="flex"><button class="btn btn-good btn-sm" onclick="if(Game.acceptSponsor('${o.id}')){UI.toast('Deal signed ✍','good');}else{UI.toast('No partner slots free.','warn');}UI.refreshRoute()">Sign</button>
<button class="btn btn-danger btn-sm" onclick="Game.rejectSponsor('${o.id}');UI.refreshRoute()">Decline</button></div></div></div>`).join('')||'<p class="muted">New offers arrive periodically based on ranking, rep & fans.</p>'}</div>
</div>`;
},
/* ================= RANKINGS ================= */
rRankings(){
const prevIdx=id=>G.rankingPrev.indexOf(id);
const rows=G.ranking.map((tid,i)=>{
const t=G.teams[tid];
const pi=prevIdx(tid);
const move=pi>=0?pi-i:null;
const arrow=move===null?'<span class="muted tiny">•</span>':move>0?`<span class="good">▲${move}</span>`:move<0?`<span class="bad">▼${-move}</span>`:'<span class="muted tiny"></span>';
return `<tr style="${t.isPlayer?'background:rgba(255,122,26,.08);font-weight:800':''}">
<td>#${i+1}</td><td>${arrow}</td>
<td><span style="display:inline-block;width:10px;height:10px;border-radius:3px;background:${t.color};margin-right:6px"></span>${U.esc(t.name)} ${t.isPlayer?'⭐':''}</td>
<td class="tiny muted">${t.region}</td><td class="num">${Math.round(t.ratingPts)}</td>
<td class="num tiny muted">${t._lastPts?`+${t._lastPts}`:''}</td></tr>`;
}).join('');
const topPlayers=Object.values(G.players).filter(p=>p.seasonStats.maps>=3)
.sort((a,b)=>(b.seasonStats.ratingSum/b.seasonStats.maps)-(a.seasonStats.ratingSum/a.seasonStats.maps)).slice(0,15);
$('view').innerHTML=`<h1 style="font-size:22px">World Rankings</h1>
<div class="card-grid mt">
<div class="card"><table class="tbl"><tr><th>#</th><th></th><th>Team</th><th>Region</th><th class="num">Points</th><th class="num">Last EV</th></tr>${rows}</table></div>
<div class="card"><h3>Player Leaderboard (season rating)</h3><table class="tbl">
<tr><th>#</th><th>Player</th><th>Club</th><th class="num">Maps</th><th class="num">Rating</th></tr>
${topPlayers.map((p,i)=>{
const rt=p.seasonStats.ratingSum/p.seasonStats.maps;
return `<tr><td>${i+1}</td><td>${p.nick} ${FLAG(p.nat)}</td>
<td class="tiny">${U.esc(Game.team(p.teamId)?.tag||'FA')}</td><td class="num">${p.seasonStats.maps}</td><td class="num"><b>${rt.toFixed(2)}</b></td></tr>`;
}).join('')||'<tr><td colspan="5" class="muted">Play some maps!</td></tr>'}</table></div>
</div>
${G.history.length?`<div class="card mt"><h3>Season History</h3><table class="tbl">
<tr><th>Season</th><th class="num">Final rank</th><th class="num">Balance</th><th class="num">Fans</th><th class="num">Trophies</th></tr>
${G.history.map(h=>`<tr><td>S${h.season}</td><td class="num">#${h.rank}</td><td class="num ${h.money>=0?'good':'bad'}">${U.fmtM(h.money)}</td><td class="num">${U.fmtNum(h.fans)}</td><td class="num">${h.trophies||0}</td></tr>`).join('')}
</table></div>`:''}`;
},
/* ================= INBOX ================= */
rInbox(){
const items=G.news.slice(0,60);
$('view').innerHTML=`<h1 style="font-size:22px">Inbox</h1>
<div class="flex mt mb"><button class="btn btn-ghost btn-sm" onclick="G.news.forEach(n=>n.read=true);UI.refreshRoute();UI.updateTopbar()">Mark all read</button></div>
<div class="card">${items.map(n=>newsRow(n,true)).join('')||'<p class="muted">Empty.</p>'}</div>`;
},
/* ================= MATCH MODALS & SIMULATION MODE ================= */
openMatchModal(fx){
window.__fx=fx;
const pv=MatchEngine.preview(fx);
const opp=Game.team(fx.a===G.playerTeamId?fx.b:fx.a);
const evName=fx.evId?G.calendar.find(e=>e.id===fx.evId)?.name:'Exhibition';
const html=`<h2>⚔️ ${U.esc(Game.me().name)} vs ${U.esc(opp.name)}</h2>
<p class="muted small">${U.esc(evName||'')} · BO${fx.bo} · ${U.fmtDate(fx.day)}</p>
<div class="card-grid mt">
<div class="card"><h3>${U.esc(Game.me().tag)} <span class="muted tiny">#${pv.rankA}</span></h3>
<p class="muted small">Avg OVR <b>${pv.sA.toFixed(1)}</b></p>
<div class="wrap mt">${Game.lineupPlayers(Game.me()).map(p=>`<span class="tagchip">${p.nick} ${playerOverall(p).toFixed(0)}${p.injuryDays?' 🏥':''}</span>`).join('')}</div></div>
<div class="card"><h3>${U.esc(opp.tag)} <span class="muted tiny">#${pv.rankB}</span></h3>
<p class="muted small">Avg OVR <b>${pv.sB.toFixed(1)}</b></p>
<div class="wrap mt">${MatchEngine.lineupOf(opp.id).map(pid=>`<span class="tagchip">${Game.p(pid).nick}</span>`).join('')}</div></div>
</div>
<p class="mt small muted">Estimated win chance: <b>${Math.round(pv.pWin*100)}%</b></p>
<div class="flex mt wrap">
<button class="btn btn-accent btn-big" onclick="UI.launchSim()">🎮 Simulation Mode</button>
<button class="btn btn-primary" onclick="UI.quickSim()">⚡ Quick Sim</button>
</div>
<p class="tiny muted mt">Simulation mode: live map veto, buy decisions, round tactics & timeouts. Quick sim lets your coach handle it.</p>`;
this.modal(html,true);
},
quickSim(){
const fx=window.__fx;
const res=MatchEngine.simulate(fx,fx.evId?G.calendar.find(e=>e.id===fx.evId):null);
this.showPostMatch(res);
},
launchSim(){
this.closeModal();
SimSession.start(window.__fx,window.__fx.evId?G.calendar.find(e=>e.id===window.__fx.evId):null);
$('simOverlay').style.display='flex';
this.renderSim();
},
renderSim(){
const S=SimSession.s;if(!S)return;
const w=$('simWrap');
if(S.phase==='veto'||S.phase==='pregame'){w.innerHTML=this.simVetoHtml(S);}
else if(S.phase==='intermission'){w.innerHTML=this.simInterHtml(S);}
else if(S.phase==='done'){w.innerHTML=this.simDoneHtml(S);}
else{w.innerHTML=this.simRoundHtml(S);}
w.querySelectorAll('.sim-feed').forEach(f=>f.scrollTop=f.scrollHeight);
// static minimap preview between rounds (a live anim owns the canvas otherwise)
if(S.phase==='round'&&S.awaiting!=='anim'&&typeof VIZ!=='undefined'&&VIZ.ok&&!VIZ.R){
const c=S.cur;if(!c)return;
const ctIsA=MatchEngine.sideOf(c,'a',c.round);
const roster=c.aLineup.concat(c.bLineup).map(pid=>({pid,nick:Game.p(pid).nick,
side:c.aLineup.includes(pid)?'a':'b',role:Game.p(pid).role,dies:false,order:0,killCount:0}));
try{VIZ.preview(c.map,roster,ctIsA?'a':'b');}catch(e){}
}
},
/* ---------- 🤖 AUTO-SPECTATE: watch the whole series hands-free ---------- */
toggleAuto(){
G.ui.autoSim=!G.ui.autoSim;
this.toast(G.ui.autoSim?'🤖 Auto-spectate ON — enjoy the show':'⏸ Auto-spectate paused');
if(G.ui.autoSim){this.renderSim();this.autoTick();}
},
autoTick(){
if(!G.ui.autoSim)return;
const S=SimSession.s;if(!S){G.ui.autoSim=false;return;}
try{
if(S.phase==='done'){
G.ui.autoSim=false;
}else if(S.phase==='veto'){
if(SimSession.vetoDone())SimSession.beginMap(0);
else if(SimSession.myTurn()){
// ban/pick the best own-affinity map still in the pool
const me=Game.me();
let best=null,bv=-99;
ACTIVE_MAPS.forEach(m=>{
if(!S.veto.pool.has(m))return;
const v=me.mapAff[m]||70;
if(v>bv){bv=v;best=m;}
});
if(best)SimSession.vetoAction(best);
}
}else if(S.phase==='pregame'){
SimSession.beginMap(0);
}else if(S.phase==='intermission'){
SimSession.beginMap(S.results.length);
}else if(S.phase==='round'){
const c=S.cur;
if(S.awaiting==='buy')SimSession.chooseBuy(MatchEngine.autoBuy(c,S.userId));
else if(S.awaiting==='tactic')SimSession.chooseTactic(U.pick(['default','default','default','aggr','patient','fake']));
else if(S.awaiting==='next')SimSession.nextRound();
// awaiting 'anim' → let the animation play, just wait
}
}catch(e){}
// never rebuild the DOM while an animation owns the canvas
if(!(S.phase==='round'&&S.awaiting==='anim'))this.renderSim();
if(G.ui.autoSim)setTimeout(()=>this.autoTick(),460);
},
simVetoHtml(S){
const v=S.veto;
const done=SimSession.vetoDone();
const myTurn=!done&&SimSession.myTurn();
const isBan=S.veto.step<SimSession.totalVetoSteps();
const aiTag=Game.team(SimSession.oppId()).tag;
return `<div class="sim-scorebar"><div class="sim-team">${U.esc(Game.team(S.aId).name)}</div>
<div class="sim-round-info"><div class="rlabel">MAP VETO · BO${S.bo}</div><div class="rscore" style="font-size:20px">🗳</div></div>
<div class="sim-team right">${U.esc(Game.team(S.bId).name)}</div></div>
<div class="sim-body" style="grid-template-columns:1fr">
<div class="sim-panel">
<div class="flex spread" style="align-items:center;margin-bottom:8px">
<span class="tiny muted">MAP VETO · BO${S.bo}</span>
<button class="btn ${G.ui.autoSim?'btn-accent':'btn-ghost'} btn-sm" onclick="UI.toggleAuto()">${G.ui.autoSim?'⏸ AUTO-SPECTATE':'🤖 AUTO-SPECTATE'}</button>
</div>
${done?`<p><b class="acc">Map pool set:</b> ${v.seq.join(' → ')}</p>
<button class="btn btn-accent btn-big" onclick="SimSession.beginMap(0);UI.renderSim()">▶ Start match on ${v.seq[0]}</button>`
:myTurn?`<p><b class="acc">Your move:</b> ${isBan?'🚫 BAN':'✅ PICK'} a map.</p>`
:`<p><b>${aiTag}</b> is deciding…</p>`}
${!done?`<div class="veto-grid">${ACTIVE_MAPS.map(mp=>{
const gone=!v.pool.has(mp);
const aff=Game.me().mapAff[mp]||0;
const picked=v.seq.includes(mp);
return `<div class="map-card ${gone?'dead':''}" ${myTurn&&!gone?`onclick="SimSession.vetoAction('${mp}');UI.renderSim()"`:''}>
<b>${mp}</b><br><span class="tiny ${aff>=0?'good':'bad'}">${aff>=0?'+':''}${aff.toFixed?aff.toFixed(1):aff} aff</span>${picked?'<br><span class="cy tiny">picked</span>':''}</div>`;
}).join('')}</div>`:''}
<div class="sim-feed" style="max-height:170px">${v.log.map(l=>`<p>${l}</p>`).join('')}</div>
</div>
</div>`;
},
simInterHtml(S){
const c=S.cur;
const last=S.results[S.results.length-1];
return `<div class="sim-scorebar"><div class="sim-team">${U.esc(Game.team(S.aId).name)}</div>
<div class="sim-round-info"><div class="rlabel">SERIES</div><div class="rscore">${S.scoreA}-${S.scoreB}</div></div>
<div class="sim-team right">${U.esc(Game.team(S.bId).name)}</div></div>
<div class="sim-body" style="grid-template-columns:1fr">
<div class="sim-panel"><h3>${last.map} — final score</h3>
<p style="font-size:34px;font-weight:900" class="${last.winner===G.playerTeamId?'good':'bad'}">${last.sa}-${last.sb}</p>
<div class="sim-feed" style="max-height:240px">${c.log.map(l=>l.startsWith('🔫')?`<p class="kf">${l}</p>`:`<p>${l}</p>`).join('')}</div>
<button class="btn btn-accent btn-big mt" onclick="SimSession.beginMap(SimSession.s.results.length);UI.renderSim()">Next map: ${S.veto.seq[S.results.length]||''} →</button></div>
</div>`;
},
simDoneHtml(S){
const win=S.scoreA>S.scoreB?S.aId:S.bId;
const iWon=win===G.playerTeamId;
const mvpP=S.m.mvp?Game.p(S.m.mvp):null;
return `<div class="sim-scorebar"><div class="sim-team">${U.esc(Game.team(S.aId).name)}</div>
<div class="sim-round-info"><div class="rlabel">FINAL</div><div class="rscore">${S.scoreA}-${S.scoreB}</div></div>
<div class="sim-team right">${U.esc(Game.team(S.bId).name)}</div></div>
<div class="sim-body" style="grid-template-columns:1fr">
<div class="sim-panel"><h2>${iWon?'🏆 VICTORY!':'💀 DEFEAT'}</h2>
<p>MVP: <b class="gold">${mvpP?`${mvpP.nick}`:'—'}</b></p>
${S.results.map(r=>`<div class="kv"><span>${r.map}</span><b>${r.sa}-${r.sb}</b></div>`).join('')}
<div class="flex mt">
<button class="btn btn-accent btn-big" onclick="$('simOverlay').style.display='none';UI.showBoxFromSim()">📊 Full report</button>
</div></div></div>`;
},
showBoxFromSim(){
const S=SimSession.s;
if(S&&S.m&&S.m.winner)this.boxScoreInline(S.m,S.ev,{continueBtn:true});
},
simRoundHtml(S){
const c=S.cur,k=S.userId;
const opT=Game.team(k==='a'?S.bId:S.aId);
const myScore=k==='a'?c.sa:c.sb,opScore=k==='a'?c.sb:c.sa;
const moPos=50+(k==='a'?c.momentum:-c.momentum)*5;
const buyOpts=SimSession.buyOptions();
let controls='';
if(S.awaiting==='buy'){
controls=`<div class="eco-cash"><span>Your team funds</span><b>$${U.fmtNum(c.fund[k])}</b></div>
<div class="buy-grid">${buyOpts.map(o=>`<button class="choice" onclick="SimSession.chooseBuy('${o.id}');UI.renderSim()"><b>${o.name}</b><small>${o.desc}</small></button>`).join('')}</div>
${c.timeouts[k]>0?`<button class="btn btn-ghost btn-sm mt" onclick="if(SimSession.callTimeout())UI.renderSim()">🎙 Tactical timeout (${c.timeouts[k]} left)</button>`:''}
<p class="tiny muted mt">Tip: eco when under 6.8K, force with 6.811.8K. Mix tactics — smart opponents read patterns.</p>`;
}else if(S.awaiting==='tactic'){
controls=`<p class="small">Buy locked: <b class="acc">${String(S.userBuy).toUpperCase()}</b>. Choose your approach:</p>
<div class="tac-grid">${TACTICS.map(t=>`<button class="choice" onclick="SimSession.chooseTactic('${t.id}')"><b>${t.name}</b><small>${t.desc}</small></button>`).join('')}</div>`;
}else if(S.awaiting==='anim'){
controls=`<div class="flex spread mt"><span class="bad tiny">🔴 LIVE</span>
<button class="btn btn-ghost btn-sm" onclick="VIZ.skip()">⏩ Skip animation</button></div>`;
}else if(S.awaiting==='next'){
controls=`<button class="btn btn-accent btn-big" onclick="SimSession.nextRound();UI.renderSim()">Next round →</button>
<button class="btn btn-ghost mt" onclick="SimSession.simRest();UI.renderSim()">⏭ Simulate rest of match</button>`;
}
const ctIsA=MatchEngine.sideOf(c,'a',c.round);
const lineupChips=(k==='a'?c.aLineup:c.bLineup).map(pid=>{
const p=Game.p(pid);const b=c.box[pid];
return `<span class="tagchip">${p.nick} <b class="tiny num">${b?`${b.k}/${b.d}`:'0/0'}</b>${p.injuryDays?' 🏥':''}</span>`;
}).join('');
return `<div class="sim-round">
<div class="sim-scorebar">
<div class="sim-team"><span style="display:inline-block;width:12px;height:12px;border-radius:3px;background:${Game.team(S.aId).color}"></span>${U.esc(Game.team(S.aId).name)}</div>
<div class="sim-round-info"><div class="rlabel">${U.esc(c.map)} · Round ${c.round+1} · MR12</div>
<div class="rscore"><span style="color:${c.sa>c.sb?'#fff':'var(--muted)'}">${c.sa}</span> : <span style="color:${c.sb>c.sa?'#fff':'var(--muted)'}">${c.sb}</span></div>
<div class="momentum"><i style="left:${U.clamp(moPos,4,92)}%"></i></div>
<div class="tiny muted mt">You: ${myScore}${opT.tag}: ${opScore} · OT target ${c.winScore}</div></div>
<div class="sim-team right">${U.esc(Game.team(S.bId).name)}<span style="display:inline-block;width:12px;height:12px;border-radius:3px;background:${Game.team(S.bId).color}"></span></div></div>
<div class="sim-map-line">${S.veto.seq.map((mp,i)=>
{const r=S.results[i];
return `<span style="${i===S.curIdx?'color:#fff;font-weight:800':''};text-decoration:${r?'line-through':'none'}">${mp}${r?` <b>${r.sa}-${r.sb}</b>`:''}</span>`;}).join(' · ')}</div>
<div class="sim-stage">
<canvas id="simCanvas" width="1280" height="640"></canvas>
<div class="stage-note tiny">${ctIsA?'Your side: <b>CT</b> — hold the sites':'Your side: <b>T</b> — execute onto a site'}</div>
</div>
<div class="sim-dock">
<div class="sim-panel">
<div class="flex spread wrap" style="align-items:center;gap:10px">
<span class="seg">
<button onclick="VIZ.setCam('follow')" title="Follow camera">🎥</button>
<button onclick="VIZ.setCam('radar')" title="Full-map radar">🗺</button>
<button onclick="UI.toast('Speed x'+VIZ.cycleSpeed())" title="Cycle speed">⏩<span id="spdLbl">${VIZ.speed}</span></button>
<button onclick="VIZ.sndOn=!VIZ.sndOn;UI.toast(VIZ.sndOn?'🔊 Sound on':'🔇 Muted')" title="Sound"><span id="sndLbl">${VIZ.sndOn?'🔊':'🔇'}</span></button>
</span>
<button class="btn ${G.ui.autoSim?'btn-accent':'btn-ghost'} btn-sm" onclick="UI.toggleAuto()">${G.ui.autoSim?'⏸ AUTO-SPECTATE':'🤖 AUTO-SPECTATE'}</button>
</div>
<h3 class="mt-0" style="margin-top:8px">${S.awaiting==='next'||S.awaiting==='anim'?'Round over':'Command the round'}</h3>${controls}
<div class="wrap mt" style="border-top:1px solid var(--line);padding-top:8px">${lineupChips}
<span class="tiny muted">🎙 ${c.timeouts[k]} timeouts left</span></div>
</div>
<div class="sim-feed dock-feed">${c.log.map(l=>l.startsWith('🔫')?`<p class="kf">${l}</p>`:`<p>${l}</p>`).join('')}</div>
</div></div>`;
},
/* ---------- post match (quick sim) ---------- */
showPostMatch(res){
const iWon=res.winner===G.playerTeamId;
const mvp=res.mvp?Game.p(res.mvp):null;
const b=res.box[res.mvp];
const html=`<h2>${iWon?'🏆 VICTORY':'💀 DEFEAT'}${U.esc(Game.team(res.winner).name)} win ${res.score}</h2>
<p class="muted small">${res.maps.map(x=>`${x.map} <b>${x.score}</b>`).join(' · ')}${res.note||''}</p>
<p class="mt">MVP: <b class="gold">${mvp?`${mvp.nick} (${(b.ratingSum/b.mapsN).toFixed(2)})`:'—'}</b></p>
<div class="flex mt wrap"><button class="btn btn-ghost" onclick="UI.closeModal();UI.boxFromQuick();">📊 Box score</button>
<button class="btn btn-primary" onclick="UI.continueAfterMatch()">Continue →</button></div>`;
this.modal(html,true);
},
boxFromQuick(){
const fx=window.__fx;
if(fx&&fx.winner)this.boxScoreInline(fx,fx.evId?G.calendar.find(e=>e.id===fx.evId):null,true);
},
/* ---------- save menu ---------- */
saveModal(){
const html=`<button class="close-x" onclick="UI.closeModal()">×</button>
<h2>Career Menu</h2>
<div class="grid mt" style="gap:10px">
<button class="btn btn-primary" onclick="Save.save(false);UI.toast('Game saved.','good');UI.closeModal()">💾 Save game</button>
<button class="btn btn-ghost" onclick="Main.exportSave()">📤 Export save to file</button>
<button class="btn btn-ghost" onclick="Main.importSave()">📥 Import save from file</button>
<button class="btn btn-danger" onclick="if(confirm('Quit to title? Progress stays in autosave.')){UI.closeModal();Main.toTitle()}">🏠 Quit to title</button>
</div>
<p class="tiny muted mt">Autosave runs every in-game week. Saves live in your browser's localStorage.</p>`;
this.modal(html);
},
/* ---------- new game wizard ---------- */
showNewGame(){
$('titleScreen').style.display='none';
$('newGameScreen').style.display='block';
Game.newWorld(document.querySelector('input[name=difficulty]:checked')?.value||'normal');
const list=$('teamPickList');
list.innerHTML=Object.values(G.teams).filter(t=>t.tier<=2).sort((a,b)=>b.tier-a.tier).map(t=>{
const best=Game.teamPlayers(t).sort((a,b)=>playerOverall(b)-playerOverall(a)).slice(0,3);
return `<div class="team-pick" onclick="UI.pickTeam('${t.id}')">
<div class="tp-name" style="color:${t.color}">${U.esc(t.name)}</div>
<div class="tp-meta"><span>${t.region} · Tier ${t.tier}</span><span>Avg ${(Game.teamPlayers(t).reduce((s,p)=>s+playerOverall(p),0)/5).toFixed(0)} OVR</span></div>
<div class="tiny muted mt">${best.map(p=>p.nick).join(' · ')}</div></div>`;
}).join('');
},
pickTeam(teamId){
const diff=document.querySelector('input[name=difficulty]:checked')?.value||'normal';
Game.newWorld(diff);
Game.startWithTeam(teamId);
this.enterGame();
},
enterGame(){
$('newGameScreen').style.display='none';
$('titleScreen').style.display='none';
this.show();
Save.save(true);
this.toast('Career started. Good luck, GM! 🎙️','good');
},
};
function newsRow(n,full){
const ACT={bid:['🔁 Review bids',"G.ui.marketTab='offers';UI.go('transfers')"],
renew:null,sponsor:['💼 Finances',"UI.go('finance')"],event:['🏆 Tournaments',"UI.go('tournaments')"]};
let act='';
if(full&&n.action){
if(n.action.kind==='renew'&&n.action.pid){
act=`<div class="mt"><button class="btn btn-accent btn-sm" onclick="UI.negotiateModal('${n.action.pid}','renew')">🖊 Renew now</button></div>`;
}else if(ACT[n.action.kind]){
act=`<div class="mt"><button class="btn btn-ghost btn-sm" onclick="${ACT[n.action.kind][1]}">${ACT[n.action.kind][0]}</button></div>`;
}
}
return `<div class="news-item ${n.read?'':'unread'}" onclick="G.news.find(x=>x.id===${n.id}).read=true;UI.updateTopbar()">
<span class="n-date">${U.fmtDateShort(n.day)}</span>
<div><div class="n-title">${U.esc(n.title)}</div>
${full?`<div class="small muted">${n.body}</div>`:''}
${act}
<span class="tiny muted">${n.tag}</span></div></div>`;
}