'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=`
${U.esc(me.name)}${me.tag} ยท #${Game.rankOf(me.id)||'โ'} WORLD
`;
$('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=`${html}
`;
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=`Club Dashboard
Balance
${U.fmtM(G.money)}
Reputation
${Math.round(G.rep)}
Board
${Math.round(G.board)}%
Chemistry
${Math.round(me.fam)}%
๐ฏ Season ${G.season} Objectives
World ranking #${obj.rank} ยท target TOP ${obj.targetRank}
Finances ${obj.moneyOk?'solvent โ๏ธ':'deficit โ ๏ธ'}
Board confidence ${obj.board}%
`;
html+=`
${liveEv?'๐ด LIVE โ '+U.esc(liveEv.name):'๐
Upcoming'} `;
if(liveEv){
html+=this.bracketMini(liveEv)+
`
Open bracket `;
}else{
const nxt=G.calendar.find(e=>e.registered&&e.status!=='done'&&e.startDay>G.day);
html+=nxt?`
${U.esc(nxt.name)} starts ${U.fmtDate(nxt.startDay)} .
Tier ${nxt.tier} ยท ${U.fmtM(nxt.prize)} to the winner ยท ${nxt.slots} teams${nxt.major?' ยท MAJOR ๐':''}
`
:`
No event registered.
Browse Tournaments โ `;
html+=`
Calendar ahead `+upc.map(e=>
`
${U.esc(e.name)}${e.registered?' โ ':''} ${U.fmtDateShort(e.startDay)} ยท T${e.tier}
`).join('');
}
html+=`
`;
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+=`
๐ Trophy Cabinet ${
G.achievements.grandSlam?'
๐ GRAND SLAM CHAMPION
':''}${
trophies.length?trophies.map(([k,v])=>`
๐ฅ ${U.esc(v.ev)} ${U.fmtDateShort(v.day)}
`).join('')
:'
Empty shelf. The first trophy tastes the sweetest.
'
}
โณ Expiring Contracts ${
expiring.map(p=>{const left=p.contractUntil-G.day;
return `
${p.nick} ${playerOverall(p).toFixed(0)} ${left}d
`;}).join('')
||'
All contracts secure โ
'
}
`;
html+=`
๐
Career Milestones ${Game.MILESTONES.map(m=>{
const got=G.milestones.includes(m.id);
return `
${got?'โ
':'๐'} ${m.name} ${got?'unlocked':U.esc(m.desc)}
`;
}).join('')}
`;
html+=`
Squad Snapshot Player Form Morale Fatigue ${
sorted.slice(0,6).map(p=>`
${playerOverall(p).toFixed(0)}
${p.nick} ${FLAG(p.nat)}${p.role}
${formStars(p.form)}
${faceFor(p.morale)}
`).join('')
}
`;
html+=`
Recent Results ${
recent.slice(0,6).map(({m,e})=>{
const won=m.winner===me.id;
return `
${won?'โ
':'โ'} vs ${Game.team(m.a===me.id?m.b:m.a).name} ${m.score} ${U.esc(e.name)}
`;
}).join('')||'
No matches played yet.
'
}
`;
html+=`
Latest News ${
G.news.slice(0,6).map(newsRow).join('')}`;
$('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+=`
${Game.team(m.a).tag} vs ${Game.team(m.b).tag}${mine?' โญ':''}
${m.winner?`${m.score} ${rnd.name} `:`${U.fmtDateShort(m.day)} `}
`;
});
});
return html||'Bracket formingโฆ
';
},
/* ================= 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 `
${playerOverall(p).toFixed(0)}
${starter}${p.nick} ${FLAG(p.nat)}${p.listedPrice!==null?' ๐':''}${U.esc(p.name)}, ${p.age}
${p.role}
${formStars(p.form)}
${faceFor(p.morale)} ${Math.round(p.morale)}
${p.injuryDays?`๐ฅ${p.injuryDays}d `:''}
${U.fmtM(p.salary)}
${U.fmtM(playerValue(p,G))}
${left<=0?'EXPIRED':left+'d'} `;
}).join('');
$('view').innerHTML=`Squad Management
Starting Lineup
${me.lineup.map((pid,i)=>{const p=Game.p(pid);return `${i+1}. ${p.nick} ${playerOverall(p).toFixed(0)}${p.injuryDays?' ๐ฅ':''} `;}).join('')}
โ Auto-pick best
๐ Scout prospects (${U.fmtM(Game.SCOUT_COST)})
OVR Player Role Form Morale Fatigue Wage/mo Value Contract
${rows}
Roster cap: 8 players (${ps.length}/8).
`;
},
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)=>``;
const html=`ร
${ovr.toFixed(0)}
${p.nick} ${FLAG(p.nat)} ${U.esc(p.name)} ยท ${p.age}y ยท ${p.role}${p.potential>ovr+4&&mine?` ยท POT ${p.potential} `:''}
Attributes ${attrs.map(([l,vv])=>bar(l,vv)).join('')}
Form ${formStars(p.form)} ยท Morale ${faceFor(p.morale)} ${Math.round(p.morale)} ยท Fatigue ${Math.round(p.fatigue)}%${p.injuryDays?` ยท Injured ${p.injuryDays}d `:''}
Professionalism ${p.personality.prof} ยท Temperament ${p.personality.temper} ยท Ambition ${p.personality.ambition}
Season Stats
Rating
${r?r.toFixed(2):'โ'}
K/D
${s.maps&&s.d?(s.k/s.d).toFixed(2):'โ'}
ADR
${s.maps?Math.round(s.adr):'โ'}
Contract ${mine?(left>0?left+' days left':`expired ${-left}d ago`):U.fmtDate(p.contractUntil)}
Value โ ${U.fmtM(playerValue(p,G))}
${mine?`
๐ฌ Talk
${inLineup
?`Bench `
:`Add to Lineup `}
๐ Renew Contract
${p.listedPrice!==null?`Unlist `
:`๐ Transfer List `}
${!inLineup&&Game.me().roster.length>5?`๐ค Loan Out `:''}
Release
`:
`
${p.teamId?`๐ฐ Make Transfer Offer `
:`โ Sign Free Agent `}
Wage demand โ ${U.fmtM(wageDemand(p,G))}/mo${p.listedPrice!==null?` ยท asking ${U.fmtM(p.listedPrice)}`:''}
`}`;
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=`Transfer List โ ${p.nick} Market value: ${U.fmtM(val)}. Listed players attract bids from other clubs (see Inbox / Transfers โ Bids).
Asking price
Confirm listing
Cancel
`;
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 `
${FLAG(p.nat)} ${p.nick} ${p.age}y ยท ${p.role} ยท ${NATIONS[p.nat]||p.nat}
${ovr.toFixed(0)} POT ${p.potential}โ
Sign (3y)
${attrs.map(([l,v])=>`${l} ${v.toFixed(0)}`).join(' ยท ')}
`;
};
this.modal(`๐ Scouting Camp โ $80K
Three unattached youngsters caught our scouts' eyes. Sign one โ the others return to obscurity. Potential scales with club reputation.
${r.list.map(card).join('')}
Walk away
`);
},
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=`Development Loan โ ${p.nick}
Loaning a young player out for 90 days earns a small fee and accelerates his growth through playing time. He returns automatically afterwards.
${clubs.map(t=>`
${U.esc(t.name)} Tier ${t.tier} ยท #${Game.rankOf(t.id)} ยท roster ${t.roster.length}/8
Loan
`).join('')}
Cancel
`;
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=`ร
Talk to ${p.nick}
Morale ${faceFor(p.morale)} ${Math.round(p.morale)} ยท Motivation ${Math.round(p.motivation)} ยท Form ${formStars(p.form)}. One meaningful talk per ~10 days.
Topic ${
topics.map(([id,l])=>`${l} `).join('')
}
Tone ${tones.map((t,i)=>`${t[0].toUpperCase()+t.slice(1)} `).join('')}
Have the conversation
Not now
`;
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='Pick a topic first.
';return;}
const res=Game.applyTalk(pid,topic,tone);
$('talkResult').innerHTML=`${U.esc(res.msg)}
${res.dM!==undefined?`
Morale ${res.dM>=0?'+':''}${res.dM} ยท Motivation ${res.dMo>=0?'+':''}${res.dMo}
`:''}
Done `;
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'?`Transfer fee (value โ ${U.fmtM(playerValue(p,G))}) `:'';
const html=`ร
${title}
His camp wants โ ${U.fmtM(demand)} /mo. Current club: ${p.teamId?U.esc(Game.team(p.teamId).name):'free agent'} ยท morale ${faceFor(p.morale)} ยท OVR ${playerOverall(p).toFixed(0)}
${feeField}
Monthly wage
Years
Signing bonus
Submit offer
Cancel
`;
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=`โ ${U.esc(res.msg)}
Close
`;
};
},
/* ================= 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=>`${f} `).join('');
const intSel=[[0,'Light'],[1,'Normal'],[2,'Hard']].map(([v,l])=>`${l} `).join('');
return `
${p.nick} ${p.role}
${playerOverall(p).toFixed(0)} โ ${p.potential}
${focusSel}
${intSel}
${p.injuryDays?`๐ฅ ${p.injuryDays}d `:'โ '} `;
}).join('');
const bootcamping=(G.flags.bootcampUntil||0)>G.day;
$('view').innerHTML=`Training Center
Weekly Plan 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.
Support Effects
Coach ${coach?`${coach.nick} (+${Math.round(coach.skill-55)}% growth)`:'vacant '}
Physio ${physio?`${physio.nick} (faster recovery)`:'vacant '}
Chemistry ${Math.round(me.fam)}%
๐ Bootcamp week ($20K)
${bootcamping?'
Bootcamp in progressโฆ
':''}
Player Level Focus Intensity Fatigue Status ${rowHtml}
`;
},
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 `
${p.nick} ${p.role} ยท ${playerOverall(p).toFixed(0)} OVR ยท value ${U.fmtM(playerValue(p,G))}
${U.esc(from.name)} offers ${U.fmtM(b.fee)} + ${U.fmtM(b.wage)}/mo wages
Accept
Reject
`;
}).join('')||'No incoming bids right now.
';
}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)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 `
${playerOverall(p).toFixed(0)}
${p.nick} ${FLAG(p.nat)}${U.esc(p.name)}, ${p.age}
${p.role}
${p.potential}
${U.fmtM(playerValue(p,G))}
${isFA?'FREE AGENT ':U.esc(Game.team(p.teamId)?.tag||'โ')}
${isFA?U.fmtM(wageDemand(p,G))+'/mo':(p.listedPrice?U.fmtM(p.listedPrice):'โ')} `;
}).join('')||'Nobody matches those filters. ';
}
$('view').innerHTML=`Transfer Center ยท ${win?'๐ข transfer window open':'โช closed'} (${winTxt})
Market
Incoming Bids${G.pendingOffers.length?` ${G.pendingOffers.length} `:''}
${tab==='market'?`
All roles ${ROLES.map(r=>`${r} `).join('')}
${[0,70,75,80,85].map(v=>`${v?('OVR โฅ '+v):'Any OVR'} `).join('')}
${[[99,'Any age'],[20,'โค20'],[23,'โค23'],[26,'โค26'],[30,'โค30']].map(([v,l])=>`${l} `).join('')}
${[['ovr','Sort: OVR'],['pot','Sort: Potential'],['val','Sort: Value'],['age','Sort: Age']].map(([v,l])=>`${l} `).join('')} `:''}
${tab==='market'?`
OVR Player Role Pot Value Club Price
${body}
`:`${body}
`}`;
},
/* ================= 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 `
${U.esc(e.name)} ${e.major?'๐':''}${e.gs?' ๐ ':''}
Tier ${e.tier}
Starts ${U.fmtDate(e.startDay)}
Winner prize ${U.fmtM(e.prize)}
Points ${e.pts}
Teams ${e.teams.length||e.slots}
${e.status==='open'?(regd?'
โ Registered โ field locks soon
'
:`
Register team `)
:e.status==='live'?`
โ LIVE${inField?' โ you are competing!':''}
View bracket `
:`
Champion: ${e.champion?U.esc(Game.team(e.champion).name):'โ'}
Results `}
`;
};
$('view').innerHTML=`Tournament Circuit
${G.achievements.grandSlam?'๐ GRAND SLAM CHAMPION ๐
':`Win ${GS_REQUIRED} of the ๐ elite events consecutively within a year for the Grand Slam (${U.fmtM(GS_BONUS)} bonus).
`}
${groups.live.length?`Happening now ${groups.live.map(card).join('')}
`:''}
Upcoming & open
${groups.open.concat(groups.locked).map(card).join('')||'
Nothing open right now.
'}
Recently finished ${groups.done.slice(-4).reverse().map(card).join('')||'
โ
'}
`;
},
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=`ร
${U.esc(ev.name)} ${ev.major?'๐':''}
${U.fmtDate(ev.startDay)} ยท Tier ${ev.tier} ยท ${U.fmtM(ev.prize)} winner prize ยท ${ev.pts} pts
`;
if(ev.champion)html+=`๐ฅ Champion: ${U.esc(Game.team(ev.champion).name)}
`;
html+='';
ev.rounds.forEach(rnd=>{
html+=`
${rnd.name} ${U.fmtDateShort(rnd.day)} `;
rnd.matches.forEach(m=>{
if(!m.a||!m.b){html+=`
TBD vs TBD
`;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+=`
${winIsA?'':''}${A.tag}${winIsA?' ':''} vs ${m.winner&&!winIsA?'':''}${B.tag}${m.winner&&!winIsA?' ':''} ${mine?'โญ':''}
${m.winner?`${m.score} ${(m.maps||[]).map(x=>`${x.map} ${x.score}`).join(' ยท ')} `:`${U.fmtDateShort(m.day)} ยท BO${m.bo} `}
`;
});
});
html+='
';
const pending=Game.findUserFixtureToday();
html+=`${pending&&ev.status==='live'?`โถ Play today's match `:''}
Close
`;
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=`Matches
Upcoming ${
future.sort((a,b)=>a.m.day-b.m.day).map(({m,e})=>`
vs ${U.esc(opp(m).name)} BO${m.bo} ยท ${U.esc(e.name)}
${U.fmtDate(m.day)}
`).join('')||'
Register for events to schedule matches.
'}
History
Date Event Opponent Result Score
${past.map(({m,e})=>{
const won=m.winner===me.id;
return `
${U.fmtDateShort(m.day)} ${U.esc(e.name)}
${U.esc(opp(m).tag)} ${won?'WIN':'LOSS'} ${m.score} `;
}).join('')}
${past.length?'':'
No matches yet.
'}
`;
},
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)=>`${U.esc(Game.team(tid).name)} ${m.winner===tid?'๐':''}
Player K D ADR Rating
${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 `${m.mvp===pid?'๐
':''}${p.nick}
${b.k} ${b.d} ${Math.round(b.adr/Math.max(b.mapsN||1,1))} ${rt.toFixed(2)} `;
}).join('')}
`;
const scoreParts=String(m.score).split('-');
this.modal(`ร
${U.esc(Game.team(m.a).tag)} ${U.esc(scoreParts[0]||'')} : ${U.esc(scoreParts[1]||'')} ${U.esc(Game.team(m.b).tag)}
${ev?U.esc(ev.name||'')+' ยท ':''}${(m.maps||[]).map(x=>`${x.map} ${x.score} `).join(' ยท ')}
${tbl(m.a,la)}${tbl(m.b,lb)}
${opts.continueBtn?`Continue โ `:''}
Close
`,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 `${role} ${s?
`
${s.nick} ${U.esc(s.name)}
${'โ
'.repeat(Math.round(s.skill/20))} ${s.skill} skill ยท ${U.fmtM(s.salary)}/mo
Fire `
:'
Vacant
'}
`;
}).join('');
const pool=Object.values(G.staff).filter(s=>!s.teamId).sort((a,b)=>b.skill-a.skill);
$('view').innerHTML=`Backroom Staff
Coaches accelerate training, analysts sharpen prep, psychologists stabilize morale, physios cut fatigue, media managers grow fans, finance directors find savings & better sponsors.
${cur}
Available Staff
Name Role Skill Salary/mo
${pool.map(s=>`${s.nick} ${U.esc(s.name)}
${s.role} ${'โ
'.repeat(Math.round(s.skill/20))} ${s.skill}
${U.fmtM(s.salary)}
Hire `).join('')||'Market is empty โ check back later. '}
`;
},
hireStaff(sid){
const s=G.staff[sid];const me=Game.me();
if(G.moneys+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=`Finances
Balance
${U.fmtM(G.money)}
Monthly Net
${net>=0?'+':''}${U.fmtM(net)}
Wages+Staff
-${U.fmtM(wagesMo)}
Merch/Stream
+${U.fmtM(merchMo)}
Sponsors
+${U.fmtM(sponsMo)}
Active Partnerships (${G.sponsors.length}/3) ${
G.sponsors.map(s=>`
${U.esc(s.brand)} ${U.esc(s.type)} ${U.fmtM(s.monthly)}/mo ยท ${Math.ceil(Math.max(0,(s.until-G.day))/28)}mo left
`).join('')||'
None yet.
'}
Pending Offers ${
G.sponsorOffers.map(o=>`
${U.esc(o.brand)} ${U.esc(o.type)} ${U.fmtM(o.monthly)}/mo ร ${o.months} months
Sign
Decline
`).join('')||'
New offers arrive periodically based on ranking, rep & fans.
'}
`;
},
/* ================= 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?'โข ':move>0?`โฒ${move} `:move<0?`โผ${-move} `:'โ ';
return `
#${i+1} ${arrow}
${U.esc(t.name)} ${t.isPlayer?'โญ':''}
${t.region} ${Math.round(t.ratingPts)}
${t._lastPts?`+${t._lastPts}`:''} `;
}).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=`World Rankings
# Team Region Points Last EV ${rows}
Player Leaderboard (season rating)
# Player Club Maps Rating
${topPlayers.map((p,i)=>{
const rt=p.seasonStats.ratingSum/p.seasonStats.maps;
return `${i+1} ${p.nick} ${FLAG(p.nat)}
${U.esc(Game.team(p.teamId)?.tag||'FA')} ${p.seasonStats.maps} ${rt.toFixed(2)} `;
}).join('')||'Play some maps! '}
${G.history.length?`Season History
Season Final rank Balance Fans Trophies
${G.history.map(h=>`S${h.season} #${h.rank} ${U.fmtM(h.money)} ${U.fmtNum(h.fans)} ${h.trophies||0} `).join('')}
`:''}`;
},
/* ================= INBOX ================= */
rInbox(){
const items=G.news.slice(0,60);
$('view').innerHTML=`Inbox
Mark all read
${items.map(n=>newsRow(n,true)).join('')||'
Empty.
'}
`;
},
/* ================= 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=`โ๏ธ ${U.esc(Game.me().name)} vs ${U.esc(opp.name)}
${U.esc(evName||'')} ยท BO${fx.bo} ยท ${U.fmtDate(fx.day)}
${U.esc(Game.me().tag)} #${pv.rankA}
Avg OVR ${pv.sA.toFixed(1)}
${Game.lineupPlayers(Game.me()).map(p=>`${p.nick} ${playerOverall(p).toFixed(0)}${p.injuryDays?' ๐ฅ':''} `).join('')}
${U.esc(opp.tag)} #${pv.rankB}
Avg OVR ${pv.sB.toFixed(1)}
${MatchEngine.lineupOf(opp.id).map(pid=>`${Game.p(pid).nick} `).join('')}
Estimated win chance: ${Math.round(pv.pWin*100)}%
๐ฎ Simulation Mode
โก Quick Sim
Simulation mode: live map veto, buy decisions, round tactics & timeouts. Quick sim lets your coach handle it.
`;
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${U.esc(Game.team(S.aId).name)}
MAP VETO ยท BO${S.bo}
๐ณ
${U.esc(Game.team(S.bId).name)}
MAP VETO ยท BO${S.bo}
${G.ui.autoSim?'โธ AUTO-SPECTATE':'๐ค AUTO-SPECTATE'}
${done?`
Map pool set: ${v.seq.join(' โ ')}
โถ Start match on ${v.seq[0]} `
:myTurn?`
Your move: ${isBan?'๐ซ BAN':'โ
PICK'} a map.
`
:`
${aiTag} is decidingโฆ
`}
${!done?`
${ACTIVE_MAPS.map(mp=>{
const gone=!v.pool.has(mp);
const aff=Game.me().mapAff[mp]||0;
const picked=v.seq.includes(mp);
return `
${mp} ${aff>=0?'+':''}${aff.toFixed?aff.toFixed(1):aff} aff ${picked?'picked ':''}
`;
}).join('')}
`:''}
${v.log.map(l=>`
${l}
`).join('')}
`;
},
simInterHtml(S){
const c=S.cur;
const last=S.results[S.results.length-1];
return `${U.esc(Game.team(S.aId).name)}
SERIES
${S.scoreA}-${S.scoreB}
${U.esc(Game.team(S.bId).name)}
${last.map} โ final score
${last.sa}-${last.sb}
${c.log.map(l=>l.startsWith('๐ซ')?`
${l}
`:`
${l}
`).join('')}
Next map: ${S.veto.seq[S.results.length]||''} โ
`;
},
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 `${U.esc(Game.team(S.aId).name)}
FINAL
${S.scoreA}-${S.scoreB}
${U.esc(Game.team(S.bId).name)}
${iWon?'๐ VICTORY!':'๐ DEFEAT'}
MVP: ${mvpP?`${mvpP.nick}`:'โ'}
${S.results.map(r=>`
${r.map} ${r.sa}-${r.sb}
`).join('')}
๐ Full report
`;
},
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=`Your team funds $${U.fmtNum(c.fund[k])}
${buyOpts.map(o=>`${o.name} ${o.desc} `).join('')}
${c.timeouts[k]>0?`๐ Tactical timeout (${c.timeouts[k]} left) `:''}
Tip: eco when under 6.8K, force with 6.8โ11.8K. Mix tactics โ smart opponents read patterns.
`;
}else if(S.awaiting==='tactic'){
controls=`Buy locked: ${String(S.userBuy).toUpperCase()} . Choose your approach:
${TACTICS.map(t=>`${t.name} ${t.desc} `).join('')}
`;
}else if(S.awaiting==='anim'){
controls=`๐ด LIVE
โฉ Skip animation
`;
}else if(S.awaiting==='next'){
controls=`Next round โ
โญ Simulate rest of match `;
}
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 `${p.nick} ${b?`${b.k}/${b.d}`:'0/0'} ${p.injuryDays?' ๐ฅ':''} `;
}).join('');
return `
${U.esc(Game.team(S.aId).name)}
${U.esc(c.map)} ยท Round ${c.round+1} ยท MR12
${c.sa} : ${c.sb}
You: ${myScore} โ ${opT.tag}: ${opScore} ยท OT target ${c.winScore}
${U.esc(Game.team(S.bId).name)}
${S.veto.seq.map((mp,i)=>
{const r=S.results[i];
return `${mp}${r?` ${r.sa}-${r.sb} `:''} `;}).join(' ยท ')}
${ctIsA?'Your side: CT โ hold the sites':'Your side: T โ execute onto a site'}
๐ฅ
๐บ
โฉ${VIZ.speed}
${VIZ.sndOn?'๐':'๐'}
${G.ui.autoSim?'โธ AUTO-SPECTATE':'๐ค AUTO-SPECTATE'}
${S.awaiting==='next'||S.awaiting==='anim'?'Round over':'Command the round'} ${controls}
${lineupChips}
๐ ${c.timeouts[k]} timeouts left
${c.log.map(l=>l.startsWith('๐ซ')?`
${l}
`:`
${l}
`).join('')}
`;
},
/* ---------- 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=`${iWon?'๐ VICTORY':'๐ DEFEAT'} โ ${U.esc(Game.team(res.winner).name)} win ${res.score}
${res.maps.map(x=>`${x.map} ${x.score} `).join(' ยท ')}${res.note||''}
MVP: ${mvp?`${mvp.nick} (${(b.ratingSum/b.mapsN).toFixed(2)})`:'โ'}
๐ Box score
Continue โ
`;
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=`ร
Career Menu
๐พ Save game
๐ค Export save to file
๐ฅ Import save from file
๐ Quit to title
Autosave runs every in-game week. Saves live in your browser's localStorage.
`;
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 `
${U.esc(t.name)}
${t.region} ยท Tier ${t.tier} Avg ${(Game.teamPlayers(t).reduce((s,p)=>s+playerOverall(p),0)/5).toFixed(0)} OVR
${best.map(p=>p.nick).join(' ยท ')}
`;
}).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=`๐ Renew now
`;
}else if(ACT[n.action.kind]){
act=`${ACT[n.action.kind][0]}
`;
}
}
return `
${U.fmtDateShort(n.day)}
${U.esc(n.title)}
${full?`
${n.body}
`:''}
${act}
${n.tag} `;
}