'use strict'; /* ================= core game engine ================= */ const SAVE_KEY='csm26_save_v1'; let G=null; const DIFFS={ easy: {label:'Rookie',wageMul:0.85,aiAggro:0.6,startMul:1.6,boardStrict:0.7}, normal:{label:'Pro', wageMul:1.0, aiAggro:1.0,startMul:1.0,boardStrict:1.0}, hard: {label:'Legend',wageMul:1.15,aiAggro:1.5,startMul:0.65,boardStrict:1.4}, }; const TRAINING_FOCUS=['Aim','Utility','Tactics','Teamwork','Movement','Rest','Media']; const FOCUS_ATTR={Aim:'aim',Utility:'util',Tactics:'iq',Teamwork:'comm',Movement:'move'}; const PRIZE_FR=[1,.5,.3,.3,.15,.15,.15,.15,.06]; const PTS_FR=[1,.6,.38,.38,.24,.24,.24,.24,.14]; const Game={ diff:null, /* ---------- world creation ---------- */ newWorld(diffKey){ G={ver:1,day:0,diffKey,diff:DIFFS[diffKey], money:0,fans:5000,rep:35,board:62, players:{},teams:{},staff:{},freeAgents:[], calendar:[],news:[],nextNewsId:1, sponsors:[],sponsorOffers:[], ranking:[],rankingPrev:[], achievements:{},grandSlamWins:[], season:1, history:[], pendingOffers:[], // incoming offers for user players milestones:[],scoutPool:[], flags:{}, ui:{}, }; // backfill for older saves if(!G.milestones)G.milestones=[]; if(!G.scoutPool)G.scoutPool=[]; // teams TEAMS_DATA.forEach((t,i)=>{ const id='t'+i; G.teams[id]={id,name:t.name,tag:t.tag,color:t.color,tier:t.tier,region:t.region, roster:[],lineup:[],staff:{},ratingPts:[1200,900,700][t.tier-1]+U.ri(-80,80), fam:U.ri(50,95),mapAff:{},isPlayer:false}; t.roster.forEach(pt=>{ const p=makePlayerFromTuple(pt,id,G); p.salary=this._aiSalary(p); G.players[p.id]=p; G.teams[id].roster.push(p.id); }); this.genMapAff(G.teams[id]); }); // free agents FREE_AGENTS.forEach(t=>{ const p=makePlayerFromTuple(t,null,G); p.salary=0; p.contractUntil=0; G.players[p.id]=p; G.freeAgents.push(p.id); }); // staff pool (unemployed) STAFF_POOL.forEach(t=>{ const sid='s'+U.uid(); G.staff[sid]={id:sid,nick:t[0],name:t[1],role:t[2],skill:t[3],salary:t[4],teamId:null}; }); // assign some staff to AI teams (flavor) const poolIds=Object.keys(G.staff); Object.values(G.teams).forEach(t=>{ if(U.chance(.8)){const s=G.staff[U.pick(poolIds)]; if(!s.teamId){s.teamId=t.id; t.staff.Coach=s.id;} } if(U.chance(.6)){const s=G.staff[U.pick(poolIds)]; if(!s.teamId){s.teamId=t.id; t.staff.Analyst=s.id;} } }); // calendar CAL_2026.forEach((e,i)=>{ G.calendar.push({id:'e'+i,...e,startDay:e.day,status:'open',teams:[],champion:null, rounds:[],registered:false,userOut:false}); }); // initial lineup = best five by overall Object.values(G.teams).forEach(t=>this.autoLineup(t)); }, genMapAff(team){ ACTIVE_MAPS.forEach(m=>{ team.mapAff[m]=+(U.gauss(0,3)).toFixed(1); }); }, _aiSalary(p){ return Math.round(wageDemand(p,G)*0.95/250)*250; }, startWithTeam(teamId){ const t=G.teams[teamId]; t.isPlayer=true; G.playerTeamId=teamId; G.money=Math.round([700000,1050000,1700000][t.tier-1]*G.diff.startMul); this.pushNews('welcome',`Welcome to ${t.name}!`, `You are the new General Manager of ${t.name}. The board expects progress. Build the roster, plan training, win trophies β€” your way.`,'org'); this.pushNews('objective','Season objectives', `Finish inside the TOP ${(t.tier===1?'8':t.tier===2?'14':'20')} of the world ranking this season and keep the club financially healthy.`,'board'); this.computeRanking(); }, startCustom(){ const name=document.getElementById('cName').value.trim()||'Northern Lights'; const tag=(document.getElementById('cTag').value.trim()||'NLHT').toUpperCase(); const region=document.getElementById('cRegion').value; const budgetSel=document.getElementById('cBudget').value; G.money=budgetSel==='small'?250000:budgetSel==='mid'?600000:1500000; const id='tcust'; const color='#'+['ff7a1a','38bdf8','34d399','a78bfa','f43f5e'][U.ri(0,4)]; G.teams[id]={id,name,tag,color,tier:4,region,roster:[],lineup:[],staff:{}, ratingPts:420,fam:20,mapAff:{},isPlayer:true}; G.playerTeamId=id; // draft 5 prospects const roles=['IGL','AWP','Entry','Support','Rifler']; roles.forEach(r=>{ const nick=U.pick(NICK_SYL1)+U.pick(NICK_SYL2); const p={ id:U.uid(),nick,name:U.pick(FIRST_NAMES)+' '+U.pick(LAST_NAMES),age:U.ri(17,21), nat:U.pick(Object.keys(NATIONS)),role:r, attrs:Object.fromEntries(['aim','pos','clutch','util','iq','move','comm'].map(k=>[k,U.ri(48,64)])), potential:U.ri(82,96), form:5,morale:75,motivation:75,fatigue:8,injuryDays:0,teamId:id, contractUntil:G.day+700,salary:U.ri(800,1800),listedPrice:null,loanTo:null, personality:{prof:U.ri(40,85),temper:U.ri(30,80),ambition:U.ri(60,95)}, stats:this.zeroStats(),seasonStats:this.zeroStats(), training:{focus:'Balanced',intensity:1},talkCooldown:0,promise:null}; G.players[p.id]=p; G.teams[id].roster.push(p.id); }); this.genMapAff(G.teams[id]); this.autoLineup(G.teams[id]); this.pushNews('welcome',`A new era begins β€” ${name}`, `You founded ${name} (${tag}) in the ${region} region. Five hungry prospects signed their first contracts. Prove the doubters wrong.`,'org'); this.pushNews('objective','Season objectives', 'Turn a squad of semi-pros into a ranked organization. Climb into the world TOP 30 and stay solvent.','board'); this.computeRanking(); if(typeof UI!=='undefined'&&UI.enterGame)UI.enterGame(); }, zeroStats(){return {maps:0,kills:0,deaths:0,adr:0,ratingSum:0,mvp:0,aces:0};}, /* ---------- helpers ---------- */ me(){return G.teams[G.playerTeamId];}, team(id){return G.teams[id];}, p(pid){return G.players[pid];}, teamPlayers(t){return t.roster.map(id=>G.players[id]);}, lineupPlayers(t){return t.lineup.map(id=>G.players[id]).filter(Boolean);}, autoLineup(t){ const avail=t.roster.filter(pid=>!G.players[pid].injuryDays && !G.players[pid].loanFrom); avail.sort((a,b)=>playerOverall(G.players[b])-playerOverall(G.players[a])); t.lineup=avail.slice(0,Math.min(5,avail.length)); if(t.lineup.length<5){ // fill with injured if desperate t.roster.filter(pid=>!t.lineup.includes(pid)&&!G.players[pid].loanFrom).slice(0,5-t.lineup.length) .forEach(pid=>t.lineup.push(pid)); } }, staffOf(t,role){return t.staff[role]?G.staff[t.staff[role]]:null;}, teamStrength(t,opt={}){ const ps=this.lineupPlayers(t); if(!ps.length)return 40; let base=U.avg(ps.map(playerOverall)); const formAdj=U.avg(ps.map(p=>(p.form-5.5)*0.8)); const morAdj=U.avg(ps.map(p=>(p.morale-60)*0.018)); const fatAdj=-Math.max(0,U.avg(ps.map(p=>p.fatigue))-55)*0.05; const chem=(t.fam-60)*0.02; let coachBonus=0,analystBonus=0; const c=this.staffOf(t,'Coach'); if(c)coachBonus=(c.skill-55)*0.035; const a=this.staffOf(t,'Analyst'); if(a)analystBonus=(a.skill-55)*0.02; return U.clamp(base+formAdj+morAdj+fatAdj+chem+coachBonus+analystBonus,30,99); }, /* season objectives for the user's club */ objectives(){ const t=this.me(); const targetRank=t.tier===1?8:t.tier===2?14:30; const rank=this.rankOf(t.id)||99; return { rank,targetRank, rankOk:rank<=targetRank, moneyOk:G.money>-100000, board:Math.round(G.board), }; }, /* ---------- rankings ---------- */ computeRanking(){ G.rankingPrev=G.ranking.slice(); G.ranking=Object.values(G.teams).sort((a,b)=>b.ratingPts-a.ratingPts).map(t=>t.id); }, rankOf(teamId){return G.ranking.indexOf(teamId)+1;}, /* ---------- news ---------- */ pushNews(type,title,body,tag,opts={}){ const n={id:G.nextNewsId++,day:G.day,type,title,body,tag:tag||'',read:false,action:opts.action||null}; G.news.unshift(n); if(G.news.length>140)G.news.length=140; return n; }, /* ---------- time ---------- */ advanceWeek(){ this._advDays=7; return this._advanceLoop(); }, advanceDay(){this._advDays=1;return this._advanceLoop();}, _advanceLoop(){ this._stopFx=null; while(this._advDays>0){ const fx=this.advanceOneDay(); this._advDays--; if(fx){this._stopFx=fx;break;} } Save.save(true); return this._stopFx; }, remainingAdvance(){return this._advDays|0;}, resumeAdvance(){ if(this._advDays<=0){this._advDays=0;return null;} return this._advanceLoop(); }, advanceOneDay(){ G.day++; const dow=U.dow(G.day); /* daily */ Object.values(G.players).forEach(p=>{ if(p.injuryDays>0){p.injuryDays--; if(p.injuryDays===0)this.pushNews('medical',`${p.nick} is back in training`,`${p.nick} has recovered and is available for selection again.`,'medical');} if(p.talkCooldown>0)p.talkCooldown--; p.fatigue=U.clamp(p.fatigue-(p.training.focus==='Rest'?4.5:2)+ (p.training.intensity===2&&!p.injuryDays?1.5:0),0,100); if(p.loanDaysLeft!==undefined&&p.loanDaysLeft>0){ p.loanDaysLeft--; if(p.loanDaysLeft===0)this.endLoan(p); } }); /* squads must always field five β€” academy promotions cover gaps */ Object.values(G.teams).forEach(t=>this.ensureSquad(t)); /* career milestones */ this.checkMilestones(); /* incoming transfer bids expire */ for(let i=G.pendingOffers.length-1;i>=0;i--){ const b=G.pendingOffers[i]; if(G.day>b.expires){ G.pendingOffers.splice(i,1); this.pushNews('offer','Bid expired',`${G.teams[b.from]?.name||'A club'} withdrew their ${U.fmtM(b.fee)} offer for ${this.p(b.pid)?.nick||'a player'}.`,'offer'); } } /* events */ G.calendar.forEach(ev=>this.tickEvent(ev)); /* weekly */ if(dow===0){this.weeklyTick();} if(G.day%28===0){this.monthlyTick();} /* find user fixture due today */ const fx=this.findUserFixtureToday(); if(fx)return fx; return null; }, genYouth(teamId,opts={}){ const nick=U.pick(NICK_SYL1)+U.pick(NICK_SYL2); const roles=['Rifler','Entry','Support','AWP','IGL','Lurker']; const p={ id:U.uid(),nick,name:U.pick(FIRST_NAMES)+' '+U.pick(LAST_NAMES), age:opts.age||U.ri(16,19), nat:U.pick(Object.keys(NATIONS)),role:U.pick(roles), attrs:Object.fromEntries(['aim','pos','clutch','util','iq','move','comm'].map(k=>[k,U.ri(44,62)])), potential:opts.pot||U.ri(72,93), form:4.5,morale:70,motivation:75,fatigue:6,injuryDays:0,teamId, contractUntil:G.day+730,salary:Math.round(U.rnd(600,1500)/100)*100, listedPrice:null,scouted:!!opts.scouted, personality:{prof:U.ri(40,90),temper:U.ri(25,85),ambition:U.ri(55,95)}, stats:this.zeroStats(),seasonStats:this.zeroStats(), training:{focus:'Balanced',intensity:1},talkCooldown:0,promise:null}; G.players[p.id]=p; return p; }, /* ---------- youth scouting ---------- */ SCOUT_COST:80000, scoutYouth(){ if(G.moneyp.id); return{ok:true,list}; }, signProspect(pid){ const p=G.players[pid]; if(!p||!(G.scoutPool||[]).includes(pid))return{ok:false,msg:'Prospect not available.'}; const me=this.me(); if(me.roster.length>=8)return{ok:false,msg:'Roster is full (8 players max).'}; (G.scoutPool||[]).forEach(x=>{if(x!==pid)delete G.players[x];}); G.scoutPool=[]; p.teamId=me.id;p.contractUntil=G.day+1095; p.salary=Math.round(wageDemand(p,G)*0.45/100)*100; me.roster.push(p.id); this.pushNews('transfer',`πŸŽ“ Academy signing: ${p.nick}`, `${p.nick} (${p.age}, ${p.role}, POT ${p.potential}) signs a three-year academy deal at ${U.fmtM(p.salary)}/mo.`,'transfer'); return{ok:true}; }, /* ---------- career milestones ---------- */ MILESTONES:[ {id:'first_win',name:'First Blood',desc:'Win your first official match', chk:()=>G.resultsRecent.some(x=>x===1)}, {id:'first_trophy',name:'Silverware',desc:'Lift your first trophy', chk:()=>Object.keys(G.achievements).filter(k=>k.startsWith('trophy_')).length>=1}, {id:'three_cups',name:'Dynasty Begins',desc:'Win three trophies', chk:()=>Object.keys(G.achievements).filter(k=>k.startsWith('trophy_')).length>=3}, {id:'top10',name:'Elite Company',desc:'Reach the world TOP 10', chk:()=>Game.rankOf(G.playerTeamId)<=10&&Game.rankOf(G.playerTeamId)>0}, {id:'top1',name:'Kings of the World',desc:'Become world #1', chk:()=>Game.rankOf(G.playerTeamId)===1}, {id:'millionaire',name:'Seven Figures',desc:'Hold $1,000,000 in the bank', chk:()=>G.money>=1000000}, {id:'star_signing',name:'Blockbuster',desc:'Field an 88+ rated superstar', chk:()=>Game.teamPlayers(Game.me()).some(p=>playerOverall(p)>=88)}, {id:'academy_star',name:'Academy Graduate',desc:'Develop a scouted youth to 80+ overall', chk:()=>Game.teamPlayers(Game.me()).some(p=>p.scouted&&playerOverall(p)>=80)}, {id:'fans_500k',name:'Global Brand',desc:'Reach 500,000 fans', chk:()=>G.fans>=500000}, {id:'grand_slam',name:'Grand Slam Champion',desc:'Win 4 elite events within one year', chk:()=>!!G.achievements.grandSlam}, ], checkMilestones(){ this.MILESTONES.forEach(m=>{ if(G.milestones.includes(m.id))return; let ok=false;try{ok=m.chk();}catch(e){} if(ok){ G.milestones.push(m.id); this.pushNews('milestone',`πŸ† Milestone unlocked: ${m.name}`,`${m.desc}. The organization grows ever stronger.`,'milestone'); } }); }, ensureSquad(t){ if(!t)return; let added=false; let guard=0; while(t.roster.length<5&&guard++<6){ const p=this.genYouth(t.id);t.roster.push(p.id);added=true; if(t.isPlayer)this.pushNews('transfer',`Academy graduate: ${p.nick}`, `${t.name} promote ${p.age}-year-old ${p.nick} (${p.role}, POT ${p.potential}) from the academy to fill the roster.`,'transfer'); } const invalid=t.lineup.length<5||t.lineup.some(pid=>{const p=G.players[pid];return !p||p.teamId!==t.id||p.injuryDays>0&&!t.roster.some(x=>x!==pid&&G.players[x].injuryDays===0);}); if(invalid)this.autoLineup(t); }, findUserFixtureToday(){ const me=this.me(); 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.day===G.day&&(m.a===me.id||m.b===me.id)&&!m.winner&&m.a&&m.b)return m; } } return null; }, weeklyTick(){ this.trainingWeekly(); this.moraleDrift(); this.contractCheck(); this.aiTransferWindow(); this.financesWeekly(); this.fansWeekly(); // rating decay Object.values(G.teams).forEach(t=>{t.ratingPts=Math.max(150,t.ratingPts*0.996);}); this.computeRanking(); this.boardCheck(); if(G.day>=364*G.season){this.newSeason();} }, monthlyTick(){ // salary payments happen weekly; monthly: sponsors pay let inc=0; G.sponsors.forEach(s=>inc+=s.monthly); G.money+=inc; if(inc>0)this.pushNews('finance',`Sponsor payments received`,`Partners paid out ${U.fmtM(inc)} this month.`,'finance'); // rent/ops cost const ops=this.me().tier===4?30000:45000; G.money-=ops; // sponsor offers quarterly-ish if(G.day%84===28)this.genSponsorOffers(); // AI squads occasionally shuffle lineups Object.values(G.teams).forEach(t=>{if(!t.isPlayer&&U.chance(.25))this.autoLineup(t);}); }, /* ---------- training & development ---------- */ trainingWeekly(){ const me=this.me(); const coach=this.staffOf(me,'Coach'),physio=this.staffOf(me,'Physio'); const gains=[]; me.roster.forEach(pid=>{ const p=G.players[pid]; if(p.injuryDays>0)return; const f=p.training.focus, inten=f==='Rest'?0:p.training.intensity; if(f==='Rest'){ p.fatigue=U.clamp(p.fatigue-14-(physio?(physio.skill-60)*0.08:0),0,100); p.morale=U.clamp(p.morale+2,0,100); p.form=U.clamp(p.form+0.2,0,10); return; } if(f==='Media'){ G.fans+=U.ri(300,1400)*(this.staffOf(me,'Media Manager')?1.5:1)|0; p.morale=U.clamp(p.morale+1,0,100); p.fatigue=U.clamp(p.fatigue-3,0,100); return; } const attr=FOCUS_ATTR[f]; const gap=p.potential-playerOverall(p); const youth=p.age<=21?1.5:p.age<=24?1.25:p.age<=27?1.0:p.age<=30?0.6:0.3; const coachFac=coach?0.7+coach.skill/130:0.55; const gainP=U.clamp(gap*0.010,0,0.4)*youth*coachFac*(inten===2?1.5:inten===1?1:0.6); if(attr&&U.chance(gainP)){ const before=p.attrs[attr]; p.attrs[attr]=U.clamp(p.attrs[attr]+U.rnd(0.4,1.1)+(gap>25?0.5:0),30,99); if(gains.length<5&&p.attrs[attr]>before+0.35){ gains.push(`${p.nick} +${(p.attrs[attr]-before).toFixed(1)} ${f.toLowerCase()}`); } } // all attrs tiny drift with hard work for youngsters if(p.age<=23&&U.chance(0.05)){const k=U.pick(Object.keys(p.attrs));p.attrs[k]=U.clamp(p.attrs[k]+0.3,30,99);} p.form=U.clamp(p.form+(inten===2?0.15:-0.05)+U.gauss(0,0.3),0.5,10); p.fatigue=U.clamp(p.fatigue+inten*7-(physio?(physio.skill-60)*0.06:0),0,100); if(f==='Teamwork'){me.fam=U.clamp(me.fam+1.2,0,100);} // injury risk if(p.fatigue>86&&U.chance(0.10)){ p.injuryDays=U.ri(7,21); p.fatigue=60; this.pushNews('medical',`${p.nick} injured`,`${p.nick} overstressed ${attr==='aim'?'aim routines':'in practice'} and will be out ~${p.injuryDays} days (overuse injury).`,'medical'); } }); me.fam=U.clamp(me.fam+1,0,100); if(gains.length){ this.pushNews('training','πŸ“ˆ Weekly training report', `Notable gains this week: ${gains.join(' Β· ')}. Consistent focus + a quality coach compounds over a season.`,'training'); } }, setTraining(pid,focus,intensity){ const p=this.p(pid); if(focus!==undefined)p.training.focus=focus; if(intensity!==undefined)p.training.intensity=intensity; }, bootcamp(){ const me=this.me(); if(G.flags.bootcampUntil>G.day)return false; if(G.money<20000)return 'nomoney'; G.money-=20000; G.flags.bootcampUntil=G.day+7; me.fam=U.clamp(me.fam+8,0,100); me.lineup.forEach(pid=>{const p=this.p(pid);p.morale=U.clamp(p.morale+4,0,100);p.attrs.iq=U.clamp(p.attrs.iq+U.rnd(0,0.8),30,99);}); this.pushNews('training','Bootcamp complete','A week of intense practice together improved team chemistry and tactical understanding.',null); return true; }, /* ---------- morale / psychology ---------- */ moraleDrift(){ const psy=this.staffOf(this.me(),'Psychologist'); Object.values(G.players).forEach(p=>{ if(p.teamId===null)return; let base=55+(p.personality.prof-50)*0.1; const t=G.teams[p.teamId]; const playing=t.lineup.includes(p.id); if(!playing)base-=12; else base+=4; if(p.promise){ base+=p.promise.broken?-15:8; if(G.day>p.promise.until){ if(p.promise.broken){p.morale=U.clamp(p.morale-18,0,100); this.pushNews('talk',`${p.nick} is frustrated`,`${p.nick} feels the promise made to him was broken. Morale dropped heavily.`,'talk'); } p.promise=null; } } if(p.salary>0){ const fair=wageDemand(p,G); if(p.salary0)return {ok:false,msg:`${p.nick} needs space β€” try again in ${p.talkCooldown} days.`}; p.talkCooldown=10; const prof=p.personality.prof, temper=p.personality.temper, amb=p.personality.ambition; let dM=0,dMo=0,txt='',ok=true; const toneF={supportive:0,demanding:1,neutral:0.5}[tone]; switch(topic){ case 'praise': ok=p.form>=4.5; txt= ok?`${p.nick} appreciated the recognition.`:`${p.nick} knows his form is poor β€” empty praise annoyed him.`; dM= ok?6+toneF*2:-4; break; case 'motivate': dMo=(tone==='demanding'? (temper>60? -8: 10):(amb>60?8:4)); ok=dMo>0; txt= ok?`${p.nick} looks fired up for the upcoming matches.`:`${p.nick} reacted badly to pressure. Motivation dropped.`; break; case 'role': ok=U.chance(0.5+prof/200); txt= ok?`${p.nick} accepts his current role in the lineup.`:`${p.nick} wants more importance in the team. Handle with care.`; dM= ok?4:-8; break; case 'discipline': dM=-6-toneF*6; dMo=tone==='demanding'?6:-2; txt=`You had a serious word about professionalism. ${temper>70?'It got tense β€” he pushed back.':'He took it professionally.'}`; if(temper<=70)p.personality.prof=U.clamp(prof+2,0,99); break; case 'promise': return this.makePromise(p); case 'contract': txt=`${p.nick} wants to be paid like a top player if he performs like one. His agent noted your interest.`; dM=2; break; } p.morale=U.clamp(p.morale+dM,0,100); p.motivation=U.clamp(p.motivation+dMo,0,100); if(dM>0)p.relationship=(p.relationship||50)+3; if(dM<0)p.relationship=(p.relationship||50)-5; return {ok,msg:txt,dM,dMo}; }, makePromise(p){ if(p.promise){return {ok:false,msg:`You already have an open promise with ${p.nick}.`};} p.promise={type:'playtime',broken:false,until:G.day+56}; p.morale=U.clamp(p.morale+10,0,100); p.motivation=U.clamp(p.motivation+12,0,100); this.pushNews('talk',`Promise made to ${p.nick}`,`You promised ${p.nick} a starter spot for the next 8 weeks. Break it and face the consequences.`,'talk'); return {ok:true,msg:`Promise made: starter for 8 weeks. ${p.nick} is motivated β€” don't betray him.`}; }, checkPromisesAfterLineup(){ this.me().roster.forEach(pid=>{ const p=this.p(pid); if(p.promise&&!p.promise.broken){ const playing=this.me().lineup.includes(pid); if(!playing){p.promise.broken=true;} } }); }, /* ---------- contracts ---------- */ contractCheck(){ const warn=d=>d===60||d===30||d===7; Object.values(G.players).forEach(p=>{ if(p.teamId===null||p.loanFrom)return; const left=p.contractUntil-G.day; const t=G.teams[p.teamId]; if(warn(left)&&t.isPlayer){ this.pushNews('contract',`${p.nick}'s contract expires in ${left} days`, `${p.nick} (${playerOverall(p).toFixed(0)} OVR, wage demand ~${U.fmtM(wageDemand(p,G))}/mo) will become a FREE AGENT. Renew now or risk losing him for nothing.`,'contract',{action:{kind:'renew',pid:p.id}}); }else if(warn(left)&&!t.isPlayer&&U.chance(.5)){ // AI renews most p.contractUntil=G.day+U.ri(365,900); p.salary=this._aiSalary(p); } if(left<=0){ if(t.isPlayer){ G.freeAgents.push(p.id); p.teamId=null; p.salary=0; p.contractUntil=0; t.roster=t.roster.filter(x=>x!==p.id); t.lineup=t.lineup.filter(x=>x!==p.id); this.pushNews('transfer',`${p.nick} left the club`, `${p.nick}'s contract expired and he signed nowhere yet β€” he is now a free agent. You receive nothing.`,'transfer'); }else{ if(U.chance(.72)){p.contractUntil=G.day+U.ri(365,900);} else{ t.roster=t.roster.filter(x=>x!==p.id); t.lineup=t.lineup.filter(x=>x!==p.id); G.freeAgents.push(p.id); p.teamId=null;p.salary=0;p.contractUntil=0; if(U.chance(.4))this.pushNews('transfer',`${p.nick} is a free agent`,`Contract expired β€” ${p.nick} hit the open market.`,'transfer'); } } } }); }, renewContract(pid,salary,years,bonus=0){ const p=this.p(pid), me=this.me(); const demand=wageDemand(p,G); const ratio=salary/Math.max(demand,1); let acc=U.logistic((ratio-1)*6+(bonus>0?0.4:0)+(p.morale-60)/150+(p.age>29?-0.3:0.1)); if(years>3&&p.age>28)acc-=0.15; if(ratio<0.75)acc*=0.25; if(G.money=1?8:3),0,100); this.pushNews('contract',`${p.nick} signs a new deal`, `${p.nick} extended for ${years} year${years>1?'s':''} at ${U.fmtM(salary)}/mo${bonus?` (+${U.fmtM(bonus)} signing bonus)`:''}.`,'contract'); return {ok:true,msg:`${p.nick} put pen to paper! πŸ–ŠοΈ`}; } p.morale=U.clamp(p.morale-3,0,100); const counter=Math.round(demand*1.1/500)*500; return {ok:false,msg:`${p.nick}'s camp rejected the offer. They want ~${U.fmtM(counter)}/mo.`}; }, releasePlayer(pid){ const p=this.p(pid),me=this.me(); const comp=p.salary*Math.max(1,(p.contractUntil-G.day)/30)*0.5; if(G.moneyx!==pid); me.lineup=me.lineup.filter(x=>x!==pid); p.teamId=null;p.salary=0;p.contractUntil=0;G.freeAgents.push(pid); this.checkPromisesAfterLineup(); this.pushNews('transfer',`${p.nick} released`,`Contract terminated. Severance paid: ${U.fmtM(comp)}.`,'transfer'); return {ok:true}; }, /* ---------- transfers ---------- */ listPlayer(pid,price){this.p(pid).listedPrice=price;}, unlistPlayer(pid){this.p(pid).listedPrice=null;}, signFreeAgent(pid,salary,years,bonus=0){ const p=this.p(pid),me=this.me(); if(me.roster.length>=8)return {ok:false,msg:'Roster is full (max 8). Sell or release someone first.'}; const demand=wageDemand(p,G); const ratio=salary/Math.max(demand,1); let acc=U.logistic((ratio-1)*5+(p.morale-50)/200); const rank=this.rankOf(me.id)||60; acc+=U.clamp((60-rank)*0.004,-0.15,0.2); if(ratio<0.8)acc*=0.3; if(G.moneyx!==pid); me.roster.push(pid); me.fam=U.clamp(me.fam-6,0,100); p.morale=U.clamp(p.morale+5,0,100); this.pushNews('transfer',`${p.nick} joins ${me.name}!`, `Free agent signing: ${playerOverall(p).toFixed(0)} OVR ${p.role} on a ${years}-year deal at ${U.fmtM(salary)}/mo.`,'transfer'); return {ok:true,msg:`Welcome to the club, ${p.nick}! πŸŽ‰`}; } return {ok:false,msg:`${p.nick} declined β€” he expects around ${U.fmtM(Math.round(demand/500)*500)}/mo${rank>25?' (and doubts our project…)':'.'}`}; }, makeTransferOffer(pid,fee,salary,years){ const p=this.p(pid),me=this.me(); const seller=G.teams[p.teamId]; if(!seller||seller.isPlayer)return {ok:false,msg:'Invalid target.'}; if(me.roster.length>=8)return {ok:false,msg:'Roster is full (max 8).'}; const val=playerValue(p,G); let acc=U.logistic((fee/Math.max(val,1)-1.15)*5); const wdemand=wageDemand(p,G); acc+=U.logistic((salary/wdemand-1)*4)*0.3-0.15; const myRank=this.rankOf(me.id)||60, theirRank=this.rankOf(seller.id)||60; if(theirRankx!==pid); seller.lineup=seller.lineup.filter(x=>x!==pid); this.autoLineup(seller); seller.fam=U.clamp(seller.fam-15,0,100); p.teamId=me.id;p.salary=salary;p.contractUntil=G.day+years*364;p.listedPrice=null; me.roster.push(pid); me.fam=U.clamp(me.fam-8,0,100); this.pushNews('transfer',`${p.nick} transferred to ${me.name}`, `Fee: ${U.fmtM(fee)} β€’ Wage ${U.fmtM(salary)}/mo β€’ ${years}y. The scene reacts to the blockbuster move.`,'transfer'); return {ok:true,msg:`Transfer complete! ${p.nick} is yours. πŸ’°`}; } const ask=Math.round(val*(theirRank0)return {ok:false,msg:`${p.nick} is injured and cannot be loaned out.`}; if(me.roster.length-1<5)return {ok:false,msg:'You must keep at least 5 players on the roster.'}; if(p.loanFrom)return {ok:false,msg:'Player is already on loan.'}; p.loanFrom=me.id; p.loanDaysLeft=90; p.teamId=targetId; me.roster=me.roster.filter(x=>x!==pid); me.lineup=me.lineup.filter(x=>x!==pid); target.roster.push(pid); this.autoLineup(target); const fee=Math.round(playerValue(p,G)*0.04/1000)*1000; G.money+=fee; this.pushNews('transfer',`${p.nick} loaned to ${target.name}`,`Development loan (90 days) for ${U.fmtM(fee)}. He'll get playing time there.`,'transfer'); return {ok:true}; }, endLoan(p){ const cur=G.teams[p.teamId],home=G.teams[p.loanFrom]; cur.roster=cur.roster.filter(x=>x!==p.id);cur.lineup=cur.lineup.filter(x=>x!==p.id); p.teamId=p.loanFrom;p.loanFrom=undefined;p.loanDaysLeft=undefined; if(home){home.roster.push(p.id);this.autoLineup(home); this.pushNews('transfer',`${p.nick} returned from loan`,`${p.nick} is back in the ${home.name} squad after his development spell.`,'transfer');} }, /* transfer windows: two per season (post-spring ~day150-230, winter ~300-364) */ inTransferWindow(){ const d=((G.day-1)%364)+1; return (d>=150&&d<=230)||(d>=300); }, aiTransferWindow(){ if(!this.inTransferWindow())return; const aggro=G.diff.aiAggro; if(!U.chance(0.5*aggro))return; const aiTeams=Object.values(G.teams).filter(t=>!t.isPlayer&&t.roster.length>3); const buyer=U.pick(aiTeams); // candidate: free agents or listed players better than buyer's weakest starter const starters=buyer.lineup.map(id=>G.players[id]); const weak=U.avg(starters.map(playerOverall)); const pool=[ ...G.freeAgents.map(id=>G.players[id]), ...Object.values(G.players).filter(p=>p.listedPrice&&G.teams[p.teamId]&&!G.teams[p.teamId].isPlayer), ...Object.values(G.players).filter(p=>!G.teams[p.teamId]||(!G.teams[p.teamId].isPlayer&&!p.listedPrice&&U.chance(0.06))), ].filter(p=>p&&playerOverall(p)>weak+3&&(!p.loanFrom)&&p.teamId!==buyer.id); if(!pool.length)return; const target=U.pickW(pool,p=>playerOverall(p)); const from=G.teams[target.teamId]; if(from&&from.isPlayer){ // bid on YOUR player β†’ inbox offer if(target.listedPrice===null&&U.chance(.5))return; const fee=target.listedPrice??Math.round(playerValue(target,G)*U.rnd(0.9,1.3)/50000)*50; const wage=Math.round(wageDemand(target,G)*U.rnd(1.05,1.35)/500)*500; this.pendingOffers.push({pid:target.id,from:buyer.id,fee,wage,expires:G.day+10}); this.pushNews('offer',`${buyer.name} bid for ${target.nick}`, `${buyer.tag} offered ${U.fmtM(fee)} (his value: ${U.fmtM(playerValue(target,G))}) with wages of ${U.fmtM(wage)}/mo. Respond in the Inbox or Squad screen.`,'offer', {action:{kind:'bid'}}); }else if(from){ const fee=Math.round(playerValue(target,G)*U.rnd(1.0,1.35)/50000)*50; from.roster=from.roster.filter(x=>x!==target.id);from.lineup=from.lineup.filter(x=>x!==target.id); this.autoLineup(from); target.teamId=buyer.id;target.salary=this._aiSalary(target);target.listedPrice=null; buyer.roster.push(target.id);this.autoLineup(buyer); buyer.fam=U.clamp(buyer.fam-10,0,100);from.fam=U.clamp(from.fam-10,0,100); this.pushNews('rumor',`TRANSFER: ${target.nick} β†’ ${buyer.tag}`, `${buyer.name} complete the signing of ${target.nick} (${playerOverall(target).toFixed(0)} OVR) from ${from.name} for a reported ${U.fmtM(fee)}.`,'rumor'); }else{ // FA signs target.teamId=buyer.id;target.salary=this._aiSalary(target);target.contractUntil=G.day+U.ri(365,730); G.freeAgents=G.freeAgents.filter(x=>x!==target.id); buyer.roster.push(target.id);this.autoLineup(buyer); } }, respondBid(i,accept){ const b=G.pendingOffers[i]; if(!b)return; const p=this.p(b.pid),me=this.me(); G.pendingOffers.splice(i,1); if(accept){ G.money+=b.fee; me.roster=me.roster.filter(x=>x!==b.pid);me.lineup=me.lineup.filter(x=>x!==b.pid); const buyer=G.teams[b.from]; p.teamId=buyer.id;p.salary=b.wage;p.listedPrice=null;p.promise=null; buyer.roster.push(b.pid);this.autoLineup(buyer); this.checkPromisesAfterLineup(); me.fam=U.clamp(me.fam-10,0,100); this.pushNews('transfer',`${p.nick} sold to ${buyer.name}`,`Deal done: ${U.fmtM(b.fee)} banked. Reinvest wisely.`,'transfer'); }else{ this.pushNews('offer',`Bid rejected`,`${this.me().name} turned down ${G.teams[b.from].tag}'s approach for ${p.nick}.`,'offer'); } }, /* ---------- finances ---------- */ financesWeekly(){ const me=this.me(); let wages=0; me.roster.forEach(pid=>wages+=G.players[pid].salary/4.33); Object.values(me.staff).forEach(sid=>{const s=G.staff[sid];if(s)wages+=s.salary/4.33;}); G.money-=Math.round(wages); // merch & streaming const media=this.staffOf(me,'Media Manager'); const merch=G.fans*0.05*(media?1+media.skill/200:1); G.money+=merch; this.lastWeeklyWages=Math.round(wages);this.lastWeeklyMerch=Math.round(merch); if(G.money<-400000&&!G.flags.debtWarned){ G.flags.debtWarned=true; this.pushNews('finance','⚠️ CRITICAL DEBT','The account is deep in red. Sell players, cut wages, or the board will force austerity.','finance'); } if(G.money>0)G.flags.debtWarned=false; const fin=this.staffOf(me,'Finance Director'); if(fin&&U.chance(0.2)){G.money+=Math.round(fin.skill*40);} // finds savings }, fansWeekly(){ const me=this.me(); const media=this.staffOf(me,'Media Manager'); let growth=G.rep*2+(media?(media.skill-60)*8:0); growth*=U.rnd(0.7,1.3); G.fans=Math.max(500,Math.round(G.fans+growth)); }, genSponsorOffers(){ const me=this.me();const rank=this.rankOf(me.id)||50; const fin=this.staffOf(me,'Finance Director'); G.sponsorOffers=[]; for(let i=0;i<3;i++){ const brand=U.pick(SPONSOR_BRANDS.filter(b=>!G.sponsors.some(s=>s.brand===b))); const type=U.pick(SPONSOR_TYPES); const quality=U.clamp(95-rank*1.1+G.rep*0.4+U.gauss(0,8),10,120); const monthly=Math.round((quality*quality*3.6)*(fin?1+fin.skill/400:1)/500)*500; G.sponsorOffers.push({id:U.uid(),brand,type,monthly,months:U.ri(6,18),req:rank}); } this.pushNews('sponsor','New sponsorship proposals',`${G.sponsorOffers.length} brands tabled offers. Review them in Finances.`,'sponsor',{action:{kind:'sponsor'}}); }, acceptSponsor(id){ const off=G.sponsorOffers.find(o=>o.id===id);if(!off)return false; if(G.sponsors.length>=3)return false; G.sponsors.push({...off,until:G.day+off.months*28}); G.sponsorOffers=G.sponsorOffers.filter(o=>o.id!==id); this.pushNews('sponsor',`${off.brand} partnership signed`,`${off.type} deal worth ${U.fmtM(off.monthly)}/mo for ${off.months} months. Welcome aboard!`,'sponsor'); return true; }, rejectSponsor(id){G.sponsorOffers=G.sponsorOffers.filter(o=>o.id!==id);}, tickSponsors(){ G.sponsors=G.sponsors.filter(s=>{ if(G.day>s.until){this.pushNews('sponsor',`${s.brand} deal expired`,`The partnership with ${s.brand} ran its course.`,'sponsor');return false;} return true; }); }, /* ---------- board ---------- */ boardCheck(){ const me=this.me(); const strict=G.diff.boardStrict; const recent=G.resultsRecent||(G.resultsRecent=[]); const wr=recent.length?U.avg(recent)/1:0.5; const expect=me.tier===1?0.52:me.tier===2?0.47:0.43; let delta=(wr-expect)*10*strict + (G.money>0?0.7:-0.9)*strict; G.board=U.clamp(G.board+U.clamp(delta,-3,3),0,100); if(G.board<25&&!G.flags.boardWarned){G.flags.boardWarned=true; this.pushNews('board','⚠️ Board is losing patience','Results and/or finances are below expectations. The board demands improvement soon.','board');} if(G.board>=40)G.flags.boardWarned=false; }, recordResult(win){ G.resultsRecent=G.resultsRecent||[]; G.resultsRecent.push(win?1:0); if(G.resultsRecent.length>10)G.resultsRecent.shift(); }, /* ---------- tournaments ---------- */ registerEvent(evId){ const ev=G.calendar.find(e=>e.id===evId);if(!ev)return; if(ev.status!=='open'||ev.registered)return; const me=this.me(); const rank=this.rankOf(me.id)||99; const cutoff=ev.tier===1?20:ev.tier===2?40:999; if(rank>cutoff&&ev.tier<3){ // wildcard lottery chance if(!U.chance(U.clamp(0.9-(rank-cutoff)*0.06,0.05,0.9))){ return {ok:false,msg:`Ranking #${rank} is outside the invite zone (top ${cutoff}). Try lower tiers.`}; } } ev.registered=true; this.pushNews('event',`Registered: ${ev.name}`,`${me.name} will compete in ${ev.name} (starts ${U.fmtDate(ev.startDay)}).`,'event'); return {ok:true}; }, lockEvents(){ const me=this.me(); G.calendar.forEach(ev=>{ if(ev.status!=='open')return; if(G.day>=ev.startDay-14){ // build field let field=[]; if(ev.registered)field.push(me.id); const others=Object.values(G.teams).filter(t=>t.id!==me.id) .sort((a,b)=>b.ratingPts-a.ratingPts); field.push(...others.slice(0,ev.slots-field.length).map(t=>t.id)); if(field.length<4)field.push(...others.slice(ev.slots,ev.slots+4-field.length).map(t=>t.id)); // trim to power of two for a clean bracket const nPow=Math.max(4,Math.pow(2,Math.floor(Math.log2(Math.min(field.length,16))))); ev.teams=field.slice(0,nPow); ev.status='locked'; if(ev.teams.includes(me.id)) this.pushNews('event',`${ev.name} β€” final team list`, `${ev.name} field is set (${ev.teams.length} teams, ${U.fmtM(ev.prize)} to the winner). Event starts ${U.fmtDateShort(ev.startDay)}.`,'event'); } }); }, tickEvent(ev){ this.lockEvents(); if(ev.status==='locked'&&G.day>=ev.startDay){ ev.status='live'; this.buildBracket(ev); this.pushNews('event',`${ev.name} begins!`,`${ev.teams.length} teams fight for ${U.fmtM(ev.prize)}${ev.major?' and Major glory':''}.`,'event'); } if(ev.status==='live'){ // cascade: fill next rounds when ready, then play everything due (past or today) for(let iter=0;iter<12;iter++){ let progressed=false; ev.rounds.forEach(rnd=>{ const i=ev.rounds.indexOf(rnd); if(i>0&&!rnd.filled){ const prev=ev.rounds[i-1]; if(prev.matches.every(m=>m.winner)){ this.advanceRound(ev,prev); rnd.filled=true; if(!rnd.matches[0].a)rnd.filled=false; // safety progressed=true; } } }); ev.rounds.forEach(rnd=>{ rnd.matches.forEach(m=>{ if(m.day<=G.day&&!m.winner&&m.a&&m.b&&(m.a!==G.playerTeamId&&m.b!==G.playerTeamId)){ MatchEngine.simulate(m,ev);progressed=true; } }); }); if(!progressed)break; } if(ev.rounds.length&&ev.rounds.at(-1).matches.every(m=>m.winner)){ this.finishEvent(ev); } } this.tickSponsors(); }, buildBracket(ev){ const teams=U.shuffle(ev.teams.slice()); const n=teams.length; const offs=ev.days>=9?[0,2,4,ev.days-3]:ev.days>=7?[0,2,3,ev.days-2]:[0,1,2,3]; let names; if(n>=16)names=['Round of 16','Quarterfinal','Semifinal','Grand Final']; else if(n>=8)names=['Quarterfinal','Semifinal','Grand Final']; else names=['Semifinal','Grand Final']; let idx=0; for(let r=0;rm.winner); nxt.matches.forEach((m,j)=>{m.a=winners[j*2];m.b=winners[j*2+1];}); }, finishEvent(ev){ ev.status='done'; const final=ev.rounds.at(-1).matches[0]; const champ=final.winner, runner=final.winner===final.a?final.b:final.a; ev.champion=champ; const sfLosers=ev.rounds.at(-2).matches.map(m=>m.winner===m.a?m.b:m.a); const qfLosers=ev.rounds.at(-3)?ev.rounds.at(-3).matches.map(m=>m.winner===m.a?m.b:m.a):[]; const places=[champ,runner,...sfLosers,...qfLosers]; places.forEach((tid,i)=>{ const fr=i0)this.pushNews('finance',`${U.esc(ev.name)}: ${U.fmtM(money)} prize money`, `Placement earnings banked. Current balance: ${U.fmtM(G.money)}.`,'finance'); } }); const ct=G.teams[champ]; this.pushNews('result',`πŸ† ${ct.name} win ${ev.name}!`, `${ct.tag} defeated ${G.teams[runner].tag} in the grand final. ${U.fmtM(ev.prize)} and ${ev.pts} ranking points to the champions.`,'result'); if(ct.isPlayer){ G.rep=U.clamp(G.rep+ (ev.major?8:ev.tier===1?5:2),0,100); G.fans+=U.ri(20000,60000); G.achievements[`trophy_${ev.id}`]={ev:ev.name,day:G.day}; if(ev.gs){ G.grandSlamWins.push({ev:ev.id,day:G.day}); const recent=G.grandSlamWins.slice(-GS_REQUIRED); if(recent.length>=GS_REQUIRED&&(recent.at(-1).day-recent[0].day)<=GS_WINDOW_DAYS){ G.money+=GS_BONUS;G.achievements.grandSlam={day:G.day}; this.pushNews('result','πŸ’Ž GRAND SLAM COMPLETE!', `By winning ${GS_REQUIRED} elite events in a row you completed the GRAND SLAM and banked a ${U.fmtM(GS_BONUS)} bonus. Legendary.`,'result'); } } }else if(G.teams[runner].isPlayer){ G.rep=U.clamp(G.rep+ (ev.major?5:3),0,100); } }, /* called by match engine after any match resolves */ finishMatch(m,res){ m.winner=res.winner;m.score=res.score;m.maps=res.maps;m.box=res.box; const loser=res.winner===m.a?m.b:m.a; const wTeam=G.teams[res.winner],lTeam=G.teams[loser]; // board tracks the user's own form only if(wTeam.isPlayer)this.recordResult(true); else if(lTeam.isPlayer)this.recordResult(false); // player stats & fatigue & form const apply=(tid,won)=>{ const t=G.teams[tid]; t.lineup.forEach(pid=>{ const p=G.players[pid];if(!p)return; const st=res.box[pid]||{k:0,d:0,adr:40,ratingSum:1,mapsN:1}; const rt=st.rating!==undefined?st.rating:(st.ratingSum||1)/Math.max(st.mapsN||1,1); p.stats.maps++;p.seasonStats.maps++; p.stats.kills+=st.k;p.seasonStats.kills+=st.k; p.stats.deaths+=st.d;p.seasonStats.deaths+=st.d; p.stats.adr=(p.stats.adr*(p.stats.maps-1)+st.adr)/p.stats.maps; p.seasonStats.adr=p.stats.adr; p.stats.ratingSum+=rt;p.seasonStats.ratingSum+=rt; if(st.mvp)p.seasonStats.mvp++; if(st.aces)p.seasonStats.aces+=st.aces; p.fatigue=U.clamp(p.fatigue+U.rnd(6,11),0,100); p.form=U.clamp(p.form+(rt>1.1?0.5:rt<0.85?-0.4:0.1)+ (won?0.25:-0.2),0.5,10); p.morale=U.clamp(p.morale+(won?2.5:-2.5)+(rt>1.2?2:rt<0.8?-2:0),0,100); }); }; apply(res.winner,true);apply(loser,false); const sw=U.ri(8,20); wTeam.ratingPts+=sw;lTeam.ratingPts=Math.max(100,lTeam.ratingPts-sw); // post-match news for user matches if(wTeam.isPlayer||lTeam.isPlayer){ const meIsWin=wTeam.isPlayer; const opp=meIsWin?lTeam:wTeam; this.pushNews('match',`${meIsWin?'βœ… Victory':'❌ Defeat'} vs ${opp.tag} (${res.score})`, `${meIsWin?'We':'We'} ${res.score} against ${opp.name}.${res.note||''}`,'match'); } }, /* ---------- seasons ---------- */ newSeason(){ const rank=this.rankOf(G.playerTeamId)||99; G.history.push({season:G.season,rank,money:G.money,fans:G.fans,trophies:Object.keys(G.achievements).length}); G.season++; Object.values(G.players).forEach(p=>{ p.age++; p.stats={...this.zeroStats()}; }); // extend calendar next year const shift=364*(G.season-1); CAL_2026.forEach((e,i)=>{ if(G.calendar.some(x=>x.baseId==='e'+i&&x.seasonNo===G.season))return; G.calendar.push({id:'e'+i+'y'+G.season,baseId:'e'+i,...e,startDay:e.day+shift, seasonNo:G.season,status:'open',teams:[],champion:null,rounds:[],registered:false,userOut:false}); }); // NOTE: finished events are kept forever β€” they hold the user's match history this.pushNews('season',`πŸŽ‰ Season ${G.season-1} review`, `Final world ranking: #${rank}. Balance: ${U.fmtM(G.money)}. A new season of events is published β€” chase greatness.`,'board'); this.computeRanking(); }, /* ---------- save/load ---------- */ serialize(){return JSON.stringify(G);}, staticLoad(json){ const obj=JSON.parse(json); if(!obj||obj.ver!==1)throw new Error('bad save'); G=obj;G.diff=DIFFS[obj.diffKey]; return true; }, }; /* tiny save facade (localStorage guarded for node tests) */ const Save={ store(){ try{return (typeof localStorage!=='undefined')?localStorage:null;}catch(e){return null;} }, save(auto){ const ls=this.store();if(!ls||!G)return; try{ls.setItem(SAVE_KEY,Game.serialize());if(auto)ls.setItem(SAVE_KEY+'_ts',String(Date.now()));}catch(e){} }, exists(){const ls=this.store();if(!ls)return false;try{return !!ls.getItem(SAVE_KEY);}catch(e){return false;}}, load(){const ls=this.store();if(!ls)return false;try{Game.staticLoad(ls.getItem(SAVE_KEY));return true;}catch(e){return false;}}, wipe(){const ls=this.store();if(ls){try{ls.removeItem(SAVE_KEY);}catch(e){}}}, };