Spaces:
Running
Running
MASSFRONT v1.32.24 — header redesign, EMP ability, stim fix
Browse files- assets/app.webmanifest +3 -3
- boot.js +1 -1
- index.html +29 -20
- src/game/commander.js +43 -2
- src/game/meta.js +13 -6
- src/game/sim.js +18 -4
- src/main.js +2 -1
- src/styles/ui.css +46 -4
- src/ui/hud.js +4 -2
- src/updater.js +1 -1
assets/app.webmanifest
CHANGED
|
@@ -11,17 +11,17 @@
|
|
| 11 |
"theme_color": "#060c16",
|
| 12 |
"icons": [
|
| 13 |
{
|
| 14 |
-
"src": "./icons/icon-192.png?v=1.32.
|
| 15 |
"sizes": "192x192",
|
| 16 |
"type": "image/png"
|
| 17 |
},
|
| 18 |
{
|
| 19 |
-
"src": "./icons/icon-512.png?v=1.32.
|
| 20 |
"sizes": "512x512",
|
| 21 |
"type": "image/png"
|
| 22 |
},
|
| 23 |
{
|
| 24 |
-
"src": "./icons/icon-1024.png?v=1.32.
|
| 25 |
"sizes": "1024x1024",
|
| 26 |
"type": "image/png",
|
| 27 |
"purpose": "any maskable"
|
|
|
|
| 11 |
"theme_color": "#060c16",
|
| 12 |
"icons": [
|
| 13 |
{
|
| 14 |
+
"src": "./icons/icon-192.png?v=1.32.24",
|
| 15 |
"sizes": "192x192",
|
| 16 |
"type": "image/png"
|
| 17 |
},
|
| 18 |
{
|
| 19 |
+
"src": "./icons/icon-512.png?v=1.32.24",
|
| 20 |
"sizes": "512x512",
|
| 21 |
"type": "image/png"
|
| 22 |
},
|
| 23 |
{
|
| 24 |
+
"src": "./icons/icon-1024.png?v=1.32.24",
|
| 25 |
"sizes": "1024x1024",
|
| 26 |
"type": "image/png",
|
| 27 |
"purpose": "any maskable"
|
boot.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
| 35 |
otherwise reuse a stale source even after the installer or local preview
|
| 36 |
has changed, which made new settings appear to be missing until cache was
|
| 37 |
cleared manually. Patch bundles keep their content-addressed Blob URLs. */
|
| 38 |
-
var PACKAGED_REV='1.32.
|
| 39 |
var DB='massfront-updates', STORE='bundles';
|
| 40 |
var bootShield=null, bootShieldTimer=0, bootShieldWatchdog=0;
|
| 41 |
var bootShieldEvents=['pointerdown','pointerup','touchstart','touchend','click'];
|
|
|
|
| 35 |
otherwise reuse a stale source even after the installer or local preview
|
| 36 |
has changed, which made new settings appear to be missing until cache was
|
| 37 |
cleared manually. Patch bundles keep their content-addressed Blob URLs. */
|
| 38 |
+
var PACKAGED_REV='1.32.24';
|
| 39 |
var DB='massfront-updates', STORE='bundles';
|
| 40 |
var bootShield=null, bootShieldTimer=0, bootShieldWatchdog=0;
|
| 41 |
var bootShieldEvents=['pointerdown','pointerup','touchstart','touchend','click'];
|
index.html
CHANGED
|
@@ -9,19 +9,19 @@
|
|
| 9 |
<meta name="mobile-web-app-capable" content="yes">
|
| 10 |
<meta name="theme-color" content="#060c16">
|
| 11 |
<meta name="format-detection" content="telephone=no">
|
| 12 |
-
<link rel="apple-touch-icon" href="./assets/icons/icon-180.png?v=1.32.
|
| 13 |
-
<link rel="apple-touch-icon" sizes="152x152" href="./assets/icons/icon-152.png?v=1.32.
|
| 14 |
-
<link rel="apple-touch-startup-image" href="./assets/icons/splash-2732.png?v=1.32.
|
| 15 |
-
<link rel="icon" href="./assets/icons/icon-192.png?v=1.32.
|
| 16 |
-
<link rel="manifest" href="./assets/app.webmanifest?v=1.32.
|
| 17 |
<title>MASSFRONT — Mobile RTS</title>
|
| 18 |
-
<link rel="stylesheet" href="./src/styles/ui.css?v=1.32.
|
| 19 |
-
<link rel="stylesheet" href="./src/styles/auth.css?v=1.32.
|
| 20 |
-
<link rel="stylesheet" href="./src/styles/tutorial.css?v=1.32.
|
| 21 |
-
<link rel="stylesheet" href="./src/styles/ads.css?v=1.32.
|
| 22 |
-
<link rel="stylesheet" href="./src/styles/store.css?v=1.32.
|
| 23 |
-
<link rel="stylesheet" href="./src/styles/restree.css?v=1.32.
|
| 24 |
-
<link rel="stylesheet" href="./src/styles/intro.css?v=1.32.
|
| 25 |
<style>
|
| 26 |
/* CRITICAL — inline so it holds on the FIRST painted frame even if the linked
|
| 27 |
intro.css above is slow or fails. The launch reveal (src/intro.js, z-index 950)
|
|
@@ -114,6 +114,7 @@ body:not(.mfIntroDone) .overlay{display:none!important}
|
|
| 114 |
<button class="cbtn abtn" id="abHeal"><span class="em">🛠</span><span>Repair</span><div class="cdring"></div></button>
|
| 115 |
<button class="cbtn abtn" id="abRage"><span class="em">⚡</span><span>Surge</span><div class="cdring"></div></button>
|
| 116 |
<button class="cbtn abtn" id="abLance"><span class="em">🛰</span><span>Lance</span><div class="cdring"></div></button>
|
|
|
|
| 117 |
<button class="cbtn abtn" id="abJump" aria-label="Commander jump jets"><span class="em">↗</span><span>Jump</span><div class="cdring"></div></button>
|
| 118 |
<button class="cbtn abtn" id="abClass" style="display:none" aria-label="Selected unit class ability"><span class="em" id="classAbEm">➤</span><span id="classAbNm">Class</span><div class="cdring"></div></button>
|
| 119 |
<button class="cbtn abtn" id="abBarrage" aria-label="Charged Barrage" title="Charged Barrage"><span class="em">☄</span><span>Barrage</span><div class="cdring"></div></button>
|
|
@@ -154,14 +155,22 @@ body:not(.mfIntroDone) .overlay{display:none!important}
|
|
| 154 |
tappable, because it is the shortest route to the screen the rest of it
|
| 155 |
moved to — plus the cores/record box, which is a live figure rather
|
| 156 |
than a progression readout. -->
|
| 157 |
-
<!--
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
| 161 |
<div id="metaHead">
|
| 162 |
-
<button type="button" id="rankEm" aria-label="Open your profile and career record">
|
| 163 |
-
|
| 164 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
</div>
|
| 166 |
<!-- The supplied metallic MASSFRONT key art replaces the old drawn shield SVG
|
| 167 |
and the CSS wordmark/tagline, so the menu carries the same brand as the
|
|
@@ -202,7 +211,7 @@ body:not(.mfIntroDone) .overlay{display:none!important}
|
|
| 202 |
<div class="updHead">
|
| 203 |
<div>
|
| 204 |
<div id="updTxt">GAME VERSION</div>
|
| 205 |
-
<div id="updSub">v1.32.
|
| 206 |
</div>
|
| 207 |
<div class="updBtns">
|
| 208 |
<button id="updCancel" style="display:none">✕</button>
|
|
|
|
| 9 |
<meta name="mobile-web-app-capable" content="yes">
|
| 10 |
<meta name="theme-color" content="#060c16">
|
| 11 |
<meta name="format-detection" content="telephone=no">
|
| 12 |
+
<link rel="apple-touch-icon" href="./assets/icons/icon-180.png?v=1.32.24">
|
| 13 |
+
<link rel="apple-touch-icon" sizes="152x152" href="./assets/icons/icon-152.png?v=1.32.24">
|
| 14 |
+
<link rel="apple-touch-startup-image" href="./assets/icons/splash-2732.png?v=1.32.24">
|
| 15 |
+
<link rel="icon" href="./assets/icons/icon-192.png?v=1.32.24">
|
| 16 |
+
<link rel="manifest" href="./assets/app.webmanifest?v=1.32.24">
|
| 17 |
<title>MASSFRONT — Mobile RTS</title>
|
| 18 |
+
<link rel="stylesheet" href="./src/styles/ui.css?v=1.32.24">
|
| 19 |
+
<link rel="stylesheet" href="./src/styles/auth.css?v=1.32.24">
|
| 20 |
+
<link rel="stylesheet" href="./src/styles/tutorial.css?v=1.32.24">
|
| 21 |
+
<link rel="stylesheet" href="./src/styles/ads.css?v=1.32.24">
|
| 22 |
+
<link rel="stylesheet" href="./src/styles/store.css?v=1.32.24">
|
| 23 |
+
<link rel="stylesheet" href="./src/styles/restree.css?v=1.32.24">
|
| 24 |
+
<link rel="stylesheet" href="./src/styles/intro.css?v=1.32.24">
|
| 25 |
<style>
|
| 26 |
/* CRITICAL — inline so it holds on the FIRST painted frame even if the linked
|
| 27 |
intro.css above is slow or fails. The launch reveal (src/intro.js, z-index 950)
|
|
|
|
| 114 |
<button class="cbtn abtn" id="abHeal"><span class="em">🛠</span><span>Repair</span><div class="cdring"></div></button>
|
| 115 |
<button class="cbtn abtn" id="abRage"><span class="em">⚡</span><span>Surge</span><div class="cdring"></div></button>
|
| 116 |
<button class="cbtn abtn" id="abLance"><span class="em">🛰</span><span>Lance</span><div class="cdring"></div></button>
|
| 117 |
+
<button class="cbtn abtn" id="abEmp"><span class="em">⚡</span><span>EMP</span><div class="cdring"></div></button>
|
| 118 |
<button class="cbtn abtn" id="abJump" aria-label="Commander jump jets"><span class="em">↗</span><span>Jump</span><div class="cdring"></div></button>
|
| 119 |
<button class="cbtn abtn" id="abClass" style="display:none" aria-label="Selected unit class ability"><span class="em" id="classAbEm">➤</span><span id="classAbNm">Class</span><div class="cdring"></div></button>
|
| 120 |
<button class="cbtn abtn" id="abBarrage" aria-label="Charged Barrage" title="Charged Barrage"><span class="em">☄</span><span>Barrage</span><div class="cdring"></div></button>
|
|
|
|
| 155 |
tappable, because it is the shortest route to the screen the rest of it
|
| 156 |
moved to — plus the cores/record box, which is a live figure rather
|
| 157 |
than a progression readout. -->
|
| 158 |
+
<!-- Identity first, economy second. The old header put the rank ladder, the
|
| 159 |
+
cores and the record on one crowded line and pushed the whole thing under
|
| 160 |
+
the OS status bar. Now: a profile icon carrying the account level, the
|
| 161 |
+
name beside it, the mailbox in the corner OF THE HEADER (so it cannot
|
| 162 |
+
overlap the numbers the way a free-floating button did), and the
|
| 163 |
+
resources on their own row underneath. -->
|
| 164 |
<div id="metaHead">
|
| 165 |
+
<button type="button" id="rankEm" aria-label="Open your profile and career record">
|
| 166 |
+
<span id="rankEmGlyph">🎖</span><span id="rankLvl">1</span>
|
| 167 |
+
</button>
|
| 168 |
+
<div id="rankGreet"><b id="greetLine">COMMANDER</b><span id="greetSub">Tap your profile for career details</span></div>
|
| 169 |
+
<button type="button" id="inboxBtn" aria-label="Inbox"><span class="sEmj">✉</span></button>
|
| 170 |
+
<div id="metaRes">
|
| 171 |
+
<div class="metaResCell"><span class="lbl">CORES</span><b id="coreV">0</b></div>
|
| 172 |
+
<div class="metaResCell"><span class="lbl">RECORD</span><b id="metaRec">0W / 0L</b></div>
|
| 173 |
+
</div>
|
| 174 |
</div>
|
| 175 |
<!-- The supplied metallic MASSFRONT key art replaces the old drawn shield SVG
|
| 176 |
and the CSS wordmark/tagline, so the menu carries the same brand as the
|
|
|
|
| 211 |
<div class="updHead">
|
| 212 |
<div>
|
| 213 |
<div id="updTxt">GAME VERSION</div>
|
| 214 |
+
<div id="updSub">v1.32.24</div>
|
| 215 |
</div>
|
| 216 |
<div class="updBtns">
|
| 217 |
<button id="updCancel" style="display:none">✕</button>
|
src/game/commander.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
| 3 |
============================================================ */
|
| 4 |
let heroLvl=1, heroXp=0, heroXpNext=100;
|
| 5 |
let heroDmgMult=1, heroRegen=14;
|
| 6 |
-
let abCool=[0,0,0,0];
|
| 7 |
-
const AB_CD=[26,20,30,70];
|
| 8 |
let heroJumpCool=0;
|
| 9 |
const HERO_JUMP={range:275,cool:28,energy:170};
|
| 10 |
let heroStuckFor=0,heroRescueAt=0;
|
|
@@ -281,6 +281,15 @@ const UPGRADES=[
|
|
| 281 |
{em:'💥', nm:'Wider Blast', ds:'Orbital Blast radius +35%', fn:()=>{ blastRadius*=1.35; }},
|
| 282 |
];
|
| 283 |
let bonusMass=0, bonusEnergy=0, armyDmgMult=1, blastRadius=110;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
function flashScreen(){
|
| 285 |
const f=document.getElementById('flash');
|
| 286 |
if(!f) return;
|
|
@@ -319,6 +328,7 @@ function tryAbility(k){
|
|
| 319 |
if(heroIdx<0){ toast('Commander is down'); return; }
|
| 320 |
if(!abUnlock[k]){
|
| 321 |
toast(k===3?'🛰 Buy the Orbital Uplink in the Armory to unlock this'
|
|
|
|
| 322 |
:'Unlocks at Commander level '+(k===1?2:3));
|
| 323 |
return;
|
| 324 |
}
|
|
@@ -337,6 +347,37 @@ function tryAbility(k){
|
|
| 337 |
addParticle(3,hx,hy,0,0,.7,190, 90,255,160);
|
| 338 |
sfx('heal'); if(typeof radioAck==='function')radioAck('ability',1,hx,hy); return;
|
| 339 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
if(k===2){ // combat surge
|
| 341 |
abCool[2]=AB_CD[2];
|
| 342 |
const hx=ux[heroIdx], hy=uy[heroIdx];
|
|
|
|
| 3 |
============================================================ */
|
| 4 |
let heroLvl=1, heroXp=0, heroXpNext=100;
|
| 5 |
let heroDmgMult=1, heroRegen=14;
|
| 6 |
+
let abCool=[0,0,0,0,0]; // blast, repair, surge, orbital lance, EMP
|
| 7 |
+
const AB_CD=[26,20,30,70,45];
|
| 8 |
let heroJumpCool=0;
|
| 9 |
const HERO_JUMP={range:275,cool:28,energy:170};
|
| 10 |
let heroStuckFor=0,heroRescueAt=0;
|
|
|
|
| 281 |
{em:'💥', nm:'Wider Blast', ds:'Orbital Blast radius +35%', fn:()=>{ blastRadius*=1.35; }},
|
| 282 |
];
|
| 283 |
let bonusMass=0, bonusEnergy=0, armyDmgMult=1, blastRadius=110;
|
| 284 |
+
/* Combat Stims are a TEMPORARY multiplier and must be kept out of armyDmgMult.
|
| 285 |
+
armyDmgMult is written ADDITIVELY by thirteen different sources — research,
|
| 286 |
+
the Veteran Doctrine level-up card, five armory weapon items, consumables,
|
| 287 |
+
develop modules and operation modifiers. Folding a *=1.45 into it and later
|
| 288 |
+
doing /=1.45 is not an inverse once any of those has landed in between:
|
| 289 |
+
(1.00*1.45 + 0.12)/1.45 = 1.083, not 1.12. A player who completed Ballistics I
|
| 290 |
+
during a stim permanently kept 0.083 of the 0.12 they paid 500 mass and 2500
|
| 291 |
+
energy for, and level-ups are MORE likely during a stim because kills spike. */
|
| 292 |
+
let stimDmgMult=1;
|
| 293 |
function flashScreen(){
|
| 294 |
const f=document.getElementById('flash');
|
| 295 |
if(!f) return;
|
|
|
|
| 328 |
if(heroIdx<0){ toast('Commander is down'); return; }
|
| 329 |
if(!abUnlock[k]){
|
| 330 |
toast(k===3?'🛰 Buy the Orbital Uplink in the Armory to unlock this'
|
| 331 |
+
:k===4?'⚡ Craft the EMP Charge module in Development to unlock this'
|
| 332 |
:'Unlocks at Commander level '+(k===1?2:3));
|
| 333 |
return;
|
| 334 |
}
|
|
|
|
| 347 |
addParticle(3,hx,hy,0,0,.7,190, 90,255,160);
|
| 348 |
sfx('heal'); if(typeof radioAck==='function')radioAck('ability',1,hx,hy); return;
|
| 349 |
}
|
| 350 |
+
if(k===4){ // EMP charge — the craftable module from Development
|
| 351 |
+
/* This module has been on sale in Development for 5 Relic Cores + 30 Isotope
|
| 352 |
+
since it was added, described as "stun everything in a wide radius", with
|
| 353 |
+
no implementation anywhere in the codebase and a flag that a four-entry
|
| 354 |
+
reset array threw away every match. Both are fixed; this is the ability.
|
| 355 |
+
|
| 356 |
+
Centred on the Commander rather than aimed: it is a panic button for when
|
| 357 |
+
the Commander is caught, and a self-centred blast needs no aiming mode —
|
| 358 |
+
one less state machine to get wrong. Friendlies are untouched. */
|
| 359 |
+
abCool[4]=AB_CD[4];
|
| 360 |
+
const hx=ux[heroIdx], hy=uy[heroIdx], R=260;
|
| 361 |
+
let n=0;
|
| 362 |
+
forUnitsIn(hx,hy,R,j=>{
|
| 363 |
+
if(!ualive[j]||uteam[j]===0||j===heroIdx) return;
|
| 364 |
+
ustun[j]=4; ucool[j]=Math.max(ucool[j],4); n++;
|
| 365 |
+
addParticle(0,ux[j],uy[j],0,-12,.5,7,150,220,255);
|
| 366 |
+
});
|
| 367 |
+
/* Enemy defensive structures are on the same grid and are just as much
|
| 368 |
+
"everything in a wide radius" as the units are. */
|
| 369 |
+
let b=0;
|
| 370 |
+
for(const B of blds){
|
| 371 |
+
if(!B.alive||B.team===0) continue;
|
| 372 |
+
if(Math.hypot(B.x-hx,B.y-hy)>R) continue;
|
| 373 |
+
B.cool=Math.max(B.cool||0,4); b++;
|
| 374 |
+
}
|
| 375 |
+
addParticle(3,hx,hy,0,0,.8,R,150,220,255);
|
| 376 |
+
toast('⚡ EMP — '+n+' unit'+(n===1?'':'s')+(b?' and '+b+' structure'+(b===1?'':'s'):'')+' disabled for 4s');
|
| 377 |
+
sfx('surge',hx,hy,1.2);
|
| 378 |
+
if(typeof radioAck==='function')radioAck('ability',n,hx,hy);
|
| 379 |
+
return;
|
| 380 |
+
}
|
| 381 |
if(k===2){ // combat surge
|
| 382 |
abCool[2]=AB_CD[2];
|
| 383 |
const hx=ux[heroIdx], hy=uy[heroIdx];
|
src/game/meta.js
CHANGED
|
@@ -340,7 +340,12 @@ function invApplyLoadout(){
|
|
| 340 |
b.ready=[]; metaSave();
|
| 341 |
return used;
|
| 342 |
}
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
/* The setup screen's supply-drop choice, kept so perks can be re-applied on top
|
| 345 |
of it every match instead of on top of last match's result. */
|
| 346 |
let crateRateBase=1;
|
|
@@ -570,13 +575,15 @@ function renderMetaHead(){
|
|
| 570 |
const r=metaRankIdx(), R=RANKS[r], next=RANKS[r+1], P=activeProf();
|
| 571 |
const set=(id,v)=>{ const e=document.getElementById(id); if(e) e.textContent=v; };
|
| 572 |
const ID=profIdentity();
|
| 573 |
-
const
|
| 574 |
-
if(
|
| 575 |
/* Wearing a commander replaces the emoji with their portrait, framed by
|
| 576 |
-
whichever frame the account has unlocked and chosen.
|
| 577 |
-
|
| 578 |
-
|
|
|
|
| 579 |
}
|
|
|
|
| 580 |
/* The name / rank label / XP bar / XP count moved to Profile ▸ CAREER
|
| 581 |
(renderCareer). The header keeps the badge and a one-line greeting; the
|
| 582 |
badge's tooltip carries the rank so the symbol is never a mystery. */
|
|
|
|
| 340 |
b.ready=[]; metaSave();
|
| 341 |
return used;
|
| 342 |
}
|
| 343 |
+
/* MUST stay in lockstep with AB_CD in commander.js — the rebuild loop below is
|
| 344 |
+
driven by AB_CD.length, so a shorter AB_BASE writes `undefined * cd` = NaN
|
| 345 |
+
into the extra slots. A NaN cooldown never satisfies `abCool[k]>0`, so the
|
| 346 |
+
ability reads as permanently ready and its cooldown never displays. Adding
|
| 347 |
+
the EMP as a fifth ability without this line did exactly that. */
|
| 348 |
+
const AB_BASE=[26,20,30,70,45]; // blast, repair, surge, lance, EMP
|
| 349 |
/* The setup screen's supply-drop choice, kept so perks can be re-applied on top
|
| 350 |
of it every match instead of on top of last match's result. */
|
| 351 |
let crateRateBase=1;
|
|
|
|
| 575 |
const r=metaRankIdx(), R=RANKS[r], next=RANKS[r+1], P=activeProf();
|
| 576 |
const set=(id,v)=>{ const e=document.getElementById(id); if(e) e.textContent=v; };
|
| 577 |
const ID=profIdentity();
|
| 578 |
+
const glyph=document.getElementById('rankEmGlyph');
|
| 579 |
+
if(glyph){
|
| 580 |
/* Wearing a commander replaces the emoji with their portrait, framed by
|
| 581 |
+
whichever frame the account has unlocked and chosen. The level badge is a
|
| 582 |
+
sibling, so it survives this swap. */
|
| 583 |
+
if(ID.portrait) glyph.innerHTML='<img class="badgePortrait fr-'+ID.frame+'" src="'+ID.portrait+'" alt="">';
|
| 584 |
+
else glyph.textContent=ID.emblem;
|
| 585 |
}
|
| 586 |
+
set('rankLvl', r+1);
|
| 587 |
/* The name / rank label / XP bar / XP count moved to Profile ▸ CAREER
|
| 588 |
(renderCareer). The header keeps the badge and a one-line greeting; the
|
| 589 |
badge's tooltip carries the rank so the symbol is never a mystery. */
|
src/game/sim.js
CHANGED
|
@@ -177,6 +177,12 @@ const HAZ_SPD=0.72, HAZ_RNG=0.70;
|
|
| 177 |
normal engagement resumes, so this cannot make a wave ignore the base it came
|
| 178 |
to attack. */
|
| 179 |
const umarch=new Uint8Array(MAXU);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
let freeList=[], unitHigh=0;
|
| 181 |
const teamCount=[0,0,0];
|
| 182 |
const titanCount=[0,0];
|
|
@@ -390,7 +396,7 @@ function spawnUnit(type,team,x,y){
|
|
| 390 |
a dead marcher's slot silently refused to engage for the rest of the match.
|
| 391 |
main.js clears the array between matches, which is why this only ever
|
| 392 |
surfaced mid-match and read as an aggro/AI bug rather than a spawn bug. */
|
| 393 |
-
umarch[i]=0;
|
| 394 |
uPatrolRoute[i]=-1; uPatrolStep[i]=0; uPatrolSlot[i]=0;
|
| 395 |
uMoveCohort[i]=-1; uCohesion[i]=1;
|
| 396 |
umode[i]=0; umodeT[i]=0;
|
|
@@ -1702,7 +1708,8 @@ function crateTick(dt){
|
|
| 1702 |
}
|
| 1703 |
if(boostDmgT>0){
|
| 1704 |
boostDmgT-=dt;
|
| 1705 |
-
|
|
|
|
| 1706 |
}
|
| 1707 |
}
|
| 1708 |
function applyCrate(k,x,y){
|
|
@@ -1713,7 +1720,7 @@ function applyCrate(k,x,y){
|
|
| 1713 |
pickupToast(k,'+250 MASS · +900 ENERGY');
|
| 1714 |
}
|
| 1715 |
else if(k.id==='vet'){
|
| 1716 |
-
|
| 1717 |
boostDmgT=30; pickupToast(k,'ARMY DAMAGE +45% · 30 SECONDS');
|
| 1718 |
}
|
| 1719 |
else if(k.id==='repair'){
|
|
@@ -3314,6 +3321,13 @@ function unitTick(dt){
|
|
| 3314 |
if(ubroodLed[i]>0) ubroodLed[i]-=dt;
|
| 3315 |
if(uhaz[i]>0) uhaz[i]-=dt;
|
| 3316 |
if(ustomp[i]>0) ustomp[i]-=dt;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3317 |
if(ushielded[i]>0) ushielded[i]-=dt;
|
| 3318 |
/* ---- FACTION HERO BEHAVIOURS ------------------------------------------
|
| 3319 |
Each hero does something its faction's army cannot, so killing it changes
|
|
@@ -3433,7 +3447,7 @@ function unitTick(dt){
|
|
| 3433 |
const vet=1+uvet[i]*0.15;
|
| 3434 |
ucool[i]=T.cool*modeCoolMul(md)*classCoolMul(i)*broodCoolMul(i)/(ubuff[i]>0?1.4:1);
|
| 3435 |
if(md===4){ umode[i]=0; umodeT[i]=MODE_SWITCH*0.4; } // firing breaks GHOST cover
|
| 3436 |
-
const dmg=T.dmg*vet*modeDmgMul(md)*classDmgMul(i)*broodDmgMul(i)*(ubuff[i]>0?1.5:1)*(uteam[i]===1?aiDmgMult:(uteam[i]===0?armyDmgMult:1))*(i===heroIdx?heroDmgMult:1);
|
| 3437 |
const ma=(T.tur?uturr[i]:uang[i])-Math.PI/2;
|
| 3438 |
const mx=ux[i]+Math.cos(ma)*T.size*0.62, my=uy[i]+Math.sin(ma)*T.size*0.62;
|
| 3439 |
if(T.wk==='m'){
|
|
|
|
| 177 |
normal engagement resumes, so this cannot make a wave ignore the base it came
|
| 178 |
to attack. */
|
| 179 |
const umarch=new Uint8Array(MAXU);
|
| 180 |
+
/* EMP stun, in seconds. A stunned unit neither moves nor fires. Declared here
|
| 181 |
+
with the rest of the per-unit state so it is impossible to miss when reading
|
| 182 |
+
what a slot carries — and it is cleared in spawnUnit() alongside the others,
|
| 183 |
+
because a recycled slot inherits every array it is not explicitly told to
|
| 184 |
+
reset. That exact omission on umarch shipped as a live bug in 1.32.18. */
|
| 185 |
+
const ustun=new Float32Array(MAXU);
|
| 186 |
let freeList=[], unitHigh=0;
|
| 187 |
const teamCount=[0,0,0];
|
| 188 |
const titanCount=[0,0];
|
|
|
|
| 396 |
a dead marcher's slot silently refused to engage for the rest of the match.
|
| 397 |
main.js clears the array between matches, which is why this only ever
|
| 398 |
surfaced mid-match and read as an aggro/AI bug rather than a spawn bug. */
|
| 399 |
+
umarch[i]=0; ustun[i]=0;
|
| 400 |
uPatrolRoute[i]=-1; uPatrolStep[i]=0; uPatrolSlot[i]=0;
|
| 401 |
uMoveCohort[i]=-1; uCohesion[i]=1;
|
| 402 |
umode[i]=0; umodeT[i]=0;
|
|
|
|
| 1708 |
}
|
| 1709 |
if(boostDmgT>0){
|
| 1710 |
boostDmgT-=dt;
|
| 1711 |
+
/* Set, not divided — see the stimDmgMult note in commander.js. */
|
| 1712 |
+
if(boostDmgT<=0){ stimDmgMult=1; toast('🎖 Combat stims worn off'); }
|
| 1713 |
}
|
| 1714 |
}
|
| 1715 |
function applyCrate(k,x,y){
|
|
|
|
| 1720 |
pickupToast(k,'+250 MASS · +900 ENERGY');
|
| 1721 |
}
|
| 1722 |
else if(k.id==='vet'){
|
| 1723 |
+
stimDmgMult=1.45;
|
| 1724 |
boostDmgT=30; pickupToast(k,'ARMY DAMAGE +45% · 30 SECONDS');
|
| 1725 |
}
|
| 1726 |
else if(k.id==='repair'){
|
|
|
|
| 3321 |
if(ubroodLed[i]>0) ubroodLed[i]-=dt;
|
| 3322 |
if(uhaz[i]>0) uhaz[i]-=dt;
|
| 3323 |
if(ustomp[i]>0) ustomp[i]-=dt;
|
| 3324 |
+
if(ustun[i]>0){
|
| 3325 |
+
ustun[i]-=dt;
|
| 3326 |
+
/* Held under EMP: no movement, no firing. The arc particle is what tells
|
| 3327 |
+
the player WHY a line of enemies has stopped, so it is not optional. */
|
| 3328 |
+
if(((i+tick)&7)===0) addParticle(0,ux[i],uy[i],rr(-14,14),rr(-14,14),.26,4,150,220,255);
|
| 3329 |
+
continue;
|
| 3330 |
+
}
|
| 3331 |
if(ushielded[i]>0) ushielded[i]-=dt;
|
| 3332 |
/* ---- FACTION HERO BEHAVIOURS ------------------------------------------
|
| 3333 |
Each hero does something its faction's army cannot, so killing it changes
|
|
|
|
| 3447 |
const vet=1+uvet[i]*0.15;
|
| 3448 |
ucool[i]=T.cool*modeCoolMul(md)*classCoolMul(i)*broodCoolMul(i)/(ubuff[i]>0?1.4:1);
|
| 3449 |
if(md===4){ umode[i]=0; umodeT[i]=MODE_SWITCH*0.4; } // firing breaks GHOST cover
|
| 3450 |
+
const dmg=T.dmg*vet*modeDmgMul(md)*classDmgMul(i)*broodDmgMul(i)*(ubuff[i]>0?1.5:1)*(uteam[i]===1?aiDmgMult:(uteam[i]===0?armyDmgMult*stimDmgMult:1))*(i===heroIdx?heroDmgMult:1);
|
| 3451 |
const ma=(T.tur?uturr[i]:uang[i])-Math.PI/2;
|
| 3452 |
const mx=ux[i]+Math.cos(ma)*T.size*0.62, my=uy[i]+Math.sin(ma)*T.size*0.62;
|
| 3453 |
if(T.wk==='m'){
|
src/main.js
CHANGED
|
@@ -231,7 +231,7 @@ function resetWorld(){
|
|
| 231 |
aiHpMult=1;
|
| 232 |
stats.kills=[0,0,0]; stats.built=[0,0]; stats.t=0; stats.nests=0; stats.reclaimed=0;
|
| 233 |
heroLvl=1; heroXp=0; heroXpNext=100; pendingLevels=0;
|
| 234 |
-
heroDmgMult=1; heroRegen=14; armyDmgMult=1; bonusMass=0; bonusEnergy=0;
|
| 235 |
playerBuildMult=1; blastRadius=110;
|
| 236 |
titanCount[0]=0; titanCount[1]=0; teamCount[2]=0;
|
| 237 |
beams.length=0; fogCov.fill(0); fogSeen.fill(0); fogSources.fill(0); fogScans.length=0;
|
|
@@ -1320,6 +1320,7 @@ function wire(){
|
|
| 1320 |
$('abHeal').addEventListener('pointerdown',()=>tryAbility(1));
|
| 1321 |
$('abRage').addEventListener('pointerdown',()=>tryAbility(2));
|
| 1322 |
$('abLance').addEventListener('pointerdown',()=>tryAbility(3));
|
|
|
|
| 1323 |
if($('abJump')) $('abJump').addEventListener('pointerdown',()=>tryCommanderJump());
|
| 1324 |
$('abBarrage').addEventListener('pointerdown',()=>tryArtilleryBarrage());
|
| 1325 |
if($('abClass')) $('abClass').addEventListener('pointerdown',()=>tryClassAbility());
|
|
|
|
| 231 |
aiHpMult=1;
|
| 232 |
stats.kills=[0,0,0]; stats.built=[0,0]; stats.t=0; stats.nests=0; stats.reclaimed=0;
|
| 233 |
heroLvl=1; heroXp=0; heroXpNext=100; pendingLevels=0;
|
| 234 |
+
heroDmgMult=1; heroRegen=14; armyDmgMult=1; stimDmgMult=1; boostDmgT=0; bonusMass=0; bonusEnergy=0;
|
| 235 |
playerBuildMult=1; blastRadius=110;
|
| 236 |
titanCount[0]=0; titanCount[1]=0; teamCount[2]=0;
|
| 237 |
beams.length=0; fogCov.fill(0); fogSeen.fill(0); fogSources.fill(0); fogScans.length=0;
|
|
|
|
| 1320 |
$('abHeal').addEventListener('pointerdown',()=>tryAbility(1));
|
| 1321 |
$('abRage').addEventListener('pointerdown',()=>tryAbility(2));
|
| 1322 |
$('abLance').addEventListener('pointerdown',()=>tryAbility(3));
|
| 1323 |
+
$('abEmp').addEventListener('pointerdown',()=>tryAbility(4));
|
| 1324 |
if($('abJump')) $('abJump').addEventListener('pointerdown',()=>tryCommanderJump());
|
| 1325 |
$('abBarrage').addEventListener('pointerdown',()=>tryArtilleryBarrage());
|
| 1326 |
if($('abClass')) $('abClass').addEventListener('pointerdown',()=>tryClassAbility());
|
src/styles/ui.css
CHANGED
|
@@ -9,7 +9,13 @@
|
|
| 9 |
--red:#ff5d43; --gold:#ffd257; --green:#5dff9a;
|
| 10 |
/* Capacitor injects --safe-area-inset-* on Android WebView versions where
|
| 11 |
CSS env() reports zero. Keep env() as the browser/iOS fallback. */
|
| 12 |
-
--
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
--sar:var(--safe-area-inset-right,env(safe-area-inset-right,0px));
|
| 14 |
--sab:var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px));
|
| 15 |
--sal:var(--safe-area-inset-left,env(safe-area-inset-left,0px));
|
|
@@ -517,7 +523,8 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 517 |
.goData small{display:block;color:#7fb29c;font:600 9px var(--fU);letter-spacing:.03em}.goData.muted{color:#7f9aab;font-size:10px}
|
| 518 |
|
| 519 |
/* ---------- META HEADER ---------- */
|
| 520 |
-
#metaHead{display:
|
|
|
|
| 521 |
border:1px solid transparent;background:var(--panelG) padding-box,var(--steelB) border-box;
|
| 522 |
box-shadow:0 4px 14px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.1);
|
| 523 |
clip-path:polygon(11px 0,calc(100% - 11px) 0,100% 11px,100% calc(100% - 11px),calc(100% - 11px) 100%,11px 100%,0 calc(100% - 11px),0 11px)}
|
|
@@ -600,7 +607,12 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 600 |
#profileScr,#settingsScr,#setupScr{display:none;justify-content:flex-start;padding-top:calc(var(--sat) + 16px)}
|
| 601 |
#setupScr{background:
|
| 602 |
radial-gradient(ellipse at 50% -10%,rgba(28,68,104,.55),transparent 60%),rgba(3,7,14,.94)}
|
| 603 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
#startScreen .title{margin-top:14px}
|
| 605 |
.menuGrid{display:grid;grid-template-columns:1fr 1fr;gap:9px;width:min(88vw,340px);margin-top:12px}
|
| 606 |
.gbtn{display:flex;align-items:center;justify-content:center;gap:8px;min-height:54px;padding:13px 8px;border-radius:11px;
|
|
@@ -1777,7 +1789,7 @@ body.godMode #godBadge{display:flex}
|
|
| 1777 |
body.uiPrimaryOpen .menuRoleInspect{min-height:27px;margin-top:auto;padding:4px 8px}
|
| 1778 |
}
|
| 1779 |
@media (max-width:380px) and (orientation:portrait){
|
| 1780 |
-
/* On
|
| 1781 |
resource identities, and both 44px command targets always remain visible. */
|
| 1782 |
#topbar .res .rate{display:none}
|
| 1783 |
#topbar{grid-template-columns:repeat(3,minmax(0,1fr)) 44px 44px}
|
|
@@ -2010,3 +2022,33 @@ body.godMode #godBadge{display:flex}
|
|
| 2010 |
background:#5be69f;box-shadow:0 0 7px rgba(91,230,159,.9)}
|
| 2011 |
#inboxBtn{position:absolute}
|
| 2012 |
body.menuMode #inboxHudBtn{display:none}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
--red:#ff5d43; --gold:#ffd257; --green:#5dff9a;
|
| 10 |
/* Capacitor injects --safe-area-inset-* on Android WebView versions where
|
| 11 |
CSS env() reports zero. Keep env() as the browser/iOS fallback. */
|
| 12 |
+
--satRaw:var(--safe-area-inset-top,env(safe-area-inset-top,0px));
|
| 13 |
+
/* Android WebView reports env(safe-area-inset-top) as 0px unless the activity
|
| 14 |
+
opts into cutout mode, so relying on it alone painted the OS status bar —
|
| 15 |
+
clock, wifi, battery — directly on top of the menu header. A floor keeps the
|
| 16 |
+
UI clear of it everywhere; on a device that DOES report a real inset (an
|
| 17 |
+
iPhone notch at 47px) max() leaves that untouched. */
|
| 18 |
+
--sat:max(var(--satRaw),26px);
|
| 19 |
--sar:var(--safe-area-inset-right,env(safe-area-inset-right,0px));
|
| 20 |
--sab:var(--safe-area-inset-bottom,env(safe-area-inset-bottom,0px));
|
| 21 |
--sal:var(--safe-area-inset-left,env(safe-area-inset-left,0px));
|
|
|
|
| 523 |
.goData small{display:block;color:#7fb29c;font:600 9px var(--fU);letter-spacing:.03em}.goData.muted{color:#7f9aab;font-size:10px}
|
| 524 |
|
| 525 |
/* ---------- META HEADER ---------- */
|
| 526 |
+
#metaHead{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-areas:'icon name mail' 'res res res';
|
| 527 |
+
align-items:center;gap:8px 10px;width:min(92vw,384px);border-radius:13px;padding:9px 12px 10px;
|
| 528 |
border:1px solid transparent;background:var(--panelG) padding-box,var(--steelB) border-box;
|
| 529 |
box-shadow:0 4px 14px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.1);
|
| 530 |
clip-path:polygon(11px 0,calc(100% - 11px) 0,100% 11px,100% calc(100% - 11px),calc(100% - 11px) 100%,11px 100%,0 calc(100% - 11px),0 11px)}
|
|
|
|
| 607 |
#profileScr,#settingsScr,#setupScr{display:none;justify-content:flex-start;padding-top:calc(var(--sat) + 16px)}
|
| 608 |
#setupScr{background:
|
| 609 |
radial-gradient(ellipse at 50% -10%,rgba(28,68,104,.55),transparent 60%),rgba(3,7,14,.94)}
|
| 610 |
+
/* `safe center` instead of plain `center`. A centred flex column that overflows
|
| 611 |
+
clips its START — on a 640px-tall screen the menu header sat at -3.6px, above
|
| 612 |
+
the top of the viewport and unreachable, which is the same "UI under the OS
|
| 613 |
+
chrome" complaint in a different disguise. `safe` falls back to flex-start the
|
| 614 |
+
moment the content no longer fits, so nothing is ever pushed off the top. */
|
| 615 |
+
#startScreen{justify-content:safe center;padding-top:calc(var(--sat) + 10px)}
|
| 616 |
#startScreen .title{margin-top:14px}
|
| 617 |
.menuGrid{display:grid;grid-template-columns:1fr 1fr;gap:9px;width:min(88vw,340px);margin-top:12px}
|
| 618 |
.gbtn{display:flex;align-items:center;justify-content:center;gap:8px;min-height:54px;padding:13px 8px;border-radius:11px;
|
|
|
|
| 1789 |
body.uiPrimaryOpen .menuRoleInspect{min-height:27px;margin-top:auto;padding:4px 8px}
|
| 1790 |
}
|
| 1791 |
@media (max-width:380px) and (orientation:portrait){
|
| 1792 |
+
/* On 320–360px phones the live rates are the only expendable text. Values,
|
| 1793 |
resource identities, and both 44px command targets always remain visible. */
|
| 1794 |
#topbar .res .rate{display:none}
|
| 1795 |
#topbar{grid-template-columns:repeat(3,minmax(0,1fr)) 44px 44px}
|
|
|
|
| 2022 |
background:#5be69f;box-shadow:0 0 7px rgba(91,230,159,.9)}
|
| 2023 |
#inboxBtn{position:absolute}
|
| 2024 |
body.menuMode #inboxHudBtn{display:none}
|
| 2025 |
+
|
| 2026 |
+
|
| 2027 |
+
/* ── Menu header: profile icon + level, resources underneath ───────────────
|
| 2028 |
+
Grid areas rather than a flex row, so the mailbox can sit in the header's own
|
| 2029 |
+
top-right corner instead of floating over the numbers, and the resources get
|
| 2030 |
+
a full-width row of their own. */
|
| 2031 |
+
#rankEm{grid-area:icon;position:relative;display:flex;align-items:center;justify-content:center}
|
| 2032 |
+
#rankEmGlyph{font-size:30px;line-height:1;display:block;filter:drop-shadow(0 0 6px rgba(255,210,87,.5))}
|
| 2033 |
+
/* The account level, as a badge on the corner of the portrait. */
|
| 2034 |
+
#rankLvl{position:absolute;right:-3px;bottom:-3px;min-width:19px;height:19px;padding:0 4px;
|
| 2035 |
+
display:flex;align-items:center;justify-content:center;border-radius:10px;
|
| 2036 |
+
font-family:var(--fT);font-size:10px;font-weight:800;line-height:1;color:#08131d;
|
| 2037 |
+
background:linear-gradient(180deg,#ffe9a3,#e0b757);border:1px solid rgba(10,20,30,.65);
|
| 2038 |
+
box-shadow:0 1px 4px rgba(0,0,0,.6)}
|
| 2039 |
+
#rankGreet{grid-area:name}
|
| 2040 |
+
#inboxBtn{grid-area:mail;position:static;width:40px;height:40px;flex:0 0 auto}
|
| 2041 |
+
#metaRes{grid-area:res;display:grid;grid-template-columns:1fr 1fr;gap:8px;
|
| 2042 |
+
padding-top:8px;border-top:1px solid rgba(120,170,205,.18)}
|
| 2043 |
+
.metaResCell{display:flex;align-items:baseline;justify-content:center;gap:6px;min-width:0}
|
| 2044 |
+
.metaResCell .lbl{font-size:8.5px;letter-spacing:.16em;color:#7fa8c4;font-weight:700;flex:0 0 auto}
|
| 2045 |
+
.metaResCell b{font-family:var(--fT);font-size:15px;color:#8be8ff;font-variant-numeric:tabular-nums;
|
| 2046 |
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 2047 |
+
.metaResCell:last-child b{font-size:13px;color:#cfe8ff}
|
| 2048 |
+
#metaRec{margin:0;font-size:inherit;color:inherit}
|
| 2049 |
+
#rankEm .badgePortrait{width:40px;height:40px;border-radius:50%}
|
| 2050 |
+
@media (max-width:430px){
|
| 2051 |
+
#rankEmGlyph{font-size:26px}
|
| 2052 |
+
#rankEm .badgePortrait{width:36px;height:36px}
|
| 2053 |
+
.metaResCell b{font-size:14px}
|
| 2054 |
+
}
|
src/ui/hud.js
CHANGED
|
@@ -1539,8 +1539,10 @@ function updateHUD(fps){
|
|
| 1539 |
$('xpFill').style.width=(heroXp/heroXpNext*100)+'%';
|
| 1540 |
$('heroLvlBadge').textContent=heroLvl;
|
| 1541 |
} else $('heroBar').style.display='none';
|
| 1542 |
-
|
| 1543 |
-
|
|
|
|
|
|
|
| 1544 |
const cd=btns[k].querySelector('.cdring');
|
| 1545 |
if(!abUnlock[k]){ btns[k].classList.add('cd'); cd.style.display='flex'; cd.textContent='🔒'; }
|
| 1546 |
else if(abCool[k]>0){ btns[k].classList.add('cd'); cd.style.display='flex'; cd.textContent=Math.ceil(abCool[k]); }
|
|
|
|
| 1539 |
$('xpFill').style.width=(heroXp/heroXpNext*100)+'%';
|
| 1540 |
$('heroLvlBadge').textContent=heroLvl;
|
| 1541 |
} else $('heroBar').style.display='none';
|
| 1542 |
+
/* Length-driven, not a hardcoded 4. The EMP module added a fifth ability and
|
| 1543 |
+
the old literal silently left it out of the cooldown/lock rendering. */
|
| 1544 |
+
const btns=[$('abOver'),$('abHeal'),$('abRage'),$('abLance'),$('abEmp')].filter(Boolean);
|
| 1545 |
+
for(let k=0;k<btns.length;k++){
|
| 1546 |
const cd=btns[k].querySelector('.cdring');
|
| 1547 |
if(!abUnlock[k]){ btns[k].classList.add('cd'); cd.style.display='flex'; cd.textContent='🔒'; }
|
| 1548 |
else if(abCool[k]>0){ btns[k].classList.add('cd'); cd.style.display='flex'; cd.textContent=Math.ceil(abCool[k]); }
|
src/updater.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
| 29 |
============================================================================ */
|
| 30 |
|
| 31 |
/* Bumped by the release script. Compared against the manifest's `version`. */
|
| 32 |
-
const APP_VERSION = '1.32.
|
| 33 |
|
| 34 |
/* The channel URL in update-config.json remains publisher-configurable, but a
|
| 35 |
production checker also needs one known-good recovery path. More importantly,
|
|
|
|
| 29 |
============================================================================ */
|
| 30 |
|
| 31 |
/* Bumped by the release script. Compared against the manifest's `version`. */
|
| 32 |
+
const APP_VERSION = '1.32.24';
|
| 33 |
|
| 34 |
/* The channel URL in update-config.json remains publisher-configurable, but a
|
| 35 |
production checker also needs one known-good recovery path. More importantly,
|