Spaces:
Running
Running
File size: 22,661 Bytes
3597f5a 502e0c9 3597f5a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 | ;
;
/* ============================================================================
DROPPED SESSION RECOVERY
----------------------------------------------------------------------------
The WebGL context-loss handler in gl.js shows an overlay reading "your
progress is saved to this device and your account", waits 1.7 seconds, and
reloads the page. That sentence was not true of the thing the player cares
about. Account progress — cores, XP, unlocks — is saved. The MATCH is not,
and nothing in this codebase ever saved one, so a context loss five minutes
into a fight threw the fight away and dropped the player back at the menu
with no way back in.
Context loss is not a bug we can promise never to hit. Android reclaims GPU
memory when it wants to, and a large swarm on a warm phone is exactly the
condition that provokes it. So the honest fix is not only to make it rarer —
it is to make it survivable.
WHAT IS SNAPSHOTTED, AND WHY THIS SET
The sim is structure-of-arrays, so a snapshot is a handful of typed-array
slices plus a plain list for structures. We store what cannot be derived:
positions, health, ownership, orders, resources, the clock and the setup the
match was generated from. We deliberately do NOT store terrain, deposits,
hives or doodads — all of those are regenerated deterministically from the
map's seed (see srand() in gl.js), so persisting them would be megabytes to
reproduce something a single integer already reproduces exactly.
WHY localStorage AND NOT IndexedDB
This runs inside a `webglcontextlost` handler with a 1.7-second budget before
the page reloads. IndexedDB is asynchronous and is not guaranteed to flush in
that window; localStorage is synchronous and is written before the handler
returns. The snapshot is capped so it cannot exceed the quota and take the
rest of the save data down with it.
============================================================================ */
const SESS_KEY = 'mf_dropped_session_v1';
const SESS_MAX_UNITS = 4000; // DEBT: map-total snapshot slice, not a pop cap. Per-seat cap is FACTION_POP_CAP (1000).
const SESS_TTL_MS = 45 * 60 * 1000;
function sessCanSnapshot(){
try{
return typeof running !== 'undefined' && running &&
typeof demoMode !== 'undefined' && !demoMode &&
typeof gameEnded !== 'undefined' && !gameEnded &&
typeof heroIdx !== 'undefined' && heroIdx >= 0;
}catch(e){ return false; }
}
/* A live unit's full order state. Kept as parallel plain arrays rather than an
array of objects: JSON.stringify of 4000 objects is several times slower and
several times larger, and this runs against a reload deadline. */
function sessCaptureUnits(){
const idx=[], out={t:[],tm:[],x:[],y:[],hp:[],ang:[],st:[],tx:[],ty:[],hold:[],mode:[],
oi:[],cmd:[],tg:[],mh:[],pr:[],pst:[],psl:[],gh:[],gg:[],qk:[],q:[]};
for(let i=0;i<unitHigh;i++) if(ualive[i]) idx.push(i);
/* Over the cap, keep the player's army and the enemy's commanders first —
losing some wildlife on a resume is survivable; losing your own force is
the thing this exists to prevent. */
if(idx.length>SESS_MAX_UNITS){
idx.sort((a,b)=>(uteam[a]===0?0:uteam[a]===1?1:2)-(uteam[b]===0?0:uteam[b]===1?1:2));
idx.length=SESS_MAX_UNITS;
}
for(const i of idx){
out.t.push(utype[i]); out.tm.push(uteam[i]);
out.x.push(Math.round(ux[i])); out.y.push(Math.round(uy[i]));
out.hp.push(Math.round(uhp[i])); out.ang.push(+uang[i].toFixed(2));
out.st.push(ustate[i]);
out.tx.push(Math.round(utx[i])); out.ty.push(Math.round(uty[i]));
out.hold.push(uhold[i]|0); out.mode.push(typeof umode!=='undefined'?(umode[i]|0):0);
/* Old slot is the remap key. spawnUnit hands out new indices, so utgt /
guard / queue / patrol members stored as raw slots would point at the
wrong hulls after a resume. ufield is omitted on purpose: the field
ring dies with the match and is rebuilt from tx/ty. */
out.oi.push(i);
out.cmd.push(typeof uCmd!=='undefined'?uCmd[i]:-1);
out.tg.push(typeof utgt!=='undefined'?utgt[i]:-1);
out.mh.push(typeof umarch!=='undefined'?umarch[i]|0:0);
out.pr.push(typeof uPatrolRoute!=='undefined'?uPatrolRoute[i]:-1);
out.pst.push(typeof uPatrolStep!=='undefined'?uPatrolStep[i]|0:0);
out.psl.push(typeof uPatrolSlot!=='undefined'?uPatrolSlot[i]|0:0);
out.gh.push(typeof uGuard!=='undefined'?uGuard[i]:-1);
out.gg.push(typeof uGuardG!=='undefined'?uGuardG[i]:-1);
out.qk.push(typeof uQkind!=='undefined'?uQkind[i]|0:0);
const Q=(typeof uQueue!=='undefined'&&uQueue[i])?uQueue[i]:null;
out.q.push(Q?Q.map(s=>({t:s.t|0,x:Math.round(s.x||0),y:Math.round(s.y||0),h:s.h|0,g:s.g|0,mv:s.mv|0})):null);
}
return out;
}
function sessCaptureBuildings(){
const out=[];
for(let bi=0;bi<blds.length;bi++){
const B=blds[bi];
if(!B||!B.alive) continue;
out.push([B.type, B.team, Math.round(B.x), Math.round(B.y),
Math.round(B.hp), +(B.prog||0).toFixed(3), B.lvl|0,
B.buildPaidM==null?null:+B.buildPaidM.toFixed(3),
B.buildPaidE==null?null:+B.buildPaidE.toFixed(3),
bi]);
}
return out;
}
function sessCaptureWallets(){
const seats=[], allies=[];
if(typeof AI!=='undefined'){
if(AI.bases) for(const S of AI.bases)
seats.push({slot:S.slot,mass:+(S.mass||0).toFixed(2),energy:+(S.energy||0).toFixed(2),
mcap:S.mcap|0,ecap:S.ecap|0});
if(AI.allies) for(const A of AI.allies)
allies.push({slot:A.slot,mass:+(A.mass||0).toFixed(2),energy:+(A.energy||0).toFixed(2)});
}
return {seats,allies};
}
function sessCapturePatrols(){
if(typeof patrolRoutes==='undefined'||!patrolRoutes.length) return [];
const out=[];
for(let ri=0;ri<patrolRoutes.length;ri++){
const R=patrolRoutes[ri];
if(!R||!R.pts||!R.members){ out.push(null); continue; }
out.push({
pts:R.pts.map(p=>({x:+p.x,y:+p.y})),
form:R.form|0, step:R.step|0,
members:R.members.map(e=>[e[0]|0,e[1]|0])
});
}
return out;
}
/* Relic handles survive a seed regen. Unit/building handles do not. */
function sessRemapHandle(h,uMap,bMap){
if(h==null||h===-1) return -1;
if(typeof isRelicTg==='function'&&isRelicTg(h)) return h;
if(h>=0) return uMap.has(h)?uMap.get(h):-1;
const bi=-2-h;
return bMap.has(bi)?(-2-bMap.get(bi)):-1;
}
function sessApplyWallets(w){
if(!w||typeof AI==='undefined') return;
if(w.seats&&AI.bases){
for(const row of w.seats){
let S=null;
for(let i=0;i<AI.bases.length;i++) if(AI.bases[i].slot===row.slot){ S=AI.bases[i]; break; }
if(!S) continue;
if(row.mass!=null) S.mass=row.mass;
if(row.energy!=null) S.energy=row.energy;
if(row.mcap) S.mcap=row.mcap;
if(row.ecap) S.ecap=row.ecap;
}
}
if(w.allies&&AI.allies){
for(const row of w.allies){
let A=null;
for(let i=0;i<AI.allies.length;i++) if(AI.allies[i].slot===row.slot){ A=AI.allies[i]; break; }
if(!A) continue;
if(row.mass!=null) A.mass=row.mass;
if(row.energy!=null) A.energy=row.energy;
}
}
/* Fresh deploy wrote commander indices we then killed. Rebind by seat. */
if(AI.bases){
for(const S of AI.bases){
let best=-1,bd=1e18;
for(let i=0;i<unitHigh;i++){
if(!ualive[i]||uteam[i]!==1) continue;
const T=TYPES[utype[i]];
if(!(utype[i]===4||(T&&T.cat==='hero'))) continue;
if(typeof uCmd!=='undefined'&&uCmd[i]===S.slot){ best=i; break; }
const d=dist2(ux[i],uy[i],S.x,S.y);
if(d<bd){ bd=d; best=i; }
}
if(best>=0){ S.commander=best; S.commanderGen=ugen[best]; }
}
}
if(typeof econMirrorAiBanks==='function') econMirrorAiBanks();
}
function sessRestoreOrders(U,spawned,uMap,bMap,patrols){
if(!U||!U.oi) return;
for(let k=0;k<spawned.length;k++){
const i=spawned[k]; if(i<0) continue;
if(typeof umarch!=='undefined'&&U.mh) umarch[i]=U.mh[k]|0;
if(typeof utgt!=='undefined'&&U.tg){
const tg=sessRemapHandle(U.tg[k],uMap,bMap);
utgt[i]=tg;
if(typeof utgtg!=='undefined') utgtg[i]=tg>=0?ugen[tg]:-1;
}
if(typeof uGuard!=='undefined'&&U.gh){
const gh=sessRemapHandle(U.gh[k],uMap,bMap);
uGuard[i]=gh;
if(typeof uGuardG!=='undefined') uGuardG[i]=gh>=0?ugen[gh]:-1;
}
if(typeof uQkind!=='undefined'&&U.qk) uQkind[i]=U.qk[k]|0;
if(typeof uQueue!=='undefined'&&U.q&&U.q[k]){
uQueue[i]=U.q[k].map(s=>{
const h=sessRemapHandle(s.h,uMap,bMap);
return {t:s.t|0,x:s.x,y:s.y,h,g:h>=0?ugen[h]:-1,mv:s.mv|0};
});
}
if(typeof ufield!=='undefined'){
ufield[i]=-1;
const T=TYPES[utype[i]];
if(T&&!T.air&&(ustate[i]===1||ustate[i]===2||ustate[i]===5)&&typeof requestField==='function')
ufield[i]=requestField(utx[i],uty[i],!!T.naval);
}
}
if(typeof patrolRoutes==='undefined') return;
patrolRoutes.length=0;
if(!patrols||!patrols.length) return;
for(const R of patrols){
if(!R||!R.pts||!R.members){ patrolRoutes.push(null); continue; }
const members=[];
for(const e of R.members){
const ni=uMap.get(e[0]);
if(ni==null||!ualive[ni]) continue;
members.push([ni,ugen[ni]]);
}
if(!members.length){ patrolRoutes.push(null); continue; }
const sel=members.map(e=>e[0]);
const targets=(typeof patrolTargetRows==='function')?patrolTargetRows(sel,R.pts,R.form):[];
const rec={pts:R.pts,targets,form:R.form|0,members,step:R.step|0,legT:0,waitT:0,gcT:.45,created:performance.now()};
const ri=patrolRoutes.length;
patrolRoutes.push(rec);
for(let k=0;k<sel.length;k++){
const i=sel[k];
if(typeof uPatrolRoute!=='undefined') uPatrolRoute[i]=ri;
if(typeof uPatrolStep!=='undefined') uPatrolStep[i]=rec.step;
if(typeof uPatrolSlot!=='undefined') uPatrolSlot[i]=k;
}
if(typeof refreshPatrolRoute==='function') refreshPatrolRoute(ri,true);
}
}
function sessSnapshot(reason){
if(!sessCanSnapshot()) return false;
try{
const snap={
v:1, at:Date.now(), reason:reason||'unknown',
/* The setup is what regenerates the world. Everything terrain-shaped
falls out of it, which is why the snapshot is kilobytes not megabytes. */
setup: (typeof META!=='undefined'&&META.setup)?META.setup:null,
map: typeof curMap!=='undefined'?curMap:null,
theme: typeof curTheme!=='undefined'?curTheme:null,
goal: typeof goalSel!=='undefined'?goalSel:null,
timeLimit: typeof timeLimit!=='undefined'?timeLimit:0,
aiFac: (typeof AI!=='undefined'&&AI.fac)?AI.fac:null,
playerFac: typeof playerFaction!=='undefined'?playerFaction:'nova',
playerCommander: typeof playerCommanderId!=='undefined'?playerCommanderId:'nova_kai',
clock: typeof matchClock!=='undefined'?matchClock:0,
t: (typeof stats!=='undefined'&&stats.t)?stats.t:0,
kills: (typeof stats!=='undefined'&&stats.kills)?stats.kills.slice():[0,0,0],
built: (typeof stats!=='undefined'&&stats.built)?stats.built.slice():[0,0],
resM: typeof resM!=='undefined'?[resM[0],resM[1]]:[0,0],
resE: typeof resE!=='undefined'?[resE[0],resE[1]]:[0,0],
wallets: sessCaptureWallets(),
wave: (typeof AI!=='undefined')?{n:AI.wave|0,timer:AI.waveTimer|0}:null,
units: sessCaptureUnits(),
blds: sessCaptureBuildings(),
patrols: sessCapturePatrols(),
};
const text=JSON.stringify(snap);
/* A snapshot that blows the quota would throw and take nothing with it —
but it could also evict the account save on some engines, so refuse
rather than risk it. 3 MB is far above a realistic match and far below
the 5 MB localStorage floor. */
if(text.length>3*1024*1024) return false;
localStorage.setItem(SESS_KEY,text);
return true;
}catch(e){ return false; }
}
function sessLoad(){
try{
const raw=localStorage.getItem(SESS_KEY);
if(!raw) return null;
const s=JSON.parse(raw);
if(!s||s.v!==1||!s.units) return null;
/* An hours-old snapshot is not a dropped session, it is an abandoned one,
and offering it teaches the player that the prompt is noise. */
if(Date.now()-(s.at||0)>SESS_TTL_MS){ sessClear(); return null; }
return s;
}catch(e){ return null; }
}
function sessClear(){ try{ localStorage.removeItem(SESS_KEY); }catch(e){} }
function sessHas(){ return !!sessLoad(); }
/* A short human description for the resume prompt. A player will not act on
"resume session"; they will act on "9 minutes in, 34 units, Vanguard Valley". */
function sessDescribe(s){
if(!s) return '';
const mins=Math.max(0,Math.round((s.t||0)/60));
const mine=(s.units.tm||[]).filter(t=>t===0).length;
const M=(typeof MAPDEFS!=='undefined'&&MAPDEFS[s.map])?MAPDEFS[s.map].nm:(s.map||'battlefield');
return mins+' min in · '+mine+' units · '+M;
}
/* ---- RESTORE ---------------------------------------------------------------
Runs AFTER the normal match generation, so the world is already built from
the same setup and the same seed. We are placing the fight back onto a
battlefield that regenerated identically, not rebuilding the battlefield. */
function sessRestoreInto(s){
if(!s) return false;
try{
/* Clear whatever the fresh start spawned before laying the snapshot down,
or the player resumes with two commanders and a doubled army. */
for(let i=0;i<unitHigh;i++) if(ualive[i]) killUnit(i,true);
for(const B of blds) if(B.alive&&B.type!=='nest') B.alive=false;
refreshBldLive();
const bMap=new Map();
for(const b of s.blds){
const [type,team,x,y,hp,prog,lvl,paidM,paidE,oldBi]=b;
try{
const B=addBld(type,team,x,y,true);
if(B){ B.hp=hp; B.prog=prog; B.lvl=lvl||1;
if(paidM!=null)B.buildPaidM=paidM;if(paidE!=null)B.buildPaidE=paidE;
if(oldBi!=null) bMap.set(oldBi,blds.length-1); }
}catch(e){}
}
refreshBldLive();
const U=s.units, uMap=new Map(), spawned=[];
for(let k=0;k<U.t.length;k++){
const cmd=U.cmd?U.cmd[k]:undefined;
const i=spawnUnit(U.t[k],U.tm[k],U.x[k],U.y[k],cmd);
spawned.push(i);
if(i<0) continue;
if(U.oi) uMap.set(U.oi[k],i);
uhp[i]=U.hp[k]; uang[i]=U.ang[k]; ustate[i]=U.st[k];
utx[i]=U.tx[k]; uty[i]=U.ty[k]; uhold[i]=U.hold[k];
if(typeof umode!=='undefined') umode[i]=U.mode[k];
}
/* Player ledger first. Seat wallets then overwrite the team-1 mirror so a
1v2/1v3 resume does not dump three mex belts into one shared bank. */
if(typeof resM!=='undefined'&&s.resM){ resM[0]=s.resM[0]; if(!s.wallets) resM[1]=s.resM[1]; }
if(typeof resE!=='undefined'&&s.resE){ resE[0]=s.resE[0]; if(!s.wallets) resE[1]=s.resE[1]; }
sessApplyWallets(s.wallets);
sessRestoreOrders(U, spawned, uMap, bMap, s.patrols);
if(typeof stats!=='undefined'){
stats.t=s.t||0;
if(s.kills) stats.kills=s.kills.slice();
if(s.built) stats.built=s.built.slice();
}
if(typeof matchClock!=='undefined') matchClock=s.clock||0;
if(typeof AI!=='undefined'&&s.wave){ AI.wave=s.wave.n||1; AI.waveTimer=s.wave.timer||60; }
/* THE COMMANDER IS WHAT checkVictory KEYS OFF. heroIdx is set once at spawn
and cleared on death; a restore that does not re-point it leaves it at -1
and the resumed match ends as an instant defeat on the very first tick.
Same for the enemy commanders, or the win condition can never be met. */
heroIdx=-1; enemyHeroIdx=-1; enemyHeroIdxs.length=0;
for(let i=0;i<unitHigh;i++){
if(!ualive[i]) continue;
const T=TYPES[utype[i]];
const isCdr = utype[i]===4 || (T && T.cat==='hero');
if(!isCdr) continue;
if(uteam[i]===0){ if(heroIdx<0) heroIdx=i; }
else if(uteam[i]===1){ if(enemyHeroIdx<0) enemyHeroIdx=i; enemyHeroIdxs.push(i); }
}
if(heroIdx<0) return (sessClear(),false); // nothing to resume into
sessClear();
return true;
}catch(e){ sessClear(); return false; }
}
/* Snapshot on the two events that actually lose a match: the GPU going away,
and the page being torn down while a fight is live. */
(function sessInstall(){
try{
const cvEl=document.getElementById('gl');
if(cvEl) cvEl.addEventListener('webglcontextlost',()=>{ sessSnapshot('contextlost'); },true);
/* `pagehide` fires on mobile where `beforeunload` frequently does not. */
window.addEventListener('pagehide',()=>{ sessSnapshot('pagehide'); });
/* Backgrounding is where Android reclaims the GPU, so take the snapshot on
the way out rather than hoping to get a handler when it does. */
document.addEventListener('visibilitychange',()=>{
if(document.visibilityState==='hidden') sessSnapshot('hidden');
});
}catch(e){}
})();
/* ---- THE PROMPT ------------------------------------------------------------
Offered on the main menu, once, and only while the snapshot is fresh. It sits
above WAR ROOM because a player who has just been dropped out of a nine-minute
fight is not looking for the menu — they are looking for their match. */
function sessRenderResume(){
const host=document.getElementById('startScreen');
if(!host) return;
let el=document.getElementById('sessResume');
const s=sessLoad();
if(!s){ if(el) el.remove(); return; }
if(!el){
el=document.createElement('button');
el.id='sessResume'; el.type='button'; el.className='mbtn';
const anchor=document.getElementById('startBtn');
if(anchor&&anchor.parentNode) anchor.parentNode.insertBefore(el,anchor);
else host.appendChild(el);
if(typeof mfBindTap==='function') mfBindTap(el,sessResume);
else el.addEventListener('click',sessResume);
}
el.innerHTML='◈ RESUME DROPPED SESSION'
+'<small style="display:block;margin-top:3px;font-size:9px;opacity:.8;letter-spacing:.06em">'
+sessDescribe(s)+'</small>';
}
function sessResume(){
const s=sessLoad();
if(!s){ sessRenderResume(); return; }
try{
/* Put the setup back BEFORE generating, so the world regenerates from the
same seed and the same rules the dropped match was played under. */
if(s.setup&&typeof META!=='undefined') META.setup=s.setup;
const su=s.setup||{};
if(su.d!=null&&typeof difficulty!=='undefined') difficulty=su.d;
if(su.pkg&&typeof deploymentPackage!=='undefined'&&typeof DEPLOYMENT_PACKAGES!=='undefined'&&DEPLOYMENT_PACKAGES[su.pkg]) deploymentPackage=su.pkg;
if(su.bs&&typeof battlefieldPreset!=='undefined'&&typeof battlefieldPresetKey==='function') battlefieldPreset=battlefieldPresetKey(su.bs);
if(su.inf!=null&&typeof infestationOn!=='undefined') infestationOn=!!su.inf;
if(su.df!=null&&typeof defenseFocus!=='undefined') defenseFocus=su.df?1:0;
if(su.rp&&typeof resPace!=='undefined') resPace=+su.rp;
if(su.cr!=null&&typeof crateRate!=='undefined') crateRate=crateRateBase=+su.cr;
if(Array.isArray(su.ais)&&typeof aiSlots!=='undefined'){
for(let i=0;i<Math.min(aiSlots.length,su.ais.length);i++){
const S=su.ais[i]||{};
aiSlots[i].on=!!S.on;
aiSlots[i].diff=(S.diff|0);
aiSlots[i].ally=!!S.ally;
if(S.zone) aiSlots[i].zone=S.zone;
if(S.behavior&&typeof aiBehaviorKey==='function') aiSlots[i].behavior=aiBehaviorKey(S.behavior);
}
}
if(s.map&&typeof curMap!=='undefined') curMap=s.map;
if(s.theme&&typeof curTheme!=='undefined') curTheme=s.theme;
if(s.goal&&typeof goalSel!=='undefined') goalSel=s.goal;
if(typeof timeLimit!=='undefined') timeLimit=s.timeLimit||0;
if(s.aiFac&&typeof aiFactionSel!=='undefined'){ aiFactionSel=s.aiFac;
if(typeof AI!=='undefined'){ AI.fac=s.aiFac; if(typeof aiFacPicked!=='undefined') aiFacPicked=true; } }
else if(su.f&&typeof aiFactionSel!=='undefined') aiFactionSel=su.f;
if(s.playerFac&&typeof playerFaction!=='undefined'&&
(typeof playableFactions!=='function'||playableFactions().includes(s.playerFac)))playerFaction=s.playerFac;
if(s.playerCommander&&typeof playerCommanderId!=='undefined'&&typeof commanderById==='function'){
const C=commanderById(s.playerCommander),R=COMMANDER_ROSTERS[playerFaction]||[];
if(C&&!C.aiOnly&&R.indexOf(C)>=0)playerCommanderId=s.playerCommander;
}
sessPending=s;
if(typeof initAudio==='function') initAudio();
if(typeof sfx==='function') sfx('ui');
if(typeof applyTheme==='function') applyTheme();
if(typeof hideFrontScreens==='function') hideFrontScreens();
if(typeof audMusicEnterMatch==='function') audMusicEnterMatch();
if(typeof newSkirmish==='function') newSkirmish();
/* SKIP THE LANDING. A resumed match should not ask the player to choose a
drop site again — they landed nine minutes ago, and their HQ is in the
snapshot. Put the carrier on the saved HQ and deploy it automatically, so
resuming lands them back in the fight rather than back at the start of
one. Deferred a frame because newSkirmish generates terrain first and the
deploy check tests the ground it is standing on. */
const hq=(s.blds||[]).find(b2=>b2[0]==='hq'&&b2[1]===0);
/* Terrain generation takes seconds and deployCarrier() refuses to land on
ground that does not exist yet, so this polls for readiness rather than
guessing a delay — a fixed timeout was silently landing nothing at all.
Bounded, so a map that never becomes deployable leaves the player at the
normal drop screen instead of hanging on a spinner. */
let tries=0;
const land=function(){
if(++tries>140) return; // ~35 s ceiling
try{
if(typeof carrierCanDeploy!=='function'||typeof deployCarrier!=='function') return;
if(hq&&typeof carrier!=='undefined'){ carrier.x=hq[2]; carrier.y=hq[3]; }
if(typeof matchLive!=='undefined'&&matchLive) return; // already down
if(carrierCanDeploy()){ deployCarrier(); return; }
}catch(e){}
setTimeout(land,250);
};
setTimeout(land,250);
}catch(e){ sessClear(); sessRenderResume(); }
}
|