Spaces:
Running
Running
Publish MASSFRONT web 1.32.0 (part 2)
Browse files- .gitattributes +1 -0
- assets/modifiers/modifier-art-atlas-v1.png +3 -0
- boot.js +4 -4
- index.html +27 -23
- src/airlift-factions.js +122 -0
- src/airlift.js +857 -0
- src/audio.js +1 -1
- src/develop.js +38 -11
- src/endgame.js +67 -20
- src/engine/mesh.js +78 -6
- src/engine/terrain.js +82 -0
- src/factions.js +46 -24
- src/game/ai.js +9 -1
- src/game/commander.js +82 -0
- src/game/economy.js +22 -1
- src/game/meta.js +28 -3
- src/game/sim.js +381 -47
- src/intro.js +137 -0
- src/main.js +102 -26
- src/restree3d.js +284 -225
- src/storeui.js +205 -42
- src/story.js +208 -0
- src/styles/intro.css +44 -0
- src/styles/restree.css +78 -100
- src/styles/store.css +97 -0
- src/styles/tutorial.css +10 -0
- src/styles/ui.css +195 -20
- src/tutorial.js +196 -48
- src/ui/hud.js +126 -12
- src/ui/input.js +19 -7
- src/ui/render3d.js +82 -3
- src/updater.js +1 -1
.gitattributes
CHANGED
|
@@ -64,3 +64,4 @@ assets/factions/cinematic/brood-swarm-v1.png filter=lfs diff=lfs merge=lfs -text
|
|
| 64 |
assets/factions/cinematic/crimson-dominion-v1.png filter=lfs diff=lfs merge=lfs -text
|
| 65 |
assets/factions/cinematic/syndicate-coalition-v1.png filter=lfs diff=lfs merge=lfs -text
|
| 66 |
assets/factions/cinematic/terran-frontline-command-v1.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 64 |
assets/factions/cinematic/crimson-dominion-v1.png filter=lfs diff=lfs merge=lfs -text
|
| 65 |
assets/factions/cinematic/syndicate-coalition-v1.png filter=lfs diff=lfs merge=lfs -text
|
| 66 |
assets/factions/cinematic/terran-frontline-command-v1.png filter=lfs diff=lfs merge=lfs -text
|
| 67 |
+
assets/modifiers/modifier-art-atlas-v1.png filter=lfs diff=lfs merge=lfs -text
|
assets/modifiers/modifier-art-atlas-v1.png
ADDED
|
Git LFS Details
|
boot.js
CHANGED
|
@@ -27,15 +27,15 @@
|
|
| 27 |
'./src/engine/models-legion.js','./src/engine/models-machine.js','./src/engine/models-infestation.js',
|
| 28 |
'./src/game/sim.js','./src/game/economy.js','./src/game/commander.js',
|
| 29 |
'./src/game/meta.js','./src/game/ai.js',
|
| 30 |
-
'./src/ui/input.js','./src/ui/hud.js','./src/ui/render3d.js',
|
| 31 |
-
'./src/factions.js','./src/
|
| 32 |
-
'./src/authportal.js','./src/tutorial.js','./src/adboards.js','./src/storeui.js','./src/restree3d.js','./src/develop.js','./src/endgame.js','./src/story.js','./src/daily.js','./src/account.js','./src/economy-net.js','./src/updater.js','./src/main.js'
|
| 33 |
];
|
| 34 |
/* Packaged scripts need a release key. WebViews and development browsers can
|
| 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.
|
| 39 |
var DB='massfront-updates', STORE='bundles';
|
| 40 |
var bootShield=null, bootShieldTimer=0;
|
| 41 |
var bootShieldEvents=['pointerdown','pointerup','touchstart','touchend','click'];
|
|
|
|
| 27 |
'./src/engine/models-legion.js','./src/engine/models-machine.js','./src/engine/models-infestation.js',
|
| 28 |
'./src/game/sim.js','./src/game/economy.js','./src/game/commander.js',
|
| 29 |
'./src/game/meta.js','./src/game/ai.js',
|
| 30 |
+
'./src/ui/input.js','./src/ui/hud.js','./src/ui/render3d.js','./src/airlift.js','./src/airlift-factions.js',
|
| 31 |
+
'./src/factions.js','./src/offline.js','./src/audio.js','./src/assetpack.js','./src/hazards.js',
|
| 32 |
+
'./src/authportal.js','./src/tutorial.js','./src/adboards.js','./src/storeui.js','./src/restree3d.js','./src/develop.js','./src/endgame.js','./src/story.js','./src/daily.js','./src/account.js','./src/economy-net.js','./src/updater.js','./src/intro.js','./src/main.js'
|
| 33 |
];
|
| 34 |
/* Packaged scripts need a release key. WebViews and development browsers can
|
| 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.0';
|
| 39 |
var DB='massfront-updates', STORE='bundles';
|
| 40 |
var bootShield=null, bootShieldTimer=0;
|
| 41 |
var bootShieldEvents=['pointerdown','pointerup','touchstart','touchend','click'];
|
index.html
CHANGED
|
@@ -16,12 +16,13 @@
|
|
| 16 |
<link rel="icon" href="./assets/icons/icon-192.png">
|
| 17 |
<link rel="manifest" href="./assets/app.webmanifest">
|
| 18 |
<title>MASSFRONT — Mobile RTS</title>
|
| 19 |
-
<link rel="stylesheet" href="./src/styles/ui.css?v=1.
|
| 20 |
-
<link rel="stylesheet" href="./src/styles/auth.css?v=1.
|
| 21 |
-
<link rel="stylesheet" href="./src/styles/tutorial.css?v=1.
|
| 22 |
-
<link rel="stylesheet" href="./src/styles/ads.css?v=1.
|
| 23 |
-
<link rel="stylesheet" href="./src/styles/store.css?v=1.
|
| 24 |
-
<link rel="stylesheet" href="./src/styles/restree.css?v=1.
|
|
|
|
| 25 |
</head>
|
| 26 |
<body>
|
| 27 |
<canvas id="gl"></canvas>
|
|
@@ -89,6 +90,7 @@
|
|
| 89 |
<button class="cbtn abtn" id="abHeal"><span class="em">🛠</span><span>Repair</span><div class="cdring"></div></button>
|
| 90 |
<button class="cbtn abtn" id="abRage"><span class="em">⚡</span><span>Surge</span><div class="cdring"></div></button>
|
| 91 |
<button class="cbtn abtn" id="abLance"><span class="em">🛰</span><span>Lance</span><div class="cdring"></div></button>
|
|
|
|
| 92 |
<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>
|
| 93 |
<button class="cbtn abtn" id="abHero"><span class="em">🤖</span><span>CDR</span><span class="lvl" id="heroLvlBadge">1</span></button>
|
| 94 |
</div>
|
|
@@ -185,7 +187,7 @@
|
|
| 185 |
<div class="updHead">
|
| 186 |
<div>
|
| 187 |
<div id="updTxt">GAME VERSION</div>
|
| 188 |
-
<div id="updSub">v1.
|
| 189 |
</div>
|
| 190 |
<div class="updBtns">
|
| 191 |
<button id="updCancel" style="display:none">✕</button>
|
|
@@ -208,6 +210,7 @@
|
|
| 208 |
<button class="setupTabBtn" data-tab="forces">FORCES</button>
|
| 209 |
<button class="setupTabBtn" data-tab="rules">RULES</button>
|
| 210 |
<button class="setupTabBtn" data-tab="economy">ECONOMY</button>
|
|
|
|
| 211 |
</div>
|
| 212 |
<div class="setupScroll">
|
| 213 |
|
|
@@ -217,13 +220,13 @@
|
|
| 217 |
</section>
|
| 218 |
|
| 219 |
<section class="setupCard" data-setup-tab="map">
|
| 220 |
-
<div class="secLbl">
|
| 221 |
<div class="optRow" id="battleScaleRow">
|
| 222 |
-
<button class="bsbtn dbtn" data-bs="compact">COMPACT</button>
|
| 223 |
-
<button class="bsbtn dbtn" data-bs="standard">STANDARD</button>
|
| 224 |
-
<button class="bsbtn dbtn
|
| 225 |
</div>
|
| 226 |
-
<div class="secHint" id="battleScaleHint">2.
|
| 227 |
</section>
|
| 228 |
|
| 229 |
<section class="setupCard deployPlan" data-setup-tab="forces">
|
|
@@ -318,15 +321,22 @@
|
|
| 318 |
<div class="secHint" id="defFocusHint">Classic RTS balance between mobile armies and static defences.</div>
|
| 319 |
</section>
|
| 320 |
|
| 321 |
-
<section class="setupCard
|
| 322 |
-
<div class="secLbl">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
<div class="optRow optGrid" id="wcRowSel">
|
| 324 |
<button class="wbtn dbtn on" data-w="0">None</button>
|
| 325 |
<button class="wbtn dbtn" data-w="1">☠ 1</button>
|
| 326 |
<button class="wbtn dbtn" data-w="2">☠ 2</button>
|
| 327 |
<button class="wbtn dbtn" data-w="3">☠ 3</button>
|
| 328 |
</div>
|
| 329 |
-
<div class="secHint danger" id="wcHint">
|
| 330 |
</section>
|
| 331 |
</div>
|
| 332 |
|
|
@@ -348,11 +358,10 @@
|
|
| 348 |
<div class="overlay" id="opsScr">
|
| 349 |
<div class="subMenuHead">
|
| 350 |
<h2>⚔ OPERATIONS</h2>
|
| 351 |
-
<span>
|
| 352 |
</div>
|
| 353 |
<div class="screenTabs" id="opsTabs" role="tablist" aria-label="Operation categories">
|
| 354 |
<button class="screenTabBtn on" id="opsTab-threat" type="button" role="tab" data-mf-tab="threat" aria-controls="opsPane-threat"><span class="tabGlyph">⚠</span><span>THREAT</span></button>
|
| 355 |
-
<button class="screenTabBtn" id="opsTab-modifiers" type="button" role="tab" data-mf-tab="modifiers" aria-controls="opsPane-modifiers"><span class="tabGlyph">◆</span><span>MODIFIERS</span></button>
|
| 356 |
<button class="screenTabBtn" id="opsTab-weekly" type="button" role="tab" data-mf-tab="weekly" aria-controls="opsPane-weekly"><span class="tabGlyph">▣</span><span>WEEKLY</span></button>
|
| 357 |
<button class="screenTabBtn" id="opsTab-mastery" type="button" role="tab" data-mf-tab="mastery" aria-controls="opsPane-mastery"><span class="tabGlyph">★</span><span>MASTERY</span></button>
|
| 358 |
</div>
|
|
@@ -370,11 +379,6 @@
|
|
| 370 |
<div id="threatRow"></div>
|
| 371 |
<div id="threatInfo" class="secHint"></div>
|
| 372 |
</section>
|
| 373 |
-
<section class="setupCard mfTabPanel" id="opsPane-modifiers" role="tabpanel" aria-labelledby="opsTab-modifiers" data-mf-panel="modifiers">
|
| 374 |
-
<div class="secLbl">OPERATION MODIFIERS — pick your own fight</div>
|
| 375 |
-
<div id="opModRow"></div>
|
| 376 |
-
<div id="opPayout"></div>
|
| 377 |
-
</section>
|
| 378 |
<section class="setupCard mfTabPanel" id="opsPane-weekly" role="tabpanel" aria-labelledby="opsTab-weekly" data-mf-panel="weekly">
|
| 379 |
<div class="secLbl">WEEKLY OPERATION</div>
|
| 380 |
<div id="weeklyBox"></div>
|
|
@@ -554,7 +558,7 @@
|
|
| 554 |
<div class="title" style="font-size:34px">PAUSED</div>
|
| 555 |
<button class="mbtn" id="resumeBtn">▶ RESUME</button>
|
| 556 |
<button class="mbtn alt" id="pauseSettings">⚙ SETTINGS</button>
|
| 557 |
-
<button class="mbtn alt" id="quitBtn">✦
|
| 558 |
</div>
|
| 559 |
<script src="./boot.js"></script>
|
| 560 |
</body>
|
|
|
|
| 16 |
<link rel="icon" href="./assets/icons/icon-192.png">
|
| 17 |
<link rel="manifest" href="./assets/app.webmanifest">
|
| 18 |
<title>MASSFRONT — Mobile RTS</title>
|
| 19 |
+
<link rel="stylesheet" href="./src/styles/ui.css?v=1.32.0">
|
| 20 |
+
<link rel="stylesheet" href="./src/styles/auth.css?v=1.32.0">
|
| 21 |
+
<link rel="stylesheet" href="./src/styles/tutorial.css?v=1.32.0">
|
| 22 |
+
<link rel="stylesheet" href="./src/styles/ads.css?v=1.32.0">
|
| 23 |
+
<link rel="stylesheet" href="./src/styles/store.css?v=1.32.0">
|
| 24 |
+
<link rel="stylesheet" href="./src/styles/restree.css?v=1.32.0">
|
| 25 |
+
<link rel="stylesheet" href="./src/styles/intro.css?v=1.32.0">
|
| 26 |
</head>
|
| 27 |
<body>
|
| 28 |
<canvas id="gl"></canvas>
|
|
|
|
| 90 |
<button class="cbtn abtn" id="abHeal"><span class="em">🛠</span><span>Repair</span><div class="cdring"></div></button>
|
| 91 |
<button class="cbtn abtn" id="abRage"><span class="em">⚡</span><span>Surge</span><div class="cdring"></div></button>
|
| 92 |
<button class="cbtn abtn" id="abLance"><span class="em">🛰</span><span>Lance</span><div class="cdring"></div></button>
|
| 93 |
+
<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>
|
| 94 |
<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>
|
| 95 |
<button class="cbtn abtn" id="abHero"><span class="em">🤖</span><span>CDR</span><span class="lvl" id="heroLvlBadge">1</span></button>
|
| 96 |
</div>
|
|
|
|
| 187 |
<div class="updHead">
|
| 188 |
<div>
|
| 189 |
<div id="updTxt">GAME VERSION</div>
|
| 190 |
+
<div id="updSub">v1.32.0</div>
|
| 191 |
</div>
|
| 192 |
<div class="updBtns">
|
| 193 |
<button id="updCancel" style="display:none">✕</button>
|
|
|
|
| 210 |
<button class="setupTabBtn" data-tab="forces">FORCES</button>
|
| 211 |
<button class="setupTabBtn" data-tab="rules">RULES</button>
|
| 212 |
<button class="setupTabBtn" data-tab="economy">ECONOMY</button>
|
| 213 |
+
<button class="setupTabBtn" data-tab="modifiers">MODIFIERS</button>
|
| 214 |
</div>
|
| 215 |
<div class="setupScroll">
|
| 216 |
|
|
|
|
| 220 |
</section>
|
| 221 |
|
| 222 |
<section class="setupCard" data-setup-tab="map">
|
| 223 |
+
<div class="secLbl">MAP SIZE</div>
|
| 224 |
<div class="optRow" id="battleScaleRow">
|
| 225 |
+
<button class="bsbtn dbtn" data-bs="compact"><b>COMPACT</b><span>1.9 KM</span><small>8–12 MIN</small></button>
|
| 226 |
+
<button class="bsbtn dbtn on" data-bs="standard"><b>STANDARD</b><span>2.2 KM</span><small>12–18 MIN</small></button>
|
| 227 |
+
<button class="bsbtn dbtn" data-bs="large"><b>LARGE</b><span>2.6 KM</span><small>18–25 MIN</small></button>
|
| 228 |
</div>
|
| 229 |
+
<div class="secHint" id="battleScaleHint">2.2 KM · EST. 12–18 MIN — Balanced travel, expansion space and contested resources.</div>
|
| 230 |
</section>
|
| 231 |
|
| 232 |
<section class="setupCard deployPlan" data-setup-tab="forces">
|
|
|
|
| 321 |
<div class="secHint" id="defFocusHint">Classic RTS balance between mobile armies and static defences.</div>
|
| 322 |
</section>
|
| 323 |
|
| 324 |
+
<section class="setupCard" data-setup-tab="modifiers">
|
| 325 |
+
<div class="secLbl">MATCH MODIFIERS — CHOOSE YOUR RISK</div>
|
| 326 |
+
<div class="secHint">Optional rules for this deployment. Selected modifiers increase the projected payout and override the random wildcard count below.</div>
|
| 327 |
+
<div id="opModRow"></div>
|
| 328 |
+
<div id="opPayout"></div>
|
| 329 |
+
</section>
|
| 330 |
+
|
| 331 |
+
<section class="setupCard danger" data-setup-tab="modifiers">
|
| 332 |
+
<div class="secLbl">RANDOM MODIFIER LOADOUT</div>
|
| 333 |
<div class="optRow optGrid" id="wcRowSel">
|
| 334 |
<button class="wbtn dbtn on" data-w="0">None</button>
|
| 335 |
<button class="wbtn dbtn" data-w="1">☠ 1</button>
|
| 336 |
<button class="wbtn dbtn" data-w="2">☠ 2</button>
|
| 337 |
<button class="wbtn dbtn" data-w="3">☠ 3</button>
|
| 338 |
</div>
|
| 339 |
+
<div class="secHint danger" id="wcHint">Quick-pick random danger modifiers · +35% XP & cores each</div>
|
| 340 |
</section>
|
| 341 |
</div>
|
| 342 |
|
|
|
|
| 358 |
<div class="overlay" id="opsScr">
|
| 359 |
<div class="subMenuHead">
|
| 360 |
<h2>⚔ OPERATIONS</h2>
|
| 361 |
+
<span>Campaign · threat ladder · weekly operation · mastery</span>
|
| 362 |
</div>
|
| 363 |
<div class="screenTabs" id="opsTabs" role="tablist" aria-label="Operation categories">
|
| 364 |
<button class="screenTabBtn on" id="opsTab-threat" type="button" role="tab" data-mf-tab="threat" aria-controls="opsPane-threat"><span class="tabGlyph">⚠</span><span>THREAT</span></button>
|
|
|
|
| 365 |
<button class="screenTabBtn" id="opsTab-weekly" type="button" role="tab" data-mf-tab="weekly" aria-controls="opsPane-weekly"><span class="tabGlyph">▣</span><span>WEEKLY</span></button>
|
| 366 |
<button class="screenTabBtn" id="opsTab-mastery" type="button" role="tab" data-mf-tab="mastery" aria-controls="opsPane-mastery"><span class="tabGlyph">★</span><span>MASTERY</span></button>
|
| 367 |
</div>
|
|
|
|
| 379 |
<div id="threatRow"></div>
|
| 380 |
<div id="threatInfo" class="secHint"></div>
|
| 381 |
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
<section class="setupCard mfTabPanel" id="opsPane-weekly" role="tabpanel" aria-labelledby="opsTab-weekly" data-mf-panel="weekly">
|
| 383 |
<div class="secLbl">WEEKLY OPERATION</div>
|
| 384 |
<div id="weeklyBox"></div>
|
|
|
|
| 558 |
<div class="title" style="font-size:34px">PAUSED</div>
|
| 559 |
<button class="mbtn" id="resumeBtn">▶ RESUME</button>
|
| 560 |
<button class="mbtn alt" id="pauseSettings">⚙ SETTINGS</button>
|
| 561 |
+
<button class="mbtn alt" id="quitBtn">✦ ABANDON MATCH</button>
|
| 562 |
</div>
|
| 563 |
<script src="./boot.js"></script>
|
| 564 |
</body>
|
src/airlift-factions.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================================
|
| 2 |
+
BUILDABLE FACTION AIRLIFTS — geometry-only takeover
|
| 3 |
+
----------------------------------------------------------------------------
|
| 4 |
+
airlift.js owns the Atlas simulation, cargo manifests and mobile orders.
|
| 5 |
+
This file loads immediately after it and changes only model factories before
|
| 6 |
+
initModels() uploads them. Keeping the art takeover separate means transport
|
| 7 |
+
saves and mechanics cannot fork by faction.
|
| 8 |
+
============================================================================ */
|
| 9 |
+
|
| 10 |
+
/* Four honest, disconnected nacelles are part of the Atlas silhouette, not
|
| 11 |
+
painted engine marks on one broad wing. The coordinates are also a compact
|
| 12 |
+
art-contract used by the render regression. */
|
| 13 |
+
const MF_AFL_ATLAS_LIFT_PODS=[[-5.4,-7.4],[-5.4,7.4],[4.8,-7.4],[4.8,7.4]];
|
| 14 |
+
const MF_AFL_PHASE_LIFT_NODES=[[5.7,-3.2],[5.7,3.2],[-.8,-5.7],[-.8,5.7],[-6.1,-8.0],[-6.1,8.0]];
|
| 15 |
+
const MF_AFL_PHASE_APERTURE=[-.2,2.0,3.2];
|
| 16 |
+
|
| 17 |
+
/* Custom Coalition colours are material-routed just like the core palette.
|
| 18 |
+
Without these bindings the builder would inherit whichever material the
|
| 19 |
+
previous primitive happened to use, making the Ark's energy nodes matte. */
|
| 20 |
+
const MF_AFL_SYN_HULL=C(72,88,92), MF_AFL_SYN_EDGE=C(142,176,166), MF_AFL_SYN_GLOW=C(104,255,146);
|
| 21 |
+
COL_MAT.set(MF_AFL_SYN_HULL,MAT.TWR_COAT);
|
| 22 |
+
COL_MAT.set(MF_AFL_SYN_EDGE,MAT.TWR_MACH);
|
| 23 |
+
COL_MAT.set(MF_AFL_SYN_GLOW,MAT.LAMP);
|
| 24 |
+
|
| 25 |
+
/* TERRAN FRONTLINE — ATLAS SKYCRANE
|
| 26 |
+
A narrow armoured cargo spine carries a framed forward cockpit and a real
|
| 27 |
+
shadowed rear ramp. Four lift pods sit beyond thin outriggers, leaving clear
|
| 28 |
+
daylight between pod, hull and neighbour at command zoom. */
|
| 29 |
+
function mfAflAtlasModel(){
|
| 30 |
+
const m=MB();
|
| 31 |
+
const spine=[[-10.2,-2.8],[-8.0,-3.5],[4.8,-3.25],[9.5,-2.0],[10.8,0],
|
| 32 |
+
[9.5,2.0],[4.8,3.25],[-8.0,3.5],[-10.2,2.8]];
|
| 33 |
+
m.extrude(0,2.7,0,spine,3.15,MET_D); // cargo pressure hull
|
| 34 |
+
m.extrude(-1.2,5.8,0,spine.map(p=>[p[0]*.72,p[1]*.70]),1.25,MET);
|
| 35 |
+
m.bevelBox(6.9,5.25,0,5.2,1.25,5.0,.38,MET_L); // cockpit frame
|
| 36 |
+
m.extrude(7.4,6.45,0,[[-2.0,-2.05],[1.45,-1.35],[2.0,0],[1.45,1.35],[-2.0,2.05]],
|
| 37 |
+
1.45,GLASS); // unmistakable forward canopy
|
| 38 |
+
m.box(7.4,7.94,0,3.5,.24,4.0,DARK); // canopy roof bar
|
| 39 |
+
for(const sd of [-1,1])m.box(7.9,6.82,sd*2.18,3.4,.28,.25,TEAM_T,.08*sd);
|
| 40 |
+
|
| 41 |
+
/* Rear opening is layered: housing, black aperture, then a projecting ramp
|
| 42 |
+
lip. At mobile tilt this remains a readable exit instead of a flat end. */
|
| 43 |
+
m.bevelBox(-8.4,3.45,0,4.1,2.6,6.2,.45,MET_D);
|
| 44 |
+
m.box(-10.28,3.78,0,.34,2.15,4.6,DARKER);
|
| 45 |
+
m.wedge(-10.72,2.75,0,1.15,.55,4.9,MET_L,0,true);
|
| 46 |
+
for(const sd of [-1,1])m.box(-10.55,3.05,sd*2.18,.25,.30,.42,LAMP);
|
| 47 |
+
|
| 48 |
+
for(const P of MF_AFL_ATLAS_LIFT_PODS){
|
| 49 |
+
const x=P[0],z=P[1],sd=Math.sign(z);
|
| 50 |
+
m.bevelBox(x,3.7,sd*5.25,3.0,.65,4.2,.22,MET_D); // narrow outrigger
|
| 51 |
+
m.bevelBox(x,2.1,z,4.7,3.0,4.0,.48,MET_D); // separated pod casing
|
| 52 |
+
m.bevelBox(x,5.05,z,3.55,1.0,3.0,.26,MET_L);
|
| 53 |
+
m.cyl(x,.35,z,1.60,1.38,1.55,14,DARKER); // downward lift bell
|
| 54 |
+
m.ring(x,1.94,z,1.02,1.48,14,ENERGY); // visible lift core
|
| 55 |
+
m.tube(x,5.98,z,1.28,.72,.42,12,DARKER); // open intake, not a flat cap
|
| 56 |
+
m.box(x,5.48,z+sd*1.58,2.35,.24,.24,TEAM_A);
|
| 57 |
+
m.box(x+1.42,4.28,z,1.05,.30,2.55,MET_L); // service hatch rail
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
/* Vehicle-scale roof detail breaks up the long transport body without
|
| 61 |
+
changing the narrow load-bearing silhouette. */
|
| 62 |
+
m.bevelBox(-1.8,7.05,0,7.6,.78,4.6,.24,TEAM_T);
|
| 63 |
+
ventBank(m,-2.0,7.86,0,4.8,3.7,6,DARK,0);
|
| 64 |
+
m.greeble(-5.8,7.58,0,2.8,3.3,.55,5,MET_L,0,2471);
|
| 65 |
+
for(const sd of [-1,1]){
|
| 66 |
+
glowStrip(m,1.7,7.26,sd*2.18,5.0,ENERGY,0);
|
| 67 |
+
m.bevelBox(-3.7,2.15,sd*3.45,4.6,.78,.82,.18,DARKER); // landing rail
|
| 68 |
+
for(const x of [-5.2,-2.2])m.cyl(x,.25,sd*3.45,.42,.42,1.25,8,RUBBER);
|
| 69 |
+
}
|
| 70 |
+
sensorMast(m,-6.2,7.25,1.15,1.75,MET_L);
|
| 71 |
+
return {hull:m.build(),tur:null,s:1.16,air:1};
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
/* SYNDICATE COALITION — PHASE ARK
|
| 75 |
+
No cockpit, tail, cargo fuselage or Terran nacelles are reused. Separate
|
| 76 |
+
shallow delta plates form a broad triangular craft around an actual open
|
| 77 |
+
centre; the illuminated ring bridges that opening as a teleport aperture.
|
| 78 |
+
Six distributed nodes provide lift, so its energy language is structural. */
|
| 79 |
+
function mfAflPhaseArkModel(){
|
| 80 |
+
const m=MB();
|
| 81 |
+
for(const sd of [-1,1]){
|
| 82 |
+
/* Two convex plates per side avoid a hidden concave-cap fan while leaving
|
| 83 |
+
the circular middle physically absent. */
|
| 84 |
+
m.extrude(0,.65,0,[[9.8,sd*1.15],[-8.6,sd*10.2],[-6.3,sd*4.15]],1.45,MF_AFL_SYN_HULL);
|
| 85 |
+
m.extrude(0,1.25,0,[[9.8,sd*1.15],[-6.3,sd*4.15],[2.75,sd*3.05]],1.05,TWR_ARM_D);
|
| 86 |
+
m.box(-1.7,2.31,sd*6.1,10.6,.20,.58,MF_AFL_SYN_EDGE,sd*.32);
|
| 87 |
+
m.box(-3.8,2.53,sd*7.65,6.0,.16,.34,TEAM_T,sd*.52);
|
| 88 |
+
}
|
| 89 |
+
m.extrude(0,.72,0,[[10.5,0],[2.75,-2.85],[2.75,2.85]],1.62,TWR_ARM_D); // delta nose, no canopy
|
| 90 |
+
m.extrude(0,.70,0,[[-8.4,-3.9],[-2.8,-3.0],[-2.8,3.0],[-8.4,3.9]],1.55,MF_AFL_SYN_HULL);
|
| 91 |
+
m.bevelBox(-5.7,2.28,0,4.4,.48,5.8,.18,MF_AFL_SYN_EDGE); // rear field bridge, not a tail
|
| 92 |
+
|
| 93 |
+
const A=MF_AFL_PHASE_APERTURE;
|
| 94 |
+
m.ring(A[0],2.18,0,A[1],A[2],28,DARKER); // structural aperture rim
|
| 95 |
+
m.ring(A[0],2.38,0,2.30,2.88,28,MF_AFL_SYN_GLOW); // open luminous teleport annulus
|
| 96 |
+
m.ring(A[0],2.48,0,1.82,2.12,28,MF_AFL_SYN_EDGE);
|
| 97 |
+
for(let k=0;k<8;k++){
|
| 98 |
+
const a=k/8*TAU;
|
| 99 |
+
m.box(A[0]+Math.cos(a)*3.05,2.22,Math.sin(a)*3.05,.72,.36,.34,MF_AFL_SYN_EDGE,-a);
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
for(const P of MF_AFL_PHASE_LIFT_NODES){
|
| 103 |
+
const x=P[0],z=P[1];
|
| 104 |
+
m.cyl(x,.34,z,.88,.72,.58,12,TWR_MACH);
|
| 105 |
+
m.ring(x,1.02,z,.62,1.05,14,MF_AFL_SYN_GLOW);
|
| 106 |
+
m.sphere(x,1.13,z,.56,8,MF_AFL_SYN_GLOW,.62,false);
|
| 107 |
+
m.ring(x,1.63,z,.35,.76,12,MF_AFL_SYN_EDGE);
|
| 108 |
+
}
|
| 109 |
+
/* Small field-control bosses sell scale while keeping the entire craft
|
| 110 |
+
shallower than one Atlas lift pod. */
|
| 111 |
+
for(const sd of [-1,1])for(const x of [-5.4,-2.2,1.2,4.6])
|
| 112 |
+
m.bevelBox(x,2.35,sd*(3.8+(.5-Math.abs(x)*.025)),1.25,.44,.92,.13,
|
| 113 |
+
(x===1.2||x===-5.4)?MF_AFL_SYN_GLOW:MF_AFL_SYN_EDGE);
|
| 114 |
+
return {hull:m.build(),tur:null,s:1.17,air:1};
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* Safe because main.js calls initModels only after every manifest script has
|
| 118 |
+
loaded. The default mesh serves Terran/Nova; only the Coalition receives the
|
| 119 |
+
Phase Ark through the existing faction registry. */
|
| 120 |
+
UNIT_MDL[MF_UT_AIRLIFT]=mfAflAtlasModel;
|
| 121 |
+
FAC_KIT.syndicate[MF_UT_AIRLIFT]=mfAflPhaseArkModel;
|
| 122 |
+
|
src/airlift.js
ADDED
|
@@ -0,0 +1,857 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================================
|
| 2 |
+
ATLAS SKYCRANE — buildable heavy air transport
|
| 3 |
+
----------------------------------------------------------------------------
|
| 4 |
+
The pre-deployment `carrier` is the orbital base lander. This module never
|
| 5 |
+
reuses that singleton: the Skycrane is an ordinary TYPES unit with its own
|
| 6 |
+
serialized passenger manifest, orders and generation-safe handles.
|
| 7 |
+
|
| 8 |
+
Passengers are deliberately removed from the live simulation while aboard.
|
| 9 |
+
Keeping a hidden ground unit alive made spatial queries, fog and splash
|
| 10 |
+
damage continue to see it; a compact manifest is both cheaper and honest.
|
| 11 |
+
============================================================================ */
|
| 12 |
+
const MF_UT_AIRLIFT=TYPES.length;
|
| 13 |
+
const MF_AIRLIFT_CAPACITY=12;
|
| 14 |
+
TYPES.push({
|
| 15 |
+
name:'Atlas Skycrane',spr:'raptor',tur:null,size:34,r:12.5,hp:1850,dmg:0,rng:0,cool:9,
|
| 16 |
+
spd:50,psp:0,ptype:0,aoe:0,wk:'n',tg:'a',cm:210,ce:820,bt:13,air:1,tier:2,
|
| 17 |
+
cat:'transport',airTransport:1,transportCap:MF_AIRLIFT_CAPACITY
|
| 18 |
+
});
|
| 19 |
+
UCAT.transport={nm:'AIR TRANSPORT',em:'⇩'};
|
| 20 |
+
UNIT_MODES[MF_UT_AIRLIFT]=[0];
|
| 21 |
+
ARM[MF_UT_AIRLIFT]=2;
|
| 22 |
+
if(typeof INTEL_ROLE_COPY!=='undefined')
|
| 23 |
+
INTEL_ROLE_COPY.transport='Heavy airlift for relocating infantry, constructors and light vehicles';
|
| 24 |
+
if(typeof INTEL_ROLE_GUIDE!=='undefined') INTEL_ROLE_GUIDE.transport={
|
| 25 |
+
tag:'HEAVY AIRLIFT',form:'LOAD / DROP',
|
| 26 |
+
use:'Carry a mixed light force over terrain, then deploy it in a readable formation.',
|
| 27 |
+
avoid:'Unarmed and vulnerable to anti-air; heavy armor, artillery and aircraft cannot board.'
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
/* A broad twin-engine silhouette remains legible at command zoom: deep cargo
|
| 31 |
+
hull, separated wing nacelles, open-looking rear ramp and sparse team trim.
|
| 32 |
+
The geometry uses the same material atlas as the current vehicle pipeline. */
|
| 33 |
+
function mfAirliftModel(){
|
| 34 |
+
const m=MB();
|
| 35 |
+
const hull=[[-9,-3.5],[-6.5,-4.8],[3.5,-4.2],[9,-2.2],[10.5,0],[9,2.2],[3.5,4.2],[-6.5,4.8],[-9,3.5]];
|
| 36 |
+
m.extrude(0,3.0,0,hull,3.4,MET_D);
|
| 37 |
+
m.extrude(-.6,6.4,0,hull.map(p=>[p[0]*.78,p[1]*.76]),2.2,MET);
|
| 38 |
+
m.wedge(7.8,8.6,0,5.8,2.0,5.6,GLASS,0,true); // flight deck
|
| 39 |
+
m.bevelBox(-6.8,5.6,0,4.0,2.2,6.2,.55,DARK); // cargo ramp housing
|
| 40 |
+
m.box(-9.0,4.0,0,.65,2.9,5.0,DARKER); // shadowed rear aperture
|
| 41 |
+
m.box(-9.38,4.2,0,.28,2.4,4.2,MET_L); // ramp lip
|
| 42 |
+
for(const side of [-1,1]){
|
| 43 |
+
m.wedge(-.2,5.8,side*6.0,14.5,1.15,5.6,TEAM_A,Math.PI/2,side<0);
|
| 44 |
+
m.bevelBox(-1.0,5.0,side*7.8,7.4,3.0,3.5,.55,MET_D);
|
| 45 |
+
cylX(m,-4.2,6.0,side*7.8,7.2,1.62,1.20,12,MET,false);
|
| 46 |
+
tubeX(m,2.5,6.0,side*7.8,1.05,1.78,.92,12,TWR_BORE);
|
| 47 |
+
m.box(-.8,8.4,side*7.8,4.8,.32,1.9,ENERGY);
|
| 48 |
+
m.bevelBox(-4.2,1.4,side*3.9,6.2,1.0,1.3,.25,DARKER); // landing rail
|
| 49 |
+
for(const x of [-6.4,-2.0]) m.cyl(x,.25,side*3.9,.48,.48,1.4,8,RUBBER);
|
| 50 |
+
}
|
| 51 |
+
m.bevelBox(-2.0,8.3,0,8.8,1.2,5.8,.35,TEAM_T);
|
| 52 |
+
ventBank(m,-3.0,9.52,0,5.2,4.4,6,MET_D,0);
|
| 53 |
+
glowStrip(m,2.2,8.55,-3.1,5.4,ENERGY,0);
|
| 54 |
+
glowStrip(m,2.2,8.55, 3.1,5.4,ENERGY,0);
|
| 55 |
+
m.box(-7.0,8.1,0,3.8,.7,1.1,LAMP); // cargo status light
|
| 56 |
+
sensorMast(m,-5.2,9.4,1.4,2.1,MET_L);
|
| 57 |
+
return {hull:m.build(),tur:null,s:1.18,air:1};
|
| 58 |
+
}
|
| 59 |
+
/* TYPES gained two non-rendered simulation entries before the transport.
|
| 60 |
+
Filling their fallback factories also keeps initModels' dense loop valid. */
|
| 61 |
+
if(!UNIT_MDL[31]) UNIT_MDL[31]=mdlBroodmother;
|
| 62 |
+
if(!UNIT_MDL[32]) UNIT_MDL[32]=mdlConstructor;
|
| 63 |
+
UNIT_MDL[MF_UT_AIRLIFT]=mfAirliftModel;
|
| 64 |
+
|
| 65 |
+
const mfAirliftHolds=[];
|
| 66 |
+
let mfAirliftBoardOrders=[];
|
| 67 |
+
let mfAirliftAim=null;
|
| 68 |
+
let mfAirliftLastLoss=0;
|
| 69 |
+
|
| 70 |
+
function mfAirliftIsLive(i,gen){
|
| 71 |
+
return mfTransportIsLive(i,gen,'atlas');
|
| 72 |
+
}
|
| 73 |
+
function mfTransportKindByType(type){
|
| 74 |
+
if(type===MF_UT_AIRLIFT)return 'atlas';
|
| 75 |
+
if(typeof MF_UT_MASSFLESH!=='undefined'&&(type===MF_UT_MASSFLESH||type===MF_UT_MASSFLESH_AIR))return 'massflesh';
|
| 76 |
+
return '';
|
| 77 |
+
}
|
| 78 |
+
function mfTransportIsLive(i,gen,kind){
|
| 79 |
+
return i>=0&&ualive[i]&&ugen[i]===gen&&mfTransportKindByType(utype[i])===kind;
|
| 80 |
+
}
|
| 81 |
+
function mfAirliftHold(i,create){
|
| 82 |
+
if(i<0) return null;
|
| 83 |
+
let H=mfAirliftHolds[i];
|
| 84 |
+
if(H&&H.gen!==ugen[i]) H=mfAirliftHolds[i]=null;
|
| 85 |
+
if(!H&&create&&ualive[i]&&utype[i]===MF_UT_AIRLIFT)
|
| 86 |
+
H=mfAirliftHolds[i]={gen:ugen[i],capacity:MF_AIRLIFT_CAPACITY,used:0,cargo:[],mission:null,fx:0};
|
| 87 |
+
return H;
|
| 88 |
+
}
|
| 89 |
+
function mfAirliftSlotCost(i){
|
| 90 |
+
if(i<0||!ualive[i]) return 0;
|
| 91 |
+
const T=TYPES[utype[i]],cat=T.cat||'veh';
|
| 92 |
+
if(T.airTransport||T.air||T.naval||cat==='art'||cat==='exp'||cat==='hero') return 0;
|
| 93 |
+
if(T.builder||T.miner) return 1;
|
| 94 |
+
if(cat==='inf') return 1;
|
| 95 |
+
if(cat==='veh'&&(ARM[utype[i]]||0)<=1&&T.size<=18) return 2;
|
| 96 |
+
return 0;
|
| 97 |
+
}
|
| 98 |
+
function mfAirliftReserved(i,gen){
|
| 99 |
+
let n=0;
|
| 100 |
+
for(const O of mfAirliftBoardOrders) if(O.sky===i&&O.skyGen===gen)n+=O.slots;
|
| 101 |
+
return n;
|
| 102 |
+
}
|
| 103 |
+
function mfAirliftSerialize(i,slots){
|
| 104 |
+
return {
|
| 105 |
+
type:utype[i],team:uteam[i],slots,
|
| 106 |
+
hpRatio:uhpm[i]>0?uhp[i]/uhpm[i]:1,
|
| 107 |
+
veteran:uvet[i],kills:ukills[i],mode:umode[i],cool:Math.max(0,ucool[i])
|
| 108 |
+
};
|
| 109 |
+
}
|
| 110 |
+
function mfAirliftBoardPoint(O,rank){
|
| 111 |
+
const side=(rank&1)?1:-1,row=Math.floor(rank/2),a=uang[O.sky]-Math.PI/2;
|
| 112 |
+
const back=17+row*5,lateral=side*(7+row*1.5);
|
| 113 |
+
return {
|
| 114 |
+
x:ux[O.sky]-Math.cos(a)*back-Math.sin(a)*lateral,
|
| 115 |
+
y:uy[O.sky]-Math.sin(a)*back+Math.cos(a)*lateral
|
| 116 |
+
};
|
| 117 |
+
}
|
| 118 |
+
function mfAirliftEligibleSelection(sky){
|
| 119 |
+
const ok=[],rejected=[];
|
| 120 |
+
for(let i=0;i<unitHigh;i++) if(ualive[i]&&usel[i]&&i!==sky){
|
| 121 |
+
const slots=mfAirliftSlotCost(i);
|
| 122 |
+
(slots?ok:rejected).push({i,gen:ugen[i],slots});
|
| 123 |
+
}
|
| 124 |
+
return {ok,rejected};
|
| 125 |
+
}
|
| 126 |
+
function mfAirliftIssueBoard(sky){
|
| 127 |
+
if(!ualive[sky]||uteam[sky]!==0||utype[sky]!==MF_UT_AIRLIFT)return false;
|
| 128 |
+
const H=mfAirliftHold(sky,true),picked=mfAirliftEligibleSelection(sky);
|
| 129 |
+
let free=H.capacity-H.used-mfAirliftReserved(sky,H.gen),accepted=0,full=0;
|
| 130 |
+
for(const P of picked.ok){
|
| 131 |
+
if(P.slots>free){full++;continue;}
|
| 132 |
+
mfAirliftBoardOrders.push({unit:P.i,unitGen:P.gen,sky,skyGen:H.gen,slots:P.slots});
|
| 133 |
+
free-=P.slots;accepted++;
|
| 134 |
+
}
|
| 135 |
+
if(!accepted){
|
| 136 |
+
toast(picked.rejected.length?'CANNOT BOARD — only infantry, constructors and light vehicles':'SKYCRANE CARGO FULL');
|
| 137 |
+
sfx('ui');return false;
|
| 138 |
+
}
|
| 139 |
+
utgt[sky]=-1;ustate[sky]=0;utx[sky]=ux[sky];uty[sky]=uy[sky];uhold[sky]=1;
|
| 140 |
+
clearSel();usel[sky]=1;updateSelInfo();
|
| 141 |
+
toast('⇩ BOARDING '+accepted+' UNIT'+(accepted===1?'':'S')+(picked.rejected.length?' · '+picked.rejected.length+' TOO HEAVY':'')+(full?' · '+full+' NO SPACE':''));
|
| 142 |
+
uiCommandAck('move',accepted,ux[sky],uy[sky]);
|
| 143 |
+
return true;
|
| 144 |
+
}
|
| 145 |
+
function mfAirliftFormation(cargo,x,y){
|
| 146 |
+
const n=cargo.length,cols=Math.max(1,Math.ceil(Math.sqrt(n*1.45))),rows=Math.ceil(n/cols),out=[];
|
| 147 |
+
let largest=5;
|
| 148 |
+
for(const P of cargo)largest=Math.max(largest,(TYPES[P.type]&&TYPES[P.type].r)||5);
|
| 149 |
+
const gap=Math.max(16,largest*2+7);
|
| 150 |
+
for(let k=0;k<n;k++){
|
| 151 |
+
const col=k%cols,row=Math.floor(k/cols),px=x+(col-(Math.min(cols,n-row*cols)-1)/2)*gap;
|
| 152 |
+
const py=y+(row-(rows-1)/2)*gap;
|
| 153 |
+
let L=findLand(clamp(px,18,MAP-18),clamp(py,18,MAP-18));
|
| 154 |
+
if(typeof battlefieldClampPoint==='function')L=battlefieldClampPoint(L[0],L[1],22);
|
| 155 |
+
out.push({x:L[0],y:L[1]});
|
| 156 |
+
}
|
| 157 |
+
return out;
|
| 158 |
+
}
|
| 159 |
+
function mfAirliftCommandUnload(sky,x,y){
|
| 160 |
+
const H=mfAirliftHold(sky,false);
|
| 161 |
+
if(!H||!H.cargo.length)return false;
|
| 162 |
+
x=clamp(x,24,MAP-24);y=clamp(y,24,MAP-24);
|
| 163 |
+
if(typeof battlefieldClampPoint==='function'){const p=battlefieldClampPoint(x,y,48);x=p[0];y=p[1];}
|
| 164 |
+
H.mission={x,y,slots:mfAirliftFormation(H.cargo,x,y)};
|
| 165 |
+
uhold[sky]=0;utgt[sky]=-1;ustate[sky]=1;utx[sky]=x;uty[sky]=y;
|
| 166 |
+
mfAirliftAim=null;mfAirliftUpdateButton();
|
| 167 |
+
addParticle(3,x,y,0,0,.72,44,92,226,255);
|
| 168 |
+
toast('⇩ DROP ZONE CONFIRMED · '+H.cargo.length+' PASSENGERS');
|
| 169 |
+
uiCommandAck('move',1,x,y);
|
| 170 |
+
return true;
|
| 171 |
+
}
|
| 172 |
+
function mfAirliftArmUnload(sky){
|
| 173 |
+
const H=mfAirliftHold(sky,false);
|
| 174 |
+
if(!H||!H.cargo.length){toast('SKYCRANE CARGO EMPTY');sfx('ui');return false;}
|
| 175 |
+
mfAirliftAim={sky,gen:ugen[sky]};
|
| 176 |
+
if(typeof aiming!=='undefined')aiming=-1;
|
| 177 |
+
if(typeof armPatrol!=='undefined'&&armPatrol)cancelPatrolDraft(true);
|
| 178 |
+
mfAirliftUpdateButton();
|
| 179 |
+
toast('⇩ TAP THE MAP TO SET A DROP ZONE');sfx('ui');return true;
|
| 180 |
+
}
|
| 181 |
+
function mfAirliftPreTick(dt){
|
| 182 |
+
for(let k=mfAirliftBoardOrders.length-1;k>=0;k--){
|
| 183 |
+
const O=mfAirliftBoardOrders[k];
|
| 184 |
+
if(!mfAirliftIsLive(O.sky,O.skyGen)||!ualive[O.unit]||ugen[O.unit]!==O.unitGen){
|
| 185 |
+
mfAirliftBoardOrders.splice(k,1);continue;
|
| 186 |
+
}
|
| 187 |
+
const rank=mfAirliftBoardOrders.filter(Q=>Q.sky===O.sky&&Q.skyGen===O.skyGen).indexOf(O);
|
| 188 |
+
const P=mfAirliftBoardPoint(O,Math.max(0,rank));
|
| 189 |
+
utgt[O.unit]=-1;utgtg[O.unit]=-1;uhold[O.unit]=0;ustate[O.unit]=1;
|
| 190 |
+
utx[O.unit]=P.x;uty[O.unit]=P.y;
|
| 191 |
+
}
|
| 192 |
+
for(let i=0;i<mfAirliftHolds.length;i++){
|
| 193 |
+
const H=mfAirliftHolds[i];if(!H||!mfAirliftIsLive(i,H.gen)||!H.mission)continue;
|
| 194 |
+
utgt[i]=-1;utgtg[i]=-1;uhold[i]=0;ustate[i]=1;utx[i]=H.mission.x;uty[i]=H.mission.y;
|
| 195 |
+
}
|
| 196 |
+
}
|
| 197 |
+
function mfAirliftUnloadNow(i,H){
|
| 198 |
+
const remaining=[],dropped=[];
|
| 199 |
+
for(let k=0;k<H.cargo.length;k++){
|
| 200 |
+
const P=H.cargo[k],S=H.mission.slots[k]||{x:H.mission.x,y:H.mission.y};
|
| 201 |
+
const u=spawnUnit(P.type,P.team,S.x,S.y);
|
| 202 |
+
if(u<0){remaining.push(P);continue;}
|
| 203 |
+
uhp[u]=clamp(uhpm[u]*P.hpRatio,1,uhpm[u]);uvet[u]=P.veteran;ukills[u]=P.kills;ucool[u]=P.cool;
|
| 204 |
+
if(unitModes(P.type).indexOf(P.mode)>=0)umode[u]=P.mode;
|
| 205 |
+
utx[u]=S.x;uty[u]=S.y;ustate[u]=0;
|
| 206 |
+
addBeam(ux[i],uy[i],ux[u],uy[u],3.4,90,225,255,.30,'tractor');
|
| 207 |
+
addParticle(3,ux[u],uy[u],0,0,.55,TYPES[P.type].size*1.9,92,226,255);
|
| 208 |
+
dropped.push(u);
|
| 209 |
+
}
|
| 210 |
+
H.cargo=remaining;H.used=remaining.reduce((n,P)=>n+P.slots,0);H.mission=null;
|
| 211 |
+
uhold[i]=0;ustate[i]=0;utx[i]=ux[i];uty[i]=uy[i];
|
| 212 |
+
sfx('deploy',ux[i],uy[i],1.15);
|
| 213 |
+
if(dropped.length){toast('⇩ '+dropped.length+' UNIT'+(dropped.length===1?'':'S')+' DEPLOYED');uiCommandAck('deploy',dropped.length,ux[i],uy[i]);}
|
| 214 |
+
if(remaining.length)toast('DROP PARTIAL — '+remaining.length+' PASSENGERS REMAIN');
|
| 215 |
+
updateSelInfo();
|
| 216 |
+
}
|
| 217 |
+
function mfAirliftPostTick(dt){
|
| 218 |
+
let changed=false;
|
| 219 |
+
for(let k=mfAirliftBoardOrders.length-1;k>=0;k--){
|
| 220 |
+
const O=mfAirliftBoardOrders[k];
|
| 221 |
+
if(!mfAirliftIsLive(O.sky,O.skyGen)||!ualive[O.unit]||ugen[O.unit]!==O.unitGen){
|
| 222 |
+
mfAirliftBoardOrders.splice(k,1);continue;
|
| 223 |
+
}
|
| 224 |
+
const reach=TYPES[utype[O.unit]].r+TYPES[MF_UT_AIRLIFT].r+8;
|
| 225 |
+
if(dist2(ux[O.unit],uy[O.unit],ux[O.sky],uy[O.sky])>reach*reach)continue;
|
| 226 |
+
const H=mfAirliftHold(O.sky,true),P=mfAirliftSerialize(O.unit,O.slots),px=ux[O.unit],py=uy[O.unit];
|
| 227 |
+
H.cargo.push(P);H.used+=O.slots;
|
| 228 |
+
addBeam(px,py,ux[O.sky],uy[O.sky],3.6,86,226,255,.34,'tractor');
|
| 229 |
+
addParticle(3,px,py,0,0,.45,TYPES[P.type].size*1.8,86,226,255);
|
| 230 |
+
killUnit(O.unit,true);mfAirliftBoardOrders.splice(k,1);changed=true;
|
| 231 |
+
sfx('laser',ux[O.sky],uy[O.sky],.62);
|
| 232 |
+
}
|
| 233 |
+
for(let i=0;i<mfAirliftHolds.length;i++){
|
| 234 |
+
const H=mfAirliftHolds[i];
|
| 235 |
+
if(!H)continue;
|
| 236 |
+
if(!mfAirliftIsLive(i,H.gen)){mfAirliftHolds[i]=null;continue;}
|
| 237 |
+
if(H.mission){
|
| 238 |
+
H.fx-=dt;
|
| 239 |
+
if(H.fx<=0){
|
| 240 |
+
H.fx=.16;
|
| 241 |
+
for(const S of H.mission.slots)addParticle(3,S.x,S.y,0,0,.23,18,86,226,255);
|
| 242 |
+
addParticle(3,H.mission.x,H.mission.y,0,0,.28,42,210,246,255);
|
| 243 |
+
}
|
| 244 |
+
if(dist2(ux[i],uy[i],H.mission.x,H.mission.y)<22*22)mfAirliftUnloadNow(i,H);
|
| 245 |
+
}
|
| 246 |
+
}
|
| 247 |
+
if(changed){
|
| 248 |
+
let loaded=0;for(const H of mfAirliftHolds)if(H)loaded+=H.cargo.length;
|
| 249 |
+
toast('⇩ CARGO SECURED · '+loaded+' ABOARD');updateSelInfo();
|
| 250 |
+
}
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
/* Takeovers preserve the base simulation as the fallback path. */
|
| 254 |
+
const mfAirliftSpawnUnitBase=spawnUnit;
|
| 255 |
+
spawnUnit=function(type,team,x,y){
|
| 256 |
+
const i=mfAirliftSpawnUnitBase(type,team,x,y);
|
| 257 |
+
if(i>=0){mfAirliftHolds[i]=null;if(type===MF_UT_AIRLIFT)mfAirliftHold(i,true);}
|
| 258 |
+
return i;
|
| 259 |
+
};
|
| 260 |
+
const mfAirliftKillUnitBase=killUnit;
|
| 261 |
+
killUnit=function(i,silent){
|
| 262 |
+
if(ualive[i]&&utype[i]===MF_UT_AIRLIFT){
|
| 263 |
+
const H=mfAirliftHold(i,false),lost=H?H.cargo.length:0;
|
| 264 |
+
mfAirliftLastLoss=lost;
|
| 265 |
+
for(const O of mfAirliftBoardOrders) if(O.sky===i&&O.skyGen===ugen[i]&&ualive[O.unit]&&ugen[O.unit]===O.unitGen){
|
| 266 |
+
ustate[O.unit]=0;utgt[O.unit]=-1;utx[O.unit]=ux[O.unit];uty[O.unit]=uy[O.unit];
|
| 267 |
+
}
|
| 268 |
+
mfAirliftBoardOrders=mfAirliftBoardOrders.filter(O=>!(O.sky===i&&O.skyGen===ugen[i]));
|
| 269 |
+
if(mfAirliftAim&&mfAirliftAim.sky===i&&mfAirliftAim.gen===ugen[i])mfAirliftAim=null;
|
| 270 |
+
if(H){H.cargo.length=0;H.used=0;H.mission=null;mfAirliftHolds[i]=null;}
|
| 271 |
+
if(lost&&!silent){
|
| 272 |
+
addParticle(3,ux[i],uy[i],0,0,.8,70,255,112,70);
|
| 273 |
+
toast('AIRLIFT LOST · '+lost+' PASSENGER'+(lost===1?'':'S')+' KILLED');
|
| 274 |
+
}
|
| 275 |
+
}
|
| 276 |
+
return mfAirliftKillUnitBase(i,silent);
|
| 277 |
+
};
|
| 278 |
+
const mfAirliftUnitTickBase=unitTick;
|
| 279 |
+
unitTick=function(dt){mfAirliftPreTick(dt);mfAirliftUnitTickBase(dt);mfAirliftPostTick(dt);};
|
| 280 |
+
|
| 281 |
+
/* A board order is intentionally higher priority than ordinary friendly-unit
|
| 282 |
+
selection: selected troops + tap Skycrane is the whole mobile gesture. */
|
| 283 |
+
const mfAirliftOnTapBase=onTap;
|
| 284 |
+
onTap=function(sx,sy){
|
| 285 |
+
const W=s2w(sx,sy),wx=W[0],wy=W[1];
|
| 286 |
+
if(mfAirliftAim){
|
| 287 |
+
const A=mfAirliftAim;mfAirliftAim=null;mfAirliftUpdateButton();
|
| 288 |
+
if(mfAirliftIsLive(A.sky,A.gen))mfAirliftCommandUnload(A.sky,wx,wy);
|
| 289 |
+
return;
|
| 290 |
+
}
|
| 291 |
+
const pk=pickUnit(wx,wy);
|
| 292 |
+
if(pk.own>=0&&utype[pk.own]===MF_UT_AIRLIFT&&selCount()>0&&!usel[pk.own]){
|
| 293 |
+
if(mfAirliftIssueBoard(pk.own))return;
|
| 294 |
+
const hasOther=(()=>{for(let i=0;i<unitHigh;i++)if(ualive[i]&&usel[i]&&i!==pk.own)return true;return false;})();
|
| 295 |
+
if(hasOther)return;
|
| 296 |
+
}
|
| 297 |
+
return mfAirliftOnTapBase(sx,sy);
|
| 298 |
+
};
|
| 299 |
+
|
| 300 |
+
function mfAirliftSvgIcon(size){
|
| 301 |
+
const w=document.createElement('div');w.className='mfAirliftIcon';w.style.width=size+'px';w.style.height=size+'px';
|
| 302 |
+
w.innerHTML='<svg viewBox="0 0 96 96" aria-hidden="true"><defs><linearGradient id="mfSkyG" x1="0" y1="0" x2="0" y2="1"><stop stop-color="#d8edf7"/><stop offset="1" stop-color="#547187"/></linearGradient></defs><path fill="#17354a" stroke="#75dcff" stroke-width="3" d="M8 53l25-12 7-24h16l7 24 25 12-4 13-27-5-2 18H41l-2-18-27 5z"/><path fill="url(#mfSkyG)" d="M39 33h18l7 22-16 12-16-12z"/><path fill="#7ee8ff" d="M42 24h12v11H42zM16 54h18v6H16zm46 0h18v6H62z"/><path fill="#07121a" d="M41 69h14v8H41z"/></svg>';
|
| 303 |
+
return w;
|
| 304 |
+
}
|
| 305 |
+
const mfAirliftUnitIconBase=unitIconEl;
|
| 306 |
+
unitIconEl=function(tIdx,size){return tIdx===MF_UT_AIRLIFT?mfAirliftSvgIcon(size):mfAirliftUnitIconBase(tIdx,size);};
|
| 307 |
+
const mfAirliftPurposeBase=intelUnitPurpose;
|
| 308 |
+
intelUnitPurpose=function(T){
|
| 309 |
+
if(T&&T.airTransport)return 'Unarmed heavy airlift. Board infantry, constructors and light vehicles, then set a protected formation drop zone.';
|
| 310 |
+
return mfAirliftPurposeBase(T);
|
| 311 |
+
};
|
| 312 |
+
|
| 313 |
+
const mfAirliftShowUnitTypeBase=showUnitTypeCard;
|
| 314 |
+
showUnitTypeCard=function(tIdx,pinned){
|
| 315 |
+
mfAirliftShowUnitTypeBase(tIdx,pinned);
|
| 316 |
+
if(tIdx!==MF_UT_AIRLIFT)return;
|
| 317 |
+
const row=$('unitCard')&&$('unitCard').querySelector('.ucChips');
|
| 318 |
+
if(row)row.insertAdjacentHTML('beforeend',intelChip('⬣','WEIGHT: HEAVY')
|
| 319 |
+
+'<span class="ucChip mfAirliftCapacity"><i>▦</i>'+MF_AIRLIFT_CAPACITY+' SLOTS</span>');
|
| 320 |
+
};
|
| 321 |
+
const mfAirliftShowUnitBase=showUnitCard;
|
| 322 |
+
showUnitCard=function(uIdx,bIdx,pinned){
|
| 323 |
+
mfAirliftShowUnitBase(uIdx,bIdx,pinned);
|
| 324 |
+
if(uIdx<0||!ualive[uIdx]||utype[uIdx]!==MF_UT_AIRLIFT)return;
|
| 325 |
+
const H=mfAirliftHold(uIdx,true),chip=$('unitCard')&&$('unitCard').querySelector('.mfAirliftCapacity');
|
| 326 |
+
if(chip)chip.innerHTML='<i>▦</i>'+H.used+' / '+H.capacity+' SLOTS';
|
| 327 |
+
};
|
| 328 |
+
|
| 329 |
+
function mfAirliftRenderCard(){
|
| 330 |
+
const g=$('prodGrid'),T=TYPES[MF_UT_AIRLIFT];g.innerHTML='';
|
| 331 |
+
renderMenuRoleBrief('unit','transport',[MF_UT_AIRLIFT]);
|
| 332 |
+
const d=document.createElement('div');d.className='bcard mfAirliftCard';
|
| 333 |
+
d.innerHTML='<div class="nm">'+T.name+'</div><div class="cost">'+T.cm+'m <span>'+T.ce+'e</span></div>'
|
| 334 |
+
+'<div class="mfAirliftBadges"><b>HEAVY</b><b>AIR TRANSPORT</b><b>'+T.transportCap+' SLOTS</b></div>'
|
| 335 |
+
+'<div class="cardPurpose">INFANTRY · CONSTRUCTORS · LIGHT VEHICLES</div>';
|
| 336 |
+
d.setAttribute('role','button');d.setAttribute('aria-label','Build Atlas Skycrane heavy air transport, capacity 12 slots');
|
| 337 |
+
const icw=document.createElement('div');icw.className='icw';icw.appendChild(unitIconEl(MF_UT_AIRLIFT,48));d.insertBefore(icw,d.firstChild);
|
| 338 |
+
d.addEventListener('pointerdown',ev=>{
|
| 339 |
+
ev.stopPropagation();if(openBld<0)return;const B=blds[openBld];
|
| 340 |
+
if(B&&B.alive&&B.queue.length<30){B.queue.push(MF_UT_AIRLIFT);sfx('ui');renderQueue();}
|
| 341 |
+
});
|
| 342 |
+
addCardIntelButton(d,'unit',MF_UT_AIRLIFT);g.appendChild(d);
|
| 343 |
+
}
|
| 344 |
+
const mfAirliftRenderProdBase=renderProdMenu;
|
| 345 |
+
renderProdMenu=function(){
|
| 346 |
+
const B=openBld>=0?blds[openBld]:null,
|
| 347 |
+
atlas=!!(B&&B.type==='airfield'&&bldFactionKey(B)!=='horde'),
|
| 348 |
+
wants=atlas&&prodTab==='transport';
|
| 349 |
+
mfAirliftRenderProdBase();
|
| 350 |
+
if(!atlas)return;
|
| 351 |
+
const tr=$('prodTabs');tr.style.display='flex';
|
| 352 |
+
const b=document.createElement('button');b.className='tabBtn'+(wants?' on':'');
|
| 353 |
+
b.innerHTML='<span class="tEm">⇩</span>AIRLIFT';b.setAttribute('aria-label','Air transport production');
|
| 354 |
+
if(wants)for(const x of tr.querySelectorAll('.tabBtn'))x.classList.remove('on');
|
| 355 |
+
if(wants)b.classList.add('on');
|
| 356 |
+
b.addEventListener('pointerdown',ev=>{ev.stopPropagation();prodTab='transport';sfx('ui');renderProdMenu();});
|
| 357 |
+
tr.appendChild(b);
|
| 358 |
+
if(wants){prodTab='transport';mfAirliftRenderCard();}
|
| 359 |
+
};
|
| 360 |
+
|
| 361 |
+
function mfAirliftSelected(){
|
| 362 |
+
let found=-1;
|
| 363 |
+
for(let i=0;i<unitHigh;i++)if(ualive[i]&&usel[i]&&utype[i]===MF_UT_AIRLIFT){if(found>=0)return -1;found=i;}
|
| 364 |
+
return found;
|
| 365 |
+
}
|
| 366 |
+
function mfAirliftUpdateButton(){
|
| 367 |
+
const b=$('mfUnloadBtn');if(!b)return;
|
| 368 |
+
const i=mfAirliftSelected(),H=i>=0?mfAirliftHold(i,true):null;
|
| 369 |
+
b.style.display=i>=0?'flex':'none';b.disabled=!H||!H.cargo.length;
|
| 370 |
+
b.classList.toggle('on',!!(mfAirliftAim&&i===mfAirliftAim.sky&&ugen[i]===mfAirliftAim.gen));
|
| 371 |
+
const nm=b.querySelector('.lbl');if(nm)nm.textContent=H&&H.cargo.length?'UNLOAD '+H.cargo.length:'UNLOAD';
|
| 372 |
+
b.setAttribute('aria-label',H?'Unload Skycrane cargo, '+H.used+' of '+H.capacity+' slots used':'Unload Skycrane cargo');
|
| 373 |
+
}
|
| 374 |
+
const mfAirliftUpdateSelBase=updateSelInfo;
|
| 375 |
+
updateSelInfo=function(){
|
| 376 |
+
mfAirliftUpdateSelBase();
|
| 377 |
+
const i=mfAirliftSelected(),H=i>=0?mfAirliftHold(i,true):null;
|
| 378 |
+
if(i<0&&mfAirliftAim){mfAirliftAim=null;}
|
| 379 |
+
if(H){
|
| 380 |
+
const line=$('selInfo')&&$('selInfo').querySelector('.selIntelCopy span');
|
| 381 |
+
if(line)line.textContent+=' · AIR TRANSPORT · CARGO '+H.used+'/'+H.capacity;
|
| 382 |
+
const chip=$('unitCard')&&$('unitCard').querySelector('.mfAirliftCapacity');
|
| 383 |
+
if(chip)chip.innerHTML='<i>▦</i>'+H.used+' / '+H.capacity+' SLOTS';
|
| 384 |
+
}
|
| 385 |
+
mfAirliftUpdateButton();
|
| 386 |
+
};
|
| 387 |
+
function mfAirliftInitUI(){
|
| 388 |
+
const row=$('tacRow');if(!row||$('mfUnloadBtn'))return;
|
| 389 |
+
const b=document.createElement('button');b.type='button';b.className='cbtn mfAirliftOrder';b.id='mfUnloadBtn';
|
| 390 |
+
b.style.display='none';b.innerHTML='<span class="em">⇩</span><span class="lbl">UNLOAD</span>';
|
| 391 |
+
b.addEventListener('pointerdown',ev=>{ev.preventDefault();ev.stopPropagation();const i=mfAirliftSelected();if(i>=0)mfAirliftArmUnload(i);});
|
| 392 |
+
row.insertBefore(b,$('clearBtn'));
|
| 393 |
+
const st=document.createElement('style');st.textContent='\n'
|
| 394 |
+
+'.mfAirliftIcon{filter:drop-shadow(0 0 7px rgba(90,220,255,.42)) drop-shadow(0 4px 4px rgba(0,0,0,.65))}.mfAirliftIcon svg{width:100%;height:100%;display:block}'
|
| 395 |
+
+'.mfAirliftBadges{display:flex;flex-wrap:wrap;justify-content:center;gap:3px;margin:3px 0}.mfAirliftBadges b{padding:3px 5px;border:1px solid rgba(100,220,255,.38);border-radius:4px;background:rgba(20,92,122,.32);color:#9beaff;font:700 7px/1 var(--fT);letter-spacing:.05em}'
|
| 396 |
+
+'.mfAirliftCard{min-height:126px}.mfAirliftOrder{min-width:49px!important;min-height:44px!important}.mfAirliftOrder:disabled{opacity:.42;filter:saturate(.45)}';
|
| 397 |
+
document.head.appendChild(st);
|
| 398 |
+
}
|
| 399 |
+
mfAirliftInitUI();
|
| 400 |
+
|
| 401 |
+
/* ============================================================================
|
| 402 |
+
MASSFLESH — Brood breakthrough transport
|
| 403 |
+
----------------------------------------------------------------------------
|
| 404 |
+
This is not a recoloured Skycrane. It is a heavy walking digestive organism
|
| 405 |
+
that absorbs a light brood, unfolds into a short-lived flying form, lashes a
|
| 406 |
+
path through rear structures, then births the stored organisms behind the
|
| 407 |
+
defensive line. Two TYPES entries let the existing simulation and renderer
|
| 408 |
+
apply honest counters without per-frame global flags:
|
| 409 |
+
landed = HEAVY ground target → anti-tank
|
| 410 |
+
winged = AIR target → anti-air
|
| 411 |
+
============================================================================ */
|
| 412 |
+
const MF_UT_MASSFLESH=TYPES.length,MF_MASS_CAPACITY=18,MF_MASS_FLIGHT=26;
|
| 413 |
+
TYPES.push({
|
| 414 |
+
name:'Massflesh Carrier',spr:'brood',tur:null,size:42,r:15.5,hp:2850,dmg:0,rng:0,cool:9,
|
| 415 |
+
spd:25,psp:0,ptype:0,aoe:0,wk:'n',tg:'g',cm:190,ce:610,bt:12,air:0,tier:2,
|
| 416 |
+
cat:'biomass',brood:1,massflesh:1,transportCap:MF_MASS_CAPACITY
|
| 417 |
+
});
|
| 418 |
+
const MF_UT_MASSFLESH_AIR=TYPES.length;
|
| 419 |
+
TYPES.push({
|
| 420 |
+
name:'Massflesh Ascendant',spr:'brood',tur:null,size:45,r:16,hp:2850,dmg:58,rng:145,cool:1.55,
|
| 421 |
+
spd:58,psp:0,ptype:0,aoe:0,wk:'n',tg:'g',cm:0,ce:0,bt:0,air:1,tier:2,
|
| 422 |
+
cat:'biomass',brood:1,massflesh:1,massfleshAir:1,transportCap:MF_MASS_CAPACITY
|
| 423 |
+
});
|
| 424 |
+
UCAT.biomass={nm:'LIVING TRANSPORT',em:'♒'};
|
| 425 |
+
UNIT_MODES[MF_UT_MASSFLESH]=[0];UNIT_MODES[MF_UT_MASSFLESH_AIR]=[0];
|
| 426 |
+
ARM[MF_UT_MASSFLESH]=2;ARM[MF_UT_MASSFLESH_AIR]=0;
|
| 427 |
+
INTEL_ROLE_COPY.biomass='Living breakthrough carrier that consumes a light brood and births it behind defenses';
|
| 428 |
+
INTEL_ROLE_GUIDE.biomass={
|
| 429 |
+
tag:'BREAKTHROUGH ORGANISM',form:'MERGE / ASCEND / BIRTH',
|
| 430 |
+
use:'Absorb a light brood, cross the defensive line during timed flight, then birth the attack in the rear.',
|
| 431 |
+
avoid:'Counter the landed body with anti-tank and the winged body with dedicated anti-air.'
|
| 432 |
+
};
|
| 433 |
+
|
| 434 |
+
/* Massflesh uses its own biological zones. Reusing CHITIN/BIO_MEM across the
|
| 435 |
+
whole animal produced a pale toy capsule in the tactical preview and a row
|
| 436 |
+
of dark beads in flight. These colours select distinct PBR atlas materials:
|
| 437 |
+
shell, wet muscle, translucent lift sac, membrane, bone, cavity and organ. */
|
| 438 |
+
const MF_CHITIN=C(72,43,39),MF_CHITIN_H=C(132,79,57),MF_FLESH=C(88,35,50),MF_FLESH_H=C(151,61,77);
|
| 439 |
+
const MF_SAC=C(148,196,84),MF_MEM=C(72,48,86),MF_TENDON=C(111,77,94),MF_WEAK=C(218,112,255),MF_WEAK_H=C(232,255,132);
|
| 440 |
+
const MF_BORE=C(11,5,10),MF_BONE=C(211,190,145);
|
| 441 |
+
COL_MAT.set(MF_CHITIN,MAT.CHITIN);COL_MAT.set(MF_CHITIN_H,MAT.CHITIN);
|
| 442 |
+
COL_MAT.set(MF_FLESH,MAT.RUST);COL_MAT.set(MF_FLESH_H,MAT.LEAF);
|
| 443 |
+
COL_MAT.set(MF_SAC,MAT.CRYST);COL_MAT.set(MF_MEM,MAT.LEAF);COL_MAT.set(MF_TENDON,MAT.LEAF);
|
| 444 |
+
COL_MAT.set(MF_WEAK,MAT.TWR_GLOW);COL_MAT.set(MF_WEAK_H,MAT.LAMP);
|
| 445 |
+
COL_MAT.set(MF_BORE,MAT.TWR_BORE);COL_MAT.set(MF_BONE,MAT.STONE);
|
| 446 |
+
|
| 447 |
+
/* Join a tapered primitive between authored anatomy points. Curved limbs use
|
| 448 |
+
several short segments: inexpensive on mobile, but far clearer than boxes
|
| 449 |
+
or disconnected bead chains. */
|
| 450 |
+
function mfMassLimb(m,a,b,r1,r2,col,seg){
|
| 451 |
+
let dx=b[0]-a[0],dy=b[1]-a[1],dz=b[2]-a[2];const len=Math.hypot(dx,dy,dz)||1;dx/=len;dy/=len;dz/=len;
|
| 452 |
+
const rx=Math.abs(dy)>.92?1:0,ry=Math.abs(dy)>.92?0:1,rz=0;
|
| 453 |
+
let vx=ry*dz-rz*dy,vy=rz*dx-rx*dz,vz=rx*dy-ry*dx,vl=Math.hypot(vx,vy,vz)||1;vx/=vl;vy/=vl;vz/=vl;
|
| 454 |
+
const wx=vy*dz-vz*dy,wy=vz*dx-vx*dz,wz=vx*dy-vy*dx,mm=MB();
|
| 455 |
+
mm.cyl(0,0,0,r1,r2,len,seg||7,col,true);
|
| 456 |
+
for(let i=0;i<mm.v.length;i+=12){
|
| 457 |
+
const px=mm.v[i],py=mm.v[i+1],pz=mm.v[i+2],nx=mm.v[i+3],ny=mm.v[i+4],nz=mm.v[i+5];
|
| 458 |
+
mm.v[i]=a[0]+vx*px+dx*py+wx*pz;mm.v[i+1]=a[1]+vy*px+dy*py+wy*pz;mm.v[i+2]=a[2]+vz*px+dz*py+wz*pz;
|
| 459 |
+
mm.v[i+3]=vx*nx+dx*ny+wx*nz;mm.v[i+4]=vy*nx+dy*ny+wy*nz;mm.v[i+5]=vz*nx+dz*ny+wz*nz;
|
| 460 |
+
}
|
| 461 |
+
const off=m.n;for(const v of mm.v)m.v.push(v);for(const ix of mm.i)m.i.push(ix+off);m.n+=mm.n;m.m=mm.m;m.tm=mm.tm;return m;
|
| 462 |
+
}
|
| 463 |
+
function mfMassWeakOrgan(m,x,y,z,r){
|
| 464 |
+
m.sphere(x,y,z,r,8,MF_WEAK,.82,false);m.ring(x,y+r*.78,z,r*1.05,r*1.36,12,MF_WEAK_H);
|
| 465 |
+
}
|
| 466 |
+
function mfMassHook(m,a,b,c,d,r){
|
| 467 |
+
mfMassLimb(m,a,b,r,r*.72,MF_TENDON,7);mfMassLimb(m,b,c,r*.73,r*.40,MF_CHITIN_H,7);
|
| 468 |
+
mfMassLimb(m,c,d,r*.42,.045,MF_BONE,6);m.sphere(b[0],b[1],b[2],r*.84,7,MF_CHITIN,.62,false);
|
| 469 |
+
}
|
| 470 |
+
|
| 471 |
+
function mfMassGroundModel(){
|
| 472 |
+
const m=MB();
|
| 473 |
+
/* Unequal wet belly, armored thorax and shielded head break the old capsule
|
| 474 |
+
silhouette before the smaller anatomy is even visible. */
|
| 475 |
+
m.sphere(-3.8,3.4,0,7.8,13,MF_FLESH,.52,false);
|
| 476 |
+
m.sphere(-8.5,4.0,-.7,5.8,11,MF_CHITIN,.66,false);
|
| 477 |
+
m.sphere(.8,4.5,.5,6.5,12,MF_CHITIN,.58,false);
|
| 478 |
+
m.sphere(7.9,4.1,-.3,4.7,11,MF_CHITIN_H,.64,false);
|
| 479 |
+
/* Layered shell scutes leave wet seams instead of coating the animal in one
|
| 480 |
+
noisy material. Their alternating offset keeps the top outline irregular. */
|
| 481 |
+
for(let k=0;k<5;k++){
|
| 482 |
+
const x=-8.0+k*3.25,z=(k&1)?.85:-.75,r=3.15-k*.12;
|
| 483 |
+
m.sphere(x,7.0+(k%3)*.24,z,r,8,k&1?MF_CHITIN_H:MF_CHITIN,.30,false);
|
| 484 |
+
for(const sd of [-1,1])m.sphere(x,5.3,sd*(3.9+(k%2)*.35),1.45,7,k&1?MF_FLESH_H:MF_CHITIN_H,.55,false);
|
| 485 |
+
}
|
| 486 |
+
/* Open dorsal birthing cavity: black lumen, fleshy lip, hooked teeth and
|
| 487 |
+
visible stored biomass remain legible at production-preview zoom. */
|
| 488 |
+
m.tube(-4.4,6.8,0,4.8,2.35,2.0,13,MF_BORE);m.ring(-4.4,8.82,0,2.25,4.85,14,MF_FLESH_H);
|
| 489 |
+
for(let k=0;k<7;k++){
|
| 490 |
+
const a=k/7*TAU,r=3.7+(k&1)*.25;
|
| 491 |
+
mfMassLimb(m,[-4.4+Math.cos(a)*r,8.6,Math.sin(a)*r],[-4.4+Math.cos(a)*r*1.13,10.0,Math.sin(a)*r*1.13],.34,.035,MF_BONE,6);
|
| 492 |
+
}
|
| 493 |
+
for(const p of [[-5.4,8.2,-1.1],[-3.5,8.4,.8],[-4.8,8.15,1.5]])m.sphere(p[0],p[1],p[2],.82,7,MF_SAC,.78,false);
|
| 494 |
+
/* Folded asymmetric lift sacs telegraph the later air state. Bright cores
|
| 495 |
+
are exposed weak organs rather than decorative team lights. */
|
| 496 |
+
const sacs=[[-1.1,6.0,-5.1,2.65],[1.0,6.5,5.7,3.45],[-6.8,5.7,5.0,2.25]];
|
| 497 |
+
for(const S of sacs){m.sphere(S[0],S[1],S[2],S[3],9,MF_SAC,.72,false);m.ring(S[0],S[1]+S[3]*.72,S[2],S[3]*.65,S[3]*.88,11,MF_CHITIN_H);}
|
| 498 |
+
mfMassWeakOrgan(m,1.0,8.0,5.7,1.02);mfMassWeakOrgan(m,-1.1,7.55,-5.1,.82);
|
| 499 |
+
for(const sd of [-1,1]){
|
| 500 |
+
/* Five load-bearing hooks per side create a low predatory stance. */
|
| 501 |
+
for(let k=0;k<5;k++){
|
| 502 |
+
const x=-7.0+k*3.35,a=[x,3.3,sd*4.5],b=[x-.7,1.4,sd*(8.0+(k&1)*.8)],c=[x+1.1,.35,sd*(11.0+k*.38)],d=[x+2.35,1.0,sd*(9.8+k*.32)];
|
| 503 |
+
mfMassHook(m,a,b,c,d,.82-k*.035);
|
| 504 |
+
}
|
| 505 |
+
/* Forward mandibles establish +X and curl back toward the mouth. */
|
| 506 |
+
mfMassHook(m,[8.2,4.0,sd*2.5],[11.2,3.0,sd*4.1],[13.5,2.0,sd*2.8],[12.0,2.7,sd*.85],.72);
|
| 507 |
+
}
|
| 508 |
+
tubeX(m,10.0,3.8,0,2.35,1.9,.9,10,MF_BORE);
|
| 509 |
+
mfMassWeakOrgan(m,8.9,6.5,-2.15,.82);mfMassWeakOrgan(m,9.5,6.25,1.75,.64);
|
| 510 |
+
for(let k=0;k<7;k++){
|
| 511 |
+
const x=-9+k*2.25,h=2.6+(k%3)*.65;mfMassLimb(m,[x,7.5,(k&1)?.45:-.45],[x-.5,7.5+h,(k&1)?1.0:-1.0],.38,.025,MF_BONE,6);
|
| 512 |
+
}
|
| 513 |
+
return {hull:m.build(),tur:null,s:1.08};
|
| 514 |
+
}
|
| 515 |
+
function mfMassAirModel(){
|
| 516 |
+
const m=MB();
|
| 517 |
+
/* Ascension rotates the siege pod upright. A pendant muscular body carries
|
| 518 |
+
the payload below a swollen neural lift organ; there are no machine-like
|
| 519 |
+
wings or aircraft surfaces. */
|
| 520 |
+
m.sphere(-1.0,8.0,0,5.7,11,MF_FLESH,.92,false);
|
| 521 |
+
m.sphere(-1.8,12.2,.3,6.0,12,MF_CHITIN,.72,false);
|
| 522 |
+
m.sphere(-2.0,18.3,.7,7.2,13,MF_SAC,1.02,false);
|
| 523 |
+
m.sphere(-4.2,17.0,-1.0,6.4,12,MF_CHITIN,.60,false);
|
| 524 |
+
m.sphere(1.4,17.8,1.0,5.3,11,MF_CHITIN_H,.58,false);
|
| 525 |
+
/* A radial armored cranial halo protects—but does not completely cover—the
|
| 526 |
+
translucent lift sac. Horns point away from the weak core. */
|
| 527 |
+
for(let k=0;k<10;k++){
|
| 528 |
+
const a=k/10*TAU+.16,ca=Math.cos(a),sa=Math.sin(a),r=7.2+(k%3)*.35;
|
| 529 |
+
m.sphere(-2+ca*r,18.2+(k&1)*.35,.7+sa*r,2.25,8,k&1?MF_CHITIN_H:MF_CHITIN,.46,false);
|
| 530 |
+
mfMassLimb(m,[-2+ca*r,18.5,.7+sa*r],[-2+ca*(r+5.0),21.0+(k%2)*1.1,.7+sa*(r+5.0)],.54,.035,k%3?MF_BONE:MF_CHITIN_H,6);
|
| 531 |
+
}
|
| 532 |
+
/* The large exposed organ is the visually honest anti-air target. */
|
| 533 |
+
m.sphere(1.9,20.0,-.5,3.55,10,MF_SAC,.94,false);mfMassWeakOrgan(m,2.8,21.2,-.5,2.15);
|
| 534 |
+
/* Raised above the horn plane so command-view cameras cannot hide the
|
| 535 |
+
anti-air weak organ behind the cranial shell. */
|
| 536 |
+
mfMassWeakOrgan(m,-1.0,23.7,.6,3.15);m.sphere(-.7,24.8,.4,2.35,9,MF_WEAK_H,1,false);
|
| 537 |
+
m.ring(-2.0,24.1,.7,3.2,5.25,15,MF_WEAK_H);
|
| 538 |
+
/* Birthing throat faces forward beneath the cranial shell. */
|
| 539 |
+
tubeX(m,2.0,12.0,0,3.8,3.0,1.45,11,MF_BORE);ringX(m,5.55,12.0,0,1.5,3.05,12,MF_FLESH_H);
|
| 540 |
+
for(const sd of [-1,1]){
|
| 541 |
+
mfMassWeakOrgan(m,2.2,15.0,sd*3.15,sd>0?.82:.62);
|
| 542 |
+
mfMassHook(m,[2.5,13.0,sd*2.6],[6.0,10.0,sd*4.3],[8.6,6.0,sd*3.3],[6.5,7.0,sd*.9],.72);
|
| 543 |
+
}
|
| 544 |
+
/* Six secondary tendrils hang from the lower body; three long primaries form
|
| 545 |
+
the recognizable hooked cephalopod silhouette and remain separated. */
|
| 546 |
+
for(let k=0;k<6;k++){
|
| 547 |
+
const a=k/6*TAU+.35,ca=Math.cos(a),sa=Math.sin(a),start=[-1+ca*3.8,9.0,sa*3.8];
|
| 548 |
+
mfMassHook(m,start,[-1+ca*6.0,5.7,sa*6.2],[-1+ca*8.2,1.0,sa*8.6],[-1+ca*6.1,2.0,sa*6.4],.68-k*.025);
|
| 549 |
+
}
|
| 550 |
+
for(const sd of [-1,0,1]){
|
| 551 |
+
const z=sd*3.4;mfMassLimb(m,[1.3,10.0,z],[5.0,5.7,z+sd*4.0],1.0,.70,MF_TENDON,8);
|
| 552 |
+
mfMassLimb(m,[5.0,5.7,z+sd*4.0],[11.5,.4,z+sd*8.0],.72,.35,MF_CHITIN_H,8);
|
| 553 |
+
mfMassLimb(m,[11.5,.4,z+sd*8.0],[8.2,1.8,z+sd*5.6],.36,.045,MF_BONE,6);
|
| 554 |
+
}
|
| 555 |
+
return {hull:m.build(),tur:null,s:1.02,air:1};
|
| 556 |
+
}
|
| 557 |
+
UNIT_MDL[MF_UT_MASSFLESH]=mfMassGroundModel;
|
| 558 |
+
UNIT_MDL[MF_UT_MASSFLESH_AIR]=mfMassAirModel;
|
| 559 |
+
|
| 560 |
+
const mfMassHolds=[];
|
| 561 |
+
let mfMassBoardOrders=[],mfMassBirthAim=null,mfMassAlertUnit=-1,mfMassAiNext=135,mfMassAiLastT=0;
|
| 562 |
+
function mfMassIsLive(i,gen){return mfTransportIsLive(i,gen,'massflesh');}
|
| 563 |
+
function mfMassHold(i,create){
|
| 564 |
+
if(i<0)return null;let H=mfMassHolds[i];
|
| 565 |
+
if(H&&H.gen!==ugen[i])H=mfMassHolds[i]=null;
|
| 566 |
+
if(!H&&create&&ualive[i]&&mfTransportKindByType(utype[i])==='massflesh')
|
| 567 |
+
H=mfMassHolds[i]={gen:ugen[i],capacity:MF_MASS_CAPACITY,used:0,cargo:[],flight:0,mission:null,fx:0,attack:0,aiT:2};
|
| 568 |
+
return H;
|
| 569 |
+
}
|
| 570 |
+
function mfMassSlotCost(i,team){
|
| 571 |
+
if(i<0||!ualive[i]||uteam[i]!==team)return 0;
|
| 572 |
+
const T=TYPES[utype[i]],cat=T.cat||'veh';
|
| 573 |
+
if(T.massflesh||T.air||T.naval||cat==='art'||cat==='exp'||cat==='hero'||(ARM[utype[i]]||0)>1)return 0;
|
| 574 |
+
if(!unitIsBrood(i))return 0;
|
| 575 |
+
return T.size<=17?1:2;
|
| 576 |
+
}
|
| 577 |
+
function mfMassReserved(i,gen){let n=0;for(const O of mfMassBoardOrders)if(O.mass===i&&O.massGen===gen)n+=O.slots;return n;}
|
| 578 |
+
function mfMassBoardPoint(O,rank){
|
| 579 |
+
const a=uang[O.mass]-Math.PI/2,side=(rank&1)?1:-1,row=Math.floor(rank/2);
|
| 580 |
+
return {x:ux[O.mass]-Math.cos(a)*(20+row*5)-Math.sin(a)*side*(8+row),
|
| 581 |
+
y:uy[O.mass]-Math.sin(a)*(20+row*5)+Math.cos(a)*side*(8+row)};
|
| 582 |
+
}
|
| 583 |
+
function mfMassQueueBoard(mass,entries,playerOrder){
|
| 584 |
+
const H=mfMassHold(mass,true);if(!H||utype[mass]!==MF_UT_MASSFLESH)return 0;
|
| 585 |
+
let free=H.capacity-H.used-mfMassReserved(mass,H.gen),n=0;
|
| 586 |
+
for(const i of entries){
|
| 587 |
+
const slots=mfMassSlotCost(i,uteam[mass]);
|
| 588 |
+
if(!slots||slots>free||mfMassBoardOrders.some(O=>O.unit===i&&O.unitGen===ugen[i]))continue;
|
| 589 |
+
mfMassBoardOrders.push({unit:i,unitGen:ugen[i],mass,massGen:H.gen,slots});free-=slots;n++;
|
| 590 |
+
}
|
| 591 |
+
if(n){
|
| 592 |
+
utgt[mass]=-1;ustate[mass]=0;utx[mass]=ux[mass];uty[mass]=uy[mass];uhold[mass]=1;
|
| 593 |
+
if(playerOrder){clearSel();usel[mass]=1;updateSelInfo();toast('♒ '+n+' BROOD MERGING INTO BIOMASS');uiCommandAck('move',n,ux[mass],uy[mass]);}
|
| 594 |
+
}
|
| 595 |
+
return n;
|
| 596 |
+
}
|
| 597 |
+
function mfMassIssueBoard(mass){
|
| 598 |
+
const candidates=[],rejected=[];
|
| 599 |
+
for(let i=0;i<unitHigh;i++)if(ualive[i]&&usel[i]&&i!==mass)(mfMassSlotCost(i,uteam[mass])?candidates:rejected).push(i);
|
| 600 |
+
const n=mfMassQueueBoard(mass,candidates,true);
|
| 601 |
+
if(!n){toast(rejected.length?'MASSFLESH REJECTS HEAVY OR NON-BROOD UNITS':'MASSFLESH BIOMASS FULL');sfx('ui');}
|
| 602 |
+
return n>0;
|
| 603 |
+
}
|
| 604 |
+
function mfMassAlert(i,state){
|
| 605 |
+
if(i<0||!ualive[i]||uteam[i]===0)return;
|
| 606 |
+
mfMassAlertUnit=i;const el=$('mfMassAlert');if(!el)return;
|
| 607 |
+
el.style.display='block';el.innerHTML='<b>⚠ MASSFLESH '+state+'</b><span>BREAKTHROUGH ORGANISM · TAP TO TRACK</span>';
|
| 608 |
+
}
|
| 609 |
+
function mfMassBeginFlight(i){
|
| 610 |
+
const H=mfMassHold(i,false);if(!H||!H.cargo.length||utype[i]!==MF_UT_MASSFLESH)return false;
|
| 611 |
+
utype[i]=MF_UT_MASSFLESH_AIR;H.flight=MF_MASS_FLIGHT;H.mission=null;H.attack=.25;uhold[i]=0;ustate[i]=0;
|
| 612 |
+
addParticle(3,ux[i],uy[i],0,0,1.0,100,185,92,255);
|
| 613 |
+
for(let k=0;k<10;k++)addParticle(4,ux[i]+rr(-12,12),uy[i]+rr(-12,12),rr(-18,18),rr(-18,18),.8,8,135,236,72);
|
| 614 |
+
sfx('cre_attack',ux[i],uy[i],1.6);
|
| 615 |
+
if(uteam[i]!==0){mfMassAlert(i,'AIRBORNE');sfx('alarm',ux[i],uy[i],1.15);if(typeof radioAck==='function')radioAck('attack',1,ux[i],uy[i]);}
|
| 616 |
+
else toast('♒ MASSFLESH ASCENDED · '+Math.ceil(H.flight)+'s FLIGHT WINDOW');
|
| 617 |
+
updateSelInfo();return true;
|
| 618 |
+
}
|
| 619 |
+
function mfMassBehindPlayerTarget(i){
|
| 620 |
+
let tx=heroIdx>=0&&ualive[heroIdx]?ux[heroIdx]:MAP*.25,ty=heroIdx>=0&&ualive[heroIdx]?uy[heroIdx]:MAP*.25;
|
| 621 |
+
for(const B of blds)if(B.alive&&B.team===0&&(B.type==='hq'||B.type==='fac')){tx=B.x;ty=B.y;if(B.type==='hq')break;}
|
| 622 |
+
let dx=tx-ux[i],dy=ty-uy[i],d=Math.hypot(dx,dy)||1;dx/=d;dy/=d;
|
| 623 |
+
const L=findLand(clamp(tx+dx*150,24,MAP-24),clamp(ty+dy*150,24,MAP-24));return {x:L[0],y:L[1]};
|
| 624 |
+
}
|
| 625 |
+
function mfMassCommandBirth(i,x,y){
|
| 626 |
+
const H=mfMassHold(i,false);if(!H||utype[i]!==MF_UT_MASSFLESH_AIR||!H.cargo.length)return false;
|
| 627 |
+
x=clamp(x,24,MAP-24);y=clamp(y,24,MAP-24);H.mission={x,y,slots:mfAirliftFormation(H.cargo,x,y)};
|
| 628 |
+
utgt[i]=-1;ustate[i]=1;utx[i]=x;uty[i]=y;mfMassBirthAim=null;mfMassUpdateUI();
|
| 629 |
+
addParticle(3,x,y,0,0,.8,58,168,92,255);
|
| 630 |
+
if(uteam[i]===0){toast('♒ BIRTH SITE MARKED · '+H.cargo.length+' ORGANISMS');uiCommandAck('deploy',H.cargo.length,x,y);}
|
| 631 |
+
return true;
|
| 632 |
+
}
|
| 633 |
+
function mfMassBirthNow(i,H){
|
| 634 |
+
const target=H.mission||{x:ux[i],y:uy[i],slots:mfAirliftFormation(H.cargo,ux[i],uy[i])},remaining=[],born=[];
|
| 635 |
+
for(let k=0;k<H.cargo.length;k++){
|
| 636 |
+
const P=H.cargo[k],S=target.slots[k]||target,u=spawnUnit(P.type,P.team,S.x,S.y);
|
| 637 |
+
if(u<0){remaining.push(P);continue;}
|
| 638 |
+
uhp[u]=clamp(uhpm[u]*P.hpRatio,1,uhpm[u]);uvet[u]=P.veteran;ukills[u]=P.kills;ucool[u]=P.cool;
|
| 639 |
+
if(unitModes(P.type).indexOf(P.mode)>=0)umode[u]=P.mode;
|
| 640 |
+
if(P.team===1){const q=mfMassBehindPlayerTarget(i);ustate[u]=2;utx[u]=q.x;uty[u]=q.y;}
|
| 641 |
+
addBeam(ux[i],uy[i],ux[u],uy[u],4.5,168,88,255,.36,'tractor');
|
| 642 |
+
addParticle(6,ux[u],uy[u],0,0,.55,TYPES[P.type].size*1.8,150,242,82);born.push(u);
|
| 643 |
+
}
|
| 644 |
+
H.cargo=remaining;H.used=remaining.reduce((n,P)=>n+P.slots,0);H.mission=null;H.flight=0;
|
| 645 |
+
utype[i]=MF_UT_MASSFLESH;ustate[i]=0;utx[i]=ux[i];uty[i]=uy[i];uhold[i]=0;
|
| 646 |
+
addParticle(3,ux[i],uy[i],0,0,.9,86,185,92,255);sfx('deploy',ux[i],uy[i],1.25);sfx('cre_attack',ux[i],uy[i],1.1);
|
| 647 |
+
if(uteam[i]===0)toast('♒ '+born.length+' ORGANISMS BORN INTO FORMATION');
|
| 648 |
+
updateSelInfo();return born;
|
| 649 |
+
}
|
| 650 |
+
function mfMassTentacleAttack(i,H){
|
| 651 |
+
let target=-1,best=145*145,kind='unit';
|
| 652 |
+
forUnitsIn(ux[i],uy[i],145,j=>{
|
| 653 |
+
if(uteam[j]===uteam[i]||TYPES[utype[j]].air)return;
|
| 654 |
+
const d=dist2(ux[i],uy[i],ux[j],uy[j]),light=(ARM[utype[j]]||0)===0;
|
| 655 |
+
const score=d*(light?.55:1.45);if(score<best){best=score;target=j;kind='unit';}
|
| 656 |
+
});
|
| 657 |
+
const b=findEnemyBld(ux[i],uy[i],uteam[i],145);
|
| 658 |
+
if(b>=0){const d=dist2(ux[i],uy[i],blds[b].x,blds[b].y)*.42;if(d<best){best=d;target=b;kind='building';}}
|
| 659 |
+
if(target<0)return null;
|
| 660 |
+
const x=kind==='unit'?ux[target]:blds[target].x,y=kind==='unit'?uy[target]:blds[target].y;
|
| 661 |
+
let dmg=TYPES[MF_UT_MASSFLESH_AIR].dmg;
|
| 662 |
+
if(kind==='building'){dmg*=1.75;damageBld(target,dmg,uteam[i]);}
|
| 663 |
+
else{dmg*=(ARM[utype[target]]||0)===0?1.45:.52;dealDamage(target,dmg,uteam[i],i);}
|
| 664 |
+
addBeam(ux[i],uy[i],x,y,5.2,176,78,255,.30,'lightning');addBeam(ux[i],uy[i],x,y,2.2,132,242,78,.34,'tractor');
|
| 665 |
+
addParticle(4,x,y,0,0,.42,15,135,238,76);sfx('cre_attack',ux[i],uy[i],1.25);sfx('sonic',x,y,.72);
|
| 666 |
+
H.attack=TYPES[MF_UT_MASSFLESH_AIR].cool;return {kind,target,dmg};
|
| 667 |
+
}
|
| 668 |
+
function mfMassPreTick(dt){
|
| 669 |
+
for(let k=mfMassBoardOrders.length-1;k>=0;k--){
|
| 670 |
+
const O=mfMassBoardOrders[k];
|
| 671 |
+
if(!mfMassIsLive(O.mass,O.massGen)||utype[O.mass]!==MF_UT_MASSFLESH||!ualive[O.unit]||ugen[O.unit]!==O.unitGen){mfMassBoardOrders.splice(k,1);continue;}
|
| 672 |
+
const rank=mfMassBoardOrders.filter(Q=>Q.mass===O.mass&&Q.massGen===O.massGen).indexOf(O),P=mfMassBoardPoint(O,Math.max(0,rank));
|
| 673 |
+
utgt[O.unit]=-1;utgtg[O.unit]=-1;ustate[O.unit]=1;utx[O.unit]=P.x;uty[O.unit]=P.y;uhold[O.unit]=0;
|
| 674 |
+
}
|
| 675 |
+
for(let i=0;i<mfMassHolds.length;i++){
|
| 676 |
+
const H=mfMassHolds[i];if(!H||!mfMassIsLive(i,H.gen)||utype[i]!==MF_UT_MASSFLESH_AIR||!H.mission)continue;
|
| 677 |
+
utgt[i]=-1;utgtg[i]=-1;ustate[i]=1;utx[i]=H.mission.x;uty[i]=H.mission.y;uhold[i]=0;
|
| 678 |
+
}
|
| 679 |
+
}
|
| 680 |
+
function mfMassPostTick(dt){
|
| 681 |
+
for(let k=mfMassBoardOrders.length-1;k>=0;k--){
|
| 682 |
+
const O=mfMassBoardOrders[k];
|
| 683 |
+
if(!mfMassIsLive(O.mass,O.massGen)||!ualive[O.unit]||ugen[O.unit]!==O.unitGen){mfMassBoardOrders.splice(k,1);continue;}
|
| 684 |
+
const reach=TYPES[utype[O.unit]].r+TYPES[MF_UT_MASSFLESH].r+9;
|
| 685 |
+
if(dist2(ux[O.unit],uy[O.unit],ux[O.mass],uy[O.mass])>reach*reach)continue;
|
| 686 |
+
const H=mfMassHold(O.mass,true),P=mfAirliftSerialize(O.unit,O.slots),x=ux[O.unit],y=uy[O.unit];
|
| 687 |
+
P.biomass=Math.round(uhp[O.unit]+TYPES[P.type].size*4);H.cargo.push(P);H.used+=O.slots;
|
| 688 |
+
for(let q=0;q<3;q++)addBeam(x+rr(-3,3),y+rr(-3,3),ux[O.mass],uy[O.mass],2.8,145,235,76,.32,'tractor');
|
| 689 |
+
addParticle(4,x,y,0,0,.55,TYPES[P.type].size*1.7,153,238,78);killUnit(O.unit,true);mfMassBoardOrders.splice(k,1);
|
| 690 |
+
sfx('cre_attack',ux[O.mass],uy[O.mass],.68);
|
| 691 |
+
}
|
| 692 |
+
for(let i=0;i<mfMassHolds.length;i++){
|
| 693 |
+
const H=mfMassHolds[i];if(!H)continue;
|
| 694 |
+
if(!mfMassIsLive(i,H.gen)){mfMassHolds[i]=null;continue;}
|
| 695 |
+
H.aiT-=dt;
|
| 696 |
+
if(utype[i]===MF_UT_MASSFLESH){
|
| 697 |
+
if(uteam[i]===1&&typeof AI!=='undefined'&&AI.fac==='horde'&&H.aiT<=0){
|
| 698 |
+
H.aiT=2.2;
|
| 699 |
+
if(H.used<6){const near=[];forUnitsIn(ux[i],uy[i],260,j=>{if(j!==i&&mfMassSlotCost(j,uteam[i]))near.push(j);});mfMassQueueBoard(i,near.slice(0,6),false);}
|
| 700 |
+
if(H.used>=4&&!mfMassReserved(i,H.gen)){
|
| 701 |
+
mfMassBeginFlight(i);const D=mfMassBehindPlayerTarget(i);mfMassCommandBirth(i,D.x,D.y);
|
| 702 |
+
}
|
| 703 |
+
}
|
| 704 |
+
continue;
|
| 705 |
+
}
|
| 706 |
+
H.flight=Math.max(0,H.flight-dt);H.attack=Math.max(0,H.attack-dt);
|
| 707 |
+
if(H.attack<=0)mfMassTentacleAttack(i,H);
|
| 708 |
+
if(uteam[i]!==0)mfMassAlert(i,'AIRBORNE · '+Math.ceil(H.flight)+'s');
|
| 709 |
+
/* Flight is a temporary breakthrough state, not a permanent aircraft mode.
|
| 710 |
+
Birth at the current position when the membrane timer ruptures even if a
|
| 711 |
+
distant order is still pending. This keeps anti-air counterplay finite. */
|
| 712 |
+
if(H.flight<=0){
|
| 713 |
+
H.mission={x:ux[i],y:uy[i],slots:mfAirliftFormation(H.cargo,ux[i],uy[i])};mfMassBirthNow(i,H);
|
| 714 |
+
}else if(H.mission){
|
| 715 |
+
H.fx-=dt;if(H.fx<=0){H.fx=.17;for(const S of H.mission.slots)addParticle(3,S.x,S.y,0,0,.24,20,168,92,255);}
|
| 716 |
+
if(dist2(ux[i],uy[i],H.mission.x,H.mission.y)<24*24)mfMassBirthNow(i,H);
|
| 717 |
+
}
|
| 718 |
+
}
|
| 719 |
+
mfMassRefreshAlert();mfMassUpdateUI();
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
const mfMassSpawnBase=spawnUnit;
|
| 723 |
+
spawnUnit=function(type,team,x,y){
|
| 724 |
+
const i=mfMassSpawnBase(type,team,x,y);
|
| 725 |
+
if(i>=0){mfMassHolds[i]=null;if(type===MF_UT_MASSFLESH||type===MF_UT_MASSFLESH_AIR)mfMassHold(i,true);}
|
| 726 |
+
return i;
|
| 727 |
+
};
|
| 728 |
+
const mfMassKillBase=killUnit;
|
| 729 |
+
killUnit=function(i,silent){
|
| 730 |
+
if(ualive[i]&&mfTransportKindByType(utype[i])==='massflesh'){
|
| 731 |
+
const H=mfMassHold(i,false),lost=H?H.cargo.length:0;
|
| 732 |
+
for(const O of mfMassBoardOrders)if(O.mass===i&&O.massGen===ugen[i]&&ualive[O.unit]&&ugen[O.unit]===O.unitGen){ustate[O.unit]=0;utx[O.unit]=ux[O.unit];uty[O.unit]=uy[O.unit];}
|
| 733 |
+
mfMassBoardOrders=mfMassBoardOrders.filter(O=>!(O.mass===i&&O.massGen===ugen[i]));
|
| 734 |
+
if(H){H.cargo.length=0;H.used=0;H.mission=null;mfMassHolds[i]=null;}
|
| 735 |
+
if(lost&&!silent){toast('MASSFLESH SLAIN · '+lost+' STORED ORGANISMS LOST');addParticle(4,ux[i],uy[i],0,0,1.0,80,150,238,72);}
|
| 736 |
+
if(mfMassAlertUnit===i)mfMassAlertUnit=-1;
|
| 737 |
+
}
|
| 738 |
+
return mfMassKillBase(i,silent);
|
| 739 |
+
};
|
| 740 |
+
const mfMassUnitTickBase=unitTick;
|
| 741 |
+
unitTick=function(dt){mfMassPreTick(dt);mfMassUnitTickBase(dt);mfMassPostTick(dt);};
|
| 742 |
+
|
| 743 |
+
const mfMassOnTapBase=onTap;
|
| 744 |
+
onTap=function(sx,sy){
|
| 745 |
+
const W=s2w(sx,sy),wx=W[0],wy=W[1];
|
| 746 |
+
if(mfMassBirthAim){const A=mfMassBirthAim;mfMassBirthAim=null;if(mfMassIsLive(A.i,A.gen))mfMassCommandBirth(A.i,wx,wy);return;}
|
| 747 |
+
const pk=pickUnit(wx,wy);
|
| 748 |
+
if(pk.own>=0&&utype[pk.own]===MF_UT_MASSFLESH&&selCount()>0&&!usel[pk.own]){
|
| 749 |
+
if(mfMassIssueBoard(pk.own))return;
|
| 750 |
+
for(let i=0;i<unitHigh;i++)if(ualive[i]&&usel[i]&&i!==pk.own)return;
|
| 751 |
+
}
|
| 752 |
+
return mfMassOnTapBase(sx,sy);
|
| 753 |
+
};
|
| 754 |
+
|
| 755 |
+
const mfMassPurposeBase=intelUnitPurpose;
|
| 756 |
+
intelUnitPurpose=function(T){
|
| 757 |
+
if(T&&T.massflesh)return T.massfleshAir
|
| 758 |
+
?'Timed winged breakthrough state. Tentacles rend structures and light units; dedicated anti-air brings it down.'
|
| 759 |
+
:'Living heavy carrier. Merge a light Brood into biomass, then ascend and birth it behind the defensive line.';
|
| 760 |
+
return mfMassPurposeBase(T);
|
| 761 |
+
};
|
| 762 |
+
const mfMassShowTypeBase=showUnitTypeCard;
|
| 763 |
+
showUnitTypeCard=function(tIdx,pinned){
|
| 764 |
+
mfMassShowTypeBase(tIdx,pinned);const T=TYPES[tIdx];if(!T||!T.massflesh)return;
|
| 765 |
+
const row=$('unitCard')&&$('unitCard').querySelector('.ucChips');
|
| 766 |
+
if(row)row.insertAdjacentHTML('beforeend',T.massfleshAir
|
| 767 |
+
?intelChip('✈','AIRBORNE')+intelChip('⌖','COUNTER: ANTI-AIR','bad')
|
| 768 |
+
:intelChip('⬣','GROUND HEAVY')+intelChip('⌖','COUNTER: ANTI-TANK','bad')+'<span class="ucChip mfMassCapacity"><i>◉</i>'+MF_MASS_CAPACITY+' BIOMASS</span>');
|
| 769 |
+
const card=$('unitCard'),warn=card&&card.querySelector('.ucCounter.caution'),ammo=card&&card.querySelector('.ucAmmo');
|
| 770 |
+
if(T.massfleshAir){if(warn)warn.remove();if(ammo)ammo.textContent='TENTACLES · +75% STRUCTURES · +45% LIGHT · WEAK VS HEAVY';}
|
| 771 |
+
};
|
| 772 |
+
function mfMassSvgIcon(size){
|
| 773 |
+
const w=document.createElement('div');w.className='mfMassIcon';w.style.width=size+'px';w.style.height=size+'px';
|
| 774 |
+
w.innerHTML='<svg viewBox="0 0 96 96" aria-hidden="true"><path fill="#27143a" stroke="#bd76ff" stroke-width="3" d="M13 52c4-22 18-37 35-37s31 15 35 37c-8 17-19 27-35 28-16-1-27-11-35-28z"/><path fill="#6c933e" d="M22 51c9-9 13-22 26-25 13 3 17 16 26 25-9 9-13 18-26 19-13-1-17-10-26-19z"/><circle cx="48" cy="45" r="11" fill="#c06cff"/><path stroke="#9eea58" stroke-width="5" stroke-linecap="round" d="M26 65L13 82m28-12-5 17m34-22 13 17M55 70l5 17"/></svg>';
|
| 775 |
+
return w;
|
| 776 |
+
}
|
| 777 |
+
const mfMassIconBase=unitIconEl;
|
| 778 |
+
unitIconEl=function(tIdx,size){return (tIdx===MF_UT_MASSFLESH||tIdx===MF_UT_MASSFLESH_AIR)?mfMassSvgIcon(size):mfMassIconBase(tIdx,size);};
|
| 779 |
+
function mfMassRenderCard(){
|
| 780 |
+
const g=$('prodGrid'),T=TYPES[MF_UT_MASSFLESH];g.innerHTML='';renderMenuRoleBrief('unit','biomass',[MF_UT_MASSFLESH]);
|
| 781 |
+
const d=document.createElement('div');d.className='bcard mfMassCard';
|
| 782 |
+
d.innerHTML='<div class="nm">'+T.name+'</div><div class="cost">'+T.cm+'m <span>'+T.ce+'e</span></div>'
|
| 783 |
+
+'<div class="mfMassBadges"><b>GROUND HEAVY</b><b>ANTI-TANK COUNTERS</b><b>'+T.transportCap+' BIOMASS</b></div>'
|
| 784 |
+
+'<div class="cardPurpose">MERGE · ASCEND · BIRTH BEHIND DEFENSES</div>';
|
| 785 |
+
d.setAttribute('role','button');d.setAttribute('aria-label','Grow Massflesh Brood breakthrough carrier');
|
| 786 |
+
const icw=document.createElement('div');icw.className='icw';icw.appendChild(unitIconEl(MF_UT_MASSFLESH,48));d.insertBefore(icw,d.firstChild);
|
| 787 |
+
d.addEventListener('pointerdown',ev=>{ev.stopPropagation();const B=openBld>=0?blds[openBld]:null;if(B&&bldFactionKey(B)==='horde'&&B.queue.length<30){B.queue.push(MF_UT_MASSFLESH);sfx('ui');renderQueue();}});
|
| 788 |
+
addCardIntelButton(d,'unit',MF_UT_MASSFLESH);g.appendChild(d);
|
| 789 |
+
}
|
| 790 |
+
const mfMassRenderProdBase=renderProdMenu;
|
| 791 |
+
renderProdMenu=function(){
|
| 792 |
+
const B=openBld>=0?blds[openBld]:null,brood=!!(B&&B.type==='fac'&&bldFactionKey(B)==='horde'),wants=brood&&prodTab==='biomass';
|
| 793 |
+
mfMassRenderProdBase();if(!brood)return;
|
| 794 |
+
const tr=$('prodTabs');tr.style.display='flex';const b=document.createElement('button');b.className='tabBtn'+(wants?' on':'');
|
| 795 |
+
b.innerHTML='<span class="tEm">♒</span>MASSFLESH';b.setAttribute('aria-label','Brood living transport production');
|
| 796 |
+
if(wants)for(const x of tr.querySelectorAll('.tabBtn'))x.classList.remove('on');
|
| 797 |
+
b.addEventListener('pointerdown',ev=>{ev.stopPropagation();prodTab='biomass';sfx('ui');renderProdMenu();});
|
| 798 |
+
/* A faction-defining organism must not be hidden beyond the phone-width tab
|
| 799 |
+
scroller. Keep it first for Brood factories while leaving Terran clean. */
|
| 800 |
+
tr.insertBefore(b,tr.firstChild);
|
| 801 |
+
if(wants){prodTab='biomass';mfMassRenderCard();}
|
| 802 |
+
};
|
| 803 |
+
function mfMassSelected(){let found=-1;for(let i=0;i<unitHigh;i++)if(ualive[i]&&usel[i]&&mfTransportKindByType(utype[i])==='massflesh'){if(found>=0)return -1;found=i;}return found;}
|
| 804 |
+
function mfMassUpdateUI(){
|
| 805 |
+
const b=$('mfMassActionBtn');if(!b)return;const i=mfMassSelected(),H=i>=0?mfMassHold(i,true):null,air=i>=0&&utype[i]===MF_UT_MASSFLESH_AIR;
|
| 806 |
+
b.style.display=i>=0?'flex':'none';b.disabled=!H||!H.cargo.length;
|
| 807 |
+
const label=b.querySelector('.lbl');if(label)label.textContent=air?'BIRTH '+Math.ceil(H.flight)+'s':'TAKE FLIGHT';
|
| 808 |
+
b.classList.toggle('on',!!(mfMassBirthAim&&i===mfMassBirthAim.i));
|
| 809 |
+
b.setAttribute('aria-label',air?'Set Massflesh birth site; airborne counter is anti-air':'Take flight; landed Massflesh counter is anti-tank');
|
| 810 |
+
}
|
| 811 |
+
const mfMassUpdateSelBase=updateSelInfo;
|
| 812 |
+
updateSelInfo=function(){
|
| 813 |
+
mfMassUpdateSelBase();const i=mfMassSelected(),H=i>=0?mfMassHold(i,true):null;
|
| 814 |
+
if(i<0)mfMassBirthAim=null;
|
| 815 |
+
if(H){const line=$('selInfo')&&$('selInfo').querySelector('.selIntelCopy span'),air=utype[i]===MF_UT_MASSFLESH_AIR;
|
| 816 |
+
if(line)line.textContent+=' · '+(air?'AIRBORNE / ANTI-AIR':'GROUND HEAVY / ANTI-TANK')+' · BIOMASS '+H.used+'/'+H.capacity;
|
| 817 |
+
const chip=$('unitCard')&&$('unitCard').querySelector('.mfMassCapacity');if(chip)chip.innerHTML='<i>◉</i>'+H.used+' / '+H.capacity+' BIOMASS';
|
| 818 |
+
}
|
| 819 |
+
mfMassUpdateUI();
|
| 820 |
+
};
|
| 821 |
+
function mfMassRefreshAlert(){
|
| 822 |
+
const el=$('mfMassAlert');if(!el)return;let best=-1,state='APPROACHING';
|
| 823 |
+
for(let i=0;i<unitHigh;i++)if(ualive[i]&&uteam[i]!==0&&mfTransportKindByType(utype[i])==='massflesh'){
|
| 824 |
+
if(utype[i]===MF_UT_MASSFLESH_AIR){best=i;state='AIRBORNE';break;}
|
| 825 |
+
for(const B of blds)if(B.alive&&B.team===0&&dist2(ux[i],uy[i],B.x,B.y)<620*620){best=i;break;}
|
| 826 |
+
}
|
| 827 |
+
if(best>=0)mfMassAlert(best,state);else{el.style.display='none';mfMassAlertUnit=-1;}
|
| 828 |
+
}
|
| 829 |
+
function mfMassInitUI(){
|
| 830 |
+
const row=$('tacRow');if(!row||$('mfMassActionBtn'))return;
|
| 831 |
+
const b=document.createElement('button');b.type='button';b.id='mfMassActionBtn';b.className='cbtn mfMassAction';b.style.display='none';
|
| 832 |
+
b.innerHTML='<span class="em">♒</span><span class="lbl">TAKE FLIGHT</span>';
|
| 833 |
+
b.addEventListener('pointerdown',ev=>{ev.preventDefault();ev.stopPropagation();const i=mfMassSelected();if(i<0)return;if(utype[i]===MF_UT_MASSFLESH)mfMassBeginFlight(i);else{mfMassBirthAim={i,gen:ugen[i]};toast('♒ TAP BEHIND THE DEFENSIVE LINE TO BIRTH THE BROOD');mfMassUpdateUI();}});
|
| 834 |
+
row.insertBefore(b,$('clearBtn'));
|
| 835 |
+
const a=document.createElement('button');a.type='button';a.id='mfMassAlert';a.style.display='none';a.setAttribute('aria-label','Track inbound Massflesh breakthrough carrier');
|
| 836 |
+
a.addEventListener('pointerdown',()=>{const i=mfMassAlertUnit;if(i>=0&&ualive[i]){cam.x=ux[i];cam.y=uy[i];camFollow=i;clampCam();camUpdateMatrices();sfx('ui');}});document.body.appendChild(a);
|
| 837 |
+
const st=document.createElement('style');st.textContent='\n'
|
| 838 |
+
+'.mfMassIcon{filter:drop-shadow(0 0 8px rgba(184,102,255,.5))}.mfMassIcon svg{width:100%;height:100%;display:block}'
|
| 839 |
+
+'.mfMassCard{width:156px!important;min-height:176px}.mfMassBadges{display:flex;flex-wrap:wrap;justify-content:center;gap:3px;margin:3px 0}.mfMassBadges b{padding:3px 5px;border:1px solid rgba(185,112,255,.48);border-radius:4px;background:rgba(72,28,92,.42);color:#d9aaff;font:700 7px/1 var(--fT)}'
|
| 840 |
+
+'.mfMassAction{min-width:58px!important;min-height:44px!important}.mfMassAction:disabled{opacity:.42}'
|
| 841 |
+
+'#mfMassAlert{position:fixed;left:50%;transform:translateX(-50%);top:calc(var(--sat) + 158px);z-index:60;min-width:260px;min-height:50px;padding:8px 14px;border:1px solid #c274ff;border-left:5px solid #aaf05e;border-radius:10px;background:linear-gradient(180deg,rgba(55,18,74,.96),rgba(21,10,35,.97));color:#f0d8ff;box-shadow:0 0 22px rgba(182,94,255,.34);font-family:var(--fT)}#mfMassAlert b{display:block;font-size:12px;letter-spacing:.08em}#mfMassAlert span{display:block;margin-top:3px;font-size:9px;color:#c9f88f;letter-spacing:.06em}body.menuMode #mfMassAlert{display:none!important}';
|
| 842 |
+
document.head.appendChild(st);
|
| 843 |
+
}
|
| 844 |
+
mfMassInitUI();
|
| 845 |
+
|
| 846 |
+
/* Seed one genuine breakthrough organism into a developed Brood AI economy.
|
| 847 |
+
The normal AI continues to own every other production choice. */
|
| 848 |
+
const mfMassAiTickBase=aiTick;
|
| 849 |
+
aiTick=function(dt){
|
| 850 |
+
if(stats.t<mfMassAiLastT){mfMassAiNext=135;}mfMassAiLastT=stats.t;
|
| 851 |
+
if(typeof AI!=='undefined'&&AI.fac==='horde'&&stats.t>=mfMassAiNext){
|
| 852 |
+
const live=(()=>{let n=0;for(let i=0;i<unitHigh;i++)if(ualive[i]&&uteam[i]===1&&mfTransportKindByType(utype[i])==='massflesh')n++;return n;})();
|
| 853 |
+
const B=blds.find(B=>B.alive&&B.team===1&&B.type==='fac'&&B.tier>=2&&B.prog>=1&&B.queue.length<3);
|
| 854 |
+
if(!live&&B){B.queue.unshift(MF_UT_MASSFLESH);mfMassAiNext=stats.t+190;}else mfMassAiNext=stats.t+12;
|
| 855 |
+
}
|
| 856 |
+
return mfMassAiTickBase(dt);
|
| 857 |
+
};
|
src/audio.js
CHANGED
|
@@ -657,7 +657,7 @@ function radioFaction(){
|
|
| 657 |
else if(typeof carrier!=='undefined'&&carrier.fac) key=carrier.fac;
|
| 658 |
}catch(e){}
|
| 659 |
const A=typeof facArt==='function'?facArt(key):null;
|
| 660 |
-
return A||{id:'nova',nm:'
|
| 661 |
}
|
| 662 |
function radioPanel(){
|
| 663 |
let el=document.getElementById('radioAck');
|
|
|
|
| 657 |
else if(typeof carrier!=='undefined'&&carrier.fac) key=carrier.fac;
|
| 658 |
}catch(e){}
|
| 659 |
const A=typeof facArt==='function'?facArt(key):null;
|
| 660 |
+
return A||{id:'nova',nm:'Terran Frontline Command',cdr:'Command',col:'#5db6ff'};
|
| 661 |
}
|
| 662 |
function radioPanel(){
|
| 663 |
let el=document.getElementById('radioAck');
|
src/develop.js
CHANGED
|
@@ -50,12 +50,26 @@ function matCostStr(c){
|
|
| 50 |
}
|
| 51 |
/* What a match pays out. Scaled by the threat the player chose to fight at, so
|
| 52 |
the endgame ladder feeds the crafting economy instead of running beside it. */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
function matsFromMatch(res){
|
|
|
|
| 54 |
const t=(typeof threatSel==='function')?threatSel():1;
|
| 55 |
const s=res.win?1:0.35;
|
|
|
|
| 56 |
return {
|
| 57 |
-
alloy : Math.round((
|
| 58 |
-
circuit: Math.round((
|
| 59 |
isotope: Math.round((res.win?2:0)+res.kills*0.0009*s*(1+t*0.10)),
|
| 60 |
relic : (res.nests>0||res.win&&t>=4)? Math.round(s*(1+t*0.08)) : 0,
|
| 61 |
};
|
|
@@ -79,8 +93,17 @@ const DEVTREE=[
|
|
| 79 |
cost:{alloy:25}, data:6, req:[]},
|
| 80 |
{id:'logistics', br:'DOCTRINE', nm:'Forward Logistics',ds:'Modules wear 25% slower',
|
| 81 |
cost:{alloy:50,circuit:18}, data:12, req:['salvage']},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
{id:'refit', br:'DOCTRINE', nm:'Field Refit', ds:'Unlocks repairing worn modules',
|
| 83 |
cost:{alloy:70,circuit:26}, data:18, req:['logistics']},
|
|
|
|
|
|
|
|
|
|
| 84 |
{id:'firemission',br:'DOCTRINE', nm:'Fire Mission Protocol',
|
| 85 |
ds:'Unlocks the player-commanded Charged Barrage for selected artillery',
|
| 86 |
cost:{alloy:90,circuit:44,isotope:8}, data:26, req:['logistics'], art:'res_refit'},
|
|
@@ -118,10 +141,11 @@ function moduleSlots(){ return 1+(devHas('slot2')?1:0)+(devHas('slot3')?1:0); }
|
|
| 118 |
function wearRate(){ return devHas('logistics')?0.75:1; }
|
| 119 |
function matYield(){ return devHas('salvage')?1.35:1; }
|
| 120 |
|
| 121 |
-
/*
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
| 125 |
function researchDataFromMatch(win){
|
| 126 |
const studies=Math.max(0,typeof resDone!=='undefined'?resDone|0:0);
|
| 127 |
let labs=0;
|
|
@@ -130,12 +154,15 @@ function researchDataFromMatch(win){
|
|
| 130 |
const studyData=studies*3;
|
| 131 |
const network=Math.min(3,labs);
|
| 132 |
const hold=win&&labs?2:0;
|
| 133 |
-
const
|
| 134 |
-
const
|
|
|
|
|
|
|
| 135 |
const threat=(typeof threatSel==='function')?1+Math.max(0,threatSel()-1)*0.06:1;
|
| 136 |
const total=Math.min(24,Math.round(raw*threat));
|
| 137 |
META.researchData=(META.researchData||0)+total;
|
| 138 |
-
return {total,parts:[['
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
/* ---- MODULES ---------------------------------------------------------------
|
|
@@ -343,10 +370,10 @@ function renderDevelop(){
|
|
| 343 |
}
|
| 344 |
/* Called at match end, after rewards. */
|
| 345 |
function developRecord(res){
|
| 346 |
-
const g=matsFromMatch(res), y=matYield();
|
| 347 |
for(const k in g) g[k]=Math.round(g[k]*y);
|
| 348 |
matGrant(g);
|
| 349 |
const broke=wearModules();
|
| 350 |
-
return {mats:g,
|
| 351 |
}
|
| 352 |
function initDevelop(){ matBag(); devDone(); modOwned(); modEquipped(); renderDevelop(); }
|
|
|
|
| 50 |
}
|
| 51 |
/* What a match pays out. Scaled by the threat the player chose to fight at, so
|
| 52 |
the endgame ladder feeds the crafting economy instead of running beside it. */
|
| 53 |
+
function fieldRecoveryFromMatch(res){
|
| 54 |
+
const committed=typeof matchCommitted==='function'?matchCommitted(res.win):(res.win||(stats.t|0)>=60);
|
| 55 |
+
if(!committed) return {alloy:0,circuit:0};
|
| 56 |
+
const lossScale=res.win?1:0.45;
|
| 57 |
+
const reserveAlloy=Math.min(res.win?12:5,Math.floor(Math.max(0,res.fieldMass||0)/300)*lossScale)|0;
|
| 58 |
+
const reclaimedAlloy=Math.min(res.win?8:4,Math.floor(Math.max(0,res.reclaimed||0)/180)*lossScale)|0;
|
| 59 |
+
return {
|
| 60 |
+
alloy:reserveAlloy+reclaimedAlloy,
|
| 61 |
+
circuit:Math.min(res.win?8:3,Math.floor(Math.max(0,res.fieldEnergy||0)/1400)*lossScale)|0,
|
| 62 |
+
reserveAlloy,reclaimedAlloy,
|
| 63 |
+
};
|
| 64 |
+
}
|
| 65 |
function matsFromMatch(res){
|
| 66 |
+
if(typeof matchCommitted==='function'&&!matchCommitted(res.win)) return {alloy:0,circuit:0,isotope:0,relic:0};
|
| 67 |
const t=(typeof threatSel==='function')?threatSel():1;
|
| 68 |
const s=res.win?1:0.35;
|
| 69 |
+
const recovery=fieldRecoveryFromMatch(res);
|
| 70 |
return {
|
| 71 |
+
alloy : Math.round((8+res.kills*0.004+res.built*0.5)*s*(1+t*0.14))+recovery.alloy,
|
| 72 |
+
circuit: Math.round((3+res.kills*0.0016)*s*(1+t*0.12))+recovery.circuit,
|
| 73 |
isotope: Math.round((res.win?2:0)+res.kills*0.0009*s*(1+t*0.10)),
|
| 74 |
relic : (res.nests>0||res.win&&t>=4)? Math.round(s*(1+t*0.08)) : 0,
|
| 75 |
};
|
|
|
|
| 93 |
cost:{alloy:25}, data:6, req:[]},
|
| 94 |
{id:'logistics', br:'DOCTRINE', nm:'Forward Logistics',ds:'Modules wear 25% slower',
|
| 95 |
cost:{alloy:50,circuit:18}, data:12, req:['salvage']},
|
| 96 |
+
{id:'breakthrough',br:'DOCTRINE', nm:'Breakthrough Protocol',
|
| 97 |
+
ds:'Unlocks a timed assault push for infantry, armour and anti-tank platoons',
|
| 98 |
+
cost:{alloy:72,circuit:30,isotope:5}, data:20, req:['logistics'], art:'res_ability'},
|
| 99 |
+
{id:'intercept', br:'DOCTRINE', nm:'Interceptor Protocol',
|
| 100 |
+
ds:'Unlocks high-speed interception for aircraft and anti-air screens',
|
| 101 |
+
cost:{alloy:58,circuit:42,isotope:7}, data:22, req:['logistics'], art:'res_optics'},
|
| 102 |
{id:'refit', br:'DOCTRINE', nm:'Field Refit', ds:'Unlocks repairing worn modules',
|
| 103 |
cost:{alloy:70,circuit:26}, data:18, req:['logistics']},
|
| 104 |
+
{id:'fieldservice',br:'DOCTRINE', nm:'Field Service Net',
|
| 105 |
+
ds:'Unlocks a selected support platoon repair-and-shield pulse',
|
| 106 |
+
cost:{alloy:82,circuit:48,isotope:6}, data:24, req:['refit'], art:'res_servos'},
|
| 107 |
{id:'firemission',br:'DOCTRINE', nm:'Fire Mission Protocol',
|
| 108 |
ds:'Unlocks the player-commanded Charged Barrage for selected artillery',
|
| 109 |
cost:{alloy:90,circuit:44,isotope:8}, data:26, req:['logistics'], art:'res_refit'},
|
|
|
|
| 141 |
function wearRate(){ return devHas('logistics')?0.75:1; }
|
| 142 |
function matYield(){ return devHas('salvage')?1.35:1; }
|
| 143 |
|
| 144 |
+
/* Every committed operation advances account research, including a defeat.
|
| 145 |
+
The Research Complex remains the fastest source through studies and a live
|
| 146 |
+
network bonus, but making it the ONLY source created a progression deadlock:
|
| 147 |
+
a newcomer who lost the lab earned zero Data and could never improve the lab.
|
| 148 |
+
Short surrender loops still pay nothing. */
|
| 149 |
function researchDataFromMatch(win){
|
| 150 |
const studies=Math.max(0,typeof resDone!=='undefined'?resDone|0:0);
|
| 151 |
let labs=0;
|
|
|
|
| 154 |
const studyData=studies*3;
|
| 155 |
const network=Math.min(3,labs);
|
| 156 |
const hold=win&&labs?2:0;
|
| 157 |
+
const committed=typeof matchCommitted==='function'?matchCommitted(win):(win||(stats.t|0)>=60);
|
| 158 |
+
const mission=committed?(win?4:2):0;
|
| 159 |
+
const challenge=win?clamp(difficulty|0,0,2):0;
|
| 160 |
+
const raw=mission+studyData+network+hold+challenge;
|
| 161 |
const threat=(typeof threatSel==='function')?1+Math.max(0,threatSel()-1)*0.06:1;
|
| 162 |
const total=Math.min(24,Math.round(raw*threat));
|
| 163 |
META.researchData=(META.researchData||0)+total;
|
| 164 |
+
return {total,parts:[['Mission recovery',mission],['Studies',studyData],['Lab network',network],
|
| 165 |
+
['Objective hold',hold],['Challenge',challenge]].filter(p=>p[1]>0)};
|
| 166 |
}
|
| 167 |
|
| 168 |
/* ---- MODULES ---------------------------------------------------------------
|
|
|
|
| 370 |
}
|
| 371 |
/* Called at match end, after rewards. */
|
| 372 |
function developRecord(res){
|
| 373 |
+
const recovery=fieldRecoveryFromMatch(res),g=matsFromMatch(res), y=matYield();
|
| 374 |
for(const k in g) g[k]=Math.round(g[k]*y);
|
| 375 |
matGrant(g);
|
| 376 |
const broke=wearModules();
|
| 377 |
+
return {mats:g,recovery,broke};
|
| 378 |
}
|
| 379 |
function initDevelop(){ matBag(); devDone(); modOwned(); modEquipped(); renderDevelop(); }
|
src/endgame.js
CHANGED
|
@@ -42,22 +42,48 @@ const THREAT_NM=['','SKIRMISH','PROBE','RAID','OFFENSIVE','CAMPAIGN','WAR',
|
|
| 42 |
something cosmetic or something crippling. Choosing which knives to hold is
|
| 43 |
the interesting decision; the dice were taking it away. */
|
| 44 |
const OPMODS=[
|
| 45 |
-
{id:'iron', nm:'Iron Enemy', ds:'Enemy units +25% health',
|
| 46 |
-
{id:'
|
| 47 |
-
{id:'
|
| 48 |
-
{id:'
|
| 49 |
-
{id:'
|
| 50 |
-
{id:'
|
| 51 |
-
{id:'
|
| 52 |
-
{id:'
|
| 53 |
-
{id:'
|
| 54 |
-
{id:'dark', nm:'Blood Moon', ds:'Permanent night',
|
| 55 |
];
|
| 56 |
function opModsOn(){ META.opmods=META.opmods||{}; return META.opmods; }
|
| 57 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
function toggleOpMod(id){
|
|
|
|
|
|
|
| 59 |
const o=opModsOn();
|
| 60 |
if(o[id]) delete o[id]; else o[id]=1;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
metaSave(); renderOps(); sfx('ui');
|
| 62 |
}
|
| 63 |
function opModMult(){
|
|
@@ -65,7 +91,10 @@ function opModMult(){
|
|
| 65 |
return m;
|
| 66 |
}
|
| 67 |
/* The total the player is playing for, shown before they commit. */
|
| 68 |
-
function payoutMult(){
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
/* ---- SCORE -----------------------------------------------------------------
|
| 71 |
One number so runs are comparable — to your own past runs, and to the weekly.
|
|
@@ -218,9 +247,8 @@ function renderOpsBrief(mode){
|
|
| 218 |
}
|
| 219 |
function renderOps(){
|
| 220 |
const el=document.getElementById('opsScr'); if(!el) return;
|
| 221 |
-
/* Keep the combined plan visible
|
| 222 |
-
|
| 223 |
-
the Modifiers tab made every other tab look like an unrelated menu. */
|
| 224 |
renderOpsBrief((MF_TAB_STATE&&MF_TAB_STATE.opsScr)||'threat');
|
| 225 |
/* Threat ladder */
|
| 226 |
const tl=document.getElementById('threatRow');
|
|
@@ -245,13 +273,32 @@ function renderOps(){
|
|
| 245 |
/* Modifiers */
|
| 246 |
const mr=document.getElementById('opModRow');
|
| 247 |
if(mr){
|
| 248 |
-
mr.innerHTML=OPMODS.map(k=>
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
}
|
| 253 |
const pm=document.getElementById('opPayout');
|
| 254 |
-
if(pm) pm.innerHTML='PAYOUT <b>×'+payoutMult().toFixed(2)+'</b>';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
/* Weekly */
|
| 256 |
const wd=weeklyDef(), wb=weeklyBest();
|
| 257 |
const wk=document.getElementById('weeklyBox');
|
|
|
|
| 42 |
something cosmetic or something crippling. Choosing which knives to hold is
|
| 43 |
the interesting decision; the dice were taking it away. */
|
| 44 |
const OPMODS=[
|
| 45 |
+
{id:'iron', nm:'Iron Enemy', ds:'Enemy units +25% health', rw:0.30,gate:{kind:'rank',n:0}},
|
| 46 |
+
{id:'fogb', nm:'Fog Bank', ds:'Vision reduced by 40%', rw:0.25,gate:{kind:'rank',n:1}},
|
| 47 |
+
{id:'veins', nm:'Scarce Veins', ds:'Your mass income -30%', rw:0.40,gate:{kind:'wins',n:1}},
|
| 48 |
+
{id:'swarm', nm:'Hiveworld', ds:'Wildlife escalates twice as fast',rw:0.35,gate:{kind:'tutorial',n:1}},
|
| 49 |
+
{id:'blitz', nm:'Blitz Doctrine', ds:'Enemy waves arrive 40% sooner', rw:0.35,gate:{kind:'rank',n:2}},
|
| 50 |
+
{id:'volatile',nm:'Volatile Cores', ds:'Every death detonates', rw:0.15,gate:{kind:'matches',n:3}},
|
| 51 |
+
{id:'titan', nm:'Early Titans', ds:'The enemy fields TITANs early', rw:0.45,gate:{kind:'threat',n:4}},
|
| 52 |
+
{id:'brittle',nm:'Brittle Frames', ds:'Your units -20% health', rw:0.40,gate:{kind:'rank',n:3}},
|
| 53 |
+
{id:'nofab', nm:'No Salvage', ds:'Wrecks cannot be reclaimed', rw:0.20,gate:{kind:'mastery',n:3}},
|
| 54 |
+
{id:'dark', nm:'Blood Moon', ds:'Permanent night', rw:0.10,gate:{kind:'wins',n:5}},
|
| 55 |
];
|
| 56 |
function opModsOn(){ META.opmods=META.opmods||{}; return META.opmods; }
|
| 57 |
+
function opModUnlockState(mod){
|
| 58 |
+
const g=mod&&mod.gate||{kind:'rank',n:0}; let value=0,label='Available';
|
| 59 |
+
if(g.kind==='rank'){
|
| 60 |
+
value=typeof metaRankIdx==='function'?metaRankIdx():0;
|
| 61 |
+
const rank=(typeof RANKS!=='undefined'&&RANKS[g.n])||{nm:'Commander Rank '+(g.n+1)};
|
| 62 |
+
label=g.n===0?'Recruit access':'Reach '+rank.nm+' rank';
|
| 63 |
+
}else if(g.kind==='wins'){
|
| 64 |
+
value=META.wins||0; label='Win '+g.n+' operation'+(g.n===1?'':'s');
|
| 65 |
+
}else if(g.kind==='matches'){
|
| 66 |
+
value=META.matches||0; label='Complete '+g.n+' operations';
|
| 67 |
+
}else if(g.kind==='tutorial'){
|
| 68 |
+
value=META.tutorial&&META.tutorial.done?1:0; label='Complete First Command training';
|
| 69 |
+
}else if(g.kind==='threat'){
|
| 70 |
+
value=typeof threatUnlocked==='function'?threatUnlocked():1; label='Unlock Threat T'+g.n;
|
| 71 |
+
}else if(g.kind==='mastery'){
|
| 72 |
+
value=typeof masteryTotal==='function'?masteryTotal().done:0; label='Earn '+g.n+' battlefield masteries';
|
| 73 |
+
}
|
| 74 |
+
return {open:value>=g.n,label,progress:Math.min(value,g.n)+' / '+g.n};
|
| 75 |
+
}
|
| 76 |
+
function opModUnlocked(mod){ return opModUnlockState(typeof mod==='string'?OPMODS.find(x=>x.id===mod):mod).open; }
|
| 77 |
+
function opModActive(id){ return opModUnlocked(id)&&!!opModsOn()[id]; }
|
| 78 |
function toggleOpMod(id){
|
| 79 |
+
const mod=OPMODS.find(x=>x.id===id),unlock=opModUnlockState(mod);
|
| 80 |
+
if(!mod||!unlock.open){ toast('🔒 '+(mod?mod.nm:'Modifier')+' — '+unlock.label+' ('+unlock.progress+')'); sfx('ui'); return; }
|
| 81 |
const o=opModsOn();
|
| 82 |
if(o[id]) delete o[id]; else o[id]=1;
|
| 83 |
+
/* Hand-picked rules and the random quick-pick are two alternatives. Keeping
|
| 84 |
+
both selected made the random buttons appear broken because chosen rules
|
| 85 |
+
intentionally win at match start. */
|
| 86 |
+
wcChoice=0; META.wcPref=0;
|
| 87 |
metaSave(); renderOps(); sfx('ui');
|
| 88 |
}
|
| 89 |
function opModMult(){
|
|
|
|
| 91 |
return m;
|
| 92 |
}
|
| 93 |
/* The total the player is playing for, shown before they commit. */
|
| 94 |
+
function payoutMult(){
|
| 95 |
+
const chosen=OPMODS.some(k=>opModActive(k.id));
|
| 96 |
+
return threatReward()*(chosen?opModMult():(1+0.35*Math.max(0,wcChoice|0)));
|
| 97 |
+
}
|
| 98 |
|
| 99 |
/* ---- SCORE -----------------------------------------------------------------
|
| 100 |
One number so runs are comparable — to your own past runs, and to the weekly.
|
|
|
|
| 247 |
}
|
| 248 |
function renderOps(){
|
| 249 |
const el=document.getElementById('opsScr'); if(!el) return;
|
| 250 |
+
/* Keep the combined plan visible in Operations while modifier selection
|
| 251 |
+
itself lives with the rest of the per-match Map Settings. */
|
|
|
|
| 252 |
renderOpsBrief((MF_TAB_STATE&&MF_TAB_STATE.opsScr)||'threat');
|
| 253 |
/* Threat ladder */
|
| 254 |
const tl=document.getElementById('threatRow');
|
|
|
|
| 273 |
/* Modifiers */
|
| 274 |
const mr=document.getElementById('opModRow');
|
| 275 |
if(mr){
|
| 276 |
+
mr.innerHTML=OPMODS.map((k,mi)=>{ const u=opModUnlockState(k),active=opModActive(k.id),
|
| 277 |
+
/* The atlas is row-major 5x2. CSS percentage positions are relative to
|
| 278 |
+
the remaining overflow, so five columns land at 0/25/50/75/100. */
|
| 279 |
+
artX=(mi%5)*25,artY=mi<5?0:100; return '<div class="opMod'
|
| 280 |
+
+(active?' on':'')+(u.open?'':' lock')+'" data-id="'+k.id+'" role="button" tabindex="0" aria-disabled="'+(!u.open)+'">'
|
| 281 |
+
+'<div class="opModArt" aria-hidden="true" style="--opx:'+artX+'%;--opy:'+artY+'%"><i></i></div>'
|
| 282 |
+
+'<div class="opTx"><b>'+k.nm+'</b><span>'+k.ds+'</span><small class="opUnlock '+(u.open?'earned':'')+'">'
|
| 283 |
+
+(u.open?'✓ ':'🔒 ')+u.label+(u.open?'':' · '+u.progress)+'</small></div>'
|
| 284 |
+
+'<div class="opRw">+'+Math.round(k.rw*100)+'%</div></div>'; }).join('');
|
| 285 |
+
mr.querySelectorAll('.opMod').forEach(d=>{
|
| 286 |
+
d.addEventListener('pointerdown',()=>toggleOpMod(d.dataset.id));
|
| 287 |
+
d.addEventListener('keydown',ev=>{
|
| 288 |
+
if(ev.key!=='Enter'&&ev.key!==' ') return;
|
| 289 |
+
ev.preventDefault(); toggleOpMod(d.dataset.id);
|
| 290 |
+
});
|
| 291 |
+
});
|
| 292 |
}
|
| 293 |
const pm=document.getElementById('opPayout');
|
| 294 |
+
if(pm) pm.innerHTML=(wcChoice?'RANDOM ×'+wcChoice+' · ':'PAYOUT ')+'<b>×'+payoutMult().toFixed(2)+'</b>';
|
| 295 |
+
const available=OPMODS.filter(opModUnlocked).length;
|
| 296 |
+
document.querySelectorAll('.wbtn').forEach(b=>{
|
| 297 |
+
const n=+b.dataset.w,blocked=n>available;
|
| 298 |
+
b.disabled=blocked; b.classList.toggle('lock',blocked);
|
| 299 |
+
b.title=blocked?'Unlock '+n+' modifiers first ('+available+' available)':'';
|
| 300 |
+
b.classList.toggle('on',!blocked&&n===wcChoice);
|
| 301 |
+
});
|
| 302 |
/* Weekly */
|
| 303 |
const wd=weeklyDef(), wb=weeklyBest();
|
| 304 |
const wk=document.getElementById('weeklyBox');
|
src/engine/mesh.js
CHANGED
|
@@ -729,7 +729,9 @@ layout(location=3) in vec2 aUV;
|
|
| 729 |
layout(location=4) in float aMat;
|
| 730 |
uniform mat4 uVP;
|
| 731 |
uniform vec3 uEye;
|
| 732 |
-
|
|
|
|
|
|
|
| 733 |
void main(){
|
| 734 |
vNrm=aNrm; vCol=aCol;
|
| 735 |
vMapUV=vec2(aPos.x,aPos.z)/uMapSize;
|
|
@@ -737,18 +739,41 @@ void main(){
|
|
| 737 |
float d=length(aPos-uEye);
|
| 738 |
vFog=clamp((d-2600.0)/4200.0,0.0,0.5);
|
| 739 |
float bd=min(min(aPos.x, MAPSIZE_CONST-aPos.x), min(aPos.z, MAPSIZE_CONST-aPos.z));
|
| 740 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 741 |
gl_Position=uVP*vec4(aPos,1.0);
|
| 742 |
}`.replace('uMapSize','MAPSIZE_CONST');
|
| 743 |
const FST=`#version 300 es
|
| 744 |
precision highp float;
|
| 745 |
-
in vec3 vNrm; in vec2 vMapUV; in vec2 vDetUV; in float vFog; in vec3 vCol;
|
| 746 |
uniform sampler2D uMap;
|
| 747 |
uniform sampler2D uDetail;
|
| 748 |
uniform sampler2D uFogMap;
|
| 749 |
uniform float uFogActive;
|
| 750 |
uniform vec3 uSun; uniform vec3 uSunC;
|
| 751 |
uniform vec3 uAmbSky; uniform vec3 uAmbGnd; uniform vec3 uFogC;
|
|
|
|
| 752 |
out vec4 o;
|
| 753 |
void main(){
|
| 754 |
vec3 n=normalize(vNrm);
|
|
@@ -764,13 +789,59 @@ void main(){
|
|
| 764 |
vec3 amb=mix(uAmbGnd,uAmbSky,hemi);
|
| 765 |
vec3 lit=base*(amb + uSunC*(ndl*0.80+wrap*0.20));
|
| 766 |
lit=vec3(1.0)-exp(-lit*1.55); // same curve as the model shader
|
| 767 |
-
lit=mix(lit,uFogC,vFog); // fog is a display-space colour: blend after
|
| 768 |
/* Fog-of-war follows the terrain itself. A screen overlay could not survive
|
| 769 |
camera tilt and the first ground-quad attempt visibly cut hills into dark
|
| 770 |
tiles. Sampling the 64x64 sensor texture in map UV space gives smooth,
|
| 771 |
depth-correct current/explored/unexplored transitions for one texture read. */
|
| 772 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 773 |
lit=mix(lit,vec3(0.008,0.016,0.030),fow);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 774 |
o=vec4(pow(max(lit,vec3(0.0)),vec3(0.4545)),1.0);
|
| 775 |
}`;
|
| 776 |
/* ============================================================================
|
|
@@ -1105,7 +1176,8 @@ function initGL3D(){
|
|
| 1105 |
gl.uniform1i(U3.uMat,0); gl.uniform1i(U3.uNrm,2); gl.uniform1i(U3.uOrm,3);
|
| 1106 |
UG.uVP=gl.getUniformLocation(progG,'uVP');
|
| 1107 |
progT=mkProg(VST.replace(/MAPSIZE_CONST/g,MAP.toFixed(1)).replace(/BFOG_CONST/g,'430.0'),FST);
|
| 1108 |
-
for(const k of ['uVP','uEye','uSun','uSunC','uAmbSky','uAmbGnd','uFogC','uMap','uDetail','uFogMap','uFogActive'
|
|
|
|
| 1109 |
UT[k]=gl.getUniformLocation(progT,k);
|
| 1110 |
gl.useProgram(progT); gl.uniform1i(UT.uMap,0); gl.uniform1i(UT.uDetail,1); gl.uniform1i(UT.uFogMap,7);
|
| 1111 |
initAO();
|
|
|
|
| 729 |
layout(location=4) in float aMat;
|
| 730 |
uniform mat4 uVP;
|
| 731 |
uniform vec3 uEye;
|
| 732 |
+
uniform vec2 uPlayBounds;
|
| 733 |
+
uniform float uEdgeStyle;
|
| 734 |
+
out vec3 vNrm; out vec2 vMapUV; out vec2 vDetUV; out float vFog; out float vBorder; out float vPlayBorder; out float vOutside; out float vExclusion; out vec3 vCol;
|
| 735 |
void main(){
|
| 736 |
vNrm=aNrm; vCol=aCol;
|
| 737 |
vMapUV=vec2(aPos.x,aPos.z)/uMapSize;
|
|
|
|
| 739 |
float d=length(aPos-uEye);
|
| 740 |
vFog=clamp((d-2600.0)/4200.0,0.0,0.5);
|
| 741 |
float bd=min(min(aPos.x, MAPSIZE_CONST-aPos.x), min(aPos.z, MAPSIZE_CONST-aPos.z));
|
| 742 |
+
vBorder=clamp((BFOG_CONST-bd)/BFOG_CONST,0.0,1.0);
|
| 743 |
+
vOutside=max(0.0,-bd);
|
| 744 |
+
/* Match the authored theatre silhouette used by the red tactical grid.
|
| 745 |
+
Finite superellipse powers keep every biome from ending in the same hard
|
| 746 |
+
square, while the harmonics create broad coast/inlet/waste variation
|
| 747 |
+
without introducing high-frequency collision traps. */
|
| 748 |
+
vec2 pc=vec2((uPlayBounds.x+uPlayBounds.y)*0.5);
|
| 749 |
+
vec2 pd=aPos.xz-pc;
|
| 750 |
+
float pa=atan(pd.y,pd.x), ca=abs(cos(pa)), sa=abs(sin(pa));
|
| 751 |
+
float power=uEdgeStyle>0.5&&uEdgeStyle<1.5?3.15:(uEdgeStyle>1.5&&uEdgeStyle<2.5?6.25:(uEdgeStyle>2.5?4.15:5.0));
|
| 752 |
+
float halfSpan=(uPlayBounds.y-uPlayBounds.x)*0.5;
|
| 753 |
+
float baseR=halfSpan/pow(pow(ca,power)+pow(sa,power),1.0/power);
|
| 754 |
+
float shape=uEdgeStyle>0.5&&uEdgeStyle<1.5
|
| 755 |
+
?0.945+0.024*sin(pa*3.0+0.65)+0.016*sin(pa*7.0-1.10)
|
| 756 |
+
:(uEdgeStyle>1.5&&uEdgeStyle<2.5
|
| 757 |
+
?0.958+0.020*sin(pa*5.0+0.30)+0.012*sin(pa*9.0+1.20)
|
| 758 |
+
:(uEdgeStyle>2.5
|
| 759 |
+
?0.948+0.025*sin(pa*2.0-0.80)+0.016*sin(pa*6.0+0.45)
|
| 760 |
+
:0.966+0.014*sin(pa*4.0+0.35)+0.010*sin(pa*7.0-0.55)));
|
| 761 |
+
float pbd=baseR*shape-length(pd);
|
| 762 |
+
vPlayBorder=clamp((160.0-pbd)/160.0,0.0,1.0);
|
| 763 |
+
vExclusion=max(0.0,-pbd);
|
| 764 |
+
vFog=max(vFog,vBorder); // border haze
|
| 765 |
gl_Position=uVP*vec4(aPos,1.0);
|
| 766 |
}`.replace('uMapSize','MAPSIZE_CONST');
|
| 767 |
const FST=`#version 300 es
|
| 768 |
precision highp float;
|
| 769 |
+
in vec3 vNrm; in vec2 vMapUV; in vec2 vDetUV; in float vFog; in float vBorder; in float vPlayBorder; in float vOutside; in float vExclusion; in vec3 vCol;
|
| 770 |
uniform sampler2D uMap;
|
| 771 |
uniform sampler2D uDetail;
|
| 772 |
uniform sampler2D uFogMap;
|
| 773 |
uniform float uFogActive;
|
| 774 |
uniform vec3 uSun; uniform vec3 uSunC;
|
| 775 |
uniform vec3 uAmbSky; uniform vec3 uAmbGnd; uniform vec3 uFogC;
|
| 776 |
+
uniform float uEdgeStyle; uniform float uEdgeTime; uniform vec3 uEdgeTint;
|
| 777 |
out vec4 o;
|
| 778 |
void main(){
|
| 779 |
vec3 n=normalize(vNrm);
|
|
|
|
| 789 |
vec3 amb=mix(uAmbGnd,uAmbSky,hemi);
|
| 790 |
vec3 lit=base*(amb + uSunC*(ndl*0.80+wrap*0.20));
|
| 791 |
lit=vec3(1.0)-exp(-lit*1.55); // same curve as the model shader
|
|
|
|
| 792 |
/* Fog-of-war follows the terrain itself. A screen overlay could not survive
|
| 793 |
camera tilt and the first ground-quad attempt visibly cut hills into dark
|
| 794 |
tiles. Sampling the 64x64 sensor texture in map UV space gives smooth,
|
| 795 |
depth-correct current/explored/unexplored transitions for one texture read. */
|
| 796 |
+
/* Border weather is a physical cover over the sensor map, not another layer
|
| 797 |
+
beneath it. The old order fogged the terrain and THEN mixed it toward FOW
|
| 798 |
+
black, which cut a navy polygon out of the otherwise matching clear
|
| 799 |
+
colour. Fade sensor darkness out as haze starts, apply it to the terrain,
|
| 800 |
+
and put the atmospheric veil on last. */
|
| 801 |
+
/* Sensor darkness must not redraw the tactical frontier as a black slab.
|
| 802 |
+
Protect both the physical terrain skirt and the currently selected safe
|
| 803 |
+
theatre edge; Compact and Standard deliberately end before the mesh does. */
|
| 804 |
+
float borderProtect=1.0-smoothstep(0.02,0.18,max(vBorder,vPlayBorder));
|
| 805 |
+
float fow=texture(uFogMap,vMapUV).a*uFogActive*borderProtect;
|
| 806 |
lit=mix(lit,vec3(0.008,0.016,0.030),fow);
|
| 807 |
+
lit=mix(lit,uFogC,vFog); // final veil: FOW can never darken it
|
| 808 |
+
/* The low-density skirt outside the simulation receives a restrained fog
|
| 809 |
+
variation. It suggests rolling ground continuing into weather without
|
| 810 |
+
becoming navigable terrain or revealing a second hard edge. Both ends of
|
| 811 |
+
the band return to the exact fog colour, so the authored seam and the far
|
| 812 |
+
framebuffer clear remain continuous. */
|
| 813 |
+
float outerBand=smoothstep(10.0,180.0,vOutside)*(1.0-smoothstep(640.0,940.0,vOutside));
|
| 814 |
+
float outerNoise=(d1*0.68+d2*0.32)-0.5;
|
| 815 |
+
vec3 outerFog=uFogC*(0.965+outerNoise*0.065);
|
| 816 |
+
lit=mix(lit,outerFog,outerBand*0.72);
|
| 817 |
+
/* The red command boundary is drawn on the last safe ground. Only AFTER
|
| 818 |
+
crossing it does this procedural exclusion treatment begin. Detail noise
|
| 819 |
+
perturbs the onset so coastlines and wasteland fronts do not read as a
|
| 820 |
+
second perfect rectangle, while a long fade returns to atmospheric fog
|
| 821 |
+
before the low-density skirt itself ends. */
|
| 822 |
+
float irregular=((d1*0.68+d2*0.32)-0.5)*42.0;
|
| 823 |
+
float onset=max(8.0,14.0+irregular);
|
| 824 |
+
float zone=smoothstep(onset,max(onset+34.0,82.0+irregular),vExclusion)
|
| 825 |
+
*(1.0-smoothstep(760.0,1180.0,vExclusion));
|
| 826 |
+
vec3 zoneCol=mix(uFogC,uEdgeTint,0.42);
|
| 827 |
+
if(uEdgeStyle>0.5&&uEdgeStyle<1.5){
|
| 828 |
+
// Coastal/island exclusion: dark water with narrow moving foam trains.
|
| 829 |
+
float wave=0.5+0.5*sin(vExclusion*0.064+vDetUV.x*0.31-vDetUV.y*0.19+uEdgeTime*0.72);
|
| 830 |
+
float foam=pow(wave,12.0)*(1.0-smoothstep(250.0,560.0,vExclusion));
|
| 831 |
+
zoneCol=mix(uFogC,uEdgeTint,0.58)+vec3(0.10,0.16,0.18)*foam;
|
| 832 |
+
}else if(uEdgeStyle>1.5&&uEdgeStyle<2.5){
|
| 833 |
+
// Dry exclusion: ash plates split by ember-lit procedural cracks.
|
| 834 |
+
float crack=pow(abs(sin(vDetUV.x*0.73+sin(vDetUV.y*.17))*sin(vDetUV.y*.61-vDetUV.x*.11)),18.0);
|
| 835 |
+
zoneCol=mix(uFogC,uEdgeTint,0.56)+vec3(0.20,0.035,0.008)*crack*(0.35+0.20*sin(uEdgeTime*.45));
|
| 836 |
+
}else if(uEdgeStyle>2.5){
|
| 837 |
+
// Highland/arctic exclusion: broad storm bands moving through dense mist.
|
| 838 |
+
float storm=0.5+0.5*sin((vDetUV.x+vDetUV.y)*0.34+outerNoise*5.0+uEdgeTime*0.34);
|
| 839 |
+
zoneCol=mix(uFogC,uEdgeTint,0.34)*(0.88+storm*0.16);
|
| 840 |
+
}else{
|
| 841 |
+
// Temperate maps retain muted terrain colour under deep perimeter mist.
|
| 842 |
+
zoneCol=mix(uFogC,uEdgeTint,0.38)*(0.94+outerNoise*0.10);
|
| 843 |
+
}
|
| 844 |
+
lit=mix(lit,zoneCol,zone*0.78);
|
| 845 |
o=vec4(pow(max(lit,vec3(0.0)),vec3(0.4545)),1.0);
|
| 846 |
}`;
|
| 847 |
/* ============================================================================
|
|
|
|
| 1176 |
gl.uniform1i(U3.uMat,0); gl.uniform1i(U3.uNrm,2); gl.uniform1i(U3.uOrm,3);
|
| 1177 |
UG.uVP=gl.getUniformLocation(progG,'uVP');
|
| 1178 |
progT=mkProg(VST.replace(/MAPSIZE_CONST/g,MAP.toFixed(1)).replace(/BFOG_CONST/g,'430.0'),FST);
|
| 1179 |
+
for(const k of ['uVP','uEye','uSun','uSunC','uAmbSky','uAmbGnd','uFogC','uMap','uDetail','uFogMap','uFogActive',
|
| 1180 |
+
'uPlayBounds','uEdgeStyle','uEdgeTime','uEdgeTint'])
|
| 1181 |
UT[k]=gl.getUniformLocation(progT,k);
|
| 1182 |
gl.useProgram(progT); gl.uniform1i(UT.uMap,0); gl.uniform1i(UT.uDetail,1); gl.uniform1i(UT.uFogMap,7);
|
| 1183 |
initAO();
|
src/engine/terrain.js
CHANGED
|
@@ -24,6 +24,14 @@ const SEABED=-26; // floor the ocean bottom so water has dept
|
|
| 24 |
|
| 25 |
let terrMesh=null, terrVerts=null, terrVAO=null, terrVBO=null, terrIBO=null, terrIdxCount=0;
|
| 26 |
let waterMesh=null, waterVAO=null, waterVBO=null, waterIBO=null, waterIdxCount=0, waterVerts=null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
/* World height in world units. Bilinear so slopes are smooth and the camera
|
| 29 |
ray-march in s2w() converges instead of stair-stepping. */
|
|
@@ -118,10 +126,78 @@ function buildTerrainMesh(themeKey){
|
|
| 118 |
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,idx,gl.STATIC_DRAW);
|
| 119 |
gl.bindVertexArray(null);
|
| 120 |
}
|
|
|
|
| 121 |
buildWaterMesh(TH);
|
| 122 |
return terrVAO;
|
| 123 |
}
|
| 124 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
/* Rewrite a rectangular window of terrain vertices — positions, normals and
|
| 126 |
colours — from the current heightfield. Normals are central differences of
|
| 127 |
the heightfield rather than face normals, which keeps the surface smooth
|
|
@@ -279,6 +355,12 @@ function drawTerrain(){
|
|
| 279 |
gl.drawElements(gl.TRIANGLES,terrIdxCount,gl.UNSIGNED_INT,0);
|
| 280 |
drawCalls++; triCount+=terrIdxCount/3;
|
| 281 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
function drawWater(){
|
| 283 |
if(!waterVAO||!waterIdxCount) return;
|
| 284 |
gl.bindVertexArray(waterVAO);
|
|
|
|
| 24 |
|
| 25 |
let terrMesh=null, terrVerts=null, terrVAO=null, terrVBO=null, terrIBO=null, terrIdxCount=0;
|
| 26 |
let waterMesh=null, waterVAO=null, waterVBO=null, waterIBO=null, waterIdxCount=0, waterVerts=null;
|
| 27 |
+
/* The playable heightfield still ends at MAP, but the camera is deliberately
|
| 28 |
+
allowed to overhang it so a corner base can remain centred while the player
|
| 29 |
+
rotates. A cheap low-density skirt gives that overhang real depth instead
|
| 30 |
+
of exposing the framebuffer clear as a flat blue/black wedge. The terrain
|
| 31 |
+
shader turns the skirt into atmospheric fake-land; it never participates in
|
| 32 |
+
pathfinding, fog sensors, placement or deformation. */
|
| 33 |
+
const TERR_EDGE_EXT=960;
|
| 34 |
+
let terrEdgeVAO=null, terrEdgeVBO=null, terrEdgeIBO=null, terrEdgeIdxCount=0;
|
| 35 |
|
| 36 |
/* World height in world units. Bilinear so slopes are smooth and the camera
|
| 37 |
ray-march in s2w() converges instead of stair-stepping. */
|
|
|
|
| 126 |
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,idx,gl.STATIC_DRAW);
|
| 127 |
gl.bindVertexArray(null);
|
| 128 |
}
|
| 129 |
+
buildTerrainEdgeMesh(TH);
|
| 130 |
buildWaterMesh(TH);
|
| 131 |
return terrVAO;
|
| 132 |
}
|
| 133 |
|
| 134 |
+
/* Four rectilinear strips cover the world outside [0,MAP]. Keeping 0 and MAP
|
| 135 |
+
as authored vertices makes the join exact, while a handful of radial bands
|
| 136 |
+
is enough because dense haze intentionally removes high-frequency detail.
|
| 137 |
+
North/south extend around the corners; west/east only fill the middle, so
|
| 138 |
+
there are no holes and no overlapping coplanar corner sheets. */
|
| 139 |
+
function buildTerrainEdgeMesh(TH){
|
| 140 |
+
const verts=[],idx=[];
|
| 141 |
+
const edgeKind=curMap==='isles'?1:(curTheme==='ashland'||curMap==='crater'?2:
|
| 142 |
+
(curTheme==='arctic'||curMap==='highland'?3:0));
|
| 143 |
+
const sink=edgeKind===1?38:edgeKind===2?13:edgeKind===3?27:20;
|
| 144 |
+
const reliefAmp=edgeKind===2?7.2:edgeKind===3?2.8:4.2;
|
| 145 |
+
const axis=(a,b,n)=>Array.from({length:n+1},(_,i)=>a+(b-a)*i/n);
|
| 146 |
+
const along=axis(-TERR_EDGE_EXT,MAP+TERR_EDGE_EXT,48);
|
| 147 |
+
const middle=axis(0,MAP,32);
|
| 148 |
+
const bands=[0,130,330,610,TERR_EDGE_EXT];
|
| 149 |
+
const smooth=q=>q*q*(3-2*q);
|
| 150 |
+
const addRect=(xs,zs)=>{
|
| 151 |
+
const base=verts.length/12, nx=xs.length;
|
| 152 |
+
for(const z of zs) for(const x of xs){
|
| 153 |
+
const outside=Math.max(0,-x,x-MAP,-z,z-MAP), q=clamp(outside/TERR_EDGE_EXT,0,1);
|
| 154 |
+
const bx=clamp(x,0,MAP), bz=clamp(z,0,MAP), join=clamp(outside/150,0,1);
|
| 155 |
+
/* Continue the edge silhouette, then let it settle slightly into the
|
| 156 |
+
weather. Relief starts at zero on the seam, so even a grazing camera
|
| 157 |
+
cannot reveal a vertical crack between authored and fake terrain. */
|
| 158 |
+
const relief=(Math.sin(x*.0067+z*.0031)+Math.sin(z*.0083-x*.0027))*(1-q)*reliefAmp*join;
|
| 159 |
+
/* Isles fall away fastest into their drowned perimeter; dry maps keep
|
| 160 |
+
harder broken shelves; storm maps flatten into a low obscured waste. */
|
| 161 |
+
const h=terrainH(bx,bz)-smooth(q)*sink+relief;
|
| 162 |
+
const cc=TH.cliff||[112,116,122];
|
| 163 |
+
verts.push(x,h,z, 0,1,0, cc[0]/255,cc[1]/255,cc[2]/255,
|
| 164 |
+
x*.035,z*.035,MAT.EARTH);
|
| 165 |
+
}
|
| 166 |
+
for(let z=0;z<zs.length-1;z++) for(let x=0;x<xs.length-1;x++){
|
| 167 |
+
const a=base+z*nx+x,b=a+1,c=a+nx,d=c+1;
|
| 168 |
+
idx.push(a,c,b,b,c,d);
|
| 169 |
+
}
|
| 170 |
+
};
|
| 171 |
+
addRect(along,bands.slice().reverse().map(d=>-d));
|
| 172 |
+
addRect(along,bands.map(d=>MAP+d));
|
| 173 |
+
addRect(bands.slice().reverse().map(d=>-d),middle);
|
| 174 |
+
addRect(bands.map(d=>MAP+d),middle);
|
| 175 |
+
terrEdgeIdxCount=idx.length;
|
| 176 |
+
const data=new Float32Array(verts),indices=new Uint32Array(idx);
|
| 177 |
+
if(!terrEdgeVAO){
|
| 178 |
+
terrEdgeVAO=gl.createVertexArray(); gl.bindVertexArray(terrEdgeVAO);
|
| 179 |
+
terrEdgeVBO=gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER,terrEdgeVBO);
|
| 180 |
+
gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);
|
| 181 |
+
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0,3,gl.FLOAT,false,VSTRIDE,0);
|
| 182 |
+
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1,3,gl.FLOAT,false,VSTRIDE,12);
|
| 183 |
+
gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2,3,gl.FLOAT,false,VSTRIDE,24);
|
| 184 |
+
gl.enableVertexAttribArray(3); gl.vertexAttribPointer(3,2,gl.FLOAT,false,VSTRIDE,36);
|
| 185 |
+
gl.enableVertexAttribArray(4); gl.vertexAttribPointer(4,1,gl.FLOAT,false,VSTRIDE,44);
|
| 186 |
+
terrEdgeIBO=gl.createBuffer(); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,terrEdgeIBO);
|
| 187 |
+
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW);
|
| 188 |
+
gl.disableVertexAttribArray(5); gl.vertexAttrib4f(5,0,0,0,1);
|
| 189 |
+
gl.disableVertexAttribArray(6); gl.vertexAttrib1f(6,0);
|
| 190 |
+
gl.disableVertexAttribArray(7); gl.vertexAttrib4f(7,1,1,1,1);
|
| 191 |
+
gl.disableVertexAttribArray(8); gl.vertexAttrib1f(8,1);
|
| 192 |
+
gl.bindVertexArray(null);
|
| 193 |
+
}else{
|
| 194 |
+
gl.bindVertexArray(terrEdgeVAO);
|
| 195 |
+
gl.bindBuffer(gl.ARRAY_BUFFER,terrEdgeVBO); gl.bufferData(gl.ARRAY_BUFFER,data,gl.STATIC_DRAW);
|
| 196 |
+
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,terrEdgeIBO); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indices,gl.STATIC_DRAW);
|
| 197 |
+
gl.bindVertexArray(null);
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
/* Rewrite a rectangular window of terrain vertices — positions, normals and
|
| 202 |
colours — from the current heightfield. Normals are central differences of
|
| 203 |
the heightfield rather than face normals, which keeps the surface smooth
|
|
|
|
| 355 |
gl.drawElements(gl.TRIANGLES,terrIdxCount,gl.UNSIGNED_INT,0);
|
| 356 |
drawCalls++; triCount+=terrIdxCount/3;
|
| 357 |
}
|
| 358 |
+
function drawTerrainEdge(){
|
| 359 |
+
if(!terrEdgeVAO||!terrEdgeIdxCount) return;
|
| 360 |
+
gl.bindVertexArray(terrEdgeVAO);
|
| 361 |
+
gl.drawElements(gl.TRIANGLES,terrEdgeIdxCount,gl.UNSIGNED_INT,0);
|
| 362 |
+
drawCalls++; triCount+=terrEdgeIdxCount/3;
|
| 363 |
+
}
|
| 364 |
function drawWater(){
|
| 365 |
if(!waterVAO||!waterIdxCount) return;
|
| 366 |
gl.bindVertexArray(waterVAO);
|
src/factions.js
CHANGED
|
@@ -18,29 +18,38 @@
|
|
| 18 |
|
| 19 |
const FACART={
|
| 20 |
nova:{
|
| 21 |
-
id:'nova', nm:'
|
| 22 |
-
motto:'
|
| 23 |
col:'#5db6ff', col2:'#0d2740',
|
| 24 |
cdr:'Captain Elara Kai',
|
| 25 |
quote:'For peace. For the people. For tomorrow.',
|
| 26 |
-
lore:'
|
| 27 |
-
'
|
| 28 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
bonusNm:'Advanced Engineering',
|
| 30 |
bonus:'Units build faster and cost less.',
|
| 31 |
units:[['STRIKER','Light Skirmisher','First on the field, first to fall. Numbers, not armour.'],
|
| 32 |
['RHINO','Main Battle Tank','The line everything else is built behind.'],
|
| 33 |
['GOLIATH','Assault Walker','450 hull and a splash cannon. Breaks a defended position.'],
|
| 34 |
-
['WASP','Interceptor','Air cover. Cheap enough to lose and fast enough not to.']
|
|
|
|
| 35 |
ascendancy:{
|
| 36 |
-
id:'ascendancy', nm:'
|
| 37 |
-
motto:'STRENGTH.
|
| 38 |
col:'#ff6b58', col2:'#3a0f12',
|
| 39 |
cdr:'Lord Darion Vex',
|
| 40 |
quote:'The weak are fuel. The strong ascend.',
|
| 41 |
-
lore:'The
|
| 42 |
-
'
|
| 43 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
bonusNm:'Martial Doctrine',
|
| 45 |
bonus:'All combat units deal more damage the longer they remain in combat.',
|
| 46 |
units:[['GOLIATH','Assault Walker','Ascendancy doctrine starts here: walk forward, do not stop.'],
|
|
@@ -53,30 +62,40 @@ const FACART={
|
|
| 53 |
col:'#8ce85a', col2:'#12300f',
|
| 54 |
cdr:'Broker Lys Renn',
|
| 55 |
quote:'Information is power. Profit is freedom.',
|
| 56 |
-
lore:'The
|
| 57 |
-
'
|
| 58 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
bonusNm:'Black Market',
|
| 60 |
bonus:'Gain additional resources from captured deposits and structures.',
|
| 61 |
units:[['KESTREL','Recon Flyer','150 range on a 120-hull airframe. Sees first, leaves first.'],
|
| 62 |
['WASP','Interceptor','Air superiority bought cheaply and replaced often.'],
|
| 63 |
['RAPTOR','Strike Craft','85 damage in a 32 splash. Hits a worker line and is gone.'],
|
| 64 |
-
['RESONATOR','Sonic Platform','Its sound passes straight through shields.']
|
|
|
|
| 65 |
horde:{
|
| 66 |
-
id:'horde', nm:'
|
| 67 |
-
motto:'
|
| 68 |
col:'#b978ff', col2:'#22103a',
|
| 69 |
cdr:'The Brood Sovereign',
|
| 70 |
quote:'It does not negotiate. It arrives.',
|
| 71 |
-
lore:'
|
| 72 |
-
'
|
| 73 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
bonusNm:'Endless Spawn',
|
| 75 |
bonus:'Cheaper broods, faster hatcheries, and packs that grow as they feed.',
|
| 76 |
-
units:[['
|
| 77 |
-
['
|
| 78 |
-
['
|
| 79 |
-
['ALPHA RAVAGER','Brood Alpha','
|
|
|
|
| 80 |
};
|
| 81 |
|
| 82 |
/* Map an ai.js faction key back to its art entry. */
|
|
@@ -140,6 +159,9 @@ function facCard(id){
|
|
| 140 |
+'<div class="facUnits">'
|
| 141 |
+A.units.map(u=>'<div class="facUnit"><b>'+u[0]+'</b><span>'+u[1]+'</span><i>'+u[2]+'</i></div>').join('')
|
| 142 |
+'</div>'
|
|
|
|
|
|
|
|
|
|
| 143 |
+'<div class="facBonus"><span class="facBonusNm">'+A.bonusNm+'</span>'+A.bonus+'</div>'
|
| 144 |
+'</section>';
|
| 145 |
}
|
|
|
|
| 18 |
|
| 19 |
const FACART={
|
| 20 |
nova:{
|
| 21 |
+
id:'nova', nm:'Terran Frontline Command', key:'nova', side:'player',
|
| 22 |
+
motto:'DISCIPLINE. TECHNOLOGY. UNITY.',
|
| 23 |
col:'#5db6ff', col2:'#0d2740',
|
| 24 |
cdr:'Captain Elara Kai',
|
| 25 |
quote:'For peace. For the people. For tomorrow.',
|
| 26 |
+
lore:'Frontline Command is a combined-arms expedition built around hardened positions, '+
|
| 27 |
+
'precision fire and dependable battlefield logistics. It wins by seeing first, '+
|
| 28 |
+
'building a line, then advancing that line without breaking it.',
|
| 29 |
+
strong:'Long-range fire · durable defence · flexible combined arms',
|
| 30 |
+
weak:'Slower deployment · costly advanced upgrades · flanking and EMP pressure',
|
| 31 |
+
endgoal:'Restore the Halcyon relay lattice, evacuate the surviving colonies, and found a defensive alliance strong enough to stop the next invasion.',
|
| 32 |
+
arc:'LANDING → RELIEF CORRIDOR → PLANETARY SHIELD',
|
| 33 |
bonusNm:'Advanced Engineering',
|
| 34 |
bonus:'Units build faster and cost less.',
|
| 35 |
units:[['STRIKER','Light Skirmisher','First on the field, first to fall. Numbers, not armour.'],
|
| 36 |
['RHINO','Main Battle Tank','The line everything else is built behind.'],
|
| 37 |
['GOLIATH','Assault Walker','450 hull and a splash cannon. Breaks a defended position.'],
|
| 38 |
+
['WASP','Interceptor','Air cover. Cheap enough to lose and fast enough not to.'],
|
| 39 |
+
['ATLAS SKYCRANE','Heavy Air Transport','Four lift pods carry a mixed light force over terrain and deploy it in formation.']]},
|
| 40 |
ascendancy:{
|
| 41 |
+
id:'ascendancy', nm:'Crimson Dominion', key:'legion', side:'ai',
|
| 42 |
+
motto:'STRENGTH. AUTHORITY. CONQUEST.',
|
| 43 |
col:'#ff6b58', col2:'#3a0f12',
|
| 44 |
cdr:'Lord Darion Vex',
|
| 45 |
quote:'The weak are fuel. The strong ascend.',
|
| 46 |
+
lore:'The Dominion is an authoritarian war machine of armoured offensives, '+
|
| 47 |
+
'suppression batteries and siege engines. It wins by forcing one decisive '+
|
| 48 |
+
'front and breaking it with overwhelming burst damage.',
|
| 49 |
+
strong:'Heavy frontal assaults · anti-armour · siege and morale pressure',
|
| 50 |
+
weak:'Predictable lanes · expensive elites · disruption and mobile harassment',
|
| 51 |
+
endgoal:'Seize every pre-collapse war forge on Halcyon and crown Vex as the sole authority capable of unifying humanity by force.',
|
| 52 |
+
arc:'CONQUEST → WAR FORGES → IMPERIAL ASCENSION',
|
| 53 |
bonusNm:'Martial Doctrine',
|
| 54 |
bonus:'All combat units deal more damage the longer they remain in combat.',
|
| 55 |
units:[['GOLIATH','Assault Walker','Ascendancy doctrine starts here: walk forward, do not stop.'],
|
|
|
|
| 62 |
col:'#8ce85a', col2:'#12300f',
|
| 63 |
cdr:'Broker Lys Renn',
|
| 64 |
quote:'Information is power. Profit is freedom.',
|
| 65 |
+
lore:'The Coalition fields autonomous constructs, predictive targeting and the '+
|
| 66 |
+
'most advanced energy weapons on Halcyon. Its machine cadres reshape an '+
|
| 67 |
+
'engagement before slower armies understand the threat.',
|
| 68 |
+
strong:'Precision energy weapons · drones · shields · area control',
|
| 69 |
+
weak:'Low hull strength · setup time · melee pressure · EMP and resource drain',
|
| 70 |
+
endgoal:'Awaken the buried planetary intelligence, merge every market and defence network into it, and make the Coalition indispensable to all survivors.',
|
| 71 |
+
arc:'INFILTRATION → MACHINE AWAKENING → PERFECT NETWORK',
|
| 72 |
bonusNm:'Black Market',
|
| 73 |
bonus:'Gain additional resources from captured deposits and structures.',
|
| 74 |
units:[['KESTREL','Recon Flyer','150 range on a 120-hull airframe. Sees first, leaves first.'],
|
| 75 |
['WASP','Interceptor','Air superiority bought cheaply and replaced often.'],
|
| 76 |
['RAPTOR','Strike Craft','85 damage in a 32 splash. Hits a worker line and is gone.'],
|
| 77 |
+
['RESONATOR','Sonic Platform','Its sound passes straight through shields.'],
|
| 78 |
+
['PHASE ARK','Energy Air Transport','A cockpitless delta carrier that phase-transfers light constructs through defended terrain.']]},
|
| 79 |
horde:{
|
| 80 |
+
id:'horde', nm:'Brood Swarm', key:'horde', side:'ai',
|
| 81 |
+
motto:'EVOLVE. ASSIMILATE. ENDURE.',
|
| 82 |
col:'#b978ff', col2:'#22103a',
|
| 83 |
cdr:'The Brood Sovereign',
|
| 84 |
quote:'It does not negotiate. It arrives.',
|
| 85 |
+
lore:'The Brood is a wholly biological hive: no vehicles, factories or machine '+
|
| 86 |
+
'weapons. It grows organisms, spreads living territory, mutates under '+
|
| 87 |
+
'pressure and overwhelms defences through relentless numbers.',
|
| 88 |
+
strong:'Critical mass · anti-light claws · structure rending · caster utility',
|
| 89 |
+
weak:'Area fire · long-range siege · detection · exposed growth organs',
|
| 90 |
+
endgoal:'Root a world-spanning neural organism in Halcyon’s mantle, assimilate every useful genome, and launch living seed-ships toward the inner systems.',
|
| 91 |
+
arc:'INFESTATION → PLANETARY NERVOUS SYSTEM → STAR SEEDING',
|
| 92 |
bonusNm:'Endless Spawn',
|
| 93 |
bonus:'Cheaper broods, faster hatcheries, and packs that grow as they feed.',
|
| 94 |
+
units:[['RAVAGER','Ravager','118 hull. Fast claws rend light armor and structures; isolated bodies die quickly.'],
|
| 95 |
+
['TIDECASTER','Brood Tidecaster','Forms from 28 nearby creatures and organizes the mass into one targeted tide.'],
|
| 96 |
+
['SPITTER','Spitter','Biological ranged utility for breaking packed light formations.'],
|
| 97 |
+
['ALPHA RAVAGER','Brood Alpha','A tougher living breach-organism, still dependent on numbers and leadership.'],
|
| 98 |
+
['MASSFLESH','Living Breach Carrier','Broods merge into its body; it can float briefly, lash structures, and birth a tide behind defensive lines.']]},
|
| 99 |
};
|
| 100 |
|
| 101 |
/* Map an ai.js faction key back to its art entry. */
|
|
|
|
| 159 |
+'<div class="facUnits">'
|
| 160 |
+A.units.map(u=>'<div class="facUnit"><b>'+u[0]+'</b><span>'+u[1]+'</span><i>'+u[2]+'</i></div>').join('')
|
| 161 |
+'</div>'
|
| 162 |
+
+'<div class="facDoctrine"><div class="facStrong"><b>STRONG AT</b>'+A.strong+'</div>'
|
| 163 |
+
+'<div class="facWeak"><b>WEAK AGAINST</b>'+A.weak+'</div></div>'
|
| 164 |
+
+'<div class="facEndgoal"><span>CAMPAIGN ENDGOAL</span><b>'+A.arc+'</b><p>'+A.endgoal+'</p></div>'
|
| 165 |
+'<div class="facBonus"><span class="facBonusNm">'+A.bonusNm+'</span>'+A.bonus+'</div>'
|
| 166 |
+'</section>';
|
| 167 |
}
|
src/game/ai.js
CHANGED
|
@@ -172,6 +172,7 @@ function aiFreeSpot(type){
|
|
| 172 |
const a=Math.random()*TAU, d=70+Math.random()*320;
|
| 173 |
const x=clamp(AI.base.x+Math.cos(a)*d,edge,MAP-edge);
|
| 174 |
const y=clamp(AI.base.y+Math.sin(a)*d,edge,MAP-edge);
|
|
|
|
| 175 |
if(!footOnLand(type,x,y,0,fac)||footBlocked(type,x,y,0,null,fac)) continue;
|
| 176 |
let ok=true;
|
| 177 |
for(const D of deposits){
|
|
@@ -240,7 +241,7 @@ function aiTick(dt){
|
|
| 240 |
const reach=(900+AI.t*0.9)*(900+AI.t*0.9);
|
| 241 |
for(let d=0;d<deposits.length;d++){
|
| 242 |
const D=deposits[d];
|
| 243 |
-
if(D.taken) continue;
|
| 244 |
const dd=dist2(D.x,D.y,AI.base.x,AI.base.y);
|
| 245 |
if(dd<bd && dd<reach){ bd=dd; bestD=d; }
|
| 246 |
}
|
|
@@ -525,9 +526,16 @@ function aiTick(dt){
|
|
| 525 |
const px3=-dy2/L, py3=dx2/L;
|
| 526 |
const c1=[clamp(mx2+px3*320,60,MAP-60),clamp(my2+py3*320,60,MAP-60)];
|
| 527 |
const c2=[clamp(mx2-px3*320,60,MAP-60),clamp(my2-py3*320,60,MAP-60)];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
const w=defenseAt(c1[0],c1[1],0)<=defenseAt(c2[0],c2[1],0)?c1:c2;
|
| 529 |
tx=clamp(tx+(w[0]-mx2)*0.7,60,MAP-60);
|
| 530 |
ty=clamp(ty+(w[1]-my2)*0.7,60,MAP-60);
|
|
|
|
|
|
|
|
|
|
| 531 |
}
|
| 532 |
/* Easy commits about half its standing army instead of nearly all of it, so
|
| 533 |
a wave is something you repel rather than something you survive — and the
|
|
|
|
| 172 |
const a=Math.random()*TAU, d=70+Math.random()*320;
|
| 173 |
const x=clamp(AI.base.x+Math.cos(a)*d,edge,MAP-edge);
|
| 174 |
const y=clamp(AI.base.y+Math.sin(a)*d,edge,MAP-edge);
|
| 175 |
+
if(typeof battlefieldContains==='function'&&!battlefieldContains(x,y,edge))continue;
|
| 176 |
if(!footOnLand(type,x,y,0,fac)||footBlocked(type,x,y,0,null,fac)) continue;
|
| 177 |
let ok=true;
|
| 178 |
for(const D of deposits){
|
|
|
|
| 241 |
const reach=(900+AI.t*0.9)*(900+AI.t*0.9);
|
| 242 |
for(let d=0;d<deposits.length;d++){
|
| 243 |
const D=deposits[d];
|
| 244 |
+
if(D.taken||depositTier(D)<=0) continue;
|
| 245 |
const dd=dist2(D.x,D.y,AI.base.x,AI.base.y);
|
| 246 |
if(dd<bd && dd<reach){ bd=dd; bestD=d; }
|
| 247 |
}
|
|
|
|
| 526 |
const px3=-dy2/L, py3=dx2/L;
|
| 527 |
const c1=[clamp(mx2+px3*320,60,MAP-60),clamp(my2+py3*320,60,MAP-60)];
|
| 528 |
const c2=[clamp(mx2-px3*320,60,MAP-60),clamp(my2-py3*320,60,MAP-60)];
|
| 529 |
+
if(typeof battlefieldClampPoint==='function'){
|
| 530 |
+
const a1=battlefieldClampPoint(c1[0],c1[1],60),a2=battlefieldClampPoint(c2[0],c2[1],60);
|
| 531 |
+
c1[0]=a1[0];c1[1]=a1[1];c2[0]=a2[0];c2[1]=a2[1];
|
| 532 |
+
}
|
| 533 |
const w=defenseAt(c1[0],c1[1],0)<=defenseAt(c2[0],c2[1],0)?c1:c2;
|
| 534 |
tx=clamp(tx+(w[0]-mx2)*0.7,60,MAP-60);
|
| 535 |
ty=clamp(ty+(w[1]-my2)*0.7,60,MAP-60);
|
| 536 |
+
if(typeof battlefieldClampPoint==='function'){
|
| 537 |
+
const p=battlefieldClampPoint(tx,ty,60);tx=p[0];ty=p[1];
|
| 538 |
+
}
|
| 539 |
}
|
| 540 |
/* Easy commits about half its standing army instead of nearly all of it, so
|
| 541 |
a wave is something you repel rather than something you survive — and the
|
src/game/commander.js
CHANGED
|
@@ -172,6 +172,87 @@ function artBarrageButtonState(){
|
|
| 172 |
b.title=desc;b.setAttribute('aria-label',desc);
|
| 173 |
}
|
| 174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
function heroXP(x){
|
| 176 |
if(heroIdx<0) return;
|
| 177 |
heroXp+=x;
|
|
@@ -290,6 +371,7 @@ function fireBlast(wx,wy){
|
|
| 290 |
}
|
| 291 |
function abilTick(dt){
|
| 292 |
for(let i=0;i<abCool.length;i++) if(abCool[i]>0) abCool[i]-=dt;
|
|
|
|
| 293 |
artBarrageTick(dt);
|
| 294 |
}
|
| 295 |
/* ---------- ORBITAL LANCE — a sweeping beam bought in the Armory ---------- */
|
|
|
|
| 172 |
b.title=desc;b.setAttribute('aria-label',desc);
|
| 173 |
}
|
| 174 |
|
| 175 |
+
/* ---------- CONTEXTUAL CLASS DOCTRINES ------------------------------------
|
| 176 |
+
One adaptive button keeps the phone HUD readable while still giving three
|
| 177 |
+
major unit families a real unlocked active. The selected composition decides
|
| 178 |
+
which command is presented; mixed selections choose the family with the most
|
| 179 |
+
eligible units. Artillery retains its authored target-and-charge Barrage. */
|
| 180 |
+
const CLASS_AB={
|
| 181 |
+
assault:{nm:'BREAK',em:'➤',req:'breakthrough',energy:130,cool:44,dur:9,
|
| 182 |
+
cats:['inf','veh','at','aoe','exp'],
|
| 183 |
+
ds:'+28% damage, +22% speed and faster fire for 9s; units take +12% damage.'},
|
| 184 |
+
intercept:{nm:'INTERCEPT',em:'⌁',req:'intercept',energy:95,cool:38,dur:8,
|
| 185 |
+
cats:['air','aa'],
|
| 186 |
+
ds:'+58% speed, +22% range and faster tracking for 8s.'},
|
| 187 |
+
service:{nm:'SERVICE',em:'✚',req:'fieldservice',energy:115,cool:34,dur:7,
|
| 188 |
+
cats:['sup'],
|
| 189 |
+
ds:'Support units restore nearby allies and project a 28% damage screen for 7s.'}
|
| 190 |
+
};
|
| 191 |
+
const classAbCool={assault:0,intercept:0,service:0};
|
| 192 |
+
function classAbilityEligible(A){
|
| 193 |
+
const out=[];
|
| 194 |
+
for(let i=0;i<unitHigh;i++)if(ualive[i]&&uteam[i]===0&&usel[i]&&A.cats.indexOf(TYPES[utype[i]].cat)>=0)out.push(i);
|
| 195 |
+
return out;
|
| 196 |
+
}
|
| 197 |
+
function classAbilityChoice(){
|
| 198 |
+
let best=null,bestN=0;
|
| 199 |
+
for(const k of ['service','intercept','assault']){
|
| 200 |
+
const n=classAbilityEligible(CLASS_AB[k]).length;
|
| 201 |
+
if(n>bestN){best=k;bestN=n;}
|
| 202 |
+
}
|
| 203 |
+
return best?{key:best,A:CLASS_AB[best],units:classAbilityEligible(CLASS_AB[best])}:null;
|
| 204 |
+
}
|
| 205 |
+
function classAbilityUnlocked(A){return typeof devHas==='function'&&devHas(A.req);}
|
| 206 |
+
function tryClassAbility(){
|
| 207 |
+
const C=classAbilityChoice();
|
| 208 |
+
if(!C){toast('Select assault, interceptor, anti-air, or support units first');sfx('ui');return;}
|
| 209 |
+
const A=C.A;
|
| 210 |
+
if(!classAbilityUnlocked(A)){
|
| 211 |
+
const n=(typeof DEVTREE!=='undefined'&&DEVTREE.find(x=>x.id===A.req));
|
| 212 |
+
toast('LOCKED — research '+(n?n.nm:A.req)+' in DEVELOPMENT > DOCTRINE');sfx('ui');return;
|
| 213 |
+
}
|
| 214 |
+
if(classAbCool[C.key]>0){toast(A.nm+' REARMING — '+Math.ceil(classAbCool[C.key])+'s');return;}
|
| 215 |
+
if(!canAfford(0,0,A.energy)){toast(A.nm+' NEEDS '+A.energy+' ENERGY');return;}
|
| 216 |
+
pay(0,0,A.energy);classAbCool[C.key]=A.cool;
|
| 217 |
+
const touched=new Set(),centres=C.units;
|
| 218 |
+
if(C.key==='service'){
|
| 219 |
+
for(const i of centres){
|
| 220 |
+
uclassBuff[i]=3;uclassBuffT[i]=A.dur;
|
| 221 |
+
forUnitsIn(ux[i],uy[i],125,j=>{
|
| 222 |
+
if(uteam[j]!==0)return;touched.add(j);uclassBuff[j]=3;uclassBuffT[j]=Math.max(uclassBuffT[j],A.dur);
|
| 223 |
+
uhp[j]=Math.min(uhpm[j],uhp[j]+uhpm[j]*.26+55);
|
| 224 |
+
addParticle(0,ux[j],uy[j],0,-8,.42,6,82,255,164);
|
| 225 |
+
});
|
| 226 |
+
for(const B of blds)if(B.alive&&B.team===0&&dist2(ux[i],uy[i],B.x,B.y)<=145*145){
|
| 227 |
+
B.hp=Math.min(B.hpm,B.hp+B.hpm*.18+80);touched.add(B);
|
| 228 |
+
}
|
| 229 |
+
addParticle(3,ux[i],uy[i],0,0,.75,250,82,255,164);
|
| 230 |
+
}
|
| 231 |
+
}else{
|
| 232 |
+
const kind=C.key==='intercept'?2:1;
|
| 233 |
+
for(const i of centres){uclassBuff[i]=kind;uclassBuffT[i]=A.dur;touched.add(i);
|
| 234 |
+
const col=kind===2?[75,225,255]:[255,112,54];
|
| 235 |
+
addParticle(3,ux[i],uy[i],0,0,.5,TYPES[utype[i]].size*2.5,col[0],col[1],col[2]);
|
| 236 |
+
}
|
| 237 |
+
}
|
| 238 |
+
let cx=0,cy=0;for(const i of centres){cx+=ux[i];cy+=uy[i];}cx/=centres.length;cy/=centres.length;
|
| 239 |
+
toast(A.em+' '+A.nm+' — '+touched.size+' '+(touched.size===1?'ASSET':'ASSETS')+' AFFECTED');
|
| 240 |
+
sfx(C.key==='service'?'heal':'surge',cx,cy,1.05);
|
| 241 |
+
if(typeof radioAck==='function')radioAck('ability',touched.size,cx,cy);
|
| 242 |
+
}
|
| 243 |
+
function classAbilityReset(){for(const k in classAbCool)classAbCool[k]=0;}
|
| 244 |
+
function classAbilityTick(dt){for(const k in classAbCool)if(classAbCool[k]>0)classAbCool[k]=Math.max(0,classAbCool[k]-dt);}
|
| 245 |
+
function classAbilityButtonState(){
|
| 246 |
+
const b=document.getElementById('abClass');if(!b)return;
|
| 247 |
+
const C=classAbilityChoice(),cd=b.querySelector('.cdring'),em=document.getElementById('classAbEm'),nm=document.getElementById('classAbNm');
|
| 248 |
+
b.style.display=C?'flex':'none';if(!C)return;
|
| 249 |
+
const A=C.A,locked=!classAbilityUnlocked(A);em.textContent=A.em;nm.textContent=A.nm;
|
| 250 |
+
let cover='';if(locked)cover='LOCK';else if(classAbCool[C.key]>0)cover=Math.ceil(classAbCool[C.key]);else if(resE[0]<A.energy)cover='E';
|
| 251 |
+
b.classList.toggle('cd',!!cover);cd.style.display=cover?'flex':'none';cd.textContent=cover;
|
| 252 |
+
const desc=A.nm+' — '+A.energy+' energy, '+A.cool+'s cooldown. '+A.ds;
|
| 253 |
+
b.title=desc;b.setAttribute('aria-label',desc);
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
function heroXP(x){
|
| 257 |
if(heroIdx<0) return;
|
| 258 |
heroXp+=x;
|
|
|
|
| 371 |
}
|
| 372 |
function abilTick(dt){
|
| 373 |
for(let i=0;i<abCool.length;i++) if(abCool[i]>0) abCool[i]-=dt;
|
| 374 |
+
classAbilityTick(dt);
|
| 375 |
artBarrageTick(dt);
|
| 376 |
}
|
| 377 |
/* ---------- ORBITAL LANCE — a sweeping beam bought in the Armory ---------- */
|
src/game/economy.js
CHANGED
|
@@ -42,7 +42,20 @@ function econTick(dt){
|
|
| 42 |
for(const B of bldLive){
|
| 43 |
if(!B.alive||B.team!==team||B.prog<1) continue;
|
| 44 |
if(B.type==='hq'){ mi+=5.0; ei+=26; }
|
| 45 |
-
if(B.type==='mex')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
else if(B.type==='pgen') ei+= B.lvl===3?38 : B.lvl===2?24 : 14;
|
| 47 |
else if(B.type==='geo') ei+=30;
|
| 48 |
else if(B.type==='silo') silos++;
|
|
@@ -131,6 +144,10 @@ function snapPlace(){
|
|
| 131 |
x = ax!==null? ax : Math.round((x-offX)/SNAP)*SNAP+offX;
|
| 132 |
y = ay!==null? ay : Math.round((y-offY)/SNAP)*SNAP+offY;
|
| 133 |
placing.x=clamp(x,40,MAP-40); placing.y=clamp(y,40,MAP-40);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
function startPlacing(type){
|
| 136 |
// pull back to a framing where the build grid is legible, if we're very close
|
|
@@ -149,6 +166,10 @@ function startPlacing(type){
|
|
| 149 |
function placementValid(){
|
| 150 |
if(!placing) return false;
|
| 151 |
const T=BT[placing.type];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
if(!canAfford(0,T.cm,T.ce)) return false;
|
| 153 |
if(placing.type==='mex'){
|
| 154 |
return depositAt(placing.x,placing.y,34)>=0;
|
|
|
|
| 42 |
for(const B of bldLive){
|
| 43 |
if(!B.alive||B.team!==team||B.prog<1) continue;
|
| 44 |
if(B.type==='hq'){ mi+=5.0; ei+=26; }
|
| 45 |
+
if(B.type==='mex'){
|
| 46 |
+
const D=B.dep>=0?deposits[B.dep]:null, before=depositTier(D);
|
| 47 |
+
if(before>0){
|
| 48 |
+
const base=(B.lvl===3?11 : B.lvl===2?7 : 4)*(DEPOSIT_YIELD[before]||1)*(team===0&&WC.veins?0.7:1);
|
| 49 |
+
const got=drainDeposit(D,base*dt);
|
| 50 |
+
mi+=got/Math.max(dt,.0001);
|
| 51 |
+
B.nodeTier=depositTier(D); B.nodeRemaining=D.remaining;
|
| 52 |
+
if(team===0&&B.nodeTier!==before){
|
| 53 |
+
if(B.nodeTier>0) toast('◇ PHASE FIELD DOWNGRADED — TIER '+B.nodeTier+' · '+Math.ceil(D.remaining)+' ore remains');
|
| 54 |
+
else toast('◇ PHASE FIELD DEPLETED — relocate the Prospector or claim another node');
|
| 55 |
+
sfx('notify',B.x,B.y,.72);
|
| 56 |
+
}
|
| 57 |
+
} else { B.nodeTier=0; B.nodeRemaining=0; }
|
| 58 |
+
}
|
| 59 |
else if(B.type==='pgen') ei+= B.lvl===3?38 : B.lvl===2?24 : 14;
|
| 60 |
else if(B.type==='geo') ei+=30;
|
| 61 |
else if(B.type==='silo') silos++;
|
|
|
|
| 144 |
x = ax!==null? ax : Math.round((x-offX)/SNAP)*SNAP+offX;
|
| 145 |
y = ay!==null? ay : Math.round((y-offY)/SNAP)*SNAP+offY;
|
| 146 |
placing.x=clamp(x,40,MAP-40); placing.y=clamp(y,40,MAP-40);
|
| 147 |
+
if(typeof battlefieldClampPoint==='function'){
|
| 148 |
+
const edgePad=Math.hypot(fw,fh)*.5+8,p=battlefieldClampPoint(placing.x,placing.y,edgePad);
|
| 149 |
+
placing.x=p[0];placing.y=p[1];
|
| 150 |
+
}
|
| 151 |
}
|
| 152 |
function startPlacing(type){
|
| 153 |
// pull back to a framing where the build grid is legible, if we're very close
|
|
|
|
| 166 |
function placementValid(){
|
| 167 |
if(!placing) return false;
|
| 168 |
const T=BT[placing.type];
|
| 169 |
+
if(typeof battlefieldContains==='function'){
|
| 170 |
+
const f=bldFoot(placing.type),edgePad=Math.hypot(f[0],f[1])*.5+8;
|
| 171 |
+
if(!battlefieldContains(placing.x,placing.y,edgePad))return false;
|
| 172 |
+
}
|
| 173 |
if(!canAfford(0,T.cm,T.ce)) return false;
|
| 174 |
if(placing.type==='mex'){
|
| 175 |
return depositAt(placing.x,placing.y,34)>=0;
|
src/game/meta.js
CHANGED
|
@@ -232,6 +232,7 @@ function invRarityRoll(win,seed){
|
|
| 232 |
function invGrantMatchLoot(win){
|
| 233 |
const b=invBag(), p=activeProf(), seed=(p?p.id:'p')+'|'+META.matches+'|'+(stats.kills[0]|0)+'|'+(stats.t|0)+'|'+difficulty;
|
| 234 |
const rarity=invRarityRoll(win,seed), loot={gear:null,consumables:[]};
|
|
|
|
| 235 |
/* Victories always award gear. A committed loss can still recover a piece,
|
| 236 |
but a quick surrender cannot be used to farm the inventory. */
|
| 237 |
const gearDrop=win||((stats.t|0)>=180&&(invHash(seed+'|gear')%100)<35);
|
|
@@ -363,7 +364,8 @@ function pickWildcards(n){
|
|
| 363 |
fallback for a player who has never opened that screen, not a tax on one
|
| 364 |
who has. */
|
| 365 |
if(typeof opModsOn==='function'){
|
| 366 |
-
const chosen=Object.keys(opModsOn())
|
|
|
|
| 367 |
if(chosen.length){
|
| 368 |
for(const id of chosen){
|
| 369 |
const w=WILDCARDS.find(x=>x.id===id);
|
|
@@ -376,7 +378,11 @@ function pickWildcards(n){
|
|
| 376 |
return;
|
| 377 |
}
|
| 378 |
}
|
| 379 |
-
const pool=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
for(let k=0;k<n&&pool.length;k++){
|
| 381 |
const w=pool.splice(Math.random()*pool.length|0,1)[0];
|
| 382 |
WC[w.id]=true; wcActive.push(w);
|
|
@@ -403,11 +409,18 @@ function renderWcRow(){
|
|
| 403 |
function rewardDayKey(){
|
| 404 |
const d=new Date(); return d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate();
|
| 405 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
/* Cores used to be one opaque formula dominated by raw kill count. That made a
|
| 407 |
defensive victory feel worse than farming wildlife and gave no clue what the
|
| 408 |
economy valued. This ledger caps farmable categories, pays the objective and
|
| 409 |
chosen difficulty, and rewards a genuinely fortified base. */
|
| 410 |
function coreRewardLedger(win){
|
|
|
|
| 411 |
const secs=stats.t|0, kills=stats.kills[0]|0;
|
| 412 |
const fort=(typeof fortOf==='function'&&fortOf(0))?fortOf(0).tier|0:0;
|
| 413 |
const completion=secs>=90?18:6;
|
|
@@ -427,12 +440,13 @@ function coreRewardLedger(win){
|
|
| 427 |
}
|
| 428 |
function metaGrant(win){
|
| 429 |
const mult=wcRewardMult();
|
|
|
|
| 430 |
/* Boosters apply on top of the wildcard multiplier, so a player who stacked
|
| 431 |
both sees a genuinely large number — which is the point of spending an
|
| 432 |
hour-long booster on a hard match rather than an easy one. */
|
| 433 |
const bx=(typeof boostMul==='function')?boostMul('xp'):1;
|
| 434 |
const bc=(typeof boostMul==='function')?boostMul('cores'):1;
|
| 435 |
-
const xp=Math.round((40+stats.kills[0]*0.35+(win?120:30)+difficulty*40)*mult*bx);
|
| 436 |
const ledger=coreRewardLedger(win);
|
| 437 |
const cores=Math.round(ledger.base*mult*bc);
|
| 438 |
const data=(typeof researchDataFromMatch==='function')?researchDataFromMatch(win):{total:0,parts:[]};
|
|
@@ -601,6 +615,17 @@ function mfSetTabs(root,key,focus){
|
|
| 601 |
if(scroller) scroller.scrollTop=0;
|
| 602 |
const b=tabs.find(x=>x.dataset.mfTab===active);
|
| 603 |
if(b) try{ b.focus({preventScroll:true}); }catch(e){ b.focus(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
if(typeof sfx==='function') sfx('ui');
|
| 605 |
}
|
| 606 |
}
|
|
|
|
| 232 |
function invGrantMatchLoot(win){
|
| 233 |
const b=invBag(), p=activeProf(), seed=(p?p.id:'p')+'|'+META.matches+'|'+(stats.kills[0]|0)+'|'+(stats.t|0)+'|'+difficulty;
|
| 234 |
const rarity=invRarityRoll(win,seed), loot={gear:null,consumables:[]};
|
| 235 |
+
if(typeof matchCommitted==='function'&&!matchCommitted(win)) return loot;
|
| 236 |
/* Victories always award gear. A committed loss can still recover a piece,
|
| 237 |
but a quick surrender cannot be used to farm the inventory. */
|
| 238 |
const gearDrop=win||((stats.t|0)>=180&&(invHash(seed+'|gear')%100)<35);
|
|
|
|
| 364 |
fallback for a player who has never opened that screen, not a tax on one
|
| 365 |
who has. */
|
| 366 |
if(typeof opModsOn==='function'){
|
| 367 |
+
const chosen=Object.keys(opModsOn()).filter(id=>typeof opModUnlocked!=='function'||opModUnlocked(id)||
|
| 368 |
+
(typeof weeklyMode!=='undefined'&&weeklyMode));
|
| 369 |
if(chosen.length){
|
| 370 |
for(const id of chosen){
|
| 371 |
const w=WILDCARDS.find(x=>x.id===id);
|
|
|
|
| 378 |
return;
|
| 379 |
}
|
| 380 |
}
|
| 381 |
+
const pool=WILDCARDS.filter(w=>{
|
| 382 |
+
if(typeof opModUnlocked!=='function') return true;
|
| 383 |
+
const mod=typeof OPMODS!=='undefined'&&OPMODS.find(o=>o.id===w.id);
|
| 384 |
+
return !mod||opModUnlocked(mod);
|
| 385 |
+
});
|
| 386 |
for(let k=0;k<n&&pool.length;k++){
|
| 387 |
const w=pool.splice(Math.random()*pool.length|0,1)[0];
|
| 388 |
WC[w.id]=true; wcActive.push(w);
|
|
|
|
| 409 |
function rewardDayKey(){
|
| 410 |
const d=new Date(); return d.getFullYear()+'-'+(d.getMonth()+1)+'-'+d.getDate();
|
| 411 |
}
|
| 412 |
+
/* One anti-farm rule for every persistent reward. A real early defeat still
|
| 413 |
+
counts when the player fought or built; opening a match and immediately
|
| 414 |
+
quitting cannot mint XP, cores, loot, Data or crafting materials. */
|
| 415 |
+
function matchCommitted(win){
|
| 416 |
+
return !!win||(stats.t|0)>=60||(stats.kills[0]|0)>=8||((stats.built&&stats.built[0])|0)>=3||(stats.nests|0)>=1;
|
| 417 |
+
}
|
| 418 |
/* Cores used to be one opaque formula dominated by raw kill count. That made a
|
| 419 |
defensive victory feel worse than farming wildlife and gave no clue what the
|
| 420 |
economy valued. This ledger caps farmable categories, pays the objective and
|
| 421 |
chosen difficulty, and rewards a genuinely fortified base. */
|
| 422 |
function coreRewardLedger(win){
|
| 423 |
+
if(!matchCommitted(win)) return {base:0,parts:[]};
|
| 424 |
const secs=stats.t|0, kills=stats.kills[0]|0;
|
| 425 |
const fort=(typeof fortOf==='function'&&fortOf(0))?fortOf(0).tier|0:0;
|
| 426 |
const completion=secs>=90?18:6;
|
|
|
|
| 440 |
}
|
| 441 |
function metaGrant(win){
|
| 442 |
const mult=wcRewardMult();
|
| 443 |
+
const committed=matchCommitted(win);
|
| 444 |
/* Boosters apply on top of the wildcard multiplier, so a player who stacked
|
| 445 |
both sees a genuinely large number — which is the point of spending an
|
| 446 |
hour-long booster on a hard match rather than an easy one. */
|
| 447 |
const bx=(typeof boostMul==='function')?boostMul('xp'):1;
|
| 448 |
const bc=(typeof boostMul==='function')?boostMul('cores'):1;
|
| 449 |
+
const xp=committed?Math.round((40+stats.kills[0]*0.35+(win?120:30)+difficulty*40)*mult*bx):0;
|
| 450 |
const ledger=coreRewardLedger(win);
|
| 451 |
const cores=Math.round(ledger.base*mult*bc);
|
| 452 |
const data=(typeof researchDataFromMatch==='function')?researchDataFromMatch(win):{total:0,parts:[]};
|
|
|
|
| 615 |
if(scroller) scroller.scrollTop=0;
|
| 616 |
const b=tabs.find(x=>x.dataset.mfTab===active);
|
| 617 |
if(b) try{ b.focus({preventScroll:true}); }catch(e){ b.focus(); }
|
| 618 |
+
/* Keep the active category fully inside its horizontal rail without using
|
| 619 |
+
scrollIntoView, which can also move the entire Android page vertically. */
|
| 620 |
+
if(b){
|
| 621 |
+
const rail=b.closest('.screenTabs');
|
| 622 |
+
if(rail) requestAnimationFrame(()=>{
|
| 623 |
+
const left=b.offsetLeft,right=left+b.offsetWidth,viewL=rail.scrollLeft,viewR=viewL+rail.clientWidth;
|
| 624 |
+
if(b===tabs[0]) rail.scrollLeft=0;
|
| 625 |
+
else if(left<viewL+4) rail.scrollLeft=Math.max(0,left-8);
|
| 626 |
+
else if(right>viewR-4) rail.scrollLeft=Math.max(0,right-rail.clientWidth+8);
|
| 627 |
+
});
|
| 628 |
+
}
|
| 629 |
if(typeof sfx==='function') sfx('ui');
|
| 630 |
}
|
| 631 |
}
|
src/game/sim.js
CHANGED
|
@@ -32,8 +32,8 @@ const TYPES=[
|
|
| 32 |
{name:'Pyro', spr:'pyro', tur:null, size:16, r:6.0, hp:240, dmg:11, rng:58, cool:.38, spd:52, psp:130, ptype:5, aoe:18, wk:'m', tg:'g', cm:40, ce:160, bt:3.5, air:0, tier:1},
|
| 33 |
{name:'Vulture', spr:'vulture',tur:null, size:16, r:6.0, hp:170, dmg:52, rng:190, cool:1.3, spd:42, psp:300, ptype:8, aoe:26, wk:'e', tg:'air',cm:42, ce:170, bt:3.6, air:0, tier:1},
|
| 34 |
{name:'Bulwark', spr:'bulwark',tur:null, size:21, r:7.8, hp:950, dmg:0, rng:0, cool:9, spd:28, psp:0, ptype:0, aoe:0, wk:'n', tg:'a', cm:90, ce:380, bt:7.0, air:0, tier:2, upkeepE:5},
|
| 35 |
-
{name:'Ravager', spr:'rav', tur:null, size:16, r:5.6, hp:
|
| 36 |
-
{name:'Alpha Ravager',spr:'rav',tur:null, size:28, r:10, hp:
|
| 37 |
{name:'Corvette', spr:'corv', tur:null, size:20, r:7.5, hp:320, dmg:24, rng:115, cool:1.0, spd:42, psp:120, ptype:6, aoe:0, wk:'i', tg:'a', cm:55, ce:220, bt:5.0, air:0, tier:1, naval:1},
|
| 38 |
{name:'Dreadnought',spr:'dread',tur:null, size:32, r:12, hp:1300, dmg:75, rng:290, cool:5.0, spd:20, psp:150, ptype:2, aoe:34, wk:'e', tg:'g', cm:170, ce:680, bt:12, air:0, tier:2, naval:1},
|
| 39 |
{name:'Bombard', spr:'bombH', tur:'bombT', size:21, r:8, hp:400, dmg:95, rng:400, cool:7.0, spd:14, psp:150, ptype:9, aoe:44, wk:'e', tg:'g', cm:140, ce:560, bt:10, air:0, tier:2, minRng:100},
|
|
@@ -62,7 +62,16 @@ const TYPES=[
|
|
| 62 |
hitting harder. They are never buildable: a faction fields its own. */
|
| 63 |
{name:'Lord Darion Vex', spr:'praetor',tur:null, size:34, r:12, hp:6200, dmg:130,rng:250, cool:2.4, spd:22, psp:210, ptype:9, aoe:70, wk:'e', tg:'a', cm:0, ce:0, bt:0, air:0, tier:0, cat:'hero', legs:1, hero:'legion'},
|
| 64 |
{name:'Broker Lys Renn', spr:'archon', tur:null, size:30, r:11, hp:4600, dmg:70, rng:165, cool:.55, spd:38, psp:0, ptype:6, aoe:10, wk:'s', tg:'a', cm:0, ce:0, bt:0, air:0, tier:0, cat:'hero', hero:'syndicate'},
|
| 65 |
-
{name:'The Brood Sovereign',spr:'brood',tur:null, size:38, r:14, hp:7400, dmg:110,rng:34, cool:1.2, spd:30, psp:0, ptype:0, aoe:26, wk:'m', tg:'g', cm:0, ce:0, bt:0, air:0, tier:0, cat:'hero', legs:1, hero:'horde'},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
];
|
| 67 |
/* UNIT CATEGORIES. Used by the build menu, the unit card and the AI's
|
| 68 |
composition logic, so a role is a real thing the game reasons about rather
|
|
@@ -89,6 +98,7 @@ const UCAT={
|
|
| 89 |
for(const T of TYPES) if(!T.cat) T.cat=byName[T.name]||'veh';
|
| 90 |
})();
|
| 91 |
const UT_ENGINEER=19; // index of the Constructor in TYPES
|
|
|
|
| 92 |
const SHIELD_REDUCE=0.72, SHIELD_R=95;
|
| 93 |
const AGGRO_MULT=1.9, AGGRO_ADD=70;
|
| 94 |
const TITAN_STOMP_DMG=210, TITAN_STOMP_R=85;
|
|
@@ -99,6 +109,24 @@ const uang=new Float32Array(MAXU), uturr=new Float32Array(MAXU);
|
|
| 99 |
const utx=new Float32Array(MAXU), uty=new Float32Array(MAXU);
|
| 100 |
const uhp=new Float32Array(MAXU), uhpm=new Float32Array(MAXU);
|
| 101 |
const ucool=new Float32Array(MAXU), ubuff=new Float32Array(MAXU), ustomp=new Float32Array(MAXU);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
const utype=new Uint8Array(MAXU), uteam=new Uint8Array(MAXU);
|
| 103 |
const ualive=new Uint8Array(MAXU), ustate=new Uint8Array(MAXU);
|
| 104 |
const usel=new Uint8Array(MAXU);
|
|
@@ -160,7 +188,7 @@ function livingEnemyCommanders(){
|
|
| 160 |
return enemyHeroIdxs;
|
| 161 |
}
|
| 162 |
function unitIsBrood(i){
|
| 163 |
-
return uteam[i]===2 ||
|
| 164 |
(uteam[i]===1 && typeof AI!=='undefined' && AI.fac==='horde');
|
| 165 |
}
|
| 166 |
|
|
@@ -251,6 +279,8 @@ const MODES=[
|
|
| 251 |
{id:3,nm:'OVERDRIVE',em:'⏵', ds:'Redlined: +60% fire rate, +35% speed, bleeds HP'},
|
| 252 |
{id:4,nm:'GHOST', em:'◌', ds:'Silent running: unseen until you fire, −35% speed'},
|
| 253 |
{id:5,nm:'SUPPRESS', em:'≋', ds:'Sustained fire: +85% rate, −30% range, no movement'},
|
|
|
|
|
|
|
| 254 |
];
|
| 255 |
/* Which modes a chassis can actually take. Artillery roots to shoot further;
|
| 256 |
assault armour digs in; light frames redline or go quiet; flamers suppress. */
|
|
@@ -286,6 +316,8 @@ const UNIT_MODES=[
|
|
| 286 |
[0,1], // 28 Praetor — siege
|
| 287 |
[0,2], // 29 Archon — guard
|
| 288 |
[0,3], // 30 Broodmother — overdrive
|
|
|
|
|
|
|
| 289 |
];
|
| 290 |
const MODE_SWITCH=1.6; // seconds locked while deploying
|
| 291 |
/* MODES ARE TRADES.
|
|
@@ -305,6 +337,10 @@ function modeCoolMul(m){ return m===3?0.625 : m===5?0.54 : 1; }
|
|
| 305 |
function modeSpdMul(m){ return (m===1||m===5)?0 : m===2?0.40 : m===3?1.35 : m===4?0.65 : 1; }
|
| 306 |
function modeTakenMul(m){ return m===2?0.55 : m===1?1.35 : m===3?1.15 : m===4?1.25 : 1; }
|
| 307 |
function unitModes(ty){ return UNIT_MODES[ty]||[0]; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
function cycleMode(i){
|
| 309 |
const list=unitModes(utype[i]);
|
| 310 |
if(list.length<2) return false;
|
|
@@ -331,13 +367,20 @@ function spawnUnit(type,team,x,y){
|
|
| 331 |
let i;
|
| 332 |
if(freeList.length) i=freeList.pop();
|
| 333 |
else { if(unitHigh>=MAXU) return -1; i=unitHigh++; }
|
|
|
|
|
|
|
|
|
|
| 334 |
const L=TYPES[type].naval? (findWater(x,y)||[x,y]) : findLand(x,y);
|
| 335 |
x=L[0]; y=L[1];
|
|
|
|
|
|
|
|
|
|
| 336 |
const T=TYPES[type];
|
| 337 |
ux[i]=x; uy[i]=y; uang[i]=team?Math.PI:0; uturr[i]=uang[i];
|
| 338 |
utx[i]=x; uty[i]=y;
|
| 339 |
uhp[i]=T.hp*(team===0?resHpMult:(team===1?aiHpMult*(WC.iron?1.25:1):1)); uhpm[i]=uhp[i];
|
| 340 |
ucool[i]=Math.random()*T.cool; ubuff[i]=0; ustomp[i]=0;
|
|
|
|
| 341 |
utype[i]=type; uteam[i]=team; ualive[i]=1; ustate[i]=0; usel[i]=0;
|
| 342 |
ugen[i]=(ugen[i]+1)|0; // this slot is now a different unit
|
| 343 |
uwalk[i]=Math.random()*6.283; // desynchronise the gait across a squad
|
|
@@ -568,7 +611,7 @@ const BT={
|
|
| 568 |
})();
|
| 569 |
const ARM=[0,1,2,1,2, 0,1,1,2, 1,1,2, 0,2, 1,2, 1,0, 1, 0,
|
| 570 |
1,1,1,1,0,0,2,2, // Reaper Cinder Lancer Resonator Warden Kestrel Basilisk Harbinger
|
| 571 |
-
2,1,2];
|
| 572 |
const ARM_NM=['LIGHT','MEDIUM','HEAVY'];
|
| 573 |
/* WEAPON vs ARMOUR.
|
| 574 |
The original spread was +-15..30%, which sounds like a counter system and
|
|
@@ -604,7 +647,9 @@ const STM={p:0.9,b:1.0,m:1.1,e:1.55,g:1.20,f:0.65,s:1.15,i:1.05,n:1}; // vs st
|
|
| 604 |
const AMMO_PTYPE={0:'CASELESS TRACER',1:'AP CANNON SHELL',2:'BALLISTIC HE SHELL',3:'COMMANDER HE SHELL',
|
| 605 |
4:'UNGUIDED ROCKET',5:'THERMAL GEL',6:'ION PLASMA ORB',7:'GUIDED MISSILE',8:'FLAK AIRBURST',9:'CLUSTER MUNITION'};
|
| 606 |
function ammoName(T){
|
| 607 |
-
if(!T
|
|
|
|
|
|
|
| 608 |
if(T.wk==='m') return T.ptype===5?'NAPALM JET':'MELEE STRIKE';
|
| 609 |
if(T.wk==='b') return T.ptype===5?'THERMAL PLASMA BEAM':T.name==='Longbow'?'COHERENT SNIPER BEAM':T.name==='TITAN'?'TITAN PARTICLE LANCE':'LASER ENERGY';
|
| 610 |
if(T.wk==='g') return 'GAUSS PENETRATOR';
|
|
@@ -736,7 +781,7 @@ const RESEARCH=[
|
|
| 736 |
{id:'bal1', nm:'Ballistics I', em:'🗡', ds:'+12% unit damage', cm:120, ce:500, t:35, req:null, clvl:3},
|
| 737 |
{id:'plate1',nm:'Plating I', em:'🛡', ds:'+15% unit HP', cm:130, ce:550, t:40, req:null, clvl:3},
|
| 738 |
{id:'optics',nm:'Optics Array', em:'🔭', ds:'+12% unit range', cm:110, ce:480, t:35, req:null, clvl:4},
|
| 739 |
-
{id:'nano', nm:'
|
| 740 |
{id:'fusion',nm:'Fusion Cells', em:'⚡', ds:'+30% energy income', cm:120, ce:500, t:35, req:null, clvl:4},
|
| 741 |
{id:'hardpoint',nm:'Hardpoint Bracing',em:'🏰',ds:'+25% structure HP',cm:140,ce:580,t:42,req:null,clvl:3},
|
| 742 |
{id:'contain',nm:'Containment Lattice',em:'◆',ds:'+50% Research Complex shield',cm:135,ce:620,t:45,req:null,clvl:4},
|
|
@@ -838,7 +883,11 @@ function bldPanelStatText(B){
|
|
| 838 |
if(B.type==='sgen') return 'FIELD '+bldNum(S.field)+' · STRUCTURE REPAIR '+bldNum(S.repair)+'/s';
|
| 839 |
if(B.type==='uplink') return 'FIELD '+bldNum(S.field)+' · RANGE BOOST +'+bldNum((S.boost-1)*100)+'%';
|
| 840 |
if(B.type==='nova') return 'STRIKE '+NOVA.dmg+' · RECHARGE '+bldNum(S.recharge)+'s';
|
| 841 |
-
if(B.type==='mex')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
if(B.type==='pgen') return 'ENERGY INCOME +'+S.income+'/s';
|
| 843 |
return (BT[B.type].bcat||'STRUCTURE').toUpperCase()+' · HP '+Math.ceil(B.hpm);
|
| 844 |
}
|
|
@@ -907,16 +956,20 @@ function addBld(type,team,x,y,instant,rot){
|
|
| 907 |
const shieldMax=type==='techlab'?900*labBufferMult:0;
|
| 908 |
const b={type,team,fac,x,y,hp:(instant?T.hp:T.hp*0.1)*hpM,hpm:T.hp*hpM,r:T.r,alive:true,prog:instant?1:0,
|
| 909 |
cool:0,queue:[],repeat:false,prodT:0,heal:0,tier:1,lvl:1,upT:0,upMax:1,tang:team?Math.PI:0,
|
| 910 |
-
seen:false,boost:0,boostM:UPLINK_BOOST,res:-1,resT:0,rally:null,rich:false,
|
| 911 |
shield:instant?shieldMax:0,shieldMax,shieldT:0,dmgT:0,
|
| 912 |
guardReady:type==='techlab',guardT:0,guardCharge:0,
|
| 913 |
rot:rot||0,footTier:bldFootTierCount(type,fac),link:0,anim:Math.random()*10,animS:0,conduit:[]};
|
| 914 |
-
if(type==='mex') for(
|
|
|
|
|
|
|
|
|
|
| 915 |
if(type==='nova') b.cool=NOVA.cd*0.6; // first charge after construction
|
| 916 |
blds.push(b); rebuildBGrid();
|
| 917 |
/* Level and pave the ground for it. Doing this at placement rather than at
|
| 918 |
completion means the site is visibly prepared while construction runs. */
|
| 919 |
if(type!=='nest') makeFoundation(b);
|
|
|
|
| 920 |
return b;
|
| 921 |
}
|
| 922 |
/* BASE UNDER ATTACK.
|
|
@@ -961,6 +1014,7 @@ function damageBld(b,dmg,attTeam){
|
|
| 961 |
B.hitT=0.35; // drives the flinch/spark anim
|
| 962 |
if(B.hp<=0){
|
| 963 |
B.alive=false;
|
|
|
|
| 964 |
spawnExplosion(B.x,B.y,BT[B.type].size*1.1,B.team===2?1:B.team);
|
| 965 |
addCrater(B.x,B.y,BT[B.type].size*1.9);
|
| 966 |
addRubble(B.x,B.y,BT[B.type].size*0.9);
|
|
@@ -989,6 +1043,30 @@ function damageBld(b,dmg,attTeam){
|
|
| 989 |
// Procedurally seeded per map — every battlefield has its own economy layout,
|
| 990 |
// always point-mirrored so both commanders get a fair start.
|
| 991 |
const deposits=[], geysers=[];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 992 |
function setupDeposits(){
|
| 993 |
deposits.length=0; geysers.length=0;
|
| 994 |
const def=MAPDEFS[curMap]||MAPDEFS.vanguard;
|
|
@@ -997,39 +1075,53 @@ function setupDeposits(){
|
|
| 997 |
const starts=skirmishSpawnPoints(),all=[],gall=[];
|
| 998 |
const addNode=(arr,x,y,rich,minD,starter)=>{
|
| 999 |
x=clamp(x,100,MAP-100); y=clamp(y,100,MAP-100);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1000 |
for(const p of arr) if(dist2(x,y,p[0],p[1])<minD*minD) return false;
|
| 1001 |
arr.push(rich==null?[x,y,null,starter||'']:[x,y,rich,starter||'']); return true;
|
| 1002 |
};
|
| 1003 |
/* The selected start owns equal nearby resources. Choosing another edge
|
| 1004 |
of the map must never secretly remove the opening economy. */
|
|
|
|
|
|
|
|
|
|
| 1005 |
for(let si=0;si<starts.length;si++){
|
| 1006 |
const S=starts[si],a=Math.atan2(MAP*.5-S.y,MAP*.5-S.x),fx=Math.cos(a),fy=Math.sin(a),rx=-fy,ry=fx;
|
| 1007 |
-
addNode(all,S.x+fx*150,S.y+fy*150,0,
|
| 1008 |
-
addNode(all,S.x+fx*250+rx*115,S.y+fy*250+ry*115,0,
|
| 1009 |
-
addNode(all,S.x+fx*250-rx*115,S.y+fy*250-ry*115,0,
|
| 1010 |
-
addNode(gall,S.x+fx*355+rx*(si&1?100:-100)
|
|
|
|
| 1011 |
}
|
| 1012 |
-
const
|
| 1013 |
-
let tries=0,target=Math.max(
|
| 1014 |
while(all.length<target&&tries++<700){
|
| 1015 |
-
const x=rr(
|
| 1016 |
-
|
| 1017 |
-
|
|
|
|
| 1018 |
}
|
| 1019 |
-
tries=0; const gTarget=
|
| 1020 |
while(gall.length<gTarget&&tries++<500){
|
| 1021 |
-
const x=rr(
|
| 1022 |
-
|
| 1023 |
-
|
|
|
|
| 1024 |
}
|
| 1025 |
window.__depPts=all.concat(gall);
|
| 1026 |
-
for(const p of all) deposits.push(
|
| 1027 |
for(const p of gall) geysers.push({x:p[0],y:p[1],taken:false,starter:p[3]||''});
|
| 1028 |
return;
|
| 1029 |
}
|
| 1030 |
const pts=[]; // [x,y,rich]
|
| 1031 |
const bx=MAP*SP_LO, by=MAP*SP_HI;
|
| 1032 |
const farOK=(x,y,minD)=>{
|
|
|
|
|
|
|
| 1033 |
for(const p of pts){
|
| 1034 |
if(dist2(x,y,p[0],p[1])<minD*minD) return false;
|
| 1035 |
if(dist2(x,y,MAP-p[0],MAP-p[1])<minD*minD) return false;
|
|
@@ -1052,6 +1144,7 @@ function setupDeposits(){
|
|
| 1052 |
while(gp.length<2 && tries++<300){
|
| 1053 |
const x=rr(300,MAP-300), y=rr(300,MAP-300);
|
| 1054 |
let ok=dist2(x,y,bx,by)>380*380 && dist2(x,y,MAP-bx,MAP-by)>380*380;
|
|
|
|
| 1055 |
if(ok) for(const p of pts) if(dist2(x,y,p[0],p[1])<150*150||dist2(x,y,MAP-p[0],MAP-p[1])<150*150){ ok=false; break; }
|
| 1056 |
if(ok) for(const g of gp) if(dist2(x,y,g[0],g[1])<220*220||dist2(x,y,MAP-g[0],MAP-g[1])<220*220){ ok=false; break; }
|
| 1057 |
if(ok) gp.push([x,y]);
|
|
@@ -1060,7 +1153,7 @@ function setupDeposits(){
|
|
| 1060 |
for(const p of pts){ all.push(p); all.push([MAP-p[0],MAP-p[1],p[2]]); }
|
| 1061 |
for(const p of gp){ gall.push(p); gall.push([MAP-p[0],MAP-p[1]]); }
|
| 1062 |
window.__depPts=all.concat(gall); // terrain raises land under all nodes
|
| 1063 |
-
for(const p of all) deposits.push(
|
| 1064 |
for(const p of gall) geysers.push({x:p[0],y:p[1],taken:false,starter:''});
|
| 1065 |
}
|
| 1066 |
function geyserAt(x,y,rad){
|
|
@@ -1110,12 +1203,14 @@ function setupNests(){
|
|
| 1110 |
const placed=[];
|
| 1111 |
while(n<nestGoal && tries++<200){
|
| 1112 |
const px2=rr(500,MAP-500), py2=rr(500,MAP-500);
|
|
|
|
| 1113 |
if(typeof farFromStartZones==='function'&&!farFromStartZones(px2,py2,650)) continue;
|
| 1114 |
if(laneDist(px2,py2)<LANE_CLEAR) continue; // keep the corridor passable
|
| 1115 |
let clear=true;
|
| 1116 |
for(const q of placed) if(dist2(px2,py2,q[0],q[1])<380*380){ clear=false; break; }
|
| 1117 |
if(!clear) continue;
|
| 1118 |
-
|
|
|
|
| 1119 |
placed.push(L);
|
| 1120 |
addBld('nest',2,L[0],L[1],true);
|
| 1121 |
/* Guard units at the hive door scale down too — on Easy a wandering scout
|
|
@@ -1363,8 +1458,10 @@ function envTick(dt){
|
|
| 1363 |
const nests=liveNests();
|
| 1364 |
if(nests.length && nests.length<[3+tier,5+tier*1.5,6+tier*2][D]){
|
| 1365 |
const N=nests[Math.random()*nests.length|0];
|
| 1366 |
-
|
| 1367 |
-
if(
|
|
|
|
|
|
|
| 1368 |
&& laneDist(L[0],L[1])>=LANE_CLEAR){
|
| 1369 |
let clear=true;
|
| 1370 |
for(const B of blds) if(B.alive&&dist2(L[0],L[1],B.x,B.y)<120*120){ clear=false; break; }
|
|
@@ -1509,7 +1606,15 @@ function spawnCrate(x,y,forced){
|
|
| 1509 |
for(const q of CRATE_KINDS){ roll-=q.w; if(roll<=0){k=q;break;} }
|
| 1510 |
k=k||CRATE_KINDS[0];
|
| 1511 |
}
|
| 1512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1513 |
crates.push({x:L[0],y:L[1],kind:k,alt:520,t:0,seen:false});
|
| 1514 |
return crates[crates.length-1];
|
| 1515 |
}
|
|
@@ -1635,8 +1740,11 @@ function planDistricts(){
|
|
| 1635 |
:dist2(x,y,spawnA[0],spawnA[1])>d*d&&dist2(x,y,spawnB[0],spawnB[1])>d*d;
|
| 1636 |
|
| 1637 |
const plot=(x,y,w,h,a,kind,zone)=>{
|
|
|
|
|
|
|
| 1638 |
cityPlan.push({x,y,w,h,a,kind,zone});
|
| 1639 |
cityZones[zone].total++;
|
|
|
|
| 1640 |
};
|
| 1641 |
const makeDistrict=(cx2,cy2,ind)=>{
|
| 1642 |
const zi=cityZones.length;
|
|
@@ -1688,6 +1796,7 @@ function planDistricts(){
|
|
| 1688 |
const tryPlace=(ind,minD)=>{
|
| 1689 |
for(let a=0;a<70;a++){
|
| 1690 |
const x=rr(MAP*0.18,MAP*0.82), y=rr(MAP*0.18,MAP*0.82);
|
|
|
|
| 1691 |
if(!farFromSpawns(x,y,700)||!isWalkable(x,y)) continue;
|
| 1692 |
let clash=false;
|
| 1693 |
for(const p of placed) if(dist2(x,y,p[0],p[1])<minD*minD){ clash=true; break; }
|
|
@@ -1718,20 +1827,23 @@ function setupRelics(){
|
|
| 1718 |
// loot scattered through every district
|
| 1719 |
for(const Z of cityZones) for(let k=0;k<(Z.ind?3:2);k++){
|
| 1720 |
const a=rnd()*TAU, d=rr(60,250);
|
| 1721 |
-
|
|
|
|
| 1722 |
spawnCrate(L[0],L[1]);
|
| 1723 |
if(crates.length) crates[crates.length-1].alt=0;
|
| 1724 |
}
|
| 1725 |
// industrial belts are ringed with volatile tanks — dangerous ground to fight over
|
| 1726 |
for(const Z of cityZones) if(Z.ind) for(let k=0;k<5;k++){
|
| 1727 |
const a=rnd()*TAU, d=rr(190,310);
|
| 1728 |
-
|
|
|
|
| 1729 |
tanks.push({x:L[0],y:L[1],s:rr(30,42),hp:260,alive:true,fuse:0});
|
| 1730 |
}
|
| 1731 |
const spawnA=[MAP*SP_LO,MAP*SP_HI], spawnB=[MAP*SP_HI,MAP*SP_LO];
|
| 1732 |
const tn=10+(rnd()*8|0);
|
| 1733 |
for(let k=0;k<tn;k++){
|
| 1734 |
-
|
|
|
|
| 1735 |
if(typeof farFromStartZones==='function'&&!farFromStartZones(L[0],L[1],420)) continue;
|
| 1736 |
tanks.push({x:L[0],y:L[1],s:rr(28,40),hp:260,alive:true,fuse:0});
|
| 1737 |
}
|
|
@@ -2408,7 +2520,7 @@ function band(a,b2){ return a+Math.random()*(b2-a); }
|
|
| 2408 |
function depositAt(x,y,rad){
|
| 2409 |
for(let d=0;d<deposits.length;d++){
|
| 2410 |
const D=deposits[d];
|
| 2411 |
-
if(!D.taken && dist2(x,y,D.x,D.y)<rad*rad) return d;
|
| 2412 |
}
|
| 2413 |
return -1;
|
| 2414 |
}
|
|
@@ -2521,11 +2633,28 @@ function setupDoodads(){
|
|
| 2521 |
if(trees.length<THEMES[curTheme].trees && h>0.42 && h<0.60 && rnd()<0.75) trees.push({x,y,s:rr(16,34),a:rr(0,TAU)});
|
| 2522 |
else if(rocks.length<60 && rnd()<0.3) rocks.push({x,y,s:rr(16,44),a:rr(0,TAU)});
|
| 2523 |
}
|
| 2524 |
-
/
|
| 2525 |
-
|
| 2526 |
-
|
| 2527 |
-
|
| 2528 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2529 |
}
|
| 2530 |
}
|
| 2531 |
}
|
|
@@ -2787,6 +2916,7 @@ function dealDamage(j,dmg,attTeam,attacker,mu,wk){
|
|
| 2787 |
if(uteam[j]===0&&attTeam!==0&&META.settings.godMode){uhp[j]=uhpm[j];return;}
|
| 2788 |
if(ushielded[j]>0 && !(wk&&WK_PIERCE[wk])) dmg*=SHIELD_REDUCE;
|
| 2789 |
if(umode[j]) dmg*=modeTakenMul(umode[j]); // GUARD stance eats the hit
|
|
|
|
| 2790 |
uhp[j]-=dmg;
|
| 2791 |
if(uhp[j]>0&&unitIsBrood(j)&&dmg>=uhpm[j]*0.04&&Math.random()<0.16)
|
| 2792 |
sfx('cre_pain',ux[j],uy[j],clamp(TYPES[utype[j]].size/18,0.65,1.5));
|
|
@@ -2928,9 +3058,202 @@ function unitSeparation(i,T,isBug,swarmLOD,total){
|
|
| 2928 |
if(mag>cap){sepVX=sepVX/mag*cap;sepVY=sepVY/mag*cap;}
|
| 2929 |
}
|
| 2930 |
let tick=0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2931 |
function unitTick(dt){
|
| 2932 |
rebuildGrid();
|
| 2933 |
tickMoveCohorts();
|
|
|
|
| 2934 |
/* Patrol planning lives in input.js, which loads after the simulation. The
|
| 2935 |
guarded call preserves boot/replay tooling that evaluates sim.js alone. */
|
| 2936 |
if(typeof tickPatrolRoutes==='function')tickPatrolRoutes(dt);
|
|
@@ -2949,6 +3272,8 @@ function unitTick(dt){
|
|
| 2949 |
const T=TYPES[utype[i]];
|
| 2950 |
if(ucool[i]>0) ucool[i]-=dt;
|
| 2951 |
if(ubuff[i]>0) ubuff[i]-=dt;
|
|
|
|
|
|
|
| 2952 |
if(uhaz[i]>0) uhaz[i]-=dt;
|
| 2953 |
if(ustomp[i]>0) ustomp[i]-=dt;
|
| 2954 |
if(ushielded[i]>0) ushielded[i]-=dt;
|
|
@@ -3001,6 +3326,7 @@ function unitTick(dt){
|
|
| 3001 |
} else if(uteam[i]===0 && (tick&63)===0) stallE=0.8;
|
| 3002 |
}
|
| 3003 |
}
|
|
|
|
| 3004 |
// validate target
|
| 3005 |
let tg=utgt[i];
|
| 3006 |
if(tg>=0 && !foeTgt(i,tg,utgtg[i])){ tg=utgt[i]=-1; }
|
|
@@ -3014,7 +3340,7 @@ function unitTick(dt){
|
|
| 3014 |
else if((i+tick)%9===0&&perfScale>0.4)
|
| 3015 |
addParticle(1,ux[i],uy[i],rr(-4,4),rr(-14,-6),.5,3, 255,140,60);
|
| 3016 |
}
|
| 3017 |
-
const rngM=(uteam[i]===0?resRngMult:1)*modeRngMul(md)*(uhaz[i]>0?HAZ_RNG:1);
|
| 3018 |
// staggered acquisition
|
| 3019 |
if(T.wk!=='n' && (i+tick)%acqMod===0 && ustate[i]!==1 && ustate[i]!==6){
|
| 3020 |
/* On the march, acquisition shrinks to self-defence range: the column
|
|
@@ -3067,9 +3393,9 @@ function unitTick(dt){
|
|
| 3067 |
// fire
|
| 3068 |
if(inRange && ucool[i]<=0 && !(T.minRng && er<T.minRng)){
|
| 3069 |
const vet=1+uvet[i]*0.15;
|
| 3070 |
-
ucool[i]=T.cool*modeCoolMul(md)/(ubuff[i]>0?1.4:1);
|
| 3071 |
if(md===4){ umode[i]=0; umodeT[i]=MODE_SWITCH*0.4; } // firing breaks GHOST cover
|
| 3072 |
-
const dmg=T.dmg*vet*modeDmgMul(md)*(ubuff[i]>0?1.5:1)*(uteam[i]===1?aiDmgMult:(uteam[i]===0?armyDmgMult:1))*(i===heroIdx?heroDmgMult:1);
|
| 3073 |
const ma=(T.tur?uturr[i]:uang[i])-Math.PI/2;
|
| 3074 |
const mx=ux[i]+Math.cos(ma)*T.size*0.62, my=uy[i]+Math.sin(ma)*T.size*0.62;
|
| 3075 |
if(T.wk==='m'){
|
|
@@ -3077,8 +3403,8 @@ function unitTick(dt){
|
|
| 3077 |
if(tg>=0){
|
| 3078 |
dealDamage(tg,dmg*dmgMul('m',utype[tg]),uteam[i],i,dmgMul('m',utype[tg]));
|
| 3079 |
if(T.aoe>0) forUnitsIn(ex,ey,T.aoe,j=>{ if(uteam[j]!==uteam[i]&&j!==tg) dealDamage(j,dmg*0.5*dmgMul('m',utype[j]),uteam[i],i); });
|
| 3080 |
-
} else if(isRelicTg(tg)) damageRelic(relics[relicOf(tg)],dmg*STM.m,uteam[i]);
|
| 3081 |
-
else damageBld(-2-tg,dmg*STM.m,uteam[i]);
|
| 3082 |
addParticle(0,ex,ey,0,0,.14,9, 255,190,90);
|
| 3083 |
addParticle(5,ex,ey,rr(-40,40),rr(-40,40),.25,4, 255,170,60);
|
| 3084 |
if((i&3)===0) sfx(unitIsBrood(i)?'cre_attack':(T.ptype===5?'flame':'hit'),ux[i],uy[i],1);
|
|
@@ -3105,7 +3431,7 @@ function unitTick(dt){
|
|
| 3105 |
if((i&3)===0) sfx(T.ptype===5?'flame':'laser',ux[i],uy[i],utype[i]===8?1.8:1);
|
| 3106 |
} else {
|
| 3107 |
const pmu=tg>=0?dmgMul(T.wk||'p',utype[tg]):(STM[T.wk||'p']||1);
|
| 3108 |
-
const pk=fireProj(T.ptype,uteam[i],mx,my,ex+rr(-3,3),ey+rr(-3,3),T.psp,dmg*pmu,T.aoe,tg);
|
| 3109 |
const commanderCannon=utype[i]===4;
|
| 3110 |
if(pk>=0){
|
| 3111 |
pmu0[pk]=pmu; pwk[pk]=T.wk||'p'; pCannon[pk]=commanderCannon?1:0;
|
|
@@ -3160,7 +3486,7 @@ function unitTick(dt){
|
|
| 3160 |
if(wantMove && distGoal>0.001){
|
| 3161 |
// ground units make better time on the old highways
|
| 3162 |
const rd=(!T.air&&!T.naval&&roadAt(ux[i],uy[i]))?ROAD_SPD:1;
|
| 3163 |
-
const sp=T.spd*spdM*(ubuff[i]>0?1.35:1)*(uhaz[i]>0?HAZ_SPD:1)*rd*uCohesion[i]*Math.min(1,distGoal/18+0.25);
|
| 3164 |
// flow-field steering for long marches (routes armies around lakes)
|
| 3165 |
let ffOk=false;
|
| 3166 |
/* Formation orders share a coarse field to the leg centre, then fan out
|
|
@@ -3214,6 +3540,10 @@ function unitTick(dt){
|
|
| 3214 |
// apply with medium constraint: ground stays on land, ships stay on water, air flies anywhere
|
| 3215 |
const ox=ux[i], oy=uy[i];
|
| 3216 |
let nx=clamp(ox+mvx*dt,8,MAP-8), ny=clamp(oy+mvy*dt,8,MAP-8);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3217 |
if(!T.air && (mvx||mvy)){
|
| 3218 |
const okAt=T.naval? ((X,Y)=>!isWalkable(X,Y)) : isWalkable;
|
| 3219 |
if(!okAt(nx,ny)){
|
|
@@ -3412,14 +3742,16 @@ function bldTick(dt){
|
|
| 3412 |
if(B.prog<1){
|
| 3413 |
/* FORCED TEMPO booster: construction only, and only for the player. */
|
| 3414 |
const doctrine=(B.team===0&&typeof defenseFocus!=='undefined'&&defenseFocus&&DEFT[B.type])?1.333:1;
|
|
|
|
| 3415 |
const bs=(B.team===0?((typeof boostMul==='function'?boostMul('build'):1)*
|
| 3416 |
-
(typeof bldSpeedMult!=='undefined'?bldSpeedMult:1)*doctrine):1);
|
| 3417 |
const wasProg=B.prog;
|
| 3418 |
B.prog=Math.min(1,B.prog+dt*bs/BT[B.type].bt);
|
| 3419 |
B.hp=Math.min(B.hpm,B.hp+B.hpm*0.9*dt/BT[B.type].bt);
|
| 3420 |
if((tick&7)===0) addParticle(2,B.x+rr(-B.r,B.r),B.y+rr(-B.r,B.r),rr(-4,4),rr(-10,-2),.3,3, 160,230,255);
|
| 3421 |
if(wasProg<1&&B.prog>=1){
|
| 3422 |
if(B.type==='techlab') B.shield=B.shieldMax;
|
|
|
|
| 3423 |
if(B.team===0) sfx('build',B.x,B.y,1);
|
| 3424 |
}
|
| 3425 |
continue;
|
|
@@ -3433,6 +3765,7 @@ function bldTick(dt){
|
|
| 3433 |
if(B.dmgT>0) B.dmgT-=dt;
|
| 3434 |
if(B.shieldT>0) B.shieldT-=dt;
|
| 3435 |
if(B.guardT>0) B.guardT-=dt;
|
|
|
|
| 3436 |
const bfac=bldFactionKey(B);
|
| 3437 |
if(B.team<2){ // fortification regen
|
| 3438 |
const F=fortOf(B.team);
|
|
@@ -3799,7 +4132,8 @@ function bldTick(dt){
|
|
| 3799 |
addParticle(1,B.x+rr(0.2,0.3)*szf,B.y-szf*0.34,rr(-2,2),rr(-10,-6),.9,5, 118,118,124);
|
| 3800 |
}
|
| 3801 |
const t=B.queue[0], T=TYPES[t];
|
| 3802 |
-
const
|
|
|
|
| 3803 |
const frac=dt*speed/T.bt;
|
| 3804 |
if(!payStream(B.team, T.cm*frac, T.ce*frac)){
|
| 3805 |
if(B.team===0){ if(resM[0]<T.cm*frac) stallM=0.8; if(resE[0]<T.ce*frac) stallE=0.8; }
|
|
|
|
| 32 |
{name:'Pyro', spr:'pyro', tur:null, size:16, r:6.0, hp:240, dmg:11, rng:58, cool:.38, spd:52, psp:130, ptype:5, aoe:18, wk:'m', tg:'g', cm:40, ce:160, bt:3.5, air:0, tier:1},
|
| 33 |
{name:'Vulture', spr:'vulture',tur:null, size:16, r:6.0, hp:170, dmg:52, rng:190, cool:1.3, spd:42, psp:300, ptype:8, aoe:26, wk:'e', tg:'air',cm:42, ce:170, bt:3.6, air:0, tier:1},
|
| 34 |
{name:'Bulwark', spr:'bulwark',tur:null, size:21, r:7.8, hp:950, dmg:0, rng:0, cool:9, spd:28, psp:0, ptype:0, aoe:0, wk:'n', tg:'a', cm:90, ce:380, bt:7.0, air:0, tier:2, upkeepE:5},
|
| 35 |
+
{name:'Ravager', spr:'rav', tur:null, size:16, r:5.6, hp:118, dmg:27, rng:20, cool:.9, spd:49, psp:0, ptype:0, aoe:0, wk:'m', tg:'g', cm:0, ce:0, bt:0, air:0, tier:0, legs:1, brood:1, bldMul:1.55},
|
| 36 |
+
{name:'Alpha Ravager',spr:'rav',tur:null, size:28, r:10, hp:780, dmg:82, rng:26, cool:1.25,spd:38, psp:0, ptype:0, aoe:12, wk:'m', tg:'g', cm:0, ce:0, bt:0, air:0, tier:0, legs:1, brood:1, bldMul:1.75},
|
| 37 |
{name:'Corvette', spr:'corv', tur:null, size:20, r:7.5, hp:320, dmg:24, rng:115, cool:1.0, spd:42, psp:120, ptype:6, aoe:0, wk:'i', tg:'a', cm:55, ce:220, bt:5.0, air:0, tier:1, naval:1},
|
| 38 |
{name:'Dreadnought',spr:'dread',tur:null, size:32, r:12, hp:1300, dmg:75, rng:290, cool:5.0, spd:20, psp:150, ptype:2, aoe:34, wk:'e', tg:'g', cm:170, ce:680, bt:12, air:0, tier:2, naval:1},
|
| 39 |
{name:'Bombard', spr:'bombH', tur:'bombT', size:21, r:8, hp:400, dmg:95, rng:400, cool:7.0, spd:14, psp:150, ptype:9, aoe:44, wk:'e', tg:'g', cm:140, ce:560, bt:10, air:0, tier:2, minRng:100},
|
|
|
|
| 62 |
hitting harder. They are never buildable: a faction fields its own. */
|
| 63 |
{name:'Lord Darion Vex', spr:'praetor',tur:null, size:34, r:12, hp:6200, dmg:130,rng:250, cool:2.4, spd:22, psp:210, ptype:9, aoe:70, wk:'e', tg:'a', cm:0, ce:0, bt:0, air:0, tier:0, cat:'hero', legs:1, hero:'legion'},
|
| 64 |
{name:'Broker Lys Renn', spr:'archon', tur:null, size:30, r:11, hp:4600, dmg:70, rng:165, cool:.55, spd:38, psp:0, ptype:6, aoe:10, wk:'s', tg:'a', cm:0, ce:0, bt:0, air:0, tier:0, cat:'hero', hero:'syndicate'},
|
| 65 |
+
{name:'The Brood Sovereign',spr:'brood',tur:null, size:38, r:14, hp:7400, dmg:110,rng:34, cool:1.2, spd:30, psp:0, ptype:0, aoe:26, wk:'m', tg:'g', cm:0, ce:0, bt:0, air:0, tier:0, cat:'hero', legs:1, hero:'horde', brood:1, bldMul:1.65},
|
| 66 |
+
/* A Tidecaster is not built. A critical mass of nearby Ravagers grows one,
|
| 67 |
+
trading an individual body for coordination, speed and a target-minded
|
| 68 |
+
tide. Its own health stays deliberately low: the counterplay is to pick the
|
| 69 |
+
leader out of the crowd and let the mass dissolve back into animals. */
|
| 70 |
+
{name:'Brood Tidecaster',spr:'brood',tur:null, size:24, r:8.5, hp:390, dmg:24, rng:142, cool:2.4, spd:40, psp:175, ptype:6, aoe:26, wk:'s', tg:'a', cm:0, ce:0, bt:0, air:0, tier:0, cat:'sup', legs:1, brood:1, caster:1, bldMul:1.35},
|
| 71 |
+
/* Mobile resource utility. Prospectors earn less than an Extractor but can
|
| 72 |
+
work a field before territory reaches it; the authored mining beam makes
|
| 73 |
+
the economic action readable from the ordinary battle camera. */
|
| 74 |
+
{name:'Prospector',spr:'warden',tur:null, size:17, r:6.2, hp:190, dmg:0, rng:0, cool:9, spd:39, psp:0, ptype:0, aoe:0, wk:'n', tg:'a', cm:52, ce:210, bt:5.0, air:0, tier:1, cat:'sup', miner:1},
|
| 75 |
];
|
| 76 |
/* UNIT CATEGORIES. Used by the build menu, the unit card and the AI's
|
| 77 |
composition logic, so a role is a real thing the game reasons about rather
|
|
|
|
| 98 |
for(const T of TYPES) if(!T.cat) T.cat=byName[T.name]||'veh';
|
| 99 |
})();
|
| 100 |
const UT_ENGINEER=19; // index of the Constructor in TYPES
|
| 101 |
+
const UT_BROOD_CASTER=31, UT_MINER=32;
|
| 102 |
const SHIELD_REDUCE=0.72, SHIELD_R=95;
|
| 103 |
const AGGRO_MULT=1.9, AGGRO_ADD=70;
|
| 104 |
const TITAN_STOMP_DMG=210, TITAN_STOMP_R=85;
|
|
|
|
| 109 |
const utx=new Float32Array(MAXU), uty=new Float32Array(MAXU);
|
| 110 |
const uhp=new Float32Array(MAXU), uhpm=new Float32Array(MAXU);
|
| 111 |
const ucool=new Float32Array(MAXU), ubuff=new Float32Array(MAXU), ustomp=new Float32Array(MAXU);
|
| 112 |
+
/* Contextual class abilities use their own effect channel. `ubuff` is the
|
| 113 |
+
Commander surge and intentionally boosts everything; class doctrine needs
|
| 114 |
+
narrower, readable trades so an interceptor does not receive the same buff
|
| 115 |
+
as a breakthrough tank. 1=breakthrough, 2=intercept, 3=service shield. */
|
| 116 |
+
const uclassBuff=new Uint8Array(MAXU), uclassBuffT=new Float32Array(MAXU);
|
| 117 |
+
function classDmgMul(i){return uclassBuffT[i]>0&&uclassBuff[i]===1?1.28:uclassBuffT[i]>0&&uclassBuff[i]===2?1.12:1;}
|
| 118 |
+
function classCoolMul(i){return uclassBuffT[i]>0&&uclassBuff[i]===1?.78:uclassBuffT[i]>0&&uclassBuff[i]===2?.65:1;}
|
| 119 |
+
function classSpdMul(i){return uclassBuffT[i]>0&&uclassBuff[i]===1?1.22:uclassBuffT[i]>0&&uclassBuff[i]===2?1.58:1;}
|
| 120 |
+
function classRngMul(i){return uclassBuffT[i]>0&&uclassBuff[i]===2?1.22:1;}
|
| 121 |
+
function classTakenMul(i){return uclassBuffT[i]>0&&uclassBuff[i]===1?1.12:uclassBuffT[i]>0&&uclassBuff[i]===2?.86:uclassBuffT[i]>0&&uclassBuff[i]===3?.72:1;}
|
| 122 |
+
/* Brood leadership is intentionally offensive utility rather than hidden
|
| 123 |
+
bonus health. A player who kills the visible Tidecaster immediately strips
|
| 124 |
+
the speed, damage and cadence from every nearby creature. */
|
| 125 |
+
const ubroodLed=new Float32Array(MAXU), uMineT=new Float32Array(MAXU);
|
| 126 |
+
const uMineNode=new Int16Array(MAXU); uMineNode.fill(-1);
|
| 127 |
+
function broodDmgMul(i){return ubroodLed[i]>0?1.18:1;}
|
| 128 |
+
function broodCoolMul(i){return ubroodLed[i]>0?.82:1;}
|
| 129 |
+
function broodSpdMul(i){return ubroodLed[i]>0?1.14:1;}
|
| 130 |
const utype=new Uint8Array(MAXU), uteam=new Uint8Array(MAXU);
|
| 131 |
const ualive=new Uint8Array(MAXU), ustate=new Uint8Array(MAXU);
|
| 132 |
const usel=new Uint8Array(MAXU);
|
|
|
|
| 188 |
return enemyHeroIdxs;
|
| 189 |
}
|
| 190 |
function unitIsBrood(i){
|
| 191 |
+
return uteam[i]===2 || !!(TYPES[utype[i]]&&TYPES[utype[i]].brood) ||
|
| 192 |
(uteam[i]===1 && typeof AI!=='undefined' && AI.fac==='horde');
|
| 193 |
}
|
| 194 |
|
|
|
|
| 279 |
{id:3,nm:'OVERDRIVE',em:'⏵', ds:'Redlined: +60% fire rate, +35% speed, bleeds HP'},
|
| 280 |
{id:4,nm:'GHOST', em:'◌', ds:'Silent running: unseen until you fire, −35% speed'},
|
| 281 |
{id:5,nm:'SUPPRESS', em:'≋', ds:'Sustained fire: +85% rate, −30% range, no movement'},
|
| 282 |
+
{id:6,nm:'ASSIST', em:'↯', ds:'Tractor beam: accelerate a factory, build site or Command HQ'},
|
| 283 |
+
{id:7,nm:'SURVEY', em:'⌾', ds:'Explore the nearest unclaimed phase-crystal field'},
|
| 284 |
];
|
| 285 |
/* Which modes a chassis can actually take. Artillery roots to shoot further;
|
| 286 |
assault armour digs in; light frames redline or go quiet; flamers suppress. */
|
|
|
|
| 316 |
[0,1], // 28 Praetor — siege
|
| 317 |
[0,2], // 29 Archon — guard
|
| 318 |
[0,3], // 30 Broodmother — overdrive
|
| 319 |
+
[0], // 31 Tidecaster — biological leader
|
| 320 |
+
[0,6,7], // 32 Prospector — mine / assist / survey
|
| 321 |
];
|
| 322 |
const MODE_SWITCH=1.6; // seconds locked while deploying
|
| 323 |
/* MODES ARE TRADES.
|
|
|
|
| 337 |
function modeSpdMul(m){ return (m===1||m===5)?0 : m===2?0.40 : m===3?1.35 : m===4?0.65 : 1; }
|
| 338 |
function modeTakenMul(m){ return m===2?0.55 : m===1?1.35 : m===3?1.15 : m===4?1.25 : 1; }
|
| 339 |
function unitModes(ty){ return UNIT_MODES[ty]||[0]; }
|
| 340 |
+
function unitModeDef(ty,m){
|
| 341 |
+
if(ty===UT_MINER&&m===0) return {id:0,nm:'MINE',em:'⛏',ds:'Lock a mining tractor beam onto an active phase-crystal field'};
|
| 342 |
+
return MODES[m]||MODES[0];
|
| 343 |
+
}
|
| 344 |
function cycleMode(i){
|
| 345 |
const list=unitModes(utype[i]);
|
| 346 |
if(list.length<2) return false;
|
|
|
|
| 367 |
let i;
|
| 368 |
if(freeList.length) i=freeList.pop();
|
| 369 |
else { if(unitHigh>=MAXU) return -1; i=unitHigh++; }
|
| 370 |
+
if(typeof battlefieldClampPoint==='function'){
|
| 371 |
+
const bp=battlefieldClampPoint(x,y,(TYPES[type].r||4)+8);x=bp[0];y=bp[1];
|
| 372 |
+
}
|
| 373 |
const L=TYPES[type].naval? (findWater(x,y)||[x,y]) : findLand(x,y);
|
| 374 |
x=L[0]; y=L[1];
|
| 375 |
+
if(typeof battlefieldClampPoint==='function'){
|
| 376 |
+
const bp=battlefieldClampPoint(x,y,(TYPES[type].r||4)+8);x=bp[0];y=bp[1];
|
| 377 |
+
}
|
| 378 |
const T=TYPES[type];
|
| 379 |
ux[i]=x; uy[i]=y; uang[i]=team?Math.PI:0; uturr[i]=uang[i];
|
| 380 |
utx[i]=x; uty[i]=y;
|
| 381 |
uhp[i]=T.hp*(team===0?resHpMult:(team===1?aiHpMult*(WC.iron?1.25:1):1)); uhpm[i]=uhp[i];
|
| 382 |
ucool[i]=Math.random()*T.cool; ubuff[i]=0; ustomp[i]=0;
|
| 383 |
+
uclassBuff[i]=0;uclassBuffT[i]=0;ubroodLed[i]=0;uMineT[i]=0;uMineNode[i]=-1;
|
| 384 |
utype[i]=type; uteam[i]=team; ualive[i]=1; ustate[i]=0; usel[i]=0;
|
| 385 |
ugen[i]=(ugen[i]+1)|0; // this slot is now a different unit
|
| 386 |
uwalk[i]=Math.random()*6.283; // desynchronise the gait across a squad
|
|
|
|
| 611 |
})();
|
| 612 |
const ARM=[0,1,2,1,2, 0,1,1,2, 1,1,2, 0,2, 1,2, 1,0, 1, 0,
|
| 613 |
1,1,1,1,0,0,2,2, // Reaper Cinder Lancer Resonator Warden Kestrel Basilisk Harbinger
|
| 614 |
+
2,1,2,0,0]; // Praetor Archon Broodmother Tidecaster Prospector
|
| 615 |
const ARM_NM=['LIGHT','MEDIUM','HEAVY'];
|
| 616 |
/* WEAPON vs ARMOUR.
|
| 617 |
The original spread was +-15..30%, which sounds like a counter system and
|
|
|
|
| 647 |
const AMMO_PTYPE={0:'CASELESS TRACER',1:'AP CANNON SHELL',2:'BALLISTIC HE SHELL',3:'COMMANDER HE SHELL',
|
| 648 |
4:'UNGUIDED ROCKET',5:'THERMAL GEL',6:'ION PLASMA ORB',7:'GUIDED MISSILE',8:'FLAK AIRBURST',9:'CLUSTER MUNITION'};
|
| 649 |
function ammoName(T){
|
| 650 |
+
if(!T) return 'UNARMED';
|
| 651 |
+
if(T.miner) return 'MINING TRACTOR LASER';
|
| 652 |
+
if(T.wk==='n') return 'UNARMED';
|
| 653 |
if(T.wk==='m') return T.ptype===5?'NAPALM JET':'MELEE STRIKE';
|
| 654 |
if(T.wk==='b') return T.ptype===5?'THERMAL PLASMA BEAM':T.name==='Longbow'?'COHERENT SNIPER BEAM':T.name==='TITAN'?'TITAN PARTICLE LANCE':'LASER ENERGY';
|
| 655 |
if(T.wk==='g') return 'GAUSS PENETRATOR';
|
|
|
|
| 781 |
{id:'bal1', nm:'Ballistics I', em:'🗡', ds:'+12% unit damage', cm:120, ce:500, t:35, req:null, clvl:3},
|
| 782 |
{id:'plate1',nm:'Plating I', em:'🛡', ds:'+15% unit HP', cm:130, ce:550, t:40, req:null, clvl:3},
|
| 783 |
{id:'optics',nm:'Optics Array', em:'🔭', ds:'+12% unit range', cm:110, ce:480, t:35, req:null, clvl:4},
|
| 784 |
+
{id:'nano', nm:'Field Automation', em:'⚙', ds:'+25% build speed · +2 Constructor/Prospector cap', cm:140, ce:600, t:40, req:null, clvl:4},
|
| 785 |
{id:'fusion',nm:'Fusion Cells', em:'⚡', ds:'+30% energy income', cm:120, ce:500, t:35, req:null, clvl:4},
|
| 786 |
{id:'hardpoint',nm:'Hardpoint Bracing',em:'🏰',ds:'+25% structure HP',cm:140,ce:580,t:42,req:null,clvl:3},
|
| 787 |
{id:'contain',nm:'Containment Lattice',em:'◆',ds:'+50% Research Complex shield',cm:135,ce:620,t:45,req:null,clvl:4},
|
|
|
|
| 883 |
if(B.type==='sgen') return 'FIELD '+bldNum(S.field)+' · STRUCTURE REPAIR '+bldNum(S.repair)+'/s';
|
| 884 |
if(B.type==='uplink') return 'FIELD '+bldNum(S.field)+' · RANGE BOOST +'+bldNum((S.boost-1)*100)+'%';
|
| 885 |
if(B.type==='nova') return 'STRIKE '+NOVA.dmg+' · RECHARGE '+bldNum(S.recharge)+'s';
|
| 886 |
+
if(B.type==='mex'){
|
| 887 |
+
const D=B.dep>=0?deposits[B.dep]:null,tier=depositTier(D),rate=S.income*(DEPOSIT_YIELD[tier]||0);
|
| 888 |
+
return tier>0?'MASS +'+bldNum(rate)+'/s · FIELD TIER '+tier+' · '+Math.ceil(D.remaining)+' ORE LEFT'
|
| 889 |
+
:'FIELD DEPLETED · RELOCATE MINERS';
|
| 890 |
+
}
|
| 891 |
if(B.type==='pgen') return 'ENERGY INCOME +'+S.income+'/s';
|
| 892 |
return (BT[B.type].bcat||'STRUCTURE').toUpperCase()+' · HP '+Math.ceil(B.hpm);
|
| 893 |
}
|
|
|
|
| 956 |
const shieldMax=type==='techlab'?900*labBufferMult:0;
|
| 957 |
const b={type,team,fac,x,y,hp:(instant?T.hp:T.hp*0.1)*hpM,hpm:T.hp*hpM,r:T.r,alive:true,prog:instant?1:0,
|
| 958 |
cool:0,queue:[],repeat:false,prodT:0,heal:0,tier:1,lvl:1,upT:0,upMax:1,tang:team?Math.PI:0,
|
| 959 |
+
seen:false,boost:0,boostM:UPLINK_BOOST,res:-1,resT:0,rally:null,rich:false,dep:-1,
|
| 960 |
shield:instant?shieldMax:0,shieldMax,shieldT:0,dmgT:0,
|
| 961 |
guardReady:type==='techlab',guardT:0,guardCharge:0,
|
| 962 |
rot:rot||0,footTier:bldFootTierCount(type,fac),link:0,anim:Math.random()*10,animS:0,conduit:[]};
|
| 963 |
+
if(type==='mex') for(let di=0;di<deposits.length;di++){
|
| 964 |
+
const D=deposits[di];
|
| 965 |
+
if(dist2(D.x,D.y,x,y)<9){ b.dep=di; b.rich=(D.initialTier||1)>=3; break; }
|
| 966 |
+
}
|
| 967 |
if(type==='nova') b.cool=NOVA.cd*0.6; // first charge after construction
|
| 968 |
blds.push(b); rebuildBGrid();
|
| 969 |
/* Level and pave the ground for it. Doing this at placement rather than at
|
| 970 |
completion means the site is visibly prepared while construction runs. */
|
| 971 |
if(type!=='nest') makeFoundation(b);
|
| 972 |
+
if(type==='mex'&&instant) deployExtractorMiner(b);
|
| 973 |
return b;
|
| 974 |
}
|
| 975 |
/* BASE UNDER ATTACK.
|
|
|
|
| 1014 |
B.hitT=0.35; // drives the flinch/spark anim
|
| 1015 |
if(B.hp<=0){
|
| 1016 |
B.alive=false;
|
| 1017 |
+
if(B.type==='mex'&&B.dep>=0) redirectProspectorsFromNode(B.dep,B.team);
|
| 1018 |
spawnExplosion(B.x,B.y,BT[B.type].size*1.1,B.team===2?1:B.team);
|
| 1019 |
addCrater(B.x,B.y,BT[B.type].size*1.9);
|
| 1020 |
addRubble(B.x,B.y,BT[B.type].size*0.9);
|
|
|
|
| 1043 |
// Procedurally seeded per map — every battlefield has its own economy layout,
|
| 1044 |
// always point-mirrored so both commanders get a fair start.
|
| 1045 |
const deposits=[], geysers=[];
|
| 1046 |
+
const DEPOSIT_BAND=1000;
|
| 1047 |
+
const DEPOSIT_YIELD=[0,1,1.48,2.12];
|
| 1048 |
+
function makeDeposit(x,y,rich,starter){
|
| 1049 |
+
/* Tier is both a gameplay value and a silhouette. Starter fields begin at
|
| 1050 |
+
Tier II, rare contested veins at Tier III, and ordinary expansion pockets
|
| 1051 |
+
at Tier I. Every band is finite and visibly collapses into the next. */
|
| 1052 |
+
const patterned=((Math.floor(x/97)+Math.floor(y/113))&3)===0;
|
| 1053 |
+
const tier=rich?3:(starter||patterned?2:1);
|
| 1054 |
+
return {x,y,taken:false,rich:tier===3,starter:starter||'',initialTier:tier,
|
| 1055 |
+
tier,capacity:DEPOSIT_BAND*tier,remaining:DEPOSIT_BAND*tier,depleted:false,pulse:rr(0,TAU)};
|
| 1056 |
+
}
|
| 1057 |
+
function depositTier(D){
|
| 1058 |
+
if(!D||D.remaining<=0){ if(D){D.remaining=0;D.tier=0;D.depleted=true;} return 0; }
|
| 1059 |
+
D.tier=clamp(Math.ceil(D.remaining/DEPOSIT_BAND),1,D.initialTier||3);
|
| 1060 |
+
D.depleted=false; return D.tier;
|
| 1061 |
+
}
|
| 1062 |
+
function depositYield(D){ return DEPOSIT_YIELD[depositTier(D)]||0; }
|
| 1063 |
+
function drainDeposit(D,amount){
|
| 1064 |
+
if(!D||amount<=0||depositTier(D)<=0) return 0;
|
| 1065 |
+
const got=Math.min(D.remaining,amount),before=D.tier;
|
| 1066 |
+
D.remaining-=got; const after=depositTier(D);
|
| 1067 |
+
if(after!==before) D.phaseFlash=1.4;
|
| 1068 |
+
return got;
|
| 1069 |
+
}
|
| 1070 |
function setupDeposits(){
|
| 1071 |
deposits.length=0; geysers.length=0;
|
| 1072 |
const def=MAPDEFS[curMap]||MAPDEFS.vanguard;
|
|
|
|
| 1075 |
const starts=skirmishSpawnPoints(),all=[],gall=[];
|
| 1076 |
const addNode=(arr,x,y,rich,minD,starter)=>{
|
| 1077 |
x=clamp(x,100,MAP-100); y=clamp(y,100,MAP-100);
|
| 1078 |
+
/* Objectives belong inside the red tactical line. Starter fields near an
|
| 1079 |
+
inlet are projected inward; random expansion fields are rejected by
|
| 1080 |
+
the loops below so their authored spacing remains natural. */
|
| 1081 |
+
if(typeof battlefieldClampPoint==='function'){
|
| 1082 |
+
const p=battlefieldClampPoint(x,y,112); x=p[0]; y=p[1];
|
| 1083 |
+
}
|
| 1084 |
for(const p of arr) if(dist2(x,y,p[0],p[1])<minD*minD) return false;
|
| 1085 |
arr.push(rich==null?[x,y,null,starter||'']:[x,y,rich,starter||'']); return true;
|
| 1086 |
};
|
| 1087 |
/* The selected start owns equal nearby resources. Choosing another edge
|
| 1088 |
of the map must never secretly remove the opening economy. */
|
| 1089 |
+
const bp=typeof battlefieldPresetDef==='function'?battlefieldPresetDef():
|
| 1090 |
+
{spread:1,nodes:18,geysers:2,world:1};
|
| 1091 |
+
const spread=bp.spread||1;
|
| 1092 |
for(let si=0;si<starts.length;si++){
|
| 1093 |
const S=starts[si],a=Math.atan2(MAP*.5-S.y,MAP*.5-S.x),fx=Math.cos(a),fy=Math.sin(a),rx=-fy,ry=fx;
|
| 1094 |
+
addNode(all,S.x+fx*150*spread,S.y+fy*150*spread,0,88*spread,S.zone);
|
| 1095 |
+
addNode(all,S.x+fx*250*spread+rx*115*spread,S.y+fy*250*spread+ry*115*spread,0,88*spread,S.zone);
|
| 1096 |
+
addNode(all,S.x+fx*250*spread-rx*115*spread,S.y+fy*250*spread-ry*115*spread,0,88*spread,S.zone);
|
| 1097 |
+
addNode(gall,S.x+fx*355*spread+rx*(si&1?100:-100)*spread,
|
| 1098 |
+
S.y+fy*355*spread+ry*(si&1?100:-100)*spread,null,125*spread,S.zone);
|
| 1099 |
}
|
| 1100 |
+
const bounds=typeof battlefieldPlayBounds==='function'?battlefieldPlayBounds(170*spread):{lo:220,hi:MAP-220};
|
| 1101 |
+
let tries=0,target=Math.max(starts.length*3+4,(bp.nodes||18)+Math.max(0,starts.length-2)*3);
|
| 1102 |
while(all.length<target&&tries++<700){
|
| 1103 |
+
const x=rr(bounds.lo,bounds.hi),y=rr(bounds.lo,bounds.hi);
|
| 1104 |
+
if(typeof battlefieldContains==='function'&&!battlefieldContains(x,y,112))continue;
|
| 1105 |
+
let clear=true; for(const S of starts) if(dist2(x,y,S.x,S.y)<(330*spread)**2){clear=false;break;}
|
| 1106 |
+
if(clear) addNode(all,x,y,(all.length%7===3)?1:0,145*spread);
|
| 1107 |
}
|
| 1108 |
+
tries=0; const gTarget=starts.length+(bp.geysers||2);
|
| 1109 |
while(gall.length<gTarget&&tries++<500){
|
| 1110 |
+
const x=rr(bounds.lo,bounds.hi),y=rr(bounds.lo,bounds.hi);
|
| 1111 |
+
if(typeof battlefieldContains==='function'&&!battlefieldContains(x,y,112))continue;
|
| 1112 |
+
let clear=true; for(const p of all) if(dist2(x,y,p[0],p[1])<(145*spread)**2){clear=false;break;}
|
| 1113 |
+
if(clear) addNode(gall,x,y,null,190*spread);
|
| 1114 |
}
|
| 1115 |
window.__depPts=all.concat(gall);
|
| 1116 |
+
for(const p of all) deposits.push(makeDeposit(p[0],p[1],!!p[2],p[3]||''));
|
| 1117 |
for(const p of gall) geysers.push({x:p[0],y:p[1],taken:false,starter:p[3]||''});
|
| 1118 |
return;
|
| 1119 |
}
|
| 1120 |
const pts=[]; // [x,y,rich]
|
| 1121 |
const bx=MAP*SP_LO, by=MAP*SP_HI;
|
| 1122 |
const farOK=(x,y,minD)=>{
|
| 1123 |
+
if(typeof battlefieldContains==='function'&&(!battlefieldContains(x,y,112)||
|
| 1124 |
+
!battlefieldContains(MAP-x,MAP-y,112)))return false;
|
| 1125 |
for(const p of pts){
|
| 1126 |
if(dist2(x,y,p[0],p[1])<minD*minD) return false;
|
| 1127 |
if(dist2(x,y,MAP-p[0],MAP-p[1])<minD*minD) return false;
|
|
|
|
| 1144 |
while(gp.length<2 && tries++<300){
|
| 1145 |
const x=rr(300,MAP-300), y=rr(300,MAP-300);
|
| 1146 |
let ok=dist2(x,y,bx,by)>380*380 && dist2(x,y,MAP-bx,MAP-by)>380*380;
|
| 1147 |
+
if(ok&&typeof battlefieldContains==='function')ok=battlefieldContains(x,y,112)&&battlefieldContains(MAP-x,MAP-y,112);
|
| 1148 |
if(ok) for(const p of pts) if(dist2(x,y,p[0],p[1])<150*150||dist2(x,y,MAP-p[0],MAP-p[1])<150*150){ ok=false; break; }
|
| 1149 |
if(ok) for(const g of gp) if(dist2(x,y,g[0],g[1])<220*220||dist2(x,y,MAP-g[0],MAP-g[1])<220*220){ ok=false; break; }
|
| 1150 |
if(ok) gp.push([x,y]);
|
|
|
|
| 1153 |
for(const p of pts){ all.push(p); all.push([MAP-p[0],MAP-p[1],p[2]]); }
|
| 1154 |
for(const p of gp){ gall.push(p); gall.push([MAP-p[0],MAP-p[1]]); }
|
| 1155 |
window.__depPts=all.concat(gall); // terrain raises land under all nodes
|
| 1156 |
+
for(const p of all) deposits.push(makeDeposit(p[0],p[1],!!p[2],''));
|
| 1157 |
for(const p of gall) geysers.push({x:p[0],y:p[1],taken:false,starter:''});
|
| 1158 |
}
|
| 1159 |
function geyserAt(x,y,rad){
|
|
|
|
| 1203 |
const placed=[];
|
| 1204 |
while(n<nestGoal && tries++<200){
|
| 1205 |
const px2=rr(500,MAP-500), py2=rr(500,MAP-500);
|
| 1206 |
+
if(typeof battlefieldContains==='function'&&!battlefieldContains(px2,py2,135))continue;
|
| 1207 |
if(typeof farFromStartZones==='function'&&!farFromStartZones(px2,py2,650)) continue;
|
| 1208 |
if(laneDist(px2,py2)<LANE_CLEAR) continue; // keep the corridor passable
|
| 1209 |
let clear=true;
|
| 1210 |
for(const q of placed) if(dist2(px2,py2,q[0],q[1])<380*380){ clear=false; break; }
|
| 1211 |
if(!clear) continue;
|
| 1212 |
+
let L=findLand(px2,py2);
|
| 1213 |
+
if(typeof battlefieldClampPoint==='function')L=battlefieldClampPoint(L[0],L[1],135);
|
| 1214 |
placed.push(L);
|
| 1215 |
addBld('nest',2,L[0],L[1],true);
|
| 1216 |
/* Guard units at the hive door scale down too — on Easy a wandering scout
|
|
|
|
| 1458 |
const nests=liveNests();
|
| 1459 |
if(nests.length && nests.length<[3+tier,5+tier*1.5,6+tier*2][D]){
|
| 1460 |
const N=nests[Math.random()*nests.length|0];
|
| 1461 |
+
let L=findLand(N.x+rr(-300,300),N.y+rr(-300,300));
|
| 1462 |
+
if(typeof battlefieldClampPoint==='function')L=battlefieldClampPoint(L[0],L[1],135);
|
| 1463 |
+
if((typeof battlefieldContains!=='function'||battlefieldContains(L[0],L[1],135))
|
| 1464 |
+
&& dist2(L[0],L[1],MAP*SP_LO,MAP*SP_HI)>480*480 && dist2(L[0],L[1],MAP*SP_HI,MAP*SP_LO)>480*480
|
| 1465 |
&& laneDist(L[0],L[1])>=LANE_CLEAR){
|
| 1466 |
let clear=true;
|
| 1467 |
for(const B of blds) if(B.alive&&dist2(L[0],L[1],B.x,B.y)<120*120){ clear=false; break; }
|
|
|
|
| 1606 |
for(const q of CRATE_KINDS){ roll-=q.w; if(roll<=0){k=q;break;} }
|
| 1607 |
k=k||CRATE_KINDS[0];
|
| 1608 |
}
|
| 1609 |
+
/* Random drops sample the physical square, while Compact and authored map
|
| 1610 |
+
edges do not. Clamp before and after the land search: the first gives the
|
| 1611 |
+
search a reachable in-theatre seed, and the second prevents findLand from
|
| 1612 |
+
stepping back across a notched/coastal command boundary. 36 covers the
|
| 1613 |
+
largest rarity crate's ground footprint; two extra world units absorb
|
| 1614 |
+
projection/float error instead of leaving its outer pixel on the line. */
|
| 1615 |
+
let P=typeof battlefieldClampPoint==='function'?battlefieldClampPoint(x,y,38):[x,y];
|
| 1616 |
+
let L=findLand(P[0],P[1]);
|
| 1617 |
+
if(typeof battlefieldClampPoint==='function')L=battlefieldClampPoint(L[0],L[1],38);
|
| 1618 |
crates.push({x:L[0],y:L[1],kind:k,alt:520,t:0,seen:false});
|
| 1619 |
return crates[crates.length-1];
|
| 1620 |
}
|
|
|
|
| 1740 |
:dist2(x,y,spawnA[0],spawnA[1])>d*d&&dist2(x,y,spawnB[0],spawnB[1])>d*d;
|
| 1741 |
|
| 1742 |
const plot=(x,y,w,h,a,kind,zone)=>{
|
| 1743 |
+
const pad=Math.max(58,Math.hypot(w,h)*.55);
|
| 1744 |
+
if(typeof battlefieldContains==='function'&&!battlefieldContains(x,y,pad))return false;
|
| 1745 |
cityPlan.push({x,y,w,h,a,kind,zone});
|
| 1746 |
cityZones[zone].total++;
|
| 1747 |
+
return true;
|
| 1748 |
};
|
| 1749 |
const makeDistrict=(cx2,cy2,ind)=>{
|
| 1750 |
const zi=cityZones.length;
|
|
|
|
| 1796 |
const tryPlace=(ind,minD)=>{
|
| 1797 |
for(let a=0;a<70;a++){
|
| 1798 |
const x=rr(MAP*0.18,MAP*0.82), y=rr(MAP*0.18,MAP*0.82);
|
| 1799 |
+
if(typeof battlefieldContains==='function'&&!battlefieldContains(x,y,310))continue;
|
| 1800 |
if(!farFromSpawns(x,y,700)||!isWalkable(x,y)) continue;
|
| 1801 |
let clash=false;
|
| 1802 |
for(const p of placed) if(dist2(x,y,p[0],p[1])<minD*minD){ clash=true; break; }
|
|
|
|
| 1827 |
// loot scattered through every district
|
| 1828 |
for(const Z of cityZones) for(let k=0;k<(Z.ind?3:2);k++){
|
| 1829 |
const a=rnd()*TAU, d=rr(60,250);
|
| 1830 |
+
let L=findLand(clamp(Z.x+Math.cos(a)*d,80,MAP-80), clamp(Z.y+Math.sin(a)*d,80,MAP-80));
|
| 1831 |
+
if(typeof battlefieldClampPoint==='function')L=battlefieldClampPoint(L[0],L[1],70);
|
| 1832 |
spawnCrate(L[0],L[1]);
|
| 1833 |
if(crates.length) crates[crates.length-1].alt=0;
|
| 1834 |
}
|
| 1835 |
// industrial belts are ringed with volatile tanks — dangerous ground to fight over
|
| 1836 |
for(const Z of cityZones) if(Z.ind) for(let k=0;k<5;k++){
|
| 1837 |
const a=rnd()*TAU, d=rr(190,310);
|
| 1838 |
+
let L=findLand(clamp(Z.x+Math.cos(a)*d,80,MAP-80), clamp(Z.y+Math.sin(a)*d,80,MAP-80));
|
| 1839 |
+
if(typeof battlefieldClampPoint==='function')L=battlefieldClampPoint(L[0],L[1],70);
|
| 1840 |
tanks.push({x:L[0],y:L[1],s:rr(30,42),hp:260,alive:true,fuse:0});
|
| 1841 |
}
|
| 1842 |
const spawnA=[MAP*SP_LO,MAP*SP_HI], spawnB=[MAP*SP_HI,MAP*SP_LO];
|
| 1843 |
const tn=10+(rnd()*8|0);
|
| 1844 |
for(let k=0;k<tn;k++){
|
| 1845 |
+
let L=findLand(rr(260,MAP-260),rr(260,MAP-260));
|
| 1846 |
+
if(typeof battlefieldClampPoint==='function')L=battlefieldClampPoint(L[0],L[1],70);
|
| 1847 |
if(typeof farFromStartZones==='function'&&!farFromStartZones(L[0],L[1],420)) continue;
|
| 1848 |
tanks.push({x:L[0],y:L[1],s:rr(28,40),hp:260,alive:true,fuse:0});
|
| 1849 |
}
|
|
|
|
| 2520 |
function depositAt(x,y,rad){
|
| 2521 |
for(let d=0;d<deposits.length;d++){
|
| 2522 |
const D=deposits[d];
|
| 2523 |
+
if(!D.taken && depositTier(D)>0 && dist2(x,y,D.x,D.y)<rad*rad) return d;
|
| 2524 |
}
|
| 2525 |
return -1;
|
| 2526 |
}
|
|
|
|
| 2633 |
if(trees.length<THEMES[curTheme].trees && h>0.42 && h<0.60 && rnd()<0.75) trees.push({x,y,s:rr(16,34),a:rr(0,TAU)});
|
| 2634 |
else if(rocks.length<60 && rnd()<0.3) rocks.push({x,y,s:rr(16,44),a:rr(0,TAU)});
|
| 2635 |
}
|
| 2636 |
+
/* PHASE-CRYSTAL FIELDS. Two decorative shards could not communicate value,
|
| 2637 |
+
ownership or depletion. Each economic tier now owns a concentric stratum:
|
| 2638 |
+
outer Tier III spires disappear first, then Tier II, leaving a small Tier I
|
| 2639 |
+
core before exhaustion. The fields are original Halcyon phase ore rather
|
| 2640 |
+
than a copy of another game's resource silhouette. */
|
| 2641 |
+
for(let di=0;di<deposits.length;di++){
|
| 2642 |
+
const D=deposits[di],tier=D.initialTier||1;
|
| 2643 |
+
/* A readable central crown makes the field recognizable at phone zoom.
|
| 2644 |
+
These are real instanced crystal meshes in the 3D pass, not map pins. */
|
| 2645 |
+
for(let k=0;k<3;k++){
|
| 2646 |
+
const a=k/3*TAU+rr(-.18,.18),d=k?rr(7,15):rr(1,5);
|
| 2647 |
+
crystals.push({x:D.x+Math.cos(a)*d,y:D.y+Math.sin(a)*d,
|
| 2648 |
+
s:rr(30,41)+(tier-1)*3,a:rr(-0.28,0.28),dep:di,band:1,phase:rr(0,TAU),core:1});
|
| 2649 |
+
}
|
| 2650 |
+
for(let band=1;band<=tier;band++){
|
| 2651 |
+
const count=band===1?7:band===2?6:7;
|
| 2652 |
+
for(let k=0;k<count;k++){
|
| 2653 |
+
const a=(k/count)*TAU+rr(-.18,.18)+band*.41;
|
| 2654 |
+
const d=band===1?rr(17,37):band===2?rr(40,62):rr(65,88);
|
| 2655 |
+
crystals.push({x:D.x+Math.cos(a)*d,y:D.y+Math.sin(a)*d,
|
| 2656 |
+
s:rr(14+band*2,22+band*3),a:rr(-0.42,0.42),dep:di,band,phase:rr(0,TAU)});
|
| 2657 |
+
}
|
| 2658 |
}
|
| 2659 |
}
|
| 2660 |
}
|
|
|
|
| 2916 |
if(uteam[j]===0&&attTeam!==0&&META.settings.godMode){uhp[j]=uhpm[j];return;}
|
| 2917 |
if(ushielded[j]>0 && !(wk&&WK_PIERCE[wk])) dmg*=SHIELD_REDUCE;
|
| 2918 |
if(umode[j]) dmg*=modeTakenMul(umode[j]); // GUARD stance eats the hit
|
| 2919 |
+
dmg*=classTakenMul(j);
|
| 2920 |
uhp[j]-=dmg;
|
| 2921 |
if(uhp[j]>0&&unitIsBrood(j)&&dmg>=uhpm[j]*0.04&&Math.random()<0.16)
|
| 2922 |
sfx('cre_pain',ux[j],uy[j],clamp(TYPES[utype[j]].size/18,0.65,1.5));
|
|
|
|
| 3058 |
if(mag>cap){sepVX=sepVX/mag*cap;sepVY=sepVY/mag*cap;}
|
| 3059 |
}
|
| 3060 |
let tick=0;
|
| 3061 |
+
const BROOD_MASS=28, BROOD_AURA=285;
|
| 3062 |
+
let broodMassT=0;
|
| 3063 |
+
function broodCriticalMassTick(dt){
|
| 3064 |
+
broodMassT-=dt; if(broodMassT>0) return;
|
| 3065 |
+
broodMassT=1.35;
|
| 3066 |
+
/* Wildlife and the Brood opponent both currently emerge through the hive
|
| 3067 |
+
team. Keeping the mechanic on the biological roster prevents a recolored
|
| 3068 |
+
machine from accidentally qualifying as a caster just because the enemy
|
| 3069 |
+
faction selector says Brood. */
|
| 3070 |
+
const fighters=[],casters=[];
|
| 3071 |
+
for(let i=0;i<unitHigh;i++) if(ualive[i]&&uteam[i]===2){
|
| 3072 |
+
if(utype[i]===12||utype[i]===13) fighters.push(i);
|
| 3073 |
+
else if(utype[i]===UT_BROOD_CASTER) casters.push(i);
|
| 3074 |
+
}
|
| 3075 |
+
const desired=Math.min(7,Math.floor(fighters.length/BROOD_MASS));
|
| 3076 |
+
if(casters.length<desired){
|
| 3077 |
+
let seed=-1,best=0;
|
| 3078 |
+
/* Sample the mass rather than doing an all-pairs search. The spatial hash
|
| 3079 |
+
still verifies that the chosen body sits inside a genuine local crowd. */
|
| 3080 |
+
for(let n=0;n<fighters.length;n+=Math.max(1,(fighters.length/40)|0)){
|
| 3081 |
+
const i=fighters[n]; if(utype[i]!==12) continue;
|
| 3082 |
+
let local=0,led=false;
|
| 3083 |
+
forUnitsIn(ux[i],uy[i],225,j=>{
|
| 3084 |
+
if(uteam[j]!==2) return;
|
| 3085 |
+
if(utype[j]===12||utype[j]===13) local++;
|
| 3086 |
+
if(utype[j]===UT_BROOD_CASTER) led=true;
|
| 3087 |
+
});
|
| 3088 |
+
if(!led&&local>best){best=local;seed=i;}
|
| 3089 |
+
}
|
| 3090 |
+
if(seed>=0&&best>=BROOD_MASS){
|
| 3091 |
+
const x=ux[seed],y=uy[seed]; killUnit(seed,true);
|
| 3092 |
+
const c=spawnUnit(UT_BROOD_CASTER,2,x,y);
|
| 3093 |
+
if(c>=0){
|
| 3094 |
+
casters.push(c); ustate[c]=2; ubroodLed[c]=3;
|
| 3095 |
+
addParticle(3,x,y,0,0,.8,92,180,92,255);
|
| 3096 |
+
addParticle(3,x,y,0,0,1.25,150,92,230,155);
|
| 3097 |
+
for(let q=0;q<12;q++) addParticle(0,x,y,rr(-46,46),rr(-46,46),.55,5,178,255,96);
|
| 3098 |
+
if(typeof fogPointVisible!=='function'||fogPointVisible(x,y)){
|
| 3099 |
+
toast('☣ CRITICAL MASS — a Brood Tidecaster has formed');
|
| 3100 |
+
sfx('alarm',x,y,.72);
|
| 3101 |
+
}
|
| 3102 |
+
}
|
| 3103 |
+
}
|
| 3104 |
+
}
|
| 3105 |
+
/* A caster converts a loose crowd into one purposeful wave. It does not make
|
| 3106 |
+
units occupy the same point: staggered target offsets retain the existing
|
| 3107 |
+
soft separation while the entire tide advances on one strategic object. */
|
| 3108 |
+
for(const c of casters){
|
| 3109 |
+
if(!ualive[c]) continue;
|
| 3110 |
+
const bi=findEnemyBld(ux[c],uy[c],2,MAP*1.5);
|
| 3111 |
+
let tx=utx[c],ty=uty[c],tg=-1;
|
| 3112 |
+
if(bi>=0){tx=blds[bi].x;ty=blds[bi].y;tg=-2-bi;}
|
| 3113 |
+
else {
|
| 3114 |
+
const e=findEnemy(ux[c],uy[c],2,MAP*1.25);
|
| 3115 |
+
if(e>=0){tx=ux[e];ty=uy[e];tg=e;}
|
| 3116 |
+
}
|
| 3117 |
+
if(tg===-1&&bi<0) continue;
|
| 3118 |
+
const fld=requestField(tx,ty); let slot=0;
|
| 3119 |
+
forUnitsIn(ux[c],uy[c],BROOD_AURA,j=>{
|
| 3120 |
+
if(uteam[j]!==2||(utype[j]!==12&&utype[j]!==13&&utype[j]!==UT_BROOD_CASTER)) return;
|
| 3121 |
+
ubroodLed[j]=2.8;
|
| 3122 |
+
const lane=(slot++%7)-3,row=(slot/7|0);
|
| 3123 |
+
ustate[j]=2; utgt[j]=-1; utgtg[j]=-1; ufield[j]=fld; umarch[j]=1;
|
| 3124 |
+
utx[j]=clamp(tx+lane*15,20,MAP-20); uty[j]=clamp(ty+row*12,20,MAP-20);
|
| 3125 |
+
});
|
| 3126 |
+
}
|
| 3127 |
+
}
|
| 3128 |
+
function nearestOreNode(x,y,rad){
|
| 3129 |
+
let best=-1,bd=rad*rad;
|
| 3130 |
+
for(let d=0;d<deposits.length;d++){
|
| 3131 |
+
const D=deposits[d]; if(depositTier(D)<=0) continue;
|
| 3132 |
+
const dd=dist2(x,y,D.x,D.y); if(dd<bd){bd=dd;best=d;}
|
| 3133 |
+
}
|
| 3134 |
+
return best;
|
| 3135 |
+
}
|
| 3136 |
+
function supportUnitCap(team){
|
| 3137 |
+
if(team!==0) return 10;
|
| 3138 |
+
const lab=hasBld(0,'techlab')?2:0,automation=(typeof researched!=='undefined'&&researched.nano)?2:0;
|
| 3139 |
+
return Math.min(12,3+Math.floor(Math.max(0,heroLvl-1)/2)+lab+automation);
|
| 3140 |
+
}
|
| 3141 |
+
function supportUnitCount(team,includeQueues){
|
| 3142 |
+
let n=0;
|
| 3143 |
+
for(let i=0;i<unitHigh;i++) if(ualive[i]&&uteam[i]===team&&(utype[i]===UT_ENGINEER||utype[i]===UT_MINER)) n++;
|
| 3144 |
+
if(includeQueues) for(const B of bldLive) if(B.alive&&B.team===team&&B.queue)
|
| 3145 |
+
for(const q of B.queue) if(q===UT_ENGINEER||q===UT_MINER)n++;
|
| 3146 |
+
return n;
|
| 3147 |
+
}
|
| 3148 |
+
function nearestSupportBuilding(i,types,rad){
|
| 3149 |
+
let best=-1,bd=rad*rad;
|
| 3150 |
+
for(let b=0;b<blds.length;b++){
|
| 3151 |
+
const B=blds[b]; if(!B.alive||B.team!==uteam[i]||types.indexOf(B.type)<0)continue;
|
| 3152 |
+
const d=dist2(ux[i],uy[i],B.x,B.y);if(d<bd){bd=d;best=b;}
|
| 3153 |
+
}
|
| 3154 |
+
return best;
|
| 3155 |
+
}
|
| 3156 |
+
function redirectProspector(i){
|
| 3157 |
+
if(!ualive[i]||utype[i]!==UT_MINER)return false;
|
| 3158 |
+
let best=-1,bd=Infinity;
|
| 3159 |
+
for(const B of bldLive){
|
| 3160 |
+
if(!B.alive||B.team!==uteam[i]||B.type!=='mex'||B.dep<0||depositTier(deposits[B.dep])<=0)continue;
|
| 3161 |
+
const d=dist2(ux[i],uy[i],B.x,B.y);if(d<bd){bd=d;best=B.dep;}
|
| 3162 |
+
}
|
| 3163 |
+
if(best>=0){
|
| 3164 |
+
const D=deposits[best];umode[i]=0;uMineNode[i]=best;ustate[i]=2;utgt[i]=-1;
|
| 3165 |
+
utx[i]=D.x+rr(-30,30);uty[i]=D.y+rr(-30,30);ufield[i]=requestField(D.x,D.y);
|
| 3166 |
+
return true;
|
| 3167 |
+
}
|
| 3168 |
+
const h=nearestSupportBuilding(i,['hq','fac','airfield','harbor','tgate'],MAP*2);
|
| 3169 |
+
umode[i]=6;uMineNode[i]=-1;
|
| 3170 |
+
if(h>=0){const B=blds[h];ustate[i]=2;utx[i]=B.x+rr(-24,24);uty[i]=B.y+rr(-24,24);ufield[i]=requestField(B.x,B.y);return true;}
|
| 3171 |
+
return false;
|
| 3172 |
+
}
|
| 3173 |
+
function redirectProspectorsFromNode(dep,team){
|
| 3174 |
+
for(let i=0;i<unitHigh;i++)if(ualive[i]&&uteam[i]===team&&utype[i]===UT_MINER&&uMineNode[i]===dep){
|
| 3175 |
+
uMineNode[i]=-1;redirectProspector(i);
|
| 3176 |
+
if(team===0)toast('⛏ EXTRACTOR LOST — Prospector reassigned automatically');
|
| 3177 |
+
}
|
| 3178 |
+
}
|
| 3179 |
+
function deployExtractorMiner(B){
|
| 3180 |
+
if(!B||B.type!=='mex'||B.freeMiner)return -1;
|
| 3181 |
+
B.freeMiner=true;
|
| 3182 |
+
/* The Extractor's attached Prospector is part of the structure package and
|
| 3183 |
+
is never denied by the manual support cap. If grants push the roster over
|
| 3184 |
+
that cap, factories simply cannot recruit more support until Commander or
|
| 3185 |
+
Research Lab progression catches up. */
|
| 3186 |
+
const i=spawnUnit(UT_MINER,B.team,B.x+B.r+18,B.y+B.r*.45);
|
| 3187 |
+
if(i>=0){uMineNode[i]=B.dep;utx[i]=B.x;uty[i]=B.y;if(B.team===0)toast('⛏ PROSPECTOR DEPLOYED — MINE / ASSIST / SURVEY orders unlocked');}
|
| 3188 |
+
return i;
|
| 3189 |
+
}
|
| 3190 |
+
function prospectorAssistTick(i,dt){
|
| 3191 |
+
const range=145;
|
| 3192 |
+
let bi=nearestSupportBuilding(i,['hq','fac','airfield','harbor','tgate'],range);
|
| 3193 |
+
if(bi<0){
|
| 3194 |
+
if(ustate[i]===2&&dist2(ux[i],uy[i],utx[i],uty[i])>35*35)return false;
|
| 3195 |
+
bi=nearestSupportBuilding(i,['hq','fac','airfield','harbor','tgate'],MAP*2);
|
| 3196 |
+
if(bi<0)return false;
|
| 3197 |
+
const G=blds[bi];ustate[i]=2;utx[i]=G.x+rr(-28,28);uty[i]=G.y+rr(-28,28);ufield[i]=requestField(G.x,G.y);return false;
|
| 3198 |
+
}
|
| 3199 |
+
const B=blds[bi],a=Math.atan2(B.y-uy[i],B.x-ux[i]);umov[i]=0;uang[i]=a+Math.PI/2;
|
| 3200 |
+
B.tractorT=.18;B.tractorN=Math.min(2,(B.tractorFrame===tick?(B.tractorN||0)+1:1));B.tractorFrame=tick;
|
| 3201 |
+
const mx=ux[i]+Math.cos(a)*TYPES[UT_MINER].size*.55,my=uy[i]+Math.sin(a)*TYPES[UT_MINER].size*.55;
|
| 3202 |
+
addBeam(mx,my,B.x,B.y,3.2,90,225,255,.11,'laser');
|
| 3203 |
+
if(B.type==='hq'&&uteam[i]<2){resM[uteam[i]]=Math.min(RES_MCAP[uteam[i]],resM[uteam[i]]+.42*dt);resE[uteam[i]]=Math.min(RES_ECAP[uteam[i]],resE[uteam[i]]+1.8*dt);}
|
| 3204 |
+
return true;
|
| 3205 |
+
}
|
| 3206 |
+
function prospectorSurveyTick(i,dt){
|
| 3207 |
+
let di=uMineNode[i],bit=1<<Math.min(2,uteam[i]);
|
| 3208 |
+
if(di<0||depositTier(deposits[di])<=0||((deposits[di].surveyed||0)&bit)){
|
| 3209 |
+
let best=-1,bd=Infinity;
|
| 3210 |
+
for(let d=0;d<deposits.length;d++){
|
| 3211 |
+
const D=deposits[d];if(depositTier(D)<=0||((D.surveyed||0)&bit))continue;
|
| 3212 |
+
const dd=dist2(ux[i],uy[i],D.x,D.y);if(dd<bd){bd=dd;best=d;}
|
| 3213 |
+
}
|
| 3214 |
+
if(best<0){umode[i]=6;return redirectProspector(i);}
|
| 3215 |
+
di=uMineNode[i]=best;const D=deposits[di];ustate[i]=2;utx[i]=D.x+50;uty[i]=D.y;ufield[i]=requestField(D.x,D.y);return false;
|
| 3216 |
+
}
|
| 3217 |
+
const D=deposits[di];if(dist2(ux[i],uy[i],D.x,D.y)>110*110)return false;
|
| 3218 |
+
D.surveyed=(D.surveyed||0)|bit;uMineT[i]=1.5;
|
| 3219 |
+
addParticle(3,D.x,D.y,0,0,.8,155,80,225,255);if(typeof mmPing==='function')mmPing(D.x,D.y);
|
| 3220 |
+
if(uteam[i]===0){resM[0]=Math.min(RES_MCAP[0],resM[0]+35+depositTier(D)*15);toast('⌾ SURVEY COMPLETE — Tier '+depositTier(D)+' phase field charted');sfx('notify',D.x,D.y,.8);}
|
| 3221 |
+
uMineNode[i]=-1;return true;
|
| 3222 |
+
}
|
| 3223 |
+
function minerUnitTick(i,dt){
|
| 3224 |
+
if(umode[i]===6)return prospectorAssistTick(i,dt);
|
| 3225 |
+
if(umode[i]===7)return prospectorSurveyTick(i,dt);
|
| 3226 |
+
const R=125,existing=uMineNode[i];
|
| 3227 |
+
let di=existing>=0&&depositTier(deposits[existing])>0?existing:-1;
|
| 3228 |
+
if(di<0||dist2(ux[i],uy[i],deposits[di].x,deposits[di].y)>R*R) di=nearestOreNode(ux[i],uy[i],R);
|
| 3229 |
+
if(di<0){uMineNode[i]=-1;if(!redirectProspector(i))return false;return false;}
|
| 3230 |
+
const D=deposits[di];
|
| 3231 |
+
/* A fresh move order always wins. The Prospector only locks onto the field
|
| 3232 |
+
after arriving, so it cannot feel as if it ignored the player's drag. */
|
| 3233 |
+
if(ustate[i]===2&&dist2(ux[i],uy[i],utx[i],uty[i])>30*30&&dist2(utx[i],uty[i],D.x,D.y)>R*R) return false;
|
| 3234 |
+
uMineNode[i]=di; umov[i]=0;
|
| 3235 |
+
const a=Math.atan2(D.y-uy[i],D.x-ux[i]); uang[i]=a+Math.PI/2;
|
| 3236 |
+
const mx=ux[i]+Math.cos(a)*TYPES[utype[i]].size*.55,my=uy[i]+Math.sin(a)*TYPES[utype[i]].size*.55;
|
| 3237 |
+
const tier=depositTier(D),col=tier===3?[205,105,255]:tier===2?[80,255,165]:[80,220,255];
|
| 3238 |
+
addBeam(mx,my,D.x+Math.cos(D.pulse||0)*5,D.y+Math.sin(D.pulse||0)*5,3.4,col[0],col[1],col[2],.11,'laser');
|
| 3239 |
+
addParticle(0,D.x+rr(-9,9),D.y+rr(-9,9),rr(-5,5),rr(-14,-4),.25,4,col[0],col[1],col[2]);
|
| 3240 |
+
uMineT[i]-=dt;
|
| 3241 |
+
if(uMineT[i]<=0){
|
| 3242 |
+
uMineT[i]=.68; const before=D.tier,got=drainDeposit(D,1.15);
|
| 3243 |
+
if(uteam[i]<2) resM[uteam[i]]=Math.min(RES_MCAP[uteam[i]],resM[uteam[i]]+got);
|
| 3244 |
+
if((tick+i)%9===0) sfx('laser',ux[i],uy[i],.42);
|
| 3245 |
+
if(uteam[i]===0&&D.tier!==before){
|
| 3246 |
+
toast(D.tier?'◇ MOBILE MINING — field dropped to Tier '+D.tier:'◇ MOBILE MINING — field depleted');
|
| 3247 |
+
sfx('notify',D.x,D.y,.7);
|
| 3248 |
+
}
|
| 3249 |
+
if(D.tier===0)redirectProspector(i);
|
| 3250 |
+
}
|
| 3251 |
+
return true;
|
| 3252 |
+
}
|
| 3253 |
function unitTick(dt){
|
| 3254 |
rebuildGrid();
|
| 3255 |
tickMoveCohorts();
|
| 3256 |
+
broodCriticalMassTick(dt);
|
| 3257 |
/* Patrol planning lives in input.js, which loads after the simulation. The
|
| 3258 |
guarded call preserves boot/replay tooling that evaluates sim.js alone. */
|
| 3259 |
if(typeof tickPatrolRoutes==='function')tickPatrolRoutes(dt);
|
|
|
|
| 3272 |
const T=TYPES[utype[i]];
|
| 3273 |
if(ucool[i]>0) ucool[i]-=dt;
|
| 3274 |
if(ubuff[i]>0) ubuff[i]-=dt;
|
| 3275 |
+
if(uclassBuffT[i]>0){uclassBuffT[i]-=dt;if(uclassBuffT[i]<=0){uclassBuffT[i]=0;uclassBuff[i]=0;}}
|
| 3276 |
+
if(ubroodLed[i]>0) ubroodLed[i]-=dt;
|
| 3277 |
if(uhaz[i]>0) uhaz[i]-=dt;
|
| 3278 |
if(ustomp[i]>0) ustomp[i]-=dt;
|
| 3279 |
if(ushielded[i]>0) ushielded[i]-=dt;
|
|
|
|
| 3326 |
} else if(uteam[i]===0 && (tick&63)===0) stallE=0.8;
|
| 3327 |
}
|
| 3328 |
}
|
| 3329 |
+
if(T.miner&&minerUnitTick(i,dt)) continue;
|
| 3330 |
// validate target
|
| 3331 |
let tg=utgt[i];
|
| 3332 |
if(tg>=0 && !foeTgt(i,tg,utgtg[i])){ tg=utgt[i]=-1; }
|
|
|
|
| 3340 |
else if((i+tick)%9===0&&perfScale>0.4)
|
| 3341 |
addParticle(1,ux[i],uy[i],rr(-4,4),rr(-14,-6),.5,3, 255,140,60);
|
| 3342 |
}
|
| 3343 |
+
const rngM=(uteam[i]===0?resRngMult:1)*modeRngMul(md)*classRngMul(i)*(uhaz[i]>0?HAZ_RNG:1);
|
| 3344 |
// staggered acquisition
|
| 3345 |
if(T.wk!=='n' && (i+tick)%acqMod===0 && ustate[i]!==1 && ustate[i]!==6){
|
| 3346 |
/* On the march, acquisition shrinks to self-defence range: the column
|
|
|
|
| 3393 |
// fire
|
| 3394 |
if(inRange && ucool[i]<=0 && !(T.minRng && er<T.minRng)){
|
| 3395 |
const vet=1+uvet[i]*0.15;
|
| 3396 |
+
ucool[i]=T.cool*modeCoolMul(md)*classCoolMul(i)*broodCoolMul(i)/(ubuff[i]>0?1.4:1);
|
| 3397 |
if(md===4){ umode[i]=0; umodeT[i]=MODE_SWITCH*0.4; } // firing breaks GHOST cover
|
| 3398 |
+
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);
|
| 3399 |
const ma=(T.tur?uturr[i]:uang[i])-Math.PI/2;
|
| 3400 |
const mx=ux[i]+Math.cos(ma)*T.size*0.62, my=uy[i]+Math.sin(ma)*T.size*0.62;
|
| 3401 |
if(T.wk==='m'){
|
|
|
|
| 3403 |
if(tg>=0){
|
| 3404 |
dealDamage(tg,dmg*dmgMul('m',utype[tg]),uteam[i],i,dmgMul('m',utype[tg]));
|
| 3405 |
if(T.aoe>0) forUnitsIn(ex,ey,T.aoe,j=>{ if(uteam[j]!==uteam[i]&&j!==tg) dealDamage(j,dmg*0.5*dmgMul('m',utype[j]),uteam[i],i); });
|
| 3406 |
+
} else if(isRelicTg(tg)) damageRelic(relics[relicOf(tg)],dmg*STM.m*(T.bldMul||1),uteam[i]);
|
| 3407 |
+
else damageBld(-2-tg,dmg*STM.m*(T.bldMul||1),uteam[i]);
|
| 3408 |
addParticle(0,ex,ey,0,0,.14,9, 255,190,90);
|
| 3409 |
addParticle(5,ex,ey,rr(-40,40),rr(-40,40),.25,4, 255,170,60);
|
| 3410 |
if((i&3)===0) sfx(unitIsBrood(i)?'cre_attack':(T.ptype===5?'flame':'hit'),ux[i],uy[i],1);
|
|
|
|
| 3431 |
if((i&3)===0) sfx(T.ptype===5?'flame':'laser',ux[i],uy[i],utype[i]===8?1.8:1);
|
| 3432 |
} else {
|
| 3433 |
const pmu=tg>=0?dmgMul(T.wk||'p',utype[tg]):(STM[T.wk||'p']||1);
|
| 3434 |
+
const pk=fireProj(T.ptype,uteam[i],mx,my,ex+rr(-3,3),ey+rr(-3,3),T.psp,dmg*pmu*(tg<=-2?(T.bldMul||1):1),T.aoe,tg);
|
| 3435 |
const commanderCannon=utype[i]===4;
|
| 3436 |
if(pk>=0){
|
| 3437 |
pmu0[pk]=pmu; pwk[pk]=T.wk||'p'; pCannon[pk]=commanderCannon?1:0;
|
|
|
|
| 3486 |
if(wantMove && distGoal>0.001){
|
| 3487 |
// ground units make better time on the old highways
|
| 3488 |
const rd=(!T.air&&!T.naval&&roadAt(ux[i],uy[i]))?ROAD_SPD:1;
|
| 3489 |
+
const sp=T.spd*spdM*classSpdMul(i)*broodSpdMul(i)*(ubuff[i]>0?1.35:1)*(uhaz[i]>0?HAZ_SPD:1)*rd*uCohesion[i]*Math.min(1,distGoal/18+0.25);
|
| 3490 |
// flow-field steering for long marches (routes armies around lakes)
|
| 3491 |
let ffOk=false;
|
| 3492 |
/* Formation orders share a coarse field to the leg centre, then fan out
|
|
|
|
| 3540 |
// apply with medium constraint: ground stays on land, ships stay on water, air flies anywhere
|
| 3541 |
const ox=ux[i], oy=uy[i];
|
| 3542 |
let nx=clamp(ox+mvx*dt,8,MAP-8), ny=clamp(oy+mvy*dt,8,MAP-8);
|
| 3543 |
+
if(typeof battlefieldClampPoint==='function'){
|
| 3544 |
+
const bp=battlefieldClampPoint(nx,ny,T.r+8);nx=bp[0];ny=bp[1];
|
| 3545 |
+
const goal=battlefieldClampPoint(utx[i],uty[i],T.r+8);utx[i]=goal[0];uty[i]=goal[1];
|
| 3546 |
+
}
|
| 3547 |
if(!T.air && (mvx||mvy)){
|
| 3548 |
const okAt=T.naval? ((X,Y)=>!isWalkable(X,Y)) : isWalkable;
|
| 3549 |
if(!okAt(nx,ny)){
|
|
|
|
| 3742 |
if(B.prog<1){
|
| 3743 |
/* FORCED TEMPO booster: construction only, and only for the player. */
|
| 3744 |
const doctrine=(B.team===0&&typeof defenseFocus!=='undefined'&&defenseFocus&&DEFT[B.type])?1.333:1;
|
| 3745 |
+
const tractor=B.tractorT>0?1+.22*Math.min(2,B.tractorN||1):1;
|
| 3746 |
const bs=(B.team===0?((typeof boostMul==='function'?boostMul('build'):1)*
|
| 3747 |
+
(typeof bldSpeedMult!=='undefined'?bldSpeedMult:1)*doctrine):1)*tractor;
|
| 3748 |
const wasProg=B.prog;
|
| 3749 |
B.prog=Math.min(1,B.prog+dt*bs/BT[B.type].bt);
|
| 3750 |
B.hp=Math.min(B.hpm,B.hp+B.hpm*0.9*dt/BT[B.type].bt);
|
| 3751 |
if((tick&7)===0) addParticle(2,B.x+rr(-B.r,B.r),B.y+rr(-B.r,B.r),rr(-4,4),rr(-10,-2),.3,3, 160,230,255);
|
| 3752 |
if(wasProg<1&&B.prog>=1){
|
| 3753 |
if(B.type==='techlab') B.shield=B.shieldMax;
|
| 3754 |
+
if(B.type==='mex') deployExtractorMiner(B);
|
| 3755 |
if(B.team===0) sfx('build',B.x,B.y,1);
|
| 3756 |
}
|
| 3757 |
continue;
|
|
|
|
| 3765 |
if(B.dmgT>0) B.dmgT-=dt;
|
| 3766 |
if(B.shieldT>0) B.shieldT-=dt;
|
| 3767 |
if(B.guardT>0) B.guardT-=dt;
|
| 3768 |
+
if(B.tractorT>0)B.tractorT-=dt;
|
| 3769 |
const bfac=bldFactionKey(B);
|
| 3770 |
if(B.team<2){ // fortification regen
|
| 3771 |
const F=fortOf(B.team);
|
|
|
|
| 4132 |
addParticle(1,B.x+rr(0.2,0.3)*szf,B.y-szf*0.34,rr(-2,2),rr(-10,-6),.9,5, 118,118,124);
|
| 4133 |
}
|
| 4134 |
const t=B.queue[0], T=TYPES[t];
|
| 4135 |
+
const tractor=B.tractorT>0?1+.22*Math.min(2,B.tractorN||1):1;
|
| 4136 |
+
const speed=(B.team===1?aiBuildMult:playerBuildMult)*(1+0.12*Math.min(2,B.adj||0))*fortOf(B.team).prod*tractor;
|
| 4137 |
const frac=dt*speed/T.bt;
|
| 4138 |
if(!payStream(B.team, T.cm*frac, T.ce*frac)){
|
| 4139 |
if(B.team===0){ if(resM[0]<T.cm*frac) stallM=0.8; if(resE[0]<T.ce*frac) stallE=0.8; }
|
src/intro.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ============================================================================
|
| 2 |
+
PRE-ALPHA TITLE REVEAL
|
| 3 |
+
----------------------------------------------------------------------------
|
| 4 |
+
The opening deliberately uses no faction dossier/contact-sheet imagery.
|
| 5 |
+
Those assets belong in the codex. Cinematic faction chapters will return
|
| 6 |
+
only when purpose-built key art exists for them.
|
| 7 |
+
============================================================================ */
|
| 8 |
+
(function(){
|
| 9 |
+
'use strict';
|
| 10 |
+
|
| 11 |
+
var SEEN_KEY='mf_prealpha_cinematic_v2';
|
| 12 |
+
var LEGACY_SEEN_KEY='mf_prealpha_cinematic_v1';
|
| 13 |
+
var el=null,timer=0,open=false,reduced=false,lastFocus=null,motionQuery=null;
|
| 14 |
+
var hadCareer=false;
|
| 15 |
+
try{
|
| 16 |
+
hadCareer=!!localStorage.getItem('massfront_profiles_v1')||
|
| 17 |
+
!!localStorage.getItem('massfront_meta_v1');
|
| 18 |
+
}catch(e){}
|
| 19 |
+
|
| 20 |
+
function version(){
|
| 21 |
+
try{
|
| 22 |
+
if(typeof currentRunningVersion==='function') return currentRunningVersion();
|
| 23 |
+
if(typeof APP_VERSION!=='undefined') return APP_VERSION;
|
| 24 |
+
if(typeof PACKAGED_REV!=='undefined') return PACKAGED_REV;
|
| 25 |
+
}catch(e){}
|
| 26 |
+
return 'PREVIEW';
|
| 27 |
+
}
|
| 28 |
+
function escapeHtml(s){
|
| 29 |
+
return String(s||'').replace(/[&<>"']/g,function(c){
|
| 30 |
+
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
function seen(){
|
| 34 |
+
try{return !!localStorage.getItem(SEEN_KEY)||!!localStorage.getItem(LEGACY_SEEN_KEY);}catch(e){return false;}
|
| 35 |
+
}
|
| 36 |
+
function markSeen(){
|
| 37 |
+
try{localStorage.setItem(SEEN_KEY,JSON.stringify({version:version(),seenAt:Date.now()}));}catch(e){}
|
| 38 |
+
}
|
| 39 |
+
function markup(){
|
| 40 |
+
return '<section id="mfPreAlphaIntro" class="mfTitleReveal" role="dialog" aria-modal="true" '+
|
| 41 |
+
'aria-labelledby="mfTitleWordmark" aria-describedby="mfTitleSubtitle" hidden>'+
|
| 42 |
+
'<div class="mfTitleBackdrop" aria-hidden="true"><div class="mfTitleGrid"></div><div class="mfTitleSweep"></div></div>'+
|
| 43 |
+
'<div class="mfTitleTop">'+
|
| 44 |
+
'<div class="mfTitleBuild"><b>PRE-ALPHA</b><span>v'+escapeHtml(version())+'</span></div>'+
|
| 45 |
+
'<button id="mfIntroSkip" class="mfTitleSkip" type="button">SKIP <span aria-hidden="true">✕</span></button>'+
|
| 46 |
+
'</div>'+
|
| 47 |
+
'<main class="mfTitleCenter">'+
|
| 48 |
+
'<div class="mfTitleInsignia"><img src="./assets/icons/icon-192.png" alt=""><i></i></div>'+
|
| 49 |
+
'<div class="mfTitleEyebrow">COMMAND UPLINK ESTABLISHED</div>'+
|
| 50 |
+
'<h1 id="mfTitleWordmark">MASSFRONT</h1>'+
|
| 51 |
+
'<div id="mfTitleSubtitle" class="mfTitleSubtitle"><i></i><span>SUPREME MOBILE WARFARE</span><i></i></div>'+
|
| 52 |
+
'<div class="mfTitleStatus"><span></span> THE FRONT AWAITS YOUR ORDERS</div>'+
|
| 53 |
+
'</main>'+
|
| 54 |
+
'<div class="mfTitleBottom">'+
|
| 55 |
+
'<button id="mfIntroStart" class="mfTitleStart" type="button"><span aria-hidden="true">▶</span> START NOW</button>'+
|
| 56 |
+
'</div>'+
|
| 57 |
+
'</section>';
|
| 58 |
+
}
|
| 59 |
+
function clearTimer(){if(timer){clearTimeout(timer);timer=0;}}
|
| 60 |
+
function scheduleClose(){
|
| 61 |
+
clearTimer();
|
| 62 |
+
if(!open||reduced) return;
|
| 63 |
+
timer=setTimeout(function(){closeIntro(false);},2800);
|
| 64 |
+
}
|
| 65 |
+
function closeIntro(startNow){
|
| 66 |
+
if(!el||!open) return;
|
| 67 |
+
markSeen();clearTimer();open=false;
|
| 68 |
+
el.classList.remove('open');el.setAttribute('aria-hidden','true');
|
| 69 |
+
document.body.classList.remove('mfIntroOpen');
|
| 70 |
+
var front=document.getElementById('startScreen');if(front) front.removeAttribute('aria-hidden');
|
| 71 |
+
setTimeout(function(){if(el&&!open) el.hidden=true;},260);
|
| 72 |
+
if(typeof initAudio==='function'){try{initAudio();}catch(e){}}
|
| 73 |
+
if(typeof sfx==='function'){try{sfx('ui');}catch(e){}}
|
| 74 |
+
var play=document.getElementById('startBtn');
|
| 75 |
+
if(startNow&&play) setTimeout(function(){play.click();},660);
|
| 76 |
+
else if(play) setTimeout(function(){try{play.focus({preventScroll:true});}catch(e){play.focus();}},90);
|
| 77 |
+
else if(lastFocus) lastFocus.focus();
|
| 78 |
+
}
|
| 79 |
+
function openIntro(force){
|
| 80 |
+
if(!el) return;
|
| 81 |
+
if(!force&&(hadCareer||seen())) return;
|
| 82 |
+
lastFocus=document.activeElement;open=true;el.hidden=false;el.removeAttribute('aria-hidden');
|
| 83 |
+
document.body.classList.add('mfIntroOpen');
|
| 84 |
+
var front=document.getElementById('startScreen');if(front) front.setAttribute('aria-hidden','true');
|
| 85 |
+
requestAnimationFrame(function(){
|
| 86 |
+
if(!open) return;el.classList.add('open');
|
| 87 |
+
var start=document.getElementById('mfIntroStart');if(start) start.focus({preventScroll:true});
|
| 88 |
+
});
|
| 89 |
+
scheduleClose();
|
| 90 |
+
}
|
| 91 |
+
function bindTap(node,fn){
|
| 92 |
+
if(typeof mfBindTap==='function') mfBindTap(node,fn);else node.addEventListener('click',fn);
|
| 93 |
+
}
|
| 94 |
+
function onKey(e){
|
| 95 |
+
if(!open) return;
|
| 96 |
+
if(e.key==='Escape'){e.preventDefault();closeIntro(false);return;}
|
| 97 |
+
if(e.key!=='Tab') return;
|
| 98 |
+
var buttons=Array.prototype.slice.call(el.querySelectorAll('button:not([disabled])')).filter(function(b){return b.offsetParent!==null;});
|
| 99 |
+
if(!buttons.length) return;
|
| 100 |
+
var first=buttons[0],last=buttons[buttons.length-1];
|
| 101 |
+
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}
|
| 102 |
+
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}
|
| 103 |
+
}
|
| 104 |
+
function addReplay(){
|
| 105 |
+
if(document.getElementById('mfIntroReplay')) return;
|
| 106 |
+
var front=document.getElementById('startScreen'),anchor=document.getElementById('updPanel');
|
| 107 |
+
if(!front) return;
|
| 108 |
+
var b=document.createElement('button');b.id='mfIntroReplay';b.className='mfIntroReplay';b.type='button';
|
| 109 |
+
b.innerHTML='<span aria-hidden="true">◉</span><b>PRE-ALPHA TITLE</b><small>REPLAY OPENING</small>';
|
| 110 |
+
bindTap(b,function(){openIntro(true);});
|
| 111 |
+
if(anchor) front.insertBefore(b,anchor);else front.appendChild(b);
|
| 112 |
+
}
|
| 113 |
+
function motionChanged(e){
|
| 114 |
+
reduced=!!e.matches;if(el) el.dataset.motion=reduced?'reduce':'full';
|
| 115 |
+
if(reduced) clearTimer();else scheduleClose();
|
| 116 |
+
}
|
| 117 |
+
function initIntro(){
|
| 118 |
+
if(el) return;
|
| 119 |
+
var host=document.createElement('div');host.innerHTML=markup();el=host.firstElementChild;document.body.appendChild(el);
|
| 120 |
+
motionQuery=window.matchMedia?matchMedia('(prefers-reduced-motion: reduce)'):null;
|
| 121 |
+
reduced=!!(motionQuery&&motionQuery.matches);el.dataset.motion=reduced?'reduce':'full';
|
| 122 |
+
if(motionQuery){
|
| 123 |
+
if(typeof motionQuery.addEventListener==='function') motionQuery.addEventListener('change',motionChanged);
|
| 124 |
+
else if(typeof motionQuery.addListener==='function') motionQuery.addListener(motionChanged);
|
| 125 |
+
}
|
| 126 |
+
bindTap(document.getElementById('mfIntroSkip'),function(){closeIntro(false);});
|
| 127 |
+
bindTap(document.getElementById('mfIntroStart'),function(){closeIntro(true);});
|
| 128 |
+
document.addEventListener('keydown',onKey,true);addReplay();
|
| 129 |
+
var force=false;try{force=new URLSearchParams(location.search).get('intro')==='1';}catch(e){}
|
| 130 |
+
setTimeout(function(){openIntro(force);},650);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
window.initIntro=initIntro;
|
| 134 |
+
window.showPreAlphaIntro=function(){openIntro(true);};
|
| 135 |
+
window.__mfIntroDebug=function(){return {open:open,phase:'title',stage:0,stageId:'title',reduced:reduced,
|
| 136 |
+
hadCareer:hadCareer,seen:seen(),wouldAuto:!hadCareer&&!seen(),version:version(),usesFactionArt:false};};
|
| 137 |
+
})();
|
src/main.js
CHANGED
|
@@ -37,20 +37,69 @@ const START_ZONES=[
|
|
| 37 |
{id:'se',n:'5',x:.82,y:.82},{id:'s', n:'6',x:.50,y:.85},
|
| 38 |
{id:'sw',n:'7',x:.18,y:.82},{id:'w', n:'8',x:.15,y:.50}
|
| 39 |
];
|
| 40 |
-
/* MAP is baked into terrain, pathfinding, fog and shaders.
|
| 41 |
-
runtime would desynchronise all four, so
|
| 42 |
-
|
| 43 |
-
|
|
|
|
| 44 |
const BATTLEFIELD_PRESETS={
|
| 45 |
-
compact:{nm:'COMPACT',
|
| 46 |
-
ds:'Close
|
| 47 |
-
standard:{nm:'STANDARD',km:'2.2 KM
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
ds:'
|
| 51 |
};
|
| 52 |
-
let battlefieldPreset='
|
| 53 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
let playerStartZone='sw', spawnPick='player';
|
| 55 |
let aiSlots=[
|
| 56 |
{on:true, diff:1,zone:'ne'},
|
|
@@ -189,11 +238,13 @@ function resetWorld(){
|
|
| 189 |
if(typeof mmPings!=='undefined') mmPings.length=0;
|
| 190 |
meteors.length=0; stormTimer=200;
|
| 191 |
fields.length=0; ffNext=0; ufield.fill(-1); ushielded.fill(0); umarch.fill(0);
|
|
|
|
| 192 |
researched={}; resDone=0; researchCarry={}; resHpMult=1; resRngMult=1; resEnergyMult=1;
|
| 193 |
resBldHpMult=1; resDefDmgMult=1; labBufferMult=1;
|
| 194 |
deformQ.length=0; musicInt=0; lastDmgTotal=0; dmgAccum=[0,0,0]; dayT=0.08; shLife.fill(0);
|
| 195 |
abCool=[0,0,0,0]; abUnlock=[true,false,false,false]; aiming=-1;
|
| 196 |
if(typeof artBarrageReset==='function') artBarrageReset();
|
|
|
|
| 197 |
salvageMult=1; bldHpMult=1; matchClock=timeLimit; goalDone=false;
|
| 198 |
infestT=230; nestSpreadT=170; infestLvl=0; tideT=260; birds.length=0; bugQ.length=0;
|
| 199 |
if(typeof hazReset==='function') hazReset(); // map hazard clock
|
|
@@ -283,7 +334,7 @@ function newSkirmish(){
|
|
| 283 |
}
|
| 284 |
let bd=1e12,bi=-1;
|
| 285 |
for(let d=0;d<deposits.length;d++){ const D=deposits[d];
|
| 286 |
-
if(D.taken) continue; const dd=dist2(D.x,D.y,exs,eys); if(dd<bd){bd=dd;bi=d;} }
|
| 287 |
if(bi>=0){ deposits[bi].taken=true; addBld('mex',1,deposits[bi].x,deposits[bi].y,true); }
|
| 288 |
aiBases.push({x:egx,y:egy,diff:S.diff,slot:S.slot,commander:h});
|
| 289 |
aiDeployArrivals.push({x:egx,y:egy,fac:AI.fac,ang:a+Math.PI,depart:0});
|
|
@@ -507,7 +558,9 @@ function endGame(win,reason){
|
|
| 507 |
/* The endgame layer scores the run, advances the threat ladder, ticks the
|
| 508 |
mastery grid and records a weekly best — all from what actually happened. */
|
| 509 |
const dv=demoMode?null:(typeof developRecord==='function'?developRecord({
|
| 510 |
-
win:!!win, kills:stats.kills[0]|0, built:(stats.built[0])|0, nests:stats.nests|0
|
|
|
|
|
|
|
| 511 |
const eg=demoMode?null:(typeof endgameRecord==='function'?endgameRecord({
|
| 512 |
win:!!win, kills:stats.kills[0]|0, built:(stats.built[0])|0,
|
| 513 |
seconds:stats.t|0, difficulty:difficulty|0}):null);
|
|
@@ -531,6 +584,7 @@ function endGame(win,reason){
|
|
| 531 |
const fld=rw.field||{mass:0,energy:0,reclaimed:0};
|
| 532 |
const mats=dv?Object.keys(dv.mats).filter(k=>dv.mats[k]>0).map(k=>
|
| 533 |
'<div class="goRes"><b>'+MATS[k].em+' '+dv.mats[k]+'</b><span>'+MATS[k].nm.toUpperCase()+'</span></div>').join(''):'';
|
|
|
|
| 534 |
let loot='';
|
| 535 |
if(rw.loot&&rw.loot.gear){
|
| 536 |
const g=rw.loot.gear, rr=invRarity(g.rarity);
|
|
@@ -545,11 +599,14 @@ function endGame(win,reason){
|
|
| 545 |
+(rw.mult>1.001?'<small>×'+rw.mult.toFixed(2)+' OPERATION MULTIPLIER</small>':'')+'</h3>'
|
| 546 |
+'<div class="goPayout"><div><b>+'+rw.xp+'</b><span>XP</span></div><div><b>+'+rw.cores+'</b><span>⬡ CORES</span></div>'
|
| 547 |
+'<div><b>+'+(rw.data||0)+'</b><span>◆ RESEARCH DATA</span></div></div>'
|
| 548 |
-
+'<div class="goBreak">'+rw.parts.map(p=>'<span>'+p[0]+' <b>+'+p[1]+'</b></span>').join('')+'</div>
|
|
|
|
| 549 |
+'<section class="goSection"><h3>FIELD RECOVERY</h3><div class="goResourceGrid">'
|
| 550 |
+'<div class="goRes"><b>'+fld.mass.toLocaleString()+'</b><span>MASS IN RESERVE</span></div>'
|
| 551 |
+'<div class="goRes"><b>'+fld.energy.toLocaleString()+'</b><span>ENERGY IN RESERVE</span></div>'
|
| 552 |
-
+'<div class="goRes"><b>'+fld.reclaimed.toLocaleString()+'</b><span>MASS RECLAIMED</span></div>
|
|
|
|
|
|
|
| 553 |
+(loot?'<section class="goSection"><h3>ITEMS ACQUIRED</h3>'+loot+'</section>':'')
|
| 554 |
+(rw.rankUp?'<div class="goNotice good">◈ PROMOTED — '+rw.rankUp.em+' '+rw.rankUp.nm.toUpperCase()+'</div>':'')
|
| 555 |
+(eg&&eg.msgs.length?'<div class="goNotice">'+eg.msgs.join('<br>')+'</div>':'')
|
|
@@ -938,9 +995,9 @@ function wire(){
|
|
| 938 |
pick('.pcbtn',b=>{ resPace=+b.dataset.p; });
|
| 939 |
pick('.crbtn',b=>{ crateRate=crateRateBase=+b.dataset.c; });
|
| 940 |
pick('.bsbtn',b=>{
|
| 941 |
-
battlefieldPreset=
|
| 942 |
-
const h=$('battleScaleHint')
|
| 943 |
-
if(h) h.textContent=
|
| 944 |
renderSpawnPlanner();
|
| 945 |
});
|
| 946 |
pick('.ifbtn',b=>{
|
|
@@ -956,13 +1013,16 @@ function wire(){
|
|
| 956 |
:'Classic RTS balance between mobile armies and static defences.';
|
| 957 |
});
|
| 958 |
initSpawnPlanner();
|
| 959 |
-
/* Battle setup is split into
|
|
|
|
|
|
|
| 960 |
useful for experienced players who want to audit an entire custom match. */
|
| 961 |
const setup=$('setupScr');setup.dataset.tab=setup.dataset.tab||'map';
|
| 962 |
document.querySelectorAll('.setupTabBtn').forEach(b=>mfBindTap(b,()=>{
|
| 963 |
setup.dataset.tab=b.dataset.tab;setup.classList.remove('setupAll');
|
| 964 |
document.querySelectorAll('.setupTabBtn').forEach(x=>x.classList.toggle('on',x===b));
|
| 965 |
$('advToggle').classList.remove('on');$('advToggle').textContent='SHOW ALL';sfx('ui');
|
|
|
|
| 966 |
const sc=setup.querySelector('.setupScroll');if(sc)sc.scrollTop=0;
|
| 967 |
}));
|
| 968 |
$('advToggle').addEventListener('pointerdown',()=>{
|
|
@@ -978,9 +1038,14 @@ function wire(){
|
|
| 978 |
if(typeof renderFacRow==='function') renderFacRow();
|
| 979 |
document.querySelectorAll('.wbtn').forEach(b=>{
|
| 980 |
b.addEventListener('pointerdown',()=>{
|
|
|
|
| 981 |
document.querySelectorAll('.wbtn').forEach(x=>x.classList.remove('on'));
|
| 982 |
b.classList.add('on'); wcChoice=+b.dataset.w;
|
|
|
|
|
|
|
|
|
|
| 983 |
META.wcPref=wcChoice; metaSave(); initAudio(); sfx('ui');
|
|
|
|
| 984 |
});
|
| 985 |
});
|
| 986 |
mfBindTap($('armoryBtn'),()=>{
|
|
@@ -1001,6 +1066,7 @@ function wire(){
|
|
| 1001 |
});
|
| 1002 |
mfBindTap($('startBtn'),()=>{ initAudio(); sfx('ui');
|
| 1003 |
renderMapRow(); renderSpawnPlanner();
|
|
|
|
| 1004 |
showFrontScreen('setupScr'); });
|
| 1005 |
mfBindTap($('setupBack'),()=>{ sfx('ui');
|
| 1006 |
renderMetaHead(); showFrontScreen('startScreen'); });
|
|
@@ -1028,7 +1094,11 @@ function wire(){
|
|
| 1028 |
mfBindTap($('restartBtn'),returnToMainMenu);
|
| 1029 |
mfBindTap($('menuBtn'),()=>{ if(!running) return; paused=true; $('pauseOverlay').style.display='flex'; });
|
| 1030 |
mfBindTap($('resumeBtn'),()=>{ paused=false; $('pauseOverlay').style.display='none'; });
|
| 1031 |
-
mfBindTap($('quitBtn'),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1032 |
mfBindTap($('pauseSettings'),()=>{ openSettings('pause'); });
|
| 1033 |
mfBindTap($('setBack'),()=>{
|
| 1034 |
$('settingsScr').style.display='none'; sfx('ui');
|
|
@@ -1112,7 +1182,10 @@ function wire(){
|
|
| 1112 |
if(!B.alive) return;
|
| 1113 |
resM[0]=Math.min(RES_MCAP[0],resM[0]+Math.round(BT[B.type].cm*0.5));
|
| 1114 |
B.alive=false; rebuildBGrid();
|
| 1115 |
-
if(B.type==='mex')
|
|
|
|
|
|
|
|
|
|
| 1116 |
if(B.type==='geo') for(const G of geysers) if(G.x===B.x&&G.y===B.y) G.taken=false;
|
| 1117 |
addParticle(3,B.x,B.y,0,0,.5,BT[B.type].size, 120,230,255);
|
| 1118 |
toast('♻ '+BT[B.type].name+' sold');
|
|
@@ -1194,6 +1267,7 @@ function wire(){
|
|
| 1194 |
$('abRage').addEventListener('pointerdown',()=>tryAbility(2));
|
| 1195 |
$('abLance').addEventListener('pointerdown',()=>tryAbility(3));
|
| 1196 |
$('abBarrage').addEventListener('pointerdown',()=>tryArtilleryBarrage());
|
|
|
|
| 1197 |
$('abHero').addEventListener('pointerdown',()=>selectHero());
|
| 1198 |
mfBindTap($('heroBar'),()=>{
|
| 1199 |
selectHero();
|
|
@@ -1226,7 +1300,9 @@ function boot(){
|
|
| 1226 |
if(su.d!=null&&su.d>=0&&su.d<=2) difficulty=su.d;
|
| 1227 |
if(THEMES[su.t]) curTheme=su.t;
|
| 1228 |
if(MAPDEFS[su.m]) curMap=su.m;
|
| 1229 |
-
|
|
|
|
|
|
|
| 1230 |
if(su.f&&(su.f==='random'||FACTIONS[su.f])) aiFactionSel=su.f;
|
| 1231 |
if(su.g&&GOALS.some(g=>g.id===su.g)) goalSel=su.g;
|
| 1232 |
if(su.tl!=null) timeLimit=+su.tl||0;
|
|
@@ -1255,8 +1331,8 @@ function boot(){
|
|
| 1255 |
document.querySelectorAll('.pcbtn').forEach(b=>b.classList.toggle('on',+b.dataset.p===resPace));
|
| 1256 |
document.querySelectorAll('.crbtn').forEach(b=>b.classList.toggle('on',+b.dataset.c===crateRate));
|
| 1257 |
document.querySelectorAll('.bsbtn').forEach(b=>b.classList.toggle('on',b.dataset.bs===battlefieldPreset));
|
| 1258 |
-
const bsh=$('battleScaleHint')
|
| 1259 |
-
if(bsh) bsh.textContent=
|
| 1260 |
document.querySelectorAll('.ifbtn').forEach(b=>b.classList.toggle('on',!!+b.dataset.i===infestationOn));
|
| 1261 |
const ifh=$('infestHint'); if(ifh) ifh.textContent=infestationOn
|
| 1262 |
?'Neutral nests spread and erupt during the battle.'
|
|
@@ -1329,7 +1405,7 @@ function boot(){
|
|
| 1329 |
if(typeof storyRefreshBadge==='function') storyRefreshBadge(); sfx('ui'); });
|
| 1330 |
/* New subsystems. Each lives in its own module and registers its own UI, so
|
| 1331 |
none of them need to touch index.html or this file beyond this line. */
|
| 1332 |
-
for(const fn of ['initOffline','initSampleAudio','initAssetPacks','initAuthPortal','initTutorial','initAdBoards','initStoreUI','initResTree3D','initEconomyNet'])
|
| 1333 |
if(typeof window[fn]==='function'){ try{ window[fn](); }catch(e){ console.error(fn,e); } }
|
| 1334 |
/* The auth portal loads its cached AP_SESSION synchronously. Initialise the
|
| 1335 |
Profile account/save panel afterwards so it renders that real session,
|
|
|
|
| 37 |
{id:'se',n:'5',x:.82,y:.82},{id:'s', n:'6',x:.50,y:.85},
|
| 38 |
{id:'sw',n:'7',x:.18,y:.82},{id:'w', n:'8',x:.15,y:.50}
|
| 39 |
];
|
| 40 |
+
/* MAP is baked into terrain, pathfinding, fog and shaders. Resizing that buffer
|
| 41 |
+
at runtime would desynchronise all four, so map size defines a playable
|
| 42 |
+
theatre inside the fixed 2.6 km terrain. Spawn separation, neutral resource
|
| 43 |
+
bounds and resource counts all use the same theatre scale; this produces a
|
| 44 |
+
genuinely faster Compact match without allocating a second terrain stack. */
|
| 45 |
const BATTLEFIELD_PRESETS={
|
| 46 |
+
compact:{nm:'COMPACT',km:'1.9 KM',dur:'8\u201312 MIN',world:.72,start:.72,spread:.84,nodes:14,geysers:1,site:.82,haz:.88,
|
| 47 |
+
ds:'Close spawns, concentrated resource fields and quick clashes.'},
|
| 48 |
+
standard:{nm:'STANDARD',km:'2.2 KM',dur:'12\u201318 MIN',world:.86,start:.86,spread:1,nodes:18,geysers:2,site:1,haz:1,
|
| 49 |
+
ds:'Balanced travel, expansion space and contested resources.'},
|
| 50 |
+
large:{nm:'LARGE',km:'2.6 KM',dur:'18\u201325 MIN',world:1,start:1,spread:1.16,nodes:22,geysers:3,site:1.18,haz:1.12,
|
| 51 |
+
ds:'Maximum separation, additional fields and room for multi-front wars.'}
|
| 52 |
};
|
| 53 |
+
let battlefieldPreset='standard';
|
| 54 |
+
function battlefieldPresetKey(key){ return key==='grand'?'large':(BATTLEFIELD_PRESETS[key]?key:'standard'); }
|
| 55 |
+
function battlefieldPresetDef(){ return BATTLEFIELD_PRESETS[battlefieldPresetKey(battlefieldPreset)]; }
|
| 56 |
+
function battlefieldPresetHint(){
|
| 57 |
+
const P=battlefieldPresetDef();
|
| 58 |
+
return P.km+' \u00b7 EST. '+P.dur+' \u2014 '+P.ds;
|
| 59 |
+
}
|
| 60 |
+
function battlefieldPlayBounds(pad){
|
| 61 |
+
const P=battlefieldPresetDef(),span=MAP*(P.world||1),inset=(MAP-span)*.5;
|
| 62 |
+
const lo=inset+(pad||0),hi=MAP-inset-(pad||0);
|
| 63 |
+
return lo<hi?{lo,hi,span}:{lo:MAP*.5-40,hi:MAP*.5+40,span:80};
|
| 64 |
+
}
|
| 65 |
+
/* THEATRE SILHOUETTES.
|
| 66 |
+
Map size still owns one stable square allocation, but the playable edge is
|
| 67 |
+
not the same square stamped onto every biome. Each theme receives a
|
| 68 |
+
deterministic superellipse plus low-frequency authored inlets/notches.
|
| 69 |
+
The same formula is duplicated in the terrain vertex shader and sampled by
|
| 70 |
+
the holographic border, so gameplay, red line and exclusion art agree. */
|
| 71 |
+
function battlefieldShapeStyle(){
|
| 72 |
+
return curMap==='isles'?1:(curTheme==='ashland'||curMap==='crater'?2:
|
| 73 |
+
(curTheme==='arctic'||curMap==='highland'?3:0));
|
| 74 |
+
}
|
| 75 |
+
function battlefieldShapeRadius(angle,pad){
|
| 76 |
+
const B=battlefieldPlayBounds(0),half=Math.max(45,(B.hi-B.lo)*.5-(pad||0));
|
| 77 |
+
const style=battlefieldShapeStyle(),ca=Math.abs(Math.cos(angle)),sa=Math.abs(Math.sin(angle));
|
| 78 |
+
const power=style===1?3.15:style===2?6.25:style===3?4.15:5.0;
|
| 79 |
+
const base=half/Math.pow(Math.pow(ca,power)+Math.pow(sa,power),1/power);
|
| 80 |
+
let shape;
|
| 81 |
+
if(style===1) shape=.945+.024*Math.sin(angle*3+.65)+.016*Math.sin(angle*7-1.10);
|
| 82 |
+
else if(style===2) shape=.958+.020*Math.sin(angle*5+.30)+.012*Math.sin(angle*9+1.20);
|
| 83 |
+
else if(style===3) shape=.948+.025*Math.sin(angle*2-.80)+.016*Math.sin(angle*6+.45);
|
| 84 |
+
else shape=.966+.014*Math.sin(angle*4+.35)+.010*Math.sin(angle*7-.55);
|
| 85 |
+
return Math.max(36,base*shape);
|
| 86 |
+
}
|
| 87 |
+
function battlefieldSignedDistance(x,y,pad){
|
| 88 |
+
const B=battlefieldPlayBounds(0),cx=(B.lo+B.hi)*.5,cy=cx,dx=x-cx,dy=y-cy;
|
| 89 |
+
const r=Math.hypot(dx,dy),a=r>0.001?Math.atan2(dy,dx):0;
|
| 90 |
+
return battlefieldShapeRadius(a,pad)-r;
|
| 91 |
+
}
|
| 92 |
+
function battlefieldContains(x,y,pad){ return battlefieldSignedDistance(x,y,pad)>=0; }
|
| 93 |
+
function battlefieldClampPoint(x,y,pad){
|
| 94 |
+
const B=battlefieldPlayBounds(0),cx=(B.lo+B.hi)*.5,cy=cx,dx=x-cx,dy=y-cy,r=Math.hypot(dx,dy);
|
| 95 |
+
const a=r>0.001?Math.atan2(dy,dx):0,limit=battlefieldShapeRadius(a,pad);
|
| 96 |
+
if(r<=limit)return [x,y];
|
| 97 |
+
const q=limit/Math.max(.001,r);return [cx+dx*q,cy+dy*q];
|
| 98 |
+
}
|
| 99 |
+
function battlefieldBoundaryPoint(angle,pad){
|
| 100 |
+
const B=battlefieldPlayBounds(0),c=(B.lo+B.hi)*.5,r=battlefieldShapeRadius(angle,pad);
|
| 101 |
+
return [c+Math.cos(angle)*r,c+Math.sin(angle)*r];
|
| 102 |
+
}
|
| 103 |
let playerStartZone='sw', spawnPick='player';
|
| 104 |
let aiSlots=[
|
| 105 |
{on:true, diff:1,zone:'ne'},
|
|
|
|
| 238 |
if(typeof mmPings!=='undefined') mmPings.length=0;
|
| 239 |
meteors.length=0; stormTimer=200;
|
| 240 |
fields.length=0; ffNext=0; ufield.fill(-1); ushielded.fill(0); umarch.fill(0);
|
| 241 |
+
ubroodLed.fill(0);uMineT.fill(0);uMineNode.fill(-1);broodMassT=0;
|
| 242 |
researched={}; resDone=0; researchCarry={}; resHpMult=1; resRngMult=1; resEnergyMult=1;
|
| 243 |
resBldHpMult=1; resDefDmgMult=1; labBufferMult=1;
|
| 244 |
deformQ.length=0; musicInt=0; lastDmgTotal=0; dmgAccum=[0,0,0]; dayT=0.08; shLife.fill(0);
|
| 245 |
abCool=[0,0,0,0]; abUnlock=[true,false,false,false]; aiming=-1;
|
| 246 |
if(typeof artBarrageReset==='function') artBarrageReset();
|
| 247 |
+
if(typeof classAbilityReset==='function') classAbilityReset();
|
| 248 |
salvageMult=1; bldHpMult=1; matchClock=timeLimit; goalDone=false;
|
| 249 |
infestT=230; nestSpreadT=170; infestLvl=0; tideT=260; birds.length=0; bugQ.length=0;
|
| 250 |
if(typeof hazReset==='function') hazReset(); // map hazard clock
|
|
|
|
| 334 |
}
|
| 335 |
let bd=1e12,bi=-1;
|
| 336 |
for(let d=0;d<deposits.length;d++){ const D=deposits[d];
|
| 337 |
+
if(D.taken||depositTier(D)<=0) continue; const dd=dist2(D.x,D.y,exs,eys); if(dd<bd){bd=dd;bi=d;} }
|
| 338 |
if(bi>=0){ deposits[bi].taken=true; addBld('mex',1,deposits[bi].x,deposits[bi].y,true); }
|
| 339 |
aiBases.push({x:egx,y:egy,diff:S.diff,slot:S.slot,commander:h});
|
| 340 |
aiDeployArrivals.push({x:egx,y:egy,fac:AI.fac,ang:a+Math.PI,depart:0});
|
|
|
|
| 558 |
/* The endgame layer scores the run, advances the threat ladder, ticks the
|
| 559 |
mastery grid and records a weekly best — all from what actually happened. */
|
| 560 |
const dv=demoMode?null:(typeof developRecord==='function'?developRecord({
|
| 561 |
+
win:!!win, kills:stats.kills[0]|0, built:(stats.built[0])|0, nests:stats.nests|0,
|
| 562 |
+
fieldMass:Math.max(0,Math.floor(resM[0]||0)),fieldEnergy:Math.max(0,Math.floor(resE[0]||0)),
|
| 563 |
+
reclaimed:Math.max(0,Math.round(stats.reclaimed||0))}):null);
|
| 564 |
const eg=demoMode?null:(typeof endgameRecord==='function'?endgameRecord({
|
| 565 |
win:!!win, kills:stats.kills[0]|0, built:(stats.built[0])|0,
|
| 566 |
seconds:stats.t|0, difficulty:difficulty|0}):null);
|
|
|
|
| 584 |
const fld=rw.field||{mass:0,energy:0,reclaimed:0};
|
| 585 |
const mats=dv?Object.keys(dv.mats).filter(k=>dv.mats[k]>0).map(k=>
|
| 586 |
'<div class="goRes"><b>'+MATS[k].em+' '+dv.mats[k]+'</b><span>'+MATS[k].nm.toUpperCase()+'</span></div>').join(''):'';
|
| 587 |
+
const dataBreak=(rw.dataParts||[]).map(p=>'<span>'+p[0]+' <b>+'+p[1]+'</b></span>').join('');
|
| 588 |
let loot='';
|
| 589 |
if(rw.loot&&rw.loot.gear){
|
| 590 |
const g=rw.loot.gear, rr=invRarity(g.rarity);
|
|
|
|
| 599 |
+(rw.mult>1.001?'<small>×'+rw.mult.toFixed(2)+' OPERATION MULTIPLIER</small>':'')+'</h3>'
|
| 600 |
+'<div class="goPayout"><div><b>+'+rw.xp+'</b><span>XP</span></div><div><b>+'+rw.cores+'</b><span>⬡ CORES</span></div>'
|
| 601 |
+'<div><b>+'+(rw.data||0)+'</b><span>◆ RESEARCH DATA</span></div></div>'
|
| 602 |
+
+'<div class="goBreak">'+rw.parts.map(p=>'<span>'+p[0]+' <b>+'+p[1]+'</b></span>').join('')+'</div>'
|
| 603 |
+
+(dataBreak?'<div class="goBreak research"><strong>RESEARCH DATA</strong>'+dataBreak+'</div>':'')+'</section>'
|
| 604 |
+'<section class="goSection"><h3>FIELD RECOVERY</h3><div class="goResourceGrid">'
|
| 605 |
+'<div class="goRes"><b>'+fld.mass.toLocaleString()+'</b><span>MASS IN RESERVE</span></div>'
|
| 606 |
+'<div class="goRes"><b>'+fld.energy.toLocaleString()+'</b><span>ENERGY IN RESERVE</span></div>'
|
| 607 |
+
+'<div class="goRes"><b>'+fld.reclaimed.toLocaleString()+'</b><span>MASS RECLAIMED</span></div></div>'
|
| 608 |
+
+(mats?'<h4 class="goAccountSalvage">ACCOUNT SALVAGE <small>Persisted to Development inventory</small></h4>'
|
| 609 |
+
+'<div class="goResourceGrid account">'+mats+'</div>':'')+'</section>'
|
| 610 |
+(loot?'<section class="goSection"><h3>ITEMS ACQUIRED</h3>'+loot+'</section>':'')
|
| 611 |
+(rw.rankUp?'<div class="goNotice good">◈ PROMOTED — '+rw.rankUp.em+' '+rw.rankUp.nm.toUpperCase()+'</div>':'')
|
| 612 |
+(eg&&eg.msgs.length?'<div class="goNotice">'+eg.msgs.join('<br>')+'</div>':'')
|
|
|
|
| 995 |
pick('.pcbtn',b=>{ resPace=+b.dataset.p; });
|
| 996 |
pick('.crbtn',b=>{ crateRate=crateRateBase=+b.dataset.c; });
|
| 997 |
pick('.bsbtn',b=>{
|
| 998 |
+
battlefieldPreset=battlefieldPresetKey(b.dataset.bs);
|
| 999 |
+
const h=$('battleScaleHint');
|
| 1000 |
+
if(h) h.textContent=battlefieldPresetHint();
|
| 1001 |
renderSpawnPlanner();
|
| 1002 |
});
|
| 1003 |
pick('.ifbtn',b=>{
|
|
|
|
| 1013 |
:'Classic RTS balance between mobile armies and static defences.';
|
| 1014 |
});
|
| 1015 |
initSpawnPlanner();
|
| 1016 |
+
/* Battle setup is split into five short task-focused pages. Match modifiers
|
| 1017 |
+
live here because they change one deployment, not the Operations hub.
|
| 1018 |
+
SHOW ALL remains
|
| 1019 |
useful for experienced players who want to audit an entire custom match. */
|
| 1020 |
const setup=$('setupScr');setup.dataset.tab=setup.dataset.tab||'map';
|
| 1021 |
document.querySelectorAll('.setupTabBtn').forEach(b=>mfBindTap(b,()=>{
|
| 1022 |
setup.dataset.tab=b.dataset.tab;setup.classList.remove('setupAll');
|
| 1023 |
document.querySelectorAll('.setupTabBtn').forEach(x=>x.classList.toggle('on',x===b));
|
| 1024 |
$('advToggle').classList.remove('on');$('advToggle').textContent='SHOW ALL';sfx('ui');
|
| 1025 |
+
if(b.dataset.tab==='modifiers'&&typeof renderOps==='function') renderOps();
|
| 1026 |
const sc=setup.querySelector('.setupScroll');if(sc)sc.scrollTop=0;
|
| 1027 |
}));
|
| 1028 |
$('advToggle').addEventListener('pointerdown',()=>{
|
|
|
|
| 1038 |
if(typeof renderFacRow==='function') renderFacRow();
|
| 1039 |
document.querySelectorAll('.wbtn').forEach(b=>{
|
| 1040 |
b.addEventListener('pointerdown',()=>{
|
| 1041 |
+
if(b.disabled) return;
|
| 1042 |
document.querySelectorAll('.wbtn').forEach(x=>x.classList.remove('on'));
|
| 1043 |
b.classList.add('on'); wcChoice=+b.dataset.w;
|
| 1044 |
+
/* Random quick-picks and hand-selected modifiers are intentionally
|
| 1045 |
+
exclusive, so neither control can silently override the other. */
|
| 1046 |
+
META.opmods={};
|
| 1047 |
META.wcPref=wcChoice; metaSave(); initAudio(); sfx('ui');
|
| 1048 |
+
if(typeof renderOps==='function') renderOps();
|
| 1049 |
});
|
| 1050 |
});
|
| 1051 |
mfBindTap($('armoryBtn'),()=>{
|
|
|
|
| 1066 |
});
|
| 1067 |
mfBindTap($('startBtn'),()=>{ initAudio(); sfx('ui');
|
| 1068 |
renderMapRow(); renderSpawnPlanner();
|
| 1069 |
+
if(typeof renderOps==='function') renderOps();
|
| 1070 |
showFrontScreen('setupScr'); });
|
| 1071 |
mfBindTap($('setupBack'),()=>{ sfx('ui');
|
| 1072 |
renderMetaHead(); showFrontScreen('startScreen'); });
|
|
|
|
| 1094 |
mfBindTap($('restartBtn'),returnToMainMenu);
|
| 1095 |
mfBindTap($('menuBtn'),()=>{ if(!running) return; paused=true; $('pauseOverlay').style.display='flex'; });
|
| 1096 |
mfBindTap($('resumeBtn'),()=>{ paused=false; $('pauseOverlay').style.display='none'; });
|
| 1097 |
+
mfBindTap($('quitBtn'),()=>{
|
| 1098 |
+
const go=()=>returnToMainMenu();
|
| 1099 |
+
if(typeof accConfirm==='function') accConfirm('Abandon this operation? The unfinished match grants no mission payout.',go);
|
| 1100 |
+
else go();
|
| 1101 |
+
});
|
| 1102 |
mfBindTap($('pauseSettings'),()=>{ openSettings('pause'); });
|
| 1103 |
mfBindTap($('setBack'),()=>{
|
| 1104 |
$('settingsScr').style.display='none'; sfx('ui');
|
|
|
|
| 1182 |
if(!B.alive) return;
|
| 1183 |
resM[0]=Math.min(RES_MCAP[0],resM[0]+Math.round(BT[B.type].cm*0.5));
|
| 1184 |
B.alive=false; rebuildBGrid();
|
| 1185 |
+
if(B.type==='mex'){
|
| 1186 |
+
if(B.dep>=0) redirectProspectorsFromNode(B.dep,B.team);
|
| 1187 |
+
for(const D of deposits) if(D.x===B.x&&D.y===B.y) D.taken=false;
|
| 1188 |
+
}
|
| 1189 |
if(B.type==='geo') for(const G of geysers) if(G.x===B.x&&G.y===B.y) G.taken=false;
|
| 1190 |
addParticle(3,B.x,B.y,0,0,.5,BT[B.type].size, 120,230,255);
|
| 1191 |
toast('♻ '+BT[B.type].name+' sold');
|
|
|
|
| 1267 |
$('abRage').addEventListener('pointerdown',()=>tryAbility(2));
|
| 1268 |
$('abLance').addEventListener('pointerdown',()=>tryAbility(3));
|
| 1269 |
$('abBarrage').addEventListener('pointerdown',()=>tryArtilleryBarrage());
|
| 1270 |
+
if($('abClass')) $('abClass').addEventListener('pointerdown',()=>tryClassAbility());
|
| 1271 |
$('abHero').addEventListener('pointerdown',()=>selectHero());
|
| 1272 |
mfBindTap($('heroBar'),()=>{
|
| 1273 |
selectHero();
|
|
|
|
| 1300 |
if(su.d!=null&&su.d>=0&&su.d<=2) difficulty=su.d;
|
| 1301 |
if(THEMES[su.t]) curTheme=su.t;
|
| 1302 |
if(MAPDEFS[su.m]) curMap=su.m;
|
| 1303 |
+
/* `grand` was the pre-1.32 name for Large. Keep old setup saves valid while
|
| 1304 |
+
storing only the player-facing Compact / Standard / Large vocabulary. */
|
| 1305 |
+
if(BATTLEFIELD_PRESETS[su.bs]||su.bs==='grand') battlefieldPreset=battlefieldPresetKey(su.bs);
|
| 1306 |
if(su.f&&(su.f==='random'||FACTIONS[su.f])) aiFactionSel=su.f;
|
| 1307 |
if(su.g&&GOALS.some(g=>g.id===su.g)) goalSel=su.g;
|
| 1308 |
if(su.tl!=null) timeLimit=+su.tl||0;
|
|
|
|
| 1331 |
document.querySelectorAll('.pcbtn').forEach(b=>b.classList.toggle('on',+b.dataset.p===resPace));
|
| 1332 |
document.querySelectorAll('.crbtn').forEach(b=>b.classList.toggle('on',+b.dataset.c===crateRate));
|
| 1333 |
document.querySelectorAll('.bsbtn').forEach(b=>b.classList.toggle('on',b.dataset.bs===battlefieldPreset));
|
| 1334 |
+
const bsh=$('battleScaleHint');
|
| 1335 |
+
if(bsh) bsh.textContent=battlefieldPresetHint();
|
| 1336 |
document.querySelectorAll('.ifbtn').forEach(b=>b.classList.toggle('on',!!+b.dataset.i===infestationOn));
|
| 1337 |
const ifh=$('infestHint'); if(ifh) ifh.textContent=infestationOn
|
| 1338 |
?'Neutral nests spread and erupt during the battle.'
|
|
|
|
| 1405 |
if(typeof storyRefreshBadge==='function') storyRefreshBadge(); sfx('ui'); });
|
| 1406 |
/* New subsystems. Each lives in its own module and registers its own UI, so
|
| 1407 |
none of them need to touch index.html or this file beyond this line. */
|
| 1408 |
+
for(const fn of ['initOffline','initSampleAudio','initAssetPacks','initAuthPortal','initTutorial','initAdBoards','initStoreUI','initResTree3D','initEconomyNet','initIntro'])
|
| 1409 |
if(typeof window[fn]==='function'){ try{ window[fn](); }catch(e){ console.error(fn,e); } }
|
| 1410 |
/* The auth portal loads its cached AP_SESSION synchronously. Initialise the
|
| 1411 |
Profile account/save panel afterwards so it renders that real session,
|
src/restree3d.js
CHANGED
|
@@ -1,274 +1,333 @@
|
|
| 1 |
/* ============================================================================
|
| 2 |
-
|
| 3 |
----------------------------------------------------------------------------
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
(the same technique #startScreen already uses for its parallax, see
|
| 10 |
-
ui.css). Everything here is display only: purchasing goes through the real
|
| 11 |
-
devHas/devAvail/devBuy in develop.js, never reimplemented.
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
(so materials row + tabs + the craft/loadout tabs are untouched); if the
|
| 19 |
-
research tab is active, its flat-list output in #devBody is then replaced
|
| 20 |
-
with the 3D tree.
|
| 21 |
============================================================================ */
|
| 22 |
(function(){
|
| 23 |
-
|
| 24 |
-
/* index.html already links this stylesheet; inject defensively in case a
|
| 25 |
-
stripped-down host page does not carry that tag. */
|
| 26 |
if(!document.querySelector('link[href*="restree.css"]')){
|
| 27 |
var lk=document.createElement('link');
|
| 28 |
lk.rel='stylesheet'; lk.href='./src/styles/restree.css';
|
| 29 |
document.head.appendChild(lk);
|
| 30 |
}
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
loop, so array index within a branch IS the tier — no topo-sort needed. */
|
| 37 |
-
var BRS=['FABRICATION','DOCTRINE','XENOLOGY'];
|
| 38 |
-
var TIER_X=98, ROW_Y=132;
|
| 39 |
-
var BASE_Z={FABRICATION:36,DOCTRINE:-18,XENOLOGY:-84};
|
| 40 |
-
var LIFT={owned:26,afford:14,avail:0,locked:-16};
|
| 41 |
-
var SVG_W=420,SVG_H=400,SVG_CX=210,SVG_CY=200,SVG_ZBACK=-160;
|
| 42 |
-
|
| 43 |
-
/* Camera/gesture state lives outside the DOM so it survives every rebuild
|
| 44 |
-
(e.g. renderDevelop() re-running after a purchase). */
|
| 45 |
-
var RT={rx:-11,ry:-15,zoom:.92,panX:0,panY:0,sel:null,pts:new Map(),multi:false,lastTap:0};
|
| 46 |
-
var RX_MIN=-42,RX_MAX=14,RY_MIN=-72,RY_MAX=72,ZOOM_MIN=.55,ZOOM_MAX=1.9,PAN_MAX=130;
|
| 47 |
-
|
| 48 |
-
function clamp(v,a,b){ return v<a?a:(v>b?b:v); }
|
| 49 |
-
function is3D(){ return !(typeof META!=='undefined'&&META.settings&&META.settings.perf==='low'); }
|
| 50 |
|
| 51 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
if(devHas(n.id)) return 'owned';
|
| 53 |
if(!devAvail(n)) return 'locked';
|
| 54 |
-
return ((META.researchData||0)>=n.data&&matHas(n.cost))?'
|
| 55 |
}
|
| 56 |
-
function
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
}
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
var
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
| 75 |
});
|
| 76 |
});
|
|
|
|
|
|
|
| 77 |
});
|
| 78 |
-
return
|
| 79 |
}
|
| 80 |
|
| 81 |
-
function
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
}
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
+'<div class="rt3d-detail-hd"><div class="rt-ic">'+itemArt(n.art||'res_'+n.id,'🔬',30)+'</div>'
|
| 95 |
-
+'<div><b>'+n.nm+'</b><span>'+n.ds+'</span></div></div>'
|
| 96 |
-
+'<div class="rt3d-detail-row"><div class="rt3d-detail-lbl">COST</div>'
|
| 97 |
-
+matCostStr(n.cost)+'<span class="costOne">◆ '+n.data+' DATA</span></div>';
|
| 98 |
-
if(n.req.length){
|
| 99 |
-
body+='<div class="rt3d-detail-row"><div class="rt3d-detail-lbl">PREREQUISITES</div><div class="rt3d-reqs">'
|
| 100 |
-
+n.req.map(function(r){
|
| 101 |
-
var rd=DEVTREE.find(function(x){ return x.id===r; });
|
| 102 |
-
var ok=devHas(r);
|
| 103 |
-
return '<span class="rt3d-reqChip '+(ok?'ok':'no')+'">'+(ok?'✓ ':'')+(rd?rd.nm:r)+'</span>';
|
| 104 |
-
}).join('')+'</div></div>';
|
| 105 |
}
|
| 106 |
-
|
| 107 |
-
var bcls = st==='owned' ? 'done' : (st==='afford' ? 'go' : '');
|
| 108 |
-
body+='<button class="rt3d-detail-buy '+bcls+'" id="rtBuy" type="button">'+label+'</button>';
|
| 109 |
-
return '<div class="rt3d-detail show" id="rtDetail">'+body+'</div>';
|
| 110 |
}
|
| 111 |
-
function
|
| 112 |
-
var
|
| 113 |
-
|
| 114 |
-
var
|
| 115 |
-
if(
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
|
|
|
|
|
|
| 120 |
}
|
| 121 |
-
function
|
| 122 |
-
var
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
-
function openDetail(id){ RT.sel=id; refreshDetail(); }
|
| 131 |
-
function closeDetail(){ RT.sel=null; refreshDetail(); }
|
| 132 |
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
| 151 |
}
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
RT.panY=clamp(RT.pb.y+(m.y-RT.pm.y), -PAN_MAX,PAN_MAX);
|
| 168 |
-
applyTransform(scene);
|
| 169 |
-
}
|
| 170 |
}
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
if(nodeEl) openDetail(nodeEl.dataset.id);
|
| 185 |
-
}
|
| 186 |
-
}
|
| 187 |
-
if(RT.pts.size===0) RT.multi=false;
|
| 188 |
}
|
| 189 |
-
|
| 190 |
-
stage.addEventListener('pointermove',onMove);
|
| 191 |
-
stage.addEventListener('pointerup',onUp);
|
| 192 |
-
stage.addEventListener('pointercancel',onUp);
|
| 193 |
}
|
| 194 |
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
var
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
+'<
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
row.n.req.forEach(function(rid){
|
| 216 |
-
var pr=
|
| 217 |
-
var
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
| 220 |
});
|
| 221 |
});
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
});
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
-
|
| 246 |
-
var
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
RT.
|
| 257 |
-
|
| 258 |
-
}
|
| 259 |
}
|
| 260 |
|
| 261 |
-
/* ---- hook-in -------------------------------------------------------------- */
|
| 262 |
function initResTree3D(){
|
| 263 |
-
if(typeof renderDevelop!=='function'
|
| 264 |
var flatRender=renderDevelop;
|
| 265 |
renderDevelop=function(){
|
| 266 |
flatRender();
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
| 272 |
};
|
| 273 |
}
|
| 274 |
|
|
|
|
| 1 |
/* ============================================================================
|
| 2 |
+
RESEARCH NETWORK — touch-first prerequisite graph
|
| 3 |
----------------------------------------------------------------------------
|
| 4 |
+
develop.js remains the authority for research data, costs, availability and
|
| 5 |
+
effects. This module only takes over the RESEARCH presentation after the
|
| 6 |
+
normal Development renderer has populated materials and tabs. Keeping the
|
| 7 |
+
purchase call routed through devBuy() prevents the visual tree from becoming
|
| 8 |
+
a second, subtly different progression system.
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
+
The old spatial view only ran above 700 px in landscape, which meant the
|
| 11 |
+
game's actual phone orientation silently fell back to the original list.
|
| 12 |
+
This graph is deliberately 2D and scrollable: prerequisite direction stays
|
| 13 |
+
readable under a thumb, cross-branch links are real SVG paths, and every
|
| 14 |
+
node remains at least 52 px tall on a 320 px-wide device.
|
|
|
|
|
|
|
|
|
|
| 15 |
============================================================================ */
|
| 16 |
(function(){
|
|
|
|
|
|
|
|
|
|
| 17 |
if(!document.querySelector('link[href*="restree.css"]')){
|
| 18 |
var lk=document.createElement('link');
|
| 19 |
lk.rel='stylesheet'; lk.href='./src/styles/restree.css';
|
| 20 |
document.head.appendChild(lk);
|
| 21 |
}
|
| 22 |
|
| 23 |
+
var RT_BRANCHES=['FABRICATION','DOCTRINE','XENOLOGY'];
|
| 24 |
+
var RT_QUEUE_SLOTS=5;
|
| 25 |
+
var RT_NODE_W=126,RT_NODE_H=76,RT_COL=154,RT_GAP_Y=94;
|
| 26 |
+
var RT={sel:null,scrollX:0,scrollY:0,branch:'FABRICATION',renderCount:0};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
function rtEsc(s){
|
| 29 |
+
return String(s==null?'':s).replace(/[&<>"']/g,function(c){
|
| 30 |
+
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
| 31 |
+
});
|
| 32 |
+
}
|
| 33 |
+
function rtNode(id){ return DEVTREE.find(function(n){ return n.id===id; }); }
|
| 34 |
+
function rtState(n){
|
| 35 |
if(devHas(n.id)) return 'owned';
|
| 36 |
if(!devAvail(n)) return 'locked';
|
| 37 |
+
return ((META.researchData||0)>=n.data&&matHas(n.cost))?'ready':'funding';
|
| 38 |
}
|
| 39 |
+
function rtDepth(n,memo,stack){
|
| 40 |
+
if(memo[n.id]!=null) return memo[n.id];
|
| 41 |
+
if(stack[n.id]) return 0;
|
| 42 |
+
stack[n.id]=1;
|
| 43 |
+
var d=0;
|
| 44 |
+
n.req.forEach(function(id){ var p=rtNode(id); if(p) d=Math.max(d,rtDepth(p,memo,stack)+1); });
|
| 45 |
+
delete stack[n.id]; memo[n.id]=d; return d;
|
| 46 |
}
|
| 47 |
+
function rtLayout(){
|
| 48 |
+
var memo={},rows=[],bands=[],top=18,maxDepth=0;
|
| 49 |
+
RT_BRANCHES.forEach(function(br){
|
| 50 |
+
var nodes=DEVTREE.filter(function(n){ return n.br===br; });
|
| 51 |
+
var layers={};
|
| 52 |
+
nodes.forEach(function(n){
|
| 53 |
+
var d=rtDepth(n,memo,{}); maxDepth=Math.max(maxDepth,d);
|
| 54 |
+
(layers[d]||(layers[d]=[])).push(n);
|
| 55 |
+
});
|
| 56 |
+
var stackMax=1;
|
| 57 |
+
Object.keys(layers).forEach(function(d){ stackMax=Math.max(stackMax,layers[d].length); });
|
| 58 |
+
var bandH=Math.max(144,58+stackMax*RT_GAP_Y);
|
| 59 |
+
var center=top+50+(bandH-62)*.5;
|
| 60 |
+
Object.keys(layers).forEach(function(k){
|
| 61 |
+
var d=+k,list=layers[k];
|
| 62 |
+
list.forEach(function(n,i){
|
| 63 |
+
rows.push({n:n,state:rtState(n),depth:d,
|
| 64 |
+
x:84+d*RT_COL,y:center+(i-(list.length-1)*.5)*RT_GAP_Y});
|
| 65 |
});
|
| 66 |
});
|
| 67 |
+
bands.push({br:br,top:top,height:bandH});
|
| 68 |
+
top+=bandH+14;
|
| 69 |
});
|
| 70 |
+
return {nodes:rows,bands:bands,width:188+maxDepth*RT_COL,height:top+8};
|
| 71 |
}
|
| 72 |
|
| 73 |
+
function rtQueue(save){
|
| 74 |
+
var before=Array.isArray(META.resQueue)?META.resQueue.slice():[];
|
| 75 |
+
var seen={};
|
| 76 |
+
var clean=before.filter(function(id){
|
| 77 |
+
if(seen[id]||!rtNode(id)||devHas(id)) return false;
|
| 78 |
+
seen[id]=1; return true;
|
| 79 |
+
}).slice(0,RT_QUEUE_SLOTS);
|
| 80 |
+
META.resQueue=clean;
|
| 81 |
+
if(save&&before.join('|')!==clean.join('|')&&typeof metaSave==='function') metaSave();
|
| 82 |
+
return clean;
|
| 83 |
}
|
| 84 |
+
function rtQueuePath(id){
|
| 85 |
+
var out=[],seen={};
|
| 86 |
+
function walk(n){
|
| 87 |
+
if(!n||seen[n.id]||devHas(n.id)) return;
|
| 88 |
+
seen[n.id]=1;
|
| 89 |
+
n.req.forEach(function(r){ walk(rtNode(r)); });
|
| 90 |
+
out.push(n.id);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
+
walk(rtNode(id)); return out;
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
+
function rtQueueAdd(id,path){
|
| 95 |
+
var n=rtNode(id); if(!n||devHas(id)) return;
|
| 96 |
+
var q=rtQueue(false).slice();
|
| 97 |
+
var add=path?rtQueuePath(id):[id];
|
| 98 |
+
add.forEach(function(x){ if(q.indexOf(x)<0&&q.length<RT_QUEUE_SLOTS) q.push(x); });
|
| 99 |
+
META.resQueue=q;
|
| 100 |
+
if(typeof metaSave==='function') metaSave();
|
| 101 |
+
if(add.some(function(x){ return q.indexOf(x)<0; })&&typeof toast==='function')
|
| 102 |
+
toast('Research queue is full — '+RT_QUEUE_SLOTS+' priorities maximum');
|
| 103 |
+
if(typeof sfx==='function') sfx('ui');
|
| 104 |
+
buildTree();
|
| 105 |
}
|
| 106 |
+
function rtQueueRemove(at){
|
| 107 |
+
var q=rtQueue(false); if(at<0||at>=q.length) return;
|
| 108 |
+
q.splice(at,1); META.resQueue=q;
|
| 109 |
+
if(typeof metaSave==='function') metaSave();
|
| 110 |
+
if(typeof sfx==='function') sfx('ui');
|
| 111 |
+
buildTree();
|
| 112 |
+
}
|
| 113 |
+
function rtBuy(n){
|
| 114 |
+
if(!n) return;
|
| 115 |
+
var had=devHas(n.id);
|
| 116 |
+
devBuy(n);
|
| 117 |
+
if(!had&&devHas(n.id)){
|
| 118 |
+
META.resQueue=rtQueue(false).filter(function(id){ return id!==n.id; });
|
| 119 |
+
if(typeof metaSave==='function') metaSave();
|
| 120 |
+
/* devBuy redraws before control returns to us. Redraw once more after
|
| 121 |
+
queue cleanup so the first slot cannot keep showing a purchased node. */
|
| 122 |
+
buildTree();
|
| 123 |
+
}
|
| 124 |
}
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
function rtCostHtml(n){
|
| 127 |
+
var b=matBag();
|
| 128 |
+
var h='<div class="rt3d-costGrid">';
|
| 129 |
+
Object.keys(n.cost).forEach(function(k){
|
| 130 |
+
var have=Math.floor(b[k]||0),need=n.cost[k],ok=have>=need;
|
| 131 |
+
h+='<div class="rt3d-cost '+(ok?'ok':'need')+'">'
|
| 132 |
+
+itemArt('mat_'+k,MATS[k].em,18)+'<span>'+rtEsc(MATS[k].nm)+'</span>'
|
| 133 |
+
+'<b>'+have+' / '+need+'</b></div>';
|
| 134 |
+
});
|
| 135 |
+
var dh=Math.floor(META.researchData||0),dok=dh>=n.data;
|
| 136 |
+
h+='<div class="rt3d-cost data '+(dok?'ok':'need')+'"><i>◆</i><span>Research Data</span>'
|
| 137 |
+
+'<b>'+dh+' / '+n.data+'</b></div></div>';
|
| 138 |
+
return h;
|
| 139 |
+
}
|
| 140 |
+
function rtDetailHtml(){
|
| 141 |
+
var n=RT.sel&&rtNode(RT.sel);
|
| 142 |
+
if(!n){
|
| 143 |
+
return '<section class="rt3d-detail empty" id="rtDetail" data-test="research-detail">'
|
| 144 |
+
+'<div class="rt3d-emptyIcon">⌬</div><div><b>SELECT A RESEARCH NODE</b>'
|
| 145 |
+
+'<span>Inspect its unlock, resource cost, and prerequisite path.</span></div></section>';
|
| 146 |
}
|
| 147 |
+
var st=rtState(n),q=rtQueue(false),queued=q.indexOf(n.id)>=0;
|
| 148 |
+
var missing=n.req.filter(function(id){ return !devHas(id); });
|
| 149 |
+
var stateText={owned:'RESEARCHED',ready:'READY TO RESEARCH',funding:'RESOURCES REQUIRED',locked:'PREREQUISITES LOCKED'}[st];
|
| 150 |
+
var h='<section class="rt3d-detail '+st+'" id="rtDetail" data-test="research-detail" data-node="'+rtEsc(n.id)+'">'
|
| 151 |
+
+'<button class="rt3d-detailClose" id="rtDetailClose" type="button" aria-label="Close research details">×</button>'
|
| 152 |
+
+'<header><div class="rt3d-detailArt">'+itemArt(n.art||'res_'+n.id,'⌬',46)+'</div>'
|
| 153 |
+
+'<div><small>'+rtEsc(n.br)+' · TIER '+(rtDepth(n,{},{} )+1)+'</small><b>'+rtEsc(n.nm)+'</b>'
|
| 154 |
+
+'<span>'+rtEsc(n.ds)+'</span></div><em>'+stateText+'</em></header>';
|
| 155 |
+
if(n.req.length){
|
| 156 |
+
h+='<div class="rt3d-detailLabel">PREREQUISITES</div><div class="rt3d-prereqs">'
|
| 157 |
+
+n.req.map(function(id){
|
| 158 |
+
var p=rtNode(id),ok=devHas(id);
|
| 159 |
+
return '<button type="button" data-prereq="'+rtEsc(id)+'" class="'+(ok?'ok':'locked')+'">'
|
| 160 |
+
+(ok?'✓ ':'🔒 ')+rtEsc(p?p.nm:id)+'</button>';
|
| 161 |
+
}).join('')+'</div>';
|
|
|
|
|
|
|
|
|
|
| 162 |
}
|
| 163 |
+
h+='<div class="rt3d-detailLabel">RESEARCH COST</div>'+rtCostHtml(n)+'<div class="rt3d-detailActions">';
|
| 164 |
+
if(st==='owned'){
|
| 165 |
+
h+='<button class="rt3d-primary done" type="button" disabled>✓ RESEARCH COMPLETE</button>';
|
| 166 |
+
}else if(st==='ready'){
|
| 167 |
+
h+='<button class="rt3d-primary" id="rtResearchNow" type="button">RESEARCH NOW</button>';
|
| 168 |
+
}else{
|
| 169 |
+
h+='<button class="rt3d-primary locked" type="button" disabled>'
|
| 170 |
+
+(st==='locked'?'COMPLETE '+missing.length+' PREREQUISITE'+(missing.length===1?'':'S'):'GATHER REQUIRED RESOURCES')+'</button>';
|
| 171 |
+
}
|
| 172 |
+
if(st!=='owned'){
|
| 173 |
+
h+='<button class="rt3d-secondary" id="rtQueueToggle" type="button">'
|
| 174 |
+
+(queued?'REMOVE FROM QUEUE':'ADD TO QUEUE')+'</button>';
|
| 175 |
+
if(st==='locked') h+='<button class="rt3d-secondary path" id="rtQueuePath" type="button">QUEUE PREREQUISITE PATH</button>';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
}
|
| 177 |
+
return h+'</div></section>';
|
|
|
|
|
|
|
|
|
|
| 178 |
}
|
| 179 |
|
| 180 |
+
function rtQueueHtml(){
|
| 181 |
+
var q=rtQueue(true),next=q.length?rtNode(q[0]):null;
|
| 182 |
+
var h='<section class="rt3d-queue" data-test="research-queue"><header><div><b>RESEARCH QUEUE</b>'
|
| 183 |
+
+'<span>Plan the next unlocks without spending resources</span></div><strong>'+q.length+'/'+RT_QUEUE_SLOTS+'</strong></header>'
|
| 184 |
+
+'<div class="rt3d-qslots">';
|
| 185 |
+
for(var i=0;i<RT_QUEUE_SLOTS;i++){
|
| 186 |
+
var n=i<q.length?rtNode(q[i]):null;
|
| 187 |
+
if(n){
|
| 188 |
+
h+='<div class="rt3d-qslot filled '+rtState(n)+'" data-queue-slot="'+i+'">'
|
| 189 |
+
+'<button class="rt3d-qinspect" type="button" data-qid="'+rtEsc(n.id)+'" aria-label="Inspect '+rtEsc(n.nm)+'">'
|
| 190 |
+
+'<small>'+(i+1)+'</small>'+itemArt(n.art||'res_'+n.id,'⌬',24)+'<span>'+rtEsc(n.nm)+'</span></button>'
|
| 191 |
+
+'<button class="rt3d-qremove" type="button" data-qremove="'+i+'" aria-label="Remove '+rtEsc(n.nm)+' from queue">×</button></div>';
|
| 192 |
+
}else{
|
| 193 |
+
h+='<div class="rt3d-qslot empty" data-queue-slot="'+i+'"><small>'+(i+1)+'</small><span>EMPTY</span></div>';
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
h+='</div>';
|
| 197 |
+
if(next){
|
| 198 |
+
var st=rtState(next);
|
| 199 |
+
h+='<button class="rt3d-next '+(st==='ready'?'ready':'')+'" id="rtResearchNext" type="button" '
|
| 200 |
+
+(st==='ready'?'':'disabled')+'>'+(st==='ready'?'RESEARCH NEXT · '+rtEsc(next.nm):'NEXT · '+rtEsc(next.nm)+' · '+(st==='locked'?'LOCKED':'NEEDS RESOURCES'))+'</button>';
|
| 201 |
+
}
|
| 202 |
+
return h+'</section>';
|
| 203 |
+
}
|
| 204 |
|
| 205 |
+
function rtGraphHtml(layout){
|
| 206 |
+
var done=DEVTREE.filter(function(n){ return devHas(n.id); }).length;
|
| 207 |
+
var ready=DEVTREE.filter(function(n){ return rtState(n)==='ready'; }).length;
|
| 208 |
+
var h='<div class="rt3d-summary"><div><small>RESEARCH NETWORK</small><b>'+done+' / '+DEVTREE.length+' NODES</b></div>'
|
| 209 |
+
+'<div class="rt3d-summaryStats"><span><i class="ready"></i>'+ready+' READY</span><span>◆ '+Math.floor(META.researchData||0)+' DATA</span></div></div>'
|
| 210 |
+
+rtQueueHtml()
|
| 211 |
+
+'<nav class="rt3d-branchNav" aria-label="Research branches">'
|
| 212 |
+
+RT_BRANCHES.map(function(br){
|
| 213 |
+
var ns=DEVTREE.filter(function(n){ return n.br===br; });
|
| 214 |
+
var count=ns.filter(function(n){ return devHas(n.id); }).length;
|
| 215 |
+
return '<button type="button" data-rtbranch="'+br+'" class="'+(RT.branch===br?'on':'')+'">'
|
| 216 |
+
+'<span>'+({FABRICATION:'⚙',DOCTRINE:'⌖',XENOLOGY:'◈'}[br])+'</span>'+br+'<small>'+count+'/'+ns.length+'</small></button>';
|
| 217 |
+
}).join('')+'</nav>'
|
| 218 |
+
+'<div class="rt3d-stage" id="rtStage" data-test="research-graph" tabindex="0" aria-label="Scrollable research prerequisite graph">'
|
| 219 |
+
+'<div class="rt3d-world" id="rtWorld" style="width:'+layout.width+'px;height:'+layout.height+'px">';
|
| 220 |
+
layout.bands.forEach(function(b){
|
| 221 |
+
h+='<div class="rt3d-band '+b.br.toLowerCase()+'" data-band="'+b.br+'" style="top:'+b.top+'px;height:'+b.height+'px">'
|
| 222 |
+
+'<span>'+({FABRICATION:'⚙ FABRICATION',DOCTRINE:'⌖ DOCTRINE',XENOLOGY:'◈ XENOLOGY'}[b.br])+'</span></div>';
|
| 223 |
+
});
|
| 224 |
+
h+='<svg class="rt3d-links" width="'+layout.width+'" height="'+layout.height+'" viewBox="0 0 '+layout.width+' '+layout.height+'" aria-hidden="true">';
|
| 225 |
+
layout.nodes.forEach(function(row){
|
| 226 |
row.n.req.forEach(function(rid){
|
| 227 |
+
var pr=layout.nodes.find(function(x){ return x.n.id===rid; }); if(!pr) return;
|
| 228 |
+
var x1=pr.x+RT_NODE_W*.5,y1=pr.y,x2=row.x-RT_NODE_W*.5,y2=row.y;
|
| 229 |
+
var bend=Math.max(28,(x2-x1)*.48);
|
| 230 |
+
var cls=(devHas(row.n.id)?'owned':devHas(rid)?'open':'locked')
|
| 231 |
+
+(RT.sel===rid||RT.sel===row.n.id?' focus':'');
|
| 232 |
+
h+='<path data-from="'+rtEsc(rid)+'" data-to="'+rtEsc(row.n.id)+'" class="'+cls+'" d="M '+x1+' '+y1+' C '+(x1+bend)+' '+y1+', '+(x2-bend)+' '+y2+', '+x2+' '+y2+'"></path>';
|
| 233 |
});
|
| 234 |
});
|
| 235 |
+
h+='</svg>';
|
| 236 |
+
layout.nodes.forEach(function(row){
|
| 237 |
+
var n=row.n,st=row.state,missing=n.req.filter(function(id){ return !devHas(id); });
|
| 238 |
+
var sub=st==='owned'?'RESEARCHED':st==='ready'?'READY':st==='funding'?'NEEDS RESOURCES':'NEEDS '+missing.length;
|
| 239 |
+
h+='<button type="button" class="rt3d-node '+st+(RT.sel===n.id?' selected':'')+'" data-research-id="'+rtEsc(n.id)+'" '
|
| 240 |
+
+'data-state="'+st+'" data-prerequisites="'+rtEsc(n.req.join(','))+'" '
|
| 241 |
+
+'style="left:'+(row.x-RT_NODE_W*.5)+'px;top:'+(row.y-RT_NODE_H*.5)+'px" aria-label="'+rtEsc(n.nm)+', '+sub+'">'
|
| 242 |
+
+'<span class="rt3d-tier">T'+(row.depth+1)+'</span><span class="rt3d-nodeArt">'+itemArt(n.art||'res_'+n.id,'⌬',30)+'</span>'
|
| 243 |
+
+'<b>'+rtEsc(n.nm)+'</b><small>'+sub+'</small></button>';
|
| 244 |
+
});
|
| 245 |
+
return h+'</div></div>'+rtDetailHtml();
|
| 246 |
+
}
|
| 247 |
|
| 248 |
+
function rtWire(){
|
| 249 |
+
var stage=document.getElementById('rtStage');
|
| 250 |
+
if(stage){
|
| 251 |
+
requestAnimationFrame(function(){ stage.scrollLeft=RT.scrollX; stage.scrollTop=RT.scrollY; });
|
| 252 |
+
stage.addEventListener('scroll',function(){ RT.scrollX=stage.scrollLeft; RT.scrollY=stage.scrollTop; },{passive:true});
|
| 253 |
+
}
|
| 254 |
+
document.querySelectorAll('[data-research-id]').forEach(function(el){
|
| 255 |
+
if(typeof mfBindTap==='function') mfBindTap(el,function(ev){ ev.stopPropagation(); rtSelect(el.dataset.researchId); });
|
| 256 |
+
else el.addEventListener('click',function(){ rtSelect(el.dataset.researchId); });
|
| 257 |
+
});
|
| 258 |
+
document.querySelectorAll('[data-rtbranch]').forEach(function(el){
|
| 259 |
+
el.addEventListener('click',function(){
|
| 260 |
+
RT.branch=el.dataset.rtbranch;
|
| 261 |
+
document.querySelectorAll('[data-rtbranch]').forEach(function(x){ x.classList.toggle('on',x===el); });
|
| 262 |
+
var band=document.querySelector('[data-band="'+RT.branch+'"]');
|
| 263 |
+
if(stage&&band){ RT.scrollY=Math.max(0,parseFloat(band.style.top)-8); stage.scrollTo({top:RT.scrollY,behavior:'smooth'}); }
|
| 264 |
+
if(typeof sfx==='function') sfx('ui');
|
| 265 |
+
});
|
| 266 |
});
|
| 267 |
+
rtWireDetail();
|
| 268 |
+
document.querySelectorAll('[data-qid]').forEach(function(el){ el.addEventListener('click',function(){ rtSelect(el.dataset.qid); }); });
|
| 269 |
+
document.querySelectorAll('[data-qremove]').forEach(function(el){ el.addEventListener('click',function(ev){ ev.stopPropagation(); rtQueueRemove(+el.dataset.qremove); }); });
|
| 270 |
+
var next=document.getElementById('rtResearchNext');
|
| 271 |
+
if(next) next.addEventListener('click',function(){ var q=rtQueue(false),n=q.length&&rtNode(q[0]); if(n) rtBuy(n); });
|
| 272 |
+
}
|
| 273 |
+
function rtWireDetail(){
|
| 274 |
+
var close=document.getElementById('rtDetailClose');
|
| 275 |
+
if(close) close.addEventListener('click',function(){ RT.sel=null; buildTree(); });
|
| 276 |
+
document.querySelectorAll('[data-prereq]').forEach(function(el){ el.addEventListener('click',function(){ rtSelect(el.dataset.prereq); }); });
|
| 277 |
+
var buy=document.getElementById('rtResearchNow');
|
| 278 |
+
if(buy) buy.addEventListener('click',function(){ rtBuy(rtNode(RT.sel)); });
|
| 279 |
+
var toggle=document.getElementById('rtQueueToggle');
|
| 280 |
+
if(toggle) toggle.addEventListener('click',function(){
|
| 281 |
+
var q=rtQueue(false),at=q.indexOf(RT.sel);
|
| 282 |
+
if(at>=0) rtQueueRemove(at); else rtQueueAdd(RT.sel,false);
|
| 283 |
+
});
|
| 284 |
+
var path=document.getElementById('rtQueuePath');
|
| 285 |
+
if(path) path.addEventListener('click',function(){ rtQueueAdd(RT.sel,true); });
|
| 286 |
+
}
|
| 287 |
+
function rtSelect(id){
|
| 288 |
+
if(!rtNode(id)) return;
|
| 289 |
+
RT.sel=id;
|
| 290 |
+
document.querySelectorAll('[data-research-id]').forEach(function(el){ el.classList.toggle('selected',el.dataset.researchId===id); });
|
| 291 |
+
document.querySelectorAll('.rt3d-links path').forEach(function(p){ p.classList.toggle('focus',p.dataset.from===id||p.dataset.to===id); });
|
| 292 |
+
var detail=document.getElementById('rtDetail');
|
| 293 |
+
if(detail){ detail.outerHTML=rtDetailHtml(); rtWireDetail(); }
|
| 294 |
+
if(typeof sfx==='function') sfx('ui');
|
| 295 |
+
}
|
| 296 |
|
| 297 |
+
function buildTree(){
|
| 298 |
+
var body=document.getElementById('devBody'); if(!body) return;
|
| 299 |
+
var layout=rtLayout();
|
| 300 |
+
if(RT.sel&&!rtNode(RT.sel)) RT.sel=null;
|
| 301 |
+
body.innerHTML='<div class="rt3d-wrap" data-test="research-tree">'+rtGraphHtml(layout)+'</div>';
|
| 302 |
+
RT.renderCount++;
|
| 303 |
+
rtWire();
|
| 304 |
+
}
|
| 305 |
|
| 306 |
+
function rtSnapshot(){
|
| 307 |
+
var layout=rtLayout(),edges=0;
|
| 308 |
+
DEVTREE.forEach(function(n){ edges+=n.req.length; });
|
| 309 |
+
return {
|
| 310 |
+
version:'2.0',nodeCount:DEVTREE.length,edgeCount:edges,
|
| 311 |
+
renderedNodes:document.querySelectorAll('[data-research-id]').length,
|
| 312 |
+
renderedEdges:document.querySelectorAll('.rt3d-links path').length,
|
| 313 |
+
branches:RT_BRANCHES.slice(),queueSlots:RT_QUEUE_SLOTS,queue:rtQueue(false).slice(),
|
| 314 |
+
selected:RT.sel,renderCount:RT.renderCount,width:layout.width,height:layout.height,
|
| 315 |
+
states:DEVTREE.reduce(function(o,n){ o[n.id]=rtState(n); return o; },{})
|
| 316 |
+
};
|
| 317 |
}
|
| 318 |
|
|
|
|
| 319 |
function initResTree3D(){
|
| 320 |
+
if(typeof renderDevelop!=='function'||typeof DEVTREE==='undefined') return;
|
| 321 |
var flatRender=renderDevelop;
|
| 322 |
renderDevelop=function(){
|
| 323 |
flatRender();
|
| 324 |
+
if(typeof devTab!=='undefined'&&devTab==='research') buildTree();
|
| 325 |
+
};
|
| 326 |
+
window.__MF_RESEARCH_TREE__={
|
| 327 |
+
version:'2.0',build:buildTree,snapshot:rtSnapshot,select:rtSelect,
|
| 328 |
+
queueAdd:function(id){ rtQueueAdd(id,false); },queuePath:function(id){ rtQueueAdd(id,true); },
|
| 329 |
+
queueRemove:rtQueueRemove,clearQueue:function(){ META.resQueue=[]; if(typeof metaSave==='function') metaSave(); buildTree(); },
|
| 330 |
+
layout:function(){ return rtLayout(); }
|
| 331 |
};
|
| 332 |
}
|
| 333 |
|
src/storeui.js
CHANGED
|
@@ -9,8 +9,10 @@
|
|
| 9 |
pattern src/audio.js uses for sfx — and rebuilds it as tabbed categories,
|
| 10 |
sorted cheapest-first, with the concrete before/after numbers spelled out.
|
| 11 |
|
| 12 |
-
Purchase rules and costs are untouched
|
| 13 |
-
|
|
|
|
|
|
|
| 14 |
============================================================================ */
|
| 15 |
|
| 16 |
/* ---- category map ---------------------------------------------------------
|
|
@@ -21,11 +23,12 @@ const ARM_CATS=[
|
|
| 21 |
{id:'operations', em:'💰', nm:'OPERATIONS', ds:'Permanent economy and recovery advantages.', items:['cache','trade','salvage','droppod','reactor']},
|
| 22 |
{id:'battlefield',em:'⚔', nm:'BATTLEFIELD', ds:'Permanent combat and fortification improvements.', items:['targeting','armor','bastion']},
|
| 23 |
{id:'commander', em:'★', nm:'COMMANDER', ds:'Commander growth, abilities, and cooldowns.', items:['neural','capacitor','orbital']},
|
| 24 |
-
{id:'
|
| 25 |
-
{id:'
|
| 26 |
{id:'identity', em:'🎨', nm:'IDENTITY', ds:'Visual identity only — no combat advantage.', items:[]},
|
| 27 |
];
|
| 28 |
let armTab='operations';
|
|
|
|
| 29 |
|
| 30 |
/* ---- concrete effect text -------------------------------------------------
|
| 31 |
applyMetaPerks() in meta.js applies every one of these as a flat multiple
|
|
@@ -93,8 +96,8 @@ function armBodyHTML(){
|
|
| 93 |
const cat=ARM_CATS.find(c=>c.id===armTab)||ARM_CATS[0];
|
| 94 |
const intro='<div class="armCatIntro"><b>'+cat.nm+'</b><span>'+cat.ds+'</span></div>';
|
| 95 |
if(cat.id==='identity') return intro+armColorsHTML();
|
| 96 |
-
if(cat.id==='
|
| 97 |
-
if(cat.id==='
|
| 98 |
const items=cat.items.map(id=>STORE.find(s=>s.id===id)).filter(Boolean)
|
| 99 |
.sort((a,b)=>a.cost[0]-b.cost[0]); // cheap-first = early-buy-first
|
| 100 |
if(!items.length) return '<div class="devNone">Nothing in this category yet.</div>';
|
|
@@ -104,31 +107,157 @@ function invRarityLegend(){
|
|
| 104 |
return '<div class="invLegend">'+INV_RARITIES.map(r=>
|
| 105 |
'<span style="--rar:'+r.col+'"><i></i>'+r.nm+'</span>').join('')+'</div>';
|
| 106 |
}
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
}
|
| 123 |
-
function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
const b=invBag();
|
| 125 |
-
return
|
| 126 |
-
+
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
+'<div class="invEm">'+c.em+'</div><div class="invTx"><i>'+r.nm+' · CONSUMABLE</i><b>'+c.nm+'</b><span>'+c.ds+'</span></div>'
|
| 130 |
-
+'<div class="invAct">'+(n?(on?'READY':'×'+n):'EMPTY')+'</div></div>';
|
| 131 |
-
}).join('');
|
| 132 |
}
|
| 133 |
function armColorsHTML(){
|
| 134 |
let h='<div class="sHead">COMMANDER COLORS</div><div id="colorRow">';
|
|
@@ -147,14 +276,13 @@ function armTabsHTML(){
|
|
| 147 |
return '<div class="armTabWrap"><div class="tabRow" id="armTabs">'
|
| 148 |
+ARM_CATS.map(c=>{
|
| 149 |
const bag=invBag();
|
| 150 |
-
const
|
| 151 |
-
|
|
|
|
| 152 |
:c.id==='identity'?0:c.items.filter(id=>{ const it=STORE.find(s=>s.id===id); return it&&(META.owned[it.id]||0)>=it.max; }).length;
|
| 153 |
-
const tot=c.items.length;
|
| 154 |
return '<button class="tabBtn'+(armTab===c.id?' on':'')+'" data-k="'+c.id+'">'
|
| 155 |
-
+'<span class="tEm">'+c.em+'</span>'+c.nm+(
|
| 156 |
-
:c.id==='consumables'?' <span class="tabCt">×'+done+'</span>'
|
| 157 |
-
:tot?' <span class="tabCt">'+done+'/'+tot+'</span>':'')+'</button>';
|
| 158 |
}).join('')
|
| 159 |
+'</div><div class="armTabHint">SWIPE CATEGORIES →</div></div>';
|
| 160 |
}
|
|
@@ -192,24 +320,59 @@ renderArmory=function(){
|
|
| 192 |
list.querySelectorAll('.tabBtn').forEach(btn=>{
|
| 193 |
mfBindTap(btn,ev=>{
|
| 194 |
ev.stopPropagation();
|
| 195 |
-
armTab=btn.dataset.k; sfx('ui'); renderArmory();
|
| 196 |
});
|
| 197 |
});
|
| 198 |
|
| 199 |
-
list.querySelectorAll('.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
mfBindTap(el,ev=>{
|
| 201 |
ev.stopPropagation();
|
| 202 |
-
|
| 203 |
-
|
|
|
|
| 204 |
});
|
| 205 |
});
|
| 206 |
-
list.querySelectorAll('.
|
| 207 |
mfBindTap(el,ev=>{
|
| 208 |
ev.stopPropagation();
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
});
|
| 212 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
|
| 214 |
list.querySelectorAll('.armIt').forEach(el=>{
|
| 215 |
el.addEventListener('pointerdown',()=>{
|
|
|
|
| 9 |
pattern src/audio.js uses for sfx — and rebuilds it as tabbed categories,
|
| 10 |
sorted cheapest-first, with the concrete before/after numbers spelled out.
|
| 11 |
|
| 12 |
+
Purchase rules and costs are untouched. The inventory half deliberately
|
| 13 |
+
keeps the legacy META.inventory shape from src/game/meta.js: the Account
|
| 14 |
+
Armory is the storage layer, while equipped/ready remain the five-slot
|
| 15 |
+
Session Loadout consumed by the existing match-start code.
|
| 16 |
============================================================================ */
|
| 17 |
|
| 18 |
/* ---- category map ---------------------------------------------------------
|
|
|
|
| 23 |
{id:'operations', em:'💰', nm:'OPERATIONS', ds:'Permanent economy and recovery advantages.', items:['cache','trade','salvage','droppod','reactor']},
|
| 24 |
{id:'battlefield',em:'⚔', nm:'BATTLEFIELD', ds:'Permanent combat and fortification improvements.', items:['targeting','armor','bastion']},
|
| 25 |
{id:'commander', em:'★', nm:'COMMANDER', ds:'Commander growth, abilities, and cooldowns.', items:['neural','capacitor','orbital']},
|
| 26 |
+
{id:'inventory', em:'▦', nm:'ARMORY', ds:'Account storage for every recovered gear item and mission supply.', items:[]},
|
| 27 |
+
{id:'loadout', em:'⬢', nm:'LOADOUT', ds:'Five limited deployment slots: three gear categories and two supplies.', items:[]},
|
| 28 |
{id:'identity', em:'🎨', nm:'IDENTITY', ds:'Visual identity only — no combat advantage.', items:[]},
|
| 29 |
];
|
| 30 |
let armTab='operations';
|
| 31 |
+
let armInvFilter='all',armInvSelectedKind='',armInvSelectedId='';
|
| 32 |
|
| 33 |
/* ---- concrete effect text -------------------------------------------------
|
| 34 |
applyMetaPerks() in meta.js applies every one of these as a flat multiple
|
|
|
|
| 96 |
const cat=ARM_CATS.find(c=>c.id===armTab)||ARM_CATS[0];
|
| 97 |
const intro='<div class="armCatIntro"><b>'+cat.nm+'</b><span>'+cat.ds+'</span></div>';
|
| 98 |
if(cat.id==='identity') return intro+armColorsHTML();
|
| 99 |
+
if(cat.id==='inventory') return intro+armInventoryHTML();
|
| 100 |
+
if(cat.id==='loadout') return intro+armSessionLoadoutHTML();
|
| 101 |
const items=cat.items.map(id=>STORE.find(s=>s.id===id)).filter(Boolean)
|
| 102 |
.sort((a,b)=>a.cost[0]-b.cost[0]); // cheap-first = early-buy-first
|
| 103 |
if(!items.length) return '<div class="devNone">Nothing in this category yet.</div>';
|
|
|
|
| 107 |
return '<div class="invLegend">'+INV_RARITIES.map(r=>
|
| 108 |
'<span style="--rar:'+r.col+'"><i></i>'+r.nm+'</span>').join('')+'</div>';
|
| 109 |
}
|
| 110 |
+
|
| 111 |
+
/* ---- two-layer inventory --------------------------------------------------
|
| 112 |
+
Account storage and a match loadout are deliberately different views of the
|
| 113 |
+
SAME legacy save fields. No migration or second source of truth: gear/counts
|
| 114 |
+
remain in inventory.gear / inventory.consumables, and the five slots remain
|
| 115 |
+
equipped.{weapon,armor,utility} plus ready[0..1]. */
|
| 116 |
+
const ARM_INV_FILTERS=[
|
| 117 |
+
{id:'all',nm:'ALL'}, {id:'weapon',nm:'WEAPONS'}, {id:'armor',nm:'ARMOR'},
|
| 118 |
+
{id:'utility',nm:'UTILITY'}, {id:'supply',nm:'SUPPLIES'},
|
| 119 |
+
];
|
| 120 |
+
const ARM_INV_ART={
|
| 121 |
+
w_rangefinder:'mod_optic',w_pulseoptic:'res_optics',w_gaussdir:'st_targeting',w_voidlens:'mod_range',w_relicscope:'res_relictech',
|
| 122 |
+
a_fieldplate:'mod_plate',a_reinforced:'st_armor',a_phaseweave:'res_metallurgy',a_aegis:'st_bastion',a_starforged:'res_slot3',
|
| 123 |
+
u_toolkit:'mod_tempo',u_fluxcell:'mod_core',u_salvage:'mod_recl',u_chronorig:'st_capacitor',u_commandcore:'res_ability',
|
| 124 |
+
c_supply:'st_cache',c_power:'bst_energy',c_nanites:'bst_build',c_overdrive:'st_targeting',c_command:'res_slot3',
|
| 125 |
+
};
|
| 126 |
+
const ARM_INV_EFFECTS={
|
| 127 |
+
w_rangefinder:{stat:'UNIT DAMAGE',value:'+3%',score:3}, w_pulseoptic:{stat:'UNIT DAMAGE',value:'+5%',score:5},
|
| 128 |
+
w_gaussdir:{stat:'UNIT DAMAGE',value:'+8%',score:8}, w_voidlens:{stat:'UNIT DAMAGE',value:'+12%',score:12},
|
| 129 |
+
w_relicscope:{stat:'UNIT DAMAGE',value:'+16%',score:16},
|
| 130 |
+
a_fieldplate:{stat:'UNIT HEALTH',value:'+3%',score:3}, a_reinforced:{stat:'UNIT HEALTH',value:'+5%',score:5},
|
| 131 |
+
a_phaseweave:{stat:'UNIT HEALTH',value:'+8%',score:8}, a_aegis:{stat:'UNIT HEALTH',value:'+12%',score:12},
|
| 132 |
+
a_starforged:{stat:'UNIT HEALTH',value:'+16%',score:16},
|
| 133 |
+
u_toolkit:{stat:'BUILD SPEED',value:'+5%',score:5}, u_fluxcell:{stat:'ENERGY INCOME',value:'+7%',score:7},
|
| 134 |
+
u_salvage:{stat:'SALVAGE RECOVERY',value:'+15%',score:15}, u_chronorig:{stat:'ABILITY COOLDOWN',value:'-10%',score:10},
|
| 135 |
+
u_commandcore:{stat:'DAMAGE + HEALTH',value:'+8% / +8%',score:8},
|
| 136 |
+
c_supply:{stat:'STARTING MASS',value:'+220',score:220}, c_power:{stat:'STARTING ENERGY',value:'+900',score:900},
|
| 137 |
+
c_nanites:{stat:'UNIT HEALTH',value:'+8%',score:8}, c_overdrive:{stat:'UNIT DAMAGE',value:'+10%',score:10},
|
| 138 |
+
c_command:{stat:'DAMAGE + HEALTH',value:'+10% / +10%',score:10},
|
| 139 |
+
};
|
| 140 |
+
function armInvEffect(id){ return ARM_INV_EFFECTS[id]||{stat:'MISSION EFFECT',value:'ACTIVE',score:0}; }
|
| 141 |
+
function armInvIcon(it,size){
|
| 142 |
+
return typeof itemArt==='function'?itemArt(ARM_INV_ART[it.id]||('inv_'+it.id),it.em,size):'<span>'+it.em+'</span>';
|
| 143 |
+
}
|
| 144 |
+
function armInvEntries(){
|
| 145 |
+
return INV_GEAR.map(it=>({kind:'gear',it})).concat(INV_CONSUMABLES.map(it=>({kind:'supply',it})));
|
| 146 |
+
}
|
| 147 |
+
function armInvOwned(e,b){ return e.kind==='gear'?(b.gear[e.it.id]||0):(b.consumables[e.it.id]||0); }
|
| 148 |
+
function armInvEquipped(e,b){ return e.kind==='gear'?b.equipped[e.it.slot]===e.it.id:b.ready.indexOf(e.it.id)>=0; }
|
| 149 |
+
function armInvStats(b){
|
| 150 |
+
const all=armInvEntries(),stored=all.reduce((n,e)=>n+armInvOwned(e,b),0);
|
| 151 |
+
const found=all.reduce((n,e)=>n+(armInvOwned(e,b)>0?1:0),0);
|
| 152 |
+
const gear=['weapon','armor','utility'].reduce((n,s)=>n+(b.equipped[s]?1:0),0);
|
| 153 |
+
return {stored,found,total:all.length,used:gear+b.ready.length,gear,supplies:b.ready.length};
|
| 154 |
}
|
| 155 |
+
function armInvLayerStatsHTML(b,layer){
|
| 156 |
+
const s=armInvStats(b);
|
| 157 |
+
return '<div class="armInvLayerHead '+layer+'"><div><i>'+(layer==='account'?'ACCOUNT LAYER':'DEPLOYMENT LAYER')+'</i>'
|
| 158 |
+
+'<b>'+(layer==='account'?'ACCOUNT ARMORY':'SESSION LOADOUT')+'</b>'
|
| 159 |
+
+'<span>'+(layer==='account'?'Items stay in this career after every operation.':'Only fitted gear and readied supplies enter the next operation.')+'</span></div>'
|
| 160 |
+
+'<div class="armInvCapacity"><b>'+(layer==='account'?s.stored:(s.used+' / 5'))+'</b><span>'+(layer==='account'?'ITEMS STORED':'SLOT CAPACITY')+'</span></div></div>'
|
| 161 |
+
+'<div class="armInvMeters"><div><b>'+s.found+' / '+s.total+'</b><span>DISCOVERED</span></div>'
|
| 162 |
+
+'<div><b>'+s.gear+' / 3</b><span>GEAR SLOTS</span></div><div><b>'+s.supplies+' / 2</b><span>MISSION SLOTS</span></div></div>';
|
| 163 |
+
}
|
| 164 |
+
function armInvFilteredEntries(b){
|
| 165 |
+
const rank=Object.fromEntries(INV_RARITIES.map((r,i)=>[r.id,i]));
|
| 166 |
+
return armInvEntries().filter(e=>armInvFilter==='all'||(armInvFilter==='supply'?e.kind==='supply':e.kind==='gear'&&e.it.slot===armInvFilter))
|
| 167 |
+
.sort((a,z)=>(armInvOwned(z,b)>0?1:0)-(armInvOwned(a,b)>0?1:0)
|
| 168 |
+
||(armInvEquipped(z,b)?1:0)-(armInvEquipped(a,b)?1:0)
|
| 169 |
+
||rank[z.it.rarity]-rank[a.it.rarity]||a.it.nm.localeCompare(z.it.nm));
|
| 170 |
+
}
|
| 171 |
+
function armInvEnsureSelection(entries,b){
|
| 172 |
+
let e=entries.find(x=>x.kind===armInvSelectedKind&&x.it.id===armInvSelectedId);
|
| 173 |
+
if(!e) e=entries.find(x=>armInvEquipped(x,b))||entries.find(x=>armInvOwned(x,b)>0)||entries[0];
|
| 174 |
+
armInvSelectedKind=e?e.kind:''; armInvSelectedId=e?e.it.id:'';
|
| 175 |
+
return e;
|
| 176 |
+
}
|
| 177 |
+
function armInvFilterHTML(){
|
| 178 |
+
return '<div class="armInvFilters" aria-label="Armory item categories">'+ARM_INV_FILTERS.map(f=>
|
| 179 |
+
'<button type="button" class="armInvFilter'+(armInvFilter===f.id?' on':'')+'" data-inv-filter="'+f.id+'">'+f.nm+'</button>').join('')+'</div>';
|
| 180 |
+
}
|
| 181 |
+
function armInvComparison(e,b){
|
| 182 |
+
const fx=armInvEffect(e.it.id);
|
| 183 |
+
if(e.kind==='supply'){
|
| 184 |
+
const at=b.ready.indexOf(e.it.id);
|
| 185 |
+
if(at>=0) return 'MISSION SLOT '+(at+1)+' · one charge will be consumed when the operation launches';
|
| 186 |
+
if(b.ready.length>=2) return 'MISSION SLOTS FULL · remove or replace one supply first';
|
| 187 |
+
return 'EMPTY MISSION SLOT AVAILABLE · one charge is consumed at deployment';
|
| 188 |
+
}
|
| 189 |
+
const cur=INV_GEAR.find(g=>g.id===b.equipped[e.it.slot]);
|
| 190 |
+
if(!cur) return 'EMPTY '+e.it.slot.toUpperCase()+' SLOT · this full effect will be added';
|
| 191 |
+
if(cur.id===e.it.id) return 'CURRENT '+e.it.slot.toUpperCase()+' SLOT · persistent until unequipped';
|
| 192 |
+
const old=armInvEffect(cur.id);
|
| 193 |
+
if(old.stat===fx.stat&&Number.isFinite(old.score)&&Number.isFinite(fx.score)){
|
| 194 |
+
const d=fx.score-old.score;
|
| 195 |
+
return (d>=0?'+':'')+d+' POINT'+(Math.abs(d)===1?'':'S')+' VS '+cur.nm.toUpperCase();
|
| 196 |
+
}
|
| 197 |
+
return 'REPLACES '+cur.nm.toUpperCase()+' · changes '+old.stat.toLowerCase()+' to '+fx.stat.toLowerCase();
|
| 198 |
+
}
|
| 199 |
+
function armInvPreviewHTML(e,b){
|
| 200 |
+
if(!e) return '<div class="armInvEmpty"><b>NO ITEMS IN THIS CATEGORY</b><span>Complete operations to recover gear and mission supplies.</span></div>';
|
| 201 |
+
const it=e.it,n=armInvOwned(e,b),on=armInvEquipped(e,b),r=invRarity(it.rarity),fx=armInvEffect(it.id);
|
| 202 |
+
const full=e.kind==='supply'&&!on&&b.ready.length>=2;
|
| 203 |
+
const action=!n?'RECOVER IN OPERATIONS':on?(e.kind==='gear'?'UNEQUIP FROM SESSION':'REMOVE FROM MISSION')
|
| 204 |
+
:full?'MISSION SLOTS FULL':(e.kind==='gear'?'EQUIP '+it.slot.toUpperCase():'READY FOR MISSION');
|
| 205 |
+
return '<section class="armInvPreview" style="--rar:'+r.col+'" aria-label="Selected item effect preview">'
|
| 206 |
+
+'<div class="armInvPreviewArt">'+armInvIcon(it,54)+'</div><div class="armInvPreviewInfo"><i>'+r.nm+' · '+(e.kind==='gear'?it.slot.toUpperCase()+' GEAR':'MISSION CONSUMABLE')+'</i>'
|
| 207 |
+
+'<b>'+it.nm+'</b><span>ACCOUNT STOCK · '+n+'</span></div>'
|
| 208 |
+
+'<div class="armInvExact"><span>EXACT EFFECT</span><b>'+fx.value+'</b><i>'+fx.stat+'</i></div>'
|
| 209 |
+
+'<p>'+it.ds+'</p><div class="armInvCompare">'+armInvComparison(e,b)+'</div>'
|
| 210 |
+
+'<button type="button" class="armInvCommit'+((!n||full)?' disabled':'')+'" data-inv-action="'+e.kind+'" '+((!n||full)?'disabled':'')+'>'+action+'</button>'
|
| 211 |
+
+'<small>'+(e.kind==='gear'?'Gear is never consumed. It remains fitted until you change this slot.':'Readied supplies remain in Account Armory until the operation begins, then consume one charge.')+'</small></section>';
|
| 212 |
+
}
|
| 213 |
+
function armInvItemHTML(e,b){
|
| 214 |
+
const it=e.it,n=armInvOwned(e,b),on=armInvEquipped(e,b),sel=e.kind===armInvSelectedKind&&it.id===armInvSelectedId;
|
| 215 |
+
const r=invRarity(it.rarity),fx=armInvEffect(it.id),slot=e.kind==='gear'?it.slot.toUpperCase():'SUPPLY';
|
| 216 |
+
return '<button type="button" class="armVaultItem'+(sel?' selected':'')+(on?' equipped':'')+(n?'':' locked')+'" data-inv-kind="'+e.kind+'" data-inv-id="'+it.id+'" style="--rar:'+r.col+'" aria-pressed="'+(sel?'true':'false')+'">'
|
| 217 |
+
+'<span class="armVaultArt">'+armInvIcon(it,38)+'</span><span class="armVaultCopy"><i>'+r.nm+' · '+slot+'</i><b>'+it.nm+'</b><em>'+fx.value+' '+fx.stat+'</em></span>'
|
| 218 |
+
+'<span class="armVaultState">'+(n?(on?'IN LOADOUT':'×'+n):'LOCKED')+'</span></button>';
|
| 219 |
+
}
|
| 220 |
+
function armInventoryHTML(){
|
| 221 |
+
const b=invBag(),entries=armInvFilteredEntries(b),selected=armInvEnsureSelection(entries,b);
|
| 222 |
+
return armInvLayerStatsHTML(b,'account')+invRarityLegend()+armInvFilterHTML()+armInvPreviewHTML(selected,b)
|
| 223 |
+
+'<div class="armInvSectionLabel"><b>STORED ITEMS</b><span>'+entries.filter(e=>armInvOwned(e,b)>0).length+' OWNED IN VIEW</span></div>'
|
| 224 |
+
+'<div class="armVaultList">'+entries.map(e=>armInvItemHTML(e,b)).join('')+'</div>';
|
| 225 |
+
}
|
| 226 |
+
function armLoadGearSlotHTML(slot,b){
|
| 227 |
+
const g=INV_GEAR.find(x=>x.id===b.equipped[slot]);
|
| 228 |
+
if(!g) return '<div class="armLoadSlot empty"><div class="armLoadSlotTop"><i>'+slot.toUpperCase()+'</i><span>GEAR SLOT</span></div>'
|
| 229 |
+
+'<b>EMPTY '+slot.toUpperCase()+'</b><small>No account gear is assigned.</small><button type="button" data-pick-slot="'+slot+'">CHOOSE '+slot.toUpperCase()+'</button></div>';
|
| 230 |
+
const r=invRarity(g.rarity),fx=armInvEffect(g.id);
|
| 231 |
+
return '<div class="armLoadSlot filled" style="--rar:'+r.col+'"><div class="armLoadSlotTop"><i>'+slot.toUpperCase()+'</i><span>'+r.nm+'</span></div>'
|
| 232 |
+
+'<div class="armLoadEquipped"><span>'+armInvIcon(g,36)+'</span><div><b>'+g.nm+'</b><em>'+fx.value+' '+fx.stat+'</em></div></div>'
|
| 233 |
+
+'<button type="button" data-remove-gear="'+g.id+'">UNEQUIP</button></div>';
|
| 234 |
+
}
|
| 235 |
+
function armLoadSupplySlotHTML(n,b){
|
| 236 |
+
const c=INV_CONSUMABLES.find(x=>x.id===b.ready[n]);
|
| 237 |
+
if(!c) return '<div class="armLoadSlot supply empty"><div class="armLoadSlotTop"><i>MISSION '+(n+1)+'</i><span>CONSUMABLE</span></div>'
|
| 238 |
+
+'<b>EMPTY SUPPLY SLOT</b><small>Choose one charge from Account Armory.</small><button type="button" data-pick-slot="supply">CHOOSE SUPPLY</button></div>';
|
| 239 |
+
const r=invRarity(c.rarity),fx=armInvEffect(c.id),stock=b.consumables[c.id]||0;
|
| 240 |
+
return '<div class="armLoadSlot supply filled" style="--rar:'+r.col+'"><div class="armLoadSlotTop"><i>MISSION '+(n+1)+'</i><span>'+r.nm+'</span></div>'
|
| 241 |
+
+'<div class="armLoadEquipped"><span>'+armInvIcon(c,36)+'</span><div><b>'+c.nm+'</b><em>'+fx.value+' '+fx.stat+'</em></div></div>'
|
| 242 |
+
+'<small>'+stock+' in Account Armory · consumes 1 at launch</small><button type="button" data-remove-cons="'+c.id+'">REMOVE</button></div>';
|
| 243 |
+
}
|
| 244 |
+
function armSessionEffectsHTML(b){
|
| 245 |
+
const active=[];
|
| 246 |
+
for(const s of ['weapon','armor','utility']){
|
| 247 |
+
const g=INV_GEAR.find(x=>x.id===b.equipped[s]); if(g) active.push({it:g,kind:'GEAR · '+s.toUpperCase()});
|
| 248 |
+
}
|
| 249 |
+
for(const id of b.ready){ const c=INV_CONSUMABLES.find(x=>x.id===id); if(c) active.push({it:c,kind:'ONE-MISSION SUPPLY'}); }
|
| 250 |
+
return '<section class="armMissionPackage"><div class="armInvSectionLabel"><b>OPERATION EFFECT PACKAGE</b><span>'+active.length+' / 5 ACTIVE</span></div>'
|
| 251 |
+
+(active.length?active.map(e=>{const r=invRarity(e.it.rarity),fx=armInvEffect(e.it.id);return '<div class="armMissionFx" style="--rar:'+r.col+'"><i></i><div><b>'+e.it.nm+'</b><span>'+e.kind+'</span></div><strong>'+fx.value+' '+fx.stat+'</strong></div>';}).join('')
|
| 252 |
+
:'<div class="armInvEmpty"><b>BASELINE DEPLOYMENT</b><span>No account equipment or mission supplies selected.</span></div>')
|
| 253 |
+
+'<p>Gear effects persist every operation. Mission supplies apply once and are consumed only when deployment begins.</p></section>';
|
| 254 |
+
}
|
| 255 |
+
function armSessionLoadoutHTML(){
|
| 256 |
const b=invBag();
|
| 257 |
+
return armInvLayerStatsHTML(b,'session')+'<div class="armLoadoutRule"><b>5 HARD SLOTS</b><span>One weapon · one armor · one utility · two mission supplies</span></div>'
|
| 258 |
+
+'<div class="armLoadoutGrid">'+['weapon','armor','utility'].map(s=>armLoadGearSlotHTML(s,b)).join('')
|
| 259 |
+
+armLoadSupplySlotHTML(0,b)+armLoadSupplySlotHTML(1,b)+'</div>'+armSessionEffectsHTML(b)
|
| 260 |
+
+'<button type="button" class="armReturnVault" data-open-vault="1">▦ OPEN ACCOUNT ARMORY</button>';
|
|
|
|
|
|
|
|
|
|
| 261 |
}
|
| 262 |
function armColorsHTML(){
|
| 263 |
let h='<div class="sHead">COMMANDER COLORS</div><div id="colorRow">';
|
|
|
|
| 276 |
return '<div class="armTabWrap"><div class="tabRow" id="armTabs">'
|
| 277 |
+ARM_CATS.map(c=>{
|
| 278 |
const bag=invBag();
|
| 279 |
+
const invStats=armInvStats(bag);
|
| 280 |
+
const done=c.id==='inventory'?invStats.found
|
| 281 |
+
:c.id==='loadout'?invStats.used
|
| 282 |
:c.id==='identity'?0:c.items.filter(id=>{ const it=STORE.find(s=>s.id===id); return it&&(META.owned[it.id]||0)>=it.max; }).length;
|
| 283 |
+
const tot=c.id==='inventory'?invStats.total:c.id==='loadout'?5:c.items.length;
|
| 284 |
return '<button class="tabBtn'+(armTab===c.id?' on':'')+'" data-k="'+c.id+'">'
|
| 285 |
+
+'<span class="tEm">'+c.em+'</span>'+c.nm+(tot?' <span class="tabCt">'+done+'/'+tot+'</span>':'')+'</button>';
|
|
|
|
|
|
|
| 286 |
}).join('')
|
| 287 |
+'</div><div class="armTabHint">SWIPE CATEGORIES →</div></div>';
|
| 288 |
}
|
|
|
|
| 320 |
list.querySelectorAll('.tabBtn').forEach(btn=>{
|
| 321 |
mfBindTap(btn,ev=>{
|
| 322 |
ev.stopPropagation();
|
| 323 |
+
armTab=btn.dataset.k; sfx('ui'); renderArmory(); list.scrollTop=0;
|
| 324 |
});
|
| 325 |
});
|
| 326 |
|
| 327 |
+
list.querySelectorAll('.armInvFilter').forEach(el=>{
|
| 328 |
+
mfBindTap(el,ev=>{
|
| 329 |
+
ev.stopPropagation();
|
| 330 |
+
armInvFilter=el.dataset.invFilter||'all';
|
| 331 |
+
armInvSelectedKind=''; armInvSelectedId='';
|
| 332 |
+
sfx('ui'); renderArmory(); list.scrollTop=0;
|
| 333 |
+
});
|
| 334 |
+
});
|
| 335 |
+
list.querySelectorAll('.armVaultItem').forEach(el=>{
|
| 336 |
mfBindTap(el,ev=>{
|
| 337 |
ev.stopPropagation();
|
| 338 |
+
armInvSelectedKind=el.dataset.invKind||''; armInvSelectedId=el.dataset.invId||'';
|
| 339 |
+
sfx('ui'); renderArmory();
|
| 340 |
+
requestAnimationFrame(()=>{ const p=list.querySelector('.armInvPreview'); if(p) list.scrollTop=Math.max(0,p.offsetTop-112); });
|
| 341 |
});
|
| 342 |
});
|
| 343 |
+
list.querySelectorAll('.armInvCommit:not([disabled])').forEach(el=>{
|
| 344 |
mfBindTap(el,ev=>{
|
| 345 |
ev.stopPropagation();
|
| 346 |
+
if(el.dataset.invAction==='gear'){
|
| 347 |
+
const g=INV_GEAR.find(x=>x.id===armInvSelectedId);
|
| 348 |
+
if(g&&invEquipGear(g.id)){
|
| 349 |
+
const on=invBag().equipped[g.slot]===g.id;
|
| 350 |
+
toast((on?'Fitted ':'Removed ')+g.nm+(on?' to the '+g.slot+' slot':'')); sfx('ui'); renderArmory();
|
| 351 |
+
}
|
| 352 |
+
}else{
|
| 353 |
+
const c=INV_CONSUMABLES.find(x=>x.id===armInvSelectedId);
|
| 354 |
+
if(c&&invReadyConsumable(c.id)){
|
| 355 |
+
const on=invBag().ready.indexOf(c.id)>=0;
|
| 356 |
+
toast((on?'Readied ':'Removed ')+c.nm+(on?' for the next mission':'')); sfx('ui'); renderArmory();
|
| 357 |
+
}
|
| 358 |
+
}
|
| 359 |
});
|
| 360 |
});
|
| 361 |
+
list.querySelectorAll('[data-pick-slot]').forEach(el=>mfBindTap(el,ev=>{
|
| 362 |
+
ev.stopPropagation(); armTab='inventory'; armInvFilter=el.dataset.pickSlot||'all';
|
| 363 |
+
armInvSelectedKind=''; armInvSelectedId=''; sfx('ui'); renderArmory(); list.scrollTop=0;
|
| 364 |
+
}));
|
| 365 |
+
list.querySelectorAll('[data-remove-gear]').forEach(el=>mfBindTap(el,ev=>{
|
| 366 |
+
ev.stopPropagation(); const g=INV_GEAR.find(x=>x.id===el.dataset.removeGear);
|
| 367 |
+
if(g&&invEquipGear(g.id)){ toast('Removed '+g.nm+' from session loadout'); sfx('ui'); renderArmory(); }
|
| 368 |
+
}));
|
| 369 |
+
list.querySelectorAll('[data-remove-cons]').forEach(el=>mfBindTap(el,ev=>{
|
| 370 |
+
ev.stopPropagation(); const c=INV_CONSUMABLES.find(x=>x.id===el.dataset.removeCons);
|
| 371 |
+
if(c&&invReadyConsumable(c.id)){ toast('Returned '+c.nm+' to Account Armory'); sfx('ui'); renderArmory(); }
|
| 372 |
+
}));
|
| 373 |
+
list.querySelectorAll('[data-open-vault]').forEach(el=>mfBindTap(el,ev=>{
|
| 374 |
+
ev.stopPropagation(); armTab='inventory'; sfx('ui'); renderArmory(); list.scrollTop=0;
|
| 375 |
+
}));
|
| 376 |
|
| 377 |
list.querySelectorAll('.armIt').forEach(el=>{
|
| 378 |
el.addEventListener('pointerdown',()=>{
|
src/story.js
CHANGED
|
@@ -326,3 +326,211 @@ function renderDossier(){
|
|
| 326 |
});
|
| 327 |
storyRefreshBadge();
|
| 328 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
});
|
| 327 |
storyRefreshBadge();
|
| 328 |
}
|
| 329 |
+
|
| 330 |
+
/* ============================================================================
|
| 331 |
+
CAMPAIGN PROLOGUE — a playable front door for first-time commanders
|
| 332 |
+
----------------------------------------------------------------------------
|
| 333 |
+
This is deliberately a takeover of Operations rather than a second mission
|
| 334 |
+
menu. The prologue launches the proven Training Operation or pre-fills the
|
| 335 |
+
real Battle Setup screen, so every BEGIN button reaches playable rules and
|
| 336 |
+
none of the PRE-ALPHA story cards pretend that an unbuilt scripted mission
|
| 337 |
+
exists. Faction keys stay identical to ai.js/save data.
|
| 338 |
+
============================================================================ */
|
| 339 |
+
const STORY_CAMPAIGN_PROLOGUE=[
|
| 340 |
+
{id:'orientation',code:'P-00',ttl:'FIELD ORIENTATION',fac:'nova',map:'vanguard',theme:'verdant',
|
| 341 |
+
threat:'COMMAND SIMULATION',goal:'training',diff:0,time:0,inf:false,defense:1,scale:'standard',
|
| 342 |
+
objective:'Deploy the Commander, establish power and production, build a defensive line, then lead a formation through its first attack order.',
|
| 343 |
+
reward:'150 CORES · COMMAND CERTIFICATION',unlock:'start',complete:'training'},
|
| 344 |
+
{id:'first-breach',code:'P-01',ttl:'THE FIRST BREACH',fac:'ascendancy',map:'vanguard',theme:'verdant',
|
| 345 |
+
threat:'CRIMSON DOMINION',goal:'annihilate',diff:0,time:0,inf:false,defense:1,scale:'standard',
|
| 346 |
+
objective:'Fortify the valley approach, preserve the Commander, and destroy the Dominion command unit before its armour column breaks your line.',
|
| 347 |
+
reward:'MATCH XP · CORES · CRIMSON SERVICE RECORD',unlock:'training',complete:'legion'},
|
| 348 |
+
{id:'coalition-intercept',code:'P-02',ttl:'COALITION INTERCEPT',fac:'syndicate',map:'highland',theme:'arctic',
|
| 349 |
+
threat:'SYNDICATE COALITION',goal:'domination',diff:1,time:600,inf:false,defense:0,scale:'standard',
|
| 350 |
+
objective:'Take and hold the Highland Scar resource lanes while Coalition energy cadres probe the flanks and contest the ruined arcology.',
|
| 351 |
+
reward:'MATCH XP · CORES · SYNDICATE SERVICE RECORD',unlock:'legion',complete:'syndicate'},
|
| 352 |
+
{id:'ground-remembers',code:'P-03',ttl:'THE GROUND REMEMBERS',fac:'horde',map:'crater',theme:'ashland',
|
| 353 |
+
threat:'BROOD SWARM',goal:'purge',diff:1,time:900,inf:true,defense:1,scale:'large',
|
| 354 |
+
objective:'Establish overlapping fields of fire in Relic Basin, burn out every living nest, and survive the Brood organisms defending the infestation.',
|
| 355 |
+
reward:'MATCH XP · CORES · BROOD SERVICE RECORD',unlock:'syndicate',complete:'horde'},
|
| 356 |
+
];
|
| 357 |
+
|
| 358 |
+
function storyCampaignComplete(m){
|
| 359 |
+
if(m.complete==='training') return !!(META.tutorial&&META.tutorial.done);
|
| 360 |
+
return !!((META.facWins||{})[m.complete]>0);
|
| 361 |
+
}
|
| 362 |
+
function storyCampaignUnlocked(m,i){
|
| 363 |
+
if(i===0||storyCampaignComplete(m)) return true;
|
| 364 |
+
return storyCampaignComplete(STORY_CAMPAIGN_PROLOGUE[i-1]);
|
| 365 |
+
}
|
| 366 |
+
function storyCampaignProgress(){
|
| 367 |
+
const states=STORY_CAMPAIGN_PROLOGUE.map((m,i)=>({
|
| 368 |
+
done:storyCampaignComplete(m),open:storyCampaignUnlocked(m,i)
|
| 369 |
+
}));
|
| 370 |
+
let current=states.findIndex(s=>s.open&&!s.done);
|
| 371 |
+
if(current<0) current=STORY_CAMPAIGN_PROLOGUE.length-1;
|
| 372 |
+
return {states,current,done:states.filter(s=>s.done).length};
|
| 373 |
+
}
|
| 374 |
+
function storyCampaignEnsureOps(){
|
| 375 |
+
const root=document.getElementById('opsScr'),tabs=document.getElementById('opsTabs');
|
| 376 |
+
if(!root||!tabs) return;
|
| 377 |
+
if(!document.getElementById('opsTab-campaign')){
|
| 378 |
+
const tab=document.createElement('button');
|
| 379 |
+
tab.className='screenTabBtn'; tab.id='opsTab-campaign'; tab.type='button';
|
| 380 |
+
tab.setAttribute('role','tab'); tab.dataset.mfTab='campaign';
|
| 381 |
+
tab.setAttribute('aria-controls','opsPane-campaign');
|
| 382 |
+
tab.innerHTML='<span class="tabGlyph">◇</span><span>CAMPAIGN</span>';
|
| 383 |
+
tabs.insertBefore(tab,tabs.firstElementChild);
|
| 384 |
+
}
|
| 385 |
+
if(!document.getElementById('opsPane-campaign')){
|
| 386 |
+
const pane=document.createElement('section');
|
| 387 |
+
pane.className='setupCard mfTabPanel storyCampaignPane';
|
| 388 |
+
pane.id='opsPane-campaign'; pane.setAttribute('role','tabpanel');
|
| 389 |
+
pane.setAttribute('aria-labelledby','opsTab-campaign'); pane.dataset.mfPanel='campaign';
|
| 390 |
+
const scroll=root.querySelector('.opsScroll'),first=scroll&&scroll.firstElementChild;
|
| 391 |
+
if(scroll) scroll.insertBefore(pane,first||null);
|
| 392 |
+
}
|
| 393 |
+
/* A new commander sees the authored starting path first. Returning players
|
| 394 |
+
keep whichever Operations category they last selected. */
|
| 395 |
+
if(typeof MF_TAB_STATE!=='undefined'&&!MF_TAB_STATE.opsScr&&!(META.matches||0))
|
| 396 |
+
MF_TAB_STATE.opsScr='campaign';
|
| 397 |
+
}
|
| 398 |
+
function storyCampaignFac(m){
|
| 399 |
+
return (typeof facArt==='function'&&facArt(m.fac))||FACART.nova;
|
| 400 |
+
}
|
| 401 |
+
function storyCampaignPortrait(a){
|
| 402 |
+
return '<img src="./assets/factions/'+a.id+'_192.jpg" alt="'+a.cdr+', '+a.nm+' commander" '
|
| 403 |
+
+'onerror="this.style.visibility=\'hidden\'">';
|
| 404 |
+
}
|
| 405 |
+
const STORY_CAMPAIGN_KEY_ART={
|
| 406 |
+
nova:'./assets/factions/cinematic/terran-frontline-command-v1.png',
|
| 407 |
+
ascendancy:'./assets/factions/cinematic/crimson-dominion-v1.png',
|
| 408 |
+
syndicate:'./assets/factions/cinematic/syndicate-coalition-v1.png',
|
| 409 |
+
horde:'./assets/factions/cinematic/brood-swarm-v1.png'
|
| 410 |
+
};
|
| 411 |
+
function storyCampaignKeyArt(fac){
|
| 412 |
+
return STORY_CAMPAIGN_KEY_ART[fac]||STORY_CAMPAIGN_KEY_ART.nova;
|
| 413 |
+
}
|
| 414 |
+
function storyCampaignRender(){
|
| 415 |
+
const pane=document.getElementById('opsPane-campaign'); if(!pane) return;
|
| 416 |
+
const progress=storyCampaignProgress();
|
| 417 |
+
let missions='';
|
| 418 |
+
STORY_CAMPAIGN_PROLOGUE.forEach((m,i)=>{
|
| 419 |
+
const S=progress.states[i],A=storyCampaignFac(m),map=MAPDEFS[m.map]||{nm:m.map,ds:''},
|
| 420 |
+
haz=(typeof MAPHAZ!=='undefined'&&MAPHAZ[m.map])||{em:'◇',nm:'FIELD CONDITIONS'},
|
| 421 |
+
state=S.done?'COMPLETE':S.open?(i===progress.current?'CURRENT':'AVAILABLE'):'LOCKED',
|
| 422 |
+
unlock=i?('Complete '+STORY_CAMPAIGN_PROLOGUE[i-1].code+' · '+STORY_CAMPAIGN_PROLOGUE[i-1].ttl):'';
|
| 423 |
+
missions+='<article class="storyCampaignMission '+state.toLowerCase()+'" style="--cf:'+A.col+';--cf2:'+A.col2+'">'
|
| 424 |
+
+'<div class="storyCampaignKeyArt"><img src="'+storyCampaignKeyArt(m.fac)+'" alt="'+A.nm+' cinematic battlefield">'
|
| 425 |
+
+'<span>'+(typeof facIcon==='function'?facIcon(m.fac,30):'◇')+'</span><small>'+A.nm+'</small></div>'
|
| 426 |
+
+'<div class="storyCampaignMissionBody"><header><div><small>'+m.code+' · PROLOGUE</small><b>'+m.ttl+'</b></div>'
|
| 427 |
+
+'<i class="storyCampaignState">'+state+'</i></header>'
|
| 428 |
+
+'<div class="storyCampaignIntel"><span><em>OBJECTIVE</em>'+m.objective+'</span>'
|
| 429 |
+
+'<span><em>BATTLEFIELD</em>'+map.nm+' · '+haz.em+' '+haz.nm+'</span>'
|
| 430 |
+
+'<span><em>FACTION THREAT</em>'+A.nm+' · '+A.strong+'</span>'
|
| 431 |
+
+'<span class="storyCampaignReward"><em>REWARDS</em>'+m.reward+'</span></div>'
|
| 432 |
+
+(S.open?'<button class="storyCampaignAction" type="button" data-campaign-mission="'+m.id+'">'
|
| 433 |
+
+(S.done?'↻ REPLAY MISSION':'▶ '+(i===0?'BEGIN TRAINING':'BEGIN MISSION'))+'</button>'
|
| 434 |
+
:'<div class="storyCampaignLock">▣ '+unlock+'</div>')
|
| 435 |
+
+'</div></article>';
|
| 436 |
+
});
|
| 437 |
+
const arcs=Object.keys(FACART).map(k=>{
|
| 438 |
+
const A=FACART[k];
|
| 439 |
+
return '<article class="storyCampaignArc" style="--cf:'+A.col+';--cf2:'+A.col2+'">'
|
| 440 |
+
+'<div class="storyCampaignArcHead">'+storyCampaignPortrait(A)
|
| 441 |
+
+'<div><small>'+A.motto+'</small><b>'+A.nm+'</b><span>'+A.cdr+'</span></div></div>'
|
| 442 |
+
+'<div class="storyCampaignArcPath">'+A.arc+'</div><p>'+A.endgoal+'</p></article>';
|
| 443 |
+
}).join('');
|
| 444 |
+
const heroMission=STORY_CAMPAIGN_PROLOGUE[progress.current]||STORY_CAMPAIGN_PROLOGUE[0];
|
| 445 |
+
pane.innerHTML='<section class="storyCampaignHero" style="--campaign-hero:url("'+storyCampaignKeyArt(heroMission.fac)+'")"><div><span>PRE-ALPHA · PLAYABLE PROLOGUE</span>'
|
| 446 |
+
+'<h3>HALCYON REACH: FIRST CONTACT</h3><p>Learn the command loop, then meet all three enemy doctrines through live battle rules. '
|
| 447 |
+
+'Acts I–IV below are the campaign direction; only the Prologue ladder is presented as playable today.</p></div>'
|
| 448 |
+
+'<div class="storyCampaignCount"><b>'+progress.done+'</b><span>/ '+STORY_CAMPAIGN_PROLOGUE.length+' CLEARED</span></div></section>'
|
| 449 |
+
+'<div class="storyCampaignSection"><span>PROLOGUE MISSION LADDER</span><small>Each deployment uses the real training or battle setup flow.</small></div>'
|
| 450 |
+
+'<div class="storyCampaignLadder">'+missions+'</div>'
|
| 451 |
+
+'<div class="storyCampaignSection storyCampaignArcLabel"><span>FOUR FACTION ENDGAMES · CAMPAIGN PREVIEW</span>'
|
| 452 |
+
+'<small>Swipe to inspect the long-form arc promised by each faction.</small></div>'
|
| 453 |
+
+'<div class="storyCampaignArcs">'+arcs+'</div>';
|
| 454 |
+
pane.querySelectorAll('.storyCampaignAction').forEach(b=>{
|
| 455 |
+
const go=()=>storyCampaignOpenMission(b.dataset.campaignMission);
|
| 456 |
+
if(typeof mfBindTap==='function') mfBindTap(b,go); else b.addEventListener('click',go);
|
| 457 |
+
});
|
| 458 |
+
}
|
| 459 |
+
function storyCampaignToggle(sel,match){
|
| 460 |
+
document.querySelectorAll(sel).forEach(b=>b.classList.toggle('on',match(b)));
|
| 461 |
+
}
|
| 462 |
+
function storyCampaignSyncSetup(){
|
| 463 |
+
const setup=document.getElementById('setupScr');
|
| 464 |
+
if(setup){
|
| 465 |
+
setup.dataset.tab='map'; setup.classList.remove('setupAll');
|
| 466 |
+
setup.querySelectorAll('.setupTabBtn').forEach(b=>b.classList.toggle('on',b.dataset.tab==='map'));
|
| 467 |
+
const adv=document.getElementById('advToggle'); if(adv){adv.classList.remove('on');adv.textContent='SHOW ALL';}
|
| 468 |
+
}
|
| 469 |
+
if(typeof renderFacRow==='function') renderFacRow();
|
| 470 |
+
storyCampaignToggle('.glbtn',b=>b.dataset.g===goalSel);
|
| 471 |
+
storyCampaignToggle('.tmbtn',b=>+b.dataset.t===timeLimit);
|
| 472 |
+
storyCampaignToggle('.pcbtn',b=>+b.dataset.p===resPace);
|
| 473 |
+
storyCampaignToggle('.crbtn',b=>+b.dataset.c===crateRate);
|
| 474 |
+
storyCampaignToggle('.bsbtn',b=>b.dataset.bs===battlefieldPreset);
|
| 475 |
+
storyCampaignToggle('.ifbtn',b=>!!+b.dataset.i===infestationOn);
|
| 476 |
+
storyCampaignToggle('.globalDiff',b=>+b.dataset.d===difficulty);
|
| 477 |
+
storyCampaignToggle('.tbtn',b=>b.dataset.t===curTheme);
|
| 478 |
+
storyCampaignToggle('.fbtn',b=>b.dataset.f===aiFactionSel);
|
| 479 |
+
storyCampaignToggle('.dfbtn',b=>+b.dataset.df===defenseFocus);
|
| 480 |
+
storyCampaignToggle('.wbtn',b=>+b.dataset.w===wcChoice);
|
| 481 |
+
const goal=document.getElementById('goalHint'); if(goal) goal.textContent=goalDef().ds;
|
| 482 |
+
const scale=document.getElementById('battleScaleHint'),scaleDef=battlefieldPresetDef();
|
| 483 |
+
if(scale) scale.textContent=scaleDef.km+' — '+scaleDef.ds;
|
| 484 |
+
const infestation=document.getElementById('infestHint'); if(infestation) infestation.textContent=infestationOn
|
| 485 |
+
?'Neutral nests spread and erupt during the battle.'
|
| 486 |
+
:'No neutral nests, infestation guards, eruptions, spread, or map-wide tides.';
|
| 487 |
+
const defence=document.getElementById('defFocusHint'); if(defence) defence.textContent=defenseFocus
|
| 488 |
+
?'Tower defence: stronger, longer-ranged fortifications against faster enemy pressure.'
|
| 489 |
+
:'Classic RTS balance between mobile armies and static defences.';
|
| 490 |
+
if(typeof renderMapRow==='function') renderMapRow();
|
| 491 |
+
if(typeof renderSpawnPlanner==='function') renderSpawnPlanner();
|
| 492 |
+
}
|
| 493 |
+
function storyCampaignOpenMission(missionId){
|
| 494 |
+
const i=STORY_CAMPAIGN_PROLOGUE.findIndex(m=>m.id===missionId),m=STORY_CAMPAIGN_PROLOGUE[i];
|
| 495 |
+
if(!m||!storyCampaignUnlocked(m,i)) return;
|
| 496 |
+
if(m.goal==='training'){
|
| 497 |
+
const debug=window.__tutDebug&&window.__tutDebug();
|
| 498 |
+
if(debug&&typeof debug.startTraining==='function'){
|
| 499 |
+
if(typeof initAudio==='function') initAudio();
|
| 500 |
+
debug.startTraining(); return;
|
| 501 |
+
}
|
| 502 |
+
if(typeof toast==='function') toast('Training systems are still loading — try again in a moment.');
|
| 503 |
+
return;
|
| 504 |
+
}
|
| 505 |
+
difficulty=m.diff; defenseFocus=m.defense?1:0; infestationOn=!!m.inf; wcChoice=0;
|
| 506 |
+
goalSel=m.goal; timeLimit=m.time; resPace=1; crateRate=crateRateBase=1;
|
| 507 |
+
curMap=m.map; curTheme=m.theme; battlefieldPreset=m.scale;
|
| 508 |
+
aiFactionSel=storyCampaignFac(m).key; playerStartZone='sw'; spawnPick='player';
|
| 509 |
+
aiSlots.forEach((A,n)=>{A.on=n===0;A.diff=m.diff;A.zone=n===0?'ne':n===1?'nw':'se';});
|
| 510 |
+
storyCampaignSyncSetup();
|
| 511 |
+
if(typeof renderOps==='function') renderOps();
|
| 512 |
+
if(typeof initAudio==='function') initAudio();
|
| 513 |
+
if(typeof sfx==='function') sfx('confirm');
|
| 514 |
+
if(typeof showFrontScreen==='function') showFrontScreen('setupScr');
|
| 515 |
+
const scroll=document.querySelector('#setupScr .setupScroll'); if(scroll) scroll.scrollTop=0;
|
| 516 |
+
if(typeof toast==='function') toast(m.code+' · '+m.ttl+' — deployment preset loaded');
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
/* story.js loads after endgame.js, so the wrapper is installed before initOps
|
| 520 |
+
binds its tab listener. tutorial.js later wraps this wrapper in turn; both
|
| 521 |
+
additions therefore survive without either feature editing the other. */
|
| 522 |
+
const STORY_CAMPAIGN_RENDER_OPS_BASE=renderOps;
|
| 523 |
+
renderOps=function(){
|
| 524 |
+
storyCampaignEnsureOps();
|
| 525 |
+
STORY_CAMPAIGN_RENDER_OPS_BASE();
|
| 526 |
+
storyCampaignRender();
|
| 527 |
+
};
|
| 528 |
+
const STORY_CAMPAIGN_RENDER_BRIEF_BASE=renderOpsBrief;
|
| 529 |
+
renderOpsBrief=function(mode){
|
| 530 |
+
STORY_CAMPAIGN_RENDER_BRIEF_BASE(mode);
|
| 531 |
+
if(mode!=='campaign') return;
|
| 532 |
+
const p=storyCampaignProgress(),set=(id,v)=>{const e=document.getElementById(id);if(e)e.textContent=v;};
|
| 533 |
+
set('opsBriefThreat','P0'); set('opsBriefThreatNm','PLAYABLE PROLOGUE');
|
| 534 |
+
set('opsBriefMods',p.done+'/'+STORY_CAMPAIGN_PROLOGUE.length); set('opsBriefModsNm','MISSIONS CLEARED');
|
| 535 |
+
set('opsBriefPayout','LIVE'); set('opsBriefState','PRE-ALPHA');
|
| 536 |
+
};
|
src/styles/intro.css
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Logo-only PRE-ALPHA opening. Faction dossier/contact-sheet art is
|
| 2 |
+
intentionally restricted to the codex and never used as a splash. */
|
| 3 |
+
.mfTitleReveal{position:fixed;inset:0;z-index:950;display:grid;grid-template-rows:auto 1fr auto;overflow:hidden;isolation:isolate;
|
| 4 |
+
padding:max(14px,calc(var(--sat) + 12px)) max(16px,calc(var(--sar) + 14px)) max(16px,calc(var(--sab) + 12px)) max(16px,calc(var(--sal) + 14px));
|
| 5 |
+
color:#edf8ff;background:#02060c;opacity:0;transform:scale(1.025);transition:opacity .28s ease,transform .7s cubic-bezier(.16,.8,.2,1);touch-action:manipulation}
|
| 6 |
+
.mfTitleReveal[hidden]{display:none!important}.mfTitleReveal.open{opacity:1;transform:none}
|
| 7 |
+
.mfTitleBackdrop{position:absolute;inset:0;z-index:-1;overflow:hidden;background:
|
| 8 |
+
radial-gradient(circle at 50% 47%,rgba(41,141,213,.16),transparent 23%),
|
| 9 |
+
radial-gradient(ellipse at 50% 105%,rgba(255,199,83,.08),transparent 43%),
|
| 10 |
+
linear-gradient(180deg,#02050a,#06101b 52%,#02050a)}
|
| 11 |
+
.mfTitleBackdrop::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.36),transparent 30%,transparent 70%,rgba(0,0,0,.36));box-shadow:inset 0 0 120px rgba(0,0,0,.75)}
|
| 12 |
+
.mfTitleGrid{position:absolute;inset:-10%;opacity:.2;background-image:
|
| 13 |
+
linear-gradient(rgba(92,190,255,.08) 1px,transparent 1px),linear-gradient(90deg,rgba(92,190,255,.07) 1px,transparent 1px);
|
| 14 |
+
background-size:42px 42px;transform:perspective(520px) rotateX(63deg) translateY(29%);mask-image:linear-gradient(180deg,transparent 16%,#000 64%,transparent)}
|
| 15 |
+
.mfTitleSweep{position:absolute;inset:-35% -20%;background:linear-gradient(103deg,transparent 45%,rgba(118,213,255,.11) 50%,transparent 55%);animation:mfTitleSweep 3.4s ease-in-out infinite}
|
| 16 |
+
@keyframes mfTitleSweep{from{transform:translateX(-28%)}to{transform:translateX(28%)}}
|
| 17 |
+
.mfTitleTop{display:flex;justify-content:space-between;align-items:center;width:min(100%,920px);margin:0 auto}
|
| 18 |
+
.mfTitleBuild{display:flex;align-items:center;gap:9px;color:#a9bfd1;font:800 10px/1 var(--fT);letter-spacing:.12em}
|
| 19 |
+
.mfTitleBuild b{padding:8px 11px;color:#160e00;background:linear-gradient(180deg,#ffeaa2,#d5a13b);clip-path:polygon(8px 0,100% 0,100% calc(100% - 8px),calc(100% - 8px) 100%,0 100%,0 8px)}
|
| 20 |
+
.mfTitleSkip{min-width:54px;min-height:48px;padding:0 12px;border:1px solid rgba(158,200,226,.34);border-radius:11px;color:#dcedf8;background:rgba(5,13,23,.78);font:800 10px/1 var(--fT);letter-spacing:.12em}
|
| 21 |
+
.mfTitleSkip span{color:#ffd46e;margin-left:4px}.mfTitleSkip:active{transform:scale(.96)}
|
| 22 |
+
.mfTitleCenter{align-self:center;justify-self:center;width:min(94vw,900px);text-align:center;animation:mfTitleCenterIn .9s cubic-bezier(.16,.8,.2,1) both}
|
| 23 |
+
@keyframes mfTitleCenterIn{from{opacity:0;transform:translateY(18px) scale(.96);filter:blur(8px)}to{opacity:1;transform:none;filter:none}}
|
| 24 |
+
.mfTitleInsignia{position:relative;width:clamp(76px,16vw,128px);height:clamp(76px,16vw,128px);margin:0 auto 18px}
|
| 25 |
+
.mfTitleInsignia img{position:relative;z-index:2;width:100%;height:100%;object-fit:contain;filter:drop-shadow(0 0 22px rgba(88,194,255,.46))}
|
| 26 |
+
.mfTitleInsignia i{position:absolute;inset:8%;border:1px solid rgba(100,203,255,.35);transform:rotate(45deg);box-shadow:0 0 30px rgba(75,181,240,.15);animation:mfTitleInsignia 2.8s linear infinite}
|
| 27 |
+
@keyframes mfTitleInsignia{to{transform:rotate(405deg)}}
|
| 28 |
+
.mfTitleEyebrow{margin-bottom:10px;color:#ffd46e;font:800 clamp(8px,1.8vw,12px)/1 var(--fU);letter-spacing:.26em;text-shadow:0 0 14px rgba(255,205,92,.32)}
|
| 29 |
+
.mfTitleCenter h1{margin:0;font:900 clamp(52px,13vw,118px)/.9 var(--fT);letter-spacing:.09em;background:linear-gradient(180deg,#fff 2%,#dbe6ed 36%,#6e8191 51%,#edf7fb 66%,#536b7d 100%);
|
| 30 |
+
-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 13px 24px rgba(0,0,0,.85));animation:mfTitleWordmark 1.4s ease both}
|
| 31 |
+
@keyframes mfTitleWordmark{0%{opacity:0;letter-spacing:.19em;filter:blur(7px)}55%{opacity:1}100%{letter-spacing:.09em;filter:drop-shadow(0 13px 24px rgba(0,0,0,.85))}}
|
| 32 |
+
.mfTitleSubtitle{display:flex;align-items:center;justify-content:center;gap:13px;margin:18px auto 0;color:#f1c969;font:900 clamp(10px,2.6vw,17px)/1 var(--fT);letter-spacing:.25em}
|
| 33 |
+
.mfTitleSubtitle i{width:min(16vw,130px);height:1px;background:linear-gradient(90deg,transparent,#d9ae50)}.mfTitleSubtitle i:last-child{transform:scaleX(-1)}
|
| 34 |
+
.mfTitleStatus{display:inline-flex;align-items:center;gap:8px;margin-top:25px;color:#91aabd;font:700 9px/1 var(--fU);letter-spacing:.19em}
|
| 35 |
+
.mfTitleStatus span{width:7px;height:7px;border-radius:50%;background:#6ed6ff;box-shadow:0 0 12px #6ed6ff;animation:mfTitlePulse 1.25s ease-in-out infinite}
|
| 36 |
+
@keyframes mfTitlePulse{50%{opacity:.3;transform:scale(.72)}}
|
| 37 |
+
.mfTitleBottom{width:min(100%,460px);margin:0 auto}.mfTitleStart{width:100%;min-height:56px;border:1px solid #fff1bb;border-radius:12px;color:#1b1101;background:linear-gradient(180deg,#fff0af,#e9bb52 47%,#bd8730 53%,#f2cd70);box-shadow:0 10px 34px rgba(0,0,0,.55),0 0 22px rgba(255,212,110,.2);font:900 14px/1 var(--fT);letter-spacing:.14em}
|
| 38 |
+
.mfTitleStart span{font-size:12px;margin-right:8px}.mfTitleStart:active{transform:scale(.98);filter:brightness(1.08)}
|
| 39 |
+
.mfIntroReplay{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:8px;width:min(88vw,340px);min-height:44px;margin-top:8px;padding:7px 11px;color:#bed7e8;text-align:left;border:1px solid rgba(102,164,205,.28);border-radius:10px;background:rgba(7,15,26,.72);font-family:var(--fT)}
|
| 40 |
+
.mfIntroReplay>span{color:#ffe39a;text-shadow:0 0 10px rgba(255,212,110,.55)}.mfIntroReplay b{font-size:9px;letter-spacing:.12em}.mfIntroReplay small{font-size:7px;letter-spacing:.09em;color:#6f9dbb}.mfIntroReplay:active{transform:scale(.98)}
|
| 41 |
+
body.mfIntroOpen{overflow:hidden}
|
| 42 |
+
@media(max-width:640px){.mfTitleCenter h1{font-size:clamp(46px,15vw,78px)}.mfTitleSubtitle{letter-spacing:.14em}.mfTitleSubtitle i{width:9vw}.mfTitleEyebrow{letter-spacing:.18em}.mfTitleStatus{font-size:8px;letter-spacing:.12em}.mfTitleInsignia{margin-bottom:13px}}
|
| 43 |
+
@media(max-height:520px){.mfTitleInsignia{width:66px;height:66px;margin-bottom:7px}.mfTitleCenter h1{font-size:48px}.mfTitleSubtitle{margin-top:9px}.mfTitleStatus{margin-top:12px}.mfTitleStart{min-height:48px}}
|
| 44 |
+
@media(prefers-reduced-motion:reduce){.mfTitleReveal,.mfTitleCenter,.mfTitleInsignia i,.mfTitleCenter h1,.mfTitleSweep,.mfTitleStatus span{animation:none!important;transition:none!important;transform:none!important;filter:none!important}.mfTitleReveal.open{opacity:1}}
|
src/styles/restree.css
CHANGED
|
@@ -1,108 +1,86 @@
|
|
| 1 |
-
/*
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
.rt3d-
|
| 11 |
-
.rt3d-legend{display:flex;gap:10px;flex-wrap:wrap;justify-content:center;font-family:var(--fU);
|
| 12 |
-
font-size:9px;font-weight:700;letter-spacing:.05em;color:#8fb3cc;padding:2px 2px 0}
|
| 13 |
-
.rt3d-legend span{display:inline-flex;align-items:center;gap:4px}
|
| 14 |
-
.rt3d-legend i{width:8px;height:8px;border-radius:2px;display:inline-block}
|
| 15 |
-
.rt3d-legend i.own{background:#5dff9a;box-shadow:0 0 5px #5dff9a}
|
| 16 |
-
.rt3d-legend i.aff{background:#41c8ff;box-shadow:0 0 5px #41c8ff}
|
| 17 |
-
.rt3d-legend i.ava{background:#ffd257}
|
| 18 |
-
.rt3d-legend i.lok{background:#5a7186}
|
| 19 |
|
| 20 |
-
.rt3d-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
.rt3d-
|
| 26 |
-
.rt3d-
|
| 27 |
-
|
| 28 |
-
.rt3d-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
.rt3d-
|
| 31 |
-
.rt3d-
|
| 32 |
-
|
| 33 |
-
.rt3d-
|
| 34 |
-
.rt3d-
|
|
|
|
| 35 |
|
| 36 |
-
.rt3d-
|
| 37 |
-
|
| 38 |
-
border:1px solid
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
.rt3d-
|
| 43 |
-
|
| 44 |
-
.rt3d-
|
| 45 |
-
|
| 46 |
-
.rt3d-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
.rt3d-
|
| 50 |
-
linear-gradient(160deg,#bff6d8,#3fdc8f 45%,#0f5a34 75%,#9be9c3) border-box;
|
| 51 |
-
box-shadow:0 6px 18px rgba(93,255,154,.22)}
|
| 52 |
-
.rt3d-node.owned .rt-badge{background:#2bd672;color:#062;}
|
| 53 |
-
.rt3d-node.afford{background:linear-gradient(180deg,rgba(15,34,50,.95),rgba(8,18,30,.96)) padding-box,
|
| 54 |
-
linear-gradient(160deg,#c6f2ff,#41c8ff 40%,#10465f 70%,#8fe0ff) border-box;
|
| 55 |
-
box-shadow:0 6px 18px rgba(65,200,255,.28);animation:rtPulse 2.2s ease-in-out infinite}
|
| 56 |
-
.rt3d-node.afford .rt-badge{background:#41c8ff;color:#04222f}
|
| 57 |
-
.rt3d-node.avail{opacity:.88}
|
| 58 |
-
.rt3d-node.avail .rt-badge{background:#ffd257;color:#3a2900}
|
| 59 |
-
.rt3d-node.locked{opacity:.5}
|
| 60 |
-
.rt3d-node.locked .rt-nm{color:#b7cfe2}
|
| 61 |
-
.rt3d-node.locked .rt-sub{color:#ff9a86}
|
| 62 |
-
.rt3d-node.locked .rt-badge{background:#3a4a5c;color:#cfe0ee}
|
| 63 |
-
.rt3d-node.sel{box-shadow:0 0 0 2px #fff,0 6px 18px rgba(255,255,255,.35)}
|
| 64 |
-
@keyframes rtPulse{0%,100%{filter:brightness(1)}50%{filter:brightness(1.22)}}
|
| 65 |
|
| 66 |
-
.rt3d-
|
| 67 |
-
|
| 68 |
-
.rt3d-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
.rt3d-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
-
.rt3d-
|
| 75 |
-
|
| 76 |
-
.rt3d-
|
| 77 |
-
.rt3d-detail{position:
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
.rt3d-
|
| 82 |
-
.rt3d-
|
| 83 |
-
|
| 84 |
-
.rt3d-
|
| 85 |
-
.rt3d-
|
| 86 |
-
|
| 87 |
-
.rt3d-
|
| 88 |
-
.rt3d-detail-hd span{display:block;margin-top:2px;font-size:10.5px;color:#8fb3cc;font-weight:600;line-height:1.4}
|
| 89 |
-
.rt3d-detail-row{margin-top:11px}
|
| 90 |
-
.rt3d-detail-lbl{font-family:var(--fT);font-size:8.5px;letter-spacing:.18em;color:#7fb4d6;font-weight:800;
|
| 91 |
-
margin-bottom:5px}
|
| 92 |
-
.rt3d-reqs{display:flex;flex-wrap:wrap;gap:6px}
|
| 93 |
-
.rt3d-reqChip{font-family:var(--fU);font-size:10px;font-weight:700;padding:4px 9px;border-radius:8px;
|
| 94 |
-
border:1px solid transparent;background:rgba(4,10,18,.6) padding-box,var(--steelB) border-box;color:#cfe8ff}
|
| 95 |
-
.rt3d-reqChip.ok{color:#0a2417;background:linear-gradient(180deg,#7ef0b4,#2bd672) padding-box,
|
| 96 |
-
linear-gradient(160deg,#d8ffe9,#1f9c62) border-box}
|
| 97 |
-
.rt3d-reqChip.no{color:#ffcfc4;background:rgba(60,20,16,.6) padding-box,var(--redB) border-box}
|
| 98 |
-
.rt3d-detail-buy{margin-top:13px;width:100%;padding:12px;border-radius:11px;text-align:center;
|
| 99 |
-
font-family:var(--fT);font-size:12px;font-weight:900;letter-spacing:.08em;color:#7fa8c6;
|
| 100 |
-
border:1px solid transparent;background:var(--panelG2) padding-box,var(--steelB) border-box}
|
| 101 |
-
.rt3d-detail-buy.go{color:#fff;background:linear-gradient(180deg,rgba(46,110,152,.96),rgba(20,52,80,.97)) padding-box,var(--goldB) border-box}
|
| 102 |
-
.rt3d-detail-buy.done{color:#0a2417;background:linear-gradient(180deg,#7ef0b4,#2bd672) padding-box,
|
| 103 |
-
linear-gradient(160deg,#d8ffe9,#1f9c62) border-box}
|
| 104 |
|
| 105 |
@media (max-width:360px){
|
| 106 |
-
.rt3d-
|
| 107 |
-
.rt3d-
|
| 108 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* RESEARCH NETWORK — dedicated touch graph and node-detail presentation. */
|
| 2 |
+
.rt3d-wrap{width:min(96vw,430px);margin:0 auto 10px;display:flex;flex-direction:column;gap:8px;color:#dff4ff}
|
| 3 |
+
.rt3d-summary{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 12px;border-radius:12px;
|
| 4 |
+
border:1px solid rgba(105,186,235,.3);background:linear-gradient(135deg,rgba(14,35,56,.96),rgba(5,13,24,.98));
|
| 5 |
+
box-shadow:inset 0 1px rgba(255,255,255,.04)}
|
| 6 |
+
.rt3d-summary small,.rt3d-summary b{display:block;font-family:var(--fT)}
|
| 7 |
+
.rt3d-summary small{font-size:7px;letter-spacing:.2em;color:#77badf}.rt3d-summary b{margin-top:2px;font-size:13px;letter-spacing:.06em}
|
| 8 |
+
.rt3d-summaryStats{display:flex;align-items:flex-end;flex-direction:column;gap:3px;font:800 8px var(--fT);color:#8db5ce}
|
| 9 |
+
.rt3d-summaryStats span{white-space:nowrap}.rt3d-summaryStats i{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:4px}
|
| 10 |
+
.rt3d-summaryStats i.ready{background:#45d5ff;box-shadow:0 0 8px #45d5ff}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
+
.rt3d-queue{padding:10px;border-radius:12px;border:1px solid rgba(110,166,205,.25);
|
| 13 |
+
background:linear-gradient(180deg,rgba(9,23,38,.96),rgba(4,11,20,.98))}
|
| 14 |
+
.rt3d-queue>header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px}
|
| 15 |
+
.rt3d-queue>header b,.rt3d-queue>header span{display:block}.rt3d-queue>header b{font:900 9px var(--fT);letter-spacing:.15em;color:#a7e8ff}
|
| 16 |
+
.rt3d-queue>header span{margin-top:2px;font-size:8px;color:#7799b1;font-weight:700}.rt3d-queue>header strong{font:900 11px var(--fT);color:#ffd875}
|
| 17 |
+
.rt3d-qslots{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px}
|
| 18 |
+
.rt3d-qslot{position:relative;min-width:0;height:58px;border-radius:9px;border:1px dashed rgba(111,151,181,.28);background:rgba(3,9,17,.64)}
|
| 19 |
+
.rt3d-qslot.empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:3px;color:#47647a}
|
| 20 |
+
.rt3d-qslot.empty small{font:900 8px var(--fT)}.rt3d-qslot.empty span{font:800 6px var(--fT);letter-spacing:.08em}
|
| 21 |
+
.rt3d-qslot.filled{border-style:solid;border-color:#637d92;background:linear-gradient(180deg,rgba(21,38,55,.95),rgba(7,16,27,.98))}
|
| 22 |
+
.rt3d-qslot.filled.ready{border-color:#43cfff;box-shadow:inset 0 0 12px rgba(67,207,255,.12)}
|
| 23 |
+
.rt3d-qslot.filled.locked{border-color:#725665}.rt3d-qslot.filled.funding{border-color:#9a783b}
|
| 24 |
+
.rt3d-qinspect{width:100%;height:100%;padding:4px 15px 3px 3px;display:flex;flex-direction:column;align-items:center;justify-content:center;color:#dff4ff;background:transparent;border:0}
|
| 25 |
+
.rt3d-qinspect small{position:absolute;left:4px;top:3px;font:900 7px var(--fT);color:#6990aa}.rt3d-qinspect span{display:block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:800 6.5px var(--fT)}
|
| 26 |
+
.rt3d-qremove{position:absolute;right:1px;top:1px;z-index:2;width:24px;height:24px;border-radius:7px;color:#ffb4a7;font-size:14px;background:rgba(56,13,16,.72)}
|
| 27 |
+
.rt3d-next{width:100%;min-height:44px;margin-top:7px;border-radius:9px;border:1px solid rgba(110,150,180,.25);font:900 8px var(--fT);letter-spacing:.05em;color:#5c7a90;background:rgba(7,16,27,.8)}
|
| 28 |
+
.rt3d-next.ready{color:#ecfbff;border-color:#40caff;background:linear-gradient(180deg,#236e98,#123a5d);box-shadow:0 0 15px rgba(64,202,255,.16)}
|
| 29 |
|
| 30 |
+
.rt3d-branchNav{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}
|
| 31 |
+
.rt3d-branchNav button{position:relative;min-width:0;min-height:52px;padding:6px 2px;border-radius:9px;border:1px solid rgba(95,144,180,.3);background:linear-gradient(180deg,rgba(18,35,51,.95),rgba(7,16,27,.98));
|
| 32 |
+
color:#7498b2;font:800 6.7px var(--fT);letter-spacing:.03em}
|
| 33 |
+
.rt3d-branchNav button>span{display:block;font-size:15px;color:#85cce9}.rt3d-branchNav button small{display:block;margin-top:2px;font-size:7px;color:#55758d}
|
| 34 |
+
.rt3d-branchNav button.on{color:#effcff;border-color:#48cfff;background:linear-gradient(180deg,rgba(33,97,134,.96),rgba(13,42,66,.98));box-shadow:0 0 14px rgba(72,207,255,.16)}
|
| 35 |
+
.rt3d-branchNav button.on small{color:#b7ecff}
|
| 36 |
|
| 37 |
+
.rt3d-stage{position:relative;height:min(55vh,500px);min-height:390px;overflow:auto;border-radius:14px;overscroll-behavior:contain;
|
| 38 |
+
touch-action:pan-x pan-y;-webkit-overflow-scrolling:touch;scrollbar-width:thin;scrollbar-color:#296c91 #07111d;
|
| 39 |
+
border:1px solid rgba(89,154,197,.34);background-color:#06101c;
|
| 40 |
+
background-image:linear-gradient(rgba(88,151,193,.045) 1px,transparent 1px),linear-gradient(90deg,rgba(88,151,193,.045) 1px,transparent 1px),radial-gradient(circle at 50% 28%,rgba(20,67,96,.5),rgba(4,10,18,.98) 70%);
|
| 41 |
+
background-size:22px 22px,22px 22px,100% 100%;box-shadow:inset 0 0 30px rgba(0,0,0,.48)}
|
| 42 |
+
.rt3d-world{position:relative;min-width:100%;isolation:isolate}
|
| 43 |
+
.rt3d-band{position:absolute;left:8px;right:8px;border:1px solid rgba(92,151,190,.14);border-radius:12px;background:rgba(9,22,35,.26);pointer-events:none}
|
| 44 |
+
.rt3d-band>span{position:sticky;left:12px;top:5px;display:inline-block;padding:5px 8px;border-radius:7px;background:#081522;border:1px solid rgba(103,160,198,.2);font:900 7px var(--fT);letter-spacing:.14em;color:#62a9d0}
|
| 45 |
+
.rt3d-band.fabrication{border-color:rgba(63,192,255,.18)}.rt3d-band.doctrine{border-color:rgba(255,190,82,.15)}.rt3d-band.xenology{border-color:rgba(170,101,255,.2)}
|
| 46 |
+
.rt3d-links{position:absolute;inset:0;z-index:1;overflow:visible;pointer-events:none}
|
| 47 |
+
.rt3d-links path{fill:none;stroke:#384d60;stroke-width:2;stroke-dasharray:5 5;opacity:.7;transition:stroke .18s,opacity .18s,stroke-width .18s}
|
| 48 |
+
.rt3d-links path.open{stroke:#40bde8;stroke-dasharray:none;filter:drop-shadow(0 0 3px rgba(64,189,232,.5))}
|
| 49 |
+
.rt3d-links path.owned{stroke:#55dc96;stroke-width:2.5;stroke-dasharray:none;filter:drop-shadow(0 0 3px rgba(85,220,150,.55))}
|
| 50 |
+
.rt3d-links path.focus{stroke:#fff2a4;stroke-width:4;opacity:1;filter:drop-shadow(0 0 5px rgba(255,226,110,.8))}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
+
.rt3d-node{position:absolute;z-index:2;width:126px;height:76px;padding:6px 7px 5px;border-radius:11px;display:grid;grid-template-columns:34px 1fr;grid-template-rows:1fr auto;gap:1px 5px;text-align:left;
|
| 53 |
+
border:1px solid #536d82;background:linear-gradient(155deg,rgba(26,44,61,.98),rgba(7,16,28,.99));box-shadow:0 6px 15px rgba(0,0,0,.3);color:#dceefa}
|
| 54 |
+
.rt3d-nodeArt{grid-row:1/3;align-self:center;width:34px;height:42px;display:flex;align-items:center;justify-content:center;border-radius:8px;background:rgba(2,8,15,.7);border:1px solid rgba(116,163,194,.2)}
|
| 55 |
+
.rt3d-node>b{align-self:end;display:block;max-height:25px;overflow:hidden;font:900 8px/1.22 var(--fT);letter-spacing:.01em}.rt3d-node>small{align-self:start;font:800 6.5px var(--fT);letter-spacing:.07em;color:#7799b0}
|
| 56 |
+
.rt3d-tier{position:absolute;left:-5px;top:-5px;min-width:22px;height:18px;padding:0 3px;border-radius:6px;display:flex;align-items:center;justify-content:center;background:#203244;border:1px solid #657d91;font:900 7px var(--fT);color:#bcd4e5}
|
| 57 |
+
.rt3d-node.owned{border-color:#50dc93;background:linear-gradient(155deg,rgba(17,67,44,.98),rgba(5,24,16,.99));box-shadow:0 5px 16px rgba(62,221,143,.14)}
|
| 58 |
+
.rt3d-node.owned>small{color:#68e1a5}.rt3d-node.ready{border-color:#42cdff;background:linear-gradient(155deg,rgba(24,77,106,.98),rgba(7,27,43,.99));box-shadow:0 5px 18px rgba(66,205,255,.2)}
|
| 59 |
+
.rt3d-node.ready>small{color:#7ce2ff}.rt3d-node.funding{border-color:#a17a36}.rt3d-node.funding>small{color:#e0b15c}.rt3d-node.locked{opacity:.62;filter:saturate(.55)}
|
| 60 |
+
.rt3d-node.locked>small{color:#e18c82}.rt3d-node.selected{z-index:3;border-color:#fff6bd;box-shadow:0 0 0 2px rgba(255,239,143,.55),0 0 22px rgba(255,216,75,.32);opacity:1;filter:none}
|
| 61 |
+
.rt3d-node:active{transform:scale(.97)}
|
| 62 |
|
| 63 |
+
.rt3d-detail{position:sticky;bottom:0;z-index:8;padding:12px;border-radius:13px;border:1px solid rgba(103,172,215,.35);background:linear-gradient(160deg,rgba(17,38,59,.985),rgba(5,13,24,.995));box-shadow:0 -8px 24px rgba(0,0,0,.44)}
|
| 64 |
+
.rt3d-detailClose{position:absolute;right:8px;top:8px;z-index:2;width:44px;height:44px;border-radius:10px;color:#d6ebf8;border:1px solid #48677d;background:#0a1825;font-size:20px}
|
| 65 |
+
.rt3d-detail.empty{min-height:66px;display:flex;align-items:center;justify-content:center;gap:10px;color:#7295ad}.rt3d-emptyIcon{font-size:28px;color:#4c91b6}
|
| 66 |
+
.rt3d-detail.empty b,.rt3d-detail.empty span{display:block}.rt3d-detail.empty b{font:900 9px var(--fT);letter-spacing:.1em}.rt3d-detail.empty span{margin-top:3px;font-size:9px;font-weight:600}
|
| 67 |
+
.rt3d-detail>header{position:relative;display:flex;align-items:center;gap:9px;padding-right:128px}.rt3d-detailArt{width:50px;height:50px;flex:0 0 auto;display:flex;align-items:center;justify-content:center;border-radius:10px;background:rgba(2,8,15,.72);border:1px solid rgba(110,165,201,.24)}
|
| 68 |
+
.rt3d-detail header small,.rt3d-detail header b,.rt3d-detail header span{display:block}.rt3d-detail header small{font:800 7px var(--fT);letter-spacing:.12em;color:#65a9cf}.rt3d-detail header b{margin-top:2px;font:900 12px var(--fT);color:#effaff}.rt3d-detail header span{margin-top:2px;font-size:9px;line-height:1.3;color:#8eafc4;font-weight:600}
|
| 69 |
+
.rt3d-detail header em{position:absolute;right:48px;top:0;max-width:78px;padding:5px;border-radius:7px;text-align:center;font:900 6.5px var(--fT);font-style:normal;line-height:1.3;color:#8faabd;border:1px solid #4b6478;background:#0a1724}
|
| 70 |
+
.rt3d-detail.ready header em{color:#caf7ff;border-color:#3dc9ff;background:#10384f}.rt3d-detail.owned header em{color:#caffdf;border-color:#45d98c;background:#103a27}.rt3d-detail.locked header em{color:#ffc2b7;border-color:#8a4d48;background:#32191b}.rt3d-detail.funding header em{color:#ffe0a5;border-color:#9a7432;background:#302512}
|
| 71 |
+
.rt3d-detailLabel{margin:10px 0 5px;font:900 7px var(--fT);letter-spacing:.16em;color:#6e9ebc}.rt3d-prereqs{display:flex;gap:5px;flex-wrap:wrap}.rt3d-prereqs button{min-height:36px;padding:6px 9px;border-radius:8px;font:800 8px var(--fT);color:#ffc0b4;border:1px solid #6f3e3f;background:rgba(45,16,18,.62)}
|
| 72 |
+
.rt3d-prereqs button.ok{color:#a7f1c9;border-color:#347a58;background:rgba(12,46,31,.7)}
|
| 73 |
+
.rt3d-costGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:5px}.rt3d-cost{min-width:0;min-height:35px;padding:5px 6px;border-radius:8px;display:grid;grid-template-columns:20px 1fr;grid-template-rows:auto auto;border:1px solid rgba(102,146,177,.22);background:rgba(3,10,18,.56)}
|
| 74 |
+
.rt3d-cost img,.rt3d-cost>i{grid-row:1/3;align-self:center}.rt3d-cost>i{font-style:normal;color:#77d9ff;text-align:center}.rt3d-cost span{font-size:7px;font-weight:700;color:#7598ae}.rt3d-cost b{font:900 8px var(--fT);color:#e8f7ff}.rt3d-cost.need b{color:#ff9c8e}.rt3d-cost.ok b{color:#80e3ad}
|
| 75 |
+
.rt3d-detailActions{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:9px}.rt3d-detailActions button{min-height:46px;padding:8px 6px;border-radius:9px;font:900 8px var(--fT);letter-spacing:.04em}
|
| 76 |
+
.rt3d-primary{color:#fff;border:1px solid #54d8ff;background:linear-gradient(180deg,#2878a4,#123b5e);box-shadow:0 0 15px rgba(58,196,246,.16)}.rt3d-primary.locked{color:#6e899d;border-color:#3a5265;background:#101e2b}.rt3d-primary.done{color:#a7f2c8;border-color:#3ca774;background:#133526}
|
| 77 |
+
.rt3d-secondary{color:#b7dbef;border:1px solid #507994;background:linear-gradient(180deg,#172d40,#0b1b2a)}.rt3d-secondary.path{grid-column:1/-1;color:#ffe2a0;border-color:#8c6a2e;background:linear-gradient(180deg,#342a17,#1c170d)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
@media (max-width:360px){
|
| 80 |
+
.rt3d-wrap{width:97vw}.rt3d-qslots{gap:3px}.rt3d-qslot{height:54px}.rt3d-qinspect span{font-size:5.8px}.rt3d-stage{height:min(51vh,445px);min-height:350px}
|
| 81 |
+
.rt3d-detail>header{padding-right:112px}.rt3d-detail header em{max-width:61px}.rt3d-detail header b{font-size:10.5px}.rt3d-detail header span{font-size:8.5px}
|
| 82 |
}
|
| 83 |
+
@media (min-width:700px) and (orientation:landscape){
|
| 84 |
+
.rt3d-wrap{width:min(94vw,760px)}.rt3d-stage{height:min(62vh,560px)}.rt3d-detailActions{grid-template-columns:1.25fr .75fr}.rt3d-costGrid{grid-template-columns:repeat(3,minmax(0,1fr))}
|
| 85 |
+
}
|
| 86 |
+
@media (prefers-reduced-motion:reduce){.rt3d-stage{scroll-behavior:auto}.rt3d-links path,.rt3d-node{transition:none}}
|
src/styles/store.css
CHANGED
|
@@ -86,3 +86,100 @@
|
|
| 86 |
.invAct{flex:0 0 66px;min-height:40px;padding:5px;display:flex;align-items:center;justify-content:center;text-align:center;border-radius:8px;
|
| 87 |
color:var(--rar);background:rgba(5,11,18,.68);border:1px solid var(--rar);font:800 7.5px var(--fT);letter-spacing:.04em}
|
| 88 |
.invItem.locked .invAct{border-color:#607486;color:#8495a3;font-size:6.3px}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
.invAct{flex:0 0 66px;min-height:40px;padding:5px;display:flex;align-items:center;justify-content:center;text-align:center;border-radius:8px;
|
| 87 |
color:var(--rar);background:rgba(5,11,18,.68);border:1px solid var(--rar);font:800 7.5px var(--fT);letter-spacing:.04em}
|
| 88 |
.invItem.locked .invAct{border-color:#607486;color:#8495a3;font-size:6.3px}
|
| 89 |
+
|
| 90 |
+
/* ---- TWO-LAYER ACCOUNT ARMORY / SESSION LOADOUT ---------------------------
|
| 91 |
+
Isolated `armInv*` names keep this denser interaction model from changing
|
| 92 |
+
perk rows, Development modules, or the in-match inventory presentation. */
|
| 93 |
+
.armInvLayerHead{display:flex;align-items:stretch;gap:8px;padding:10px;border-radius:12px;text-align:left;
|
| 94 |
+
border:1px solid rgba(91,177,226,.3);background:linear-gradient(135deg,rgba(21,56,81,.94),rgba(7,16,27,.98));
|
| 95 |
+
box-shadow:0 5px 16px rgba(0,0,0,.4),inset 0 1px 0 rgba(255,255,255,.07)}
|
| 96 |
+
.armInvLayerHead.session{border-color:rgba(255,205,91,.34);background:linear-gradient(135deg,rgba(67,49,19,.58),rgba(8,17,29,.98))}
|
| 97 |
+
.armInvLayerHead>div:first-child{flex:1;min-width:0}
|
| 98 |
+
.armInvLayerHead i{display:block;color:#70d9ff;font:800 7px var(--fT);font-style:normal;letter-spacing:.17em}
|
| 99 |
+
.armInvLayerHead.session i{color:#ffd86d}
|
| 100 |
+
.armInvLayerHead>div>b{display:block;margin-top:3px;color:#e8f8ff;font:900 15px var(--fT);letter-spacing:.09em}
|
| 101 |
+
.armInvLayerHead>div>span{display:block;margin-top:4px;color:#9abbd1;font-size:10px;font-weight:600;line-height:1.3}
|
| 102 |
+
.armInvCapacity{flex:0 0 88px;display:flex!important;flex-direction:column;align-items:center;justify-content:center;padding:6px;
|
| 103 |
+
border-radius:9px;text-align:center;background:rgba(3,10,17,.6);border:1px solid rgba(117,196,238,.22)}
|
| 104 |
+
.armInvCapacity b{margin:0!important;color:#8ce7ff!important;font-size:18px!important;letter-spacing:.02em!important;font-variant-numeric:tabular-nums}
|
| 105 |
+
.armInvLayerHead.session .armInvCapacity b{color:#ffe083!important}
|
| 106 |
+
.armInvCapacity span{margin-top:2px!important;color:#779ab1!important;font:800 6.4px var(--fT)!important;letter-spacing:.1em!important}
|
| 107 |
+
.armInvMeters{display:grid;grid-template-columns:repeat(3,1fr);gap:5px}
|
| 108 |
+
.armInvMeters div{min-width:0;padding:7px 4px;border-radius:8px;text-align:center;background:rgba(10,23,36,.86);border:1px solid rgba(90,147,182,.18)}
|
| 109 |
+
.armInvMeters b{display:block;color:#d9f4ff;font:800 10px var(--fT);font-variant-numeric:tabular-nums}
|
| 110 |
+
.armInvMeters span{display:block;margin-top:2px;color:#6389a2;font:800 6.4px var(--fT);letter-spacing:.08em;white-space:nowrap}
|
| 111 |
+
|
| 112 |
+
.armInvFilters{display:flex;gap:5px;max-width:100%;overflow-x:auto;padding:1px 0 3px;scrollbar-width:none;overscroll-behavior-x:contain}
|
| 113 |
+
.armInvFilters::-webkit-scrollbar{display:none}
|
| 114 |
+
.armInvFilter{flex:0 0 auto;min-width:72px;min-height:46px;padding:0 10px;border-radius:9px;color:#789cb5;
|
| 115 |
+
border:1px solid rgba(99,155,190,.26);background:linear-gradient(180deg,rgba(23,39,56,.96),rgba(8,16,27,.97));
|
| 116 |
+
font:800 7.5px var(--fT);letter-spacing:.08em;touch-action:manipulation}
|
| 117 |
+
.armInvFilter.on{color:#d8f6ff;border-color:#52cfff;background:linear-gradient(180deg,rgba(37,112,151,.8),rgba(12,45,70,.96));box-shadow:0 0 10px rgba(65,200,255,.28)}
|
| 118 |
+
|
| 119 |
+
.armInvPreview{--rar:#aeb9c6;display:grid;grid-template-columns:58px minmax(0,1fr) 82px;gap:8px;align-items:center;padding:10px;border-radius:13px;text-align:left;
|
| 120 |
+
border:1px solid var(--rar);border-left:4px solid var(--rar);background:radial-gradient(circle at 8% 20%,color-mix(in srgb,var(--rar) 18%,transparent),transparent 36%),linear-gradient(145deg,rgba(27,42,58,.98),rgba(5,12,21,.99));
|
| 121 |
+
box-shadow:0 4px 15px rgba(0,0,0,.48),0 0 14px color-mix(in srgb,var(--rar) 16%,transparent)}
|
| 122 |
+
.armInvPreviewArt{width:54px;height:54px;display:flex;align-items:center;justify-content:center;border-radius:10px;
|
| 123 |
+
background:rgba(2,8,14,.55);box-shadow:inset 0 0 12px color-mix(in srgb,var(--rar) 22%,transparent)}
|
| 124 |
+
.armInvPreviewArt .itemArt{width:54px!important;height:54px!important;object-fit:contain}
|
| 125 |
+
.armInvPreviewInfo{min-width:0}.armInvPreviewInfo i{display:block;color:var(--rar);font:800 6.8px var(--fT);font-style:normal;letter-spacing:.1em}
|
| 126 |
+
.armInvPreviewInfo b{display:block;margin-top:3px;color:#f1faff;font:900 13px var(--fT);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 127 |
+
.armInvPreviewInfo span{display:block;margin-top:4px;color:#799bb2;font:700 7px var(--fT);letter-spacing:.07em}
|
| 128 |
+
.armInvExact{align-self:stretch;min-width:0;padding:6px 4px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;border-radius:9px;
|
| 129 |
+
background:rgba(2,8,14,.67);border:1px solid color-mix(in srgb,var(--rar) 45%,transparent)}
|
| 130 |
+
.armInvExact span{color:#7697ad;font:800 5.8px var(--fT);letter-spacing:.08em}.armInvExact b{margin-top:2px;color:var(--rar);font:900 15px var(--fT)}
|
| 131 |
+
.armInvExact i{margin-top:2px;color:#bad2e1;font:800 5.8px var(--fT);font-style:normal;letter-spacing:.05em}
|
| 132 |
+
.armInvPreview p{grid-column:1/-1;margin:0;color:#acc7d8;font-size:10.5px;font-weight:600;line-height:1.35}
|
| 133 |
+
.armInvCompare{grid-column:1/-1;padding:6px 8px;border-radius:7px;color:#d0e9f5;background:rgba(51,102,132,.18);
|
| 134 |
+
border-left:2px solid var(--rar);font:800 7px var(--fT);letter-spacing:.055em;line-height:1.35}
|
| 135 |
+
.armInvCommit{grid-column:1/-1;min-height:48px;border-radius:9px;color:#081018;border:1px solid transparent;
|
| 136 |
+
background:linear-gradient(180deg,color-mix(in srgb,var(--rar) 74%,white),var(--rar));font:900 9px var(--fT);letter-spacing:.1em;touch-action:manipulation}
|
| 137 |
+
.armInvCommit.disabled{color:#637889;border-color:#526575;background:rgba(19,30,41,.85)}
|
| 138 |
+
.armInvPreview small{grid-column:1/-1;color:#6f90a6;font-size:9px;font-weight:600;line-height:1.35;text-align:center}
|
| 139 |
+
.armInvSectionLabel{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:3px 2px}
|
| 140 |
+
.armInvSectionLabel b{color:#93ddfa;font:800 8px var(--fT);letter-spacing:.14em}.armInvSectionLabel span{color:#607f94;font:800 6.5px var(--fT);letter-spacing:.07em}
|
| 141 |
+
|
| 142 |
+
.armVaultList{display:flex;flex-direction:column;gap:6px}
|
| 143 |
+
.armVaultItem{--rar:#aeb9c6;width:100%;min-height:68px;display:flex;align-items:center;gap:9px;padding:7px 8px;border-radius:10px;text-align:left;
|
| 144 |
+
color:inherit;border:1px solid rgba(105,149,178,.23);border-left:3px solid var(--rar);background:linear-gradient(180deg,rgba(25,39,54,.96),rgba(7,14,23,.98));
|
| 145 |
+
box-shadow:0 3px 8px rgba(0,0,0,.38);touch-action:manipulation}
|
| 146 |
+
.armVaultItem.selected{border-color:var(--rar);box-shadow:0 0 12px color-mix(in srgb,var(--rar) 34%,transparent),inset 0 1px 0 rgba(255,255,255,.08)}
|
| 147 |
+
.armVaultItem.equipped{background:linear-gradient(90deg,color-mix(in srgb,var(--rar) 13%,rgba(15,28,42,.98)),rgba(7,14,23,.98))}
|
| 148 |
+
.armVaultItem.locked{opacity:.42;filter:saturate(.35)}
|
| 149 |
+
.armVaultArt{flex:0 0 42px;height:44px;display:flex;align-items:center;justify-content:center}.armVaultArt .itemArt{width:40px!important;height:40px!important;object-fit:contain}
|
| 150 |
+
.armVaultCopy{flex:1;min-width:0}.armVaultCopy i{display:block;color:var(--rar);font:800 6.7px var(--fT);font-style:normal;letter-spacing:.1em}
|
| 151 |
+
.armVaultCopy b{display:block;margin-top:2px;color:#e9f7ff;font:800 10.5px var(--fT);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 152 |
+
.armVaultCopy em{display:block;margin-top:3px;color:#93b5ca;font-size:9px;font-weight:700;font-style:normal}
|
| 153 |
+
.armVaultState{flex:0 0 65px;min-height:34px;display:flex;align-items:center;justify-content:center;padding:3px;border-radius:7px;text-align:center;
|
| 154 |
+
color:var(--rar);background:rgba(2,8,14,.56);border:1px solid color-mix(in srgb,var(--rar) 60%,transparent);font:800 6.5px var(--fT);letter-spacing:.05em}
|
| 155 |
+
.armInvEmpty{padding:18px 12px;border-radius:10px;text-align:center;background:rgba(10,22,35,.74);border:1px dashed rgba(95,153,189,.3)}
|
| 156 |
+
.armInvEmpty b{display:block;color:#a6cadd;font:800 9px var(--fT);letter-spacing:.1em}.armInvEmpty span{display:block;margin-top:4px;color:#6d8da2;font-size:9.5px;font-weight:600}
|
| 157 |
+
|
| 158 |
+
.armLoadoutRule{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:8px 10px;border-radius:9px;text-align:left;
|
| 159 |
+
background:linear-gradient(90deg,rgba(111,77,20,.35),rgba(11,24,38,.88));border-left:3px solid #ffd567}
|
| 160 |
+
.armLoadoutRule b{color:#ffe08a;font:900 8px var(--fT);letter-spacing:.1em;white-space:nowrap}.armLoadoutRule span{color:#a9bdc9;font-size:9px;font-weight:600;text-align:right}
|
| 161 |
+
.armLoadoutGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}
|
| 162 |
+
.armLoadSlot{--rar:#758da0;min-width:0;min-height:126px;display:flex;flex-direction:column;padding:8px;border-radius:11px;text-align:left;
|
| 163 |
+
border:1px solid color-mix(in srgb,var(--rar) 48%,transparent);border-top:3px solid var(--rar);background:linear-gradient(150deg,color-mix(in srgb,var(--rar) 10%,rgba(20,34,48,.98)),rgba(5,12,20,.99));box-shadow:0 4px 10px rgba(0,0,0,.4)}
|
| 164 |
+
.armLoadSlot.empty{border-style:dashed;opacity:.86}.armLoadSlotTop{display:flex;justify-content:space-between;gap:5px}.armLoadSlotTop i{color:var(--rar);font:900 7px var(--fT);font-style:normal;letter-spacing:.1em}
|
| 165 |
+
.armLoadSlotTop span{color:#6c8da3;font:800 6px var(--fT);letter-spacing:.07em}.armLoadSlot>b{margin-top:14px;color:#99b6c8;font:800 9px var(--fT)}
|
| 166 |
+
.armLoadSlot>small{display:block;margin-top:4px;color:#6e8da1;font-size:8px;font-weight:600;line-height:1.25}
|
| 167 |
+
.armLoadEquipped{display:flex;align-items:center;gap:6px;margin-top:8px;min-width:0}.armLoadEquipped>span{flex:0 0 38px;height:38px;display:flex;align-items:center;justify-content:center}
|
| 168 |
+
.armLoadEquipped .itemArt{width:36px!important;height:36px!important;object-fit:contain}.armLoadEquipped>div{min-width:0}.armLoadEquipped b{display:block;color:#edf9ff;font:800 9px var(--fT);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 169 |
+
.armLoadEquipped em{display:block;margin-top:3px;color:var(--rar);font-size:7.8px;font-weight:800;font-style:normal;line-height:1.2}
|
| 170 |
+
.armLoadSlot button{width:100%;min-height:44px;margin-top:auto;border-radius:8px;color:var(--rar);border:1px solid color-mix(in srgb,var(--rar) 68%,transparent);
|
| 171 |
+
background:rgba(4,10,17,.7);font:900 7px var(--fT);letter-spacing:.09em;touch-action:manipulation}
|
| 172 |
+
.armLoadSlot.empty button{color:#9bdfff;border-color:#3aaedc;background:rgba(25,83,111,.25)}
|
| 173 |
+
.armMissionPackage{padding:9px;border-radius:12px;text-align:left;background:linear-gradient(160deg,rgba(18,35,51,.96),rgba(5,12,20,.99));border:1px solid rgba(91,153,190,.24)}
|
| 174 |
+
.armMissionFx{--rar:#aeb9c6;display:grid;grid-template-columns:8px minmax(0,1fr) auto;gap:7px;align-items:center;padding:7px 4px;border-bottom:1px solid rgba(99,142,168,.13)}
|
| 175 |
+
.armMissionFx>i{width:7px;height:7px;border-radius:50%;background:var(--rar);box-shadow:0 0 6px var(--rar)}.armMissionFx div{min-width:0}
|
| 176 |
+
.armMissionFx div b{display:block;color:#dceef7;font:800 8.5px var(--fT);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.armMissionFx div span{display:block;margin-top:2px;color:#698ba0;font:700 6px var(--fT);letter-spacing:.07em}
|
| 177 |
+
.armMissionFx strong{max-width:115px;color:var(--rar);font:800 7.5px var(--fT);text-align:right}.armMissionPackage>p{margin:8px 3px 1px;color:#738fa2;font-size:8.5px;font-weight:600;line-height:1.35;text-align:center}
|
| 178 |
+
.armReturnVault{min-height:48px;border-radius:9px;color:#b9ecff;border:1px solid #46bde9;background:linear-gradient(180deg,rgba(37,100,135,.7),rgba(12,39,61,.94));
|
| 179 |
+
font:900 8px var(--fT);letter-spacing:.11em;touch-action:manipulation}
|
| 180 |
+
|
| 181 |
+
@media(max-width:350px){
|
| 182 |
+
.armInvPreview{grid-template-columns:50px minmax(0,1fr) 72px;padding:8px;gap:6px}
|
| 183 |
+
.armInvPreviewArt,.armInvPreviewArt .itemArt{width:48px!important;height:48px!important}
|
| 184 |
+
.armInvCapacity{flex-basis:78px}.armLoadoutGrid{grid-template-columns:1fr}
|
| 185 |
+
}
|
src/styles/tutorial.css
CHANGED
|
@@ -82,6 +82,15 @@
|
|
| 82 |
#keelTxt{
|
| 83 |
margin-top:2px; font-size:12.5px; line-height:1.36; font-weight:600; color:#eaf7ff;
|
| 84 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
#keelNext{
|
| 87 |
display:none;align-items:center;justify-content:center;min-width:92px;min-height:44px;margin-top:8px;padding:8px 15px;
|
|
@@ -181,5 +190,6 @@
|
|
| 181 |
#keelBar{ transition:none; }
|
| 182 |
#keelBar.pulse{ animation:none; }
|
| 183 |
#keelBar.pulse #keelBadge .kr1,#keelBar.pulse #keelBadge .kr2{ animation:none; }
|
|
|
|
| 184 |
.keelFocus,#keelMapCue{animation:none!important}
|
| 185 |
}
|
|
|
|
| 82 |
#keelTxt{
|
| 83 |
margin-top:2px; font-size:12.5px; line-height:1.36; font-weight:600; color:#eaf7ff;
|
| 84 |
}
|
| 85 |
+
#keelProgress{
|
| 86 |
+
height:3px;margin-top:7px;border-radius:4px;overflow:hidden;background:rgba(4,12,20,.78);
|
| 87 |
+
box-shadow:inset 0 0 0 1px rgba(120,205,242,.16);
|
| 88 |
+
}
|
| 89 |
+
#keelProgress i{
|
| 90 |
+
display:block;width:0;height:100%;border-radius:inherit;
|
| 91 |
+
background:linear-gradient(90deg,#42c9ff,#ffe06b);box-shadow:0 0 7px rgba(88,205,255,.62);
|
| 92 |
+
transition:width .3s ease;
|
| 93 |
+
}
|
| 94 |
|
| 95 |
#keelNext{
|
| 96 |
display:none;align-items:center;justify-content:center;min-width:92px;min-height:44px;margin-top:8px;padding:8px 15px;
|
|
|
|
| 190 |
#keelBar{ transition:none; }
|
| 191 |
#keelBar.pulse{ animation:none; }
|
| 192 |
#keelBar.pulse #keelBadge .kr1,#keelBar.pulse #keelBadge .kr2{ animation:none; }
|
| 193 |
+
#keelProgress i{transition:none}
|
| 194 |
.keelFocus,#keelMapCue{animation:none!important}
|
| 195 |
}
|
src/styles/ui.css
CHANGED
|
@@ -52,18 +52,30 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 52 |
border:1px solid transparent;background:var(--panelG) padding-box,var(--steelB) border-box;
|
| 53 |
box-shadow:0 2px 8px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.1);
|
| 54 |
clip-path:polygon(6px 0,100% 0,100% calc(100% - 6px),calc(100% - 6px) 100%,0 100%,0 6px)}
|
| 55 |
-
.res .ic{width:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
.res b{font-variant-numeric:tabular-nums;font-size:15px;font-weight:700;letter-spacing:.02em}
|
| 57 |
.res .rate{font-size:11px;opacity:.85;font-variant-numeric:tabular-nums;font-weight:600}
|
| 58 |
-
#massIc{background:linear-gradient(160deg,#
|
| 59 |
-
#enIc{background:linear-gradient(160deg,#
|
| 60 |
-
#unitRes .ic{background:linear-gradient(160deg,#
|
| 61 |
#fps{margin-left:auto;padding:3px 9px;font-size:11px;opacity:.9;border-radius:8px;font-weight:600;
|
| 62 |
border:1px solid transparent;background:var(--panelG) padding-box,var(--steelB) border-box}
|
| 63 |
#menuBtn,#spdBtn{pointer-events:auto;color:#dfeeff;border-radius:9px;padding:4px 12px;font-size:15px;
|
|
|
|
| 64 |
border:1px solid transparent;background:var(--panelG2) padding-box,var(--steelB) border-box;
|
| 65 |
-
box-shadow:0 2px 8px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.12)}
|
| 66 |
#spdBtn{font-size:12px;font-weight:800;font-family:var(--fT);padding:5px 8px}
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
/* ---------- ALERT / TOAST ---------- */
|
| 69 |
/* z-index 110, not 20. Overlays (menus, profile, settings, armory) sit at 50,
|
|
@@ -193,8 +205,11 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 193 |
#wcRow{position:absolute;top:calc(var(--sat) + 36px);right:8px;z-index:9;display:none;gap:4px;align-items:center;
|
| 194 |
border:1px solid transparent;background:var(--panelG) padding-box,var(--redB) border-box;
|
| 195 |
border-radius:9px;padding:2px 8px;pointer-events:auto;box-shadow:0 2px 8px rgba(0,0,0,.5)}
|
| 196 |
-
.wcChip{font-size:15px
|
|
|
|
|
|
|
| 197 |
.wcMult{font-size:11px;color:#ffb0a2;font-weight:700;font-family:var(--fT)}
|
|
|
|
| 198 |
|
| 199 |
/* ---------- OBJECTIVE / CLOCK BAR ---------- */
|
| 200 |
#goalBar{position:absolute;top:calc(var(--sat) + 36px);left:50%;transform:translateX(-50%);z-index:9;display:none;
|
|
@@ -290,6 +305,28 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 290 |
width:max-content;max-width:min(96vw,398px);max-height:min(56vh,56dvh);overflow-y:auto;-webkit-overflow-scrolling:touch;
|
| 291 |
clip-path:polygon(14px 0,calc(100% - 14px) 0,100% 14px,100% calc(100% - 14px),calc(100% - 14px) 100%,14px 100%,0 calc(100% - 14px),0 14px)}
|
| 292 |
#buildMenu .grid,#prodMenu .grid{display:flex;gap:7px;flex-wrap:wrap;justify-content:center}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
.bcard{width:84px;border-radius:10px;padding:6px 3px 5px;display:flex;flex-direction:column;align-items:center;gap:2px;
|
| 294 |
font-size:10.5px;pointer-events:auto;position:relative;overflow:hidden;
|
| 295 |
border:1px solid transparent;background:linear-gradient(180deg,rgba(30,46,64,.95),rgba(13,21,33,.97)) padding-box,var(--steelB) border-box;
|
|
@@ -323,6 +360,10 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 323 |
border:1px solid transparent;background:var(--panelG2) padding-box,var(--steelB) border-box;font-family:var(--fU);text-transform:uppercase}
|
| 324 |
#upBtn{color:var(--gold);background:linear-gradient(180deg,rgba(64,48,14,.95),rgba(30,22,6,.97)) padding-box,var(--goldB) border-box}
|
| 325 |
#repeatBtn.on{color:var(--green);box-shadow:0 0 9px rgba(93,255,154,.35)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
/* ---------- PLACEMENT CONFIRM ---------- */
|
| 328 |
#placeUI{position:absolute;left:50%;transform:translateX(-50%);bottom:calc(var(--sab) + 68px);z-index:12;display:none;gap:12px}
|
|
@@ -441,6 +482,12 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 441 |
.goBreak span{padding:3px 6px;border-radius:6px;font-family:var(--fU);font-size:9px;letter-spacing:.04em;
|
| 442 |
color:#9fc6e0;background:rgba(22,38,56,.8);border:1px solid rgba(110,165,205,.22)}
|
| 443 |
.goBreak b{color:#ffe9ad}.goData{margin-top:7px;color:#9dffcf;font-size:12px;letter-spacing:.08em}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 444 |
.goData small{display:block;color:#7fb29c;font:600 9px var(--fU);letter-spacing:.03em}.goData.muted{color:#7f9aab;font-size:10px}
|
| 445 |
|
| 446 |
/* ---------- META HEADER ---------- */
|
|
@@ -654,11 +701,11 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 654 |
background:var(--panelG2) padding-box,var(--steelB) border-box}
|
| 655 |
.setupAdv.on{color:#fff;background:linear-gradient(180deg,rgba(46,110,152,.96),rgba(20,52,80,.97)) padding-box,
|
| 656 |
linear-gradient(160deg,#c6f2ff,#41c8ff 40%,#10465f 70%,#8fe0ff) border-box}
|
| 657 |
-
.setupTabs{flex:0 0 auto;width:100%;display:grid;grid-template-columns:repeat(
|
| 658 |
padding:0 calc(var(--sar) + 14px) 10px calc(var(--sal) + 14px);
|
| 659 |
background:rgba(6,12,22,.98);z-index:4}
|
| 660 |
-
.setupTabBtn{min-width:0;min-height:48px;padding:9px
|
| 661 |
-
font-size:
|
| 662 |
background:linear-gradient(180deg,rgba(23,36,53,.96),rgba(9,17,28,.97))}
|
| 663 |
.setupTabBtn.on{color:#f0fbff;border-color:#41c8ff;background:linear-gradient(180deg,rgba(43,113,154,.9),rgba(15,44,68,.96));
|
| 664 |
box-shadow:0 0 12px rgba(65,200,255,.25)}
|
|
@@ -677,6 +724,14 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 677 |
display:flex;align-items:center;justify-content:center;text-align:center;line-height:1.15}
|
| 678 |
.optGrid{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
| 679 |
.optGrid .dbtn{min-width:0}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 680 |
.secHint{margin-top:11px;font-size:11.5px;line-height:1.4;color:#8fb3cc;font-weight:600;text-align:left}
|
| 681 |
.secHint.danger{color:#c98d80}
|
| 682 |
.advWrap{display:contents!important}
|
|
@@ -684,7 +739,8 @@ canvas#gl{position:absolute;inset:0;width:100%;height:100%;display:block;touch-a
|
|
| 684 |
#setupScr:not(.setupAll)[data-tab="map"] [data-setup-tab]:not([data-setup-tab="map"]),
|
| 685 |
#setupScr:not(.setupAll)[data-tab="forces"] [data-setup-tab]:not([data-setup-tab="forces"]),
|
| 686 |
#setupScr:not(.setupAll)[data-tab="rules"] [data-setup-tab]:not([data-setup-tab="rules"]),
|
| 687 |
-
#setupScr:not(.setupAll)[data-tab="economy"] [data-setup-tab]:not([data-setup-tab="economy"])
|
|
|
|
| 688 |
.setupFoot{flex:0 0 auto;width:100%;display:flex;gap:10px;align-items:center;justify-content:center;
|
| 689 |
/* Some Android WebViews report a zero bottom inset while still drawing a
|
| 690 |
3-button/navigation shelf. The 54px floor is the physical escape hatch:
|
|
@@ -1276,18 +1332,41 @@ body.menuMode #fps,body.menuMode #atkAlert,body.menuMode #waveAlert{display:none
|
|
| 1276 |
box-shadow:0 0 14px rgba(255,106,85,.35)}
|
| 1277 |
.thBtn.on b{color:#fff} .thBtn.on span{color:#ffb0a2}
|
| 1278 |
.thBtn.lock{opacity:.34}
|
| 1279 |
-
#opModRow{display:flex;flex-direction:column;gap:
|
| 1280 |
-
.opMod{display:
|
|
|
|
| 1281 |
border:1px solid transparent;
|
| 1282 |
background:linear-gradient(180deg,rgba(20,30,46,.92),rgba(10,17,28,.94)) padding-box,
|
| 1283 |
linear-gradient(160deg,rgba(120,170,205,.24),rgba(30,60,88,.14)) border-box}
|
|
|
|
| 1284 |
.opMod.on{background:linear-gradient(180deg,rgba(58,20,16,.94),rgba(28,10,8,.96)) padding-box,
|
| 1285 |
linear-gradient(160deg,rgba(255,150,130,.5),rgba(90,30,22,.3)) border-box}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1286 |
.opTx{flex:1;min-width:0}
|
| 1287 |
.opTx b{display:block;font-family:var(--fT);font-size:11.5px;color:#eaf7ff;letter-spacing:.04em}
|
| 1288 |
.opTx span{display:block;font-size:10px;color:#8fb3cc;font-weight:600;margin-top:2px}
|
| 1289 |
-
.
|
|
|
|
|
|
|
| 1290 |
.opMod.on .opRw{color:#ffb0a2}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1291 |
#opPayout{margin-top:11px;text-align:center;font-family:var(--fT);font-size:11px;letter-spacing:.14em;
|
| 1292 |
color:#8fb3cc;font-weight:800}
|
| 1293 |
#opPayout b{color:#ffe9ad;font-size:15px}
|
|
@@ -1486,6 +1565,16 @@ body.menuMode #fps,body.menuMode #atkAlert,body.menuMode #waveAlert{display:none
|
|
| 1486 |
.facUnit b{display:block;font-family:var(--fT);font-size:10px;letter-spacing:.09em;color:#eaf4ff}
|
| 1487 |
.facUnit span{display:block;margin-top:1px;font-size:8.5px;font-weight:800;letter-spacing:.05em;color:var(--fc)}
|
| 1488 |
.facUnit i{display:block;margin-top:3px;font-size:9px;font-style:normal;line-height:1.4;color:#93b0c6;font-weight:600}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1489 |
.facBonus{margin-top:9px;padding:8px 9px;border-radius:8px;font-size:10px;line-height:1.45;
|
| 1490 |
color:#c3dcee;font-weight:600;background:color-mix(in srgb,var(--fc) 11%,rgba(4,9,16,.7));
|
| 1491 |
border:1px solid color-mix(in srgb,var(--fc) 28%,transparent)}
|
|
@@ -1548,11 +1637,17 @@ body.godMode #godBadge{display:flex}
|
|
| 1548 |
.setRow[data-set="godMode"] .sTx b{color:#ffe08a}
|
| 1549 |
|
| 1550 |
@media (max-width:600px) and (orientation:portrait){
|
| 1551 |
-
|
| 1552 |
-
|
| 1553 |
-
|
| 1554 |
-
#
|
| 1555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1556 |
|
| 1557 |
#cmdbar{left:0;right:0;bottom:0;width:auto;gap:5px;align-items:flex-end;
|
| 1558 |
padding:7px calc(var(--sar) + 8px) max(calc(var(--sab) + 8px),46px) calc(var(--sal) + 8px);
|
|
@@ -1580,7 +1675,87 @@ body.godMode #godBadge{display:flex}
|
|
| 1580 |
#placeUI{bottom:max(calc(var(--sab) + 160px),200px);gap:7px}
|
| 1581 |
#placeUI .cbtn{width:52px;height:52px;font-size:22px}
|
| 1582 |
#placeRotL,#placeRotR{width:46px!important;height:46px!important}
|
| 1583 |
-
#goalBar{
|
| 1584 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1585 |
#atkAlert,#waveAlert{max-width:calc(100vw - var(--sal) - var(--sar) - 20px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 1586 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
border:1px solid transparent;background:var(--panelG) padding-box,var(--steelB) border-box;
|
| 53 |
box-shadow:0 2px 8px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.1);
|
| 54 |
clip-path:polygon(6px 0,100% 0,100% calc(100% - 6px),calc(100% - 6px) 100%,0 100%,0 6px)}
|
| 55 |
+
.res .ic{position:relative;isolation:isolate;flex:0 0 auto;width:18px;height:18px;overflow:hidden;border-radius:5px;
|
| 56 |
+
border:1px solid rgba(255,255,255,.62);box-shadow:inset 0 1px 2px rgba(255,255,255,.28),0 0 8px currentColor}
|
| 57 |
+
.res .ic::after{content:'';position:absolute;z-index:1;inset:3px;background:#f5fbff;
|
| 58 |
+
filter:drop-shadow(0 1px 1px rgba(0,0,0,.68)) drop-shadow(0 0 2px rgba(255,255,255,.72))}
|
| 59 |
+
#massIc::after{clip-path:polygon(50% 0,92% 25%,78% 88%,22% 88%,8% 25%)}
|
| 60 |
+
#enIc::after{clip-path:polygon(56% 0,25% 50%,48% 50%,35% 100%,78% 40%,55% 40%)}
|
| 61 |
+
#unitRes .ic::after{inset:2px;background:
|
| 62 |
+
radial-gradient(circle at 50% 26%,#f5fbff 0 18%,transparent 20%),
|
| 63 |
+
radial-gradient(ellipse at 50% 90%,#f5fbff 0 43%,transparent 45%)}
|
| 64 |
.res b{font-variant-numeric:tabular-nums;font-size:15px;font-weight:700;letter-spacing:.02em}
|
| 65 |
.res .rate{font-size:11px;opacity:.85;font-variant-numeric:tabular-nums;font-weight:600}
|
| 66 |
+
#massIc{color:#5dff9a;background:linear-gradient(160deg,#45f194,#159952 62%,#063d23)}
|
| 67 |
+
#enIc{color:#ffd257;background:linear-gradient(160deg,#ffe786,#d29d22 62%,#654607)}
|
| 68 |
+
#unitRes .ic{color:#41c8ff;background:linear-gradient(160deg,#7fe0ff,#248dbb 62%,#0a405e)}
|
| 69 |
#fps{margin-left:auto;padding:3px 9px;font-size:11px;opacity:.9;border-radius:8px;font-weight:600;
|
| 70 |
border:1px solid transparent;background:var(--panelG) padding-box,var(--steelB) border-box}
|
| 71 |
#menuBtn,#spdBtn{pointer-events:auto;color:#dfeeff;border-radius:9px;padding:4px 12px;font-size:15px;
|
| 72 |
+
min-width:44px;min-height:44px;
|
| 73 |
border:1px solid transparent;background:var(--panelG2) padding-box,var(--steelB) border-box;
|
| 74 |
+
box-shadow:0 2px 8px rgba(0,0,0,.5),inset 0 1px 0 rgba(255,255,255,.12);transition:transform .08s ease,filter .08s ease,box-shadow .08s ease}
|
| 75 |
#spdBtn{font-size:12px;font-weight:800;font-family:var(--fT);padding:5px 8px}
|
| 76 |
+
#menuBtn:active,#spdBtn:active{transform:translateY(1px) scale(.96);filter:brightness(1.28);
|
| 77 |
+
box-shadow:0 1px 3px rgba(0,0,0,.62),inset 0 2px 7px rgba(0,0,0,.38)}
|
| 78 |
+
#menuBtn:focus-visible,#spdBtn:focus-visible{outline:2px solid #8ee5ff;outline-offset:2px}
|
| 79 |
|
| 80 |
/* ---------- ALERT / TOAST ---------- */
|
| 81 |
/* z-index 110, not 20. Overlays (menus, profile, settings, armory) sit at 50,
|
|
|
|
| 205 |
#wcRow{position:absolute;top:calc(var(--sat) + 36px);right:8px;z-index:9;display:none;gap:4px;align-items:center;
|
| 206 |
border:1px solid transparent;background:var(--panelG) padding-box,var(--redB) border-box;
|
| 207 |
border-radius:9px;padding:2px 8px;pointer-events:auto;box-shadow:0 2px 8px rgba(0,0,0,.5)}
|
| 208 |
+
.wcChip{display:grid;place-items:center;flex:0 0 27px;width:27px;height:27px;border-radius:7px;font-size:15px;
|
| 209 |
+
color:#fff;border:1px solid rgba(255,190,171,.42);background:linear-gradient(180deg,rgba(98,45,39,.9),rgba(36,17,22,.94));
|
| 210 |
+
box-shadow:inset 0 1px 0 rgba(255,255,255,.14),0 1px 4px rgba(0,0,0,.45);filter:saturate(1.18) contrast(1.08)}
|
| 211 |
.wcMult{font-size:11px;color:#ffb0a2;font-weight:700;font-family:var(--fT)}
|
| 212 |
+
#wcRow:active{filter:brightness(1.2)}
|
| 213 |
|
| 214 |
/* ---------- OBJECTIVE / CLOCK BAR ---------- */
|
| 215 |
#goalBar{position:absolute;top:calc(var(--sat) + 36px);left:50%;transform:translateX(-50%);z-index:9;display:none;
|
|
|
|
| 305 |
width:max-content;max-width:min(96vw,398px);max-height:min(56vh,56dvh);overflow-y:auto;-webkit-overflow-scrolling:touch;
|
| 306 |
clip-path:polygon(14px 0,calc(100% - 14px) 0,100% 14px,100% calc(100% - 14px),calc(100% - 14px) 100%,14px 100%,0 calc(100% - 14px),0 14px)}
|
| 307 |
#buildMenu .grid,#prodMenu .grid{display:flex;gap:7px;flex-wrap:wrap;justify-content:center}
|
| 308 |
+
.menuRoleBrief{position:relative;display:grid;grid-template-columns:112px minmax(0,1fr);min-height:116px;
|
| 309 |
+
margin:7px 0 9px;overflow:hidden;border-radius:11px;border:1px solid rgba(88,189,238,.27);
|
| 310 |
+
background:linear-gradient(115deg,rgba(15,39,60,.96),rgba(8,18,30,.98));
|
| 311 |
+
box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 4px 13px rgba(0,0,0,.3)}
|
| 312 |
+
.menuRoleBrief::after{content:'';position:absolute;inset:0;pointer-events:none;background:
|
| 313 |
+
linear-gradient(105deg,transparent 48%,rgba(65,200,255,.04) 48.5%,transparent 49%),
|
| 314 |
+
radial-gradient(circle at 88% 10%,rgba(65,200,255,.13),transparent 45%)}
|
| 315 |
+
.menuRoleVisual{min-width:0;min-height:116px;border-right:1px solid rgba(88,189,238,.2);z-index:1}
|
| 316 |
+
.menuRoleVisual .mfIntelPreview{height:100%;min-height:116px;border:0;border-radius:0;background:transparent}
|
| 317 |
+
.menuRoleVisual .mfIntelPreviewTag{font-size:6px;top:5px;left:5px}
|
| 318 |
+
.menuRoleCopy{position:relative;z-index:2;min-width:0;padding:8px 9px 7px;display:flex;flex-direction:column;align-items:flex-start}
|
| 319 |
+
.menuRoleEyebrow{width:100%;display:flex;align-items:center;gap:5px;color:#7fdfff;
|
| 320 |
+
font:800 7px/1 var(--fT);letter-spacing:.1em;text-transform:uppercase}
|
| 321 |
+
.menuRoleEyebrow>b{margin-left:auto;color:#7c9bb0;font-size:6px;white-space:nowrap}
|
| 322 |
+
.menuRoleCopy>strong{margin-top:6px;color:#effaff;font:800 13px/1 var(--fT);letter-spacing:.08em;text-transform:uppercase}
|
| 323 |
+
.menuRoleCopy>p{margin:5px 0 0;color:#b9cedc;font-size:8.5px;line-height:1.3}
|
| 324 |
+
.menuRoleDoctrine{margin-top:5px;color:#ffd66f;font:800 7px/1.2 var(--fT);letter-spacing:.055em}
|
| 325 |
+
.menuRoleDoctrine>span{margin-right:5px;color:#6f9bb6}
|
| 326 |
+
.menuRoleCopy>small{margin-top:4px;color:#f0a99b;font-size:7px;line-height:1.2}
|
| 327 |
+
.menuRoleInspect{margin-top:auto;min-height:31px;padding:5px 9px;border-radius:6px;color:#9eeaff;
|
| 328 |
+
border:1px solid rgba(95,198,244,.32);background:rgba(24,66,91,.62);font:800 7px/1 var(--fT);letter-spacing:.08em}
|
| 329 |
+
.menuRoleInspect:active{background:rgba(45,114,151,.84);transform:scale(.97)}
|
| 330 |
.bcard{width:84px;border-radius:10px;padding:6px 3px 5px;display:flex;flex-direction:column;align-items:center;gap:2px;
|
| 331 |
font-size:10.5px;pointer-events:auto;position:relative;overflow:hidden;
|
| 332 |
border:1px solid transparent;background:linear-gradient(180deg,rgba(30,46,64,.95),rgba(13,21,33,.97)) padding-box,var(--steelB) border-box;
|
|
|
|
| 360 |
border:1px solid transparent;background:var(--panelG2) padding-box,var(--steelB) border-box;font-family:var(--fU);text-transform:uppercase}
|
| 361 |
#upBtn{color:var(--gold);background:linear-gradient(180deg,rgba(64,48,14,.95),rgba(30,22,6,.97)) padding-box,var(--goldB) border-box}
|
| 362 |
#repeatBtn.on{color:var(--green);box-shadow:0 0 9px rgba(93,255,154,.35)}
|
| 363 |
+
@media(max-width:365px){
|
| 364 |
+
.menuRoleBrief{grid-template-columns:96px minmax(0,1fr)}
|
| 365 |
+
.menuRoleCopy>p{font-size:8px}.menuRoleCopy>small{display:none}
|
| 366 |
+
}
|
| 367 |
|
| 368 |
/* ---------- PLACEMENT CONFIRM ---------- */
|
| 369 |
#placeUI{position:absolute;left:50%;transform:translateX(-50%);bottom:calc(var(--sab) + 68px);z-index:12;display:none;gap:12px}
|
|
|
|
| 482 |
.goBreak span{padding:3px 6px;border-radius:6px;font-family:var(--fU);font-size:9px;letter-spacing:.04em;
|
| 483 |
color:#9fc6e0;background:rgba(22,38,56,.8);border:1px solid rgba(110,165,205,.22)}
|
| 484 |
.goBreak b{color:#ffe9ad}.goData{margin-top:7px;color:#9dffcf;font-size:12px;letter-spacing:.08em}
|
| 485 |
+
.goBreak.research{padding-top:7px;border-top:1px solid rgba(95,181,215,.18)}
|
| 486 |
+
.goBreak.research strong{flex:0 0 100%;text-align:center;color:#79d9ff;font:800 7px var(--fT);letter-spacing:.15em}
|
| 487 |
+
.goAccountSalvage{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:10px 1px 6px;
|
| 488 |
+
color:#9fffc4;font:800 8px var(--fT);letter-spacing:.13em}
|
| 489 |
+
.goAccountSalvage small{color:#789aae;font:600 7px var(--fU);letter-spacing:.02em;text-transform:none}
|
| 490 |
+
.goResourceGrid.account .goRes{border-color:rgba(95,225,156,.2);background:rgba(12,38,32,.64)}
|
| 491 |
.goData small{display:block;color:#7fb29c;font:600 9px var(--fU);letter-spacing:.03em}.goData.muted{color:#7f9aab;font-size:10px}
|
| 492 |
|
| 493 |
/* ---------- META HEADER ---------- */
|
|
|
|
| 701 |
background:var(--panelG2) padding-box,var(--steelB) border-box}
|
| 702 |
.setupAdv.on{color:#fff;background:linear-gradient(180deg,rgba(46,110,152,.96),rgba(20,52,80,.97)) padding-box,
|
| 703 |
linear-gradient(160deg,#c6f2ff,#41c8ff 40%,#10465f 70%,#8fe0ff) border-box}
|
| 704 |
+
.setupTabs{flex:0 0 auto;width:100%;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;
|
| 705 |
padding:0 calc(var(--sar) + 14px) 10px calc(var(--sal) + 14px);
|
| 706 |
background:rgba(6,12,22,.98);z-index:4}
|
| 707 |
+
.setupTabBtn{min-width:0;min-height:48px;padding:9px 2px;border-radius:9px;font-family:var(--fT);
|
| 708 |
+
font-size:8px;letter-spacing:.045em;font-weight:800;color:#789bb5;border:1px solid rgba(120,170,205,.24);
|
| 709 |
background:linear-gradient(180deg,rgba(23,36,53,.96),rgba(9,17,28,.97))}
|
| 710 |
.setupTabBtn.on{color:#f0fbff;border-color:#41c8ff;background:linear-gradient(180deg,rgba(43,113,154,.9),rgba(15,44,68,.96));
|
| 711 |
box-shadow:0 0 12px rgba(65,200,255,.25)}
|
|
|
|
| 724 |
display:flex;align-items:center;justify-content:center;text-align:center;line-height:1.15}
|
| 725 |
.optGrid{display:grid;grid-template-columns:1fr 1fr;gap:9px}
|
| 726 |
.optGrid .dbtn{min-width:0}
|
| 727 |
+
#battleScaleRow{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}
|
| 728 |
+
#battleScaleRow .bsbtn{min-width:0;min-height:68px;padding:8px 4px;flex-direction:column;gap:2px}
|
| 729 |
+
#battleScaleRow .bsbtn b{font-family:var(--fT);font-size:10px;letter-spacing:.07em;color:#e8f6ff}
|
| 730 |
+
#battleScaleRow .bsbtn span{font-family:var(--fT);font-size:9px;letter-spacing:.04em;color:#79b9df}
|
| 731 |
+
#battleScaleRow .bsbtn small{font-size:8px;font-weight:800;letter-spacing:.04em;color:#718ca0}
|
| 732 |
+
#battleScaleRow .bsbtn.on b{color:#fff}
|
| 733 |
+
#battleScaleRow .bsbtn.on span{color:#9deaff}
|
| 734 |
+
#battleScaleRow .bsbtn.on small{color:#c8f3ff}
|
| 735 |
.secHint{margin-top:11px;font-size:11.5px;line-height:1.4;color:#8fb3cc;font-weight:600;text-align:left}
|
| 736 |
.secHint.danger{color:#c98d80}
|
| 737 |
.advWrap{display:contents!important}
|
|
|
|
| 739 |
#setupScr:not(.setupAll)[data-tab="map"] [data-setup-tab]:not([data-setup-tab="map"]),
|
| 740 |
#setupScr:not(.setupAll)[data-tab="forces"] [data-setup-tab]:not([data-setup-tab="forces"]),
|
| 741 |
#setupScr:not(.setupAll)[data-tab="rules"] [data-setup-tab]:not([data-setup-tab="rules"]),
|
| 742 |
+
#setupScr:not(.setupAll)[data-tab="economy"] [data-setup-tab]:not([data-setup-tab="economy"]),
|
| 743 |
+
#setupScr:not(.setupAll)[data-tab="modifiers"] [data-setup-tab]:not([data-setup-tab="modifiers"]){display:none}
|
| 744 |
.setupFoot{flex:0 0 auto;width:100%;display:flex;gap:10px;align-items:center;justify-content:center;
|
| 745 |
/* Some Android WebViews report a zero bottom inset while still drawing a
|
| 746 |
3-button/navigation shelf. The 54px floor is the physical escape hatch:
|
|
|
|
| 1332 |
box-shadow:0 0 14px rgba(255,106,85,.35)}
|
| 1333 |
.thBtn.on b{color:#fff} .thBtn.on span{color:#ffb0a2}
|
| 1334 |
.thBtn.lock{opacity:.34}
|
| 1335 |
+
#opModRow{display:flex;flex-direction:column;gap:8px}
|
| 1336 |
+
.opMod{position:relative;display:grid;grid-template-columns:96px minmax(0,1fr) auto;align-items:center;gap:10px;min-height:96px;padding:7px 10px 7px 7px;border-radius:12px;text-align:left;overflow:hidden;
|
| 1337 |
+
cursor:pointer;touch-action:manipulation;
|
| 1338 |
border:1px solid transparent;
|
| 1339 |
background:linear-gradient(180deg,rgba(20,30,46,.92),rgba(10,17,28,.94)) padding-box,
|
| 1340 |
linear-gradient(160deg,rgba(120,170,205,.24),rgba(30,60,88,.14)) border-box}
|
| 1341 |
+
.opMod::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(90deg,transparent 0 58%,rgba(112,184,224,.035) 58.5% 59%,transparent 59.5%)}
|
| 1342 |
.opMod.on{background:linear-gradient(180deg,rgba(58,20,16,.94),rgba(28,10,8,.96)) padding-box,
|
| 1343 |
linear-gradient(160deg,rgba(255,150,130,.5),rgba(90,30,22,.3)) border-box}
|
| 1344 |
+
.opModArt{position:relative;align-self:stretch;min-height:80px;overflow:hidden;border-radius:9px;border:1px solid rgba(126,181,215,.25);
|
| 1345 |
+
background-image:url('../../assets/modifiers/modifier-art-atlas-v1.png');background-size:500% 200%;background-position:var(--opx) var(--opy);background-repeat:no-repeat;
|
| 1346 |
+
box-shadow:inset 0 0 18px rgba(0,0,0,.28),0 4px 10px rgba(0,0,0,.28)}
|
| 1347 |
+
.opModArt::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,transparent 45%,rgba(5,12,21,.18)),linear-gradient(180deg,rgba(82,180,228,.03),rgba(1,5,10,.25));pointer-events:none}
|
| 1348 |
+
.opModArt i{position:absolute;left:6px;bottom:6px;width:18px;height:3px;border-radius:2px;background:#69caef;box-shadow:0 0 8px #55c4ee;opacity:.8}
|
| 1349 |
+
.opMod.on .opModArt{border-color:rgba(255,139,110,.64);box-shadow:inset 0 0 20px rgba(92,17,8,.1),0 0 15px rgba(255,89,58,.2)}
|
| 1350 |
+
.opMod.on .opModArt i{background:#ff8a70;box-shadow:0 0 8px #ff6248}
|
| 1351 |
+
.opMod.lock{cursor:not-allowed;opacity:.68}
|
| 1352 |
+
.opMod.lock .opModArt{filter:saturate(.38) brightness(.62)}
|
| 1353 |
+
.opMod.lock .opModArt::before{content:"🔒";position:absolute;z-index:2;left:50%;top:50%;transform:translate(-50%,-50%);width:34px;height:34px;display:flex;align-items:center;justify-content:center;border-radius:50%;background:rgba(3,8,14,.82);border:1px solid rgba(190,211,224,.25);font-size:15px}
|
| 1354 |
+
.opMod.lock .opRw{color:#526b7d}
|
| 1355 |
.opTx{flex:1;min-width:0}
|
| 1356 |
.opTx b{display:block;font-family:var(--fT);font-size:11.5px;color:#eaf7ff;letter-spacing:.04em}
|
| 1357 |
.opTx span{display:block;font-size:10px;color:#8fb3cc;font-weight:600;margin-top:2px}
|
| 1358 |
+
.opUnlock{display:block;margin-top:5px;color:#f0ad86;font:800 8px var(--fT);letter-spacing:.055em;line-height:1.25}
|
| 1359 |
+
.opUnlock.earned{color:#70d9a4}
|
| 1360 |
+
.opRw{position:relative;z-index:1;align-self:start;margin-top:3px;padding:5px 6px;border-radius:7px;border:1px solid rgba(102,151,182,.24);background:rgba(4,11,19,.7);font-family:var(--fT);font-size:10px;font-weight:900;color:#7fa8c6}
|
| 1361 |
.opMod.on .opRw{color:#ffb0a2}
|
| 1362 |
+
@media (max-width:360px){
|
| 1363 |
+
.opMod{grid-template-columns:82px minmax(0,1fr) auto;gap:7px;min-height:90px;padding-right:7px}.opModArt{min-height:76px}.opTx b{font-size:10.5px}.opTx span{font-size:9px}.opRw{font-size:9px;padding:4px}
|
| 1364 |
+
}
|
| 1365 |
+
@media (orientation:landscape) and (min-width:760px){
|
| 1366 |
+
#opModRow{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.opMod{grid-template-columns:112px minmax(0,1fr) auto;min-height:102px}.opModArt{min-height:86px}
|
| 1367 |
+
}
|
| 1368 |
+
#setupScr[data-tab="modifiers"] .setupCard>.secHint:first-of-type{margin:-3px 0 12px}
|
| 1369 |
+
#setupScr .wbtn.lock{opacity:.38;filter:saturate(.35);cursor:not-allowed}
|
| 1370 |
#opPayout{margin-top:11px;text-align:center;font-family:var(--fT);font-size:11px;letter-spacing:.14em;
|
| 1371 |
color:#8fb3cc;font-weight:800}
|
| 1372 |
#opPayout b{color:#ffe9ad;font-size:15px}
|
|
|
|
| 1565 |
.facUnit b{display:block;font-family:var(--fT);font-size:10px;letter-spacing:.09em;color:#eaf4ff}
|
| 1566 |
.facUnit span{display:block;margin-top:1px;font-size:8.5px;font-weight:800;letter-spacing:.05em;color:var(--fc)}
|
| 1567 |
.facUnit i{display:block;margin-top:3px;font-size:9px;font-style:normal;line-height:1.4;color:#93b0c6;font-weight:600}
|
| 1568 |
+
.facDoctrine{display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:9px}
|
| 1569 |
+
.facDoctrine>div{min-width:0;padding:7px 8px;border-radius:8px;font-size:8.5px;line-height:1.4;font-weight:650;
|
| 1570 |
+
background:rgba(4,9,16,.7);border:1px solid rgba(130,165,190,.19);color:#adc5d6}
|
| 1571 |
+
.facDoctrine b{display:block;margin-bottom:3px;font:800 8px/1 var(--fT);letter-spacing:.11em}
|
| 1572 |
+
.facStrong b{color:#76efaa}.facWeak b{color:#ff927f}
|
| 1573 |
+
.facEndgoal{margin-top:7px;padding:8px 9px;border-radius:8px;background:rgba(4,9,16,.72);
|
| 1574 |
+
border:1px solid color-mix(in srgb,var(--fc) 25%,transparent)}
|
| 1575 |
+
.facEndgoal>span{display:block;color:var(--fc);font:800 8px/1 var(--fT);letter-spacing:.13em}
|
| 1576 |
+
.facEndgoal>b{display:block;margin-top:5px;color:#f1f8ff;font:800 9px/1.25 var(--fT);letter-spacing:.05em}
|
| 1577 |
+
.facEndgoal>p{margin:5px 0 0;color:#9eb8ca;font-size:9px;line-height:1.45;font-weight:600}
|
| 1578 |
.facBonus{margin-top:9px;padding:8px 9px;border-radius:8px;font-size:10px;line-height:1.45;
|
| 1579 |
color:#c3dcee;font-weight:600;background:color-mix(in srgb,var(--fc) 11%,rgba(4,9,16,.7));
|
| 1580 |
border:1px solid color-mix(in srgb,var(--fc) 28%,transparent)}
|
|
|
|
| 1637 |
.setRow[data-set="godMode"] .sTx b{color:#ffe08a}
|
| 1638 |
|
| 1639 |
@media (max-width:600px) and (orientation:portrait){
|
| 1640 |
+
/* Explicit portrait rows keep variable-length objectives and modifier sets
|
| 1641 |
+
from borrowing the same pixels. These offsets all share the same safe-area
|
| 1642 |
+
origin so a notched WebView cannot shift only one part of the stack. */
|
| 1643 |
+
#topbar{left:var(--sal);right:var(--sar);min-height:54px;display:grid;
|
| 1644 |
+
grid-template-columns:repeat(3,minmax(0,1fr)) 44px 44px;gap:4px;padding:5px 6px;font-size:12px}
|
| 1645 |
+
#topbar .res{min-width:0;min-height:36px;gap:4px;padding:3px 5px 3px 4px;border-radius:8px;overflow:hidden}
|
| 1646 |
+
#topbar .res b{min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:13px;white-space:nowrap}
|
| 1647 |
+
#topbar .res .rate{flex:0 0 auto;font-size:9px}.res .ic{width:19px;height:19px}
|
| 1648 |
+
#menuBtn,#spdBtn{display:grid;place-items:center;width:44px;min-width:44px;height:44px;min-height:44px;padding:0}
|
| 1649 |
+
#menuBtn{font-size:19px}#fps{display:none!important}
|
| 1650 |
+
body.godMode #godBadge{position:fixed;top:calc(var(--sat) + 128px);right:calc(var(--sar) + 8px);z-index:12;max-width:38vw}
|
| 1651 |
|
| 1652 |
#cmdbar{left:0;right:0;bottom:0;width:auto;gap:5px;align-items:flex-end;
|
| 1653 |
padding:7px calc(var(--sar) + 8px) max(calc(var(--sab) + 8px),46px) calc(var(--sal) + 8px);
|
|
|
|
| 1675 |
#placeUI{bottom:max(calc(var(--sab) + 160px),200px);gap:7px}
|
| 1676 |
#placeUI .cbtn{width:52px;height:52px;font-size:22px}
|
| 1677 |
#placeRotL,#placeRotR{width:46px!important;height:46px!important}
|
| 1678 |
+
#goalBar{top:calc(var(--sat) + 58px);min-height:30px;max-height:30px;
|
| 1679 |
+
max-width:calc(100vw - var(--sal) - var(--sar) - 16px);overflow:hidden;text-overflow:ellipsis}
|
| 1680 |
+
/* Modifier icons and live threat status share a reserved split row. Fixed
|
| 1681 |
+
columns plus local overflow mean neither panel can cover its neighbour. */
|
| 1682 |
+
#wcRow{top:calc(var(--sat) + 92px);left:calc(var(--sal) + 8px);right:auto;
|
| 1683 |
+
width:calc(58vw - var(--sal) - 12px);min-height:32px;max-height:32px;padding:2px 5px;
|
| 1684 |
+
overflow-x:auto;overflow-y:hidden;white-space:nowrap;scrollbar-width:none}
|
| 1685 |
+
#wcRow::-webkit-scrollbar{display:none}
|
| 1686 |
+
#infMeter{top:calc(var(--sat) + 92px);left:auto;right:calc(var(--sar) + 8px);
|
| 1687 |
+
width:calc(42vw - var(--sar) - 12px);min-height:32px;max-height:32px;justify-content:center;
|
| 1688 |
+
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:3px 6px}
|
| 1689 |
+
#heroBar{top:calc(var(--sat) + 128px);left:calc(var(--sal) + 8px);right:auto;
|
| 1690 |
+
width:clamp(168px,55vw,220px);max-width:55vw;min-height:34px}
|
| 1691 |
+
/* Transient panels begin below the complete live-HUD stack. Their base rules
|
| 1692 |
+
predate the split-row layout and used to open on top of modifiers, threat,
|
| 1693 |
+
and the Commander bars even though the resting HUD looked correct. */
|
| 1694 |
+
#toast,#radioAck{top:calc(var(--sat) + 176px)}
|
| 1695 |
+
#unitCard{top:calc(var(--sat) + 176px)}
|
| 1696 |
+
#abBarrage .cdring,#abClass .cdring{font-size:10px;letter-spacing:0}
|
| 1697 |
+
#abBarrage>span:not(.em):not(.lvl),#abClass>span:not(.em):not(.lvl){font-size:7px;letter-spacing:0}
|
| 1698 |
#atkAlert,#waveAlert{max-width:calc(100vw - var(--sal) - var(--sar) - 20px);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 1699 |
}
|
| 1700 |
+
@media (max-width:380px) and (orientation:portrait){
|
| 1701 |
+
/* On 320–360px phones the live rates are the only expendable text. Values,
|
| 1702 |
+
resource identities, and both 44px command targets always remain visible. */
|
| 1703 |
+
#topbar .res .rate{display:none}
|
| 1704 |
+
#topbar{grid-template-columns:repeat(3,minmax(0,1fr)) 44px 44px}
|
| 1705 |
+
}
|
| 1706 |
+
|
| 1707 |
+
/* Operations / Campaign takeover. Every selector is rooted to the injected
|
| 1708 |
+
pane so the normal Threat, Weekly and Mastery layouts keep their own rhythm. */
|
| 1709 |
+
#opsPane-campaign{padding:0;border:0;background:none;box-shadow:none;overflow:visible}
|
| 1710 |
+
.storyCampaignHero{display:flex;align-items:flex-start;gap:14px;margin:0 0 12px;padding:16px;
|
| 1711 |
+
border:1px solid rgba(101,202,255,.34);border-radius:14px;
|
| 1712 |
+
background:linear-gradient(90deg,rgba(5,14,25,.97) 0%,rgba(5,14,25,.87) 56%,rgba(5,14,25,.32)),
|
| 1713 |
+
var(--campaign-hero) center 42%/cover no-repeat,
|
| 1714 |
+
linear-gradient(145deg,rgba(13,32,52,.98),rgba(5,14,25,.97));box-shadow:0 12px 30px rgba(0,0,0,.28)}
|
| 1715 |
+
.storyCampaignHero>div:first-child{min-width:0;flex:1}.storyCampaignHero span,.storyCampaignSection>span{
|
| 1716 |
+
color:#7edcff;font-size:9px;letter-spacing:1.7px;font-weight:900}.storyCampaignHero h3{margin:5px 0 6px;
|
| 1717 |
+
color:#f4fbff;font-size:18px;line-height:1.08;letter-spacing:.5px}.storyCampaignHero p{margin:0;color:#adc0d2;
|
| 1718 |
+
font-size:11px;line-height:1.45}.storyCampaignCount{flex:0 0 64px;text-align:center;padding:9px 5px;border-radius:10px;
|
| 1719 |
+
border:1px solid rgba(126,220,255,.3);background:rgba(3,11,21,.62)}.storyCampaignCount b{display:block;
|
| 1720 |
+
color:#ffe089;font-size:25px;line-height:1}.storyCampaignCount span{display:block;margin-top:5px;color:#8ba7bd;
|
| 1721 |
+
font-size:7px;letter-spacing:.8px}
|
| 1722 |
+
.storyCampaignSection{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin:14px 3px 8px}
|
| 1723 |
+
.storyCampaignSection small{color:#7890a5;font-size:9px;text-align:right}.storyCampaignLadder{display:grid;gap:9px}
|
| 1724 |
+
.storyCampaignMission{display:grid;grid-template-columns:132px minmax(0,1fr);gap:11px;padding:11px;border-radius:13px;
|
| 1725 |
+
border:1px solid color-mix(in srgb,var(--cf) 34%,#26384a);background:linear-gradient(125deg,var(--cf2),rgba(7,16,28,.98) 58%);
|
| 1726 |
+
box-shadow:inset 3px 0 0 color-mix(in srgb,var(--cf) 72%,transparent),0 7px 18px rgba(0,0,0,.22)}
|
| 1727 |
+
.storyCampaignMission.current{border-color:color-mix(in srgb,var(--cf) 78%,white);box-shadow:inset 3px 0 0 var(--cf),
|
| 1728 |
+
0 0 18px color-mix(in srgb,var(--cf) 18%,transparent)}.storyCampaignMission.locked{filter:saturate(.55);opacity:.68}
|
| 1729 |
+
.storyCampaignKeyArt{position:relative;align-self:stretch;min-height:146px;border-radius:10px;overflow:hidden;
|
| 1730 |
+
border:1px solid color-mix(in srgb,var(--cf) 55%,#294158);background:#07101b}
|
| 1731 |
+
.storyCampaignKeyArt>img{width:100%;height:100%;display:block;object-fit:cover}.storyCampaignKeyArt::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,transparent 42%,rgba(2,7,13,.9))}
|
| 1732 |
+
.storyCampaignKeyArt>span{position:absolute;z-index:2;right:5px;top:5px;
|
| 1733 |
+
display:grid;place-items:center;width:30px;height:30px;padding:2px;border-radius:8px;background:rgba(3,9,17,.88);
|
| 1734 |
+
border:1px solid color-mix(in srgb,var(--cf) 60%,transparent)}.storyCampaignKeyArt>span img{width:100%;height:100%;object-fit:contain}
|
| 1735 |
+
.storyCampaignKeyArt>small{position:absolute;z-index:2;left:8px;right:8px;bottom:7px;color:#eff8ff;font:900 7px/1.25 var(--fT);letter-spacing:.08em;text-shadow:0 2px 6px #000}
|
| 1736 |
+
.storyCampaignMissionBody{min-width:0}.storyCampaignMission header{display:flex;align-items:flex-start;justify-content:space-between;
|
| 1737 |
+
gap:8px;margin-bottom:7px}.storyCampaignMission header small{display:block;color:var(--cf);font-size:8px;letter-spacing:1.2px;
|
| 1738 |
+
font-style:normal}.storyCampaignMission header b{display:block;margin-top:2px;color:#f5f9fc;font-size:13px;line-height:1.14}
|
| 1739 |
+
.storyCampaignState{flex:0 0 auto;padding:4px 6px;border:1px solid color-mix(in srgb,var(--cf) 54%,transparent);
|
| 1740 |
+
border-radius:6px;background:color-mix(in srgb,var(--cf2) 75%,black);color:var(--cf);font-size:7px;letter-spacing:.9px;font-style:normal}
|
| 1741 |
+
.storyCampaignIntel{display:grid;gap:5px}.storyCampaignIntel>span{color:#aabccc;font-size:9.5px;line-height:1.33}
|
| 1742 |
+
.storyCampaignIntel em{display:block;margin-bottom:1px;color:#6f8aa1;font-size:7px;font-style:normal;font-weight:900;letter-spacing:1px}
|
| 1743 |
+
.storyCampaignIntel .storyCampaignReward{color:#f2d47f}.storyCampaignAction{width:100%;min-height:48px;margin-top:9px;
|
| 1744 |
+
border:1px solid color-mix(in srgb,var(--cf) 75%,white);border-radius:9px;color:#06111b;font:900 10px var(--font);
|
| 1745 |
+
letter-spacing:1.1px;background:linear-gradient(180deg,color-mix(in srgb,var(--cf) 78%,white),var(--cf));
|
| 1746 |
+
box-shadow:0 5px 14px color-mix(in srgb,var(--cf) 18%,transparent)}.storyCampaignAction:active{transform:translateY(1px)}
|
| 1747 |
+
.storyCampaignLock{display:flex;align-items:center;min-height:44px;margin-top:8px;padding:0 9px;border-radius:8px;
|
| 1748 |
+
border:1px dashed rgba(135,154,172,.28);color:#8295a7;font-size:8.5px;line-height:1.3;letter-spacing:.4px}
|
| 1749 |
+
.storyCampaignArcLabel{margin-top:19px}.storyCampaignArcs{display:flex;gap:10px;padding:0 2px 8px;overflow-x:auto;
|
| 1750 |
+
scroll-snap-type:x mandatory;scrollbar-width:none}.storyCampaignArcs::-webkit-scrollbar{display:none}.storyCampaignArc{flex:0 0 270px;
|
| 1751 |
+
min-height:194px;padding:11px;border:1px solid color-mix(in srgb,var(--cf) 42%,#203245);border-radius:13px;
|
| 1752 |
+
background:radial-gradient(circle at 15% 0,color-mix(in srgb,var(--cf) 18%,transparent),transparent 46%),
|
| 1753 |
+
linear-gradient(145deg,var(--cf2),#07101b 72%);scroll-snap-align:start}.storyCampaignArcHead{display:flex;gap:10px;align-items:center}
|
| 1754 |
+
.storyCampaignArcHead>img{width:58px;height:70px;object-fit:cover;border-radius:8px;border:1px solid var(--cf)}
|
| 1755 |
+
.storyCampaignArcHead>div{min-width:0}.storyCampaignArcHead small{display:block;color:var(--cf);font-size:7px;letter-spacing:.7px}
|
| 1756 |
+
.storyCampaignArcHead b{display:block;margin:3px 0;color:#f1f7fb;font-size:12px;line-height:1.15}.storyCampaignArcHead span{color:#91a8b8;font-size:8px}
|
| 1757 |
+
.storyCampaignArcPath{margin:10px 0 6px;padding:7px;border-radius:7px;background:rgba(0,0,0,.28);color:var(--cf);
|
| 1758 |
+
font-size:8px;font-weight:900;line-height:1.35;letter-spacing:.55px;text-align:center}.storyCampaignArc p{margin:0;color:#9fb2c1;font-size:9px;line-height:1.4}
|
| 1759 |
+
@media(max-width:430px){.storyCampaignHero{padding:13px;min-height:142px;background-position:center 42%}.storyCampaignHero h3{font-size:16px}.storyCampaignHero p{font-size:10px}
|
| 1760 |
+
.storyCampaignMission{grid-template-columns:1fr;gap:9px;padding:9px}.storyCampaignKeyArt{height:112px;min-height:112px}
|
| 1761 |
+
.storyCampaignMission header{display:block}.storyCampaignState{display:inline-block;margin-top:5px}.storyCampaignArc{flex-basis:252px}}
|
src/tutorial.js
CHANGED
|
@@ -41,15 +41,14 @@
|
|
| 41 |
--------------------------------------------------------------------------- */
|
| 42 |
var KEEL_NAME='KEEL';
|
| 43 |
var KEEL_TAG='SHIP LIAISON · LANCE OF MORNING';
|
| 44 |
-
var GUIDE_VERSION=
|
| 45 |
|
| 46 |
var GREETING='KEEL online — ship liaison, Lance of Morning. Follow the gold signal: it marks the exact control for your next action. '+
|
| 47 |
'Tap SKIP any time; replay this guide from Settings.';
|
| 48 |
|
| 49 |
var SKIP_LINE='Understood. Find me again under Settings → Tutorial if you change your mind.';
|
| 50 |
|
| 51 |
-
var GRADUATION='
|
| 52 |
-
'Everything after this is those same systems, faster, with worse odds. '+
|
| 53 |
"I'll go quiet now — unless something's on fire.";
|
| 54 |
|
| 55 |
/* Each step tests REAL state. `say` is shown the moment a step becomes
|
|
@@ -58,55 +57,100 @@ var GRADUATION='That’s the job: a Commander, mass, power, production, territor
|
|
| 58 |
out of order gets fast-forwarded in silence instead of congratulated for
|
| 59 |
a step they finished five minutes ago. */
|
| 60 |
var STEPS=[
|
| 61 |
-
{ id:'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
say:'Tap open ground to fly the carrier there. When the landing signal turns green, tap DEPLOY BASE HERE.',
|
| 63 |
done:"Hull's down, engines cold. That was the easy part.",
|
| 64 |
test:function(){ return typeof matchLive!=='undefined' && !!matchLive; } },
|
| 65 |
|
| 66 |
-
{ id:'commander',
|
| 67 |
-
say:'Your COMMANDER is
|
| 68 |
done:'Commander identified. Keep it alive: losing every Commander loses the match.',
|
| 69 |
test:function(){ return typeof heroIdx==='number'&&heroIdx>=0&&!!usel[heroIdx]; } },
|
| 70 |
|
| 71 |
-
{ id:'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
say:'Mass buys units and structures. Tap BUILD → ECONOMY → Extractor, drag its hologram onto a ◆ deposit, then tap ✓.',
|
| 73 |
done:"Extractor placed. That's your first income line.",
|
| 74 |
test:function(){ return hasPlayerBld('mex'); } },
|
| 75 |
|
| 76 |
-
{ id:'power',
|
| 77 |
say:'Production also needs energy. Tap BUILD → ECONOMY → Reactor, place it on open ground inside the blue grid, then tap ✓.',
|
| 78 |
done:'Reactor placed. Mass and energy must both stay positive while building.',
|
| 79 |
test:function(){ return hasPlayerBld('pgen')||hasPlayerBld('geo'); } },
|
| 80 |
|
| 81 |
-
{ id:'fac',
|
| 82 |
-
say:
|
| 83 |
-
done:
|
| 84 |
test:function(){ return hasPlayerBld('fac'); } },
|
| 85 |
|
| 86 |
-
{ id:'territory', manual:
|
| 87 |
-
say:'
|
| 88 |
-
done:'
|
| 89 |
test:function(){ return MATCH.territoryAck; } },
|
| 90 |
|
| 91 |
-
{ id:'queue',
|
| 92 |
say:'Tap the Factory on the map, then tap the Striker unit card. The queue line shows what is being built.',
|
| 93 |
-
done:
|
| 94 |
test:function(){ return MATCH.sawQueue; } },
|
| 95 |
|
| 96 |
-
{ id:'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
say:'Protect the economy before the first push. Tap BUILD → DEFENCE → Sentinel and place it on the likely approach.',
|
| 98 |
done:'Basic perimeter online. Towers buy your army time; they do not replace it.',
|
| 99 |
test:function(){ return hasPlayerBld('turret'); } },
|
| 100 |
|
| 101 |
-
{ id:'
|
| 102 |
-
say:'
|
| 103 |
-
done:'
|
| 104 |
-
test:function(){ return MATCH.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
-
{ id:'
|
| 107 |
-
say:'
|
| 108 |
-
done:
|
| 109 |
-
test:function(){ return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
];
|
| 111 |
|
| 112 |
var BASE_ATTACK_LINES=[
|
|
@@ -136,8 +180,10 @@ var trainingPrev=null, trainingLaunched=false;
|
|
| 136 |
/* Facts about THIS match, tracked whether or not the tutorial is running, so
|
| 137 |
a mid-match "replay" (from Settings) can credit things the player already
|
| 138 |
demonstrably knows how to do instead of waiting for them to happen again. */
|
| 139 |
-
var MATCH={ sawSel:false, sawCombatSel:false, sawMove:false, sawQueue:false,
|
| 140 |
-
|
|
|
|
|
|
|
| 141 |
/* Reactive one-per-match latches. */
|
| 142 |
var REACT={ baseAttack:false, lowPower:false, hazard:false, unitLost:false, wave:false,
|
| 143 |
lastAlarmT:0, lastStallE:false, lastHazWarn:false, lastWarned:false };
|
|
@@ -148,7 +194,7 @@ function tutMeta(){
|
|
| 148 |
META.tutorial=META.tutorial||{done:false,skipped:false,version:0};
|
| 149 |
/* `progress` is additive save metadata: old profiles load exactly as before,
|
| 150 |
while the briefing can distinguish new, interrupted and completed runs.
|
| 151 |
-
It is a high-water mark only; the
|
| 152 |
authority whenever a mission is running. */
|
| 153 |
if(typeof META.tutorial.progress!=='number') META.tutorial.progress=META.tutorial.done?STEPS.length:0;
|
| 154 |
META.tutorial.progress=Math.max(0,Math.min(STEPS.length,META.tutorial.progress|0));
|
|
@@ -179,6 +225,10 @@ function hasCombatUnit(){
|
|
| 179 |
for(var i=0;i<unitHigh;i++) if(isCombatUnit(i)) return true;
|
| 180 |
return false;
|
| 181 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
function hasSelectedCombatUnit(){
|
| 183 |
if(typeof unitHigh!=='number') return false;
|
| 184 |
for(var i=0;i<unitHigh;i++) if(isCombatUnit(i)&&usel[i]) return true;
|
|
@@ -221,6 +271,7 @@ function buildDOM(){
|
|
| 221 |
'<div id="keelBody">'+
|
| 222 |
'<div id="keelHd"><span id="keelName" title="'+KEEL_TAG+'">'+KEEL_NAME+'</span><span id="keelStepTag"></span></div>'+
|
| 223 |
'<div id="keelTxt"></div>'+
|
|
|
|
| 224 |
'<button id="keelNext" type="button">GOT IT</button>'+
|
| 225 |
'</div>'+
|
| 226 |
'<button id="keelSkip" type="button" aria-label="Skip tutorial">SKIP</button>'+
|
|
@@ -234,8 +285,9 @@ function buildDOM(){
|
|
| 234 |
var nextBtn=document.getElementById('keelNext');
|
| 235 |
nextBtn.addEventListener('pointerdown',function(ev){
|
| 236 |
ev.preventDefault(); ev.stopPropagation();
|
| 237 |
-
|
| 238 |
-
|
|
|
|
| 239 |
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
|
| 240 |
});
|
| 241 |
/* Tapping the body re-shows the full-strength card if it has settled into
|
|
@@ -250,6 +302,12 @@ function renderBubble(text,tag){
|
|
| 250 |
if(!domReady) return;
|
| 251 |
if(text!==lastText){ var t=document.getElementById('keelTxt'); if(t) t.textContent=text; lastText=text; }
|
| 252 |
var tg=document.getElementById('keelStepTag'); if(tg) tg.textContent=tag||'';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
function showWrap(fresh){
|
| 255 |
if(!domReady) return;
|
|
@@ -337,8 +395,15 @@ function syncFocus(){
|
|
| 337 |
clearFocus();
|
| 338 |
if(!TUT.active||TUT.stepIdx>=STEPS.length) return;
|
| 339 |
var S=STEPS[TUT.stepIdx],next=document.getElementById('keelNext');
|
| 340 |
-
if(next)
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
var dep=document.getElementById('deployBtn');
|
| 343 |
if(shown(dep)) focusEl(dep); else cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.52,'TAP LANDING SITE');
|
| 344 |
} else if(S.id==='commander'){
|
|
@@ -346,6 +411,9 @@ function syncFocus(){
|
|
| 346 |
var cdr=document.getElementById('abHero');
|
| 347 |
if(shown(cdr)) focusEl(cdr);
|
| 348 |
else focusEl(document.querySelector('.hudDeckBtn[data-deck="powers"]'));
|
|
|
|
|
|
|
|
|
|
| 349 |
} else if(S.id==='mex') buildFocus('ECONOMY','Extractor','mex');
|
| 350 |
else if(S.id==='power') buildFocus('ECONOMY','Reactor','pgen');
|
| 351 |
else if(S.id==='fac') buildFocus('PRODUCTION','Factory','fac');
|
|
@@ -357,15 +425,35 @@ function syncFocus(){
|
|
| 357 |
var pm=document.getElementById('prodMenu');
|
| 358 |
if(shown(pm)) focusEl(byText('prodGrid','.bcard','Striker')||pm.querySelector('.bcard'));
|
| 359 |
else { var F=playerBld('fac'); if(F) cueWorld(F.x,F.y,'TAP FACTORY'); }
|
| 360 |
-
} else if(S.id==='
|
| 361 |
-
else if(S.id==='select'){
|
| 362 |
if(hasCombatUnit()) focusEl(document.getElementById('armyBtn'));
|
| 363 |
else { var F2=playerBld('fac'); if(F2) cueWorld(F2.x,F2.y,'UNIT BUILDING'); }
|
| 364 |
-
} else if(S.id==='
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
var sx=0,sy=0,n=0;
|
| 366 |
if(typeof unitHigh==='number') for(var j=0;j<unitHigh;j++) if(isCombatUnit(j)&&usel[j]){sx+=ux[j];sy+=uy[j];n++;}
|
| 367 |
-
if(n) cueWorld(sx/n+
|
| 368 |
else focusEl(document.getElementById('armyBtn'));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 369 |
}
|
| 370 |
}
|
| 371 |
/* Capped so a burst of state changes (several buildings finishing within the
|
|
@@ -426,6 +514,11 @@ function speakVoice(text){
|
|
| 426 |
--------------------------------------------------------------------------- */
|
| 427 |
function trackMatchSignals(){
|
| 428 |
if(sawQueuedUnit()) MATCH.sawQueue=true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
if(typeof unitHigh==='number' && (!MATCH.sawSel||!MATCH.sawCombatSel)){
|
| 430 |
for(var i=0;i<unitHigh;i++){
|
| 431 |
if(!ualive[i]||uteam[i]!==0) continue;
|
|
@@ -434,6 +527,11 @@ function trackMatchSignals(){
|
|
| 434 |
if(MATCH.sawSel&&MATCH.sawCombatSel) break;
|
| 435 |
}
|
| 436 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
if(typeof abCool!=='undefined'){
|
| 438 |
for(var k=0;k<abCool.length;k++){
|
| 439 |
if(abCool[k]>(MATCH.lastAbCool[k]||0)+0.01) MATCH.usedAbility=true;
|
|
@@ -452,8 +550,12 @@ function beginRun(){
|
|
| 452 |
TUT.stepIdx=0;
|
| 453 |
TUT.shownStepIdx=-1;
|
| 454 |
TUT.doneFlags=STEPS.map(function(){ return false; });
|
| 455 |
-
MATCH.sawSel=false;
|
| 456 |
-
MATCH.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 457 |
queue.length=0;
|
| 458 |
speak(GREETING,5.5,'greeting');
|
| 459 |
}
|
|
@@ -489,7 +591,7 @@ function evalSteps(){
|
|
| 489 |
}
|
| 490 |
var cur=STEPS[TUT.stepIdx];
|
| 491 |
restingText=cur.say;
|
| 492 |
-
restingTag='STEP '+(TUT.stepIdx+1)+' / '+STEPS.length;
|
| 493 |
if(TUT.shownStepIdx!==TUT.stepIdx){
|
| 494 |
TUT.shownStepIdx=TUT.stepIdx;
|
| 495 |
speak(cur.say,5.5,'step');
|
|
@@ -565,7 +667,11 @@ function onNewMatchBegin(){
|
|
| 565 |
REACT.baseAttack=false; REACT.lowPower=false; REACT.hazard=false; REACT.unitLost=false; REACT.wave=false;
|
| 566 |
REACT.lastAlarmT=(typeof alarmT!=='undefined')?alarmT:0;
|
| 567 |
REACT.lastStallE=false; REACT.lastHazWarn=false; REACT.lastWarned=false;
|
| 568 |
-
MATCH.sawSel=false;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
MATCH.lastAbCool=(typeof abCool!=='undefined')?abCool.slice():[0,0,0,0];
|
| 570 |
if(TUT.trainingMode){ TUT.forceNext=false; beginRun(); }
|
| 571 |
}
|
|
@@ -619,6 +725,27 @@ function restoreTrainingConfig(){
|
|
| 619 |
META.opmods=trainingPrev.opmods; META.threatSel=trainingPrev.threatSel;
|
| 620 |
trainingPrev=null;
|
| 621 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
function trainingSafety(){
|
| 623 |
if(!TUT.trainingMode||typeof running==='undefined'||!running||typeof matchLive==='undefined'||!matchLive) return;
|
| 624 |
/* No scripted ambush while the player is reading. The enemy still builds a
|
|
@@ -632,6 +759,10 @@ function trainingSafety(){
|
|
| 632 |
if(typeof resE!=='undefined'&&typeof RES_ECAP!=='undefined') resE[0]=Math.min(RES_ECAP[0],Math.max(resE[0],2200));
|
| 633 |
if(typeof heroIdx==='number'&&heroIdx>=0&&ualive[heroIdx]) uhp[heroIdx]=Math.max(uhp[heroIdx],uhpm[heroIdx]*.72);
|
| 634 |
var H=playerBld('hq'); if(H) H.hp=Math.max(H.hp,H.hpm*.75);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 635 |
}
|
| 636 |
function startTrainingMission(){
|
| 637 |
if(typeof running!=='undefined'&&running){ toast('Leave the current battle before starting training'); return; }
|
|
@@ -652,7 +783,7 @@ function startTrainingMission(){
|
|
| 652 |
if(typeof hideFrontScreens==='function') hideFrontScreens();
|
| 653 |
var load=document.getElementById('loadScr'); if(load) load.style.display='flex';
|
| 654 |
requestAnimationFrame(function(){requestAnimationFrame(function(){
|
| 655 |
-
applyTheme(); newSkirmish();
|
| 656 |
if(load) load.style.display='none';
|
| 657 |
toast('TRAINING OPERATION — threat paused while KEEL guides you');
|
| 658 |
});});
|
|
@@ -731,7 +862,7 @@ function resumeTrainingMission(){
|
|
| 731 |
function ensureTrainingEntry(){
|
| 732 |
var start=document.getElementById('startBtn'); if(!start||document.getElementById('keelTrainingCallout')) return;
|
| 733 |
var d=document.createElement('div'); d.id='keelTrainingCallout';
|
| 734 |
-
d.innerHTML='<div class="ktMiniIdentity"><img src="./assets/factions/nova_icon_96.png" alt="
|
| 735 |
+'<div><span class="ktState">FIRST OPERATION</span><b>FIELD ORIENTATION</b><small class="ktProgress">0 / '+STEPS.length+' OBJECTIVES</small></div></div>'
|
| 736 |
+'<button type="button">VIEW TRAINING</button>';
|
| 737 |
start.parentNode.insertBefore(d,start);
|
|
@@ -754,14 +885,15 @@ function appendTrainingOperation(){
|
|
| 754 |
+'<span>'+(typeof facIcon==='function'?facIcon('nova',38,'ktoCrestImg'):'✦')+'</span></div>'
|
| 755 |
+'<div class="ktoIdentity"><small>TRAINING OPERATION 01</small><b>FIELD ORIENTATION</b>'
|
| 756 |
+'<i>'+commander+' · KEEL tactical guidance</i>'
|
| 757 |
-
+'<p>A protected live-fire drop
|
| 758 |
-
+'<div class="ktoTeach" aria-label="Training topics"><span>⬡ ECONOMY</span><span>▣ PRODUCTION</span>'
|
| 759 |
-
+'<span>⛨ DEFENCE</span><span>
|
|
|
|
| 760 |
+'<div class="ktoProgress"><div><span>OBJECTIVE PROGRESS</span><b>'+S.progress+' / '+STEPS.length+'</b></div>'
|
| 761 |
+'<div class="ktoProgressTrack"><i style="width:'+progressPct+'%"></i></div>'
|
| 762 |
+'<small>'+(S.done?'Orientation certified · replay does not repeat the first-clear reward':
|
| 763 |
S.active?'Current lesson: '+(STEPS[Math.min(TUT.stepIdx,STEPS.length-1)].id||'field systems').toUpperCase():
|
| 764 |
-
S.interrupted?'Previous best retained · a new drop restarts at
|
| 765 |
+'<div class="ktoFacts"><div><b>'+STEPS.length+'</b><span>OBJECTIVES</span></div><div><b>EASY</b><span>FIXED OPPONENT</span></div>'
|
| 766 |
+'<div><b>SAFE</b><span>NO INFESTATION</span></div></div>'
|
| 767 |
+'<button class="ktoAction" type="button">'+S.action+'</button>';
|
|
@@ -859,11 +991,27 @@ function initTutorial(){
|
|
| 859 |
window.__keelMoveWrapped=true;
|
| 860 |
var _keelOrderMove=orderMove;
|
| 861 |
orderMove=function(wx,wy,patrol){
|
| 862 |
-
var combat=hasSelectedCombatUnit(),
|
| 863 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
return ok;
|
| 865 |
};
|
| 866 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
setInterval(tutTick,350);
|
| 868 |
}
|
| 869 |
window.initTutorial=initTutorial;
|
|
|
|
| 41 |
--------------------------------------------------------------------------- */
|
| 42 |
var KEEL_NAME='KEEL';
|
| 43 |
var KEEL_TAG='SHIP LIAISON · LANCE OF MORNING';
|
| 44 |
+
var GUIDE_VERSION=3;
|
| 45 |
|
| 46 |
var GREETING='KEEL online — ship liaison, Lance of Morning. Follow the gold signal: it marks the exact control for your next action. '+
|
| 47 |
'Tap SKIP any time; replay this guide from Settings.';
|
| 48 |
|
| 49 |
var SKIP_LINE='Understood. Find me again under Settings → Tutorial if you change your mind.';
|
| 50 |
|
| 51 |
+
var GRADUATION='Field orientation complete. Camera, economy, logistics, formations, scouting, technology and extraction are certified. '+
|
|
|
|
| 52 |
"I'll go quiet now — unless something's on fire.";
|
| 53 |
|
| 54 |
/* Each step tests REAL state. `say` is shown the moment a step becomes
|
|
|
|
| 57 |
out of order gets fast-forwarded in silence instead of congratulated for
|
| 58 |
a step they finished five minutes ago. */
|
| 59 |
var STEPS=[
|
| 60 |
+
{ id:'camera', icon:'◇',
|
| 61 |
+
say:'DRAG the battlefield to pan. PINCH to zoom and twist, or use VIEW → ROT / TILT. Move the camera once to confirm control.',
|
| 62 |
+
done:'Command view responding. The minimap can jump across the whole battlefield.',
|
| 63 |
+
test:function(){ return MATCH.sawCamera; } },
|
| 64 |
+
|
| 65 |
+
{ id:'deploy', icon:'⚓',
|
| 66 |
say:'Tap open ground to fly the carrier there. When the landing signal turns green, tap DEPLOY BASE HERE.',
|
| 67 |
done:"Hull's down, engines cold. That was the easy part.",
|
| 68 |
test:function(){ return typeof matchLive!=='undefined' && !!matchLive; } },
|
| 69 |
|
| 70 |
+
{ id:'commander', icon:'★',
|
| 71 |
+
say:'Your COMMANDER is the marked hero and a mobile builder. Tap POWERS, then CDR to select and center it.',
|
| 72 |
done:'Commander identified. Keep it alive: losing every Commander loses the match.',
|
| 73 |
test:function(){ return typeof heroIdx==='number'&&heroIdx>=0&&!!usel[heroIdx]; } },
|
| 74 |
|
| 75 |
+
{ id:'pickup', icon:'◆',
|
| 76 |
+
say:'A gold SUPPLY CACHE is marked nearby. Select the Commander or Constructor, then tap beside the cache so the unit collects it.',
|
| 77 |
+
done:'Cache secured. Pickups can grant resources, repairs, scans, veterancy or rare strike codes.',
|
| 78 |
+
test:function(){ return MATCH.sawPickup; } },
|
| 79 |
+
|
| 80 |
+
{ id:'mex', icon:'⛏',
|
| 81 |
say:'Mass buys units and structures. Tap BUILD → ECONOMY → Extractor, drag its hologram onto a ◆ deposit, then tap ✓.',
|
| 82 |
done:"Extractor placed. That's your first income line.",
|
| 83 |
test:function(){ return hasPlayerBld('mex'); } },
|
| 84 |
|
| 85 |
+
{ id:'power', icon:'⚡',
|
| 86 |
say:'Production also needs energy. Tap BUILD → ECONOMY → Reactor, place it on open ground inside the blue grid, then tap ✓.',
|
| 87 |
done:'Reactor placed. Mass and energy must both stay positive while building.',
|
| 88 |
test:function(){ return hasPlayerBld('pgen')||hasPlayerBld('geo'); } },
|
| 89 |
|
| 90 |
+
{ id:'fac', icon:'▣',
|
| 91 |
+
say:"Build a Factory under BUILD → PRODUCTION. It is this army's barracks and vehicle plant, and expands construction territory.",
|
| 92 |
+
done:'Factory placed. Open it to train the first field unit.',
|
| 93 |
test:function(){ return hasPlayerBld('fac'); } },
|
| 94 |
|
| 95 |
+
{ id:'territory', icon:'⌁', manual:'territoryAck', action:'GOT IT',
|
| 96 |
+
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.',
|
| 97 |
+
done:'Logistics and territory rules logged.',
|
| 98 |
test:function(){ return MATCH.territoryAck; } },
|
| 99 |
|
| 100 |
+
{ id:'queue', icon:'▤',
|
| 101 |
say:'Tap the Factory on the map, then tap the Striker unit card. The queue line shows what is being built.',
|
| 102 |
+
done:'Striker queued. Production consumes mass and energy over build time.',
|
| 103 |
test:function(){ return MATCH.sawQueue; } },
|
| 104 |
|
| 105 |
+
{ id:'train', icon:'⚔',
|
| 106 |
+
say:'Keep the Factory supplied until the Striker rolls out. Tap ARMY when it appears to select every field unit.',
|
| 107 |
+
done:'Combat unit trained and selected. Unit cards show role, range, armor and ammunition.',
|
| 108 |
+
test:function(){ return MATCH.sawCombatSel&&combatCount()>MATCH.startCombat; } },
|
| 109 |
+
|
| 110 |
+
{ id:'turret', icon:'⛨',
|
| 111 |
say:'Protect the economy before the first push. Tap BUILD → DEFENCE → Sentinel and place it on the likely approach.',
|
| 112 |
done:'Basic perimeter online. Towers buy your army time; they do not replace it.',
|
| 113 |
test:function(){ return hasPlayerBld('turret'); } },
|
| 114 |
|
| 115 |
+
{ id:'platoon', icon:'Ⅳ',
|
| 116 |
+
say:'With combat units selected, open PLATOONS and HOLD P1 to save them. Tap P1 later to recall; double-tap focuses the camera.',
|
| 117 |
+
done:'Platoon P1 stored. Saved groups remember membership and formation.',
|
| 118 |
+
test:function(){ return MATCH.sawPlatoon; } },
|
| 119 |
+
|
| 120 |
+
{ id:'formation', icon:'⌃',
|
| 121 |
+
say:'Open ORDERS, tap the formation button, then drag the hologram on the map and release. Every marker is one unit slot.',
|
| 122 |
+
done:'Formation placed. Spacing keeps mixed units readable and prevents stacking.',
|
| 123 |
+
test:function(){ return MATCH.sawFormation; } },
|
| 124 |
+
|
| 125 |
+
{ id:'attack', icon:'➜',
|
| 126 |
+
say:'Leave A-MOVE active and tap unexplored ground. Units advance and engage contacts. Green matchup chips are strong; red chips warn of counters.',
|
| 127 |
+
done:'Attack-move confirmed. Long-press a unit or structure for its full counter card.',
|
| 128 |
+
test:function(){ return MATCH.sawAttackMove; } },
|
| 129 |
+
|
| 130 |
+
{ id:'fog', icon:'⌾',
|
| 131 |
+
say:'Black fog hides enemies; grey terrain is remembered, not currently visible. Move the marked SCOUT toward the signal to reveal the sector.',
|
| 132 |
+
done:'Sensor contact expanded. Enemy positions remain hidden until a friendly sensor sees them.',
|
| 133 |
+
test:function(){ return MATCH.sawScout; } },
|
| 134 |
|
| 135 |
+
{ id:'tech', icon:'⌬',
|
| 136 |
+
say:'Field rank 3 granted for training. Build BUILD → TECH → Research Complex; it unlocks studies and banks account-level ◆ Data after battle.',
|
| 137 |
+
done:'Research Complex online. Its shield buffer protects studies from a sudden rush.',
|
| 138 |
+
test:function(){ return hasPlayerBld('techlab'); } },
|
| 139 |
+
|
| 140 |
+
{ id:'ability', icon:'✦',
|
| 141 |
+
say:'Select the Commander, open POWERS and use BLAST on the marked safe target. Abilities cost energy and have cooldowns.',
|
| 142 |
+
done:'Special ability confirmed. Research and Commander levels unlock additional powers.',
|
| 143 |
+
test:function(){ return MATCH.usedAbility; } },
|
| 144 |
+
|
| 145 |
+
{ id:'objective', icon:'◎', manual:'objectiveAck', action:'CALL EXTRACTION',
|
| 146 |
+
say:'Normal operations end by their selected objective: destroy Commanders, hold territory, purge hives or survive. Training is secure—tap CALL EXTRACTION.',
|
| 147 |
+
done:'Extraction acknowledged. Match resources, rewards and research are summarized after every operation.',
|
| 148 |
+
test:function(){ return MATCH.objectiveAck; } },
|
| 149 |
+
|
| 150 |
+
{ id:'cloud', icon:'☁', manual:'cloudAck', action:'FINISH TRAINING',
|
| 151 |
+
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.',
|
| 152 |
+
done:'Career storage confirmed.',
|
| 153 |
+
test:function(){ return MATCH.cloudAck; } }
|
| 154 |
];
|
| 155 |
|
| 156 |
var BASE_ATTACK_LINES=[
|
|
|
|
| 180 |
/* Facts about THIS match, tracked whether or not the tutorial is running, so
|
| 181 |
a mid-match "replay" (from Settings) can credit things the player already
|
| 182 |
demonstrably knows how to do instead of waiting for them to happen again. */
|
| 183 |
+
var MATCH={ sawSel:false, sawCombatSel:false, sawMove:false, sawQueue:false, sawCamera:false,
|
| 184 |
+
sawPickup:false, sawPlatoon:false, sawFormation:false, sawAttackMove:false,sawScout:false,
|
| 185 |
+
territoryAck:false,objectiveAck:false,cloudAck:false,usedAbility:false,
|
| 186 |
+
startCombat:0,cameraBase:null,pickup:null,scoutIdx:-1,scoutStart:null,lastAbCool:[0,0,0,0] };
|
| 187 |
/* Reactive one-per-match latches. */
|
| 188 |
var REACT={ baseAttack:false, lowPower:false, hazard:false, unitLost:false, wave:false,
|
| 189 |
lastAlarmT:0, lastStallE:false, lastHazWarn:false, lastWarned:false };
|
|
|
|
| 194 |
META.tutorial=META.tutorial||{done:false,skipped:false,version:0};
|
| 195 |
/* `progress` is additive save metadata: old profiles load exactly as before,
|
| 196 |
while the briefing can distinguish new, interrupted and completed runs.
|
| 197 |
+
It is a high-water mark only; the live-state gates below remain the
|
| 198 |
authority whenever a mission is running. */
|
| 199 |
if(typeof META.tutorial.progress!=='number') META.tutorial.progress=META.tutorial.done?STEPS.length:0;
|
| 200 |
META.tutorial.progress=Math.max(0,Math.min(STEPS.length,META.tutorial.progress|0));
|
|
|
|
| 225 |
for(var i=0;i<unitHigh;i++) if(isCombatUnit(i)) return true;
|
| 226 |
return false;
|
| 227 |
}
|
| 228 |
+
function combatCount(){
|
| 229 |
+
if(typeof unitHigh!=='number') return 0;
|
| 230 |
+
var n=0;for(var i=0;i<unitHigh;i++)if(isCombatUnit(i))n++;return n;
|
| 231 |
+
}
|
| 232 |
function hasSelectedCombatUnit(){
|
| 233 |
if(typeof unitHigh!=='number') return false;
|
| 234 |
for(var i=0;i<unitHigh;i++) if(isCombatUnit(i)&&usel[i]) return true;
|
|
|
|
| 271 |
'<div id="keelBody">'+
|
| 272 |
'<div id="keelHd"><span id="keelName" title="'+KEEL_TAG+'">'+KEEL_NAME+'</span><span id="keelStepTag"></span></div>'+
|
| 273 |
'<div id="keelTxt"></div>'+
|
| 274 |
+
'<div id="keelProgress" role="progressbar" aria-label="Training objective progress" aria-valuemin="0"><i></i></div>'+
|
| 275 |
'<button id="keelNext" type="button">GOT IT</button>'+
|
| 276 |
'</div>'+
|
| 277 |
'<button id="keelSkip" type="button" aria-label="Skip tutorial">SKIP</button>'+
|
|
|
|
| 285 |
var nextBtn=document.getElementById('keelNext');
|
| 286 |
nextBtn.addEventListener('pointerdown',function(ev){
|
| 287 |
ev.preventDefault(); ev.stopPropagation();
|
| 288 |
+
var S=TUT.active&&STEPS[TUT.stepIdx];
|
| 289 |
+
if(!S||!S.manual) return;
|
| 290 |
+
MATCH[S.manual]=true;
|
| 291 |
if(typeof sfx==='function'){ try{ sfx('ui'); }catch(e){} }
|
| 292 |
});
|
| 293 |
/* Tapping the body re-shows the full-strength card if it has settled into
|
|
|
|
| 302 |
if(!domReady) return;
|
| 303 |
if(text!==lastText){ var t=document.getElementById('keelTxt'); if(t) t.textContent=text; lastText=text; }
|
| 304 |
var tg=document.getElementById('keelStepTag'); if(tg) tg.textContent=tag||'';
|
| 305 |
+
var pg=document.getElementById('keelProgress');
|
| 306 |
+
if(pg){
|
| 307 |
+
var at=Math.min(STEPS.length,TUT.stepIdx),pct=STEPS.length?at/STEPS.length*100:0;
|
| 308 |
+
pg.setAttribute('aria-valuemax',STEPS.length);pg.setAttribute('aria-valuenow',at);
|
| 309 |
+
var fill=pg.querySelector('i');if(fill)fill.style.width=pct+'%';
|
| 310 |
+
}
|
| 311 |
}
|
| 312 |
function showWrap(fresh){
|
| 313 |
if(!domReady) return;
|
|
|
|
| 395 |
clearFocus();
|
| 396 |
if(!TUT.active||TUT.stepIdx>=STEPS.length) return;
|
| 397 |
var S=STEPS[TUT.stepIdx],next=document.getElementById('keelNext');
|
| 398 |
+
if(next){
|
| 399 |
+
next.style.display=S.manual?'inline-flex':'none';next.textContent=S.action||'GOT IT';
|
| 400 |
+
next.setAttribute('aria-label',(S.action||'Confirm')+' — '+S.id+' lesson');
|
| 401 |
+
}
|
| 402 |
+
if(S.id==='camera'){
|
| 403 |
+
focusEl(document.querySelector('.hudDeckBtn[data-deck="view"]'));
|
| 404 |
+
focusEl(document.getElementById('rotL'));focusEl(document.getElementById('tiltBtn'));focusEl(document.getElementById('rotR'));
|
| 405 |
+
cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.52,'DRAG / PINCH');
|
| 406 |
+
} else if(S.id==='deploy'){
|
| 407 |
var dep=document.getElementById('deployBtn');
|
| 408 |
if(shown(dep)) focusEl(dep); else cueAt((typeof VW==='number'?VW:innerWidth)*.5,(typeof VH==='number'?VH:innerHeight)*.52,'TAP LANDING SITE');
|
| 409 |
} else if(S.id==='commander'){
|
|
|
|
| 411 |
var cdr=document.getElementById('abHero');
|
| 412 |
if(shown(cdr)) focusEl(cdr);
|
| 413 |
else focusEl(document.querySelector('.hudDeckBtn[data-deck="powers"]'));
|
| 414 |
+
} else if(S.id==='pickup'){
|
| 415 |
+
if(MATCH.pickup&&crates.indexOf(MATCH.pickup)>=0)cueWorld(MATCH.pickup.x,MATCH.pickup.y,'◆ COLLECT CACHE');
|
| 416 |
+
focusEl(document.getElementById('heroBar'));
|
| 417 |
} else if(S.id==='mex') buildFocus('ECONOMY','Extractor','mex');
|
| 418 |
else if(S.id==='power') buildFocus('ECONOMY','Reactor','pgen');
|
| 419 |
else if(S.id==='fac') buildFocus('PRODUCTION','Factory','fac');
|
|
|
|
| 425 |
var pm=document.getElementById('prodMenu');
|
| 426 |
if(shown(pm)) focusEl(byText('prodGrid','.bcard','Striker')||pm.querySelector('.bcard'));
|
| 427 |
else { var F=playerBld('fac'); if(F) cueWorld(F.x,F.y,'TAP FACTORY'); }
|
| 428 |
+
} else if(S.id==='train'){
|
|
|
|
| 429 |
if(hasCombatUnit()) focusEl(document.getElementById('armyBtn'));
|
| 430 |
else { var F2=playerBld('fac'); if(F2) cueWorld(F2.x,F2.y,'UNIT BUILDING'); }
|
| 431 |
+
} else if(S.id==='turret') buildFocus('DEFENCE','Sentinel','turret');
|
| 432 |
+
else if(S.id==='platoon'){
|
| 433 |
+
if(shown(document.getElementById('grpRow')))focusEl(document.getElementById('grpBtn1'));
|
| 434 |
+
else focusEl(document.querySelector('.hudDeckBtn[data-deck="platoons"]'));
|
| 435 |
+
} else if(S.id==='formation'){
|
| 436 |
+
if(shown(document.getElementById('tacRow'))){
|
| 437 |
+
focusEl(document.getElementById('formBtn'));
|
| 438 |
+
if(orderPreview)cueWorld(orderPreview.x,orderPreview.y,'DRAG FORMATION');
|
| 439 |
+
} else focusEl(document.querySelector('.hudDeckBtn[data-deck="orders"]'));
|
| 440 |
+
} else if(S.id==='attack'){
|
| 441 |
+
if(shown(document.getElementById('tacRow')))focusEl(document.getElementById('moveBtn'));
|
| 442 |
+
else focusEl(document.querySelector('.hudDeckBtn[data-deck="orders"]'));
|
| 443 |
var sx=0,sy=0,n=0;
|
| 444 |
if(typeof unitHigh==='number') for(var j=0;j<unitHigh;j++) if(isCombatUnit(j)&&usel[j]){sx+=ux[j];sy+=uy[j];n++;}
|
| 445 |
+
if(n) cueWorld(sx/n+150,sy/n-110,'ATTACK-MOVE');
|
| 446 |
else focusEl(document.getElementById('armyBtn'));
|
| 447 |
+
} else if(S.id==='fog'){
|
| 448 |
+
focusEl(document.getElementById('minimapWrap'));
|
| 449 |
+
if(MATCH.scoutIdx>=0&&ualive[MATCH.scoutIdx])cueWorld(ux[MATCH.scoutIdx]+190,uy[MATCH.scoutIdx]-150,'SCOUT THIS SECTOR');
|
| 450 |
+
} else if(S.id==='tech') buildFocus('TECH','Research Complex','techlab');
|
| 451 |
+
else if(S.id==='ability'){
|
| 452 |
+
var blast=document.getElementById('abOver');
|
| 453 |
+
if(shown(blast))focusEl(blast);else focusEl(document.querySelector('.hudDeckBtn[data-deck="powers"]'));
|
| 454 |
+
if(typeof heroIdx==='number'&&heroIdx>=0&&ualive[heroIdx])cueWorld(ux[heroIdx]+105,uy[heroIdx]-70,'BLAST TARGET');
|
| 455 |
+
} else if(S.id==='objective'||S.id==='cloud'){
|
| 456 |
+
focusEl(next);
|
| 457 |
}
|
| 458 |
}
|
| 459 |
/* Capped so a burst of state changes (several buildings finishing within the
|
|
|
|
| 514 |
--------------------------------------------------------------------------- */
|
| 515 |
function trackMatchSignals(){
|
| 516 |
if(sawQueuedUnit()) MATCH.sawQueue=true;
|
| 517 |
+
if(MATCH.cameraBase){
|
| 518 |
+
var C=MATCH.cameraBase;
|
| 519 |
+
if((typeof camUserT==='number'&&camUserT>0)||Math.abs(yawTarget-C.yaw)>.06||
|
| 520 |
+
Math.abs(pitchTarget-C.pitch)>.035||Math.abs(orthoSpan-C.span)>24)MATCH.sawCamera=true;
|
| 521 |
+
}
|
| 522 |
if(typeof unitHigh==='number' && (!MATCH.sawSel||!MATCH.sawCombatSel)){
|
| 523 |
for(var i=0;i<unitHigh;i++){
|
| 524 |
if(!ualive[i]||uteam[i]!==0) continue;
|
|
|
|
| 527 |
if(MATCH.sawSel&&MATCH.sawCombatSel) break;
|
| 528 |
}
|
| 529 |
}
|
| 530 |
+
if(typeof activePlatoon==='number'&&activePlatoon>=0&&ctrlGroups[activePlatoon]&&groupLive(ctrlGroups[activePlatoon]).length)
|
| 531 |
+
MATCH.sawPlatoon=true;
|
| 532 |
+
if(typeof orderConfirm!=='undefined'&&orderConfirm)MATCH.sawFormation=true;
|
| 533 |
+
if(MATCH.scoutIdx>=0&&MATCH.scoutStart&&ualive[MATCH.scoutIdx]&&
|
| 534 |
+
Math.hypot(ux[MATCH.scoutIdx]-MATCH.scoutStart.x,uy[MATCH.scoutIdx]-MATCH.scoutStart.y)>115)MATCH.sawScout=true;
|
| 535 |
if(typeof abCool!=='undefined'){
|
| 536 |
for(var k=0;k<abCool.length;k++){
|
| 537 |
if(abCool[k]>(MATCH.lastAbCool[k]||0)+0.01) MATCH.usedAbility=true;
|
|
|
|
| 550 |
TUT.stepIdx=0;
|
| 551 |
TUT.shownStepIdx=-1;
|
| 552 |
TUT.doneFlags=STEPS.map(function(){ return false; });
|
| 553 |
+
MATCH.sawSel=false;MATCH.sawCombatSel=false;MATCH.sawMove=false;MATCH.sawQueue=false;MATCH.sawCamera=false;
|
| 554 |
+
MATCH.sawPickup=false;MATCH.sawPlatoon=false;MATCH.sawFormation=false;MATCH.sawAttackMove=false;MATCH.sawScout=false;
|
| 555 |
+
MATCH.territoryAck=false;MATCH.objectiveAck=false;MATCH.cloudAck=false;MATCH.usedAbility=false;
|
| 556 |
+
MATCH.startCombat=combatCount();MATCH.pickup=null;MATCH.scoutIdx=-1;MATCH.scoutStart=null;
|
| 557 |
+
MATCH.cameraBase={yaw:yawTarget,pitch:pitchTarget,span:orthoSpan};
|
| 558 |
+
MATCH.lastAbCool=(typeof abCool!=='undefined')?abCool.slice():[0,0,0,0];
|
| 559 |
queue.length=0;
|
| 560 |
speak(GREETING,5.5,'greeting');
|
| 561 |
}
|
|
|
|
| 591 |
}
|
| 592 |
var cur=STEPS[TUT.stepIdx];
|
| 593 |
restingText=cur.say;
|
| 594 |
+
restingTag=(cur.icon||'◇')+' STEP '+(TUT.stepIdx+1)+' / '+STEPS.length;
|
| 595 |
if(TUT.shownStepIdx!==TUT.stepIdx){
|
| 596 |
TUT.shownStepIdx=TUT.stepIdx;
|
| 597 |
speak(cur.say,5.5,'step');
|
|
|
|
| 667 |
REACT.baseAttack=false; REACT.lowPower=false; REACT.hazard=false; REACT.unitLost=false; REACT.wave=false;
|
| 668 |
REACT.lastAlarmT=(typeof alarmT!=='undefined')?alarmT:0;
|
| 669 |
REACT.lastStallE=false; REACT.lastHazWarn=false; REACT.lastWarned=false;
|
| 670 |
+
MATCH.sawSel=false;MATCH.sawCombatSel=false;MATCH.sawMove=false;MATCH.sawQueue=false;MATCH.sawCamera=false;
|
| 671 |
+
MATCH.sawPickup=false;MATCH.sawPlatoon=false;MATCH.sawFormation=false;MATCH.sawAttackMove=false;MATCH.sawScout=false;
|
| 672 |
+
MATCH.territoryAck=false;MATCH.objectiveAck=false;MATCH.cloudAck=false;MATCH.usedAbility=false;
|
| 673 |
+
MATCH.startCombat=combatCount();MATCH.pickup=null;MATCH.scoutIdx=-1;MATCH.scoutStart=null;
|
| 674 |
+
MATCH.cameraBase={yaw:yawTarget,pitch:pitchTarget,span:orthoSpan};
|
| 675 |
MATCH.lastAbCool=(typeof abCool!=='undefined')?abCool.slice():[0,0,0,0];
|
| 676 |
if(TUT.trainingMode){ TUT.forceNext=false; beginRun(); }
|
| 677 |
}
|
|
|
|
| 725 |
META.opmods=trainingPrev.opmods; META.threatSel=trainingPrev.threatSel;
|
| 726 |
trainingPrev=null;
|
| 727 |
}
|
| 728 |
+
function trainingStepId(){
|
| 729 |
+
return TUT.active&&STEPS[TUT.stepIdx]?STEPS[TUT.stepIdx].id:'';
|
| 730 |
+
}
|
| 731 |
+
function ensureTrainingPickup(){
|
| 732 |
+
if(MATCH.sawPickup||(MATCH.pickup&&crates.indexOf(MATCH.pickup)>=0))return;
|
| 733 |
+
var H=playerBld('hq');if(!H||typeof spawnCrate!=='function')return;
|
| 734 |
+
var C=spawnCrate(H.x+155,H.y-55,'supply');
|
| 735 |
+
C.alt=0;C.seen=true;C.t=0;C.site=true;C.siteName='TRAINING CACHE';C.announced=true;
|
| 736 |
+
MATCH.pickup=C;
|
| 737 |
+
if(typeof mmPing==='function')mmPing(C.x,C.y);
|
| 738 |
+
}
|
| 739 |
+
function ensureTrainingScout(){
|
| 740 |
+
if(MATCH.scoutIdx>=0&&ualive[MATCH.scoutIdx])return;
|
| 741 |
+
var H=playerBld('hq'),ty=-1;if(!H)return;
|
| 742 |
+
for(var i=0;i<TYPES.length;i++)if(TYPES[i]&&TYPES[i].scout){ty=i;break;}
|
| 743 |
+
if(ty<0)return;
|
| 744 |
+
var u=spawnUnit(ty,0,H.x+95,H.y+55);if(u<0)return;
|
| 745 |
+
clearSel();usel[u]=1;MATCH.scoutIdx=u;MATCH.scoutStart={x:ux[u],y:uy[u]};
|
| 746 |
+
updateSelInfo();cam.x=ux[u];cam.y=uy[u];clampCam();camUpdateMatrices();
|
| 747 |
+
toast('⌾ TRAINING SCOUT DEPLOYED — move it into black fog');
|
| 748 |
+
}
|
| 749 |
function trainingSafety(){
|
| 750 |
if(!TUT.trainingMode||typeof running==='undefined'||!running||typeof matchLive==='undefined'||!matchLive) return;
|
| 751 |
/* No scripted ambush while the player is reading. The enemy still builds a
|
|
|
|
| 759 |
if(typeof resE!=='undefined'&&typeof RES_ECAP!=='undefined') resE[0]=Math.min(RES_ECAP[0],Math.max(resE[0],2200));
|
| 760 |
if(typeof heroIdx==='number'&&heroIdx>=0&&ualive[heroIdx]) uhp[heroIdx]=Math.max(uhp[heroIdx],uhpm[heroIdx]*.72);
|
| 761 |
var H=playerBld('hq'); if(H) H.hp=Math.max(H.hp,H.hpm*.75);
|
| 762 |
+
var step=trainingStepId();
|
| 763 |
+
if(step==='pickup')ensureTrainingPickup();
|
| 764 |
+
if(step==='fog')ensureTrainingScout();
|
| 765 |
+
if(step==='tech'&&typeof heroLvl==='number')heroLvl=Math.max(heroLvl,3);
|
| 766 |
}
|
| 767 |
function startTrainingMission(){
|
| 768 |
if(typeof running!=='undefined'&&running){ toast('Leave the current battle before starting training'); return; }
|
|
|
|
| 783 |
if(typeof hideFrontScreens==='function') hideFrontScreens();
|
| 784 |
var load=document.getElementById('loadScr'); if(load) load.style.display='flex';
|
| 785 |
requestAnimationFrame(function(){requestAnimationFrame(function(){
|
| 786 |
+
applyTheme(); newSkirmish();fogOn=true;updateFog();trainingLaunched=true;
|
| 787 |
if(load) load.style.display='none';
|
| 788 |
toast('TRAINING OPERATION — threat paused while KEEL guides you');
|
| 789 |
});});
|
|
|
|
| 862 |
function ensureTrainingEntry(){
|
| 863 |
var start=document.getElementById('startBtn'); if(!start||document.getElementById('keelTrainingCallout')) return;
|
| 864 |
var d=document.createElement('div'); d.id='keelTrainingCallout';
|
| 865 |
+
d.innerHTML='<div class="ktMiniIdentity"><img src="./assets/factions/nova_icon_96.png" alt="Terran Frontline Command">'
|
| 866 |
+'<div><span class="ktState">FIRST OPERATION</span><b>FIELD ORIENTATION</b><small class="ktProgress">0 / '+STEPS.length+' OBJECTIVES</small></div></div>'
|
| 867 |
+'<button type="button">VIEW TRAINING</button>';
|
| 868 |
start.parentNode.insertBefore(d,start);
|
|
|
|
| 885 |
+'<span>'+(typeof facIcon==='function'?facIcon('nova',38,'ktoCrestImg'):'✦')+'</span></div>'
|
| 886 |
+'<div class="ktoIdentity"><small>TRAINING OPERATION 01</small><b>FIELD ORIENTATION</b>'
|
| 887 |
+'<i>'+commander+' · KEEL tactical guidance</i>'
|
| 888 |
+
+'<p>A protected live-fire drop covering the complete command loop without an early enemy rush.</p></div></div>'
|
| 889 |
+
+'<div class="ktoTeach" aria-label="Training topics"><span>◇ CAMERA</span><span>⬡ ECONOMY</span><span>▣ PRODUCTION</span>'
|
| 890 |
+
+'<span>⛨ DEFENCE</span><span>Ⅳ PLATOONS</span><span>⌾ SCOUTING</span>'
|
| 891 |
+
+'<span>⌬ TECH</span><span>✦ POWERS</span><span>☁ SAVES</span></div>'
|
| 892 |
+'<div class="ktoProgress"><div><span>OBJECTIVE PROGRESS</span><b>'+S.progress+' / '+STEPS.length+'</b></div>'
|
| 893 |
+'<div class="ktoProgressTrack"><i style="width:'+progressPct+'%"></i></div>'
|
| 894 |
+'<small>'+(S.done?'Orientation certified · replay does not repeat the first-clear reward':
|
| 895 |
S.active?'Current lesson: '+(STEPS[Math.min(TUT.stepIdx,STEPS.length-1)].id||'field systems').toUpperCase():
|
| 896 |
+
S.interrupted?'Previous best retained · a new drop restarts at camera control':STEPS.length+' guided objectives · progress is saved to this profile')+'</small></div>'
|
| 897 |
+'<div class="ktoFacts"><div><b>'+STEPS.length+'</b><span>OBJECTIVES</span></div><div><b>EASY</b><span>FIXED OPPONENT</span></div>'
|
| 898 |
+'<div><b>SAFE</b><span>NO INFESTATION</span></div></div>'
|
| 899 |
+'<button class="ktoAction" type="button">'+S.action+'</button>';
|
|
|
|
| 991 |
window.__keelMoveWrapped=true;
|
| 992 |
var _keelOrderMove=orderMove;
|
| 993 |
orderMove=function(wx,wy,patrol){
|
| 994 |
+
var combat=hasSelectedCombatUnit(),formationDone=MATCH.sawFormation,attackMode=typeof moveMode==='undefined'||!moveMode,
|
| 995 |
+
ok=_keelOrderMove(wx,wy,patrol);
|
| 996 |
+
if(ok&&combat&&!patrol){
|
| 997 |
+
MATCH.sawMove=true;
|
| 998 |
+
/* The formation release itself also calls orderMove. Requiring a
|
| 999 |
+
formation observed on an earlier poll ensures the next deliberate
|
| 1000 |
+
map tap teaches attack-move instead of silently auto-completing. */
|
| 1001 |
+
if(formationDone&&attackMode)MATCH.sawAttackMove=true;
|
| 1002 |
+
}
|
| 1003 |
return ok;
|
| 1004 |
};
|
| 1005 |
}
|
| 1006 |
+
if(typeof applyCrate==='function'&&!window.__keelPickupWrapped){
|
| 1007 |
+
window.__keelPickupWrapped=true;
|
| 1008 |
+
var _keelApplyCrate=applyCrate;
|
| 1009 |
+
applyCrate=function(k,x,y){
|
| 1010 |
+
var out=_keelApplyCrate(k,x,y);
|
| 1011 |
+
if(TUT.trainingMode)MATCH.sawPickup=true;
|
| 1012 |
+
return out;
|
| 1013 |
+
};
|
| 1014 |
+
}
|
| 1015 |
setInterval(tutTick,350);
|
| 1016 |
}
|
| 1017 |
window.initTutorial=initTutorial;
|
src/ui/hud.js
CHANGED
|
@@ -259,9 +259,23 @@ function renderLegacySprites(dtDraw){
|
|
| 259 |
for(const r of rocks){ if(r.x<x0||r.x>x1||r.y<y0||r.y>y1) continue;
|
| 260 |
batN.add(sRock,r.x,r.y,r.s,r.s,r.a,255,255,255,255); }
|
| 261 |
for(const d of deposits){
|
| 262 |
-
if(d.
|
| 263 |
-
|
| 264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
}
|
| 266 |
for(const gz of geysers){
|
| 267 |
if(gz.x<x0||gz.x>x1||gz.y<y0||gz.y>y1) continue;
|
|
@@ -270,7 +284,13 @@ function renderLegacySprites(dtDraw){
|
|
| 270 |
addParticle(1,gz.x+rr(-5,5),gz.y+rr(-4,4),rr(-2,2),rr(-17,-10),1.3,9, 165,225,250);
|
| 271 |
}
|
| 272 |
for(const cst of crystals){ if(cst.x<x0||cst.x>x1||cst.y<y0||cst.y>y1) continue;
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
// ---- contact shadows for standing relief (drawn flat, on the ground) ----
|
| 275 |
for(const M of relief){
|
| 276 |
if(M.x<x0-60||M.x>x1+60||M.y<y0-60||M.y>y1+60) continue;
|
|
@@ -597,6 +617,18 @@ function renderLegacySprites(dtDraw){
|
|
| 597 |
const tc=TEAMC[uteam[i]];
|
| 598 |
if(usel[i]) batA.add(selRing,X,Y,T.size*1.6,T.size*1.6,0,60,220,140,170);
|
| 599 |
if(ubuff[i]>0) batA.add(glow,X,Y,T.size*1.9,T.size*1.9,0,255,210,60,40);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 600 |
// organic sway for walkers & beasts
|
| 601 |
let bodyRot=uang[i];
|
| 602 |
if(T.wk==='m'||utype[i]===0||utype[i]===8||utype[i]===11) bodyRot+=Math.sin(t*8+i*1.7)*0.045;
|
|
@@ -676,6 +708,11 @@ function renderLegacySprites(dtDraw){
|
|
| 676 |
const ha=(T.tur?uturr[i]:uang[i])-Math.PI/2;
|
| 677 |
batA.add(glow,X+Math.cos(ha)*T.size*0.8,Y+Math.sin(ha)*T.size*0.8,
|
| 678 |
T.size*1.3,T.size*1.3,0, 255,190,110, 54*dep);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
}
|
| 680 |
}
|
| 681 |
}
|
|
@@ -684,6 +721,10 @@ function renderLegacySprites(dtDraw){
|
|
| 684 |
const i=airTmp[k], T=TYPES[utype[i]], tc=TEAMC[uteam[i]];
|
| 685 |
const bob=Math.sin(t*3+i)*1.5;
|
| 686 |
if(usel[i]) batA.add(selRing,ux[i],uy[i],T.size*1.6,T.size*1.6,0,60,220,140,170);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 687 |
if(use3D&&UNIT_UV[T.spr]){
|
| 688 |
batU.add(UNIT_UV[T.spr][angFrame(uang[i]+camYaw)],ux[i],uy[i]-7+bob,T.size*S3D,T.size*S3D,0,tc[0],tc[1],tc[2],255,52);
|
| 689 |
} else {
|
|
@@ -1239,8 +1280,10 @@ function renderMinimap(){
|
|
| 1239 |
mm.drawImage(mmBg,0,0);
|
| 1240 |
mm.fillStyle='#3dd68a';
|
| 1241 |
for(const d of deposits){
|
| 1242 |
-
|
| 1243 |
-
|
|
|
|
|
|
|
| 1244 |
}
|
| 1245 |
for(const B of blds){
|
| 1246 |
if(!B.alive) continue;
|
|
@@ -1384,7 +1427,7 @@ function updateSelInfo(){
|
|
| 1384 |
if(mr){
|
| 1385 |
mr.style.display=deck==='platoons'&&modeable?'flex':'none';
|
| 1386 |
if(modeable){
|
| 1387 |
-
const M=
|
| 1388 |
$('modeEm').textContent=mixed?'⁇':M.em;
|
| 1389 |
$('modeNm').textContent=mixed?'Mixed':M.nm;
|
| 1390 |
}
|
|
@@ -1426,10 +1469,10 @@ function cycleSelectedModes(){
|
|
| 1426 |
const cur=list.indexOf(umode[i]);
|
| 1427 |
decided[ty]=list[(cur+1)%list.length];
|
| 1428 |
}
|
| 1429 |
-
if(setMode(i,decided[ty])){ changed++; label=
|
| 1430 |
}
|
| 1431 |
if(changed){
|
| 1432 |
-
const
|
| 1433 |
toast(M.em+' '+changed+' units → '+M.nm+' — '+M.ds);
|
| 1434 |
sfx('ui');
|
| 1435 |
} else toast('Selected units have no alternate stance');
|
|
@@ -1471,6 +1514,7 @@ function updateHUD(fps){
|
|
| 1471 |
}
|
| 1472 |
if(aiming===0) $('abOver').classList.add('on'); else $('abOver').classList.remove('on');
|
| 1473 |
if(typeof artBarrageButtonState==='function') artBarrageButtonState();
|
|
|
|
| 1474 |
// carrier deployment prompt
|
| 1475 |
const db=$('deployBtn');
|
| 1476 |
if(db){
|
|
@@ -1567,6 +1611,32 @@ const INTEL_BUILD_COPY={
|
|
| 1567 |
tech:'Unlocks and strengthens advanced systems', sup:'Extends and sustains the base',
|
| 1568 |
sup2:'Strategic weapon for decisive strikes'
|
| 1569 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1570 |
const INTEL_BLD_WEAPONS={
|
| 1571 |
turret:{wk:'b',rng:()=>TURRET_RNG,tg:'a'}, bunker:{wk:'e',rng:()=>BUNKER.rng,tg:'g'},
|
| 1572 |
aatower:{wk:'e',rng:()=>AA.rng,tg:'air'}, bastion:{wk:'e',rng:()=>BASTION.rng,min:()=>BASTION.minRng,tg:'g'},
|
|
@@ -1586,6 +1656,8 @@ function intelRangeBand(r){
|
|
| 1586 |
return '—';
|
| 1587 |
}
|
| 1588 |
function intelUnitPurpose(T){
|
|
|
|
|
|
|
| 1589 |
if(T.builder) return 'Mobile engineer that establishes forward bases and repairs structures.';
|
| 1590 |
if(T.name==='Bulwark') return 'Mobile shield projector that reduces damage to nearby allied units.';
|
| 1591 |
if(T.scout) return 'High-speed reconnaissance aircraft for finding threats and flanking exposed targets.';
|
|
@@ -1809,6 +1881,33 @@ function mfIntelPreviewSet(host,kind,id){
|
|
| 1809 |
if(V&&V.setSubject(kind,id)) return;
|
| 1810 |
if(host){host.innerHTML='';host.appendChild(mfIntelPreviewWindow(kind,id,'LIVE 3D'));}
|
| 1811 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1812 |
let mfIntelMenuTimer=0;
|
| 1813 |
function mfIntelMenuInit(tryN){
|
| 1814 |
const host=$('menuIntelModel');if(!host||host.dataset.ready) return;
|
|
@@ -1849,6 +1948,16 @@ function showUnitTypeCard(tIdx,pinned){
|
|
| 1849 |
?'☄ ACTIVE: CHARGED BARRAGE · select artillery, open POWERS, then target the map'
|
| 1850 |
:'🔒 CHARGED BARRAGE · research Fire Mission Protocol in Development > Doctrine';
|
| 1851 |
card.appendChild(info);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1852 |
}
|
| 1853 |
mfIntelAttachPreview('unit',tIdx);
|
| 1854 |
}
|
|
@@ -2046,7 +2155,7 @@ function unitIconEl(tIdx,size){
|
|
| 2046 |
13:'unit_alpha',18:'unit_scorcher',19:'unit_constructor',
|
| 2047 |
20:'unit_reaper',21:'unit_cinder',22:'unit_lancer',23:'unit_resonator',
|
| 2048 |
24:'unit_warden',25:'unit_kestrel',26:'unit_basilisk',27:'unit_harbinger',
|
| 2049 |
-
28:'unit_praetor',29:'unit_archon',30:'unit_brood'
|
| 2050 |
}[tIdx];
|
| 2051 |
if(special&&typeof itemArt==='function'){
|
| 2052 |
w.innerHTML=itemArt(special,T.name,size);
|
|
@@ -2098,8 +2207,8 @@ function renderProdMenu(){
|
|
| 2098 |
if(B.type==='tgate') list=[8,26];
|
| 2099 |
else if(B.type==='harbor') list=[14,15];
|
| 2100 |
else if(B.type==='airfield') list=[5,17,25];
|
| 2101 |
-
else list = B.tier===2? [0,1,9,18,10,2,3,6,7,11,16,19,20,21,22,23,24,27]
|
| 2102 |
-
: [0,1,9,10,19,24];
|
| 2103 |
/* ROLE TABS. A flat grid of eighteen cards is a wall; one tap per role lets
|
| 2104 |
the player find the answer to whatever is killing them without reading
|
| 2105 |
every stat block on the way there. */
|
|
@@ -2118,6 +2227,7 @@ function renderProdMenu(){
|
|
| 2118 |
b.addEventListener('pointerdown',ev=>{ ev.stopPropagation(); prodTab=c; sfx('ui'); renderProdMenu(); });
|
| 2119 |
tr.appendChild(b);
|
| 2120 |
}
|
|
|
|
| 2121 |
(groups[prodTab]||[]).forEach(tIdx=>{
|
| 2122 |
const T=TYPES[tIdx];
|
| 2123 |
const d=document.createElement('div');
|
|
@@ -2134,6 +2244,9 @@ function renderProdMenu(){
|
|
| 2134 |
if(openBld<0) return;
|
| 2135 |
const Bb=blds[openBld];
|
| 2136 |
if(tIdx===8 && titanCount[0]+Bb.queue.filter(q=>q===8).length>=3){ toast('Max 3 TITANs'); return; }
|
|
|
|
|
|
|
|
|
|
| 2137 |
if(Bb.queue.length<30){ Bb.queue.push(tIdx); sfx('ui'); renderQueue(); }
|
| 2138 |
if(tIdx!==8){ // hold to queue a batch of 5
|
| 2139 |
const hold=setTimeout(()=>{
|
|
@@ -2204,6 +2317,7 @@ function renderBuildMenu(){
|
|
| 2204 |
b.addEventListener('pointerdown',ev=>{ ev.stopPropagation(); bldTab=c; sfx('ui'); renderBuildMenu(); });
|
| 2205 |
tr.appendChild(b);
|
| 2206 |
}
|
|
|
|
| 2207 |
for(const key of (grp[bldTab]||[])){
|
| 2208 |
const T=BT[key];
|
| 2209 |
const d=document.createElement('div');
|
|
|
|
| 259 |
for(const r of rocks){ if(r.x<x0||r.x>x1||r.y<y0||r.y>y1) continue;
|
| 260 |
batN.add(sRock,r.x,r.y,r.s,r.s,r.a,255,255,255,255); }
|
| 261 |
for(const d of deposits){
|
| 262 |
+
if(d.x<x0||d.x>x1||d.y<y0||d.y>y1) continue;
|
| 263 |
+
const tier=depositTier(d),frac=d.capacity?d.remaining/d.capacity:0;
|
| 264 |
+
const col=tier===3?[188,82,255]:tier===2?[58,225,150]:tier===1?[58,190,255]:[72,76,88];
|
| 265 |
+
/* Corrupted terrain is a world feature, not a UI pin. Layered fractured
|
| 266 |
+
decals, subsurface bloom and counter-rotating rings remain under an
|
| 267 |
+
Extractor and desaturate as the finite field collapses. */
|
| 268 |
+
const fieldR=72+(d.initialTier||1)*20;
|
| 269 |
+
batN.add(sCrater,d.x,d.y,fieldR*1.8,fieldR*1.32,(d.pulse||0)+.35,col[0]*.62|0,col[1]*.58|0,col[2]*.68|0,tier?72:42);
|
| 270 |
+
batN.add(sCrater,d.x+8,d.y-5,fieldR*1.35,fieldR*.95,-(d.pulse||0)-.7,col[0]*.86|0,col[1]*.78|0,col[2]*.92|0,tier?46:28);
|
| 271 |
+
batN.add(glow,d.x,d.y,fieldR*2.05,fieldR*2.05,0,col[0],col[1],col[2],tier?(42+24*frac)|0:8);
|
| 272 |
+
batN.add(selRing,d.x,d.y,fieldR*1.45,fieldR*1.45,t*.10+(d.pulse||0),col[0],col[1],col[2],tier?76:20);
|
| 273 |
+
if(tier>0&&!d.taken){
|
| 274 |
+
if(tier===3) batN.add(sprites.depR,d.x,d.y,39,39,0,col[0],col[1],col[2],255);
|
| 275 |
+
else batN.add(sDep,d.x,d.y,30+tier*3,30+tier*3,0,col[0],col[1],col[2],255);
|
| 276 |
+
}
|
| 277 |
+
if(tier>0&&perfScale>.45&&Math.random()<.018*tier)
|
| 278 |
+
addParticle(0,d.x+rr(-fieldR*.55,fieldR*.55),d.y+rr(-fieldR*.42,fieldR*.42),rr(-3,3),rr(-12,-4),.55,3.5,col[0],col[1],col[2]);
|
| 279 |
}
|
| 280 |
for(const gz of geysers){
|
| 281 |
if(gz.x<x0||gz.x>x1||gz.y<y0||gz.y>y1) continue;
|
|
|
|
| 284 |
addParticle(1,gz.x+rr(-5,5),gz.y+rr(-4,4),rr(-2,2),rr(-17,-10),1.3,9, 165,225,250);
|
| 285 |
}
|
| 286 |
for(const cst of crystals){ if(cst.x<x0||cst.x>x1||cst.y<y0||cst.y>y1) continue;
|
| 287 |
+
const D=deposits[cst.dep],tier=depositTier(D);if(!D||cst.band>tier)continue;
|
| 288 |
+
const bandFill=clamp((D.remaining-(tier-1)*DEPOSIT_BAND)/DEPOSIT_BAND,0,1);
|
| 289 |
+
const edge=cst.band===tier?(.42+.58*bandFill):1,pulse=.94+.08*Math.sin(t*1.45+(cst.phase||0));
|
| 290 |
+
const s=cst.s*edge*pulse*(cst.core?1.42:1.18),col=cst.band===3?[220,112,255]:cst.band===2?[78,255,170]:[78,225,255];
|
| 291 |
+
batN.add(glow,cst.x,cst.y,s*1.8,s*1.8,0,col[0],col[1],col[2],cst.core?78:55);
|
| 292 |
+
batN.add(sCry,cst.x,cst.y,s,s*1.22,cst.a+Math.sin(t*.24+(cst.phase||0))*.035,col[0],col[1],col[2],255);
|
| 293 |
+
}
|
| 294 |
// ---- contact shadows for standing relief (drawn flat, on the ground) ----
|
| 295 |
for(const M of relief){
|
| 296 |
if(M.x<x0-60||M.x>x1+60||M.y<y0-60||M.y>y1+60) continue;
|
|
|
|
| 617 |
const tc=TEAMC[uteam[i]];
|
| 618 |
if(usel[i]) batA.add(selRing,X,Y,T.size*1.6,T.size*1.6,0,60,220,140,170);
|
| 619 |
if(ubuff[i]>0) batA.add(glow,X,Y,T.size*1.9,T.size*1.9,0,255,210,60,40);
|
| 620 |
+
if(typeof uclassBuffT!=='undefined'&&uclassBuffT[i]>0){
|
| 621 |
+
const cc=uclassBuff[i]===1?[255,112,54]:uclassBuff[i]===2?[75,225,255]:[82,255,164];
|
| 622 |
+
batA.add(selRing,X,Y,T.size*2.05,T.size*2.05,t*(uclassBuff[i]===2?1.8:.65),cc[0],cc[1],cc[2],120);
|
| 623 |
+
batA.add(glow,X,Y,T.size*1.65,T.size*1.65,0,cc[0],cc[1],cc[2],34);
|
| 624 |
+
}
|
| 625 |
+
if(typeof ubroodLed!=='undefined'&&ubroodLed[i]>0){
|
| 626 |
+
batA.add(selRing,X,Y,T.size*1.65,T.size*1.65,t*.72+i*.17,176,92,255,T.caster?155:54);
|
| 627 |
+
if(T.caster){
|
| 628 |
+
batA.add(selRing,X,Y,T.size*2.75,T.size*2.75,-t*.38,116,255,105,128);
|
| 629 |
+
batA.add(glow,X,Y-T.size*.24,T.size*2.25,T.size*2.25,0,178,82,255,64+Math.sin(t*4+i)*24);
|
| 630 |
+
}
|
| 631 |
+
}
|
| 632 |
// organic sway for walkers & beasts
|
| 633 |
let bodyRot=uang[i];
|
| 634 |
if(T.wk==='m'||utype[i]===0||utype[i]===8||utype[i]===11) bodyRot+=Math.sin(t*8+i*1.7)*0.045;
|
|
|
|
| 708 |
const ha=(T.tur?uturr[i]:uang[i])-Math.PI/2;
|
| 709 |
batA.add(glow,X+Math.cos(ha)*T.size*0.8,Y+Math.sin(ha)*T.size*0.8,
|
| 710 |
T.size*1.3,T.size*1.3,0, 255,190,110, 54*dep);
|
| 711 |
+
} else if(mo===6){ // ASSIST: logistics lock
|
| 712 |
+
batA.add(selRing,X,Y,T.size*2.1,T.size*2.1,t*.7,72,225,255,120*dep);
|
| 713 |
+
} else if(mo===7){ // SURVEY: scanning wave
|
| 714 |
+
const scan=1+((t*.55+i*.13)%1)*1.8;
|
| 715 |
+
batA.add(selRing,X,Y,T.size*scan,T.size*scan,-t*.35,100,255,185,(125/scan)*dep);
|
| 716 |
}
|
| 717 |
}
|
| 718 |
}
|
|
|
|
| 721 |
const i=airTmp[k], T=TYPES[utype[i]], tc=TEAMC[uteam[i]];
|
| 722 |
const bob=Math.sin(t*3+i)*1.5;
|
| 723 |
if(usel[i]) batA.add(selRing,ux[i],uy[i],T.size*1.6,T.size*1.6,0,60,220,140,170);
|
| 724 |
+
if(typeof uclassBuffT!=='undefined'&&uclassBuffT[i]>0){
|
| 725 |
+
const cc=uclassBuff[i]===2?[75,225,255]:[255,112,54];
|
| 726 |
+
batA.add(selRing,ux[i],uy[i],T.size*2.25,T.size*2.25,t*1.8,cc[0],cc[1],cc[2],135);
|
| 727 |
+
}
|
| 728 |
if(use3D&&UNIT_UV[T.spr]){
|
| 729 |
batU.add(UNIT_UV[T.spr][angFrame(uang[i]+camYaw)],ux[i],uy[i]-7+bob,T.size*S3D,T.size*S3D,0,tc[0],tc[1],tc[2],255,52);
|
| 730 |
} else {
|
|
|
|
| 1280 |
mm.drawImage(mmBg,0,0);
|
| 1281 |
mm.fillStyle='#3dd68a';
|
| 1282 |
for(const d of deposits){
|
| 1283 |
+
const tier=depositTier(d);
|
| 1284 |
+
if(tier<=0||(fogOn&&!demoMode&&!fogExploredAt(d.x,d.y))) continue;
|
| 1285 |
+
mm.fillStyle=tier===3?'#d06bff':tier===2?'#55eea3':'#4edcff';
|
| 1286 |
+
const rs=tier+2;mm.fillRect(d.x*k-rs/2,d.y*k-rs/2,rs,rs);
|
| 1287 |
}
|
| 1288 |
for(const B of blds){
|
| 1289 |
if(!B.alive) continue;
|
|
|
|
| 1427 |
if(mr){
|
| 1428 |
mr.style.display=deck==='platoons'&&modeable?'flex':'none';
|
| 1429 |
if(modeable){
|
| 1430 |
+
const M=unitModeDef(utype[first],mixed?0:Math.max(0,curMode));
|
| 1431 |
$('modeEm').textContent=mixed?'⁇':M.em;
|
| 1432 |
$('modeNm').textContent=mixed?'Mixed':M.nm;
|
| 1433 |
}
|
|
|
|
| 1469 |
const cur=list.indexOf(umode[i]);
|
| 1470 |
decided[ty]=list[(cur+1)%list.length];
|
| 1471 |
}
|
| 1472 |
+
if(setMode(i,decided[ty])){ changed++; label=unitModeDef(ty,decided[ty]).nm; }
|
| 1473 |
}
|
| 1474 |
if(changed){
|
| 1475 |
+
const firstTy=+Object.keys(decided)[0],M=unitModeDef(firstTy,decided[firstTy]);
|
| 1476 |
toast(M.em+' '+changed+' units → '+M.nm+' — '+M.ds);
|
| 1477 |
sfx('ui');
|
| 1478 |
} else toast('Selected units have no alternate stance');
|
|
|
|
| 1514 |
}
|
| 1515 |
if(aiming===0) $('abOver').classList.add('on'); else $('abOver').classList.remove('on');
|
| 1516 |
if(typeof artBarrageButtonState==='function') artBarrageButtonState();
|
| 1517 |
+
if(typeof classAbilityButtonState==='function') classAbilityButtonState();
|
| 1518 |
// carrier deployment prompt
|
| 1519 |
const db=$('deployBtn');
|
| 1520 |
if(db){
|
|
|
|
| 1611 |
tech:'Unlocks and strengthens advanced systems', sup:'Extends and sustains the base',
|
| 1612 |
sup2:'Strategic weapon for decisive strikes'
|
| 1613 |
};
|
| 1614 |
+
/* A category tab must answer more than “which cards are hidden underneath
|
| 1615 |
+
it?”. These short tactical briefs make the taxonomy useful to a newcomer:
|
| 1616 |
+
what the role accomplishes, which formation supports it, and the mistake
|
| 1617 |
+
that normally gets it killed. They deliberately sit above the card grid so
|
| 1618 |
+
the player can understand the decision before spending resources. */
|
| 1619 |
+
const INTEL_ROLE_GUIDE={
|
| 1620 |
+
inf:{tag:'LINE TROOPS',form:'WEDGE / SPREAD',use:'Capture ground, screen expensive weapons, and pressure exposed economy.',avoid:'Do not mass into splash or flame.'},
|
| 1621 |
+
veh:{tag:'ARMOURED CORE',form:'BATTLE LINE',use:'Hold the centre and trade efficiently under sustained fire.',avoid:'Screen against gauss and dedicated anti-tank.'},
|
| 1622 |
+
at :{tag:'HEAVY HUNTERS',form:'LINE / ARC',use:'Delete plated targets with focused, deliberate volleys.',avoid:'Keep light swarms outside minimum range.'},
|
| 1623 |
+
aoe:{tag:'CROWD BREAKERS',form:'SPREAD',use:'Punish dense infantry and biological waves with splash damage.',avoid:'Protect them from long-range armour hunters.'},
|
| 1624 |
+
art:{tag:'SIEGE BATTERY',form:'ARC / LINE',use:'Break towers and formations from beyond normal weapon range.',avoid:'Scout first; artillery folds at close range.'},
|
| 1625 |
+
aa :{tag:'AIR DENIAL',form:'SPREAD / BOX',use:'Escort valuable columns and deny bomber approaches.',avoid:'Specialists lose value against ground-only armies.'},
|
| 1626 |
+
air:{tag:'RAPID RESPONSE',form:'WEDGE',use:'Scout, flank, and strike weak points without following ground paths.',avoid:'Never loiter inside layered anti-air.'},
|
| 1627 |
+
nav:{tag:'SEA CONTROL',form:'LINE',use:'Own coastlines and project heavy fire onto nearby land.',avoid:'Water-only movement makes positioning a commitment.'},
|
| 1628 |
+
sup:{tag:'FORCE MULTIPLIER',form:'BOX / COLUMN',use:'Repair, shield, build, and keep the fighting line operational.',avoid:'Unarmed support needs an escort.'},
|
| 1629 |
+
exp:{tag:'DECISIVE ASSET',form:'BOX',use:'Anchor a late-game push and force the enemy to answer one threat.',avoid:'High cost makes isolation and focus fire disastrous.'}
|
| 1630 |
+
};
|
| 1631 |
+
const INTEL_BUILD_GUIDE={
|
| 1632 |
+
eco :{tag:'RESOURCE GRID',use:'Secure income and storage before expanding production.',chain:'DEPOSIT → EXTRACTOR → STORAGE'},
|
| 1633 |
+
prod:{tag:'ARMY PIPELINE',use:'Set rally points, maintain queues, and diversify unit counters.',chain:'POWER → FACTORY → RALLY'},
|
| 1634 |
+
def :{tag:'DEFENCE LAYER',use:'Overlap ranges so one tower covers another tower’s weakness.',chain:'SCOUT → SCREEN → KILL ZONE'},
|
| 1635 |
+
wall:{tag:'PATH CONTROL',use:'Shape enemy movement without trapping your own reinforcements.',chain:'WALL → GATE → CROSS-FIRE'},
|
| 1636 |
+
tech:{tag:'ACCOUNT RESEARCH',use:'Protect the lab network; completed studies persist beyond the match.',chain:'LAB → STUDY → ACCOUNT DATA'},
|
| 1637 |
+
sup :{tag:'BATTLE NETWORK',use:'Extend territory, shielding, repairs, and targeting coverage.',chain:'UPLINK → COVERAGE → ADVANCE'},
|
| 1638 |
+
sup2:{tag:'STRATEGIC STRIKE',use:'Scout the target and force movement before committing the long cooldown.',chain:'VISION → LOCK → LAUNCH'}
|
| 1639 |
+
};
|
| 1640 |
const INTEL_BLD_WEAPONS={
|
| 1641 |
turret:{wk:'b',rng:()=>TURRET_RNG,tg:'a'}, bunker:{wk:'e',rng:()=>BUNKER.rng,tg:'g'},
|
| 1642 |
aatower:{wk:'e',rng:()=>AA.rng,tg:'air'}, bastion:{wk:'e',rng:()=>BASTION.rng,min:()=>BASTION.minRng,tg:'g'},
|
|
|
|
| 1656 |
return '—';
|
| 1657 |
}
|
| 1658 |
function intelUnitPurpose(T){
|
| 1659 |
+
if(T.miner) return 'Mobile phase-ore miner. Cycle MINE, ASSIST and SURVEY orders to gather or accelerate production.';
|
| 1660 |
+
if(T.caster) return 'Critical-mass Brood leader. Its aura turns nearby creatures into a faster coordinated tide.';
|
| 1661 |
if(T.builder) return 'Mobile engineer that establishes forward bases and repairs structures.';
|
| 1662 |
if(T.name==='Bulwark') return 'Mobile shield projector that reduces damage to nearby allied units.';
|
| 1663 |
if(T.scout) return 'High-speed reconnaissance aircraft for finding threats and flanking exposed targets.';
|
|
|
|
| 1881 |
if(V&&V.setSubject(kind,id)) return;
|
| 1882 |
if(host){host.innerHTML='';host.appendChild(mfIntelPreviewWindow(kind,id,'LIVE 3D'));}
|
| 1883 |
}
|
| 1884 |
+
function mfMenuRoleHost(menuId,gridId){
|
| 1885 |
+
const menu=$(menuId),grid=$(gridId);if(!menu||!grid)return null;
|
| 1886 |
+
let host=menu.querySelector('.menuRoleBrief');
|
| 1887 |
+
if(!host){host=document.createElement('section');host.className='menuRoleBrief';grid.insertAdjacentElement('beforebegin',host);}
|
| 1888 |
+
return host;
|
| 1889 |
+
}
|
| 1890 |
+
function renderMenuRoleBrief(kind,key,ids){
|
| 1891 |
+
const isUnit=kind==='unit',host=mfMenuRoleHost(isUnit?'prodMenu':'buildMenu',isUnit?'prodGrid':'buildGrid');
|
| 1892 |
+
if(!host||!ids||!ids.length)return;
|
| 1893 |
+
const C=isUnit?(UCAT[key]||UCAT.veh):(BCAT[key]||BCAT.sup);
|
| 1894 |
+
const G=isUnit?(INTEL_ROLE_GUIDE[key]||INTEL_ROLE_GUIDE.veh):(INTEL_BUILD_GUIDE[key]||INTEL_BUILD_GUIDE.sup);
|
| 1895 |
+
const first=ids[0],T=isUnit?TYPES[first]:BT[first];
|
| 1896 |
+
host.innerHTML='<div class="menuRoleVisual"></div><div class="menuRoleCopy">'
|
| 1897 |
+
+'<div class="menuRoleEyebrow"><span>'+C.em+'</span>'+G.tag+'<b>'+ids.length+' AVAILABLE</b></div>'
|
| 1898 |
+
+'<strong>'+C.nm+'</strong><p>'+G.use+'</p>'
|
| 1899 |
+
+'<div class="menuRoleDoctrine"><span>'+(isUnit?'FORMATION':'BUILD CHAIN')+'</span>'+(isUnit?G.form:G.chain)+'</div>'
|
| 1900 |
+
+(isUnit?'<small>⚠ '+G.avoid+'</small>':'')
|
| 1901 |
+
+'<button type="button" class="menuRoleInspect">INSPECT '+T.name.toUpperCase()+'</button></div>';
|
| 1902 |
+
const visual=host.querySelector('.menuRoleVisual');
|
| 1903 |
+
visual.appendChild(mfIntelPreviewWindow(isUnit?'unit':'building',first,'TACTICAL PREVIEW'));
|
| 1904 |
+
const inspect=host.querySelector('.menuRoleInspect');
|
| 1905 |
+
inspect.addEventListener('pointerdown',ev=>{
|
| 1906 |
+
ev.preventDefault();ev.stopPropagation();
|
| 1907 |
+
if(isUnit)showUnitTypeCard(first,true);else showBuildingTypeCard(first,-1,true);
|
| 1908 |
+
sfx('ui');
|
| 1909 |
+
});
|
| 1910 |
+
}
|
| 1911 |
let mfIntelMenuTimer=0;
|
| 1912 |
function mfIntelMenuInit(tryN){
|
| 1913 |
const host=$('menuIntelModel');if(!host||host.dataset.ready) return;
|
|
|
|
| 1948 |
?'☄ ACTIVE: CHARGED BARRAGE · select artillery, open POWERS, then target the map'
|
| 1949 |
:'🔒 CHARGED BARRAGE · research Fire Mission Protocol in Development > Doctrine';
|
| 1950 |
card.appendChild(info);
|
| 1951 |
+
}else if(typeof CLASS_AB!=='undefined'){
|
| 1952 |
+
const k=Object.keys(CLASS_AB).find(id=>CLASS_AB[id].cats.indexOf(T.cat)>=0);
|
| 1953 |
+
if(k){
|
| 1954 |
+
const A=CLASS_AB[k],card=$('unitCard'),info=document.createElement('div'),open=classAbilityUnlocked(A);
|
| 1955 |
+
info.className='ucCounter'+(open?'':' caution');
|
| 1956 |
+
info.textContent=open
|
| 1957 |
+
?A.em+' ACTIVE: '+A.nm+' · select this class and open POWERS'
|
| 1958 |
+
:'🔒 '+A.nm+' · research '+((typeof DEVTREE!=='undefined'&&DEVTREE.find(x=>x.id===A.req)||{}).nm||A.req)+' in Development > Doctrine';
|
| 1959 |
+
card.appendChild(info);
|
| 1960 |
+
}
|
| 1961 |
}
|
| 1962 |
mfIntelAttachPreview('unit',tIdx);
|
| 1963 |
}
|
|
|
|
| 2155 |
13:'unit_alpha',18:'unit_scorcher',19:'unit_constructor',
|
| 2156 |
20:'unit_reaper',21:'unit_cinder',22:'unit_lancer',23:'unit_resonator',
|
| 2157 |
24:'unit_warden',25:'unit_kestrel',26:'unit_basilisk',27:'unit_harbinger',
|
| 2158 |
+
28:'unit_praetor',29:'unit_archon',30:'unit_brood',31:'unit_brood'
|
| 2159 |
}[tIdx];
|
| 2160 |
if(special&&typeof itemArt==='function'){
|
| 2161 |
w.innerHTML=itemArt(special,T.name,size);
|
|
|
|
| 2207 |
if(B.type==='tgate') list=[8,26];
|
| 2208 |
else if(B.type==='harbor') list=[14,15];
|
| 2209 |
else if(B.type==='airfield') list=[5,17,25];
|
| 2210 |
+
else list = B.tier===2? [0,1,9,18,10,2,3,6,7,11,16,19,20,21,22,23,24,27,32]
|
| 2211 |
+
: [0,1,9,10,19,24,32];
|
| 2212 |
/* ROLE TABS. A flat grid of eighteen cards is a wall; one tap per role lets
|
| 2213 |
the player find the answer to whatever is killing them without reading
|
| 2214 |
every stat block on the way there. */
|
|
|
|
| 2227 |
b.addEventListener('pointerdown',ev=>{ ev.stopPropagation(); prodTab=c; sfx('ui'); renderProdMenu(); });
|
| 2228 |
tr.appendChild(b);
|
| 2229 |
}
|
| 2230 |
+
renderMenuRoleBrief('unit',prodTab,groups[prodTab]||[]);
|
| 2231 |
(groups[prodTab]||[]).forEach(tIdx=>{
|
| 2232 |
const T=TYPES[tIdx];
|
| 2233 |
const d=document.createElement('div');
|
|
|
|
| 2244 |
if(openBld<0) return;
|
| 2245 |
const Bb=blds[openBld];
|
| 2246 |
if(tIdx===8 && titanCount[0]+Bb.queue.filter(q=>q===8).length>=3){ toast('Max 3 TITANs'); return; }
|
| 2247 |
+
if((tIdx===UT_ENGINEER||tIdx===UT_MINER)&&supportUnitCount(0,true)>=supportUnitCap(0)){
|
| 2248 |
+
toast('⚙ SUPPORT CAP '+supportUnitCap(0)+' — raise Commander level or operate a Research Lab');return;
|
| 2249 |
+
}
|
| 2250 |
if(Bb.queue.length<30){ Bb.queue.push(tIdx); sfx('ui'); renderQueue(); }
|
| 2251 |
if(tIdx!==8){ // hold to queue a batch of 5
|
| 2252 |
const hold=setTimeout(()=>{
|
|
|
|
| 2317 |
b.addEventListener('pointerdown',ev=>{ ev.stopPropagation(); bldTab=c; sfx('ui'); renderBuildMenu(); });
|
| 2318 |
tr.appendChild(b);
|
| 2319 |
}
|
| 2320 |
+
renderMenuRoleBrief('building',bldTab,grp[bldTab]||[]);
|
| 2321 |
for(const key of (grp[bldTab]||[])){
|
| 2322 |
const T=BT[key];
|
| 2323 |
const d=document.createElement('div');
|
src/ui/input.js
CHANGED
|
@@ -146,8 +146,11 @@ function formationTargets(sel,wx,wy,form,angle){
|
|
| 146 |
angle=Math.atan2(wy-cy2/sel.length,wx-cx2/sel.length);
|
| 147 |
}
|
| 148 |
const ct=Math.cos(angle),st=Math.sin(angle),offs=formationOffsets(sel.length,form,formationSpacing(sel));
|
| 149 |
-
return offs.map(o=>
|
| 150 |
-
|
|
|
|
|
|
|
|
|
|
| 151 |
}
|
| 152 |
function setFormation(n,quiet){
|
| 153 |
selFormation=((n%FORMS.length)+FORMS.length)%FORMS.length;
|
|
@@ -182,6 +185,7 @@ function toggleMoveMode(){
|
|
| 182 |
function orderMove(wx,wy,patrol){
|
| 183 |
const sel=formationMembers();
|
| 184 |
if(!sel.length) return false;
|
|
|
|
| 185 |
const fld=requestField(wx,wy); // flow field routes the army around lakes/cliffs
|
| 186 |
const form=FORMS[selFormation].id;
|
| 187 |
const targets=formationTargets(sel,wx,wy,form);
|
|
@@ -215,7 +219,9 @@ function beginPatrolDraft(){
|
|
| 215 |
}
|
| 216 |
function addPatrolWaypoint(wx,wy){
|
| 217 |
if(!patrolDraft)return;
|
| 218 |
-
|
|
|
|
|
|
|
| 219 |
const last=patrolDraft.pts[patrolDraft.pts.length-1];
|
| 220 |
if(Math.hypot(p.x-last.x,p.y-last.y)<35){toast('Place the next waypoint farther along the route');return;}
|
| 221 |
if(patrolDraft.pts.length>=6){toast('Route has 5 waypoints — tap START');return;}
|
|
@@ -409,7 +415,9 @@ function onTap(sx,sy){
|
|
| 409 |
if(armRally>=0){
|
| 410 |
const bi2=armRally; armRally=-1;
|
| 411 |
if(blds[bi2]&&blds[bi2].alive){
|
| 412 |
-
|
|
|
|
|
|
|
| 413 |
addParticle(3,wx,wy,0,0,.5,30, 120,255,170);
|
| 414 |
toast('⚑ Rally point set — new units will gather there');
|
| 415 |
sfx('ui');
|
|
@@ -418,7 +426,9 @@ function onTap(sx,sy){
|
|
| 418 |
}
|
| 419 |
// pre-deployment: taps steer the carrier to your chosen landing ground
|
| 420 |
if(carrier.active&&carrier.phase===1){
|
| 421 |
-
|
|
|
|
|
|
|
| 422 |
addParticle(3,carrier.tx,carrier.ty,0,0,.5,32, 120,220,255);
|
| 423 |
uiCommandAck('deploy',1,carrier.tx,carrier.ty);
|
| 424 |
return;
|
|
@@ -475,8 +485,9 @@ cv.addEventListener('pointerdown',e=>{
|
|
| 475 |
}
|
| 476 |
if(ptrs.size===1&&armFormation&&!placing&&!boxMode&&running&&selCount()){
|
| 477 |
const [wx,wy]=s2w(e.clientX,e.clientY);
|
|
|
|
| 478 |
orderConfirm=null;
|
| 479 |
-
orderPreview={pid:e.pointerId,x:
|
| 480 |
members:formationMembers(),form:selFormation};
|
| 481 |
clearTimeout(holdTimer);return;
|
| 482 |
}
|
|
@@ -518,7 +529,8 @@ cv.addEventListener('pointermove',e=>{
|
|
| 518 |
}
|
| 519 |
if(orderPreview&&orderPreview.pid===e.pointerId){
|
| 520 |
const [wx,wy]=s2w(e.clientX,e.clientY);
|
| 521 |
-
|
|
|
|
| 522 |
}
|
| 523 |
if(ptrs.size===1){
|
| 524 |
if(placing&&dragGhost){
|
|
|
|
| 146 |
angle=Math.atan2(wy-cy2/sel.length,wx-cx2/sel.length);
|
| 147 |
}
|
| 148 |
const ct=Math.cos(angle),st=Math.sin(angle),offs=formationOffsets(sel.length,form,formationSpacing(sel));
|
| 149 |
+
return offs.map(o=>{
|
| 150 |
+
let x=clamp(wx-o[1]*ct-o[0]*st,15,MAP-15),y=clamp(wy-o[1]*st+o[0]*ct,15,MAP-15);
|
| 151 |
+
if(typeof battlefieldClampPoint==='function'){const p=battlefieldClampPoint(x,y,15);x=p[0];y=p[1];}
|
| 152 |
+
return {x,y};
|
| 153 |
+
});
|
| 154 |
}
|
| 155 |
function setFormation(n,quiet){
|
| 156 |
selFormation=((n%FORMS.length)+FORMS.length)%FORMS.length;
|
|
|
|
| 185 |
function orderMove(wx,wy,patrol){
|
| 186 |
const sel=formationMembers();
|
| 187 |
if(!sel.length) return false;
|
| 188 |
+
if(typeof battlefieldClampPoint==='function'){const p=battlefieldClampPoint(wx,wy,24);wx=p[0];wy=p[1];}
|
| 189 |
const fld=requestField(wx,wy); // flow field routes the army around lakes/cliffs
|
| 190 |
const form=FORMS[selFormation].id;
|
| 191 |
const targets=formationTargets(sel,wx,wy,form);
|
|
|
|
| 219 |
}
|
| 220 |
function addPatrolWaypoint(wx,wy){
|
| 221 |
if(!patrolDraft)return;
|
| 222 |
+
let px=clamp(wx,20,MAP-20),py=clamp(wy,20,MAP-20);
|
| 223 |
+
if(typeof battlefieldClampPoint==='function'){const q=battlefieldClampPoint(px,py,24);px=q[0];py=q[1];}
|
| 224 |
+
const p={x:px,y:py};
|
| 225 |
const last=patrolDraft.pts[patrolDraft.pts.length-1];
|
| 226 |
if(Math.hypot(p.x-last.x,p.y-last.y)<35){toast('Place the next waypoint farther along the route');return;}
|
| 227 |
if(patrolDraft.pts.length>=6){toast('Route has 5 waypoints — tap START');return;}
|
|
|
|
| 415 |
if(armRally>=0){
|
| 416 |
const bi2=armRally; armRally=-1;
|
| 417 |
if(blds[bi2]&&blds[bi2].alive){
|
| 418 |
+
let rx=clamp(wx,20,MAP-20),ry=clamp(wy,20,MAP-20);
|
| 419 |
+
if(typeof battlefieldClampPoint==='function'){const p=battlefieldClampPoint(rx,ry,24);rx=p[0];ry=p[1];}
|
| 420 |
+
blds[bi2].rally={x:rx,y:ry};
|
| 421 |
addParticle(3,wx,wy,0,0,.5,30, 120,255,170);
|
| 422 |
toast('⚑ Rally point set — new units will gather there');
|
| 423 |
sfx('ui');
|
|
|
|
| 426 |
}
|
| 427 |
// pre-deployment: taps steer the carrier to your chosen landing ground
|
| 428 |
if(carrier.active&&carrier.phase===1){
|
| 429 |
+
let cx=clamp(wx,60,MAP-60),cy=clamp(wy,60,MAP-60);
|
| 430 |
+
if(typeof battlefieldClampPoint==='function'){const p=battlefieldClampPoint(cx,cy,80);cx=p[0];cy=p[1];}
|
| 431 |
+
carrier.tx=cx; carrier.ty=cy;
|
| 432 |
addParticle(3,carrier.tx,carrier.ty,0,0,.5,32, 120,220,255);
|
| 433 |
uiCommandAck('deploy',1,carrier.tx,carrier.ty);
|
| 434 |
return;
|
|
|
|
| 485 |
}
|
| 486 |
if(ptrs.size===1&&armFormation&&!placing&&!boxMode&&running&&selCount()){
|
| 487 |
const [wx,wy]=s2w(e.clientX,e.clientY);
|
| 488 |
+
const pp=typeof battlefieldClampPoint==='function'?battlefieldClampPoint(wx,wy,24):[clamp(wx,15,MAP-15),clamp(wy,15,MAP-15)];
|
| 489 |
orderConfirm=null;
|
| 490 |
+
orderPreview={pid:e.pointerId,x:pp[0],y:pp[1],
|
| 491 |
members:formationMembers(),form:selFormation};
|
| 492 |
clearTimeout(holdTimer);return;
|
| 493 |
}
|
|
|
|
| 529 |
}
|
| 530 |
if(orderPreview&&orderPreview.pid===e.pointerId){
|
| 531 |
const [wx,wy]=s2w(e.clientX,e.clientY);
|
| 532 |
+
const pp=typeof battlefieldClampPoint==='function'?battlefieldClampPoint(wx,wy,24):[clamp(wx,15,MAP-15),clamp(wy,15,MAP-15)];
|
| 533 |
+
orderPreview.x=pp[0];orderPreview.y=pp[1];return;
|
| 534 |
}
|
| 535 |
if(ptrs.size===1){
|
| 536 |
if(placing&&dragGhost){
|
src/ui/render3d.js
CHANGED
|
@@ -94,7 +94,10 @@ function drawShadows(S){
|
|
| 94 |
as a decal painted on the grass. */
|
| 95 |
for(const o of rocks) if(vis(o.x,o.y,60)) put(o.x,o.y,o.s*0.62,o.s*0.75);
|
| 96 |
for(const o of trees) if(vis(o.x,o.y,60)) put(o.x,o.y,o.s*0.52,o.s*1.15);
|
| 97 |
-
for(const o of crystals)
|
|
|
|
|
|
|
|
|
|
| 98 |
if(typeof carrier!=='undefined'&&carrier.active&&carrierEffectiveAlt()<260)
|
| 99 |
put(carrier.x,carrier.y,54,Math.max(2,carrierEffectiveAlt()*0.6));
|
| 100 |
if(!FX.shadow.n) return;
|
|
@@ -137,6 +140,65 @@ function begin3D(nA){
|
|
| 137 |
}
|
| 138 |
function setEmis(v){ gl.uniform1f(U3.uEmis,v); }
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
/* A beam is a camera-facing textured ribbon, not a row of glow dots. Project
|
| 141 |
both endpoints first so the rectangle follows the beam on screen even while
|
| 142 |
the player rotates and tilts the camera. Long paths are split into a handful
|
|
@@ -342,7 +404,16 @@ function render(dtDraw){
|
|
| 342 |
the active unit, so it cannot alias a material or AO sampler. */
|
| 343 |
gl.activeTexture(gl.TEXTURE7); gl.bindTexture(gl.TEXTURE_2D,fogTex||terrainTex);
|
| 344 |
gl.uniform1f(UT.uFogActive,fogOn&&!demoMode&&fogTex?1:0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
gl.activeTexture(gl.TEXTURE0);
|
|
|
|
| 346 |
drawTerrain();
|
| 347 |
drawShadows(Sun); // ground shadows go on before anything stands on them
|
| 348 |
begin3D(S_nA); // back to the lit model program
|
|
@@ -351,9 +422,16 @@ function render(dtDraw){
|
|
| 351 |
for(const r of rocks){ if(!vis(r.x,r.y,40)) continue; FX.rock.add(r.x,r.y,gh(r.x,r.y),r.s*0.035,r.a,190,186,178,255); }
|
| 352 |
const tt=THEMES[curTheme].treeTint;
|
| 353 |
for(const tr of trees){ if(!vis(tr.x,tr.y,50)) continue; FX.tree.add(tr.x,tr.y,gh(tr.x,tr.y),tr.s*0.030,tr.a,tt[0],tt[1],tt[2],255); }
|
| 354 |
-
for(const cs of crystals){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
for(const D of deposits){ if(!vis(D.x,D.y,60)) continue;
|
| 356 |
-
|
|
|
|
| 357 |
for(const G of geysers){ if(!vis(G.x,G.y,60)) continue; FX.geyser.add(G.x,G.y,gh(G.x,G.y),1,0,210,220,225,255); }
|
| 358 |
// crater berms — real mounds standing on the deformed ground
|
| 359 |
for(const M of relief){ if(!vis(M.x,M.y,60)) continue;
|
|
@@ -882,6 +960,7 @@ function render(dtDraw){
|
|
| 882 |
gl.blendFunc(gl.SRC_ALPHA,gl.ONE);
|
| 883 |
gl.depthMask(false);
|
| 884 |
gl.disable(gl.CULL_FACE);
|
|
|
|
| 885 |
/* At the strategic overview, thousands of individually correct glows placed
|
| 886 |
on regimented formation rows merge into solid stripes. Sample the effects
|
| 887 |
there; the full billboard stack returns automatically as the player zooms
|
|
|
|
| 94 |
as a decal painted on the grass. */
|
| 95 |
for(const o of rocks) if(vis(o.x,o.y,60)) put(o.x,o.y,o.s*0.62,o.s*0.75);
|
| 96 |
for(const o of trees) if(vis(o.x,o.y,60)) put(o.x,o.y,o.s*0.52,o.s*1.15);
|
| 97 |
+
for(const o of crystals){
|
| 98 |
+
const D=deposits[o.dep],tier=depositTier(D);if(!D||o.band>tier||!vis(o.x,o.y,60))continue;
|
| 99 |
+
put(o.x,o.y,o.s*(o.core?.15:.11),o.s*(o.core?.34:.24));
|
| 100 |
+
}
|
| 101 |
if(typeof carrier!=='undefined'&&carrier.active&&carrierEffectiveAlt()<260)
|
| 102 |
put(carrier.x,carrier.y,54,Math.max(2,carrierEffectiveAlt()*0.6));
|
| 103 |
if(!FX.shadow.n) return;
|
|
|
|
| 140 |
}
|
| 141 |
function setEmis(v){ gl.uniform1f(U3.uEmis,v); }
|
| 142 |
|
| 143 |
+
/* A battlefield limit should read as command infrastructure, not the renderer
|
| 144 |
+
running out of world. The red core and EVERY grid rung sit on the safe side;
|
| 145 |
+
procedural exclusion art starts beyond them in the terrain shader. It is
|
| 146 |
+
flushed by the unlit additive pass, so sensor fog cannot turn it black and
|
| 147 |
+
the physical-edge haze cannot erase it. */
|
| 148 |
+
function terrainExclusionStyle(mapKey,themeKey){
|
| 149 |
+
const TH=THEMES[themeKey]||THEMES.verdant;
|
| 150 |
+
const style=mapKey==='isles'?1:(themeKey==='ashland'||mapKey==='crater'?2:
|
| 151 |
+
(themeKey==='arctic'||mapKey==='highland'?3:0));
|
| 152 |
+
const tint=style===1?TH.wDeep:(style===2?(TH.cliff||[64,48,42]):
|
| 153 |
+
(style===3?(themeKey==='arctic'?[108,128,150]:[76,88,104]):(TH.g0||[62,82,54])));
|
| 154 |
+
return {style,tint};
|
| 155 |
+
}
|
| 156 |
+
let mapBoundaryDrawCount=0,mapBoundaryOutsideCount=0;
|
| 157 |
+
function queueBattlefieldEdgeGrid(t,vis){
|
| 158 |
+
mapBoundaryDrawCount=0;mapBoundaryOutsideCount=0;
|
| 159 |
+
const B=typeof battlefieldPlayBounds==='function'?battlefieldPlayBounds(0):{lo:0,hi:MAP,span:MAP};
|
| 160 |
+
const d=typeof battlefieldSignedDistance==='function'?Math.abs(battlefieldSignedDistance(cam.x,cam.y,0)):
|
| 161 |
+
Math.min(Math.abs(cam.x-B.lo),Math.abs(cam.x-B.hi),Math.abs(cam.y-B.lo),Math.abs(cam.y-B.hi));
|
| 162 |
+
const reach=Math.max(480,orthoSpan*.42);
|
| 163 |
+
if(d>=reach) return 0;
|
| 164 |
+
const reveal=clamp((reach-d)/(reach*.64),0,1),span=B.hi-B.lo;
|
| 165 |
+
const seg=Math.max(64,Math.round(span/28)),inner=Math.min(112,span*.07),safe=9;
|
| 166 |
+
const pulse=.78+.22*Math.sin(t*3.2), coreA=(180+55*pulse)*reveal, gridA=(58+38*pulse)*reveal;
|
| 167 |
+
const inside=(x,y,pad)=>typeof battlefieldContains==='function'?battlefieldContains(x,y,pad||0):
|
| 168 |
+
x>=B.lo+(pad||0)&&x<=B.hi-(pad||0)&&y>=B.lo+(pad||0)&&y<=B.hi-(pad||0);
|
| 169 |
+
const put=(x,y,len,ang,r,g,b,a,w,endpoints)=>{
|
| 170 |
+
if(a<5||!vis(x,y,len*.55+55)) return;
|
| 171 |
+
FX.line.add(x,y,terrainH(x,y)+7.5,len,ang,r,g,b,a,w);
|
| 172 |
+
if(!inside(x,y,0)||(endpoints&&endpoints.some(P=>!inside(P[0],P[1],0))))mapBoundaryOutsideCount++;
|
| 173 |
+
mapBoundaryDrawCount++;
|
| 174 |
+
};
|
| 175 |
+
const point=(a,pad)=>typeof battlefieldBoundaryPoint==='function'?battlefieldBoundaryPoint(a,pad):[
|
| 176 |
+
(B.lo+B.hi)*.5+Math.cos(a)*span*.48,(B.lo+B.hi)*.5+Math.sin(a)*span*.48];
|
| 177 |
+
/* Sample the authored silhouette itself. Coastal inlets become a soft,
|
| 178 |
+
rounded perimeter; scorched maps keep sharper chamfers; storm fronts bow
|
| 179 |
+
asymmetrically. No map is forced through the old four-sided stamp. */
|
| 180 |
+
for(let k=0;k<seg;k++){
|
| 181 |
+
const a0=k/seg*TAU,a1=(k+1)/seg*TAU;
|
| 182 |
+
const A=point(a0,safe),C=point(a1,safe),dx=C[0]-A[0],dy=C[1]-A[1],len=Math.hypot(dx,dy);
|
| 183 |
+
put((A[0]+C[0])*.5,(A[1]+C[1])*.5,len,Math.atan2(dy,dx),255,42,30,coreA,3.8,[A,C]);
|
| 184 |
+
const D=point(a0,safe+inner*.42),E=point(a1,safe+inner*.42),ex=E[0]-D[0],ey=E[1]-D[1];
|
| 185 |
+
put((D[0]+E[0])*.5,(D[1]+E[1])*.5,Math.hypot(ex,ey),Math.atan2(ey,ex),255,72,48,gridA,2.0,[D,E]);
|
| 186 |
+
if((k&3)===0){
|
| 187 |
+
const O=point((a0+a1)*.5,safe+2),I=point((a0+a1)*.5,safe+inner),rx=I[0]-O[0],ry=I[1]-O[1];
|
| 188 |
+
put((O[0]+I[0])*.5,(O[1]+I[1])*.5,Math.hypot(rx,ry),Math.atan2(ry,rx),255,58,42,gridA*1.15,1.8,[O,I]);
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
const nodeR=5.2+1.0*pulse;
|
| 192 |
+
for(let k=0;k<8;k++){
|
| 193 |
+
const P=point(k/8*TAU,safe+nodeR+1),x=P[0],y=P[1];
|
| 194 |
+
if(!vis(x,y,90))continue;
|
| 195 |
+
FX.ring.add(x,y,terrainH(x,y)+9,nodeR,-t*.55,255,58,38,205*reveal);
|
| 196 |
+
if(!inside(x,y,nodeR*.15))mapBoundaryOutsideCount++;
|
| 197 |
+
mapBoundaryDrawCount++;
|
| 198 |
+
}
|
| 199 |
+
return mapBoundaryDrawCount;
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
/* A beam is a camera-facing textured ribbon, not a row of glow dots. Project
|
| 203 |
both endpoints first so the rectangle follows the beam on screen even while
|
| 204 |
the player rotates and tilts the camera. Long paths are split into a handful
|
|
|
|
| 404 |
the active unit, so it cannot alias a material or AO sampler. */
|
| 405 |
gl.activeTexture(gl.TEXTURE7); gl.bindTexture(gl.TEXTURE_2D,fogTex||terrainTex);
|
| 406 |
gl.uniform1f(UT.uFogActive,fogOn&&!demoMode&&fogTex?1:0);
|
| 407 |
+
{
|
| 408 |
+
const E=typeof battlefieldPlayBounds==='function'?battlefieldPlayBounds(0):{lo:0,hi:MAP};
|
| 409 |
+
const ES=terrainExclusionStyle(curMap,curTheme),style=ES.style,ec=ES.tint;
|
| 410 |
+
const lc=_lin([ec[0]/255,ec[1]/255,ec[2]/255]);
|
| 411 |
+
gl.uniform2f(UT.uPlayBounds,E.lo,E.hi);
|
| 412 |
+
gl.uniform1f(UT.uEdgeStyle,style);gl.uniform1f(UT.uEdgeTime,t);
|
| 413 |
+
gl.uniform3f(UT.uEdgeTint,lc[0],lc[1],lc[2]);
|
| 414 |
+
}
|
| 415 |
gl.activeTexture(gl.TEXTURE0);
|
| 416 |
+
drawTerrainEdge();
|
| 417 |
drawTerrain();
|
| 418 |
drawShadows(Sun); // ground shadows go on before anything stands on them
|
| 419 |
begin3D(S_nA); // back to the lit model program
|
|
|
|
| 422 |
for(const r of rocks){ if(!vis(r.x,r.y,40)) continue; FX.rock.add(r.x,r.y,gh(r.x,r.y),r.s*0.035,r.a,190,186,178,255); }
|
| 423 |
const tt=THEMES[curTheme].treeTint;
|
| 424 |
for(const tr of trees){ if(!vis(tr.x,tr.y,50)) continue; FX.tree.add(tr.x,tr.y,gh(tr.x,tr.y),tr.s*0.030,tr.a,tt[0],tt[1],tt[2],255); }
|
| 425 |
+
for(const cs of crystals){
|
| 426 |
+
const D=deposits[cs.dep],tier=depositTier(D);if(!D||cs.band>tier||!vis(cs.x,cs.y,52))continue;
|
| 427 |
+
const fill=clamp((D.remaining-(tier-1)*DEPOSIT_BAND)/DEPOSIT_BAND,0,1),edge=cs.band===tier?(.46+.54*fill):1;
|
| 428 |
+
const col=cs.band===3?[255,120,255]:cs.band===2?[105,255,176]:[105,226,255];
|
| 429 |
+
const sc=cs.s*(cs.core?.073:.058)*edge;
|
| 430 |
+
FX.crystal.add(cs.x,cs.y,gh(cs.x,cs.y),sc,cs.a,col[0],col[1],col[2],255);
|
| 431 |
+
}
|
| 432 |
for(const D of deposits){ if(!vis(D.x,D.y,60)) continue;
|
| 433 |
+
const tier=depositTier(D),col=tier===3?[255,122,255]:tier===2?[110,255,180]:tier===1?[112,228,255]:[88,92,100];
|
| 434 |
+
FX.dep.add(D.x,D.y,gh(D.x,D.y),.9+tier*.14,0,col[0],col[1],col[2],tier?255:145); }
|
| 435 |
for(const G of geysers){ if(!vis(G.x,G.y,60)) continue; FX.geyser.add(G.x,G.y,gh(G.x,G.y),1,0,210,220,225,255); }
|
| 436 |
// crater berms — real mounds standing on the deformed ground
|
| 437 |
for(const M of relief){ if(!vis(M.x,M.y,60)) continue;
|
|
|
|
| 960 |
gl.blendFunc(gl.SRC_ALPHA,gl.ONE);
|
| 961 |
gl.depthMask(false);
|
| 962 |
gl.disable(gl.CULL_FACE);
|
| 963 |
+
queueBattlefieldEdgeGrid(t,vis);
|
| 964 |
/* At the strategic overview, thousands of individually correct glows placed
|
| 965 |
on regimented formation rows merge into solid stripes. Sample the effects
|
| 966 |
there; the full billboard stack returns automatically as the player zooms
|
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.
|
| 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.0';
|
| 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,
|