massfront-playtest / src /tutorial.js
CREATORJD's picture
Publish MASSFRONT web 1.33.33 (part 3)
3597f5a verified
Raw
History Blame Contribute Delete
118 kB
;
;
/* ============================================================================
TUTORIAL โ€” KEEL, ship liaison intelligence, Lance of Morning
----------------------------------------------------------------------------
Two jobs live in this file:
1. A first-drop tutorial, narrated in character rather than read off a
card. Every step is gated on the SAME state the rest of the game
already tracks โ€” bldLive, unit arrays, ability cooldowns, kill counts
โ€” polled from a private setInterval. Nothing here edits the frame
loop, and nothing here can desync from what actually happened: if the
player builds a Factory before a Reactor, or wins a fight before they
ever open the build menu, the step order just adapts around them
instead of stalling on a script it expected.
2. A small set of reactive barks that outlive the tutorial โ€” first base
attack, first loss, a hazard telegraph, a wave massing, the grid
running dry. Each fires at most once per match. The mechanical coach
in hud.js already tells you WHAT to fix; KEEL never repeats that,
it only ever reacts to what it means.
KEEL is written the way the dispatches are written: short, declarative,
never a pep talk. It respects that the player can read a build menu; it
just narrates the war while they do.
File ownership: this module speaks to the rest of the game entirely
through globals that already exist (toast, sfx, $, META, metaSave,
bldLive, TYPES, BT, the unit arrays) and extends exactly one function it
does not own โ€” renderSettings() โ€” using the same wrap-and-call-through
pattern src/game/commander.js and src/game/meta.js already use on
econTick() and heroXP(). Nothing here is imported; it is one more script
in the shared boot scope, loaded after meta.js and before main.js.
============================================================================ */
(function(){
/* ---------------------------------------------------------------------------
THE SCRIPT
Every line KEEL says lives here, in one place, so the voice stays one
voice. Step copy is fixed (a lesson should say the same thing every time);
reactive lines get two or three variants because a player who finishes a
dozen matches will hear the same trigger a dozen times.
--------------------------------------------------------------------------- */
var KEEL_NAME='KEEL';
var KEEL_TAG='SHIP LIAISON ยท LANCE OF MORNING';
var GUIDE_VERSION=4;
var GREETING='KEEL online โ€” ship liaison, Lance of Morning. Follow the gold signal: it marks the exact control for your next action. '+
'Tap SKIP any time; replay this guide from Settings.';
var SKIP_LINE='Understood. Find me again under Settings โ†’ Tutorial if you change your mind.';
var GRADUATION='Field orientation complete. Camera, economy, orders, logistics, formations, scouting, technology and extraction are certified. '+
'Real operations are chosen at the WAR TABLE: system, planet, region, then a site. '+
"I'll go quiet now โ€” unless something's on fire.";
/* Each step tests REAL state. `say` is shown the moment a step becomes
current; `done` fires once, the moment its test flips true โ€” but only if
it was the step on screen when that happened, so a player who does things
out of order gets fast-forwarded in silence instead of congratulated for
a step they finished five minutes ago. */
var STEPS=[
{ id:'camera', icon:'โ—‡',
say:'DRAG the battlefield to pan. PINCH to zoom and twist, or use VIEW โ†’ ROT / TILT. Move the camera once to confirm control.',
done:'Command view responding. The minimap can jump across the whole battlefield.',
test:function(){ return MATCH.sawCamera; } },
{ id:'deploy', icon:'โš“',
say:'Tap open ground to fly the carrier there. When the landing signal turns green, tap DEPLOY BASE HERE.',
done:"Hull's down, engines cold. That was the easy part.",
test:function(){ return typeof matchLive!=='undefined' && !!matchLive; } },
{ id:'commander', icon:'โ˜…',
say:'Your COMMANDER is the marked hero and a mobile builder. Tap the Commander bar at top-left to select and center it.',
done:'Commander identified. Keep it alive: losing every Commander loses the match.',
test:function(){ return typeof heroIdx==='number'&&heroIdx>=0&&!!usel[heroIdx]; } },
{ id:'pickup', icon:'โ—†',
say:'A gold SUPPLY CACHE is marked nearby. Select the Commander or Constructor, then tap beside the cache so the unit collects it.',
done:'Cache secured. Pickups can grant resources, repairs, scans, veterancy or rare strike codes.',
test:function(){ return MATCH.sawPickup; } },
/* Moved ahead of the economy steps. `power` tells the player to place a
Reactor "inside the blue grid" โ€” and the step that explains what the blue
grid is used to sit three objectives further down. */
{ id:'territory', icon:'โŒ', manual:'territoryAck', action:'GOT IT',
say:'LOGISTICS: blue grid is legal build territory. Builders extend a mobile zone; Factories and Uplinks anchor it. Silos prevent resource overflow. Tap GOT IT.',
done:'Logistics and territory rules logged.',
test:function(){ return MATCH.territoryAck; } },
{ id:'mex', icon:'โ›',
say:'Mass buys units and structures. Tap BUILD โ†’ ECONOMY โ†’ Extractor, drag its hologram onto a โ—† deposit, then tap โœ“.',
done:"Extractor placed. That's your first income line.",
test:function(){ return hasPlayerBld('mex'); } },
{ id:'power', icon:'โšก',
say:'Production also needs energy. Tap BUILD โ†’ ECONOMY โ†’ Reactor, place it on open ground inside the blue grid, then tap โœ“.',
done:'Reactor placed. Mass and energy must both stay positive while building.',
test:function(){ return hasPlayerBld('pgen')||hasPlayerBld('geo'); } },
{ id:'fac', icon:'โ–ฃ',
say:"Build a Factory under BUILD โ†’ PRODUCTION. It is this army's barracks and vehicle plant, and expands construction territory.",
done:'Factory placed. Open it to train the first field unit.',
test:function(){ return hasPlayerBld('fac'); } },
{ id:'queue', icon:'โ–ค',
say:'Tap the Factory on the map, then tap the Striker unit card. The queue line shows what is being built. Each unit also costs population โ€” the HUD counter reads pop n/1K, and 1K is this commander\'s cap.',
done:'Striker queued. Production burns mass and energy over the build time. Pop n/1K is live headcount against the 1K seat cap โ€” theatre size adds more seats, it does not raise yours.',
test:function(){ return MATCH.sawQueue; } },
{ id:'train', icon:'โš”',
say:'Keep the Factory supplied until the Striker rolls out. Tap ARMY when it appears to select every field unit.',
done:'Combat unit trained and selected. Unit cards show role, range, armor and ammunition.',
test:function(){ return MATCH.sawCombatSel&&combatCount()>MATCH.startCombat; } },
/* ORDERS ARE A GESTURE LANGUAGE AND NOTHING ON THE HUD SAYS SO.
Ground tap, enemy tap and double-tap ground write three different unit
states in orderMove()/orderAttack() (src/ui/input.js). Two of them a
player discovers by accident within a minute; the double-tap retreat is
the one nobody finds, and it is the one that saves an army โ€” which is
why this step gates on the retreat specifically rather than on any
order at all. */
{ id:'orders', icon:'โžค',
say:'Orders are gestures. SINGLE-TAP GROUND is attack-move: units advance and fight what they meet. TAP AN ENEMY and they chase it. DOUBLE-TAP OPEN GROUND to RETREAT: they drop the target and reposition without stopping to fight. Break contact once now.',
done:'Retreat confirmed โ€” double-tap is how an army leaves a fight it is losing. Single-tap remains attack-move. Stop in the command row and Hold under ORDERS both plant units where they stand: they fire at whatever enters weapon range and never chase it out. Stop is the panic button, Hold is the same order given on purpose.',
test:function(){ return MATCH.sawRetreat; } },
/* THE ONE STEP THAT TEACHES THE GAME RATHER THAN THE BUTTONS.
Every other objective here is "tap X to make Y happen", and the training
match is deliberately combat-free โ€” trainingSafety() parks the wave timers
and floors resources โ€” so a player can complete all of it without ever
making a wrong-weapon decision. Meanwhile the counter card already renders
the whole 0.45x-1.85x chip grid, splash, minimum range and shield-pierce
prose, and it opens on a 520 ms long-press that nothing ever asks for.
This step exists to make the player perform that gesture once. */
{ id:'intel', icon:'โฌข',
say:'Long-press any unit โ€” yours or the enemy\'s โ€” to open its counter card. Weapon class against armour class swings damage from 0.45x to 1.85x: KINETIC shreds LIGHT and dents HEAVY, GAUSS is the exact reverse.',
done:'Counter card read. The wrong weapon against the wrong plate is a quarter of the damage you thought you had โ€” and SONIC is the only thing in the roster that ignores Bulwark shields.',
test:function(){ return shown(document.getElementById('unitCard')); } },
{ id:'turret', icon:'โ›จ',
say:'Protect the economy before the first push. Tap BUILD โ†’ DEFENCE โ†’ Sentinel and place it on the likely approach.',
done:'Basic perimeter online. Towers buy your army time; they do not replace it. Long-ranged guns โ€” Bastions, Thumpers, Bombards โ€” also have a MIN RANGE, and an enemy inside it is an enemy they cannot shoot.',
test:function(){ return hasPlayerBld('turret'); } },
{ id:'platoon', icon:'โ…ฃ',
say:'With combat units selected, open PLATOONS and HOLD P1 to save them. Tap P1 later to recall; double-tap focuses the camera.',
done:'Platoon P1 stored. Saved groups remember membership and formation.',
test:function(){ return MATCH.sawPlatoon; } },
{ id:'formation', icon:'โŒƒ',
say:'Open ORDERS, tap the formation button, then drag the hologram on the map and release. Every marker is one unit slot.',
done:'Formation placed. Spacing keeps mixed units readable and prevents stacking.',
test:function(){ return MATCH.sawFormation; } },
{ id:'fog', icon:'โŒพ',
say:'Black fog hides enemies; grey terrain is remembered, not currently visible. Move the marked SCOUT toward the signal to reveal the sector.',
done:'Sensor contact expanded. Enemy positions remain hidden until a friendly sensor sees them.',
test:function(){ return MATCH.sawScout; } },
{ id:'attack', icon:'โžœ',
say:'Leave A-MOVE active and SINGLE-TAP unexplored ground. That is attack-move: units advance and engage contacts. DOUBLE-TAP is still retreat. Watch what actually does the killing: a unit with SPLASH on its card hits everything inside the radius, which beats raw damage against anything bunched up.',
done:'Attack-move confirmed โ€” single-tap ground fights on the way; double-tap ground breaks contact. Long-press a unit or structure for its full counter card.',
test:function(){ return MATCH.sawAttackMove; } },
{ id:'tech', icon:'โŒฌ',
say:'Field rank 3 granted for training. Build BUILD โ†’ TECH โ†’ Research Complex; it unlocks studies and banks account-level โ—† Data after battle.',
done:'Research Complex online. Its shield buffer protects studies from a sudden rush.',
test:function(){ return hasPlayerBld('techlab'); } },
{ id:'ability', icon:'โœฆ',
say:'Select the Commander, then tap BLAST in the action bar above the selection panel. Abilities cost energy and have cooldowns.',
done:'Special ability confirmed. Research and Commander levels unlock additional powers.',
test:function(){ return MATCH.usedAbility; } },
{ id:'objective', icon:'โ—Ž', manual:'objectiveAck', action:'CALL EXTRACTION',
say:'Normal operations end by their selected objective: destroy Commanders, hold territory, purge hives or survive. Training is secureโ€”tap CALL EXTRACTION.',
done:'Extraction acknowledged. Match resources, rewards and research are summarized after every operation.',
test:function(){ return MATCH.objectiveAck; } },
{ id:'cloud', icon:'โ˜', manual:'cloudAck', action:'FINISH TRAINING',
say:'Your career autosaves locally. After extraction, ACCOUNT can compare and sync a cloud save; PROFILE can export a portable .mfsave backup. Tap FINISH TRAINING.',
done:'Career storage confirmed.',
test:function(){ return MATCH.cloudAck; } }
];
var BASE_ATTACK_LINES=[
'Contact on the perimeter โ€” tap the โš  alert, top of screen, and go see for yourself.',
"Something's testing your walls. The โš  alert will take you straight there."
];
var UNIT_LOST_LINES=[
"That one's not getting up. Filed under acceptable losses โ€” keep the file short.",
'Lost one. It happens to everyone who actually fights. Keep moving.'
];
var LOW_POWER_LINE="Grid's in the red. Whatever's queued behind it just stopped moving.";
var WAVE_LINE="Enemy's massing for a real push โ€” you'll feel this one land. Get your army somewhere useful.";
var HAZARD_LINES={
vanguard:'Dust front inbound โ€” sight and speed go with it. Get exposed units under cover.',
highland:"The slope's about to become the valley floor. Clear the marked ground.",
isles:"Storm's earthing through whatever's biggest and made of metal. Spread out.",
crater:"The relic's stirring. The basin's about to go dark โ€” don't be standing in it.",
_default:"Something's inbound that isn't the enemy. Read the warning; it isn't decoration."
};
/* ---------------------------------------------------------------------------
STATE
--------------------------------------------------------------------------- */
var TUT={ active:false, stepIdx:0, doneFlags:[], shownStepIdx:-1, graduated:false, forceNext:false,
trainingMode:false, finishTimer:0 };
var trainingPrev=null, trainingLaunched=false;
/* Set only while re-entering tutSkip from its own confirmation callback, so the
dialog cannot ask twice. */
var tutSkipConfirmed=false;
/* Facts about THIS match, tracked whether or not the tutorial is running, so
a mid-match "replay" (from Settings) can credit things the player already
demonstrably knows how to do instead of waiting for them to happen again. */
var MATCH={ sawSel:false, sawCombatSel:false, sawMove:false, sawQueue:false, sawCamera:false,
sawPickup:false, sawPlatoon:false, sawFormation:false, sawAttackMove:false,sawScout:false,
sawRetreat:false,
territoryAck:false,objectiveAck:false,cloudAck:false,usedAbility:false,
startCombat:0,cameraBase:null,pickup:null,scoutIdx:-1,scoutStart:null,lastAbCool:[0,0,0,0] };
/* Reactive one-per-match latches. */
var REACT={ baseAttack:false, lowPower:false, hazard:false, unitLost:false, wave:false,
lastAlarmT:0, lastStallE:false, lastHazWarn:false, lastWarned:false };
var prevDropping=false;
function tutMeta(){
if(typeof META==='undefined'||!META) return {done:false,skipped:false,version:0};
META.tutorial=META.tutorial||{done:false,skipped:false,version:0};
/* `progress` is additive save metadata: old profiles load exactly as before,
while the briefing can distinguish new, interrupted and completed runs.
It is a high-water mark only; the live-state gates below remain the
authority whenever a mission is running. */
if(typeof META.tutorial.progress!=='number') META.tutorial.progress=META.tutorial.done?STEPS.length:0;
META.tutorial.progress=Math.max(0,Math.min(STEPS.length,META.tutorial.progress|0));
return META.tutorial;
}
function pick(arr){ return arr[(Math.random()*arr.length)|0]; }
function hasPlayerBld(type){
if(typeof bldLive==='undefined') return false;
for(var i=0;i<bldLive.length;i++){
var B=bldLive[i];
/* The lesson is about making a valid placement. Waiting through the full
construction timer left the highlight pointing back at BUILD after the
player had already done the requested action, which looked like a loop. */
if(B.alive!==false && B.team===0 && B.type===type) return true;
}
return false;
}
function playerBld(type){
if(typeof bldLive==='undefined') return null;
for(var i=0;i<bldLive.length;i++) if(bldLive[i].alive!==false&&bldLive[i].team===0&&bldLive[i].type===type) return bldLive[i];
return null;
}
function isCombatUnit(i){
return typeof ualive!=='undefined'&&ualive[i]&&uteam[i]===0&&i!==heroIdx&&utype[i]!==19;
}
function hasCombatUnit(){
if(typeof unitHigh!=='number') return false;
for(var i=0;i<unitHigh;i++) if(isCombatUnit(i)) return true;
return false;
}
function combatCount(){
if(typeof unitHigh!=='number') return 0;
var n=0;for(var i=0;i<unitHigh;i++)if(isCombatUnit(i))n++;return n;
}
function hasSelectedCombatUnit(){
if(typeof unitHigh!=='number') return false;
for(var i=0;i<unitHigh;i++) if(isCombatUnit(i)&&usel[i]) return true;
return false;
}
function sawQueuedUnit(){
if(typeof bldLive==='undefined') return false;
/* A queued item stays at B.queue[0] for its full build time (>=1.1s for the
cheapest unit โ€” see TYPES[0].bt) and is only shifted off on completion,
so a non-empty queue is never a one-frame flicker: it is guaranteed to be
visible across many polls of this timer, with no need for a population
backstop that would have to guess at a baseline. */
for(var i=0;i<bldLive.length;i++){
var B=bldLive[i];
if(B.team===0 && B.queue && B.queue.length>0 &&
(B.type==='fac'||B.type==='tgate'||B.type==='harbor'||B.type==='airfield')) return true;
}
return false;
}
/* ---------------------------------------------------------------------------
THE BUBBLE โ€” one element, two moods. "Speaking" (a fresh line just landed,
full emphasis, brief pulse) settles into "resting" (the current step's
objective, held at lower emphasis) once its hold time elapses. Reactive
barks are not tied to a step: they interrupt, hold briefly, then hand back
to whatever the resting text was (or hide, if the tutorial isn't running).
--------------------------------------------------------------------------- */
var queue=[], holdUntil=0, restingText='', restingTag='', restingId='', lastText=null, domReady=false;
var focusEls=[], mapCue=null;
function buildDOM(){
if(domReady) return;
domReady=true;
var wrap=document.createElement('div');
wrap.id='keelWrap';
wrap.setAttribute('aria-live','polite');
wrap.innerHTML=
'<div id="keelBar">'+
'<div id="keelBadge" aria-hidden="true"><span class="kr kr1"></span><span class="kr kr2"></span><span class="kd"></span></div>'+
'<div id="keelBody">'+
'<div id="keelHd"><span id="keelName" title="'+KEEL_TAG+'">'+KEEL_NAME+'</span><span id="keelStepTag"></span></div>'+
'<div id="keelTxt"></div>'+
'<div id="keelProgress" role="progressbar" aria-label="Training objective progress" aria-valuemin="0"><i></i></div>'+
'<button id="keelNext" type="button">GOT IT</button>'+
'</div>'+
'<button id="keelSkip" type="button" aria-label="Skip tutorial">SKIP</button>'+
'</div>';
document.body.appendChild(wrap);
var cue=document.createElement('div');
cue.id='keelMapCue'; cue.setAttribute('aria-hidden','true'); cue.innerHTML='<span></span>';
document.body.appendChild(cue);
var mode=document.createElement('div');
mode.id='keelModeBadge'; mode.setAttribute('aria-live','polite');
mode.innerHTML='<b>TRAINING OPERATION</b><span>KEEL GUIDANCE ACTIVE</span>';
document.body.appendChild(mode);
var skipBtn=document.getElementById('keelSkip');
skipBtn.addEventListener('pointerdown',function(ev){ ev.stopPropagation(); tutSkip(); });
/* A control that abandons a match should not be styled as a hint. In a
training match the label becomes END and the button takes the warning
treatment, so the destructive one is never the quiet one. */
skipBtn.classList.toggle('keelSkipEnd',!!TUT.trainingMode);
skipBtn.textContent=TUT.trainingMode?'END':'SKIP';
var nextBtn=document.getElementById('keelNext');
nextBtn.addEventListener('pointerdown',function(ev){
ev.preventDefault(); ev.stopPropagation();
var S=TUT.active&&STEPS[TUT.stepIdx];
if(!S||!S.manual) return;
MATCH[S.manual]=true;
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
});
/* Tapping the body re-shows the full-strength card if it has settled into
its dim resting state โ€” a quiet "say that again" without extra chrome. */
document.getElementById('keelBar').addEventListener('pointerdown',function(ev){
if(ev.target===skipBtn||ev.target===nextBtn) return;
if(restingText) speak(restingText,4.2,'recall',restingId);
});
mapCue=cue;
}
function renderBubble(text,tag){
if(!domReady) return;
if(text!==lastText){ var t=document.getElementById('keelTxt'); if(t) t.textContent=text; lastText=text; }
var tg=document.getElementById('keelStepTag'); if(tg) tg.textContent=tag||'';
var pg=document.getElementById('keelProgress');
if(pg){
var at=Math.min(STEPS.length,TUT.stepIdx),pct=STEPS.length?at/STEPS.length*100:0;
pg.setAttribute('aria-valuemax',STEPS.length);pg.setAttribute('aria-valuenow',at);
var fill=pg.querySelector('i');if(fill)fill.style.width=pct+'%';
}
}
function showWrap(fresh){
if(!domReady) return;
var w=document.getElementById('keelWrap');
if(w) w.classList.add('show');
var bar=document.getElementById('keelBar');
if(bar){
bar.classList.toggle('resting',!fresh);
if(fresh){ bar.classList.remove('pulse'); void bar.offsetWidth; bar.classList.add('pulse'); }
}
}
function hideWrap(){
if(!domReady) return;
var w=document.getElementById('keelWrap');
if(w) w.classList.remove('show');
}
/* ---------------------------------------------------------------------------
CONTEXT SIGNAL โ€” the copy names a control and this signal marks that exact
control. It is recalculated every poll because BUILD changes from button โ†’
tab โ†’ card โ†’ world placement without changing tutorial steps. The signal is
purely visual (pointer-events:none), so it can never become an input shield.
--------------------------------------------------------------------------- */
function shown(el){
if(!el||!el.isConnected) return false;
var s=getComputedStyle(el),r=el.getBoundingClientRect();
return s.display!=='none'&&s.visibility!=='hidden'&&r.width>1&&r.height>1;
}
function byText(rootId,selector,text){
var root=document.getElementById(rootId); if(!root) return null;
var list=root.querySelectorAll(selector),want=String(text).toUpperCase();
for(var i=0;i<list.length;i++) if((list[i].textContent||'').toUpperCase().indexOf(want)>=0) return list[i];
return null;
}
function clearFocus(){
for(var i=0;i<focusEls.length;i++){
focusEls[i].classList.remove('keelFocus');
if(focusEls[i].getAttribute('aria-describedby')==='keelTxt') focusEls[i].removeAttribute('aria-describedby');
}
focusEls.length=0;
if(mapCue){ mapCue.classList.remove('show'); mapCue.style.removeProperty('left'); mapCue.style.removeProperty('top'); }
}
function focusEl(el){
if(!shown(el)||focusEls.indexOf(el)>=0) return;
el.classList.add('keelFocus'); el.setAttribute('aria-describedby','keelTxt'); focusEls.push(el);
}
function cueAt(x,y,label){
if(!mapCue) return;
var pad=62,bot=typeof VH==='number'?Math.max(210,VH*.25):220;
x=Math.max(pad,Math.min((typeof VW==='number'?VW:innerWidth)-pad,x));
y=Math.max(170,Math.min((typeof VH==='number'?VH:innerHeight)-bot,y));
mapCue.style.left=x+'px'; mapCue.style.top=y+'px';
var t=mapCue.querySelector('span'); if(t) t.textContent=label||'TAP MAP';
mapCue.classList.add('show');
}
function cueWorld(x,y,label){
try{ var p=w2s(x,y); cueAt(p[0],p[1],label); }
catch(e){ cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.5,label); }
}
function buildFocus(cat,name,type){
if(typeof placing!=='undefined'&&placing&&placing.type===type){
var valid=false;
try{ valid=!!placementValid(); }catch(e){}
if(valid) focusEl(document.getElementById('placeOk'));
if(type==='mex'&&!valid&&typeof deposits!=='undefined'){
var best=null,bd=Infinity;
for(var i=0;i<deposits.length;i++) if(!deposits[i].taken){
var d=placing?Math.hypot(deposits[i].x-placing.x,deposits[i].y-placing.y):i;
if(d<bd){bd=d;best=deposits[i];}
}
if(best) cueWorld(best.x,best.y,'โ—† DEPOSIT');
} else cueWorld(placing.x,placing.y,valid?'TAP โœ“':'BLUE GRID');
return;
}
var bm=document.getElementById('buildMenu');
if(shown(bm)){
var tab=byText('buildTabs','.tabBtn',cat);
var card=byText('buildGrid','.bcard',name);
if(card) focusEl(card); else if(tab) focusEl(tab);
return;
}
focusEl(document.getElementById('buildBtn'));
}
function syncFocus(){
clearFocus();
var badge=document.getElementById('keelModeBadge');
/* The KEEL card already carries STEP / TOTAL and the current objective.
Repeating that information in a second fixed badge consumed the same
narrow phone lane as the goal bar and rank row, creating the exact stack
the tutorial is meant to teach through. Keep the legacy node for old OTA
shells, but it is no longer a visible surface. */
if(badge) badge.classList.remove('show');
if(!TUT.active||TUT.stepIdx>=STEPS.length) return;
var S=STEPS[TUT.stepIdx],next=document.getElementById('keelNext');
if(badge){
var bs=badge.querySelector('span'); if(bs) bs.textContent='OBJECTIVE '+(TUT.stepIdx+1)+' / '+STEPS.length+' ยท '+S.id.toUpperCase();
}
if(next){
next.style.display=S.manual?'inline-flex':'none';next.textContent=S.action||'GOT IT';
next.setAttribute('aria-label',(S.action||'Confirm')+' โ€” '+S.id+' lesson');
}
if(S.id==='camera'){
focusEl(document.querySelector('.hudDeckBtn[data-deck="view"]'));
focusEl(document.getElementById('rotL'));focusEl(document.getElementById('zoomIn'));focusEl(document.getElementById('tiltBtn'));focusEl(document.getElementById('zoomOut'));focusEl(document.getElementById('rotR'));
cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.52,'DRAG / PINCH');
} else if(S.id==='deploy'){
var dep=document.getElementById('deployBtn');
if(shown(dep)) focusEl(dep); else cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.52,'TAP LANDING SITE');
} else if(S.id==='commander'){
focusEl(document.getElementById('heroBar'));
} else if(S.id==='pickup'){
if(MATCH.pickup&&crates.indexOf(MATCH.pickup)>=0)cueWorld(MATCH.pickup.x,MATCH.pickup.y,'โ—† COLLECT CACHE');
focusEl(document.getElementById('heroBar'));
} else if(S.id==='mex') buildFocus('ECONOMY','Extractor','mex');
else if(S.id==='power') buildFocus('ECONOMY','Reactor','pgen');
else if(S.id==='fac') buildFocus('PRODUCTION','Factory','fac');
else if(S.id==='territory'){
focusEl(document.getElementById('buildBtn'));
var H=playerBld('hq');
if(H) cueWorld(H.x,H.y,'BLUE BUILD GRID');
} else if(S.id==='queue'){
focusEl(document.getElementById('unitRes'));
var pm=document.getElementById('prodMenu');
if(shown(pm)) focusEl(byText('prodGrid','.bcard','Striker')||pm.querySelector('.bcard'));
else { var F=playerBld('fac'); if(F) cueWorld(F.x,F.y,'TAP FACTORY'); }
} else if(S.id==='train'){
if(hasCombatUnit()) focusEl(document.getElementById('armyBtn'));
else { var F2=playerBld('fac'); if(F2) cueWorld(F2.x,F2.y,'UNIT BUILDING'); }
} else if(S.id==='orders'){
/* No button teaches this one โ€” the cue has to sit on the ground the
player is being asked to double-tap, offset clear of the units so the
marker is not sitting under the very gesture it is asking for. */
var ox=0,oy=0,on=0;
if(typeof unitHigh==='number') for(var q=0;q<unitHigh;q++) if(isCombatUnit(q)&&usel[q]){ox+=ux[q];oy+=uy[q];on++;}
if(on) cueWorld(ox/on-170,oy/on+130,'DOUBLE-TAP = RETREAT');
else focusEl(document.getElementById('armyBtn'));
} else if(S.id==='turret') buildFocus('DEFENCE','Sentinel','turret');
else if(S.id==='platoon'){
if(shown(document.getElementById('grpRow')))focusEl(document.getElementById('grpBtn1'));
else focusEl(document.querySelector('.hudDeckBtn[data-deck="platoons"]'));
} else if(S.id==='formation'){
if(shown(document.getElementById('tacRow'))){
focusEl(document.getElementById('formBtn'));
if(orderPreview)cueWorld(orderPreview.x,orderPreview.y,'DRAG FORMATION');
} else focusEl(document.querySelector('.hudDeckBtn[data-deck="orders"]'));
} else if(S.id==='attack'){
if(shown(document.getElementById('tacRow')))focusEl(document.getElementById('moveBtn'));
else focusEl(document.querySelector('.hudDeckBtn[data-deck="orders"]'));
var sx=0,sy=0,n=0;
if(typeof unitHigh==='number') for(var j=0;j<unitHigh;j++) if(isCombatUnit(j)&&usel[j]){sx+=ux[j];sy+=uy[j];n++;}
if(n) cueWorld(sx/n+150,sy/n-110,'SINGLE-TAP = A-MOVE');
else focusEl(document.getElementById('armyBtn'));
} else if(S.id==='fog'){
focusEl(document.getElementById('minimapWrap'));
if(MATCH.scoutIdx>=0&&ualive[MATCH.scoutIdx])cueWorld(ux[MATCH.scoutIdx]+190,uy[MATCH.scoutIdx]-150,'SCOUT THIS SECTOR');
} else if(S.id==='tech') buildFocus('TECH','Research Complex','techlab');
else if(S.id==='ability'){
focusEl(document.getElementById('heroBar'));
if(typeof heroIdx==='number'&&heroIdx>=0&&ualive[heroIdx])cueWorld(ux[heroIdx]+105,uy[heroIdx]-70,'BLAST TARGET');
} else if(S.id==='objective'||S.id==='cloud'){
focusEl(next);
}
}
/* Capped so a burst of state changes (several buildings finishing within the
same poll, or a mid-match replay that silently back-fills a lot of
already-true steps) can never queue up a minute of stale dialogue. Once
the backlog is deep, older lines are dropped in favour of newer ones โ€” the
step the player is looking at right now matters more than a "done" bark
for something they finished three actions ago. */
var MAX_QUEUE=3;
/* `id` is the AUTHORED voice key for this line (see keenLineId). It travels with
the line through the queue instead of being reconstructed from the copy on the
far side โ€” reconstructing it is what let the gate and the player disagree. */
function speak(text,holdSec,kind,id){
if(!text) return;
queue.push({text:text,hold:holdSec||4.4,kind:kind||'bark',id:id||null});
while(queue.length>MAX_QUEUE) queue.shift();
}
function pump(){
var now=performance.now()/1000;
if(now<holdUntil) return;
if(queue.length){
var m=queue.shift();
renderBubble(m.text,restingTag);
/* Hold time compresses while a backlog remains, so a burst of catch-up
lines still reads in order without pinning the HUD for the better
part of a minute. */
holdUntil=now+(queue.length? Math.min(m.hold,2.4) : m.hold);
showWrap(true);
/* Pass the line's identity through, so a rendered take can be found for it
instead of every line silently falling through to synthesis. */
speakVoice(m.text,'keen',keenLineId(m.kind,m.id));
return;
}
if(TUT.active && restingText){
renderBubble(restingText,restingTag);
showWrap(false);
} else hideWrap();
}
/* ---------------------------------------------------------------------------
VOICE โ€” opt-in, best-effort, silent when unsupported. Never awaited, never
on the hot path: a browser with no speechSynthesis (or one that throws)
behaves exactly like voice is off.
--------------------------------------------------------------------------- */
/* KEEN WAS MUTE FOR THREE REASONS AT ONCE.
1. This gate required `'speechSynthesis' in window` โ€” the very dependency the
voice rework existed to remove. A device with a downloaded voice pack and
no TTS engine (ordinary on Android WebView) reported voice as unavailable
and never even looked at the pack.
2. pump() called speakVoice(text) with no action, so the `voPlay` branch was
unreachable and every line fell through to speech synthesis regardless.
3. On the platforms that DO expose speechSynthesis, an Android WebView with
no installed voice makes speak() a silent no-op, so the fallback failed
quietly too โ€” which is why this reads as "the voice doesn't work" rather
than as an error.
Voice is now on when EITHER path can carry it. */
function voiceOn(){
try{
if(!(typeof META!=='undefined' && META.settings && META.settings.tutorialVoice)) return false;
/* voHas asks the BANK MANIFEST; voReady asks whether the buffer is decoded
yet. Gating on voReady alone made the answer "no voice available" for the
whole of a cold start, because nothing is decoded until something asks for
it โ€” and this gate is the thing that asks. */
if(typeof voHas==='function' && voHas('keen','greeting')) return true;
if(typeof voReady==='function' && voReady('keen','greeting')) return true;
return 'speechSynthesis' in window;
}catch(e){ return false; }
}
/* A stable id per scripted line, so a rendered take can be looked up for it.
KEEN's script is FIXED โ€” every step's say/done, the greeting, the graduation
and the reactive barks are constants โ€” which is precisely what makes them
renderable offline the same way the command acks already are.
THE ID IS AUTHORED AT THE CALL SITE. It must match an action key under
`lines.keen` in data:application/json;base64,ewogIl9ub3RlIjogIkdFTkVSQVRFRCBieSB0b29scy9tYWtlLXZvaWNlcy5weSDigJQgZG8gbm90IGhhbmQtZWRpdC4gUmUtcnVuIHRoZSB0b29sOyBzZWUgZG9jcy9WT0lDRS1QSVBFTElORS5tZC4iLAogImdlbmVyYXRlZCI6ICIyMDI2LTA4LTA5VDAzOjA4OjEzWiIsCiAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogInZvaWNlcyI6IHsKICAiYXNjZW5kYW5jeSI6ICJibV9nZW9yZ2UiLAogICJob3JkZSI6ICJibV9mYWJsZSIsCiAgImtlZW4iOiAiYWZfaGVhcnQiLAogICJub3ZhIjogImFtX21pY2hhZWwiLAogICJzeW5kaWNhdGUiOiAiYWZfYmVsbGEiCiB9LAogImxpbmVzIjogewogICJhc2NlbmRhbmN5IjogewogICAiYWJpbGl0eSI6IFsKICAgICJhc2NlbmRhbmN5X2FiaWxpdHlfMCIsCiAgICAiYXNjZW5kYW5jeV9hYmlsaXR5XzEiLAogICAgImFzY2VuZGFuY3lfYWJpbGl0eV8yIgogICBdLAogICAiYXR0YWNrIjogWwogICAgImFzY2VuZGFuY3lfYXR0YWNrXzAiLAogICAgImFzY2VuZGFuY3lfYXR0YWNrXzEiLAogICAgImFzY2VuZGFuY3lfYXR0YWNrXzIiCiAgIF0sCiAgICJidWlsZCI6IFsKICAgICJhc2NlbmRhbmN5X2J1aWxkXzAiLAogICAgImFzY2VuZGFuY3lfYnVpbGRfMSIsCiAgICAiYXNjZW5kYW5jeV9idWlsZF8yIgogICBdLAogICAiZGVwbG95IjogWwogICAgImFzY2VuZGFuY3lfZGVwbG95XzAiLAogICAgImFzY2VuZGFuY3lfZGVwbG95XzEiLAogICAgImFzY2VuZGFuY3lfZGVwbG95XzIiCiAgIF0sCiAgICJob2xkIjogWwogICAgImFzY2VuZGFuY3lfaG9sZF8wIiwKICAgICJhc2NlbmRhbmN5X2hvbGRfMSIsCiAgICAiYXNjZW5kYW5jeV9ob2xkXzIiCiAgIF0sCiAgICJtb3ZlIjogWwogICAgImFzY2VuZGFuY3lfbW92ZV8wIiwKICAgICJhc2NlbmRhbmN5X21vdmVfMSIsCiAgICAiYXNjZW5kYW5jeV9tb3ZlXzIiCiAgIF0sCiAgICJwYXRyb2wiOiBbCiAgICAiYXNjZW5kYW5jeV9wYXRyb2xfMCIsCiAgICAiYXNjZW5kYW5jeV9wYXRyb2xfMSIsCiAgICAiYXNjZW5kYW5jeV9wYXRyb2xfMiIKICAgXSwKICAgInNlbGVjdCI6IFsKICAgICJhc2NlbmRhbmN5X3NlbGVjdF8wIiwKICAgICJhc2NlbmRhbmN5X3NlbGVjdF8xIiwKICAgICJhc2NlbmRhbmN5X3NlbGVjdF8yIgogICBdLAogICAic3RvcCI6IFsKICAgICJhc2NlbmRhbmN5X3N0b3BfMCIsCiAgICAiYXNjZW5kYW5jeV9zdG9wXzEiLAogICAgImFzY2VuZGFuY3lfc3RvcF8yIgogICBdCiAgfSwKICAiaG9yZGUiOiB7CiAgICJhYmlsaXR5IjogWwogICAgImhvcmRlX2FiaWxpdHlfMCIsCiAgICAiaG9yZGVfYWJpbGl0eV8xIiwKICAgICJob3JkZV9hYmlsaXR5XzIiCiAgIF0sCiAgICJhdHRhY2siOiBbCiAgICAiaG9yZGVfYXR0YWNrXzAiLAogICAgImhvcmRlX2F0dGFja18xIiwKICAgICJob3JkZV9hdHRhY2tfMiIKICAgXSwKICAgImJ1aWxkIjogWwogICAgImhvcmRlX2J1aWxkXzAiLAogICAgImhvcmRlX2J1aWxkXzEiLAogICAgImhvcmRlX2J1aWxkXzIiCiAgIF0sCiAgICJkZXBsb3kiOiBbCiAgICAiaG9yZGVfZGVwbG95XzAiLAogICAgImhvcmRlX2RlcGxveV8xIiwKICAgICJob3JkZV9kZXBsb3lfMiIKICAgXSwKICAgImhvbGQiOiBbCiAgICAiaG9yZGVfaG9sZF8wIiwKICAgICJob3JkZV9ob2xkXzEiLAogICAgImhvcmRlX2hvbGRfMiIKICAgXSwKICAgIm1vdmUiOiBbCiAgICAiaG9yZGVfbW92ZV8wIiwKICAgICJob3JkZV9tb3ZlXzEiLAogICAgImhvcmRlX21vdmVfMiIKICAgXSwKICAgInBhdHJvbCI6IFsKICAgICJob3JkZV9wYXRyb2xfMCIsCiAgICAiaG9yZGVfcGF0cm9sXzEiLAogICAgImhvcmRlX3BhdHJvbF8yIgogICBdLAogICAic2VsZWN0IjogWwogICAgImhvcmRlX3NlbGVjdF8wIiwKICAgICJob3JkZV9zZWxlY3RfMSIsCiAgICAiaG9yZGVfc2VsZWN0XzIiCiAgIF0sCiAgICJzdG9wIjogWwogICAgImhvcmRlX3N0b3BfMCIsCiAgICAiaG9yZGVfc3RvcF8xIiwKICAgICJob3JkZV9zdG9wXzIiCiAgIF0KICB9LAogICJrZWVuIjogewogICAiZG9uZV9hYmlsaXR5IjogWwogICAgImtlZW5fZG9uZV9hYmlsaXR5IgogICBdLAogICAiZG9uZV9hdHRhY2siOiBbCiAgICAia2Vlbl9kb25lX2F0dGFjayIKICAgXSwKICAgImRvbmVfY2FtZXJhIjogWwogICAgImtlZW5fZG9uZV9jYW1lcmEiCiAgIF0sCiAgICJkb25lX2Nsb3VkIjogWwogICAgImtlZW5fZG9uZV9jbG91ZCIKICAgXSwKICAgImRvbmVfY29tbWFuZGVyIjogWwogICAgImtlZW5fZG9uZV9jb21tYW5kZXIiCiAgIF0sCiAgICJkb25lX2RlcGxveSI6IFsKICAgICJrZWVuX2RvbmVfZGVwbG95IgogICBdLAogICAiZG9uZV9mYWMiOiBbCiAgICAia2Vlbl9kb25lX2ZhYyIKICAgXSwKICAgImRvbmVfZm9nIjogWwogICAgImtlZW5fZG9uZV9mb2ciCiAgIF0sCiAgICJkb25lX2Zvcm1hdGlvbiI6IFsKICAgICJrZWVuX2RvbmVfZm9ybWF0aW9uIgogICBdLAogICAiZG9uZV9pbnRlbCI6IFsKICAgICJrZWVuX2RvbmVfaW50ZWwiCiAgIF0sCiAgICJkb25lX21leCI6IFsKICAgICJrZWVuX2RvbmVfbWV4IgogICBdLAogICAiZG9uZV9vYmplY3RpdmUiOiBbCiAgICAia2Vlbl9kb25lX29iamVjdGl2ZSIKICAgXSwKICAgImRvbmVfcGlja3VwIjogWwogICAgImtlZW5fZG9uZV9waWNrdXAiCiAgIF0sCiAgICJkb25lX3BsYXRvb24iOiBbCiAgICAia2Vlbl9kb25lX3BsYXRvb24iCiAgIF0sCiAgICJkb25lX3Bvd2VyIjogWwogICAgImtlZW5fZG9uZV9wb3dlciIKICAgXSwKICAgImRvbmVfcXVldWUiOiBbCiAgICAia2Vlbl9kb25lX3F1ZXVlIgogICBdLAogICAiZG9uZV90ZWNoIjogWwogICAgImtlZW5fZG9uZV90ZWNoIgogICBdLAogICAiZG9uZV90ZXJyaXRvcnkiOiBbCiAgICAia2Vlbl9kb25lX3RlcnJpdG9yeSIKICAgXSwKICAgImRvbmVfdHJhaW4iOiBbCiAgICAia2Vlbl9kb25lX3RyYWluIgogICBdLAogICAiZG9uZV90dXJyZXQiOiBbCiAgICAia2Vlbl9kb25lX3R1cnJldCIKICAgXSwKICAgImdyYWR1YXRpb24iOiBbCiAgICAia2Vlbl9ncmFkdWF0aW9uIgogICBdLAogICAiZ3JlZXRpbmciOiBbCiAgICAia2Vlbl9ncmVldGluZyIKICAgXSwKICAgInJlYWN0X2Jhc2VfYXR0YWNrMCI6IFsKICAgICJrZWVuX3JlYWN0X2Jhc2VfYXR0YWNrMCIKICAgXSwKICAgInJlYWN0X2Jhc2VfYXR0YWNrMSI6IFsKICAgICJrZWVuX3JlYWN0X2Jhc2VfYXR0YWNrMSIKICAgXSwKICAgInJlYWN0X2hhemFyZF9jcmF0ZXIiOiBbCiAgICAia2Vlbl9yZWFjdF9oYXphcmRfY3JhdGVyIgogICBdLAogICAicmVhY3RfaGF6YXJkX2RlZmF1bHQiOiBbCiAgICAia2Vlbl9yZWFjdF9oYXphcmRfZGVmYXVsdCIKICAgXSwKICAgInJlYWN0X2hhemFyZF9oaWdobGFuZCI6IFsKICAgICJrZWVuX3JlYWN0X2hhemFyZF9oaWdobGFuZCIKICAgXSwKICAgInJlYWN0X2hhemFyZF9pc2xlcyI6IFsKICAgICJrZWVuX3JlYWN0X2hhemFyZF9pc2xlcyIKICAgXSwKICAgInJlYWN0X2hhemFyZF92YW5ndWFyZCI6IFsKICAgICJrZWVuX3JlYWN0X2hhemFyZF92YW5ndWFyZCIKICAgXSwKICAgInJlYWN0X2xvd19wb3dlciI6IFsKICAgICJrZWVuX3JlYWN0X2xvd19wb3dlciIKICAgXSwKICAgInJlYWN0X3VuaXRfbG9zdDAiOiBbCiAgICAia2Vlbl9yZWFjdF91bml0X2xvc3QwIgogICBdLAogICAicmVhY3RfdW5pdF9sb3N0MSI6IFsKICAgICJrZWVuX3JlYWN0X3VuaXRfbG9zdDEiCiAgIF0sCiAgICJyZWFjdF93YXZlIjogWwogICAgImtlZW5fcmVhY3Rfd2F2ZSIKICAgXSwKICAgInNraXAiOiBbCiAgICAia2Vlbl9za2lwIgogICBdLAogICAic3RlcF9hYmlsaXR5IjogWwogICAgImtlZW5fc3RlcF9hYmlsaXR5IgogICBdLAogICAic3RlcF9hdHRhY2siOiBbCiAgICAia2Vlbl9zdGVwX2F0dGFjayIKICAgXSwKICAgInN0ZXBfY2FtZXJhIjogWwogICAgImtlZW5fc3RlcF9jYW1lcmEiCiAgIF0sCiAgICJzdGVwX2Nsb3VkIjogWwogICAgImtlZW5fc3RlcF9jbG91ZCIKICAgXSwKICAgInN0ZXBfY29tbWFuZGVyIjogWwogICAgImtlZW5fc3RlcF9jb21tYW5kZXIiCiAgIF0sCiAgICJzdGVwX2RlcGxveSI6IFsKICAgICJrZWVuX3N0ZXBfZGVwbG95IgogICBdLAogICAic3RlcF9mYWMiOiBbCiAgICAia2Vlbl9zdGVwX2ZhYyIKICAgXSwKICAgInN0ZXBfZm9nIjogWwogICAgImtlZW5fc3RlcF9mb2ciCiAgIF0sCiAgICJzdGVwX2Zvcm1hdGlvbiI6IFsKICAgICJrZWVuX3N0ZXBfZm9ybWF0aW9uIgogICBdLAogICAic3RlcF9pbnRlbCI6IFsKICAgICJrZWVuX3N0ZXBfaW50ZWwiCiAgIF0sCiAgICJzdGVwX21leCI6IFsKICAgICJrZWVuX3N0ZXBfbWV4IgogICBdLAogICAic3RlcF9vYmplY3RpdmUiOiBbCiAgICAia2Vlbl9zdGVwX29iamVjdGl2ZSIKICAgXSwKICAgInN0ZXBfcGlja3VwIjogWwogICAgImtlZW5fc3RlcF9waWNrdXAiCiAgIF0sCiAgICJzdGVwX3BsYXRvb24iOiBbCiAgICAia2Vlbl9zdGVwX3BsYXRvb24iCiAgIF0sCiAgICJzdGVwX3Bvd2VyIjogWwogICAgImtlZW5fc3RlcF9wb3dlciIKICAgXSwKICAgInN0ZXBfcXVldWUiOiBbCiAgICAia2Vlbl9zdGVwX3F1ZXVlIgogICBdLAogICAic3RlcF90ZWNoIjogWwogICAgImtlZW5fc3RlcF90ZWNoIgogICBdLAogICAic3RlcF90ZXJyaXRvcnkiOiBbCiAgICAia2Vlbl9zdGVwX3RlcnJpdG9yeSIKICAgXSwKICAgInN0ZXBfdHJhaW4iOiBbCiAgICAia2Vlbl9zdGVwX3RyYWluIgogICBdLAogICAic3RlcF90dXJyZXQiOiBbCiAgICAia2Vlbl9zdGVwX3R1cnJldCIKICAgXQogIH0sCiAgIm5vdmEiOiB7CiAgICJhYmlsaXR5IjogWwogICAgIm5vdmFfYWJpbGl0eV8wIiwKICAgICJub3ZhX2FiaWxpdHlfMSIsCiAgICAibm92YV9hYmlsaXR5XzIiCiAgIF0sCiAgICJhdHRhY2siOiBbCiAgICAibm92YV9hdHRhY2tfMCIsCiAgICAibm92YV9hdHRhY2tfMSIsCiAgICAibm92YV9hdHRhY2tfMiIKICAgXSwKICAgImJ1aWxkIjogWwogICAgIm5vdmFfYnVpbGRfMCIsCiAgICAibm92YV9idWlsZF8xIiwKICAgICJub3ZhX2J1aWxkXzIiCiAgIF0sCiAgICJkZXBsb3kiOiBbCiAgICAibm92YV9kZXBsb3lfMCIsCiAgICAibm92YV9kZXBsb3lfMSIsCiAgICAibm92YV9kZXBsb3lfMiIKICAgXSwKICAgImhvbGQiOiBbCiAgICAibm92YV9ob2xkXzAiLAogICAgIm5vdmFfaG9sZF8xIiwKICAgICJub3ZhX2hvbGRfMiIKICAgXSwKICAgIm1vdmUiOiBbCiAgICAibm92YV9tb3ZlXzAiLAogICAgIm5vdmFfbW92ZV8xIiwKICAgICJub3ZhX21vdmVfMiIKICAgXSwKICAgInBhdHJvbCI6IFsKICAgICJub3ZhX3BhdHJvbF8wIiwKICAgICJub3ZhX3BhdHJvbF8xIiwKICAgICJub3ZhX3BhdHJvbF8yIgogICBdLAogICAic2VsZWN0IjogWwogICAgIm5vdmFfc2VsZWN0XzAiLAogICAgIm5vdmFfc2VsZWN0XzEiLAogICAgIm5vdmFfc2VsZWN0XzIiCiAgIF0sCiAgICJzdG9wIjogWwogICAgIm5vdmFfc3RvcF8wIiwKICAgICJub3ZhX3N0b3BfMSIsCiAgICAibm92YV9zdG9wXzIiCiAgIF0KICB9LAogICJzeW5kaWNhdGUiOiB7CiAgICJhYmlsaXR5IjogWwogICAgInN5bmRpY2F0ZV9hYmlsaXR5XzAiLAogICAgInN5bmRpY2F0ZV9hYmlsaXR5XzEiLAogICAgInN5bmRpY2F0ZV9hYmlsaXR5XzIiCiAgIF0sCiAgICJhdHRhY2siOiBbCiAgICAic3luZGljYXRlX2F0dGFja18wIiwKICAgICJzeW5kaWNhdGVfYXR0YWNrXzEiLAogICAgInN5bmRpY2F0ZV9hdHRhY2tfMiIKICAgXSwKICAgImJ1aWxkIjogWwogICAgInN5bmRpY2F0ZV9idWlsZF8wIiwKICAgICJzeW5kaWNhdGVfYnVpbGRfMSIsCiAgICAic3luZGljYXRlX2J1aWxkXzIiCiAgIF0sCiAgICJkZXBsb3kiOiBbCiAgICAic3luZGljYXRlX2RlcGxveV8wIiwKICAgICJzeW5kaWNhdGVfZGVwbG95XzEiLAogICAgInN5bmRpY2F0ZV9kZXBsb3lfMiIKICAgXSwKICAgImhvbGQiOiBbCiAgICAic3luZGljYXRlX2hvbGRfMCIsCiAgICAic3luZGljYXRlX2hvbGRfMSIsCiAgICAic3luZGljYXRlX2hvbGRfMiIKICAgXSwKICAgIm1vdmUiOiBbCiAgICAic3luZGljYXRlX21vdmVfMCIsCiAgICAic3luZGljYXRlX21vdmVfMSIsCiAgICAic3luZGljYXRlX21vdmVfMiIKICAgXSwKICAgInBhdHJvbCI6IFsKICAgICJzeW5kaWNhdGVfcGF0cm9sXzAiLAogICAgInN5bmRpY2F0ZV9wYXRyb2xfMSIsCiAgICAic3luZGljYXRlX3BhdHJvbF8yIgogICBdLAogICAic2VsZWN0IjogWwogICAgInN5bmRpY2F0ZV9zZWxlY3RfMCIsCiAgICAic3luZGljYXRlX3NlbGVjdF8xIiwKICAgICJzeW5kaWNhdGVfc2VsZWN0XzIiCiAgIF0sCiAgICJzdG9wIjogWwogICAgInN5bmRpY2F0ZV9zdG9wXzAiLAogICAgInN5bmRpY2F0ZV9zdG9wXzEiLAogICAgInN5bmRpY2F0ZV9zdG9wXzIiCiAgIF0KICB9CiB9LAogInRha2VzIjogewogICJhc2NlbmRhbmN5X2FiaWxpdHlfMCI6IHsKICAgInNoYSI6ICI2YjdiMDMzNjViZTFhYzFjIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi45MzUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NDMxLAogICAibTRhIjogMTYxMjQKICB9LAogICJhc2NlbmRhbmN5X2FiaWxpdHlfMSI6IHsKICAgInNoYSI6ICI3ZTIyNGEyYzJkMzc5YmRkIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4wOTksCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE2MTM1LAogICAibTRhIjogMTY5MzYKICB9LAogICJhc2NlbmRhbmN5X2FiaWxpdHlfMiI6IHsKICAgInNoYSI6ICI1MGUyOTk5YzEzMWUwZDYxIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4wNTEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NzQzLAogICAibTRhIjogMTY3NTQKICB9LAogICJhc2NlbmRhbmN5X2F0dGFja18wIjogewogICAic2hhIjogImNhNjc4ZWU1YzJiMmZhNmIiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjAzNywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTE0OTUsCiAgICJtNGEiOiAxMTUyOAogIH0sCiAgImFzY2VuZGFuY3lfYXR0YWNrXzEiOiB7CiAgICJzaGEiOiAiOGE5YjVlYTY1MjM4NTdiNSIsCiAgICJ2b2ljZSI6ICJibV9nZW9yZ2UiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMDIsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1MzgyLAogICAibTRhIjogMTY1MzkKICB9LAogICJhc2NlbmRhbmN5X2F0dGFja18yIjogewogICAic2hhIjogIjkyZTE4MDY5Y2Q3YTFjMTciLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjg1OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTUyNDEsCiAgICJtNGEiOiAxNTgyOQogIH0sCiAgImFzY2VuZGFuY3lfYnVpbGRfMCI6IHsKICAgInNoYSI6ICIzMzdmYjM1NzQyOGEwNGY4IiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4xOTMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE2MzA1LAogICAibTRhIjogMTc0OTUKICB9LAogICJhc2NlbmRhbmN5X2J1aWxkXzEiOiB7CiAgICJzaGEiOiAiY2NhNDUwMjhmY2M1YTAwMyIsCiAgICJ2b2ljZSI6ICJibV9nZW9yZ2UiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMzk5LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNzU3MiwKICAgIm00YSI6IDE4NTIyCiAgfSwKICAiYXNjZW5kYW5jeV9idWlsZF8yIjogewogICAic2hhIjogImViZGExZjE4NjRkMWIzZTYiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjY0MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ0MTgsCiAgICJtNGEiOiAxNDYzMAogIH0sCiAgImFzY2VuZGFuY3lfZGVwbG95XzAiOiB7CiAgICJzaGEiOiAiNGQwOTcyZGYzYTRhNTg0NyIsCiAgICJ2b2ljZSI6ICJibV9nZW9yZ2UiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMDIyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNTY0MywKICAgIm00YSI6IDE2NjEyCiAgfSwKICAiYXNjZW5kYW5jeV9kZXBsb3lfMSI6IHsKICAgInNoYSI6ICI1MTQzY2NkNmNjMjY2Yzc5IiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi45MjMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NjIzLAogICAibTRhIjogMTYwNzAKICB9LAogICJhc2NlbmRhbmN5X2RlcGxveV8yIjogewogICAic2hhIjogIjVmMTJlZTdiMzZhYzI0YTMiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjg4MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTUyNzksCiAgICJtNGEiOiAxNTg4MwogIH0sCiAgImFzY2VuZGFuY3lfaG9sZF8wIjogewogICAic2hhIjogIjQyNDg2YzAxNzhkNGRkZDciLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI2MywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI3MjQsCiAgICJtNGEiOiAxMjcwOAogIH0sCiAgImFzY2VuZGFuY3lfaG9sZF8xIjogewogICAic2hhIjogImMzODMyNDdiZTgzZDU2M2IiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjgwMywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ3NTIsCiAgICJtNGEiOiAxNTQyNQogIH0sCiAgImFzY2VuZGFuY3lfaG9sZF8yIjogewogICAic2hhIjogImY1MThiYjgzNTFmNGQwYjAiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjM5NCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTMxODYsCiAgICJtNGEiOiAxMzQzMgogIH0sCiAgImFzY2VuZGFuY3lfbW92ZV8wIjogewogICAic2hhIjogIjllOGU3YmU2MzE5NzhiMWQiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjQ5NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTM3MTAsCiAgICJtNGEiOiAxMzg2OAogIH0sCiAgImFzY2VuZGFuY3lfbW92ZV8xIjogewogICAic2hhIjogIjc0OTRiZWJhNTdjY2NmODkiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjc5OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ1ODksCiAgICJtNGEiOiAxNTM5MwogIH0sCiAgImFzY2VuZGFuY3lfbW92ZV8yIjogewogICAic2hhIjogIjFhNTMyOThjYWY1NTIzYmYiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjYwNCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTAwNTUsCiAgICJtNGEiOiA5NDQ0CiAgfSwKICAiYXNjZW5kYW5jeV9wYXRyb2xfMCI6IHsKICAgInNoYSI6ICI3MzFmZDQ3NjZkMDU3ODc3IiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4zOTUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzNjA5LAogICAibTRhIjogMTM0MTYKICB9LAogICJhc2NlbmRhbmN5X3BhdHJvbF8xIjogewogICAic2hhIjogImNjOTBmYWE2MmU0YjVjNDEiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjA2LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjAxNSwKICAgIm00YSI6IDExNjUyCiAgfSwKICAiYXNjZW5kYW5jeV9wYXRyb2xfMiI6IHsKICAgInNoYSI6ICJlMzVjNWVmNmQ2YTE4MTIwIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi42MzEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE0MDkyLAogICAibTRhIjogMTQ2NTgKICB9LAogICJhc2NlbmRhbmN5X3NlbGVjdF8wIjogewogICAic2hhIjogImNmZTJlZjVlYmYwNDljZWYiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjk5LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNTcwMCwKICAgIm00YSI6IDE2MzkyCiAgfSwKICAiYXNjZW5kYW5jeV9zZWxlY3RfMSI6IHsKICAgInNoYSI6ICIwYWQwZTBhZjVhMDljNTUyIiwKICAgInZvaWNlIjogImJtX2dlb3JnZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi42NjYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE0Mjg5LAogICAibTRhIjogMTQ3MjkKICB9LAogICJhc2NlbmRhbmN5X3NlbGVjdF8yIjogewogICAic2hhIjogIjA4YzVjMTE3MTI5YjdhNDMiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjYyOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTM5NjIsCiAgICJtNGEiOiAxNDY2NQogIH0sCiAgImFzY2VuZGFuY3lfc3RvcF8wIjogewogICAic2hhIjogImE5ZTQ2MTE4OWI3NGIwMjYiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjg1OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ5MjgsCiAgICJtNGEiOiAxNTg4NgogIH0sCiAgImFzY2VuZGFuY3lfc3RvcF8xIjogewogICAic2hhIjogImNmYWFmYzRiOGI3Y2MxZjciLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjIzNSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTIzMTQsCiAgICJtNGEiOiAxMjU1NAogIH0sCiAgImFzY2VuZGFuY3lfc3RvcF8yIjogewogICAic2hhIjogImRlMWI1MWZiZDk3YjdkMzgiLAogICAidm9pY2UiOiAiYm1fZ2VvcmdlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjk3OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTE2NTQsCiAgICJtNGEiOiAxMTMxNgogIH0sCiAgImhvcmRlX2FiaWxpdHlfMCI6IHsKICAgInNoYSI6ICI5MDBiZmQyNjFjZGZhMWE3IiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjgyNCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTUwNjEsCiAgICJtNGEiOiAxNTU4MAogIH0sCiAgImhvcmRlX2FiaWxpdHlfMSI6IHsKICAgInNoYSI6ICJhNzZhZDI4OTdkMDAwMDE2IiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAzLjE5NCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTY3MzgsCiAgICJtNGEiOiAxNzQ3NQogIH0sCiAgImhvcmRlX2FiaWxpdHlfMiI6IHsKICAgInNoYSI6ICIyMjFlYjQ0MTU2MzY2NTIxIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjkwNCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTU1MDYsCiAgICJtNGEiOiAxNjA3MQogIH0sCiAgImhvcmRlX2F0dGFja18wIjogewogICAic2hhIjogImQxM2U1ZWJiZmMwZTIwNDYiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzU0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDI4OCwKICAgIm00YSI6IDEwMjA0CiAgfSwKICAiaG9yZGVfYXR0YWNrXzEiOiB7CiAgICJzaGEiOiAiNjgyOGQwMmY1NmJiNTFlZCIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4wNjQsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NjYwLAogICAibTRhIjogMTY4NjYKICB9LAogICJob3JkZV9hdHRhY2tfMiI6IHsKICAgInNoYSI6ICI1NWJlNTYxZTg3Y2UwYTllIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjY4OSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQzODMsCiAgICJtNGEiOiAxNDk2NQogIH0sCiAgImhvcmRlX2J1aWxkXzAiOiB7CiAgICJzaGEiOiAiODhmZjc1NjZlYzhkMjVkOSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi43ODMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE0Njg3LAogICAibTRhIjogMTU0NjMKICB9LAogICJob3JkZV9idWlsZF8xIjogewogICAic2hhIjogImJhMWRhN2Y0NDViN2VmNmMiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMjI4LAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiAxNjc0MywKICAgIm00YSI6IDE3NzYyCiAgfSwKICAiaG9yZGVfYnVpbGRfMiI6IHsKICAgInNoYSI6ICI3ZDM2MWQ0NzcyYzUxZGUzIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjEyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjQ2NCwKICAgIm00YSI6IDEyMDgzCiAgfSwKICAiaG9yZGVfZGVwbG95XzAiOiB7CiAgICJzaGEiOiAiMzc0ZmI4NDBkZjcxZmQwMiIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy4wMDUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1NTc4LAogICAibTRhIjogMTY2MDYKICB9LAogICJob3JkZV9kZXBsb3lfMSI6IHsKICAgInNoYSI6ICIwMGQxOThkZGYwMjg3ODdiIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjk2MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTU2NzEsCiAgICJtNGEiOiAxNjM0OQogIH0sCiAgImhvcmRlX2RlcGxveV8yIjogewogICAic2hhIjogIjM0YmFjMWI1MDNiMjBlN2UiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuMDE0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNTU1OSwKICAgIm00YSI6IDE2NTM1CiAgfSwKICAiaG9yZGVfaG9sZF8wIjogewogICAic2hhIjogIjk2YTYxNDU3Y2UyYjNjNDYiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuODUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDExMDYzLAogICAibTRhIjogMTA2NjUKICB9LAogICJob3JkZV9ob2xkXzEiOiB7CiAgICJzaGEiOiAiOTkwODU1OGI2MWI4NDBjNyIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi42MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTM5ODMsCiAgICJtNGEiOiAxNDY3NAogIH0sCiAgImhvcmRlX2hvbGRfMiI6IHsKICAgInNoYSI6ICJkMDkwMDU3NTkwZGU1M2QzIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjg4MywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTExNjQsCiAgICJtNGEiOiAxMDg2MAogIH0sCiAgImhvcmRlX21vdmVfMCI6IHsKICAgInNoYSI6ICJhNTdjNjhkOTkzMTQ1NDU3IiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI3NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI5MTcsCiAgICJtNGEiOiAxMjgwMAogIH0sCiAgImhvcmRlX21vdmVfMSI6IHsKICAgInNoYSI6ICJkODI3YzFjOWY5ZTkyMDA4IiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI4MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI1MDUsCiAgICJtNGEiOiAxMjg1OQogIH0sCiAgImhvcmRlX21vdmVfMiI6IHsKICAgInNoYSI6ICI1NzNhMWU1NWYwZDUyMzAzIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjMwOCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogOTAwOSwKICAgIm00YSI6IDc3MTgKICB9LAogICJob3JkZV9wYXRyb2xfMCI6IHsKICAgInNoYSI6ICJhZjYwODc5NDExZDRlMTRjIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjEzOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI1OTgsCiAgICJtNGEiOiAxMjE0NAogIH0sCiAgImhvcmRlX3BhdHJvbF8xIjogewogICAic2hhIjogImY4ZjliMDUzZWVhY2I3MmIiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzU3LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDg0MCwKICAgIm00YSI6IDEwMTc5CiAgfSwKICAiaG9yZGVfcGF0cm9sXzIiOiB7CiAgICJzaGEiOiAiMmQ1YWI3M2M4MGE1ZjFjZiIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi40ODksCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzNjgxLAogICAibTRhIjogMTM4NzgKICB9LAogICJob3JkZV9zZWxlY3RfMCI6IHsKICAgInNoYSI6ICI0ZGRmMjFjMDc0YjRkZDRiIiwKICAgInZvaWNlIjogImJtX2ZhYmxlIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjc2NywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTQ3MDIsCiAgICJtNGEiOiAxNTM0MQogIH0sCiAgImhvcmRlX3NlbGVjdF8xIjogewogICAic2hhIjogImM2MDNlYTQzN2Y1ZDY2ZTIiLAogICAidm9pY2UiOiAiYm1fZmFibGUiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTgzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjQ5MCwKICAgIm00YSI6IDEyMzg0CiAgfSwKICAiaG9yZGVfc2VsZWN0XzIiOiB7CiAgICJzaGEiOiAiNzIxOTQ0YjAwNGQ2N2VhMSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi40NjksCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzMTc0LAogICAibTRhIjogMTM3NTYKICB9LAogICJob3JkZV9zdG9wXzAiOiB7CiAgICJzaGEiOiAiZWZmNTAzMGIzNGQzM2MxZSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi41NzcsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzNzcyLAogICAibTRhIjogMTQ0MjkKICB9LAogICJob3JkZV9zdG9wXzEiOiB7CiAgICJzaGEiOiAiYzI4MTFmMWYyNThjZGZhOSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS44MTMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEwODE0LAogICAibTRhIjogMTA0NzYKICB9LAogICJob3JkZV9zdG9wXzIiOiB7CiAgICJzaGEiOiAiNDMxMWM4ODc2NTQyMTBjMSIsCiAgICJ2b2ljZSI6ICJibV9mYWJsZSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS44MTQsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEwOTg4LAogICAibTRhIjogMTA0MTgKICB9LAogICJrZWVuX2RvbmVfYWJpbGl0eSI6IHsKICAgInNoYSI6ICI5OGU5ZDdkMjc4MWY3NDI3IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1Ljk2NywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjczNTMsCiAgICJtNGEiOiAzMDEzMwogIH0sCiAgImtlZW5fZG9uZV9hdHRhY2siOiB7CiAgICJzaGEiOiAiNmJmZmYyOGU3YjIyYzFjMiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS41MjksCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI1MzgzLAogICAibTRhIjogMjgxNDMKICB9LAogICJrZWVuX2RvbmVfY2FtZXJhIjogewogICAic2hhIjogImRhNDc5OGQ4YmU3NGU4N2EiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDUuMzM1LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAyNDgwMCwKICAgIm00YSI6IDI3MzA1CiAgfSwKICAia2Vlbl9kb25lX2Nsb3VkIjogewogICAic2hhIjogIjQzODMwZTAyNzkwNmRiZjAiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDY3LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjA3NywKICAgIm00YSI6IDExNzU2CiAgfSwKICAia2Vlbl9kb25lX2NvbW1hbmRlciI6IHsKICAgInNoYSI6ICIzMTczMTgyM2UzYWUxMDljIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1Ljc5OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjYyNjcsCiAgICJtNGEiOiAyOTQ2NAogIH0sCiAgImtlZW5fZG9uZV9kZXBsb3kiOiB7CiAgICJzaGEiOiAiYTg1MDRjZWU3YWM1N2IzMSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMy44NjIsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE5MTAxLAogICAibTRhIjogMjA2NzYKICB9LAogICJrZWVuX2RvbmVfZmFjIjogewogICAic2hhIjogImZmYmQ1MGIxMTkwOGU0ZDgiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDMuODkxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxOTE3MCwKICAgIm00YSI6IDIwODc2CiAgfSwKICAia2Vlbl9kb25lX2ZvZyI6IHsKICAgInNoYSI6ICIzZTkyZDI2NTQzNmYxYzFhIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA2Ljk2NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMzEzOTcsCiAgICJtNGEiOiAzNDg1NAogIH0sCiAgImtlZW5fZG9uZV9mb3JtYXRpb24iOiB7CiAgICJzaGEiOiAiYWE4ZjA4ZDBmMzYwNzlmMCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS4yNTgsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI0Nzc3LAogICAibTRhIjogMjcyMjkKICB9LAogICJrZWVuX2RvbmVfaW50ZWwiOiB7CiAgICJzaGEiOiAiNGM4ODhkNmMzOWZkYTI5MiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTEuNDY4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA0ODE3MSwKICAgIm00YSI6IDU1OTAyCiAgfSwKICAia2Vlbl9kb25lX21leCI6IHsKICAgInNoYSI6ICJmNjgzZmZkNDNlNzlmZDAxIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAzLjU0NCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTgxODIsCiAgICJtNGEiOiAxOTE1MwogIH0sCiAgImtlZW5fZG9uZV9vYmplY3RpdmUiOiB7CiAgICJzaGEiOiAiNDdmODZjMDY1Y2FmYTk1NCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNy4zNTgsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDMyODc3LAogICAibTRhIjogMzcwNjQKICB9LAogICJrZWVuX2RvbmVfcGlja3VwIjogewogICAic2hhIjogIjNmYjY0ZWU0ZWQwYmI2OGMiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDYuOTYyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzMDg4MywKICAgIm00YSI6IDM0ODUyCiAgfSwKICAia2Vlbl9kb25lX3BsYXRvb24iOiB7CiAgICJzaGEiOiAiZTVhNzQxMWQwOTBkNzExZCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS4wMTIsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDIzNDI2LAogICAibTRhIjogMjU2OTEKICB9LAogICJrZWVuX2RvbmVfcG93ZXIiOiB7CiAgICJzaGEiOiAiYTVlNTk1MDQ4OTgxN2I0YyIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS4wMjEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDIzNjk1LAogICAibTRhIjogMjYyMzMKICB9LAogICJrZWVuX2RvbmVfcXVldWUiOiB7CiAgICJzaGEiOiAiMDU0NTYxOWZkNmFiYzU0ZiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS4wMDEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDIzNDIyLAogICAibTRhIjogMjU2MjMKICB9LAogICJrZWVuX2RvbmVfdGVjaCI6IHsKICAgInNoYSI6ICI5YzIwNTNkODA2YmYwMDYyIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1LjYyNSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjU4OTUsCiAgICJtNGEiOiAyODcwNgogIH0sCiAgImtlZW5fZG9uZV90ZXJyaXRvcnkiOiB7CiAgICJzaGEiOiAiN2FjOWM2ZmVjN2I1ODZlOCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi44NzEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDE1MzQ0LAogICAibTRhIjogMTU4MzEKICB9LAogICJrZWVuX2RvbmVfdHJhaW4iOiB7CiAgICJzaGEiOiAiYjFlNTAyNTI4ZDc2NTZiMSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNi42OSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjkyMTQsCiAgICJtNGEiOiAzMzQ5NwogIH0sCiAgImtlZW5fZG9uZV90dXJyZXQiOiB7CiAgICJzaGEiOiAiYmIyNWNiMTI0NmIzMzg3YiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTQuNTYyLAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiA1OTE0NSwKICAgIm00YSI6IDcxMTQ1CiAgfSwKICAia2Vlbl9ncmFkdWF0aW9uIjogewogICAic2hhIjogImU5ZDJkNGU5ZjY2MDFhMDAiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEyLjA5NywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogNTA5MTAsCiAgICJtNGEiOiA1OTAwOQogIH0sCiAgImtlZW5fZ3JlZXRpbmciOiB7CiAgICJzaGEiOiAiM2YyZGQ2M2JjMmExYjQ3NiIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTIuMTIzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA1MDA5OSwKICAgIm00YSI6IDU4ODIwCiAgfSwKICAia2Vlbl9yZWFjdF9iYXNlX2F0dGFjazAiOiB7CiAgICJzaGEiOiAiNjdhY2M5NWI5MzliMWQxZSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS40MTUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI1NDAwLAogICAibTRhIjogMjc5OTUKICB9LAogICJrZWVuX3JlYWN0X2Jhc2VfYXR0YWNrMSI6IHsKICAgInNoYSI6ICI2MDk3YWVhZmUwNGM0MDdhIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0LjA0NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTk4OTIsCiAgICJtNGEiOiAyMTM3NQogIH0sCiAgImtlZW5fcmVhY3RfaGF6YXJkX2NyYXRlciI6IHsKICAgInNoYSI6ICIzOTE3ZDBhODlkZGExMzMwIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0LjgyMywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjM1NTcsCiAgICJtNGEiOiAyNTIwNwogIH0sCiAgImtlZW5fcmVhY3RfaGF6YXJkX2RlZmF1bHQiOiB7CiAgICJzaGEiOiAiNThhMDNmODJkYjQ5OTM5NSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS43NzUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI2MjM5LAogICAibTRhIjogMjkzNzMKICB9LAogICJrZWVuX3JlYWN0X2hhemFyZF9oaWdobGFuZCI6IHsKICAgInNoYSI6ICJiMTBmNTUxYjFmYmM2NjA1IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0LjM4NCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjA4ODUsCiAgICJtNGEiOiAyMzA0NQogIH0sCiAgImtlZW5fcmVhY3RfaGF6YXJkX2lzbGVzIjogewogICAic2hhIjogImVlM2Q1ZGQ5ZGYxNDgzNjYiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDQuNjIzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAyMjM4NywKICAgIm00YSI6IDI0MDE1CiAgfSwKICAia2Vlbl9yZWFjdF9oYXphcmRfdmFuZ3VhcmQiOiB7CiAgICJzaGEiOiAiNzY1OGRkMTFiNmZkZDdmMyIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNS41OTMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDI2MDE2LAogICAibTRhIjogMjg3NjIKICB9LAogICJrZWVuX3JlYWN0X2xvd19wb3dlciI6IHsKICAgInNoYSI6ICJlZGE4M2E5ZDBhMmU3NTEzIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0LjEzOCwKICAgInBlYWsiOiAtMC42LAogICAib2dnIjogMjAyNDIsCiAgICJtNGEiOiAyMjAxOAogIH0sCiAgImtlZW5fcmVhY3RfdW5pdF9sb3N0MCI6IHsKICAgInNoYSI6ICIzMGU4NmQzODFhMzk5NzY3IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1LjI1MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjQ4NTIsCiAgICJtNGEiOiAyNzAyMAogIH0sCiAgImtlZW5fcmVhY3RfdW5pdF9sb3N0MSI6IHsKICAgInNoYSI6ICIxYmEzMzE2MjM4YTVlYWNjIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA0Ljk2NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjMzMzksCiAgICJtNGEiOiAyNTg0NAogIH0sCiAgImtlZW5fcmVhY3Rfd2F2ZSI6IHsKICAgInNoYSI6ICIyNjQzMGVmMTU4YjA4M2ViIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA2LjI5NSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjc3MjIsCiAgICJtNGEiOiAzMTk3MAogIH0sCiAgImtlZW5fc2tpcCI6IHsKICAgInNoYSI6ICIzNjMwOWJhN2EwNDk4MTc2IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA1LjMxNSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjQ5NjgsCiAgICJtNGEiOiAyNzI4NwogIH0sCiAgImtlZW5fc3RlcF9hYmlsaXR5IjogewogICAic2hhIjogIjQ1Yzg1OTdmZjEwNWJkNDAiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDguNjg1LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzNzc2NywKICAgIm00YSI6IDQyODI4CiAgfSwKICAia2Vlbl9zdGVwX2F0dGFjayI6IHsKICAgInNoYSI6ICI4ZmNkYTA5YTViZGQwNDkwIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxNi43NTcsCiAgICJwZWFrIjogLTAuNiwKICAgIm9nZyI6IDY4Njc4LAogICAibTRhIjogODEzNzEKICB9LAogICJrZWVuX3N0ZXBfY2FtZXJhIjogewogICAic2hhIjogIjE5MGRkNTM1MWEzOWI5MTIiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDkuNDE2LAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiA0MDE2NSwKICAgIm00YSI6IDQ2NDk5CiAgfSwKICAia2Vlbl9zdGVwX2Nsb3VkIjogewogICAic2hhIjogIjVhM2ExMTYxNTcxYzAyZjUiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEyLjI0NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogNTEzNjMsCiAgICJtNGEiOiA1OTg3OAogIH0sCiAgImtlZW5fc3RlcF9jb21tYW5kZXIiOiB7CiAgICJzaGEiOiAiNjQ2OGI2NTIwMzg3ZjRiMCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogNy40NTUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDMzNDQ4LAogICAibTRhIjogMzcyMTcKICB9LAogICJrZWVuX3N0ZXBfZGVwbG95IjogewogICAic2hhIjogIjRlMmQ3MWNhN2I2NDJjNjIiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDYuODkxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzMDE2MiwKICAgIm00YSI6IDM0ODQ5CiAgfSwKICAia2Vlbl9zdGVwX2ZhYyI6IHsKICAgInNoYSI6ICIwNGNhMjMwMjY2ZDE2Y2UyIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA4LjQ4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzNzMzMiwKICAgIm00YSI6IDQyMDc2CiAgfSwKICAia2Vlbl9zdGVwX2ZvZyI6IHsKICAgInNoYSI6ICIxNjI3MzQyNzQ4Y2MyMmVkIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA5LjI0MywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMzk2NzgsCiAgICJtNGEiOiA0NTgwMAogIH0sCiAgImtlZW5fc3RlcF9mb3JtYXRpb24iOiB7CiAgICJzaGEiOiAiNzBlMjllMWJmZmYxMmIwNCIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogOC4wODUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDM1MTgwLAogICAibTRhIjogNDAyMzQKICB9LAogICJrZWVuX3N0ZXBfaW50ZWwiOiB7CiAgICJzaGEiOiAiNjc0Mzk1M2U2ZTg4YmMyYyIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTcuNjUyLAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiA3MTMwOCwKICAgIm00YSI6IDg0ODIwCiAgfSwKICAia2Vlbl9zdGVwX21leCI6IHsKICAgInNoYSI6ICIyN2FlMDI1N2NmMTg2ZWJlIiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA5Ljk4NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogNDI4NzMsCiAgICJtNGEiOiA0OTU0MAogIH0sCiAgImtlZW5fc3RlcF9vYmplY3RpdmUiOiB7CiAgICJzaGEiOiAiZDg1YTY5MDg5N2JkYjM4ZSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTEuNjQ0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA0ODk0NiwKICAgIm00YSI6IDU3MDE1CiAgfSwKICAia2Vlbl9zdGVwX3BpY2t1cCI6IHsKICAgInNoYSI6ICJmMjI2ODY5MjBhNTkyNTU5IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA4LjU5MiwKICAgInBlYWsiOiAtMC42LAogICAib2dnIjogMzc4NjIsCiAgICJtNGEiOiA0MjU2NwogIH0sCiAgImtlZW5fc3RlcF9wbGF0b29uIjogewogICAic2hhIjogImE4NjdhMGU4ZTA3NDFhMTIiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDkuODExLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA0MTY2MSwKICAgIm00YSI6IDQ4NTg1CiAgfSwKICAia2Vlbl9zdGVwX3Bvd2VyIjogewogICAic2hhIjogIjY2NWY3YWVlZjNiNTMxY2MiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEwLjE2NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogNDM3NjAsCiAgICJtNGEiOiA1MDI3OQogIH0sCiAgImtlZW5fc3RlcF9xdWV1ZSI6IHsKICAgInNoYSI6ICI4Zjg4MDE5MWRlYTE4YTk5IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA2Ljc1MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMjk4MTQsCiAgICJtNGEiOiAzMzgzOQogIH0sCiAgImtlZW5fc3RlcF90ZWNoIjogewogICAic2hhIjogIjM3MWYzZDU5NDA4MzNhNGYiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEwLjA2NiwKICAgInBlYWsiOiAtMC42LAogICAib2dnIjogNDM3MzIsCiAgICJtNGEiOiA0OTY4OQogIH0sCiAgImtlZW5fc3RlcF90ZXJyaXRvcnkiOiB7CiAgICJzaGEiOiAiOWJmYjVmMzlhNjliMGQzMSIsCiAgICJ2b2ljZSI6ICJhZl9oZWFydCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMTIuMTMzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA1MDk0OCwKICAgIm00YSI6IDU5NDM5CiAgfSwKICAia2Vlbl9zdGVwX3RyYWluIjogewogICAic2hhIjogImJjMjg0MDgwNjc4NjBiY2UiLAogICAidm9pY2UiOiAiYWZfaGVhcnQiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDcuMzc0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAzMjkwMSwKICAgIm00YSI6IDM2ODcxCiAgfSwKICAia2Vlbl9zdGVwX3R1cnJldCI6IHsKICAgInNoYSI6ICIzYTNkZjk1ZDZjMzdkODM4IiwKICAgInZvaWNlIjogImFmX2hlYXJ0IiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiA3Ljc4MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMzM5MjAsCiAgICJtNGEiOiAzODY5NQogIH0sCiAgIm5vdmFfYWJpbGl0eV8wIjogewogICAic2hhIjogIjUzNzcwZmZmYTUzZWM4NDgiLAogICAidm9pY2UiOiAiYW1fbWljaGFlbCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4xNzcsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEyNDM3LAogICAibTRhIjogMTIyMzgKICB9LAogICJub3ZhX2FiaWxpdHlfMSI6IHsKICAgInNoYSI6ICIxZTFkMjg4YzZmNWNjZTE0IiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuNTc0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxNDQ0NywKICAgIm00YSI6IDE0MjY1CiAgfSwKICAibm92YV9hYmlsaXR5XzIiOiB7CiAgICJzaGEiOiAiMWE3YmMwZThhYWJjOTVjYSIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjY4OSwKICAgIm00YSI6IDEyODI3CiAgfSwKICAibm92YV9hdHRhY2tfMCI6IHsKICAgInNoYSI6ICJhMzJmMmZmNTIxN2M3ZWFhIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNjM0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA5ODI0LAogICAibTRhIjogOTQ5MgogIH0sCiAgIm5vdmFfYXR0YWNrXzEiOiB7CiAgICJzaGEiOiAiMWUxYWM2NWQ4MzBiNjUxZCIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjMwOCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI5NTcsCiAgICJtNGEiOiAxMjk0MwogIH0sCiAgIm5vdmFfYXR0YWNrXzIiOiB7CiAgICJzaGEiOiAiYWU3MTQ0M2Q1YTM5MTZhNiIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjIzNiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI3OTAsCiAgICJtNGEiOiAxMjU5MAogIH0sCiAgIm5vdmFfYnVpbGRfMCI6IHsKICAgInNoYSI6ICI2NjQyZjg2MzNjOTdjYzQ3IiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuNDYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEzNDU2LAogICAibTRhIjogMTM3MDIKICB9LAogICJub3ZhX2J1aWxkXzEiOiB7CiAgICJzaGEiOiAiMDQ2MTA5YTg4N2Y4ZjcxNyIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjQxOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTM3NzEsCiAgICJtNGEiOiAxMzUwMwogIH0sCiAgIm5vdmFfYnVpbGRfMiI6IHsKICAgInNoYSI6ICJiODY5ZGJkYWI0M2JkNjkyIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTkxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTk5NywKICAgIm00YSI6IDExMzk5CiAgfSwKICAibm92YV9kZXBsb3lfMCI6IHsKICAgInNoYSI6ICI2NDliYjMyMjkyMTZmNDY0IiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMjg0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjc0NCwKICAgIm00YSI6IDEyODM2CiAgfSwKICAibm92YV9kZXBsb3lfMSI6IHsKICAgInNoYSI6ICI2YWVhYWVjZGMwNzBjMGVkIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTczLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjQyNywKICAgIm00YSI6IDEyMjY5CiAgfSwKICAibm92YV9kZXBsb3lfMiI6IHsKICAgInNoYSI6ICI2NGRiODcwOTE1NzBlYWRiIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTQ5LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjM0NywKICAgIm00YSI6IDEyMTQyCiAgfSwKICAibm92YV9ob2xkXzAiOiB7CiAgICJzaGEiOiAiNjcyMTJkYzY5NDU4ODkzNiIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjkxNCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTEzOTcsCiAgICJtNGEiOiAxMDkxMQogIH0sCiAgIm5vdmFfaG9sZF8xIjogewogICAic2hhIjogIjhkMDQyYzE4NzkxZDlhNTIiLAogICAidm9pY2UiOiAiYW1fbWljaGFlbCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4xNDEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEyMDgxLAogICAibTRhIjogMTIwNTUKICB9LAogICJub3ZhX2hvbGRfMiI6IHsKICAgInNoYSI6ICJhYjdkMDA1NGQ4NWQ2NDJmIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuODc4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTEwMSwKICAgIm00YSI6IDEwODY4CiAgfSwKICAibm92YV9tb3ZlXzAiOiB7CiAgICJzaGEiOiAiNDNiMTNiYjVmMWEyY2MyYiIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjgxOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTEwNjEsCiAgICJtNGEiOiAxMDQyOAogIH0sCiAgIm5vdmFfbW92ZV8xIjogewogICAic2hhIjogIjNiZjJmYzU5MDgzYjAzYjQiLAogICAidm9pY2UiOiAiYW1fbWljaGFlbCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4wMzYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDExODc1LAogICAibTRhIjogMTE1NzcKICB9LAogICJub3ZhX21vdmVfMiI6IHsKICAgInNoYSI6ICIxYzNhNGNlZWU0M2Y4MTJjIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuMzkyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiA5MjAzLAogICAibTRhIjogODIwMQogIH0sCiAgIm5vdmFfcGF0cm9sXzAiOiB7CiAgICJzaGEiOiAiMGIxMzY1NmY5ZGU2YTRkYSIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjkzOSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTE1NDksCiAgICJtNGEiOiAxMTA3OQogIH0sCiAgIm5vdmFfcGF0cm9sXzEiOiB7CiAgICJzaGEiOiAiNDc0YzJjZDFhYzZjODQwMyIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjYzNiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTAyNjksCiAgICJtNGEiOiA5NTEwCiAgfSwKICAibm92YV9wYXRyb2xfMiI6IHsKICAgInNoYSI6ICI4NGFmMWYyNWQwZTEwMWNmIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDk5LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjA3MSwKICAgIm00YSI6IDExOTM5CiAgfSwKICAibm92YV9zZWxlY3RfMCI6IHsKICAgInNoYSI6ICIzZTdmMWFkZmQ0NWJmZDk5IiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDkzLAogICAicGVhayI6IC0wLjYsCiAgICJvZ2ciOiAxMjAyMiwKICAgIm00YSI6IDExODI4CiAgfSwKICAibm92YV9zZWxlY3RfMSI6IHsKICAgInNoYSI6ICI0NDFhNjJjOTE4NGIzYjhkIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDcyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjEyMCwKICAgIm00YSI6IDExODA5CiAgfSwKICAibm92YV9zZWxlY3RfMiI6IHsKICAgInNoYSI6ICJjZjU4YjJlM2E2OTA0NzNmIiwKICAgInZvaWNlIjogImFtX21pY2hhZWwiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDQzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTQ0MywKICAgIm00YSI6IDExNTQ4CiAgfSwKICAibm92YV9zdG9wXzAiOiB7CiAgICJzaGEiOiAiYmMwOTdhN2UyZmI5MjQ1YyIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjE4MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTIyMzAsCiAgICJtNGEiOiAxMjI4MQogIH0sCiAgIm5vdmFfc3RvcF8xIjogewogICAic2hhIjogIjJmYzIxMWVmMzUyYWVlNmYiLAogICAidm9pY2UiOiAiYW1fbWljaGFlbCIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS42NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTAxMjMsCiAgICJtNGEiOiA5NTk5CiAgfSwKICAibm92YV9zdG9wXzIiOiB7CiAgICJzaGEiOiAiYmI0MTNlOTJmNjA4YmQxZCIsCiAgICJ2b2ljZSI6ICJhbV9taWNoYWVsIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjY2LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDIyMSwKICAgIm00YSI6IDk2NTgKICB9LAogICJzeW5kaWNhdGVfYWJpbGl0eV8wIjogewogICAic2hhIjogImNjMzE1OWJiMDdiNjQ5YzkiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDAxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTU5NSwKICAgIm00YSI6IDExNDAwCiAgfSwKICAic3luZGljYXRlX2FiaWxpdHlfMSI6IHsKICAgInNoYSI6ICIzMzJhZmEwMWQ5ZmU3MzU5IiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjI5NiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI5NDIsCiAgICJtNGEiOiAxMzA2OQogIH0sCiAgInN5bmRpY2F0ZV9hYmlsaXR5XzIiOiB7CiAgICJzaGEiOiAiZDJjZmZiZTYxM2E1MzhhOSIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4wMzUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDExODc5LAogICAibTRhIjogMTE2MTUKICB9LAogICJzeW5kaWNhdGVfYXR0YWNrXzAiOiB7CiAgICJzaGEiOiAiMWJiMGYyOWJiZTA1MGRiZCIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS41MDUsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDkzMDAsCiAgICJtNGEiOiA4Nzc5CiAgfSwKICAic3luZGljYXRlX2F0dGFja18xIjogewogICAic2hhIjogIjA1NDlmNzc2ZmFlZDQ2YTkiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTgzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjM5MCwKICAgIm00YSI6IDEyMzU3CiAgfSwKICAic3luZGljYXRlX2F0dGFja18yIjogewogICAic2hhIjogIjQzYTNhMjg0YTZkYjY4ZDIiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMDYxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjA4OCwKICAgIm00YSI6IDExNjU2CiAgfSwKICAic3luZGljYXRlX2J1aWxkXzAiOiB7CiAgICJzaGEiOiAiMGIyOWU2YmU1ZGRjZDQ3OSIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMi4wOTYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEyMDEwLAogICAibTRhIjogMTE4OTYKICB9LAogICJzeW5kaWNhdGVfYnVpbGRfMSI6IHsKICAgInNoYSI6ICI2YjNiMDBkZWQxMWEwODE5IiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjE4NywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTI1OTksCiAgICJtNGEiOiAxMjQwOAogIH0sCiAgInN5bmRpY2F0ZV9idWlsZF8yIjogewogICAic2hhIjogIjFjNDUzZTQyM2QyMWZiZTMiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzY2LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDgxNywKICAgIm00YSI6IDEwMjA0CiAgfSwKICAic3luZGljYXRlX2RlcGxveV8wIjogewogICAic2hhIjogIjBhNWFlMTczNGFlNmQ5MTgiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDIuMTE3LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMjA3NiwKICAgIm00YSI6IDEyMDQ1CiAgfSwKICAic3luZGljYXRlX2RlcGxveV8xIjogewogICAic2hhIjogIjM0Y2QxYTM4NTZhYmEwMjkiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTg0LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTg0NSwKICAgIm00YSI6IDExMzM5CiAgfSwKICAic3luZGljYXRlX2RlcGxveV8yIjogewogICAic2hhIjogIjFhMWQ3Zjk0NDRiZTg5NjgiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTYsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDExNTc2LAogICAibTRhIjogMTEyMTMKICB9LAogICJzeW5kaWNhdGVfaG9sZF8wIjogewogICAic2hhIjogIjIyMDgzMGU5NTIxMzk5ODgiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNjU3LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDIxNiwKICAgIm00YSI6IDk2NTQKICB9LAogICJzeW5kaWNhdGVfaG9sZF8xIjogewogICAic2hhIjogIjFkZTBjMzgwNzhiYjJmN2EiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTUyLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTQzMSwKICAgIm00YSI6IDExMTMyCiAgfSwKICAic3luZGljYXRlX2hvbGRfMiI6IHsKICAgInNoYSI6ICI5ZDE1NDk1NjFiN2JiZWY4IiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjcxMywKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTA0MjIsCiAgICJtNGEiOiA5OTczCiAgfSwKICAic3luZGljYXRlX21vdmVfMCI6IHsKICAgInNoYSI6ICI4MmU1OTY0MWY1Zjk4ZWVmIiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjY1MiwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTAxNzgsCiAgICJtNGEiOiA5NzA1CiAgfSwKICAic3luZGljYXRlX21vdmVfMSI6IHsKICAgInNoYSI6ICI4MThmMjNiNzYzMjUxZGJkIiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjg3OCwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTEwMjAsCiAgICJtNGEiOiAxMDg0MQogIH0sCiAgInN5bmRpY2F0ZV9tb3ZlXzIiOiB7CiAgICJzaGEiOiAiNDM3YWVmMmNlM2Q4YjhjZSIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS4zMjMsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDg5MDAsCiAgICJtNGEiOiA4MDA2CiAgfSwKICAic3luZGljYXRlX3BhdHJvbF8wIjogewogICAic2hhIjogIjU1ZTJkMzg1YWY4ZTRmYzIiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzcsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDEwNzU0LAogICAibTRhIjogMTAyMzAKICB9LAogICJzeW5kaWNhdGVfcGF0cm9sXzEiOiB7CiAgICJzaGEiOiAiNjI3MGI4OWNlNGUxMTU3MiIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS40MzgsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDkyODMsCiAgICJtNGEiOiA4NDg0CiAgfSwKICAic3luZGljYXRlX3BhdHJvbF8yIjogewogICAic2hhIjogIjJkM2Y0MGFjNzk1OGQzOGYiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuODgxLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTAyMiwKICAgIm00YSI6IDEwODcwCiAgfSwKICAic3luZGljYXRlX3NlbGVjdF8wIjogewogICAic2hhIjogIjhjYmU4ODUyNmU4MGM1MTIiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuOTE1LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMTM1MCwKICAgIm00YSI6IDEwOTM1CiAgfSwKICAic3luZGljYXRlX3NlbGVjdF8xIjogewogICAic2hhIjogImYxMTg4ZTRhYzg1NzJhODgiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzQzLAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDU2MiwKICAgIm00YSI6IDEwMTAxCiAgfSwKICAic3luZGljYXRlX3NlbGVjdF8yIjogewogICAic2hhIjogIjhkMjg1OWY0NGE2ZjRkMTAiLAogICAidm9pY2UiOiAiYWZfYmVsbGEiLAogICAiY2hhaW4iOiAia29rb3JvLWNvbW1zLTEiLAogICAic2Vjb25kcyI6IDEuNzQ4LAogICAicGVhayI6IC0wLjcsCiAgICJvZ2ciOiAxMDM3MSwKICAgIm00YSI6IDEwMTQzCiAgfSwKICAic3luZGljYXRlX3N0b3BfMCI6IHsKICAgInNoYSI6ICJiNzcxYzUzZmZhYTgxOWIwIiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAyLjA2OSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogMTE4MDQsCiAgICJtNGEiOiAxMTc4MgogIH0sCiAgInN5bmRpY2F0ZV9zdG9wXzEiOiB7CiAgICJzaGEiOiAiMDdhN2E4YzlkNmZlYzZmOCIsCiAgICJ2b2ljZSI6ICJhZl9iZWxsYSIsCiAgICJjaGFpbiI6ICJrb2tvcm8tY29tbXMtMSIsCiAgICJzZWNvbmRzIjogMS40OTEsCiAgICJwZWFrIjogLTAuNywKICAgIm9nZyI6IDk1NjEsCiAgICJtNGEiOiA4NzcxCiAgfSwKICAic3luZGljYXRlX3N0b3BfMiI6IHsKICAgInNoYSI6ICI4ZTdiYWE5YzY0YjA2NzEwIiwKICAgInZvaWNlIjogImFmX2JlbGxhIiwKICAgImNoYWluIjogImtva29yby1jb21tcy0xIiwKICAgInNlY29uZHMiOiAxLjQ4MSwKICAgInBlYWsiOiAtMC43LAogICAib2dnIjogOTI5MiwKICAgIm00YSI6IDg3NzYKICB9CiB9Cn0K (step_camera, done_camera, greeting,
skip, graduation, react_low_power, react_hazard_default, โ€ฆ), which is what
verifykeen.mjs diffs. This used to hash the line's own TEXT, which meant the
gate at voiceOn() probed vo_keen_greeting while playback asked for
vo_keen_greeting_19hur50, no rendered take could ever match either, and
correcting a typo silently orphaned a take.
`kind` no longer contributes to the key โ€” it survives only as the bubble's
hold/emphasis class. That is deliberate: it is what lets a 'recall' replay
resolve to the SAME take as the 'step' line it is repeating instead of
demanding a second recording of identical copy. */
function keenLineId(kind,id){
if(!id) return null;
return String(id).toLowerCase().replace(/[^a-z0-9_]/g,'');
}
/* CHARACTER VOICES.
This used to be window.speechSynthesis. That could not work on the platforms
this game ships to: Android WebView often has no installed TTS voice at all
(silent no-op), iOS WKWebView gates speech behind a gesture and drops queued
utterances on backgrounding, the voice you get is whatever the host OS
happens to have, and none of it passes through the game's mixer โ€” no volume
slider, no ducking, no priority culling.
The lines are now pre-rendered by tools/make-voices.py with Piper (open
source, one distinct voice per faction) through a comms-radio filter chain,
and played as ordinary samples. speechSynthesis survives only as the
last-resort fallback for a build whose voice pack has not downloaded yet. */
function speakVoice(text,faction,action,idx,wx,wy){
if(!voiceOn()) return;
var fac=faction||'nova';
if(action&&typeof voPlay==='function'){
/* A slot the player has not heard yet is not decoded yet, and the decode is
asynchronous. Reporting "no take" at that instant is what made the FIRST
airing of every line โ€” which for KEEN is the only airing โ€” fall through to
speech synthesis. voPlay now owns the wait and calls back only if the take
genuinely cannot be produced. */
if(voPlay(fac,action,wx,wy,idx,function(){ speakVoiceFallback(text,fac); })) return;
}
speakVoiceFallback(text,fac);
}
/* Chrome returns an EMPTY voice list on the first getVoices() call and fills it
asynchronously, so the very first line of the tutorial โ€” the greeting, the
one a new player is most likely to notice missing โ€” picked no voice and, on
some builds, did not speak at all. Warm the list once at load. */
try{
if(window.speechSynthesis && window.speechSynthesis.getVoices &&
!window.speechSynthesis.getVoices().length){
window.speechSynthesis.addEventListener('voiceschanged',function once(){
window.speechSynthesis.removeEventListener('voiceschanged',once);
});
window.speechSynthesis.getVoices();
}
}catch(e){}
function speakVoiceFallback(text,faction){
try{
if(!window.speechSynthesis) return;
window.speechSynthesis.cancel();
var u=new SpeechSynthesisUtterance(String(text).replace(/[\u2192\u25c6\u26cf\ud83c\udfd7\ud83c\udfed\ud83d\udca5\u26a0\u2715]/g,''));
var v={keen:[1.06,.94],nova:[1.02,.82],ascendancy:[.92,.66],syndicate:[1.10,1.03],horde:[.78,.52]}[faction]||[1.02,.82];
u.rate=v[0]; u.pitch=v[1]; u.volume=0.85;
var voices=window.speechSynthesis.getVoices?window.speechSynthesis.getVoices():[];
if(voices.length&&faction){
var prefer=faction==='ascendancy'?'male':(faction==='syndicate'||faction==='keen')?'female':faction==='horde'?'en':null;
var chosen=voices.find(function(q){return prefer&&String(q.name).toLowerCase().indexOf(prefer)>=0;})||
voices.find(function(q){return /^en/i.test(q.lang);});
if(chosen)u.voice=chosen;
}
window.speechSynthesis.speak(u);
}catch(e){ /* degrade silently โ€” the radio panel already carries the message */ }
}
/* ---------------------------------------------------------------------------
MATCH-LEVEL SIGNAL TRACKING โ€” runs every tick regardless of whether the
tutorial is active, so replaying it mid-match can credit what the player
already demonstrably did.
--------------------------------------------------------------------------- */
function trackMatchSignals(){
if(sawQueuedUnit()) MATCH.sawQueue=true;
if(MATCH.cameraBase){
var C=MATCH.cameraBase;
if((typeof camUserT==='number'&&camUserT>0)||Math.abs(yawTarget-C.yaw)>.06||
Math.abs(pitchTarget-C.pitch)>.035||Math.abs(orthoSpan-C.span)>24)MATCH.sawCamera=true;
}
if(typeof unitHigh==='number' && (!MATCH.sawSel||!MATCH.sawCombatSel)){
for(var i=0;i<unitHigh;i++){
if(!ualive[i]||uteam[i]!==0) continue;
if(usel[i]) MATCH.sawSel=true;
if(isCombatUnit(i)&&usel[i]) MATCH.sawCombatSel=true;
if(MATCH.sawSel&&MATCH.sawCombatSel) break;
}
}
if(typeof activePlatoon==='number'&&activePlatoon>=0&&ctrlGroups[activePlatoon]&&groupLive(ctrlGroups[activePlatoon]).length)
MATCH.sawPlatoon=true;
if(typeof orderConfirm!=='undefined'&&orderConfirm)MATCH.sawFormation=true;
if(MATCH.scoutIdx>=0&&MATCH.scoutStart&&ualive[MATCH.scoutIdx]&&
Math.hypot(ux[MATCH.scoutIdx]-MATCH.scoutStart.x,uy[MATCH.scoutIdx]-MATCH.scoutStart.y)>115)MATCH.sawScout=true;
if(typeof abCool!=='undefined'){
for(var k=0;k<abCool.length;k++){
if(abCool[k]>(MATCH.lastAbCool[k]||0)+0.01) MATCH.usedAbility=true;
MATCH.lastAbCool[k]=abCool[k];
}
}
}
/* ---------------------------------------------------------------------------
TUTORIAL ENGINE
--------------------------------------------------------------------------- */
function beginRun(){
buildDOM();
TUT.active=true;
TUT.graduated=false;
TUT.stepIdx=0;
TUT.shownStepIdx=-1;
TUT.doneFlags=STEPS.map(function(){ return false; });
MATCH.sawSel=false;MATCH.sawCombatSel=false;MATCH.sawMove=false;MATCH.sawQueue=false;MATCH.sawCamera=false;
MATCH.sawPickup=false;MATCH.sawPlatoon=false;MATCH.sawFormation=false;MATCH.sawAttackMove=false;MATCH.sawScout=false;MATCH.sawRetreat=false;
MATCH.territoryAck=false;MATCH.objectiveAck=false;MATCH.cloudAck=false;MATCH.usedAbility=false;
MATCH.startCombat=combatCount();MATCH.pickup=null;MATCH.scoutIdx=-1;MATCH.scoutStart=null;
MATCH.cameraBase={yaw:yawTarget,pitch:pitchTarget,span:orthoSpan};
MATCH.lastAbCool=(typeof abCool!=='undefined')?abCool.slice():[0,0,0,0];
queue.length=0;
speak(GREETING,5.5,'greeting','greeting');
}
function evalSteps(){
if(!TUT.active) return;
for(var i=0;i<STEPS.length;i++){
if(TUT.doneFlags[i]) continue;
var ok=false;
try{ ok=!!STEPS[i].test(); }catch(e){ ok=false; }
if(ok){
TUT.doneFlags[i]=true;
if(i===TUT.stepIdx) speak(STEPS[i].done,3.8,'done','done_'+STEPS[i].id);
}
}
while(TUT.stepIdx<STEPS.length && TUT.doneFlags[TUT.stepIdx]) TUT.stepIdx++;
var progressMeta=tutMeta();
if(TUT.stepIdx>(progressMeta.progress|0)){
progressMeta.progress=TUT.stepIdx;
if(typeof metaSave==='function') metaSave();
}
if(TUT.stepIdx>=STEPS.length){
if(!TUT.graduated){
TUT.graduated=true; TUT.active=false;
var M=tutMeta(); M.done=true; M.skipped=false; M.version=GUIDE_VERSION;
M.progress=STEPS.length;
restingText=''; restingTag=''; restingId='';
speak(GRADUATION,6.5,'graduation','graduation');
if(typeof sfx==='function'){ try{ sfx('level'); }catch(e){} }
if(TUT.trainingMode&&!TUT.finishTimer) TUT.finishTimer=setTimeout(finishTrainingMission,1300);
else if(typeof metaSave==='function') metaSave();
}
return;
}
var cur=STEPS[TUT.stepIdx];
restingText=cur.say;
restingId='step_'+cur.id;
restingTag=(cur.icon||'โ—‡')+' STEP '+(TUT.stepIdx+1)+' / '+STEPS.length;
if(TUT.shownStepIdx!==TUT.stepIdx){
TUT.shownStepIdx=TUT.stepIdx;
speak(cur.say,5.5,'step',restingId);
}
}
/* SKIP ends GUIDANCE. In a training MATCH it also ends the match โ€” and it
used to do that on a single tap of a 9px transparent label sitting on the
guidance bar for the whole session, with no confirmation and no undo. The
deliberate way out (pause > ABANDON MATCH) has always asked first; the
accidental way out did not. Same question, same wording, before anything is
thrown away. Guidance-only skips stay instant: nothing is lost. */
function tutSkip(){
if(!TUT.active) return;
var liveTraining=TUT.trainingMode&&(typeof running!=='undefined'&&running)&&
!(typeof gameEnded!=='undefined'&&gameEnded);
if(liveTraining&&!tutSkipConfirmed&&typeof accConfirm==='function'){
accConfirm('Leave training and return to the menu? This operation grants no payout.',
function(){ tutSkipConfirmed=true; tutSkip(); tutSkipConfirmed=false; });
return;
}
TUT.active=false;
var wasTraining=TUT.trainingMode;
var M=tutMeta(); M.skipped=true; M.version=GUIDE_VERSION;
queue.length=0;
restingText=''; restingTag=''; restingId='';
clearFocus();
if(wasTraining){
TUT.trainingMode=false; trainingLaunched=false; restoreTrainingConfig();
if(typeof metaSave==='function') metaSave();
if(typeof returnToMainMenu==='function') returnToMainMenu();
} else if(typeof metaSave==='function') metaSave();
speak(SKIP_LINE,4.0,'skip','skip');
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
}
/* ---------------------------------------------------------------------------
REACTIVE NARRATION โ€” beyond the tutorial. Sparse on purpose: each of these
fires at most once per match. The mechanical coach in hud.js already tells
the player WHAT to fix (low energy, low mass); these only ever react to
what just happened, and never repeat that advice.
--------------------------------------------------------------------------- */
function checkReactive(){
/* KEEL is the Tutorial guide, not the selected battlefield commander.
Standard/Campaign radio belongs to the chosen faction commander. */
if(!TUT.trainingMode) return;
if(typeof running==='undefined'||!running) return;
if(typeof demoMode!=='undefined'&&demoMode) return;
/* Nothing here is meaningful before the HQ is actually down โ€” there is no
base to attack, no grid to stall, no wave to mass against. Gating on
matchLive (rather than just `running`) also means any state left over
from the tail of a PREVIOUS match โ€” a grid still reading "stalled" in
the last instant before defeat โ€” has already decayed by the time it
could matter again, instead of firing a premature bark during the next
drop before the player has even landed. */
if(typeof matchLive==='undefined'||!matchLive) return;
if(typeof alarmT!=='undefined' && alarmT>0 && alarmT!==REACT.lastAlarmT){
REACT.lastAlarmT=alarmT;
/* pick() throws the chosen index away, and the index is exactly what the
voice key needs โ€” the two variants are two separate recordings. Indexed
inline rather than changing pick(), which has other callers. */
if(!REACT.baseAttack){ REACT.baseAttack=true;
var bi=(Math.random()*BASE_ATTACK_LINES.length)|0;
speak(BASE_ATTACK_LINES[bi],5.0,'reactive','react_base_attack'+bi); }
}
if(typeof stallE!=='undefined'){
var nowStall=stallE>0;
if(nowStall && !REACT.lastStallE && !REACT.lowPower){ REACT.lowPower=true; speak(LOW_POWER_LINE,4.6,'reactive','react_low_power'); }
REACT.lastStallE=nowStall;
}
if(typeof HAZ!=='undefined'){
var nowWarn=HAZ.warn>0;
if(nowWarn && !REACT.lastHazWarn && !REACT.hazard){
REACT.hazard=true;
var key=(typeof curMap!=='undefined')?curMap:'';
/* HAZARD_LINES._default is keyed with a leading underscore so it cannot
collide with a map id; the rendered take is plain `react_hazard_default`.
Guard the underscore explicitly โ€” a map literally named "_default" would
otherwise ask for a take that was never rendered. */
var hk=(key&&key.charAt(0)!=='_'&&HAZARD_LINES[key])?key:'default';
speak(HAZARD_LINES[key]||HAZARD_LINES._default,5.2,'reactive','react_hazard_'+hk);
}
REACT.lastHazWarn=nowWarn;
}
if(typeof stats!=='undefined' && stats.kills && !REACT.unitLost && ((stats.kills[1]|0)+(stats.kills[2]|0))>0){
REACT.unitLost=true;
var ui=(Math.random()*UNIT_LOST_LINES.length)|0;
speak(UNIT_LOST_LINES[ui],4.2,'reactive','react_unit_lost'+ui);
}
if(typeof AI!=='undefined'){
if(AI.warned && !REACT.lastWarned && !REACT.wave){ REACT.wave=true; speak(WAVE_LINE,5.0,'reactive','react_wave'); }
REACT.lastWarned=!!AI.warned;
}
}
/* ---------------------------------------------------------------------------
MATCH-START DETECTION โ€” the carrier goes active/phase 0 exactly once per
drop (see newSkirmish() in src/main.js), so that edge is what arms a fresh
run and resets every once-per-match latch. Polled, not hooked: this file
never touches newSkirmish itself. */
function onNewMatchBegin(){
REACT.baseAttack=false; REACT.lowPower=false; REACT.hazard=false; REACT.unitLost=false; REACT.wave=false;
REACT.lastAlarmT=(typeof alarmT!=='undefined')?alarmT:0;
REACT.lastStallE=false; REACT.lastHazWarn=false; REACT.lastWarned=false;
MATCH.sawSel=false;MATCH.sawCombatSel=false;MATCH.sawMove=false;MATCH.sawQueue=false;MATCH.sawCamera=false;
MATCH.sawPickup=false;MATCH.sawPlatoon=false;MATCH.sawFormation=false;MATCH.sawAttackMove=false;MATCH.sawScout=false;MATCH.sawRetreat=false;
MATCH.territoryAck=false;MATCH.objectiveAck=false;MATCH.cloudAck=false;MATCH.usedAbility=false;
MATCH.startCombat=combatCount();MATCH.pickup=null;MATCH.scoutIdx=-1;MATCH.scoutStart=null;
MATCH.cameraBase={yaw:yawTarget,pitch:pitchTarget,span:orthoSpan};
MATCH.lastAbCool=(typeof abCool!=='undefined')?abCool.slice():[0,0,0,0];
if(TUT.trainingMode&&!TUT.active){ TUT.forceNext=false; beginRun(); }
}
function detectMatchEdge(){
var dropping=(typeof carrier!=='undefined')&&carrier.active&&carrier.phase===0;
if(dropping&&!prevDropping) onNewMatchBegin();
prevDropping=dropping;
}
function tutTick(){
try{
detectMatchEdge();
trackMatchSignals();
trainingSafety();
if(TUT.active) evalSteps();
checkReactive();
pump();
/* A carrier/menu transition can hide KEEL while its speech queue is still
holding the current line. TUT.active is the authoritative mission state,
so never leave an active training objective visually absent just because
the voice pump is between messages. */
if(TUT.active&&restingText) showWrap(false);
syncFocus();
updateTrainingEntry();
/* A carrier transition, a modal, or opening an interface briefly changes
`running` in some mobile builds. Treating that as an abort was the
reason a tap in training could silently throw the player to the menu.
Training now ends only through Skip, completion, or the explicit return
to main-menu transaction below. */
}catch(e){ if(window.console) console.error('tutorial tick',e); }
}
/* ---------------------------------------------------------------------------
TRAINING OPERATION โ€” an explicit, safe mission. Normal PLAY never inherits
these rules: every changed session field is snapshotted before launch and
restored on completion, skip, or an early return to the main menu.
--------------------------------------------------------------------------- */
var TRAINING_REWARD=150;
function saveTrainingConfig(){
trainingPrev={difficulty:difficulty,defenseFocus:defenseFocus,infestationOn:infestationOn,
wcChoice:wcChoice,goalSel:goalSel,timeLimit:timeLimit,resPace:resPace,crateRate:crateRate,
curMap:curMap,curTheme:curTheme,aiFactionSel:aiFactionSel,playerStartZone:playerStartZone,
activeWarMode:typeof activeWarMode==='string'?activeWarMode:'standard',
aiSlots:aiSlots.map(function(A){return {on:!!A.on,diff:A.diff|0,zone:A.zone,ally:!!A.ally,behavior:A.behavior||'balanced'};}),
opmods:Object.assign({},META.opmods||{}),threatSel:META.threatSel};
}
function restoreTrainingConfig(){
if(!trainingPrev) return;
difficulty=trainingPrev.difficulty; defenseFocus=trainingPrev.defenseFocus;
infestationOn=trainingPrev.infestationOn; wcChoice=trainingPrev.wcChoice;
goalSel=trainingPrev.goalSel; timeLimit=trainingPrev.timeLimit;
resPace=trainingPrev.resPace; crateRate=trainingPrev.crateRate;
curMap=trainingPrev.curMap; curTheme=trainingPrev.curTheme;
aiFactionSel=trainingPrev.aiFactionSel; playerStartZone=trainingPrev.playerStartZone;
if(typeof activeWarMode!=='undefined') activeWarMode=trainingPrev.activeWarMode||'standard';
for(var i=0;i<aiSlots.length;i++){
var A=trainingPrev.aiSlots[i]; if(!A) continue;
aiSlots[i].on=A.on; aiSlots[i].diff=A.diff; aiSlots[i].zone=A.zone;aiSlots[i].ally=!!A.ally;aiSlots[i].behavior=A.behavior||'balanced';
}
META.opmods=trainingPrev.opmods; META.threatSel=trainingPrev.threatSel;
trainingPrev=null;
}
function cancelTrainingMission(){
if(!TUT.trainingMode&&!trainingPrev) return;
TUT.trainingMode=false; trainingLaunched=false; TUT.active=false; TUT.finishTimer=0;
queue.length=0; restingText=''; restingTag=''; restingId=''; clearFocus(); hideWrap();
document.body.classList.remove('trainingOperation');
restoreTrainingConfig();
/* Recover old OTA saves that retained trainingMode after losing the borrowed
config snapshot; an invisible training mode must never reach Standard. */
if(typeof activeWarMode!=='undefined'&&activeWarMode==='training') activeWarMode='standard';
updateTrainingEntry();
}
function trainingStepId(){
return TUT.active&&STEPS[TUT.stepIdx]?STEPS[TUT.stepIdx].id:'';
}
function ensureTrainingPickup(){
if(MATCH.sawPickup||(MATCH.pickup&&crates.indexOf(MATCH.pickup)>=0))return;
var H=playerBld('hq');if(!H||typeof spawnCrate!=='function')return;
var C=spawnCrate(H.x+155,H.y-55,'supply');
C.alt=0;C.seen=true;C.t=0;C.site=true;C.siteName='TRAINING CACHE';C.announced=true;
MATCH.pickup=C;
if(typeof mmPing==='function')mmPing(C.x,C.y);
}
function ensureTrainingScout(){
if(MATCH.scoutIdx>=0&&ualive[MATCH.scoutIdx])return;
var H=playerBld('hq'),ty=-1;if(!H)return;
for(var i=0;i<TYPES.length;i++)if(TYPES[i]&&TYPES[i].scout){ty=i;break;}
if(ty<0)return;
var u=spawnUnit(ty,0,H.x+95,H.y+55);if(u<0)return;
clearSel();usel[u]=1;MATCH.scoutIdx=u;MATCH.scoutStart={x:ux[u],y:uy[u]};
updateSelInfo();cam.x=ux[u];cam.y=uy[u];clampCam();camUpdateMatrices();
toast('โŒพ TRAINING SCOUT DEPLOYED โ€” move it into black fog');
}
function trainingSafety(){
if(!TUT.trainingMode||typeof running==='undefined'||!running||typeof matchLive==='undefined'||!matchLive) return;
/* No scripted ambush while the player is reading. The enemy still builds a
visible base and army, but its attack clocks remain parked until training
completes. A resource floor prevents one mistaken duplicate placement from
making the required lesson unaffordable. */
if(typeof AI!=='undefined'){
AI.waveTimer=Math.max(AI.waveTimer||0,300); AI.harassTimer=Math.max(AI.harassTimer||0,300); AI.warned=false;
}
if(typeof resM!=='undefined'&&typeof RES_MCAP!=='undefined') resM[0]=Math.min(RES_MCAP[0],Math.max(resM[0],620));
if(typeof resE!=='undefined'&&typeof RES_ECAP!=='undefined') resE[0]=Math.min(RES_ECAP[0],Math.max(resE[0],2200));
if(typeof heroIdx==='number'&&heroIdx>=0&&ualive[heroIdx]) uhp[heroIdx]=Math.max(uhp[heroIdx],uhpm[heroIdx]*.72);
var H=playerBld('hq'); if(H) H.hp=Math.max(H.hp,H.hpm*.75);
var step=trainingStepId();
if(step==='pickup')ensureTrainingPickup();
if(step==='fog')ensureTrainingScout();
if(step==='tech'&&typeof heroLvl==='number')heroLvl=Math.max(heroLvl,3);
}
function startTrainingMission(){
if(typeof running!=='undefined'&&running){ toast('Leave the current battle before starting training'); return; }
if(!trainingPrev) saveTrainingConfig();
difficulty=0; defenseFocus=0; infestationOn=false; wcChoice=0;
goalSel='annihilate'; timeLimit=0; resPace=1; crateRate=1;
curMap='vanguard'; curTheme='verdant'; aiFactionSel='legion'; playerStartZone='sw';
aiSlots[0].on=true; aiSlots[0].diff=0; aiSlots[0].zone='ne';aiSlots[0].ally=false;aiSlots[0].behavior='balanced';
for(var i=1;i<aiSlots.length;i++){aiSlots[i].on=false;aiSlots[i].ally=false;aiSlots[i].behavior='balanced';}
META.opmods={}; META.threatSel=1;
var M=tutMeta(); M.skipped=false;
TUT.trainingMode=true; TUT.forceNext=true; TUT.finishTimer=0; trainingLaunched=false;
/* Training is a real dedicated mode, not a Standard match with tutorial
chrome painted over it. The saved mode is restored by the same transaction
as the fixed map/rules, so reward and menu state cannot misclassify it. */
if(typeof activeWarMode!=='undefined') activeWarMode='training';
buildDOM(); document.body.classList.add('trainingOperation');
/* The menu diorama can also animate a phase-0 carrier. Reset the edge latch
before newSkirmish so that decorative drop cannot consume the real
Training Operation's start signal. */
prevDropping=false;
if(typeof initAudio==='function') initAudio();
if(typeof hideFrontScreens==='function') hideFrontScreens();
var load=document.getElementById('loadScr'); if(load) load.style.display='flex';
requestAnimationFrame(function(){requestAnimationFrame(function(){
applyTheme(); newSkirmish();fogOn=true;updateFog();trainingLaunched=true;
/* The carrier edge is also observed by the normal polling path, but a
loaded/fast device can advance the drop state between two 350ms polls.
Training launch is the authoritative transaction, so arm guidance here
and let the edge detector's !active guard avoid a second reset. */
if(!TUT.active){TUT.forceNext=false;beginRun();}
if(load) load.style.display='none';
/* The loader counted as a front screen during the first layout. Recompute
now so the dedicated Tutorial exposes KEEL and its deployment HUD. */
if(typeof stopAttract==='function') stopAttract();
if(typeof mfFlowLayout==='function') mfFlowLayout();
toast('TRAINING OPERATION โ€” war table skipped; threat paused while KEEL guides you');
});});
}
function finishTrainingMission(){
TUT.finishTimer=0;
if(!TUT.trainingMode) return;
var M=tutMeta(),reward=(M.rewardedVersion|0)<GUIDE_VERSION?TRAINING_REWARD:0;
M.done=true; M.skipped=false; M.version=GUIDE_VERSION;
M.progress=STEPS.length;
if(reward){ META.cores=(META.cores||0)+reward; M.rewardedVersion=GUIDE_VERSION; }
TUT.trainingMode=false; trainingLaunched=false; TUT.active=false;
document.body.classList.remove('trainingOperation');
clearFocus(); restoreTrainingConfig();
if(typeof metaSave==='function') metaSave();
if(typeof renderMetaHead==='function') renderMetaHead();
if(typeof gameEnded!=='undefined') gameEnded=true;
if(typeof paused!=='undefined') paused=false;
if(typeof running!=='undefined') running=false;
var title=document.getElementById('goTitle'); if(title){title.textContent='TRAINING COMPLETE';title.style.color='#9fffc4';}
var out=document.getElementById('goOutcome'); if(out) out.textContent='FIELD ORIENTATION ยท ALL '+STEPS.length+' OBJECTIVES COMPLETE';
var gs=document.getElementById('goStats');
if(gs) gs.innerHTML='<div class="goStatGrid"><div><b>'+STEPS.length+' / '+STEPS.length+'</b><span>OBJECTIVES</span></div>'
+'<div><b>'+((stats&&stats.built&&stats.built[0])|0)+'</b><span>STRUCTURES BUILT</span></div>'
+'<div><b>'+((stats&&stats.t)|0)+'s</b><span>TRAINING TIME</span></div>'
+'<div><b>READY</b><span>FIELD STATUS</span></div></div>';
var rw=document.getElementById('goRewards');
if(rw) rw.innerHTML='<section class="goSection"><h3>TRAINING PAYOUT</h3><div class="goPayout">'
+'<div><b>+'+reward+'</b><span>โฌก CORES</span></div><div><b>COMPLETE</b><span>KEEL ORIENTATION</span></div></div>'
+'<div class="goNotice good">NORMAL SKIRMISHES ARE NOW READY ยท REPLAY ANY TIME FROM SETTINGS OR OPERATIONS</div></section>';
if(typeof drawMatchChart==='function') drawMatchChart();
var go=document.getElementById('gameOver'); if(go) go.style.display='flex';
if(typeof sfx==='function'){ try{sfx('level');}catch(e){} }
updateTrainingEntry();
}
function needsTraining(){
var M=tutMeta(); return !M.skipped&&(!M.done||(M.version|0)<GUIDE_VERSION);
}
function trainingUiState(){
var M=tutMeta(),done=!!M.done&&(M.version|0)>=GUIDE_VERSION,
active=!!TUT.trainingMode&&!!TUT.active&&typeof running!=='undefined'&&!!running,
progress=done?STEPS.length:(active?TUT.stepIdx:Math.min(STEPS.length-1,M.progress|0)),
interrupted=!done&&!active&&(progress>0||!!M.skipped),rewarded=(M.rewardedVersion|0)>=GUIDE_VERSION;
return {done:done,active:active,interrupted:interrupted,progress:progress,rewarded:rewarded,
state:done?'COMPLETED ยท REPLAYABLE':active?'TRAINING PAUSED ยท RESUMABLE':interrupted?'INCOMPLETE ยท RESTARTABLE':'RECOMMENDED ยท SKIPS WAR TABLE',
action:done?'โ†ป REPLAY TRAINING':active?'โ–ถ RESUME TRAINING':interrupted?'โ†ป RESTART TRAINING':'โ–ถ START TRAINING'};
}
function updateTrainingEntry(){
var S=trainingUiState();
/* The training card's label and state line come from trainingUiState(), the
same source the Operations card reads, so the two can never disagree. If
the War Room is on screen, refresh it in place. */
var wr=document.getElementById('warScr');
if(wr&&wr.style.display&&wr.style.display!=='none'&&typeof renderWarRoom==='function') renderWarRoom();
var op=document.getElementById('keelTrainingOp'),sig=[S.done,S.active,S.interrupted,S.progress,S.rewarded].join(':');
if(op&&op.dataset.stateSig!==sig) appendTrainingOperation();
}
function openTrainingBrief(){
if(typeof renderOps==='function') renderOps();
if(typeof showFrontScreen==='function') showFrontScreen('opsScr');
var ops=document.getElementById('opsScr');
if(ops&&typeof mfSetTabs==='function') mfSetTabs(ops,'threat',false);
var sc=ops&&ops.querySelector('.opsScroll'); if(sc) sc.scrollTop=0;
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
}
function resumeTrainingMission(){
var S=trainingUiState();
if(!S.active){ startTrainingMission(); return; }
if(typeof hideFrontScreens==='function') hideFrontScreens();
var po=document.getElementById('pauseOverlay'); if(po) po.style.display='none';
if(typeof paused!=='undefined') paused=false;
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
}
function ensureTrainingEntry(){
/* The old entry was an identity CARD inserted BEFORE #startBtn, so the first
thing on the menu was a training advert and PLAY got pushed down the screen
behind it. Training is one destination, not a banner: it is now a single
dedicated button sitting directly UNDER PLAY, styled as a peer of it
(.mbtn.alt) so the two read as one primary + one secondary action.
openTrainingBrief() is still reachable from Operations and Settings. */
/* Training is a WAR ROOM card now, not a menu button โ€” the menu is back to a
single primary control. Both of this function's older entries (the identity
callout above PLAY, and the standalone #trainBtn below it) are torn down
here so an OTA patch landing on either shell converges on the same menu. */
var stale=document.getElementById('keelTrainingCallout'); if(stale) stale.remove();
var oldBtn=document.getElementById('trainBtn'); if(oldBtn) oldBtn.remove();
updateTrainingEntry();
}
function appendTrainingOperation(){
var pane=document.getElementById('opsPane-threat'); if(!pane) return;
var old=document.getElementById('keelTrainingOp'); if(old) old.remove();
var S=trainingUiState(),nova=typeof facArt==='function'?facArt('nova'):null,
commander=(nova&&nova.cdr)||'Captain Elara Kai',progressPct=Math.round(S.progress/STEPS.length*100),
rewardText=S.rewarded?'FIRST-CLEAR REWARD CLAIMED':'FIRST CLEAR ยท +'+TRAINING_REWARD+' โฌก CORES';
var d=document.createElement('article'); d.id='keelTrainingOp';
d.className='keelTrainingOp'+(S.done?' done':'')+(S.active?' active':'');
d.dataset.stateSig=[S.done,S.active,S.interrupted,S.progress,S.rewarded].join(':');
d.dataset.progress=String(S.progress);
d.innerHTML='<header class="ktoHead"><span>'+S.state+'</span><b>'+rewardText+'</b></header>'
+'<div class="ktoHero"><div class="ktoPortrait"><img src="./assets/factions/nova_192.jpg" alt="'+commander+', Nova commander">'
+'<span>'+(typeof facIcon==='function'?facIcon('nova',38,'ktoCrestImg'):'โœฆ')+'</span></div>'
+'<div class="ktoIdentity"><small>TRAINING OPERATION 01</small><b>FIELD ORIENTATION</b>'
+'<i>'+commander+' ยท KEEL tactical guidance</i>'
+'<p>A protected live-fire drop. Skips the galaxy war table and lands on a fixed training map with no early enemy rush.</p></div></div>'
+'<div class="ktoTeach" aria-label="Training topics"><span>โ—‡ CAMERA</span><span>โฌก ECONOMY</span><span>โ–ฃ PRODUCTION</span>'
+'<span>โžค ORDERS</span><span>n/1K POP</span><span>โ›จ DEFENCE</span><span>โ…ฃ PLATOONS</span><span>โŒพ SCOUTING</span>'
+'<span>โŒฌ TECH</span><span>โœฆ POWERS</span><span>โ˜ SAVES</span></div>'
+'<div class="ktoProgress"><div><span>OBJECTIVE PROGRESS</span><b>'+S.progress+' / '+STEPS.length+'</b></div>'
+'<div class="ktoProgressTrack"><i style="width:'+progressPct+'%"></i></div>'
+'<small>'+(S.done?'Orientation certified ยท replay does not repeat the first-clear reward':
S.active?'Current lesson: '+(STEPS[Math.min(TUT.stepIdx,STEPS.length-1)].id||'field systems').toUpperCase():
S.interrupted?'Previous best retained ยท a new drop restarts at camera control':STEPS.length+' guided objectives ยท progress is saved to this profile')+'</small></div>'
+'<div class="ktoFacts"><div><b>'+STEPS.length+'</b><span>OBJECTIVES</span></div><div><b>EASY</b><span>FIXED OPPONENT</span></div>'
+'<div><b>SAFE</b><span>NO INFESTATION</span></div></div>'
+'<button class="ktoAction" type="button">'+S.action+'</button>';
pane.insertBefore(d,pane.firstChild);
var b=d.querySelector('button');
var act=S.active?resumeTrainingMission:startTrainingMission;
if(typeof mfBindTap==='function') mfBindTap(b,act); else b.addEventListener('click',act);
}
/* ---------------------------------------------------------------------------
SETTINGS โ€” a "Tutorial" row and a voice toggle, appended to #setList after
renderSettings() (src/game/meta.js) rebuilds it. That function replaces
#setList's entire innerHTML on every call โ€” including every time a player
flips ANY setting โ€” so appending once at boot would vanish on the first
unrelated toggle. Wrapping the function, the same way commander.js wraps
econTick() and meta.js wraps heroXP(), means our row survives every
rebuild without ever editing meta.js. */
function tutSettingsAction(){
if(trainingUiState().active){
resumeTrainingMission();
return;
}
var midMatch=typeof running!=='undefined'&&running;
if(midMatch){
toast('Training is a separate operation โ€” return to the main menu to replay it');
} else {
startTrainingMission();
}
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
if(midMatch&&typeof renderSettings==='function') renderSettings();
}
function appendTutorialSettingsRow(){
var list=document.getElementById('setList');
if(!list) return;
var M=tutMeta();
var status=TUT.active? ('Training in progress โ€” step '+(TUT.stepIdx+1)+' of '+STEPS.length)
: M.done? 'Completed'
: M.skipped? 'Skipped'
: 'Not started yet';
var label=trainingUiState().active?'โ–ถ RESUME':(!M.done&&!M.skipped&&!TUT.active)? 'โ–ถ START' : 'โ†บ REPLAY';
var row=document.createElement('div');
row.className='sItem setRow';
row.id='keelSetRow';
row.setAttribute('role','button');
row.setAttribute('tabindex','0');
row.innerHTML='<div class="sTx"><b>๐ŸŽ“ Training Operation</b><div class="sDs">'+status+' โ€” skips the war table; protected mission with guided controls</div></div>'
+'<div class="sBuy">'+label+'</div>';
var act=function(ev){ if(ev){ ev.preventDefault(); } tutSettingsAction(); };
row.addEventListener('pointerdown',act);
row.addEventListener('keydown',function(ev){ if(ev.key==='Enter'||ev.key===' ') act(ev); });
list.appendChild(row);
var vOn=!!(typeof META!=='undefined'&&META.settings&&META.settings.tutorialVoice);
var vrow=document.createElement('div');
vrow.className='sItem setRow';
vrow.setAttribute('role','button');
vrow.setAttribute('tabindex','0');
vrow.innerHTML='<div class="sTx"><b>๐Ÿ”Š KEEL Voice</b><div class="sDs">Speak tutorial lines aloud, if your device supports it</div></div>'
+'<div class="sBuy togB'+(vOn?' onT':'')+'">'+(vOn?'ON':'OFF')+'</div>';
var vact=function(ev){
if(ev){ ev.preventDefault(); }
if(typeof META==='undefined'||!META.settings) return;
META.settings.tutorialVoice=!META.settings.tutorialVoice;
if(typeof metaSave==='function') metaSave();
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
if(typeof renderSettings==='function') renderSettings();
};
vrow.addEventListener('pointerdown',vact);
vrow.addEventListener('keydown',function(ev){ if(ev.key==='Enter'||ev.key===' ') vact(ev); });
list.appendChild(vrow);
}
if(typeof renderSettings==='function'){
var _keelRenderSettings=renderSettings;
renderSettings=function(){
_keelRenderSettings();
appendTutorialSettingsRow();
};
}
/* ---------------------------------------------------------------------------
BOOT
--------------------------------------------------------------------------- */
function initTutorial(){
if(window.__keelInit) return;
window.__keelInit=true;
buildDOM();
ensureTrainingEntry();
if(typeof renderOps==='function'&&!window.__keelOpsWrapped){
window.__keelOpsWrapped=true;
var _keelRenderOps=renderOps;
renderOps=function(){ _keelRenderOps(); appendTrainingOperation(); };
appendTrainingOperation();
}
if(typeof orderMove==='function'&&!window.__keelMoveWrapped){
window.__keelMoveWrapped=true;
var _keelOrderMove=orderMove;
orderMove=function(wx,wy,patrol,retreat){
var combat=hasSelectedCombatUnit(),formationDone=MATCH.sawFormation,attackMode=typeof moveMode==='undefined'||!moveMode,
ok=_keelOrderMove(wx,wy,patrol,retreat);
/* Retreat is the one order the player cannot reach from a button, so
it is credited from any live selection rather than a combat-only
one โ€” the Commander breaking off counts as having learned it. */
if(ok&&retreat&&!patrol) MATCH.sawRetreat=true;
if(ok&&combat&&!patrol){
MATCH.sawMove=true;
/* The formation release itself also calls orderMove. Requiring a
formation observed on an earlier poll ensures the next deliberate
map tap teaches attack-move instead of silently auto-completing. */
if(formationDone&&attackMode&&!retreat)MATCH.sawAttackMove=true;
}
return ok;
};
}
if(typeof applyCrate==='function'&&!window.__keelPickupWrapped){
window.__keelPickupWrapped=true;
var _keelApplyCrate=applyCrate;
applyCrate=function(k,x,y){
var out=_keelApplyCrate(k,x,y);
if(TUT.trainingMode)MATCH.sawPickup=true;
return out;
};
}
setInterval(tutTick,350);
}
window.initTutorial=initTutorial;
/* Main-menu navigation owns the application-level exit transaction. Export
the training cleanup explicitly so returnToMainMenu() can actually call it;
this file is intentionally scoped in an IIFE, so a bare function declaration
was never global and the old typeof guard silently did nothing. */
window.cancelTrainingMission=cancelTrainingMission;
window.trainingMissionActive=function(){return !!(TUT.trainingMode||trainingPrev);};
/* Inspection hook for automated verification โ€” read-only, harmless to leave
wired for real players (no different from any other console-reachable
global in this codebase). */
window.__tutDebug=function(){ return {TUT:TUT,MATCH:MATCH,REACT:REACT,STEPS:STEPS,
needsTraining:needsTraining(),startTraining:startTrainingMission,
/* The voice plumbing lives inside this IIFE, so verifykeen.mjs cannot reach
speak()/keenLineId()/pump() any other way. Read-only handles, no state. */
speak:speak,pump:pump,queue:queue,voiceOn:voiceOn,keenLineId:keenLineId,
speakVoice:speakVoice,beginRun:beginRun,
keenKey:function(kind,id){ return 'vo_keen_'+keenLineId(kind,id); }}; };
})();