massfront-playtest / src /warprimer.js
CREATORJD's picture
Publish MASSFRONT web 1.33.99 (part 5)
35f08b0 verified
Raw History Blame Contribute Delete
15.4 kB
;
;
/* ============================================================================
WAR TABLE PRIMER β€” first-run orientation for the deployment route
----------------------------------------------------------------------------
KEEL (src/tutorial.js) teaches the battlefield. Nothing taught the screen
the player has to get through BEFORE a battlefield exists: the war table's
galaxy -> system -> planet -> region -> deploy chain. A new commander who
opens it sees four stars, three of them locked, and no explanation of what
a "region" is or why there are twelve sites; the stepper across the top
names the five stages but not what any of them are for.
This module walks that chain once. It is deliberately NOT part of KEEL:
* KEEL is gated on live match state and only exists inside a drop. The
war table is a front screen with no match, no units and no sim to read.
* KEEL's panel is fixed-position HUD chrome. On this screen a floating
card would have to fight the stage stepper, the mode contract row and
the footer dock for the same narrow phone lane, and lose.
So the card is inserted INTO the active stage panel near the choice it
describes. It scrolls with that content, never overlaps chrome, and needs
no z-index or pointer-events rules at all. galaxyui.js rebuilds the system,
planet and region panels with innerHTML on every stage change, which would
delete the card β€” so the poll below re-inserts it whenever it has gone
missing, the same self-healing pattern tutorial.js uses to keep its card
alive across renderOps().
galaxyui.js is not rewritten. A one-line stage hook (wtpOnStage) plus a
takeover of openPlanetarySetup let this file compact the lecture after
Training and keep the two must-see cards alive across SKIP GUIDE.
============================================================================ */
(function(){
/* ---------------------------------------------------------------------------
THE SCRIPT β€” one card per stage, in the order the route walks them. Copy
names the real controls and the real numbers the screen prints ("TAP AN
UNLOCKED STAR", "4 REGIONS Β· 12 SITES", "3 BATTLEFIELD SITES") so the card
and the panel underneath it can never disagree.
--------------------------------------------------------------------------- */
var PRIMER_VERSION=2;
var STAGES=[
{ id:'galaxy', nm:'GALAXY',
tx:'Sombrero-I is open. Select it, then tap ENTER. Other stars unlock as you conquer sites.' },
{ id:'system', nm:'SYSTEM',
tx:'Select the homeworld to reach its regions. Clear its 12 sites to unlock the next star.' },
{ id:'planet', nm:'PLANET',
tx:'Choose a region on the globe or in the list. Conquest opens neighboring regions.' },
{ id:'region', nm:'REGION',
/* Subtraction pass (audit item 1): region stacks this card on three ~50-word
site dossiers, so it gets one line β€” the choice itself is on the screen. */
tx:'Choose a site. STANDARD is the balanced pick.' },
{ id:'deploy', nm:'DEPLOY',
tx:'Review your force, then START BATTLE. Pick landing ground and tap DEPLOY BASE HERE.' }
];
/* The two facts a first Standard visit must still teach after Training or
SKIP GUIDE. Optional stages (system / planet / deploy) may be dropped. */
var MUST_SEE=['galaxy','region'];
/* ---------------------------------------------------------------------------
STATE β€” rides the existing profile save, like META.tutorial does. This
module never writes its own storage blob.
--------------------------------------------------------------------------- */
function primerMeta(){
if(typeof META==='undefined'||!META) return {done:false,version:0,seen:{}};
META.warPrimer=META.warPrimer||{done:false,version:0,seen:{}};
if(!META.warPrimer.seen) META.warPrimer.seen={};
return META.warPrimer;
}
function isMust(id){
return MUST_SEE.indexOf(id)>=0;
}
function mustPending(){
var M=primerMeta();
for(var i=0;i<MUST_SEE.length;i++) if(!M.seen[MUST_SEE[i]]) return true;
return false;
}
function markSeen(id){
var M=primerMeta();
M.seen=M.seen||{};
if(id) M.seen[id]=true;
}
function markOptionalSeen(){
var M=primerMeta(),changed=false,i,id;
M.seen=M.seen||{};
for(i=0;i<STAGES.length;i++){
id=STAGES[i].id;
if(!isMust(id)&&!M.seen[id]){ M.seen[id]=true; changed=true; }
}
return changed;
}
function trainedCareer(){
/* Training / KEEL skip never open the war table. Compact to must-see so
Standard is not a five-card lecture after the field guide. */
if(typeof META==='undefined'||!META||!META.tutorial) return false;
var T=META.tutorial;
return !!(T.done||T.skipped);
}
function ensureMustSeeRoute(persist){
/* A prior SKIP GUIDE set done without recording the two facts. Re-open
only those cards β€” do not rebuild the five-stage lecture. Replay sets
forceFull so a Training graduate can still walk all five on purpose. */
var M=primerMeta(),changed=false;
if(M.forceFull) return false;
if(mustPending()&&(M.done||trainedCareer())) changed=markOptionalSeen();
if(changed&&persist&&typeof metaSave==='function') metaSave();
return changed;
}
function unseenLeft(){
var M=primerMeta(),i;
for(i=0;i<STAGES.length;i++) if(!M.seen[STAGES[i].id]) return true;
return false;
}
function visibleCount(id){
var M=primerMeta(),n=0,at=0,i,sid;
for(i=0;i<STAGES.length;i++){
sid=STAGES[i].id;
if(M.seen&&M.seen[sid]&&sid!==id) continue;
n++;
if(sid===id) at=n;
}
return {at:at,n:n,last:at===n};
}
function armed(){
ensureMustSeeRoute(false);
if(mustPending()) return true;
var M=primerMeta();
return !M.done||(M.version|0)<PRIMER_VERSION;
}
function finish(){
var M=primerMeta(),i;
M.done=true; M.version=PRIMER_VERSION; M.forceFull=false;
M.seen=M.seen||{};
/* finish() is the end of the route, never a skip. Must-see are already
seen when we get here; stamp the rest so a later load does not revive them. */
for(i=0;i<STAGES.length;i++) M.seen[STAGES[i].id]=true;
if(typeof metaSave==='function') metaSave();
var card=document.getElementById('wtpCard');
if(card) card.remove();
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
if(typeof renderSettings==='function'&&shown(document.getElementById('settingsScr'))) renderSettings();
}
function replay(){
var M=primerMeta();
M.done=false; M.version=0; M.seen={}; M.forceFull=true;
if(typeof metaSave==='function') metaSave();
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
if(typeof running!=='undefined'&&running){ if(typeof toast==='function') toast('War table primer armed β€” it shows on the next deployment'); }
else if(typeof showFrontScreen==='function') showFrontScreen('setupScr');
if(typeof renderSettings==='function') renderSettings();
}
/* ---------------------------------------------------------------------------
PLACEMENT
--------------------------------------------------------------------------- */
function shown(el){
if(!el||!el.isConnected) return false;
var s=getComputedStyle(el);
if(s.display==='none'||s.visibility==='hidden') return false;
var r=el.getBoundingClientRect();
return r.width>1&&r.height>1;
}
function activePanel(){
var scr=document.getElementById('setupScr');
if(!shown(scr)) return null;
var list=scr.querySelectorAll('.mfStagePanel.on');
for(var i=0;i<list.length;i++) if(shown(list[i])) return list[i];
return null;
}
function stageIdx(id){
for(var i=0;i<STAGES.length;i++) if(STAGES[i].id===id) return i;
return -1;
}
function dismissStage(){
/* GOT IT hides THIS stage only. The old DISMISS called finish() and wiped
the rest of the route β€” so a player who cleared the galaxy card to see
the hologram never learned that Standard is the medium theatre. */
var panel=activePanel(),id=panel&&panel.dataset.stage;
markSeen(id);
if(!unseenLeft()){ finish(); return; }
if(typeof metaSave==='function') metaSave();
var card=document.getElementById('wtpCard');
if(card) card.remove();
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
}
function skipGuide(){
/* SKIP GUIDE drops the optional lecture. It must not stamp done until the
two must-see cards have actually been on screen β€” otherwise SKIP on
galaxy erases the medium-theatre card. */
var panel=activePanel(),id=panel&&panel.dataset.stage;
markSeen(id);
markOptionalSeen();
if(!mustPending()){ finish(); return; }
if(typeof metaSave==='function') metaSave();
var card=document.getElementById('wtpCard');
if(card) card.remove();
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
}
function build(S,idx,last){
var vis=visibleCount(S.id);
var d=document.createElement('aside');
d.id='wtpCard';
d.className='wtpCard'+(last?' wtpLast':'');
d.dataset.stage=S.id;
d.setAttribute('aria-label','War table orientation');
d.innerHTML='<header><span>WAR TABLE ORIENTATION</span><b>'+vis.at+' / '+vis.n+' Β· '+S.nm+'</b></header>'
+'<p>'+S.tx+'</p>'
+'<div class="wtpFoot">'
+'<button type="button" class="wtpDone">'+(last?'GOT IT Β· FINISH':'GOT IT')+'</button>'
+(last?'':'<button type="button" class="wtpSkip">SKIP GUIDE</button>')+'</div>';
var b=d.querySelector('.wtpDone');
/* mfBindTap is the codebase's touch-safe binder; fall back to click so an
OTA shell that predates it still gets a working dismiss. */
var doneFn=last?finish:dismissStage;
if(typeof mfBindTap==='function') mfBindTap(b,doneFn); else b.addEventListener('click',doneFn);
var sk=d.querySelector('.wtpSkip');
if(sk){ if(typeof mfBindTap==='function') mfBindTap(sk,skipGuide); else sk.addEventListener('click',skipGuide); }
return d;
}
function sync(){
if(!armed()){
var stale=document.getElementById('wtpCard');
if(stale) stale.remove();
return;
}
var panel=activePanel();
if(!panel) return;
var id=panel.dataset.stage,idx=stageIdx(id);
if(idx<0) return;
var M=primerMeta();
var card=document.getElementById('wtpCard');
if(M.seen&&M.seen[id]){
/* Optional stages already skipped still rebuild their panels. Drop a
leftover must-see card so it cannot ride a hidden galaxy panel. */
if(card) card.remove();
return;
}
/* Re-insert on ANY of: panel rebuilt beneath us, stage advanced, or the
card landing in a panel that is no longer the visible one. Comparing the
rendered stage against the panel's own dataset is what makes a rebuild
indistinguishable from a stage change here β€” both are just "the card
that should be on screen is not". */
if(card&&card.dataset.stage===id&&card.parentNode===panel) return;
if(card) card.remove();
var vis=visibleCount(id);
/* The guide must not displace the primary map or battle-plan action from a
short phone's first fold. Keep it in flow after that stage's first choice;
galaxy and region still show their must-see facts beside the live map. */
var anchor=id==='galaxy'?panel.querySelector('#mfConquestContinue'):
id==='region'?panel.querySelector('#mfRegionHero'):
id==='deploy'?panel.querySelector('#mfQuickSetup'):
id==='system'?panel.querySelector('.mfSystemTheatre'):
id==='planet'?panel.querySelector('.mfPlanetViewport'):null;
var next=build(STAGES[idx],idx,vis.last);
if(anchor&&anchor.parentNode===panel) anchor.insertAdjacentElement('afterend',next);
else panel.insertBefore(next,panel.firstChild);
}
/* ---------------------------------------------------------------------------
SETTINGS β€” one replay row, appended after renderSettings() rebuilds
#setList. src/game/meta.js replaces that list's entire innerHTML on every
call, so the row has to be re-appended each time rather than inserted once;
src/tutorial.js already wraps the same function for its own two rows and
this wrap simply chains onto that one.
--------------------------------------------------------------------------- */
function appendPrimerSettingsRow(){
var list=document.getElementById('setList');
if(!list||document.getElementById('wtpSetRow')) return;
var M=primerMeta(),done=!!M.done&&(M.version|0)>=PRIMER_VERSION&&!mustPending();
var row=document.createElement('div');
row.className='sItem setRow';
row.id='wtpSetRow';
row.setAttribute('role','button');
row.setAttribute('tabindex','0');
row.innerHTML='<div class="sTx"><b>πŸ—Ί War Table Primer</b><div class="sDs">'
+(done?'Seen':'Armed for the next visit')
+' β€” walks galaxy, system, planet, region and deployment</div></div>'
+'<div class="sBuy">'+(done?'β†Ί REPLAY':'ARMED')+'</div>';
var act=function(ev){ if(ev){ ev.preventDefault(); } if(done) replay(); };
row.addEventListener('pointerdown',act);
row.addEventListener('keydown',function(ev){ if(ev.key==='Enter'||ev.key===' ') act(ev); });
list.appendChild(row);
}
if(typeof renderSettings==='function'&&!window.__wtpSettingsWrapped){
window.__wtpSettingsWrapped=true;
var _wtpRenderSettings=renderSettings;
renderSettings=function(){ _wtpRenderSettings(); appendPrimerSettingsRow(); };
}
/* ---------------------------------------------------------------------------
BOOT β€” poll keeps the card alive across galaxyui innerHTML rebuilds. The
Standard entry wrap (takeover, after initGalaxyUI) and wtpOnStage hook
make the first paint immediate so SKIP / Training compact is not a 320 ms
late.
--------------------------------------------------------------------------- */
function onStandardOpen(){
ensureMustSeeRoute(true);
try{ sync(); }catch(e){}
}
function wrapStandardEntry(){
if(window.__wtpOpenWrapped) return;
var prev=window.openPlanetarySetup;
if(typeof prev!=='function') return;
window.__wtpOpenWrapped=true;
window.openPlanetarySetup=function(mode){
var r=prev.apply(this,arguments);
if(mode==='standard') onStandardOpen();
return r;
};
}
function initWarPrimer(){
if(window.__wtpInit) return;
window.__wtpInit=true;
wrapStandardEntry();
ensureMustSeeRoute(true);
setInterval(function(){ try{ sync(); }catch(e){ if(window.console) console.error('war primer',e); } },320);
}
window.initWarPrimer=initWarPrimer;
window.wtpEnsureMustSee=function(){ return ensureMustSeeRoute(true); };
window.wtpOnStage=function(){ try{ sync(); }catch(e){} };
/* Read-only inspection handle for verification scripts, matching the
__tutDebug hook src/tutorial.js already exposes. */
window.__wtpDebug=function(){ return {STAGES:STAGES,MUST_SEE:MUST_SEE,armed:armed(),
mustPending:mustPending(),trained:trainedCareer(),meta:primerMeta(),
stage:(activePanel()||{dataset:{}}).dataset.stage||'',
finish:finish,replay:replay,skipGuide:skipGuide}; };
/* SELF-INITIALISE. src/main.js (manifest index 68) lists initWarPrimer in the
init loop it runs from boot(), but this file is index 73 - so the function
did not exist yet and was SKIPPED IN SILENCE, exactly like initGalaxyUI. The
war primer has been dead in every build for the same reason, and nobody
noticed because nothing errored. Found by the new init-order gate in
tools/bundle.mjs on its very first run.
Safe here: __wtpInit makes it idempotent, and this file loads AFTER
src/galaxyui.js (71), which is the ordering its own wrapStandardEntry
comment requires. */
try{ initWarPrimer(); }catch(e){ if(window.console) console.error('initWarPrimer threw',e); }
})();