Spaces:
Running
Running
MASSFRONT 1.33.76 exact packed browser release (part 5)
Browse files- src/galactic-operations.js +24 -12
- src/game/matchconsumer.js +99 -4
- src/game/meta.js +76 -6
- src/game/sim.js +93 -25
- src/game/statehash.js +7 -1
- src/intro.js +9 -2
- src/launcher.js +61 -37
- src/main.js +42 -15
- src/onboarding.js +2 -1
- src/ui/hud.js +27 -6
- src/ui/input.js +0 -0
- src/ui/render3d.js +1 -1
- src/updater.js +2 -2
- sw.js +1 -1
src/galactic-operations.js
CHANGED
|
@@ -332,7 +332,13 @@
|
|
| 332 |
return typeof PROFILES!=='undefined'&&PROFILES&&text(PROFILES.active)?PROFILES.active:'';
|
| 333 |
}
|
| 334 |
function currentFlagOn(){
|
| 335 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
function stripBridgeQuery(){
|
| 338 |
if(typeof history==='undefined'||typeof history.replaceState!=='function')return;
|
|
@@ -456,10 +462,8 @@
|
|
| 456 |
the authored abandon action can refund the deployment. A malformed nonce
|
| 457 |
is never reflected into a URL. */
|
| 458 |
setTimeout(()=>{
|
| 459 |
-
const target=NONCE_RE.test(bridge.nonce)
|
| 460 |
-
|
| 461 |
-
:'./modules/space_exploration/index.html';
|
| 462 |
-
try{location.href=target;}
|
| 463 |
catch(e){bridge.status='return-error';bridge.reason='REJECTION_RETURN_FAILED';}
|
| 464 |
},900);
|
| 465 |
}
|
|
@@ -712,8 +716,8 @@
|
|
| 712 |
function returnRejectedToNexus(){
|
| 713 |
if(bridge.returning)return false;
|
| 714 |
bridge.returning=true;bridge.status='returning-rejected';
|
| 715 |
-
const target='
|
| 716 |
-
try{location.href=target;return true;}
|
| 717 |
catch(e){bridge.returning=false;bridge.status='return-error';bridge.reason='RETURN_NAVIGATION_FAILED';return false;}
|
| 718 |
}
|
| 719 |
function returnToNexus(){
|
|
@@ -723,14 +727,14 @@
|
|
| 723 |
return false;
|
| 724 |
}
|
| 725 |
bridge.returning=true;bridge.status='returning';
|
| 726 |
-
const target='
|
| 727 |
-
try{location.href=target;return true;}
|
| 728 |
catch(e){bridge.returning=false;bridge.status='return-error';bridge.reason='RETURN_NAVIGATION_FAILED';if(typeof toast==='function')toast('NEXUS-VII return route could not be opened');return false;}
|
| 729 |
}
|
| 730 |
function returnExistingReportToNexus(record){
|
| 731 |
bridge.active=false;bridge.report=clone(record);bridge.returning=true;bridge.status='returning-existing';bridge.reason='';
|
| 732 |
-
const target='
|
| 733 |
-
try{location.href=target;return true;}
|
| 734 |
catch(e){bridge.returning=false;bridge.status='return-error';bridge.reason='RETURN_NAVIGATION_FAILED';return false;}
|
| 735 |
}
|
| 736 |
|
|
@@ -837,7 +841,9 @@
|
|
| 837 |
};
|
| 838 |
}
|
| 839 |
|
|
|
|
| 840 |
const api={validateEntryTicket,validateRouteRequest,validateRequest,validateRequestMirror,validateTacticalReport,validateResultMirror,
|
|
|
|
| 841 |
validateDeploymentContract,describeOperationEffects,checksum:envelopeChecksum};
|
| 842 |
Object.defineProperties(api,{
|
| 843 |
active:{enumerable:true,get:()=>bridge.active},
|
|
@@ -866,7 +872,13 @@
|
|
| 866 |
if(bridge.menuRouteActive&&(id==='startScreen'||id==='warScr')){
|
| 867 |
bridge.menuRouteActive=false;
|
| 868 |
if(currentFlagOn()&&typeof mfOpenExploration==='function'){
|
| 869 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 870 |
if(!opened)baseShowFrontScreen.call(this,id);
|
| 871 |
});
|
| 872 |
return true;
|
|
|
|
| 332 |
return typeof PROFILES!=='undefined'&&PROFILES&&text(PROFILES.active)?PROFILES.active:'';
|
| 333 |
}
|
| 334 |
function currentFlagOn(){
|
| 335 |
+
return window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true
|
| 336 |
+
||window.__MF_OTA_HAS_GALACTIC_DELIVERY===true
|
| 337 |
+
||!!(typeof META!=='undefined'&&META&&META.settings&&META.settings.experimentalExploration===true);
|
| 338 |
+
}
|
| 339 |
+
function explorationReturnTarget(query=''){
|
| 340 |
+
const target='./modules/space_exploration/index.html'+query;
|
| 341 |
+
return typeof mfContentExplorationReturnUrl==='function'?mfContentExplorationReturnUrl(target):target;
|
| 342 |
}
|
| 343 |
function stripBridgeQuery(){
|
| 344 |
if(typeof history==='undefined'||typeof history.replaceState!=='function')return;
|
|
|
|
| 462 |
the authored abandon action can refund the deployment. A malformed nonce
|
| 463 |
is never reflected into a URL. */
|
| 464 |
setTimeout(()=>{
|
| 465 |
+
const target=explorationReturnTarget(NONCE_RE.test(bridge.nonce)?'?groundRejected='+encodeURIComponent(bridge.nonce):'');
|
| 466 |
+
try{if(!target)throw new Error('Content return unavailable');location.href=target;}
|
|
|
|
|
|
|
| 467 |
catch(e){bridge.status='return-error';bridge.reason='REJECTION_RETURN_FAILED';}
|
| 468 |
},900);
|
| 469 |
}
|
|
|
|
| 716 |
function returnRejectedToNexus(){
|
| 717 |
if(bridge.returning)return false;
|
| 718 |
bridge.returning=true;bridge.status='returning-rejected';
|
| 719 |
+
const target=explorationReturnTarget('?groundRejected='+encodeURIComponent(bridge.nonce));
|
| 720 |
+
try{if(!target)throw new Error('Content return unavailable');location.href=target;return true;}
|
| 721 |
catch(e){bridge.returning=false;bridge.status='return-error';bridge.reason='RETURN_NAVIGATION_FAILED';return false;}
|
| 722 |
}
|
| 723 |
function returnToNexus(){
|
|
|
|
| 727 |
return false;
|
| 728 |
}
|
| 729 |
bridge.returning=true;bridge.status='returning';
|
| 730 |
+
const target=explorationReturnTarget('?groundResult='+encodeURIComponent(bridge.nonce));
|
| 731 |
+
try{if(!target)throw new Error('Content return unavailable');location.href=target;return true;}
|
| 732 |
catch(e){bridge.returning=false;bridge.status='return-error';bridge.reason='RETURN_NAVIGATION_FAILED';if(typeof toast==='function')toast('NEXUS-VII return route could not be opened');return false;}
|
| 733 |
}
|
| 734 |
function returnExistingReportToNexus(record){
|
| 735 |
bridge.active=false;bridge.report=clone(record);bridge.returning=true;bridge.status='returning-existing';bridge.reason='';
|
| 736 |
+
const target=explorationReturnTarget('?groundResult='+encodeURIComponent(bridge.nonce));
|
| 737 |
+
try{if(!target)throw new Error('Content return unavailable');location.href=target;return true;}
|
| 738 |
catch(e){bridge.returning=false;bridge.status='return-error';bridge.reason='RETURN_NAVIGATION_FAILED';return false;}
|
| 739 |
}
|
| 740 |
|
|
|
|
| 841 |
};
|
| 842 |
}
|
| 843 |
|
| 844 |
+
let menuReturnContext=null;
|
| 845 |
const api={validateEntryTicket,validateRouteRequest,validateRequest,validateRequestMirror,validateTacticalReport,validateResultMirror,
|
| 846 |
+
isMenuReturnContext:value=>value!==null&&value===menuReturnContext&&bridge.status==='menu-route',
|
| 847 |
validateDeploymentContract,describeOperationEffects,checksum:envelopeChecksum};
|
| 848 |
Object.defineProperties(api,{
|
| 849 |
active:{enumerable:true,get:()=>bridge.active},
|
|
|
|
| 872 |
if(bridge.menuRouteActive&&(id==='startScreen'||id==='warScr')){
|
| 873 |
bridge.menuRouteActive=false;
|
| 874 |
if(currentFlagOn()&&typeof mfOpenExploration==='function'){
|
| 875 |
+
// Only this synchronous call can authorize returning an existing
|
| 876 |
+
// neutral career. It grants no faction, personnel, or mission access.
|
| 877 |
+
menuReturnContext=Object.freeze({kind:'validated-menu-return'});
|
| 878 |
+
let opening;
|
| 879 |
+
try{opening=mfOpenExploration('campaign_hub',{menuReturn:menuReturnContext});}
|
| 880 |
+
finally{menuReturnContext=null;}
|
| 881 |
+
Promise.resolve(opening).then(opened=>{
|
| 882 |
if(!opened)baseShowFrontScreen.call(this,id);
|
| 883 |
});
|
| 884 |
return true;
|
src/game/matchconsumer.js
CHANGED
|
@@ -17,12 +17,14 @@
|
|
| 17 |
const MC_INT=(v,min,max)=>Number.isSafeInteger(v)&&v>=min&&v<=max;
|
| 18 |
const MC_KEYS=(v,keys)=>!!v&&typeof v==='object'&&!Array.isArray(v)&&
|
| 19 |
Object.keys(v).length===keys.length&&Object.keys(v).every(k=>keys.includes(k));
|
| 20 |
-
const MC_TYPES=Object.freeze(['move','stop','hold','attack','guard','build','produce','research','commander','repair','recycle','upgrade']);
|
| 21 |
const MC_UNIT_COMMAND_MAX=64,MC_BATCH_COMMAND_MAX=8,MC_COMMAND_BYTES_MAX=2048,
|
| 22 |
MC_LOGICAL_UNIT_MAX=MC_UNIT_COMMAND_MAX*MC_BATCH_COMMAND_MAX,
|
| 23 |
MC_FORM_IDS=Object.freeze(['spread','line','wedge','box','column','arc']);
|
| 24 |
let mcRules=null,mcWelcome=null,mcStart=null,mcLaunchStarted=false,mcSubmitFailure='',mcApplyFailure='';
|
| 25 |
let mcUpgradePendingTick=-1,mcUpgradePendingSeq=-1;
|
|
|
|
|
|
|
| 26 |
|
| 27 |
function mcLobbyRules(){
|
| 28 |
const live=window.MFSocialUI&&MFSocialUI.state&&MFSocialUI.state.lobby,
|
|
@@ -40,12 +42,14 @@
|
|
| 40 |
packet and the pending service receipt before replay could reconcile it. */
|
| 41 |
if(mcWelcome.resumed!==true){
|
| 42 |
mcUpgradePendingTick=mcUpgradePendingSeq=-1;
|
|
|
|
|
|
|
| 43 |
mcLastCommittedTick=Number.isSafeInteger(mcWelcome.tick)?mcWelcome.tick:0;mcAppliedTicks.clear();
|
| 44 |
if(mcQueuedTick){const Q=mcQueuedTick;mcQueuedTick=null;Q.resolve(false);}
|
| 45 |
}
|
| 46 |
}
|
| 47 |
if(e&&e.type==='massfront-match:start'){
|
| 48 |
-
mcStart=e.detail||null;mcLastCommittedTick=0;mcAppliedTicks.clear();
|
| 49 |
if(typeof queueMicrotask==='function')queueMicrotask(mcBootstrapMatch);else setTimeout(mcBootstrapMatch,0);
|
| 50 |
}
|
| 51 |
}
|
|
@@ -214,6 +218,34 @@
|
|
| 214 |
if(typeof populationCanSpawn==='function'&&!populationCanSpawn(command.unit,authority.team,authority.slot))return null;
|
| 215 |
return {type:'produce',building:ref.id,unit:command.unit,count:command.count};
|
| 216 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
if(command.type==='research'){
|
| 218 |
if(!MC_KEYS(command,['type','building','study'])||typeof command.study!=='string')return null;
|
| 219 |
const ref=mcBuildingRef(command.building,authority,['techlab']),idx=typeof RESEARCH!=='undefined'?
|
|
@@ -284,6 +316,8 @@
|
|
| 284 |
for(const i of P.indices){if(upgrades.has(i)||services.has(i))return false;upgrades.add(i);}
|
| 285 |
const key=P.authority.team+':'+P.authority.slot,E=escrow.get(key)||{m:0,e:0,a:P.authority};
|
| 286 |
E.m+=P.costM;E.e+=P.costE;escrow.set(key,E);
|
|
|
|
|
|
|
| 287 |
}else if(P.type==='produce'){
|
| 288 |
if(services.has(P.building))return false;
|
| 289 |
const n=(prod.get(P.building)||0)+P.count,B=blds[P.building];prod.set(P.building,n);
|
|
@@ -425,8 +459,30 @@
|
|
| 425 |
return;
|
| 426 |
}
|
| 427 |
if(plan.type==='produce'){
|
| 428 |
-
const B=blds[plan.building];for(let n=0;n<plan.count;n++)B.queue.push(plan.unit);
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
if(plan.type==='research'){
|
| 431 |
const B=blds[plan.building],R=RESEARCH[plan.study];B.res=plan.study;B.resT=Math.min(R.t-.01,researchResumeTime(R.id));return;
|
| 432 |
}
|
|
@@ -499,6 +555,11 @@
|
|
| 499 |
}
|
| 500 |
function mcClearUpgradeAt(tick){
|
| 501 |
if(mcUpgradePendingTick>=0&&tick>=mcUpgradePendingTick)mcUpgradePendingTick=mcUpgradePendingSeq=-1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
}
|
| 503 |
function mcDuplicateTick(packet,signature){return mcAppliedTicks.has(packet.tick)&&mcAppliedTicks.get(packet.tick)===signature;}
|
| 504 |
async function mcApplyTick(packet){
|
|
@@ -556,6 +617,11 @@
|
|
| 556 |
accepted one remains pending until its authoritative target tick commits. */
|
| 557 |
if(mcUpgradePendingSeq>=0&&(mcUpgradePendingSeq>lastSeq||mcUpgradePendingTick<=mcLastCommittedTick))
|
| 558 |
mcUpgradePendingTick=mcUpgradePendingSeq=-1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
return true;
|
| 560 |
}
|
| 561 |
function mcRuntimeActive(){
|
|
@@ -682,6 +748,33 @@
|
|
| 682 |
if(receipt){mcUpgradePendingTick=receipt.targetTick;mcUpgradePendingSeq=receipt.seq;}
|
| 683 |
return receipt;
|
| 684 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
function mcUnitRefs(indices){return indices.map(i=>({id:i,generation:ugen[i]}));}
|
| 686 |
function mcSelected(){const out=[];for(let i=0;i<unitHigh;i++)if(ualive[i]&&usel[i])out.push(i);return out;}
|
| 687 |
function mcWrap(name,make){
|
|
@@ -744,6 +837,8 @@
|
|
| 744 |
lastAppliedTick:()=>mcLastCommittedTick,resumeState:mcResumeState,
|
| 745 |
submit:mcSubmit,takeover:mcTakeover,
|
| 746 |
submitRepair:mcSubmitRepair,submitRecycle:mcSubmitRecycle,submitUpgrade:mcSubmitUpgrade,
|
|
|
|
|
|
|
| 747 |
upgradePending:()=>mcSessionLockstep()&&mcUpgradePendingTick>=0,buildingRef:mcBuildingHandle,
|
| 748 |
lastFailure:()=>mcApplyFailure||mcSubmitFailure,
|
| 749 |
bootstrap:()=>Object.freeze({localSeat:mcWelcome&&mcWelcome.seat||0,seats:mcStart&&Array.isArray(mcStart.seats)?mcStart.seats.slice():[],rules:mcLobbyRules()}),seatAuthority:seat=>{
|
|
|
|
| 17 |
const MC_INT=(v,min,max)=>Number.isSafeInteger(v)&&v>=min&&v<=max;
|
| 18 |
const MC_KEYS=(v,keys)=>!!v&&typeof v==='object'&&!Array.isArray(v)&&
|
| 19 |
Object.keys(v).length===keys.length&&Object.keys(v).every(k=>keys.includes(k));
|
| 20 |
+
const MC_TYPES=Object.freeze(['move','stop','hold','attack','guard','build','produce','research','commander','repair','recycle','upgrade','repeat','rally','cancel_production']);
|
| 21 |
const MC_UNIT_COMMAND_MAX=64,MC_BATCH_COMMAND_MAX=8,MC_COMMAND_BYTES_MAX=2048,
|
| 22 |
MC_LOGICAL_UNIT_MAX=MC_UNIT_COMMAND_MAX*MC_BATCH_COMMAND_MAX,
|
| 23 |
MC_FORM_IDS=Object.freeze(['spread','line','wedge','box','column','arc']);
|
| 24 |
let mcRules=null,mcWelcome=null,mcStart=null,mcLaunchStarted=false,mcSubmitFailure='',mcApplyFailure='';
|
| 25 |
let mcUpgradePendingTick=-1,mcUpgradePendingSeq=-1;
|
| 26 |
+
const mcRepeatPending=new Map();
|
| 27 |
+
const mcCancelPending=new Map();
|
| 28 |
|
| 29 |
function mcLobbyRules(){
|
| 30 |
const live=window.MFSocialUI&&MFSocialUI.state&&MFSocialUI.state.lobby,
|
|
|
|
| 42 |
packet and the pending service receipt before replay could reconcile it. */
|
| 43 |
if(mcWelcome.resumed!==true){
|
| 44 |
mcUpgradePendingTick=mcUpgradePendingSeq=-1;
|
| 45 |
+
mcRepeatPending.clear();
|
| 46 |
+
mcCancelPending.clear();
|
| 47 |
mcLastCommittedTick=Number.isSafeInteger(mcWelcome.tick)?mcWelcome.tick:0;mcAppliedTicks.clear();
|
| 48 |
if(mcQueuedTick){const Q=mcQueuedTick;mcQueuedTick=null;Q.resolve(false);}
|
| 49 |
}
|
| 50 |
}
|
| 51 |
if(e&&e.type==='massfront-match:start'){
|
| 52 |
+
mcStart=e.detail||null;mcLastCommittedTick=0;mcAppliedTicks.clear();mcRepeatPending.clear();mcCancelPending.clear();
|
| 53 |
if(typeof queueMicrotask==='function')queueMicrotask(mcBootstrapMatch);else setTimeout(mcBootstrapMatch,0);
|
| 54 |
}
|
| 55 |
}
|
|
|
|
| 218 |
if(typeof populationCanSpawn==='function'&&!populationCanSpawn(command.unit,authority.team,authority.slot))return null;
|
| 219 |
return {type:'produce',building:ref.id,unit:command.unit,count:command.count};
|
| 220 |
}
|
| 221 |
+
if(command.type==='repeat'||command.type==='rally'){
|
| 222 |
+
const repeat=command.type==='repeat';
|
| 223 |
+
if(!MC_KEYS(command,repeat?['type','building','active']:['type','building','x','y'])||
|
| 224 |
+
repeat&&typeof command.active!=='boolean'||!repeat&&!(point=mcPoint(command)))return null;
|
| 225 |
+
const ref=mcBuildingRef(command.building,authority,['fac','tgate','harbor','airfield']);
|
| 226 |
+
if(!ref||!Array.isArray(ref.B.queue)||!(ref.B.prog>=1)||typeof commanderSlotForBuilding!=='function')return null;
|
| 227 |
+
if(repeat)return {type:'repeat',building:ref.id,active:command.active};
|
| 228 |
+
/* Transport carries map coordinates, not a client-resolved path. All seats
|
| 229 |
+
clamp the same marker; production later projects it for each chassis. */
|
| 230 |
+
point=point.map(v=>Math.max(20,Math.min(MAP-20,v)));
|
| 231 |
+
if(typeof battlefieldClampPoint==='function')point=battlefieldClampPoint(point[0],point[1],24);
|
| 232 |
+
if(!point||point.length!==2||!point.every(v=>Number.isFinite(v)&&v>=0&&v<=MAP))return null;
|
| 233 |
+
return {type:'rally',building:ref.id,point};
|
| 234 |
+
}
|
| 235 |
+
if(command.type==='cancel_production'){
|
| 236 |
+
const cap=typeof MF_PRODUCTION_QUEUE_CAP==='number'?MF_PRODUCTION_QUEUE_CAP:30;
|
| 237 |
+
if(!MC_KEYS(command,['type','building','start','unit','revision','queue'])||
|
| 238 |
+
!MC_INT(command.revision,0,1e9)||!Array.isArray(command.queue)||!command.queue.length||command.queue.length>cap||
|
| 239 |
+
!command.queue.every(t=>MC_INT(t,0,TYPES.length-1))||!MC_INT(command.start,0,command.queue.length-1)||
|
| 240 |
+
command.unit!==command.queue[command.start])return null;
|
| 241 |
+
const ref=mcBuildingRef(command.building,authority,['fac','tgate','harbor','airfield']);
|
| 242 |
+
if(!ref||!Array.isArray(ref.B.queue)||!(ref.B.prog>=1)||typeof commanderSlotForBuilding!=='function'||typeof credit!=='function')return null;
|
| 243 |
+
let last=command.start;while(last+1<command.queue.length&&command.queue[last+1]===command.unit)last++;
|
| 244 |
+
const T=TYPES[command.unit];if(!T||!Number.isFinite(T.bt)||T.bt<=0)return null;
|
| 245 |
+
const cost=typeof factionDoctrineUnitCost==='function'?factionDoctrineUnitCost(T,authority.team):{m:T.cm,e:T.ce};
|
| 246 |
+
if(!cost||!Number.isFinite(cost.m)||cost.m<0||!Number.isFinite(cost.e)||cost.e<0)return null;
|
| 247 |
+
return {type:'cancel_production',building:ref.id,last,revision:command.revision,queue:command.queue.slice(),cost,authority};
|
| 248 |
+
}
|
| 249 |
if(command.type==='research'){
|
| 250 |
if(!MC_KEYS(command,['type','building','study'])||typeof command.study!=='string')return null;
|
| 251 |
const ref=mcBuildingRef(command.building,authority,['techlab']),idx=typeof RESEARCH!=='undefined'?
|
|
|
|
| 316 |
for(const i of P.indices){if(upgrades.has(i)||services.has(i))return false;upgrades.add(i);}
|
| 317 |
const key=P.authority.team+':'+P.authority.slot,E=escrow.get(key)||{m:0,e:0,a:P.authority};
|
| 318 |
E.m+=P.costM;E.e+=P.costE;escrow.set(key,E);
|
| 319 |
+
}else if(P.type==='repeat'||P.type==='rally'||P.type==='cancel_production'){
|
| 320 |
+
if(services.has(P.building))return false;
|
| 321 |
}else if(P.type==='produce'){
|
| 322 |
if(services.has(P.building))return false;
|
| 323 |
const n=(prod.get(P.building)||0)+P.count,B=blds[P.building];prod.set(P.building,n);
|
|
|
|
| 459 |
return;
|
| 460 |
}
|
| 461 |
if(plan.type==='produce'){
|
| 462 |
+
const B=blds[plan.building];for(let n=0;n<plan.count;n++)B.queue.push(plan.unit);
|
| 463 |
+
B.queueRevision=(B.queueRevision||0)+1;return;
|
| 464 |
+
}
|
| 465 |
+
if(plan.type==='cancel_production'){
|
| 466 |
+
const B=blds[plan.building];
|
| 467 |
+
/* A repeat completion can replace [unit] with an identical [unit]. The
|
| 468 |
+
revision, not just the visible stack, identifies the work being cancelled.
|
| 469 |
+
Stale clicks and duplicate proposals consume no-op instead of freezing
|
| 470 |
+
every peer or cancelling the next chassis/refunding it a second time. */
|
| 471 |
+
if((B.queueRevision||0)!==plan.revision||B.queue.length!==plan.queue.length||
|
| 472 |
+
!B.queue.every((t,i)=>t===plan.queue[i])){
|
| 473 |
+
const local=mcLocalAuthority();
|
| 474 |
+
if(local&&local.seat===plan.authority.seat&&typeof toast==='function')toast('Queue changed β cancel not applied; select the current stack again');
|
| 475 |
+
return;
|
| 476 |
+
}
|
| 477 |
+
if(plan.last===0){
|
| 478 |
+
const T=TYPES[B.queue[0]],frac=Math.min(1,Math.max(0,B.prodT||0)/T.bt);
|
| 479 |
+
if(frac>0)credit(plan.authority.team,plan.cost.m*frac,plan.cost.e*frac,plan.authority.slot);
|
| 480 |
+
B.queue.shift();B.prodT=0;
|
| 481 |
+
}else B.queue.splice(plan.last,1);
|
| 482 |
+
B.queueRevision=(B.queueRevision||0)+1;return;
|
| 483 |
+
}
|
| 484 |
+
if(plan.type==='repeat'){blds[plan.building].repeat=plan.active;return;}
|
| 485 |
+
if(plan.type==='rally'){blds[plan.building].rally={x:plan.point[0],y:plan.point[1]};return;}
|
| 486 |
if(plan.type==='research'){
|
| 487 |
const B=blds[plan.building],R=RESEARCH[plan.study];B.res=plan.study;B.resT=Math.min(R.t-.01,researchResumeTime(R.id));return;
|
| 488 |
}
|
|
|
|
| 555 |
}
|
| 556 |
function mcClearUpgradeAt(tick){
|
| 557 |
if(mcUpgradePendingTick>=0&&tick>=mcUpgradePendingTick)mcUpgradePendingTick=mcUpgradePendingSeq=-1;
|
| 558 |
+
for(const [id,rows] of mcRepeatPending){
|
| 559 |
+
const pending=rows.filter(r=>r.tick>tick);
|
| 560 |
+
if(pending.length)mcRepeatPending.set(id,pending);else mcRepeatPending.delete(id);
|
| 561 |
+
}
|
| 562 |
+
for(const [id,r] of mcCancelPending)if(r.tick<=tick)mcCancelPending.delete(id);
|
| 563 |
}
|
| 564 |
function mcDuplicateTick(packet,signature){return mcAppliedTicks.has(packet.tick)&&mcAppliedTicks.get(packet.tick)===signature;}
|
| 565 |
async function mcApplyTick(packet){
|
|
|
|
| 617 |
accepted one remains pending until its authoritative target tick commits. */
|
| 618 |
if(mcUpgradePendingSeq>=0&&(mcUpgradePendingSeq>lastSeq||mcUpgradePendingTick<=mcLastCommittedTick))
|
| 619 |
mcUpgradePendingTick=mcUpgradePendingSeq=-1;
|
| 620 |
+
for(const [id,rows] of mcRepeatPending){
|
| 621 |
+
const pending=rows.filter(r=>r.seq<=lastSeq&&r.tick>mcLastCommittedTick);
|
| 622 |
+
if(pending.length)mcRepeatPending.set(id,pending);else mcRepeatPending.delete(id);
|
| 623 |
+
}
|
| 624 |
+
for(const [id,r] of mcCancelPending)if(r.seq>lastSeq||r.tick<=mcLastCommittedTick)mcCancelPending.delete(id);
|
| 625 |
return true;
|
| 626 |
}
|
| 627 |
function mcRuntimeActive(){
|
|
|
|
| 748 |
if(receipt){mcUpgradePendingTick=receipt.targetTick;mcUpgradePendingSeq=receipt.seq;}
|
| 749 |
return receipt;
|
| 750 |
}
|
| 751 |
+
function mcRepeatIntent(target){
|
| 752 |
+
const ref=mcBuildingHandle(target),B=ref&&blds[ref.id],rows=ref&&mcSessionLockstep()&&mcRepeatPending.get(ref.id),
|
| 753 |
+
last=rows&&rows[rows.length-1],pending=!!(last&&last.type===ref.type);
|
| 754 |
+
return {active:pending?last.active:!!(B&&B.repeat),pending};
|
| 755 |
+
}
|
| 756 |
+
function mcSubmitRepeat(target,active,delay){
|
| 757 |
+
const ref=mcBuildingHandle(target),receipt=mcSubmit({type:'repeat',building:ref,active},delay);
|
| 758 |
+
if(receipt){
|
| 759 |
+
/* Keep intent outside simulation state. Rapid ON then OFF before a tick
|
| 760 |
+
must transmit two explicit states, not two toggles of stale B.repeat. */
|
| 761 |
+
const rows=mcRepeatPending.get(ref.id)||[];
|
| 762 |
+
rows.push({type:ref.type,active,tick:receipt.targetTick,seq:receipt.seq});mcRepeatPending.set(ref.id,rows);
|
| 763 |
+
}
|
| 764 |
+
return receipt;
|
| 765 |
+
}
|
| 766 |
+
function mcSubmitRally(target,x,y,delay){
|
| 767 |
+
return mcSubmit({type:'rally',building:mcBuildingHandle(target),x:Math.round(x),y:Math.round(y)},delay);
|
| 768 |
+
}
|
| 769 |
+
function mcSubmitCancelProduction(target,start,snapshot,delay){
|
| 770 |
+
const ref=mcBuildingHandle(target),B=ref&&blds[ref.id];
|
| 771 |
+
if(ref&&mcCancelPending.has(ref.id)){mcSubmitFailure='Waiting for the previous cancellation';return null;}
|
| 772 |
+
const queue=snapshot?snapshot.queue:B&&B.queue,revision=snapshot?snapshot.revision:B&&B.queueRevision||0,
|
| 773 |
+
receipt=mcSubmit({type:'cancel_production',building:ref,start,unit:Array.isArray(queue)?queue[start]:null,
|
| 774 |
+
revision,queue:Array.isArray(queue)?queue.slice():null},delay);
|
| 775 |
+
if(receipt)mcCancelPending.set(ref.id,{tick:receipt.targetTick,seq:receipt.seq});
|
| 776 |
+
return receipt;
|
| 777 |
+
}
|
| 778 |
function mcUnitRefs(indices){return indices.map(i=>({id:i,generation:ugen[i]}));}
|
| 779 |
function mcSelected(){const out=[];for(let i=0;i<unitHigh;i++)if(ualive[i]&&usel[i])out.push(i);return out;}
|
| 780 |
function mcWrap(name,make){
|
|
|
|
| 837 |
lastAppliedTick:()=>mcLastCommittedTick,resumeState:mcResumeState,
|
| 838 |
submit:mcSubmit,takeover:mcTakeover,
|
| 839 |
submitRepair:mcSubmitRepair,submitRecycle:mcSubmitRecycle,submitUpgrade:mcSubmitUpgrade,
|
| 840 |
+
submitRepeat:mcSubmitRepeat,repeatIntent:mcRepeatIntent,submitRally:mcSubmitRally,
|
| 841 |
+
submitCancelProduction:mcSubmitCancelProduction,
|
| 842 |
upgradePending:()=>mcSessionLockstep()&&mcUpgradePendingTick>=0,buildingRef:mcBuildingHandle,
|
| 843 |
lastFailure:()=>mcApplyFailure||mcSubmitFailure,
|
| 844 |
bootstrap:()=>Object.freeze({localSeat:mcWelcome&&mcWelcome.seat||0,seats:mcStart&&Array.isArray(mcStart.seats)?mcStart.seats.slice():[],rules:mcLobbyRules()}),seatAuthority:seat=>{
|
src/game/meta.js
CHANGED
|
@@ -1711,13 +1711,84 @@ function mfBindTap(el,fn){
|
|
| 1711 |
pointer timing while adding one guarded keyboard path. Preventing the key's
|
| 1712 |
native click and suppressing a same-turn synthetic click avoids the classic
|
| 1713 |
double activation without making screen-reader click depend on key events. */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1714 |
function mfBindNativePress(el,fn,pointerEvent){
|
| 1715 |
if(!el||typeof fn!=='function'||(el.dataset&&el.dataset.mfNativePress==='1')) return;
|
| 1716 |
if(el.dataset) el.dataset.mfNativePress='1';
|
| 1717 |
const now=typeof mfTapNow==='function'?mfTapNow:()=>performance.now();
|
| 1718 |
-
let pointerCommit=-1e9,keyCommit=-1e9;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1719 |
el.addEventListener(pointerEvent||'pointerdown',ev=>{
|
| 1720 |
if(el.disabled)return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1721 |
pointerCommit=now(); fn(ev);
|
| 1722 |
});
|
| 1723 |
el.addEventListener('keydown',ev=>{
|
|
@@ -2009,11 +2080,11 @@ function renderSettings(){
|
|
| 2009 |
const HBD={always:'Bars hover over every visible unit and structure',
|
| 2010 |
select:'Bars appear above selected units and the opened structure',
|
| 2011 |
off:'All battlefield health bars are hidden'};
|
| 2012 |
-
const explorationOn=!!META.settings.experimentalExploration;
|
| 2013 |
const textScale=mfTextScaleValue(META.settings.textScale);
|
| 2014 |
const explorationOpen=explorationOn
|
| 2015 |
-
?'<div class="sItem setRow" data-set="openExperimentalExploration" role="button" tabindex="0"><div class="sTx"><b>
|
| 2016 |
-
+'<div class="sDs">
|
| 2017 |
+'<div class="sBuy togB onT">OPEN</div></div>'
|
| 2018 |
:'';
|
| 2019 |
h+=group('battle','GAMEPLAY & BATTLEFIELD','Information shown while commanding units and optional experimental experiences.',
|
|
@@ -2023,8 +2094,7 @@ function renderSettings(){
|
|
| 2023 |
+tog('shake','Impact Camera Shake','Recoil and explosions move the camera')
|
| 2024 |
+tog('haptics','Haptic Feedback','Short vibration cues for confirmations and impacts')
|
| 2025 |
+tog('experimentalExploration','Experimental: Galactic Campaign',
|
| 2026 |
-
'
|
| 2027 |
-
+'Open it from the row below when you want the preview.')
|
| 2028 |
+explorationOpen);
|
| 2029 |
|
| 2030 |
const perf=META.settings.perf;
|
|
|
|
| 1711 |
pointer timing while adding one guarded keyboard path. Preventing the key's
|
| 1712 |
native click and suppressing a same-turn synthetic click avoids the classic
|
| 1713 |
double activation without making screen-reader click depend on key events. */
|
| 1714 |
+
/* Is this control inside something the player can pan?
|
| 1715 |
+
*
|
| 1716 |
+
* Committing on pointerdown is deliberate for the battle HUD -- an order must
|
| 1717 |
+
* not wait for a finger to lift. But the same rule fires the instant a finger
|
| 1718 |
+
* lands on a button in a SCROLLABLE row, so panning the build/production/
|
| 1719 |
+
* command sub-menus activated whatever the pan happened to start on. Asking
|
| 1720 |
+
* the live layout is better than a marker attribute: only one element in the
|
| 1721 |
+
* whole HUD carries data-mf-scroll-rail, and a row only misfires while it
|
| 1722 |
+
* actually overflows, which changes with content and text scale.
|
| 1723 |
+
*
|
| 1724 |
+
* Checked at press time, not bind time, because a row that fits four cards and
|
| 1725 |
+
* later holds nine becomes pannable without ever being rebound. */
|
| 1726 |
+
function mfPressInsideScroller(el){
|
| 1727 |
+
if(!el||typeof getComputedStyle!=='function') return false;
|
| 1728 |
+
for(let p=el.parentElement,hops=0;p&&hops<8&&p!==document.body;p=p.parentElement,hops++){
|
| 1729 |
+
let cs; try{ cs=getComputedStyle(p); }catch(_){ return false; }
|
| 1730 |
+
const ox=cs.overflowX,oy=cs.overflowY;
|
| 1731 |
+
if((ox==='auto'||ox==='scroll')&&p.scrollWidth>p.clientWidth+2) return true;
|
| 1732 |
+
if((oy==='auto'||oy==='scroll')&&p.scrollHeight>p.clientHeight+2) return true;
|
| 1733 |
+
}
|
| 1734 |
+
return false;
|
| 1735 |
+
}
|
| 1736 |
function mfBindNativePress(el,fn,pointerEvent){
|
| 1737 |
if(!el||typeof fn!=='function'||(el.dataset&&el.dataset.mfNativePress==='1')) return;
|
| 1738 |
if(el.dataset) el.dataset.mfNativePress='1';
|
| 1739 |
const now=typeof mfTapNow==='function'?mfTapNow:()=>performance.now();
|
| 1740 |
+
let pointerCommit=-1e9,keyCommit=-1e9,pan=null;
|
| 1741 |
+
/* Slop matches mfBindTap so the two binders cannot disagree about what
|
| 1742 |
+
counts as a tap on the same screen. */
|
| 1743 |
+
const SLOP=12;
|
| 1744 |
+
/* Held-but-not-yet-committed feedback.
|
| 1745 |
+
*
|
| 1746 |
+
* A deferred control is silent about whether the touch still counts, so a
|
| 1747 |
+
* player panning a row cannot tell the difference between "I am scrolling"
|
| 1748 |
+
* and "I am about to fire this". Light it on contact and extinguish it the
|
| 1749 |
+
* instant the gesture becomes a pan; that teaches the rule in one gesture.
|
| 1750 |
+
*
|
| 1751 |
+
* Deliberately INSTANT and STATIC. Anything that grows or fills reads as a
|
| 1752 |
+
* hold-to-activate progress ring, which would promise something this control
|
| 1753 |
+
* does not do. Inline styles rather than a class because CSS ships only in
|
| 1754 |
+
* the APK and the Space, never over the air. */
|
| 1755 |
+
const arm=on=>{
|
| 1756 |
+
if(!el.style) return;
|
| 1757 |
+
if(on){
|
| 1758 |
+
if(el.dataset&&el.dataset.mfPressLit==='1') return;
|
| 1759 |
+
if(el.dataset) el.dataset.mfPressLit='1';
|
| 1760 |
+
el._mfPressF=el.style.filter; el._mfPressS=el.style.boxShadow;
|
| 1761 |
+
el.style.filter=(el._mfPressF?el._mfPressF+' ':'')+'brightness(1.28)';
|
| 1762 |
+
el.style.boxShadow=(el._mfPressS?el._mfPressS+',':'')+'inset 0 0 0 1px rgba(150,220,255,.85)';
|
| 1763 |
+
}else{
|
| 1764 |
+
if(!el.dataset||el.dataset.mfPressLit!=='1') return;
|
| 1765 |
+
delete el.dataset.mfPressLit;
|
| 1766 |
+
el.style.filter=el._mfPressF||''; el.style.boxShadow=el._mfPressS||'';
|
| 1767 |
+
}
|
| 1768 |
+
};
|
| 1769 |
+
el.addEventListener('pointermove',ev=>{
|
| 1770 |
+
if(!pan||ev.pointerId!==pan.id) return;
|
| 1771 |
+
if(!pan.moved&&Math.hypot(ev.clientX-pan.x,ev.clientY-pan.y)>SLOP){ pan.moved=true; arm(false); }
|
| 1772 |
+
},{passive:true});
|
| 1773 |
+
el.addEventListener('pointercancel',ev=>{ if(pan&&ev.pointerId===pan.id){ pan=null; arm(false); } },{passive:true});
|
| 1774 |
+
el.addEventListener('lostpointercapture',()=>{ if(pan){ pan=null; arm(false); } },{passive:true});
|
| 1775 |
+
el.addEventListener('pointerup',ev=>{
|
| 1776 |
+
if(!pan||ev.pointerId!==pan.id) return;
|
| 1777 |
+
const ok=!pan.moved&&el.contains(ev.target);
|
| 1778 |
+
pan=null; arm(false);
|
| 1779 |
+
if(!ok||el.disabled) return;
|
| 1780 |
+
pointerCommit=now(); fn(ev);
|
| 1781 |
+
});
|
| 1782 |
el.addEventListener(pointerEvent||'pointerdown',ev=>{
|
| 1783 |
if(el.disabled)return;
|
| 1784 |
+
/* Inside a pannable row, wait for the lift and require the finger to have
|
| 1785 |
+
stayed put. Everywhere else keep the immediate commit. */
|
| 1786 |
+
if(!pointerEvent&&mfPressInsideScroller(el)){
|
| 1787 |
+
pan={id:ev.pointerId,x:ev.clientX,y:ev.clientY,moved:false};
|
| 1788 |
+
arm(true);
|
| 1789 |
+
return;
|
| 1790 |
+
}
|
| 1791 |
+
pan=null; arm(false);
|
| 1792 |
pointerCommit=now(); fn(ev);
|
| 1793 |
});
|
| 1794 |
el.addEventListener('keydown',ev=>{
|
|
|
|
| 2080 |
const HBD={always:'Bars hover over every visible unit and structure',
|
| 2081 |
select:'Bars appear above selected units and the opened structure',
|
| 2082 |
off:'All battlefield health bars are hidden'};
|
| 2083 |
+
const explorationOn=window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true||!!META.settings.experimentalExploration;
|
| 2084 |
const textScale=mfTextScaleValue(META.settings.textScale);
|
| 2085 |
const explorationOpen=explorationOn
|
| 2086 |
+
?'<div class="sItem setRow" data-set="openExperimentalExploration" role="button" tabindex="0"><div class="sTx"><b>Return to UGA Headquarters</b>'
|
| 2087 |
+
+'<div class="sDs">Manage ship sections, prepare expeditions, or choose a mode at the War Table.</div></div>'
|
| 2088 |
+'<div class="sBuy togB onT">OPEN</div></div>'
|
| 2089 |
:'';
|
| 2090 |
h+=group('battle','GAMEPLAY & BATTLEFIELD','Information shown while commanding units and optional experimental experiences.',
|
|
|
|
| 2094 |
+tog('shake','Impact Camera Shake','Recoil and explosions move the camera')
|
| 2095 |
+tog('haptics','Haptic Feedback','Short vibration cues for confirmations and impacts')
|
| 2096 |
+tog('experimentalExploration','Experimental: Galactic Campaign',
|
| 2097 |
+
'The UGA is your shared strategic layer. This setting enables Galactic entry on older packages; current builds include it.')
|
|
|
|
| 2098 |
+explorationOpen);
|
| 2099 |
|
| 2100 |
const perf=META.settings.perf;
|
src/game/sim.js
CHANGED
|
@@ -1683,7 +1683,9 @@ function mfNavPass(i,naval,clearance){
|
|
| 1683 |
function mfNavDirectApproachClear(x0,y0,x1,y1,T){
|
| 1684 |
const naval=!!(T&&T.naval),clearance=mfNavUnitClearance(T),grid=mfNavClearanceGrid(naval),
|
| 1685 |
need=MF_NAV_CLEARANCE_COST[mfNavClearanceToken(clearance,naval)],cell=MAP/PGS,
|
| 1686 |
-
dx=x1-x0,dy=y1-y0,steps=Math.max(1,Math.
|
|
|
|
|
|
|
| 1687 |
for(let s=1;s<=steps;s++){
|
| 1688 |
const t=s/steps,c=ffCell(x0+dx*t,y0+dy*t);
|
| 1689 |
if(!grid||grid[c]<=need)return false;
|
|
@@ -1941,6 +1943,55 @@ function requestField(tx,ty,naval,clearance,defer){
|
|
| 1941 |
if(defer)mfNavQueueBuild(fields[f]);
|
| 1942 |
return f;
|
| 1943 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1944 |
function mfNavFindAttackBlocker(i,gx,gy){
|
| 1945 |
const T=TYPES[utype[i]];if(!T||!(T.dmg>0)||T.air)return -1;
|
| 1946 |
const ax=ux[i],ay=uy[i],vx=gx-ax,vy=gy-ay,ll=vx*vx+vy*vy;if(ll<1)return -1;
|
|
@@ -2778,7 +2829,7 @@ function addBld(type,team,x,y,instant,rot,suppressPackageGrant){
|
|
| 2778 |
const hpM=(team===0?bldHpMult*doctrineHp*resBldHpMult:1);
|
| 2779 |
const shieldMax=type==='techlab'?900*labBufferMult:0;
|
| 2780 |
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,
|
| 2781 |
-
cool:0,queue:[],repeat:false,prodT:0,heal:0,tier:1,lvl:1,upT:0,upMax:1,tang:team?Math.PI:0,gunPitch:0,
|
| 2782 |
seen:false,boost:0,boostM:UPLINK_BOOST,res:-1,resT:0,rally:null,rich:false,dep:-1,geo:-1,
|
| 2783 |
shield:instant?shieldMax:0,shieldMax,shieldT:0,dmgT:0,
|
| 2784 |
repairOn:false,repairStalled:false,
|
|
@@ -8834,13 +8885,14 @@ function unitTick(dt){
|
|
| 8834 |
: (umarch[i]===1) ? distGoal>arriveR
|
| 8835 |
: (engaging ? (!inRange || er > T.rng*rngM*0.92+arriveR) : distGoal>arriveR);
|
| 8836 |
if(T.air&&typeof mfAirShouldMove==='function') wantMove=!uhold[i]&&spdM>0&&mfAirShouldMove(i);
|
|
|
|
| 8837 |
if(wantMove && distGoal>0.001){
|
| 8838 |
// ground units make better time on the old highways
|
| 8839 |
const rd=(!T.air&&!T.naval&&roadAt(ux[i],uy[i]))?ROAD_SPD:1;
|
| 8840 |
const sp=T.spd*spdM*classSpdMul(i)*broodSpdMul(i)*mfDomainSpeedMul(i)*(ubuff[i]>0?1.35:1)*(uhaz[i]>0?HAZ_SPD:1)*rd*uCohesion[i]*Math.min(1,distGoal/(arriveR+12)+0.25);
|
| 8841 |
moveCap=sp;
|
| 8842 |
// flow-field steering for long marches (routes armies around lakes)
|
| 8843 |
-
let ffOk=false,
|
| 8844 |
/* Formation orders share a coarse field to the leg centre, then fan out
|
| 8845 |
early enough to settle into their own slots before the turn. */
|
| 8846 |
/* Formations fan into their assigned lanes before the last turn. At 170
|
|
@@ -8848,31 +8900,39 @@ function unitTick(dt){
|
|
| 8848 |
460 wu final approach preserves the shell mapping while long marches
|
| 8849 |
retain the shared field around strategic terrain. */
|
| 8850 |
const slotApproach=uMoveCohort[i]>=0?460:(uPatrolRoute[i]>=0?170:70);
|
| 8851 |
-
const directApproachClear=
|
| 8852 |
mfNavDirectApproachClear(ux[i],uy[i],gx,gy,T);
|
| 8853 |
-
if(!
|
| 8854 |
-
|
| 8855 |
-
|
| 8856 |
-
if(F&&F.dirs&&!!F.naval===!!T.naval){
|
| 8857 |
const k=F.dirs[ffCell(ux[i],uy[i])];
|
| 8858 |
if(k<8){
|
| 8859 |
const inv=1/Math.hypot(DIRX[k],DIRY[k]);
|
| 8860 |
mvx=DIRX[k]*inv*sp; mvy=DIRY[k]*inv*sp;
|
| 8861 |
ffOk=true;
|
| 8862 |
}else{
|
| 8863 |
-
|
|
|
|
|
|
|
| 8864 |
if(P){const dx=P.x-ux[i],dy=P.y-uy[i],dl=Math.hypot(dx,dy)||1;mvx=dx/dl*sp;mvy=dy/dl*sp;ffOk=true;}
|
| 8865 |
-
else
|
| 8866 |
}
|
| 8867 |
-
}
|
| 8868 |
}
|
| 8869 |
if(!ffOk){
|
| 8870 |
-
if(
|
| 8871 |
-
|
| 8872 |
-
|
| 8873 |
-
const B=blds[b];utgt[i]=-2-b;utgtg[i]=-1;ustate[i]=2;umarch[i]=0;utx[i]=B.x;uty[i]=B.y;ufield[i]=-1;
|
| 8874 |
-
}else{ustate[i]=0;utgt[i]=-1;utgtg[i]=-1;ufield[i]=-1;umarch[i]=0;utx[i]=ux[i];uty[i]=uy[i];}
|
| 8875 |
mvx=0;mvy=0;moving=false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8876 |
}else{mvx=(gx-ux[i])/distGoal*sp;mvy=(gy-uy[i])/distGoal*sp;moving=true;}
|
| 8877 |
}else moving=true;
|
| 8878 |
}
|
|
@@ -9441,6 +9501,21 @@ function mfBldCachedEnemy(B,range,domain,dt){
|
|
| 9441 |
}
|
| 9442 |
return e;
|
| 9443 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9444 |
function bldTick(dt){
|
| 9445 |
for(let b=0;b<blds.length;b++){
|
| 9446 |
const B=blds[b]; if(!B.alive) continue;
|
|
@@ -10020,16 +10095,9 @@ function bldTick(dt){
|
|
| 10020 |
if(i<0){B.prodStalled='population';continue;}
|
| 10021 |
B.prodT=0; B.queue.shift();
|
| 10022 |
if(B.repeat) B.queue.push(t);
|
|
|
|
| 10023 |
ustate[i]=2;
|
| 10024 |
-
|
| 10025 |
-
if(B.team===0 && B.rally){ // player rally point
|
| 10026 |
-
rx=clamp(B.rally.x+mfSimRange(-26,26),20,MAP-20);
|
| 10027 |
-
ry=clamp(B.rally.y+mfSimRange(-26,26),20,MAP-20);
|
| 10028 |
-
} else {
|
| 10029 |
-
rx=clamp(B.x+(B.team===0?mfSimRange(60,120):mfSimRange(-120,-60)),20,MAP-20);
|
| 10030 |
-
ry=clamp(B.y+(B.team===0?mfSimRange(60,120):mfSimRange(-120,-60)),20,MAP-20);
|
| 10031 |
-
}
|
| 10032 |
-
const L=TYPES[t].naval? (findWater(rx,ry)||[ux[i],uy[i]]) : findLand(rx,ry);
|
| 10033 |
utx[i]=L[0]; uty[i]=L[1];
|
| 10034 |
if(!TYPES[t].air&&dist2(ux[i],uy[i],L[0],L[1])>70*70)
|
| 10035 |
ufield[i]=requestField(L[0],L[1],!!TYPES[t].naval,mfNavUnitClearance(TYPES[t]));
|
|
|
|
| 1683 |
function mfNavDirectApproachClear(x0,y0,x1,y1,T){
|
| 1684 |
const naval=!!(T&&T.naval),clearance=mfNavUnitClearance(T),grid=mfNavClearanceGrid(naval),
|
| 1685 |
need=MF_NAV_CLEARANCE_COST[mfNavClearanceToken(clearance,naval)],cell=MAP/PGS,
|
| 1686 |
+
dx=x1-x0,dy=y1-y0,steps=Math.max(1,Math.ceil(Math.hypot(dx,dy)/(cell*.72)));
|
| 1687 |
+
// A bounded probe must fail closed rather than skip thin walls on long legs.
|
| 1688 |
+
if(steps>64)return false;
|
| 1689 |
for(let s=1;s<=steps;s++){
|
| 1690 |
const t=s/steps,c=ffCell(x0+dx*t,y0+dy*t);
|
| 1691 |
if(!grid||grid[c]<=need)return false;
|
|
|
|
| 1943 |
if(defer)mfNavQueueBuild(fields[f]);
|
| 1944 |
return f;
|
| 1945 |
}
|
| 1946 |
+
const uNavProgressGen=new Uint32Array(MAXU),uNavProgressRevision=new Uint32Array(MAXU),uNavGoalX=new Float32Array(MAXU),uNavGoalY=new Float32Array(MAXU),
|
| 1947 |
+
uNavProbeX=new Float32Array(MAXU),uNavProbeY=new Float32Array(MAXU),uNavProbeAge=new Float32Array(MAXU),uNavRetryFor=new Float32Array(MAXU);
|
| 1948 |
+
const uNavPursuitField=new Uint8Array(MAXU);
|
| 1949 |
+
function mfNavProgressReset(){
|
| 1950 |
+
for(const a of [uNavProgressGen,uNavProgressRevision,uNavGoalX,uNavGoalY,uNavProbeX,uNavProbeY,uNavProbeAge,uNavRetryFor,uNavPursuitField])a.fill(0);
|
| 1951 |
+
}
|
| 1952 |
+
function mfNavProgressRepath(i,T,gx,gy,dt,wantMove){
|
| 1953 |
+
uNavRetryFor[i]=Math.max(0,uNavRetryFor[i]-dt);
|
| 1954 |
+
const newUnit=uNavProgressGen[i]!==ugen[i],fresh=newUnit||uNavProgressRevision[i]!==mfMoveBlockRevision;
|
| 1955 |
+
if(fresh||!wantMove||dist2(uNavGoalX[i],uNavGoalY[i],gx,gy)>32*32){
|
| 1956 |
+
uNavProgressGen[i]=ugen[i];uNavProgressRevision[i]=mfMoveBlockRevision;uNavGoalX[i]=gx;uNavGoalY[i]=gy;
|
| 1957 |
+
uNavProbeX[i]=ux[i];uNavProbeY[i]=uy[i];uNavProbeAge[i]=0;
|
| 1958 |
+
if(fresh)uNavRetryFor[i]=0;
|
| 1959 |
+
if(newUnit)uNavPursuitField[i]=0;
|
| 1960 |
+
return false;
|
| 1961 |
+
}
|
| 1962 |
+
uNavProbeAge[i]+=dt;if(uNavProbeAge[i]<1.25)return false;
|
| 1963 |
+
// Net displacement, not velocity intent or accumulated crowd jitter. A
|
| 1964 |
+
// legitimate detour counts as progress even while it moves away from goal.
|
| 1965 |
+
const moved=Math.hypot(ux[i]-uNavProbeX[i],uy[i]-uNavProbeY[i]);
|
| 1966 |
+
uNavProbeAge[i]=0;uNavProbeX[i]=ux[i];uNavProbeY[i]=uy[i];
|
| 1967 |
+
return moved<Math.max(.75,Math.min(4,T.spd*.08));
|
| 1968 |
+
}
|
| 1969 |
+
function mfNavFieldForMove(i,T,gx,gy,engaging,repath){
|
| 1970 |
+
let F=fields[ufield[i]];
|
| 1971 |
+
const resumeStrategic=!engaging&&uNavPursuitField[i]===1;
|
| 1972 |
+
uNavPursuitField[i]=engaging?1:0;
|
| 1973 |
+
const mismatch=resumeStrategic||!F||!!F.naval!==!!T.naval||F.clearance!==mfNavUnitClearance(T)||
|
| 1974 |
+
(engaging&&dist2(F.tx,F.ty,gx,gy)>70*70);
|
| 1975 |
+
if(mismatch){
|
| 1976 |
+
// A completed chase must not leave the unit following that enemy's field
|
| 1977 |
+
// forever. Reacquire the retained order once; requestField still coalesces
|
| 1978 |
+
// nearby goals without imposing a new formation layout.
|
| 1979 |
+
if(F||resumeStrategic)uNavRetryFor[i]=0;
|
| 1980 |
+
ufield[i]=-1;
|
| 1981 |
+
if(uNavRetryFor[i]>0)return null;
|
| 1982 |
+
uNavRetryFor[i]=.75;
|
| 1983 |
+
ufield[i]=requestField(gx,gy,!!T.naval,mfNavUnitClearance(T),true);
|
| 1984 |
+
F=fields[ufield[i]];
|
| 1985 |
+
}
|
| 1986 |
+
if(!F)return null;
|
| 1987 |
+
F=mfMoveFieldFresh(F);
|
| 1988 |
+
// Multiple blocked units share a field. One stalled member must not restart
|
| 1989 |
+
// its queued job every frame and prevent the rest of the army receiving it.
|
| 1990 |
+
if(repath&&!F.pending&&(F.repathTick==null||tick<F.repathTick||tick-F.repathTick>=90)){
|
| 1991 |
+
F.repathTick=tick;F.rev=0;mfNavQueueBuild(F);
|
| 1992 |
+
}
|
| 1993 |
+
return F;
|
| 1994 |
+
}
|
| 1995 |
function mfNavFindAttackBlocker(i,gx,gy){
|
| 1996 |
const T=TYPES[utype[i]];if(!T||!(T.dmg>0)||T.air)return -1;
|
| 1997 |
const ax=ux[i],ay=uy[i],vx=gx-ax,vy=gy-ay,ll=vx*vx+vy*vy;if(ll<1)return -1;
|
|
|
|
| 2829 |
const hpM=(team===0?bldHpMult*doctrineHp*resBldHpMult:1);
|
| 2830 |
const shieldMax=type==='techlab'?900*labBufferMult:0;
|
| 2831 |
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,
|
| 2832 |
+
cool:0,queue:[],queueRevision:0,repeat:false,prodT:0,heal:0,tier:1,lvl:1,upT:0,upMax:1,tang:team?Math.PI:0,gunPitch:0,
|
| 2833 |
seen:false,boost:0,boostM:UPLINK_BOOST,res:-1,resT:0,rally:null,rich:false,dep:-1,geo:-1,
|
| 2834 |
shield:instant?shieldMax:0,shieldMax,shieldT:0,dmgT:0,
|
| 2835 |
repairOn:false,repairStalled:false,
|
|
|
|
| 8885 |
: (umarch[i]===1) ? distGoal>arriveR
|
| 8886 |
: (engaging ? (!inRange || er > T.rng*rngM*0.92+arriveR) : distGoal>arriveR);
|
| 8887 |
if(T.air&&typeof mfAirShouldMove==='function') wantMove=!uhold[i]&&spdM>0&&mfAirShouldMove(i);
|
| 8888 |
+
const navRepath=!T.air&&mfNavProgressRepath(i,T,gx,gy,dt,wantMove);
|
| 8889 |
if(wantMove && distGoal>0.001){
|
| 8890 |
// ground units make better time on the old highways
|
| 8891 |
const rd=(!T.air&&!T.naval&&roadAt(ux[i],uy[i]))?ROAD_SPD:1;
|
| 8892 |
const sp=T.spd*spdM*classSpdMul(i)*broodSpdMul(i)*mfDomainSpeedMul(i)*(ubuff[i]>0?1.35:1)*(uhaz[i]>0?HAZ_SPD:1)*rd*uCohesion[i]*Math.min(1,distGoal/(arriveR+12)+0.25);
|
| 8893 |
moveCap=sp;
|
| 8894 |
// flow-field steering for long marches (routes armies around lakes)
|
| 8895 |
+
let ffOk=false,routeUnreachable=false;
|
| 8896 |
/* Formation orders share a coarse field to the leg centre, then fan out
|
| 8897 |
early enough to settle into their own slots before the turn. */
|
| 8898 |
/* Formations fan into their assigned lanes before the last turn. At 170
|
|
|
|
| 8900 |
460 wu final approach preserves the shell mapping while long marches
|
| 8901 |
retain the shared field around strategic terrain. */
|
| 8902 |
const slotApproach=uMoveCohort[i]>=0?460:(uPatrolRoute[i]>=0?170:70);
|
| 8903 |
+
const directApproachClear=T.air||
|
| 8904 |
mfNavDirectApproachClear(ux[i],uy[i],gx,gy,T);
|
| 8905 |
+
if(!T.air && (distGoal>slotApproach||!directApproachClear||navRepath)){
|
| 8906 |
+
const F=mfNavFieldForMove(i,T,gx,gy,engaging,navRepath);
|
| 8907 |
+
if(F&&!F.pending&&F.rev===mfMoveBlockRevision&&F.dirs&&!!F.naval===!!T.naval){
|
|
|
|
| 8908 |
const k=F.dirs[ffCell(ux[i],uy[i])];
|
| 8909 |
if(k<8){
|
| 8910 |
const inv=1/Math.hypot(DIRX[k],DIRY[k]);
|
| 8911 |
mvx=DIRX[k]*inv*sp; mvy=DIRY[k]*inv*sp;
|
| 8912 |
ffOk=true;
|
| 8913 |
}else{
|
| 8914 |
+
// Exact reachability outranks coarse portal links. Escape hints
|
| 8915 |
+
// remain useful only for spawns outside the clearance graph.
|
| 8916 |
+
const P=!mfNavPass(ffCell(ux[i],uy[i]),F.naval,F.clearance)?mfNavSectorWaypoint(F,ux[i],uy[i]):null;
|
| 8917 |
if(P){const dx=P.x-ux[i],dy=P.y-uy[i],dl=Math.hypot(dx,dy)||1;mvx=dx/dl*sp;mvy=dy/dl*sp;ffOk=true;}
|
| 8918 |
+
else routeUnreachable=true;
|
| 8919 |
}
|
| 8920 |
+
}
|
| 8921 |
}
|
| 8922 |
if(!ffOk){
|
| 8923 |
+
if(!directApproachClear){
|
| 8924 |
+
// Pending, stale or disconnected is not arrival. Retain the player's
|
| 8925 |
+
// destination/queue and retry after progress timeout or map revision.
|
|
|
|
|
|
|
| 8926 |
mvx=0;mvy=0;moving=false;
|
| 8927 |
+
if(routeUnreachable&&ustate[i]===2){
|
| 8928 |
+
const blocker=mfNavFindAttackBlocker(i,gx,gy);
|
| 8929 |
+
if(blocker>=0){
|
| 8930 |
+
// Breach an actual enemy obstruction only after a completed
|
| 8931 |
+
// route proves disconnected. Keep the strategic order intact
|
| 8932 |
+
// so destruction of this temporary target resumes the march.
|
| 8933 |
+
utgt[i]=-2-blocker;utgtg[i]=-1;umarch[i]=0;
|
| 8934 |
+
}
|
| 8935 |
+
}
|
| 8936 |
}else{mvx=(gx-ux[i])/distGoal*sp;mvy=(gy-uy[i])/distGoal*sp;moving=true;}
|
| 8937 |
}else moving=true;
|
| 8938 |
}
|
|
|
|
| 9501 |
}
|
| 9502 |
return e;
|
| 9503 |
}
|
| 9504 |
+
function mfFactoryRallyGoal(B,T,spawnX,spawnY){
|
| 9505 |
+
let rx,ry;
|
| 9506 |
+
if(B.rally&&Number.isFinite(B.rally.x)&&Number.isFinite(B.rally.y)){
|
| 9507 |
+
// Opposing human seats own factories too; a team-0 gate discarded their
|
| 9508 |
+
// authoritative marker. AI factories without a marker retain the fallback.
|
| 9509 |
+
// The marker is the order, not the centre of a random 52wu square.
|
| 9510 |
+
// Collision separation and the hull-sized arrival band own final spacing.
|
| 9511 |
+
rx=clamp(B.rally.x,20,MAP-20);ry=clamp(B.rally.y,20,MAP-20);
|
| 9512 |
+
}else{
|
| 9513 |
+
rx=clamp(B.x+(B.team===0?mfSimRange(60,120):mfSimRange(-120,-60)),20,MAP-20);
|
| 9514 |
+
ry=clamp(B.y+(B.team===0?mfSimRange(60,120):mfSimRange(-120,-60)),20,MAP-20);
|
| 9515 |
+
}
|
| 9516 |
+
if(T.air)return [rx,ry];
|
| 9517 |
+
return T.naval?(findWater(rx,ry)||[spawnX,spawnY]):findLand(rx,ry);
|
| 9518 |
+
}
|
| 9519 |
function bldTick(dt){
|
| 9520 |
for(let b=0;b<blds.length;b++){
|
| 9521 |
const B=blds[b]; if(!B.alive) continue;
|
|
|
|
| 10095 |
if(i<0){B.prodStalled='population';continue;}
|
| 10096 |
B.prodT=0; B.queue.shift();
|
| 10097 |
if(B.repeat) B.queue.push(t);
|
| 10098 |
+
B.queueRevision=(B.queueRevision||0)+1;
|
| 10099 |
ustate[i]=2;
|
| 10100 |
+
const L=mfFactoryRallyGoal(B,T,ux[i],uy[i]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10101 |
utx[i]=L[0]; uty[i]=L[1];
|
| 10102 |
if(!TYPES[t].air&&dist2(ux[i],uy[i],L[0],L[1])>70*70)
|
| 10103 |
ufield[i]=requestField(L[0],L[1],!!TYPES[t].naval,mfNavUnitClearance(TYPES[t]));
|
src/game/statehash.js
CHANGED
|
@@ -66,8 +66,14 @@ function MF_SH_snapshot(){
|
|
| 66 |
for(const x of ua)MF_SH_indexed(w,x[0],x[1],units);
|
| 67 |
const opt=[['uUtilityJob',typeof uUtilityJob!=='undefined'?uUtilityJob:null],['uUtilityGoalX',typeof uUtilityGoalX!=='undefined'?uUtilityGoalX:null],['uUtilityGoalY',typeof uUtilityGoalY!=='undefined'?uUtilityGoalY:null],['uUtilityAuto',typeof uUtilityAuto!=='undefined'?uUtilityAuto:null],['uUtilityProgressX',typeof uUtilityProgressX!=='undefined'?uUtilityProgressX:null],['uUtilityProgressY',typeof uUtilityProgressY!=='undefined'?uUtilityProgressY:null],['uUtilityProgressAt',typeof uUtilityProgressAt!=='undefined'?uUtilityProgressAt:null],['uUtilityRetryAt',typeof uUtilityRetryAt!=='undefined'?uUtilityRetryAt:null],['uAirGen',typeof uAirGen!=='undefined'?uAirGen:null],['uAirTarget',typeof uAirTarget!=='undefined'?uAirTarget:null],['uAirTargetG',typeof uAirTargetG!=='undefined'?uAirTargetG:null],['uAirEscort',typeof uAirEscort!=='undefined'?uAirEscort:null],['uAirEscortG',typeof uAirEscortG!=='undefined'?uAirEscortG:null],['uAirBand',typeof uAirBand!=='undefined'?uAirBand:null],['uAirBandReq',typeof uAirBandReq!=='undefined'?uAirBandReq:null],['uAirMission',typeof uAirMission!=='undefined'?uAirMission:null],['uAirHomeMission',typeof uAirHomeMission!=='undefined'?uAirHomeMission:null],['uAirPhase',typeof uAirPhase!=='undefined'?uAirPhase:null],['uAirFire',typeof uAirFire!=='undefined'?uAirFire:null],['uAirScanClock',typeof uAirScanClock!=='undefined'?uAirScanClock:null],['uAirAlt',typeof uAirAlt!=='undefined'?uAirAlt:null],['uAirPhaseT',typeof uAirPhaseT!=='undefined'?uAirPhaseT:null],['uAirAnchorX',typeof uAirAnchorX!=='undefined'?uAirAnchorX:null],['uAirAnchorY',typeof uAirAnchorY!=='undefined'?uAirAnchorY:null],['uAirGoalX',typeof uAirGoalX!=='undefined'?uAirGoalX:null],['uAirGoalY',typeof uAirGoalY!=='undefined'?uAirGoalY:null],['uAirAimX',typeof uAirAimX!=='undefined'?uAirAimX:null],['uAirAimY',typeof uAirAimY!=='undefined'?uAirAimY:null],['uAirVx',typeof uAirVx!=='undefined'?uAirVx:null],['uAirVy',typeof uAirVy!=='undefined'?uAirVy:null],['uAirCourse',typeof uAirCourse!=='undefined'?uAirCourse:null],['uAirYawRate',typeof uAirYawRate!=='undefined'?uAirYawRate:null],['uAirPass',typeof uAirPass!=='undefined'?uAirPass:null],['uAirReleaseN',typeof uAirReleaseN!=='undefined'?uAirReleaseN:null],['uAirReacquireN',typeof uAirReacquireN!=='undefined'?uAirReacquireN:null],['uAirSortie',typeof uAirSortie!=='undefined'?uAirSortie:null]];
|
| 68 |
for(const x of opt)if(x[1])MF_SH_indexed(w,x[0],x[1],units);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
MF_SH_named(w,'unitQueues',units.map(i=>[i,uQueue[i]||null]));MF_SH_named(w,'patrolRoutes',typeof patrolRoutes!=='undefined'?patrolRoutes:[]);MF_SH_named(w,'moveCohorts',typeof moveCohorts!=='undefined'?moveCohorts:[]);
|
| 70 |
-
const bf=['type','team','fac','x','y','hp','hpm','r','alive','prog','cool','queue','repeat','prodT','heal','tier','lvl','upT','upMax','tang','gunPitch','boost','boostM','res','resT','rally','rich','dep','geo','shield','shieldMax','shieldT','guardReady','guardT','guardCharge','rot','footTier','link','conduit','buildPaidM','buildPaidE','buildStalled','repairOn','dmgT','aim','aimG','target','targetG','mode','state','stun','haz'];
|
| 71 |
MF_SH_named(w,'buildings',blds.map((b,i)=>[i,MF_SH_fields(b,bf)]));MF_SH_named(w,'economy',{resM:typeof resM!=='undefined'?resM:[],resE:typeof resE!=='undefined'?resE:[],mSpendAcc:typeof mSpendAcc!=='undefined'?mSpendAcc:0,eSpendAcc:typeof eSpendAcc!=='undefined'?eSpendAcc:0,spendT:typeof spendT!=='undefined'?spendT:0,stats:typeof stats!=='undefined'?stats:{},researched:typeof researched!=='undefined'?researched:{},researchCarry:typeof researchCarry!=='undefined'?researchCarry:{},researching:typeof researching!=='undefined'?researching:null});
|
| 72 |
MF_SH_named(w,'pHigh',pHigh);MF_SH_named(w,'projectileFree',typeof pFree!=='undefined'?pFree:[]);
|
| 73 |
const pa=[['px',px],['py',py],['pvx',pvx],['pvy',pvy],['plife',plife],['pdmg',pdmg],['paoe',paoe],['ptype',ptype],['pteam',pteam],['ptgt',ptgt],['ptgtg',ptgtg],['pmu0',pmu0],['pwk',pwk],['psx',psx],['psy',psy],['pex',pex],['pey',pey],['pt',pt],['pmax',pmax],['pSplit',pSplit],['pCannon',pCannon],['pBio',pBio],['pBarrage',pBarrage],['pFlightId',pFlightId],['pBaseSpeed',pBaseSpeed],['pSpeed',pSpeed],['pAge',pAge],['pSrcUnit',pSrcUnit],['pSrcGen',pSrcGen],['pArc',pArc],['pz0',pz0],['pz1',pz1],['pz',pz],['pLastTX',pLastTX],['pLastTY',pLastTY]];for(const x of pa)MF_SH_indexed(w,x[0],x[1],shots);
|
|
|
|
| 66 |
for(const x of ua)MF_SH_indexed(w,x[0],x[1],units);
|
| 67 |
const opt=[['uUtilityJob',typeof uUtilityJob!=='undefined'?uUtilityJob:null],['uUtilityGoalX',typeof uUtilityGoalX!=='undefined'?uUtilityGoalX:null],['uUtilityGoalY',typeof uUtilityGoalY!=='undefined'?uUtilityGoalY:null],['uUtilityAuto',typeof uUtilityAuto!=='undefined'?uUtilityAuto:null],['uUtilityProgressX',typeof uUtilityProgressX!=='undefined'?uUtilityProgressX:null],['uUtilityProgressY',typeof uUtilityProgressY!=='undefined'?uUtilityProgressY:null],['uUtilityProgressAt',typeof uUtilityProgressAt!=='undefined'?uUtilityProgressAt:null],['uUtilityRetryAt',typeof uUtilityRetryAt!=='undefined'?uUtilityRetryAt:null],['uAirGen',typeof uAirGen!=='undefined'?uAirGen:null],['uAirTarget',typeof uAirTarget!=='undefined'?uAirTarget:null],['uAirTargetG',typeof uAirTargetG!=='undefined'?uAirTargetG:null],['uAirEscort',typeof uAirEscort!=='undefined'?uAirEscort:null],['uAirEscortG',typeof uAirEscortG!=='undefined'?uAirEscortG:null],['uAirBand',typeof uAirBand!=='undefined'?uAirBand:null],['uAirBandReq',typeof uAirBandReq!=='undefined'?uAirBandReq:null],['uAirMission',typeof uAirMission!=='undefined'?uAirMission:null],['uAirHomeMission',typeof uAirHomeMission!=='undefined'?uAirHomeMission:null],['uAirPhase',typeof uAirPhase!=='undefined'?uAirPhase:null],['uAirFire',typeof uAirFire!=='undefined'?uAirFire:null],['uAirScanClock',typeof uAirScanClock!=='undefined'?uAirScanClock:null],['uAirAlt',typeof uAirAlt!=='undefined'?uAirAlt:null],['uAirPhaseT',typeof uAirPhaseT!=='undefined'?uAirPhaseT:null],['uAirAnchorX',typeof uAirAnchorX!=='undefined'?uAirAnchorX:null],['uAirAnchorY',typeof uAirAnchorY!=='undefined'?uAirAnchorY:null],['uAirGoalX',typeof uAirGoalX!=='undefined'?uAirGoalX:null],['uAirGoalY',typeof uAirGoalY!=='undefined'?uAirGoalY:null],['uAirAimX',typeof uAirAimX!=='undefined'?uAirAimX:null],['uAirAimY',typeof uAirAimY!=='undefined'?uAirAimY:null],['uAirVx',typeof uAirVx!=='undefined'?uAirVx:null],['uAirVy',typeof uAirVy!=='undefined'?uAirVy:null],['uAirCourse',typeof uAirCourse!=='undefined'?uAirCourse:null],['uAirYawRate',typeof uAirYawRate!=='undefined'?uAirYawRate:null],['uAirPass',typeof uAirPass!=='undefined'?uAirPass:null],['uAirReleaseN',typeof uAirReleaseN!=='undefined'?uAirReleaseN:null],['uAirReacquireN',typeof uAirReacquireN!=='undefined'?uAirReacquireN:null],['uAirSortie',typeof uAirSortie!=='undefined'?uAirSortie:null]];
|
| 68 |
for(const x of opt)if(x[1])MF_SH_indexed(w,x[0],x[1],units);
|
| 69 |
+
const nav=[['uNavProgressGen',typeof uNavProgressGen!=='undefined'?uNavProgressGen:null],
|
| 70 |
+
['uNavGoalX',typeof uNavGoalX!=='undefined'?uNavGoalX:null],['uNavGoalY',typeof uNavGoalY!=='undefined'?uNavGoalY:null],
|
| 71 |
+
['uNavProbeX',typeof uNavProbeX!=='undefined'?uNavProbeX:null],['uNavProbeY',typeof uNavProbeY!=='undefined'?uNavProbeY:null],
|
| 72 |
+
['uNavProbeAge',typeof uNavProbeAge!=='undefined'?uNavProbeAge:null],['uNavRetryFor',typeof uNavRetryFor!=='undefined'?uNavRetryFor:null],
|
| 73 |
+
['uNavPursuitField',typeof uNavPursuitField!=='undefined'?uNavPursuitField:null]];
|
| 74 |
+
for(const x of nav)if(x[1])MF_SH_indexed(w,x[0],x[1],units);
|
| 75 |
MF_SH_named(w,'unitQueues',units.map(i=>[i,uQueue[i]||null]));MF_SH_named(w,'patrolRoutes',typeof patrolRoutes!=='undefined'?patrolRoutes:[]);MF_SH_named(w,'moveCohorts',typeof moveCohorts!=='undefined'?moveCohorts:[]);
|
| 76 |
+
const bf=['type','team','fac','x','y','hp','hpm','r','alive','prog','cool','queue','queueRevision','repeat','prodT','heal','tier','lvl','upT','upMax','tang','gunPitch','boost','boostM','res','resT','rally','rich','dep','geo','shield','shieldMax','shieldT','guardReady','guardT','guardCharge','rot','footTier','link','conduit','buildPaidM','buildPaidE','buildStalled','repairOn','dmgT','aim','aimG','target','targetG','mode','state','stun','haz'];
|
| 77 |
MF_SH_named(w,'buildings',blds.map((b,i)=>[i,MF_SH_fields(b,bf)]));MF_SH_named(w,'economy',{resM:typeof resM!=='undefined'?resM:[],resE:typeof resE!=='undefined'?resE:[],mSpendAcc:typeof mSpendAcc!=='undefined'?mSpendAcc:0,eSpendAcc:typeof eSpendAcc!=='undefined'?eSpendAcc:0,spendT:typeof spendT!=='undefined'?spendT:0,stats:typeof stats!=='undefined'?stats:{},researched:typeof researched!=='undefined'?researched:{},researchCarry:typeof researchCarry!=='undefined'?researchCarry:{},researching:typeof researching!=='undefined'?researching:null});
|
| 78 |
MF_SH_named(w,'pHigh',pHigh);MF_SH_named(w,'projectileFree',typeof pFree!=='undefined'?pFree:[]);
|
| 79 |
const pa=[['px',px],['py',py],['pvx',pvx],['pvy',pvy],['plife',plife],['pdmg',pdmg],['paoe',paoe],['ptype',ptype],['pteam',pteam],['ptgt',ptgt],['ptgtg',ptgtg],['pmu0',pmu0],['pwk',pwk],['psx',psx],['psy',psy],['pex',pex],['pey',pey],['pt',pt],['pmax',pmax],['pSplit',pSplit],['pCannon',pCannon],['pBio',pBio],['pBarrage',pBarrage],['pFlightId',pFlightId],['pBaseSpeed',pBaseSpeed],['pSpeed',pSpeed],['pAge',pAge],['pSrcUnit',pSrcUnit],['pSrcGen',pSrcGen],['pArc',pArc],['pz0',pz0],['pz1',pz1],['pz',pz],['pLastTX',pLastTX],['pLastTY',pLastTY]];for(const x of pa)MF_SH_indexed(w,x[0],x[1],shots);
|
src/intro.js
CHANGED
|
@@ -82,6 +82,7 @@
|
|
| 82 |
that never opened. Once only β revealFront can be reached twice. */
|
| 83 |
if(!gateFired){
|
| 84 |
gateFired=true;
|
|
|
|
| 85 |
setTimeout(function(){
|
| 86 |
if(typeof mfAuthGate==='function'){ try{ mfAuthGate(); }catch(e){} }
|
| 87 |
},340);
|
|
@@ -181,7 +182,7 @@
|
|
| 181 |
shape may bypass the reveal; ordinary launches and look-alike query
|
| 182 |
strings still receive the title screen. The route owner validates and
|
| 183 |
consumes the secured session record later in galactic-operations.js. */
|
| 184 |
-
try{return /^\?galacticRoute=[A-Za-z0-9_-]{16,128}$/.test(String(location.search||''));}
|
| 185 |
catch(e){return false;}
|
| 186 |
}
|
| 187 |
function dismissIntroForGalacticRoute(){
|
|
@@ -223,7 +224,13 @@
|
|
| 223 |
established commander returning to the game. */
|
| 224 |
/* Open on the next frame, not after a 650ms delay: the menu must never
|
| 225 |
be visible first. The boot cover + intro.css keep it hidden until then. */
|
| 226 |
-
if(typeof
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 227 |
else openIntro();
|
| 228 |
}
|
| 229 |
/* Safety net: if the intro never manages to open (module error, boot failure),
|
|
|
|
| 82 |
that never opened. Once only β revealFront can be reached twice. */
|
| 83 |
if(!gateFired){
|
| 84 |
gateFired=true;
|
| 85 |
+
try{window.dispatchEvent(new CustomEvent('massfront:intro-complete'));}catch(e){}
|
| 86 |
setTimeout(function(){
|
| 87 |
if(typeof mfAuthGate==='function'){ try{ mfAuthGate(); }catch(e){} }
|
| 88 |
},340);
|
|
|
|
| 182 |
shape may bypass the reveal; ordinary launches and look-alike query
|
| 183 |
strings still receive the title screen. The route owner validates and
|
| 184 |
consumes the secured session record later in galactic-operations.js. */
|
| 185 |
+
try{return /^\?(?:galacticRoute|groundOperation)=[A-Za-z0-9_-]{16,128}$/.test(String(location.search||''));}
|
| 186 |
catch(e){return false;}
|
| 187 |
}
|
| 188 |
function dismissIntroForGalacticRoute(){
|
|
|
|
| 224 |
established commander returning to the game. */
|
| 225 |
/* Open on the next frame, not after a 650ms delay: the menu must never
|
| 226 |
be visible first. The boot cover + intro.css keep it hidden until then. */
|
| 227 |
+
if(typeof window.mfLauncherAwaitingUpdateIntro==='function'&&window.mfLauncherAwaitingUpdateIntro()){
|
| 228 |
+
// Updating owns the first screen. Do not invoke revealFront here: it opens login.
|
| 229 |
+
document.body.classList.add('mfIntroDone');
|
| 230 |
+
var cover=document.getElementById('mfBootCover');if(cover)cover.remove();
|
| 231 |
+
el.hidden=true;el.setAttribute('aria-hidden','true');
|
| 232 |
+
}
|
| 233 |
+
else if(typeof requestAnimationFrame==='function') requestAnimationFrame(function(){openIntro();});
|
| 234 |
else openIntro();
|
| 235 |
}
|
| 236 |
/* Safety net: if the intro never manages to open (module error, boot failure),
|
src/launcher.js
CHANGED
|
@@ -44,10 +44,10 @@
|
|
| 44 |
'Preserved rollback after the patched runtime passed probation.'],
|
| 45 |
upcoming:['Large-update resume and clearer update categories.']}
|
| 46 |
];
|
| 47 |
-
var L={inited:false,gate:true,passed:false,bypass:false,identity:null,update:null,
|
| 48 |
notesTab:'features',historyTab:'published',release:null,releaseKey:'',primary:'wait',
|
| 49 |
packBusy:false,packTicket:0,packTimer:0,packAudioReady:false,packAudioIds:[],
|
| 50 |
-
galacticBusy:false,galacticTicket:0,galacticReady:false};
|
| 51 |
|
| 52 |
function byId(id){return document.getElementById(id);}
|
| 53 |
function text(id,value){var el=byId(id);if(el)el.textContent=value==null?'':String(value);return el;}
|
|
@@ -74,7 +74,7 @@
|
|
| 74 |
return true;
|
| 75 |
}
|
| 76 |
function isGalacticReturn(){
|
| 77 |
-
try{return /^\?galacticRoute=[A-Za-z0-9_-]{16,128}$/.test(String(location.search||''));}
|
| 78 |
catch(e){return false;}
|
| 79 |
}
|
| 80 |
function identitySnapshot(){
|
|
@@ -225,7 +225,7 @@
|
|
| 225 |
var busy=['channeling','checking','downloading','staging','applying','rollingBack'].indexOf(state)>=0;
|
| 226 |
var primary=byId('mfLaunchPlay'),offline=byId('mfLaunchOffline');if(!primary||!offline)return;
|
| 227 |
offline.style.display='none';offline.disabled=true;
|
| 228 |
-
if(id.state==='pending'){
|
| 229 |
/* Identity can stay pending for reasons the player cannot influence: a
|
| 230 |
cleared 401 session that never reopened the gate, a missing gate
|
| 231 |
overlay, or a verification request that never returned. Hiding the
|
|
@@ -246,6 +246,9 @@
|
|
| 246 |
offline.style.display='block';offline.disabled=false;return;
|
| 247 |
}
|
| 248 |
if(busy){L.primary='wait';primary.textContent=String(action.label||'PLEASE WAIT');primary.disabled=true;return;}
|
|
|
|
|
|
|
|
|
|
| 249 |
if(id.state==='offline'||!connected||state==='unset'||state==='offline'){
|
| 250 |
/* This is the only enabled control at the gate when identity cannot be
|
| 251 |
confirmed, so its label has to be honest about what it does. It no
|
|
@@ -305,6 +308,12 @@
|
|
| 305 |
var sc=byId('mfLauncherScroll');if(sc)sc.scrollTop=0;renderAll();
|
| 306 |
}
|
| 307 |
function enterGame(forceOffline){
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
/* PLAY OFFLINE means "start now without waiting for the network", not
|
| 309 |
"disable networking on this device from now on". It used to latch
|
| 310 |
netSetOffline(true), which persists to localStorage, and since the
|
|
@@ -324,6 +333,13 @@
|
|
| 324 |
if(typeof setupAttract==='function')try{setupAttract();}catch(e){}
|
| 325 |
var play=byId('startBtn');if(play)setTimeout(function(){try{play.focus({preventScroll:true});}catch(e){play.focus();}},60);
|
| 326 |
try{window.dispatchEvent(new CustomEvent('massfront:launcher-exit',{detail:{mode:forceOffline?'offline':'connected'}}));}catch(e){}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
}
|
| 328 |
function primaryAction(){
|
| 329 |
if(L.primary==='play-connected'){enterGame(false);return;}
|
|
@@ -338,21 +354,15 @@
|
|
| 338 |
}
|
| 339 |
async function renderPacks(){
|
| 340 |
var ticket=++L.packTicket,button=byId('mfLaunchPackAudio'),state=byId('mfLaunchPackAudioState');if(!button||!state)return;
|
| 341 |
-
relocatePackPanel();
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
L.packAudioReady=!!installed;L.packAudioIds=audio.map(function(row){return row.id;});
|
| 351 |
-
if(installed){state.textContent='INSTALLED Β· '+fmtBytes(total);button.textContent='VERIFY';button.disabled=false;}
|
| 352 |
-
else if(!online){state.textContent='REQUIRES INTERNET';button.textContent='OFFLINE';button.disabled=true;}
|
| 353 |
-
else {state.textContent=(statuses.some(function(row){return row&&((row.installed||row.partial)&&!row.updateAvailable);})?'RESUME / REPAIR Β· ':'OPTIONAL Β· ')+fmtBytes(total);button.textContent='DOWNLOAD';button.disabled=L.packBusy;}
|
| 354 |
-
button.setAttribute('aria-disabled',button.disabled?'true':'false');
|
| 355 |
-
}catch(e){state.textContent=online?'STATUS UNAVAILABLE':'REQUIRES INTERNET';button.textContent=online?'RETRY':'OFFLINE';button.disabled=!online||L.packBusy;}
|
| 356 |
}
|
| 357 |
function schedulePackRender(delay){
|
| 358 |
clearTimeout(L.packTimer);L.packTimer=setTimeout(function(){L.packTimer=0;renderPacks();},Math.max(0,delay||0));
|
|
@@ -375,16 +385,9 @@
|
|
| 375 |
}catch(e){if(typeof toast==='function')toast(online?'Audio download needs attention β retry when connected':'Audio repair requires an Internet connection');}
|
| 376 |
finally{L.packBusy=false;relocatePackPanel();renderPacks();}
|
| 377 |
}
|
| 378 |
-
/*
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
assetpack.js has carried a complete installer for them
|
| 382 |
-
(mfInstallExplorationPack: signed manifest, per-file SHA-256, storage
|
| 383 |
-
preflight, resumable chunked install, legacy-blob migration). Nothing ever
|
| 384 |
-
called it. This card only ever reported whether the *build* happened to
|
| 385 |
-
bundle the module, so a player on a slim install was told
|
| 386 |
-
"INSTALLER REQUIRED" and given no way to get content that was sitting on
|
| 387 |
-
the CDN ready to download. Offer the real install path instead. */
|
| 388 |
var GALACTIC_PACK_ID='galactic-exploration';
|
| 389 |
async function renderGalactic(){
|
| 390 |
var state=byId('mfLaunchPackGalacticState'),button=byId('mfLaunchPackGalactic');if(!state||!button)return;
|
|
@@ -402,8 +405,18 @@
|
|
| 402 |
if(ticket!==L.galacticTicket)return;
|
| 403 |
var installed=!!(status&&status.ok!==false&&status.installed&&!status.updateAvailable);
|
| 404 |
var partial=!!(status&&(status.partial||(status.installed&&status.updateAvailable)));
|
| 405 |
-
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
if(card)card.classList.remove('onlineOnly');
|
| 408 |
state.textContent='INSTALLED'+(status&&status.bytes?' Β· '+fmtBytes(status.bytes):'');
|
| 409 |
button.textContent='VERIFY';button.disabled=L.galacticBusy;
|
|
@@ -412,7 +425,7 @@
|
|
| 412 |
state.textContent='REQUIRES INTERNET';button.textContent='OFFLINE';button.disabled=true;
|
| 413 |
}else{
|
| 414 |
if(card)card.classList.remove('onlineOnly');
|
| 415 |
-
state.textContent=(partial?'RESUME Β· ':'
|
| 416 |
button.textContent=partial?'RESUME':'INSTALL';button.disabled=L.galacticBusy;
|
| 417 |
}
|
| 418 |
button.setAttribute('aria-disabled',button.disabled?'true':'false');
|
|
@@ -421,7 +434,15 @@
|
|
| 421 |
if(L.galacticBusy)return;
|
| 422 |
if(window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true)return;
|
| 423 |
var verifyOnly=L.galacticReady,online=onlineAllowed(),api=window.MASSFRONT_ASSET_PACKS;
|
| 424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 425 |
L.galacticBusy=true;
|
| 426 |
var button=byId('mfLaunchPackGalactic');
|
| 427 |
if(button){button.disabled=true;button.textContent=verifyOnly?'VERIFYINGβ¦':'PREPARINGβ¦';}
|
|
@@ -478,13 +499,14 @@
|
|
| 478 |
if(typeof netSetOffline==='function'&&L.identity.state==='offline'&&explicitOffline)netSetOffline(true);
|
| 479 |
else if(typeof netSetOffline==='function'&&L.identity.state==='connected')netSetOffline(false);
|
| 480 |
}catch(e){}
|
| 481 |
-
|
| 482 |
/* A cached token can expire after the title closes. Re-open the auth
|
| 483 |
choice when /me rejects it instead of leaving PLAY disabled forever. */
|
| 484 |
-
if(L.identity.state==='pending'&&L.identity.source==='signed-out'&&typeof mfAuthGate==='function')
|
| 485 |
setTimeout(function(){try{mfAuthGate();}catch(e){}},0);
|
| 486 |
-
if(!L.bypass&&(L.identity.state==='connected'||L.identity.state==='offline'))
|
| 487 |
});
|
|
|
|
| 488 |
window.addEventListener('massfront:update-state',function(event){
|
| 489 |
var before=L.update&&L.update.state,next=event&&event.detail||updaterSnapshot();renderUpdate(next);
|
| 490 |
/* Byte progress is deliberately frequent. Pack manifests are unrelated
|
|
@@ -492,6 +514,7 @@
|
|
| 492 |
updater crosses a material state boundary. */
|
| 493 |
if(before!==next.state)schedulePackRender(160);
|
| 494 |
});
|
|
|
|
| 495 |
window.addEventListener('online',function(){
|
| 496 |
if(L.identity&&L.identity.source==='session-unverified'&&typeof apVerifySession==='function')
|
| 497 |
try{apVerifySession();}catch(e){}
|
|
@@ -519,12 +542,13 @@
|
|
| 519 |
}
|
| 520 |
function mfLauncherShouldDeferAttract(){return !L.bypass&&!L.passed;}
|
| 521 |
function mfLauncherSnapshot(){
|
| 522 |
-
return {gate:L.gate,passed:L.passed,bypass:L.bypass,primary:L.primary,
|
| 523 |
identity:L.identity?Object.assign({},L.identity):null,update:L.update?{state:L.update.state,status:L.update.status}:null,
|
| 524 |
notesTab:L.notesTab,historyTab:L.historyTab};
|
| 525 |
}
|
| 526 |
|
| 527 |
window.initLauncherGateway=initLauncherGateway;
|
|
|
|
| 528 |
window.mfLauncherOpenDetails=mfLauncherOpenDetails;
|
| 529 |
window.mfLauncherHandleBack=mfLauncherHandleBack;
|
| 530 |
window.mfLauncherShouldDeferAttract=mfLauncherShouldDeferAttract;
|
|
|
|
| 44 |
'Preserved rollback after the patched runtime passed probation.'],
|
| 45 |
upcoming:['Large-update resume and clearer update categories.']}
|
| 46 |
];
|
| 47 |
+
var L={inited:false,gate:true,passed:false,bypass:false,phase:'updater',entering:false,identity:null,update:null,
|
| 48 |
notesTab:'features',historyTab:'published',release:null,releaseKey:'',primary:'wait',
|
| 49 |
packBusy:false,packTicket:0,packTimer:0,packAudioReady:false,packAudioIds:[],
|
| 50 |
+
galacticBusy:false,galacticTicket:0,galacticReady:false,galacticCached:false};
|
| 51 |
|
| 52 |
function byId(id){return document.getElementById(id);}
|
| 53 |
function text(id,value){var el=byId(id);if(el)el.textContent=value==null?'':String(value);return el;}
|
|
|
|
| 74 |
return true;
|
| 75 |
}
|
| 76 |
function isGalacticReturn(){
|
| 77 |
+
try{return /^\?(?:galacticRoute|groundOperation)=[A-Za-z0-9_-]{16,128}$/.test(String(location.search||''));}
|
| 78 |
catch(e){return false;}
|
| 79 |
}
|
| 80 |
function identitySnapshot(){
|
|
|
|
| 225 |
var busy=['channeling','checking','downloading','staging','applying','rollingBack'].indexOf(state)>=0;
|
| 226 |
var primary=byId('mfLaunchPlay'),offline=byId('mfLaunchOffline');if(!primary||!offline)return;
|
| 227 |
offline.style.display='none';offline.disabled=true;
|
| 228 |
+
if(id.state==='pending'&&L.phase!=='updater'){
|
| 229 |
/* Identity can stay pending for reasons the player cannot influence: a
|
| 230 |
cleared 401 session that never reopened the gate, a missing gate
|
| 231 |
overlay, or a verification request that never returned. Hiding the
|
|
|
|
| 246 |
offline.style.display='block';offline.disabled=false;return;
|
| 247 |
}
|
| 248 |
if(busy){L.primary='wait';primary.textContent=String(action.label||'PLEASE WAIT');primary.disabled=true;return;}
|
| 249 |
+
if(L.phase==='updater'){
|
| 250 |
+
L.primary='play-offline';primary.textContent='CONTINUE TO INTRO';primary.disabled=false;return;
|
| 251 |
+
}
|
| 252 |
if(id.state==='offline'||!connected||state==='unset'||state==='offline'){
|
| 253 |
/* This is the only enabled control at the gate when identity cannot be
|
| 254 |
confirmed, so its label has to be honest about what it does. It no
|
|
|
|
| 308 |
var sc=byId('mfLauncherScroll');if(sc)sc.scrollTop=0;renderAll();
|
| 309 |
}
|
| 310 |
function enterGame(forceOffline){
|
| 311 |
+
if(L.phase==='updater'){
|
| 312 |
+
L.phase='intro';L.gate=false;document.body.classList.remove('mfLauncherGate');
|
| 313 |
+
if(typeof window.showPreAlphaIntro==='function')window.showPreAlphaIntro();
|
| 314 |
+
else {L.phase='login';if(typeof mfAuthGate==='function')mfAuthGate();}
|
| 315 |
+
return;
|
| 316 |
+
}
|
| 317 |
/* PLAY OFFLINE means "start now without waiting for the network", not
|
| 318 |
"disable networking on this device from now on". It used to latch
|
| 319 |
netSetOffline(true), which persists to localStorage, and since the
|
|
|
|
| 333 |
if(typeof setupAttract==='function')try{setupAttract();}catch(e){}
|
| 334 |
var play=byId('startBtn');if(play)setTimeout(function(){try{play.focus({preventScroll:true});}catch(e){play.focus();}},60);
|
| 335 |
try{window.dispatchEvent(new CustomEvent('massfront:launcher-exit',{detail:{mode:forceOffline?'offline':'connected'}}));}catch(e){}
|
| 336 |
+
if(!L.bypass&&!L.entering&&typeof mfOpenExploration==='function'
|
| 337 |
+
&&(window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true||window.__MF_OTA_HAS_GALACTIC_DELIVERY===true)){
|
| 338 |
+
L.entering=true;L.phase='career';
|
| 339 |
+
Promise.resolve(mfOpenExploration('system')).catch(function(){
|
| 340 |
+
if(typeof toast==='function')toast('Ship entry needs attention. Use START to retry.');
|
| 341 |
+
}).finally(function(){L.entering=false;});
|
| 342 |
+
}
|
| 343 |
}
|
| 344 |
function primaryAction(){
|
| 345 |
if(L.primary==='play-connected'){enterGame(false);return;}
|
|
|
|
| 354 |
}
|
| 355 |
async function renderPacks(){
|
| 356 |
var ticket=++L.packTicket,button=byId('mfLaunchPackAudio'),state=byId('mfLaunchPackAudioState');if(!button||!state)return;
|
| 357 |
+
relocatePackPanel();
|
| 358 |
+
/* The current voice bank and available score are package invariants, not
|
| 359 |
+
optional network packs. Future audio expansions use the generic startup
|
| 360 |
+
delivery lane and its shared progress panel instead of duplicating these
|
| 361 |
+
base files in IndexedDB. */
|
| 362 |
+
if(ticket!==L.packTicket)return;
|
| 363 |
+
L.packAudioReady=true;L.packAudioIds=[];
|
| 364 |
+
state.textContent='INCLUDED IN THIS BUILD';button.textContent='READY';button.disabled=true;
|
| 365 |
+
button.setAttribute('aria-disabled','true');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
}
|
| 367 |
function schedulePackRender(delay){
|
| 368 |
clearTimeout(L.packTimer);L.packTimer=setTimeout(function(){L.packTimer=0;renderPacks();},Math.max(0,delay||0));
|
|
|
|
| 385 |
}catch(e){if(typeof toast==='function')toast(online?'Audio download needs attention β retry when connected':'Audio repair requires an Internet connection');}
|
| 386 |
finally{L.packBusy=false;relocatePackPanel();renderPacks();}
|
| 387 |
}
|
| 388 |
+
/* Cached download bytes and a launchable resource tree are different states.
|
| 389 |
+
Older native installs must finish verified staging before this card says
|
| 390 |
+
READY; the content generation is pinned by their new OTA descriptor. */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 391 |
var GALACTIC_PACK_ID='galactic-exploration';
|
| 392 |
async function renderGalactic(){
|
| 393 |
var state=byId('mfLaunchPackGalacticState'),button=byId('mfLaunchPackGalactic');if(!state||!button)return;
|
|
|
|
| 405 |
if(ticket!==L.galacticTicket)return;
|
| 406 |
var installed=!!(status&&status.ok!==false&&status.installed&&!status.updateAvailable);
|
| 407 |
var partial=!!(status&&(status.partial||(status.installed&&status.updateAvailable)));
|
| 408 |
+
var mount=null;try{mount=window.MFNativeExplorationContent&&window.MFNativeExplorationContent.snapshot();}catch(e){}
|
| 409 |
+
var needsMount=window.__MF_OTA_HAS_GALACTIC_DELIVERY===true;
|
| 410 |
+
var mounted=!!(mount&&[mount.good,mount.prepared].some(function(candidate){
|
| 411 |
+
return candidate&&typeof APP_VERSION==='string'&&candidate.version===APP_VERSION&&candidate.generation!==mount.failed;
|
| 412 |
+
}));
|
| 413 |
+
L.galacticCached=installed;
|
| 414 |
+
L.galacticReady=installed&&(!needsMount||mounted);
|
| 415 |
+
if(installed&&needsMount&&!mounted){
|
| 416 |
+
if(card)card.classList.remove('onlineOnly');
|
| 417 |
+
state.textContent=mount&&mount.failed?'STARTUP NEEDS RETRY':'DOWNLOADED Β· STAGING REQUIRED';
|
| 418 |
+
button.textContent=mount&&mount.failed?'RETRY':'STAGE';button.disabled=L.galacticBusy;
|
| 419 |
+
}else if(installed){
|
| 420 |
if(card)card.classList.remove('onlineOnly');
|
| 421 |
state.textContent='INSTALLED'+(status&&status.bytes?' Β· '+fmtBytes(status.bytes):'');
|
| 422 |
button.textContent='VERIFY';button.disabled=L.galacticBusy;
|
|
|
|
| 425 |
state.textContent='REQUIRES INTERNET';button.textContent='OFFLINE';button.disabled=true;
|
| 426 |
}else{
|
| 427 |
if(card)card.classList.remove('onlineOnly');
|
| 428 |
+
state.textContent=(partial?'RESUME Β· ':'CONTENT Β· ')+(status&&status.bytes?fmtBytes(status.bytes):'GALACTIC EXPLORATION');
|
| 429 |
button.textContent=partial?'RESUME':'INSTALL';button.disabled=L.galacticBusy;
|
| 430 |
}
|
| 431 |
button.setAttribute('aria-disabled',button.disabled?'true':'false');
|
|
|
|
| 434 |
if(L.galacticBusy)return;
|
| 435 |
if(window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true)return;
|
| 436 |
var verifyOnly=L.galacticReady,online=onlineAllowed(),api=window.MASSFRONT_ASSET_PACKS;
|
| 437 |
+
/* A bound OTA can stage verified cached bytes without a network. The
|
| 438 |
+
installer still rejects missing/damaged bytes offline; this flag only
|
| 439 |
+
permits reaching its existing local verification/copy path. */
|
| 440 |
+
var localStage=window.__MF_OTA_HAS_GALACTIC_DELIVERY===true&&L.galacticCached;
|
| 441 |
+
if(!verifyOnly&&!online&&!localStage)return;
|
| 442 |
+
try{
|
| 443 |
+
var mountApi=window.MFNativeExplorationContent,mountState=mountApi&&mountApi.snapshot();
|
| 444 |
+
if(mountState&&mountState.failed){mountApi.retry();L.galacticReady=false;verifyOnly=false;}
|
| 445 |
+
}catch(e){}
|
| 446 |
L.galacticBusy=true;
|
| 447 |
var button=byId('mfLaunchPackGalactic');
|
| 448 |
if(button){button.disabled=true;button.textContent=verifyOnly?'VERIFYINGβ¦':'PREPARINGβ¦';}
|
|
|
|
| 499 |
if(typeof netSetOffline==='function'&&L.identity.state==='offline'&&explicitOffline)netSetOffline(true);
|
| 500 |
else if(typeof netSetOffline==='function'&&L.identity.state==='connected')netSetOffline(false);
|
| 501 |
}catch(e){}
|
| 502 |
+
renderAll();
|
| 503 |
/* A cached token can expire after the title closes. Re-open the auth
|
| 504 |
choice when /me rejects it instead of leaving PLAY disabled forever. */
|
| 505 |
+
if(L.phase==='login'&&L.identity.state==='pending'&&L.identity.source==='signed-out'&&typeof mfAuthGate==='function')
|
| 506 |
setTimeout(function(){try{mfAuthGate();}catch(e){}},0);
|
| 507 |
+
if(!L.bypass&&L.phase==='login'&&(L.identity.state==='connected'||L.identity.state==='offline'))enterGame(L.identity.state==='offline');
|
| 508 |
});
|
| 509 |
+
window.addEventListener('massfront:intro-complete',function(){if(!L.bypass){L.phase='login';renderAll();}});
|
| 510 |
window.addEventListener('massfront:update-state',function(event){
|
| 511 |
var before=L.update&&L.update.state,next=event&&event.detail||updaterSnapshot();renderUpdate(next);
|
| 512 |
/* Byte progress is deliberately frequent. Pack manifests are unrelated
|
|
|
|
| 514 |
updater crosses a material state boundary. */
|
| 515 |
if(before!==next.state)schedulePackRender(160);
|
| 516 |
});
|
| 517 |
+
window.addEventListener('massfront:assetpack-state',function(){relocatePackPanel();});
|
| 518 |
window.addEventListener('online',function(){
|
| 519 |
if(L.identity&&L.identity.source==='session-unverified'&&typeof apVerifySession==='function')
|
| 520 |
try{apVerifySession();}catch(e){}
|
|
|
|
| 542 |
}
|
| 543 |
function mfLauncherShouldDeferAttract(){return !L.bypass&&!L.passed;}
|
| 544 |
function mfLauncherSnapshot(){
|
| 545 |
+
return {gate:L.gate,passed:L.passed,bypass:L.bypass,phase:L.phase,primary:L.primary,
|
| 546 |
identity:L.identity?Object.assign({},L.identity):null,update:L.update?{state:L.update.state,status:L.update.status}:null,
|
| 547 |
notesTab:L.notesTab,historyTab:L.historyTab};
|
| 548 |
}
|
| 549 |
|
| 550 |
window.initLauncherGateway=initLauncherGateway;
|
| 551 |
+
window.mfLauncherAwaitingUpdateIntro=function(){return !isGalacticReturn()&&L.phase==='updater';};
|
| 552 |
window.mfLauncherOpenDetails=mfLauncherOpenDetails;
|
| 553 |
window.mfLauncherHandleBack=mfLauncherHandleBack;
|
| 554 |
window.mfLauncherShouldDeferAttract=mfLauncherShouldDeferAttract;
|
src/main.js
CHANGED
|
@@ -605,6 +605,7 @@ function resetWorld(){
|
|
| 605 |
if(typeof commanderDialogueReset==='function') commanderDialogueReset();
|
| 606 |
if(typeof cmdrTxReset==='function') cmdrTxReset();
|
| 607 |
ualive.fill(0); usel.fill(0); ugen.fill(0); utgtg.fill(-1);
|
|
|
|
| 608 |
freeList=[]; unitHigh=0; teamCount[0]=0; teamCount[1]=0;
|
| 609 |
if(typeof activeUnitReset==='function')activeUnitReset();
|
| 610 |
if(typeof tick==='number')tick=0;
|
|
@@ -2703,30 +2704,31 @@ function wire(){
|
|
| 2703 |
if(typeof renderOps==='function') renderOps();
|
| 2704 |
});
|
| 2705 |
});
|
| 2706 |
-
/*
|
| 2707 |
The module is a separate document with its own three.js and its own WebGL
|
| 2708 |
canvas, so it is opened as a document rather than imported: two contexts
|
| 2709 |
fighting over one canvas is the failure this avoids, and a crash in an
|
| 2710 |
unfinished module then cannot take the RTS down with it.
|
| 2711 |
-
|
| 2712 |
-
|
| 2713 |
START enters live space for the world introduction, whose skippable choice
|
| 2714 |
continues either to protected planetary Training or the Galactic War Table.
|
| 2715 |
Nothing here runs during a match or touches saves. */
|
| 2716 |
(function mfWireExploration(){
|
| 2717 |
const URL_='./modules/space_exploration/index.html';
|
| 2718 |
mfOpenExploration=async function(entryView){
|
| 2719 |
-
const enabled=
|
|
|
|
| 2720 |
if(!enabled){ if(typeof toast==='function') toast('Enable Experimental: Galactic Campaign first'); return false; }
|
| 2721 |
entryView=entryView==='system'?'system':'campaign_hub';
|
| 2722 |
initAudio(); sfx('ui');
|
| 2723 |
-
let present=
|
| 2724 |
-
try{ const r=await fetch(URL_,{method:'HEAD',cache:'no-store'}); present=r.ok; }
|
| 2725 |
catch(e){}
|
| 2726 |
if(!present){
|
| 2727 |
const packed=typeof mfInstallExplorationPack==='function'&&await mfInstallExplorationPack();
|
| 2728 |
if(!packed||!packed.ok){ if(typeof toast==='function') toast('Galactic preview is not installed in this build'); return false; }
|
| 2729 |
-
/*
|
| 2730 |
if(packed.openUrl) packUrl=packed.openUrl;
|
| 2731 |
else { if(typeof toast==='function') toast('Galactic pack downloaded β War Room stays available in this build'); return false; }
|
| 2732 |
}
|
|
@@ -2781,7 +2783,12 @@ function wire(){
|
|
| 2781 |
}catch(e){}
|
| 2782 |
if(!ticketReady){ if(typeof toast==='function') toast('Galactic link could not be secured on this device'); return false; }
|
| 2783 |
/* Same tab: the ship and RTS renderers must never compete for WebGL. */
|
| 2784 |
-
try{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2785 |
catch(e){ if(typeof toast==='function') toast('Galactic preview could not be opened'); return false; }
|
| 2786 |
};
|
| 2787 |
/* boot() is asynchronous while the tail manifest scripts continue loading.
|
|
@@ -2859,11 +2866,13 @@ function wire(){
|
|
| 2859 |
showFrontScreen('warScr');
|
| 2860 |
} else if(typeof openSkirmishSetup==='function') openSkirmishSetup();
|
| 2861 |
};
|
| 2862 |
-
mfBindTap($('startBtn'),()=>{ initAudio(); sfx('ui');
|
| 2863 |
-
/*
|
| 2864 |
-
|
| 2865 |
-
|
| 2866 |
-
|
|
|
|
|
|
|
| 2867 |
});
|
| 2868 |
mfBindTap($('warBack'),()=>{ sfx('ui');
|
| 2869 |
renderMetaHead(); showFrontScreen('startScreen'); });
|
|
@@ -3150,6 +3159,8 @@ function wire(){
|
|
| 3150 |
mfBindNativePress($('formBtn'),()=>{armFormationOrder();});
|
| 3151 |
mfBindNativePress($('rallyBtn'),()=>{
|
| 3152 |
if(openBld<0) return;
|
|
|
|
|
|
|
| 3153 |
armRally=openBld;
|
| 3154 |
closeMenus();
|
| 3155 |
toast('β Tap the map to place the rally flag');
|
|
@@ -3183,9 +3194,25 @@ function wire(){
|
|
| 3183 |
mfBindNativePress($('placeRotR'),e=>{ e.preventDefault(); rotatePlacement(1); });
|
| 3184 |
mfBindNativePress($('repeatBtn'),()=>{
|
| 3185 |
if(openBld<0) return;
|
| 3186 |
-
const B=blds[openBld];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3187 |
$('repeatBtn').textContent='REPEAT: '+(B.repeat?'ON':'OFF');
|
| 3188 |
-
$('repeatBtn').classList.toggle('on',B.repeat);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3189 |
});
|
| 3190 |
mfBindNativePress($('abOver'),()=>{ if(aiming===0){aiming=-1;return;} tryAbility(0); });
|
| 3191 |
mfBindNativePress($('abHeal'),()=>tryAbility(1));
|
|
|
|
| 605 |
if(typeof commanderDialogueReset==='function') commanderDialogueReset();
|
| 606 |
if(typeof cmdrTxReset==='function') cmdrTxReset();
|
| 607 |
ualive.fill(0); usel.fill(0); ugen.fill(0); utgtg.fill(-1);
|
| 608 |
+
if(typeof mfNavProgressReset==='function')mfNavProgressReset();
|
| 609 |
freeList=[]; unitHigh=0; teamCount[0]=0; teamCount[1]=0;
|
| 610 |
if(typeof activeUnitReset==='function')activeUnitReset();
|
| 611 |
if(typeof tick==='number')tick=0;
|
|
|
|
| 2704 |
if(typeof renderOps==='function') renderOps();
|
| 2705 |
});
|
| 2706 |
});
|
| 2707 |
+
/* GALACTIC EXPLORATION β same-tab strategic layer.
|
| 2708 |
The module is a separate document with its own three.js and its own WebGL
|
| 2709 |
canvas, so it is opened as a document rather than imported: two contexts
|
| 2710 |
fighting over one canvas is the failure this avoids, and a crash in an
|
| 2711 |
unfinished module then cannot take the RTS down with it.
|
| 2712 |
+
Complete installers enable this layer directly. Older/slim shells retain
|
| 2713 |
+
their opt-in and installation fallback, checked by a launch-time HEAD probe.
|
| 2714 |
START enters live space for the world introduction, whose skippable choice
|
| 2715 |
continues either to protected planetary Training or the Galactic War Table.
|
| 2716 |
Nothing here runs during a match or touches saves. */
|
| 2717 |
(function mfWireExploration(){
|
| 2718 |
const URL_='./modules/space_exploration/index.html';
|
| 2719 |
mfOpenExploration=async function(entryView){
|
| 2720 |
+
const enabled=window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true||window.__MF_OTA_HAS_GALACTIC_DELIVERY===true
|
| 2721 |
+
||!!(typeof META!=='undefined'&&META.settings&&META.settings.experimentalExploration);
|
| 2722 |
if(!enabled){ if(typeof toast==='function') toast('Enable Experimental: Galactic Campaign first'); return false; }
|
| 2723 |
entryView=entryView==='system'?'system':'campaign_hub';
|
| 2724 |
initAudio(); sfx('ui');
|
| 2725 |
+
let present=window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true,packUrl=URL_;
|
| 2726 |
+
try{ if(!present){const r=await fetch(URL_,{method:'HEAD',cache:'no-store'}); present=r.ok;} }
|
| 2727 |
catch(e){}
|
| 2728 |
if(!present){
|
| 2729 |
const packed=typeof mfInstallExplorationPack==='function'&&await mfInstallExplorationPack();
|
| 2730 |
if(!packed||!packed.ok){ if(typeof toast==='function') toast('Galactic preview is not installed in this build'); return false; }
|
| 2731 |
+
/* Cached bytes need a same-origin resource mount before navigation. */
|
| 2732 |
if(packed.openUrl) packUrl=packed.openUrl;
|
| 2733 |
else { if(typeof toast==='function') toast('Galactic pack downloaded β War Room stays available in this build'); return false; }
|
| 2734 |
}
|
|
|
|
| 2783 |
}catch(e){}
|
| 2784 |
if(!ticketReady){ if(typeof toast==='function') toast('Galactic link could not be secured on this device'); return false; }
|
| 2785 |
/* Same tab: the ship and RTS renderers must never compete for WebGL. */
|
| 2786 |
+
try{
|
| 2787 |
+
if(typeof mfBeginExplorationMountLaunch==='function'&&!(await mfBeginExplorationMountLaunch(packUrl))){
|
| 2788 |
+
if(typeof toast==='function')toast('Galactic content is not ready to launch safely');return false;
|
| 2789 |
+
}
|
| 2790 |
+
location.href=packUrl;return true;
|
| 2791 |
+
}
|
| 2792 |
catch(e){ if(typeof toast==='function') toast('Galactic preview could not be opened'); return false; }
|
| 2793 |
};
|
| 2794 |
/* boot() is asynchronous while the tail manifest scripts continue loading.
|
|
|
|
| 2866 |
showFrontScreen('warScr');
|
| 2867 |
} else if(typeof openSkirmishSetup==='function') openSkirmishSetup();
|
| 2868 |
};
|
| 2869 |
+
mfBindTap($('startBtn'),async()=>{ initAudio(); sfx('ui');
|
| 2870 |
+
/* Keep the launcher/authentication boundary, then enter the strategic home.
|
| 2871 |
+
Older/slim installations still need their working local War Room. */
|
| 2872 |
+
const enabled=window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true||window.__MF_OTA_HAS_GALACTIC_DELIVERY===true
|
| 2873 |
+
||!!(typeof META!=='undefined'&&META.settings&&META.settings.experimentalExploration);
|
| 2874 |
+
const opened=enabled&&typeof mfOpenExploration==='function'&&await mfOpenExploration('system');
|
| 2875 |
+
if(!opened)openLegacyWarRoom();
|
| 2876 |
});
|
| 2877 |
mfBindTap($('warBack'),()=>{ sfx('ui');
|
| 2878 |
renderMetaHead(); showFrontScreen('startScreen'); });
|
|
|
|
| 3159 |
mfBindNativePress($('formBtn'),()=>{armFormationOrder();});
|
| 3160 |
mfBindNativePress($('rallyBtn'),()=>{
|
| 3161 |
if(openBld<0) return;
|
| 3162 |
+
const B=blds[openBld];
|
| 3163 |
+
if(!B||!B.alive||!(typeof mfLocalOwnsBuilding==='function'?mfLocalOwnsBuilding(B):B.team===0)||!Array.isArray(B.queue))return;
|
| 3164 |
armRally=openBld;
|
| 3165 |
closeMenus();
|
| 3166 |
toast('β Tap the map to place the rally flag');
|
|
|
|
| 3194 |
mfBindNativePress($('placeRotR'),e=>{ e.preventDefault(); rotatePlacement(1); });
|
| 3195 |
mfBindNativePress($('repeatBtn'),()=>{
|
| 3196 |
if(openBld<0) return;
|
| 3197 |
+
const B=blds[openBld];
|
| 3198 |
+
if(!B||!B.alive||!(typeof mfLocalOwnsBuilding==='function'?mfLocalOwnsBuilding(B):B.team===0)||!Array.isArray(B.queue))return;
|
| 3199 |
+
const C=mfMatchConsumer();
|
| 3200 |
+
if(C&&C.requiresLockstep()){
|
| 3201 |
+
const active=!C.repeatIntent(openBld).active,receipt=C.submitRepeat(openBld,active);
|
| 3202 |
+
if(!receipt){toast(C.lastFailure()||'Repeat order could not be sent');return;}
|
| 3203 |
+
$('repeatBtn').textContent='REPEAT: '+(active?'ON':'OFF')+' β PENDING';
|
| 3204 |
+
$('repeatBtn').classList.toggle('on',active);
|
| 3205 |
+
$('repeatBtn').setAttribute('aria-pressed',String(active));
|
| 3206 |
+
toast(active?'Repeat ON queued for the shared match tick':'Repeat OFF queued β existing queued units are retained');
|
| 3207 |
+
sfx('ui');return;
|
| 3208 |
+
}
|
| 3209 |
+
B.repeat=!B.repeat;
|
| 3210 |
$('repeatBtn').textContent='REPEAT: '+(B.repeat?'ON':'OFF');
|
| 3211 |
+
$('repeatBtn').classList.toggle('on',B.repeat);
|
| 3212 |
+
$('repeatBtn').setAttribute('aria-pressed',String(B.repeat));
|
| 3213 |
+
if(typeof toast==='function')toast(B.repeat?'Repeat enabled β completed units return to this queue':
|
| 3214 |
+
'Repeat off β no new repeats; '+B.queue.length+' queued unit'+(B.queue.length===1?'':'s')+' remaining');
|
| 3215 |
+
sfx('ui');
|
| 3216 |
});
|
| 3217 |
mfBindNativePress($('abOver'),()=>{ if(aiming===0){aiming=-1;return;} tryAbility(0); });
|
| 3218 |
mfBindNativePress($('abHeal'),()=>tryAbility(1));
|
src/onboarding.js
CHANGED
|
@@ -202,7 +202,8 @@ function eligible(){
|
|
| 202 |
return true;
|
| 203 |
}
|
| 204 |
function experimentalEnabled(){
|
| 205 |
-
return
|
|
|
|
| 206 |
}
|
| 207 |
function automaticEligible(){
|
| 208 |
/* Experimental new careers make their choice after live space appears. A
|
|
|
|
| 202 |
return true;
|
| 203 |
}
|
| 204 |
function experimentalEnabled(){
|
| 205 |
+
return window.__MF_BUILD_HAS_GALACTIC_EXPLORATION===true
|
| 206 |
+
||!!(typeof META!=='undefined'&&META&&META.settings&&META.settings.experimentalExploration);
|
| 207 |
}
|
| 208 |
function automaticEligible(){
|
| 209 |
/* Experimental new careers make their choice after live space appears. A
|
src/ui/hud.js
CHANGED
|
@@ -2851,12 +2851,20 @@ function renderProdMenu(){ if(openBldGone()) return;
|
|
| 2851 |
: ('β¬ UPGRADE TO TECH 2 ('+BUP.fac[0].cm+'m '+BUP.fac[0].ce+'e)');
|
| 2852 |
} else ub.style.display='none';
|
| 2853 |
renderQueue(true);
|
| 2854 |
-
|
| 2855 |
-
rb.textContent='REPEAT: '+(B.repeat?'ON':'OFF');
|
| 2856 |
-
rb.classList.toggle('on',B.repeat);
|
| 2857 |
const ry=$('rallyBtn');
|
| 2858 |
if(ry){ ry.style.display='block'; ry.textContent=B.rally?'β RALLY SET β TAP TO MOVE':'β SET RALLY POINT'; }
|
| 2859 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2860 |
function queueStacks(q){
|
| 2861 |
const out=[];
|
| 2862 |
if(!q||!q.length) return out;
|
|
@@ -2871,8 +2879,16 @@ function queueStacks(q){
|
|
| 2871 |
}
|
| 2872 |
return out;
|
| 2873 |
}
|
| 2874 |
-
function cancelQueuedUnit(B,start){
|
| 2875 |
if(!B||!B.queue||start<0||start>=B.queue.length) return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2876 |
const type=B.queue[start];
|
| 2877 |
let end=start;
|
| 2878 |
while(end<B.queue.length&&B.queue[end]===type) end++;
|
|
@@ -2891,6 +2907,7 @@ function cancelQueuedUnit(B,start){
|
|
| 2891 |
B.queue.shift();
|
| 2892 |
B.prodT=0;
|
| 2893 |
} else B.queue.splice(last,1);
|
|
|
|
| 2894 |
return true;
|
| 2895 |
}
|
| 2896 |
function renderQueue(forceControls){ if(openBldGone()) return;
|
|
@@ -2898,6 +2915,9 @@ function renderQueue(forceControls){ if(openBldGone()) return;
|
|
| 2898 |
const B=blds[openBld];
|
| 2899 |
const prodPanel=$('prodMenu'),prodVisible=!!(prodPanel&&prodPanel.style.display==='block');
|
| 2900 |
if(forceControls||prodVisible){
|
|
|
|
|
|
|
|
|
|
| 2901 |
mfRenderBuildingActivityControls(B,'prodMenu');
|
| 2902 |
mfRenderBuildingServiceControls(B,'prodMenu');
|
| 2903 |
mfRenderBuildingUpgradeControls(B,'prodMenu');
|
|
@@ -2915,7 +2935,7 @@ function renderQueue(forceControls){ if(openBldGone()) return;
|
|
| 2915 |
const q=B.queue||[];
|
| 2916 |
mfSyncProductionQueueCards(B);
|
| 2917 |
const stacks=queueStacks(q);
|
| 2918 |
-
const sig=stacks.map(s=>s.t+':'+s.n+':'+s.i).join(',')+'|'+(B.adj||0);
|
| 2919 |
if(el._mfQ!==sig){
|
| 2920 |
el._mfQ=sig;
|
| 2921 |
el.innerHTML='';
|
|
@@ -2927,6 +2947,7 @@ function renderQueue(forceControls){ if(openBldGone()) return;
|
|
| 2927 |
const row=document.createElement('div');
|
| 2928 |
row.className='qRow';
|
| 2929 |
stacks.forEach((S,si)=>{
|
|
|
|
| 2930 |
const plate=document.createElement('button');
|
| 2931 |
plate.type='button';
|
| 2932 |
plate.className='qPlate'+(si===0?' active':'');
|
|
@@ -2955,7 +2976,7 @@ function renderQueue(forceControls){ if(openBldGone()) return;
|
|
| 2955 |
return;
|
| 2956 |
}
|
| 2957 |
plate._mfQCancelAt=0;
|
| 2958 |
-
if(cancelQueuedUnit(Bb,S.i)){ if(typeof sfx==='function') sfx('ui'); renderQueue(); }
|
| 2959 |
};
|
| 2960 |
mfHudBindQueueCancel(plate,requestCancel);
|
| 2961 |
row.appendChild(plate);
|
|
|
|
| 2851 |
: ('β¬ UPGRADE TO TECH 2 ('+BUP.fac[0].cm+'m '+BUP.fac[0].ce+'e)');
|
| 2852 |
} else ub.style.display='none';
|
| 2853 |
renderQueue(true);
|
| 2854 |
+
mfRenderProductionRepeatControl(B);
|
|
|
|
|
|
|
| 2855 |
const ry=$('rallyBtn');
|
| 2856 |
if(ry){ ry.style.display='block'; ry.textContent=B.rally?'β RALLY SET β TAP TO MOVE':'β SET RALLY POINT'; }
|
| 2857 |
}
|
| 2858 |
+
function mfRenderProductionRepeatControl(B){
|
| 2859 |
+
if(!B||!Array.isArray(B.queue))return;
|
| 2860 |
+
const rb=$('repeatBtn');
|
| 2861 |
+
if(!rb)return;
|
| 2862 |
+
const repeatConsumer=window.MFMatchCommandConsumer,
|
| 2863 |
+
repeatState=repeatConsumer?repeatConsumer.repeatIntent(openBld):{active:!!B.repeat,pending:false};
|
| 2864 |
+
rb.textContent='REPEAT: '+(repeatState.active?'ON':'OFF')+(repeatState.pending?' β PENDING':'');
|
| 2865 |
+
rb.classList.toggle('on',repeatState.active);
|
| 2866 |
+
rb.setAttribute('aria-pressed',String(repeatState.active));
|
| 2867 |
+
}
|
| 2868 |
function queueStacks(q){
|
| 2869 |
const out=[];
|
| 2870 |
if(!q||!q.length) return out;
|
|
|
|
| 2879 |
}
|
| 2880 |
return out;
|
| 2881 |
}
|
| 2882 |
+
function cancelQueuedUnit(B,start,snapshot){
|
| 2883 |
if(!B||!B.queue||start<0||start>=B.queue.length) return false;
|
| 2884 |
+
const C=window.MFMatchCommandConsumer;
|
| 2885 |
+
if(C&&C.requiresLockstep()){
|
| 2886 |
+
const receipt=C.submitCancelProduction(blds.indexOf(B),start,snapshot);
|
| 2887 |
+
if(typeof toast==='function')toast(receipt?'Cancellation queued for the shared match tick':C.lastFailure()||'Cancellation could not be sent');
|
| 2888 |
+
return !!receipt;
|
| 2889 |
+
}
|
| 2890 |
+
if(snapshot&&((B.queueRevision||0)!==snapshot.revision||B.queue.length!==snapshot.queue.length||
|
| 2891 |
+
!B.queue.every((t,i)=>t===snapshot.queue[i])))return false;
|
| 2892 |
const type=B.queue[start];
|
| 2893 |
let end=start;
|
| 2894 |
while(end<B.queue.length&&B.queue[end]===type) end++;
|
|
|
|
| 2907 |
B.queue.shift();
|
| 2908 |
B.prodT=0;
|
| 2909 |
} else B.queue.splice(last,1);
|
| 2910 |
+
B.queueRevision=(B.queueRevision||0)+1;
|
| 2911 |
return true;
|
| 2912 |
}
|
| 2913 |
function renderQueue(forceControls){ if(openBldGone()) return;
|
|
|
|
| 2915 |
const B=blds[openBld];
|
| 2916 |
const prodPanel=$('prodMenu'),prodVisible=!!(prodPanel&&prodPanel.style.display==='block');
|
| 2917 |
if(forceControls||prodVisible){
|
| 2918 |
+
// The 800ms production refresh, not menu reopening, clears pending intent
|
| 2919 |
+
// after its authoritative tick (or a reconnect rejection) is acknowledged.
|
| 2920 |
+
mfRenderProductionRepeatControl(B);
|
| 2921 |
mfRenderBuildingActivityControls(B,'prodMenu');
|
| 2922 |
mfRenderBuildingServiceControls(B,'prodMenu');
|
| 2923 |
mfRenderBuildingUpgradeControls(B,'prodMenu');
|
|
|
|
| 2935 |
const q=B.queue||[];
|
| 2936 |
mfSyncProductionQueueCards(B);
|
| 2937 |
const stacks=queueStacks(q);
|
| 2938 |
+
const sig=stacks.map(s=>s.t+':'+s.n+':'+s.i).join(',')+'|'+(B.adj||0)+'|'+(B.queueRevision||0);
|
| 2939 |
if(el._mfQ!==sig){
|
| 2940 |
el._mfQ=sig;
|
| 2941 |
el.innerHTML='';
|
|
|
|
| 2947 |
const row=document.createElement('div');
|
| 2948 |
row.className='qRow';
|
| 2949 |
stacks.forEach((S,si)=>{
|
| 2950 |
+
const cancelSnapshot={revision:B.queueRevision||0,queue:q.slice()};
|
| 2951 |
const plate=document.createElement('button');
|
| 2952 |
plate.type='button';
|
| 2953 |
plate.className='qPlate'+(si===0?' active':'');
|
|
|
|
| 2976 |
return;
|
| 2977 |
}
|
| 2978 |
plate._mfQCancelAt=0;
|
| 2979 |
+
if(cancelQueuedUnit(Bb,S.i,cancelSnapshot)){ if(typeof sfx==='function') sfx('ui'); renderQueue(); }
|
| 2980 |
};
|
| 2981 |
mfHudBindQueueCancel(plate,requestCancel);
|
| 2982 |
row.appendChild(plate);
|
src/ui/input.js
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/ui/render3d.js
CHANGED
|
@@ -2324,7 +2324,7 @@ function render(dtDraw){
|
|
| 2324 |
build even though the panel button reported "RALLY SET β TAP TO MOVE".
|
| 2325 |
Ground decals, so they inherit depth and survive camera tilt. */
|
| 2326 |
for(const Bd of blds){
|
| 2327 |
-
if(!Bd.alive||Bd.team
|
| 2328 |
const R2=Bd.rally;
|
| 2329 |
if(!vis(R2.x,R2.y,40))continue;
|
| 2330 |
const rh=gh(R2.x,R2.y),rp=0.55+0.45*Math.sin(t*3.1+R2.x*0.01);
|
|
|
|
| 2324 |
build even though the panel button reported "RALLY SET β TAP TO MOVE".
|
| 2325 |
Ground decals, so they inherit depth and survive camera tilt. */
|
| 2326 |
for(const Bd of blds){
|
| 2327 |
+
if(!Bd.alive||!(typeof mfLocalOwnsBuilding==='function'?mfLocalOwnsBuilding(Bd):Bd.team===0)||!Bd.rally)continue;
|
| 2328 |
const R2=Bd.rally;
|
| 2329 |
if(!vis(R2.x,R2.y,40))continue;
|
| 2330 |
const rh=gh(R2.x,R2.y),rp=0.55+0.45*Math.sin(t*3.1+R2.x*0.01);
|
src/updater.js
CHANGED
|
@@ -42,14 +42,14 @@
|
|
| 42 |
============================================================================ */
|
| 43 |
|
| 44 |
/* Bumped by the release script. Compared against the manifest's `version`. */
|
| 45 |
-
const APP_VERSION = '1.33.
|
| 46 |
|
| 47 |
/* Release notes for the PACKAGED build, bumped by the release script beside
|
| 48 |
APP_VERSION and PACKAGED_REV. A device that has never taken an OTA has no
|
| 49 |
download history to read notes from, and an offline device can never fetch
|
| 50 |
them, so the build carries its own copy β otherwise a fresh install shows a
|
| 51 |
permanently empty first entry in the mailbox. */
|
| 52 |
-
const APP_NOTES = "
|
| 53 |
|
| 54 |
/* The channel URL in update-config.json remains publisher-configurable, but a
|
| 55 |
production checker also needs one known-good recovery path. More importantly,
|
|
|
|
| 42 |
============================================================================ */
|
| 43 |
|
| 44 |
/* Bumped by the release script. Compared against the manifest's `version`. */
|
| 45 |
+
const APP_VERSION = '1.33.76';
|
| 46 |
|
| 47 |
/* Release notes for the PACKAGED build, bumped by the release script beside
|
| 48 |
APP_VERSION and PACKAGED_REV. A device that has never taken an OTA has no
|
| 49 |
download history to read notes from, and an offline device can never fetch
|
| 50 |
them, so the build carries its own copy β otherwise a fresh install shows a
|
| 51 |
permanently empty first entry in the mailbox. */
|
| 52 |
+
const APP_NOTES = "Overhaul β Features: cinematic loading with the original title art; authored UGA cutaway sections and upgrade navigation; hired-commander solo exploration, mission debrief and return-to-ship services; Classic opens the original Standard War Table; accepted models, commander voices and adaptive music beds. Fixes: real-time obstacle rerouting; exact rally targets and owner-visible markers for both multiplayer sides; synchronized repeat and queue cancellation with seat-correct refunds; portrait fallback; duplicate-reward protection; lossless UGA delivery; repaired Nova hull, rotors, hatch and ramp in a larger hangar; expandable loadout; download status clear of controls; Galactic music; hash-checked resumable native content staging with same-origin launch and rollback. Known incomplete work: guided practical space onboarding, optional Galactic allies/human co-op and Versus, dropped-session production restoration, and remaining mobile acceptance. These are not completed features.";
|
| 53 |
|
| 54 |
/* The channel URL in update-config.json remains publisher-configurable, but a
|
| 55 |
production checker also needs one known-good recovery path. More importantly,
|
sw.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
only after the player has successfully fetched them. */
|
| 4 |
'use strict';
|
| 5 |
|
| 6 |
-
const MF_SW_VERSION = '1.33.
|
| 7 |
const MF_SW_PREFIX = 'massfront-pwa-';
|
| 8 |
const MF_SW_CACHE = MF_SW_PREFIX + MF_SW_VERSION;
|
| 9 |
/* Runtime code stays in the shell cache so an installed PWA always reaches the
|
|
|
|
| 3 |
only after the player has successfully fetched them. */
|
| 4 |
'use strict';
|
| 5 |
|
| 6 |
+
const MF_SW_VERSION = '1.33.76-shell3';
|
| 7 |
const MF_SW_PREFIX = 'massfront-pwa-';
|
| 8 |
const MF_SW_CACHE = MF_SW_PREFIX + MF_SW_VERSION;
|
| 9 |
/* Runtime code stays in the shell cache so an installed PWA always reaches the
|