; ; /* ============================================================================ LAUNCH TITLE REVEAL ---------------------------------------------------------------------------- The opening deliberately uses no faction dossier/contact-sheet imagery. Those assets belong in the codex. Cinematic faction chapters will return only when purpose-built key art exists for them. ============================================================================ */ (function(){ 'use strict'; var el=null,timer=0,open=false,reduced=false,lastFocus=null,motionQuery=null; function version(){ try{ if(typeof currentRunningVersion==='function') return currentRunningVersion(); if(typeof APP_VERSION!=='undefined') return APP_VERSION; if(typeof PACKAGED_REV!=='undefined') return PACKAGED_REV; }catch(e){} return 'PREVIEW'; } function escapeHtml(s){ return String(s||'').replace(/[&<>"']/g,function(c){ return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } function markup(){ return ''; } /* An OTA can replace markup before a native WebView has learned about a new binary asset. Never leave a broken-image icon in the title slot: a compact vector-like text lockup is a deliberate fallback, not an empty failure. */ function titleFallback(img){ if(!img||img.dataset.mfFallback||!img.parentNode) return; img.dataset.mfFallback='1'; var d=document.createElement('div');d.className='mfTitleFallback'; d.innerHTML='MASSFRONTCOMMAND. CONQUER. OVERWHELM.'; img.parentNode.replaceChild(d,img); } function protectTitleArt(){ ['mfTitleWordmark','menuBrand'].forEach(function(id){ var img=document.getElementById(id);if(!img) return; if(img.complete&&!img.naturalWidth){titleFallback(img);return;} img.addEventListener('error',function(){titleFallback(img);},{once:true}); }); } function clearTimer(){if(timer){clearTimeout(timer);timer=0;}} function scheduleClose(){ clearTimer(); if(!open||reduced) return; timer=setTimeout(function(){closeIntro(false);},2800); } /* Reveal the front end exactly once: mark so the menu (and every other overlay) may show, and drop the opaque #mfBootCover node. Removing the node — rather than hiding it with a body.mfIntroDone rule — keeps the menu covered even if a stale mfIntroDone persisted across an OTA body.innerHTML swap (that swap replaces 's children but not its class list). */ var gateFired=false; function revealFront(){ document.body.classList.add('mfIntroDone'); var cover=document.getElementById('mfBootCover'); if(cover&&cover.parentNode) cover.parentNode.removeChild(cover); /* Sign in / register sits between the launch title and the menu. Fired from here rather than from closeIntro() so it also runs on the reduced-motion and failsafe paths, which reveal the front end without closing a reveal that never opened. Once only — revealFront can be reached twice. */ if(!gateFired){ gateFired=true; setTimeout(function(){ if(typeof mfAuthGate==='function'){ try{ mfAuthGate(); }catch(e){} } },340); } } function closeIntro(startNow){ if(!el||!open) return; clearTimer();open=false; /* The title has now played, so the menu may show as the reveal fades out. */ revealFront(); /* Move focus OUT of #mfPreAlphaIntro BEFORE aria-hidden goes on it. openIntro() focuses #mfIntroStart. Parking that focus and then marking the subtree aria-hidden is the aria-hidden-focus violation: Chrome refuses the attribute, and a screen reader is left on a control that is supposed to have vanished. Do not hand off to #startBtn first — #startScreen is still aria-hidden at this instant, so that focus move would only relocate the same defect. Blur to now; the deferred #startBtn focus below runs after startScreen is unhidden. The account gate may steal that focus ~340ms later, which is fine. */ if(el.contains(document.activeElement)){ var parked=document.activeElement; if(parked&&parked.blur) parked.blur(); } el.classList.remove('open');el.setAttribute('aria-hidden','true'); document.body.classList.remove('mfIntroOpen'); var front=document.getElementById('startScreen');if(front) front.removeAttribute('aria-hidden'); setTimeout(function(){if(el&&!open) el.hidden=true;},260); if(typeof initAudio==='function'){try{initAudio();}catch(e){}} if(typeof sfx==='function'){try{sfx('ui');}catch(e){}} var play=document.getElementById('startBtn'); /* The reveal's START button used to click #startBtn, which back then meant PLAY and opened skirmish setup. #startBtn is now WAR ROOM, so that same auto-click threw every launch straight past the menu into the mode list. The intro hands off to the MAIN MENU and nowhere else; startNow only decides whether WAR ROOM gets focus immediately. */ if(play) setTimeout(function(){try{play.focus({preventScroll:true});}catch(e){play.focus();}},startNow?120:90); else if(lastFocus) lastFocus.focus(); /* First-run Standard still has to teach locked stars + medium theatre even if they go to Training next. Arm must-see now; do not start a lecture. */ if(typeof window.wtpEnsureMustSee==='function'){ try{ window.wtpEnsureMustSee(); }catch(e){} } } function openIntro(){ if(!el) return; lastFocus=document.activeElement;open=true;el.hidden=false;el.removeAttribute('aria-hidden'); document.body.classList.add('mfIntroOpen'); var front=document.getElementById('startScreen');if(front) front.setAttribute('aria-hidden','true'); requestAnimationFrame(function(){ if(!open) return;el.classList.add('open'); var start=document.getElementById('mfIntroStart');if(start) start.focus({preventScroll:true}); }); scheduleClose(); } function bindTap(node,fn){ if(typeof mfBindTap==='function') mfBindTap(node,fn);else node.addEventListener('click',fn); } function onKey(e){ if(!open) return; if(e.key==='Escape'){e.preventDefault();closeIntro(false);return;} if(e.key!=='Tab') return; var buttons=Array.prototype.slice.call(el.querySelectorAll('button:not([disabled])')).filter(function(b){return b.offsetParent!==null;}); if(!buttons.length) return; var first=buttons[0],last=buttons[buttons.length-1]; if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();} else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();} } function motionChanged(e){ reduced=!!e.matches;if(el) el.dataset.motion=reduced?'reduce':'full'; if(reduced) clearTimer();else scheduleClose(); } function stampFirstRunHint(root){ /* Title card is not a tutorial. One line for a career that has never dropped Standard, so Training-first still sees locked-stars / medium before they leave the reveal. Veterans keep the stock status. */ if(!root||root.querySelector('.mfTitleHint')) return; var fresh=true; try{ fresh=typeof META==='undefined'||!META||((META.standardMatches|0)+(META.matches|0))===0; }catch(e){} if(!fresh) return; var p=document.createElement('p'); p.className='mfTitleHint'; p.style.cssText='margin:10px 16px 0;max-width:22em;color:#9bb4c6;font:600 10px/1.35 var(--fU,system-ui);letter-spacing:.06em;text-align:center'; p.textContent='STANDARD walks the galaxy. Locked stars stay put. The mode is the medium theatre.'; var status=root.querySelector('.mfTitleStatus'); if(status&&status.parentNode) status.parentNode.insertBefore(p,status.nextSibling); else root.appendChild(p); } function initIntro(){ if(el) return; /* Arm the launch gate for THIS run. An OTA body swap keeps 's class list, so a mfIntroDone left by the previous session would let the menu show through; clearing it re-hides the front end until this reveal is dismissed. */ document.body.classList.remove('mfIntroDone'); var host=document.createElement('div');host.innerHTML=markup();el=host.firstElementChild; if(!el){ revealFront(); return; } // never strand the menu behind the boot cover document.body.appendChild(el); stampFirstRunHint(el); protectTitleArt(); motionQuery=window.matchMedia?matchMedia('(prefers-reduced-motion: reduce)'):null; reduced=!!(motionQuery&&motionQuery.matches);el.dataset.motion=reduced?'reduce':'full'; if(motionQuery){ if(typeof motionQuery.addEventListener==='function') motionQuery.addEventListener('change',motionChanged); else if(typeof motionQuery.addListener==='function') motionQuery.addListener(motionChanged); } bindTap(document.getElementById('mfIntroSkip'),function(){closeIntro(false);}); bindTap(document.getElementById('mfIntroStart'),function(){closeIntro(true);}); document.addEventListener('keydown',onKey,true); /* This is the app's launch sequence, not a one-time tutorial reward. Always present it after the shell has initialized, including to an established commander returning to the game. */ /* Open on the next frame, not after a 650ms delay: the menu must never be visible first. The boot cover + intro.css keep it hidden until then. */ if(typeof requestAnimationFrame==='function') requestAnimationFrame(function(){openIntro();}); else openIntro(); } /* Safety net: if the intro never manages to open (module error, boot failure), reveal the menu anyway so the game can never be stuck behind the opaque boot cover. Runs on both the packaged and OTA paths (this file loads on both) and calls revealFront so the cover NODE is removed — no CSS rule hides it — not merely flagged. Does nothing once the reveal is open or already finished. */ setTimeout(function(){ var b=document.body; if(!b) return; if(!b.classList.contains('mfIntroOpen')&&!b.classList.contains('mfIntroDone')) revealFront(); },9000); window.initIntro=initIntro; window.showPreAlphaIntro=function(){openIntro();}; window.__mfIntroDebug=function(){return {open:open,phase:'title',stage:0,stageId:'title',reduced:reduced, launchSequence:true,version:version(),usesFactionArt:false};}; })();