Files
counterstrikemanager/js/ui.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

996 lines
65 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>`;
}