Spaces:
Running
Running
File size: 15,529 Bytes
b51e03a 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 3597f5a fcce7e3 b51e03a fcce7e3 b51e03a fcce7e3 3597f5a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | ;
;
/* ============================================================================
DAILY ORDERS & BOOSTERS
----------------------------------------------------------------------------
Two systems that only make sense together: orders are a reason to open the
game today, and boosters are what an order pays out with. A currency reward
alone buys a percentage in a store; a booster changes the next hour of play,
which is a better prize and a better hook.
Orders are drawn from the day's date rather than stored, so the same three
appear on every device the player owns and nobody can reroll by reinstalling.
Progress is tracked against what actually happened in a match, never against
a button press. Orders are also flavoured β tied to a faction, a map, a
mission type or a hazard β using globals the match itself already leaves
behind (AI.fac, curMap, goalSel, the active wildcards) rather than anything
invented for this file. A win is still a win either way; the order just
asks which kind.
Boosters are timed multipliers, not consumable charges. Buying an hour means
an hour of wall clock β a player who has to stop after ten minutes has lost
fifty of them, which is exactly why the durations are offered as a choice.
============================================================================ */
const BOOSTS={
xp :{nm:'FIELD PROMOTION', em:'β¬', ds:'+60% career XP', mul:1.60},
cores:{nm:'SALVAGE RIGHTS', em:'⬑', ds:'+60% cores earned', mul:1.60},
res :{nm:'PRIORITY SUPPLY', em:'β', ds:'+20% mass and energy', mul:1.20},
build:{nm:'FORCED TEMPO', em:'β', ds:'+25% build speed', mul:1.25},
};
const BOOST_DUR=[
{k:'30m', s:1800, nm:'30 MIN'},
{k:'1h', s:3600, nm:'1 HOUR'},
{k:'24h', s:86400, nm:'24 HOURS'},
];
function boostState(){ META.boosts=META.boosts||{}; return META.boosts; }
function boostLeft(k){
const b=boostState()[k];
return b? Math.max(0,(b-Date.now())/1000) : 0;
}
function boostActive(k){ return boostLeft(k)>0; }
/* Granting an active booster EXTENDS it. Overwriting would mean a 24-hour
reward could shorten a 24-hour booster the player already had. */
function grantBoost(k,secs){
const b=boostState();
const base=Math.max(Date.now(), b[k]||0);
b[k]=base+secs*1000;
metaSave(); renderBoosts();
}
function boostMul(k){ return boostActive(k)? BOOSTS[k].mul : 1; }
function fmtLeft(s){
s=Math.max(0,s|0);
if(s>=3600) return ((s/3600)|0)+'h '+(((s%3600)/60)|0)+'m';
if(s>=60) return ((s/60)|0)+'m';
return s+'s';
}
function renderBoosts(){
const el=document.getElementById('boostRow'); if(!el) return;
const on=Object.keys(BOOSTS).filter(boostActive);
el.style.display=on.length?'flex':'none';
el.innerHTML=on.map(k=>'<div class="boostChip">'
+(typeof itemArt==='function'?itemArt('bst_'+k,BOOSTS[k].em,18):'<span>'+BOOSTS[k].em+'</span>')
+BOOSTS[k].nm+' <b>'+fmtLeft(boostLeft(k))+'</b></div>').join('');
}
/* ---- ORDERS ---------------------------------------------------------------- */
/* Deterministic from the date: the same day gives the same three orders on
every device, and reinstalling does not reroll them. */
function dayKey(d){
d=d||new Date();
return d.getFullYear()*10000+(d.getMonth()+1)*100+d.getDate();
}
function dayRand(seed){
let s=seed>>>0;
return ()=>{ s=(s*1664525+1013904223)>>>0; return s/4294967296; };
}
/* A rotating one-line SITREP above the order list β no mechanical weight,
just a reminder that the war in src/story.js keeps moving on days you don't
rank up. Deterministic per day like everything else here. */
const SITREPS=[
'Hive density rising across all four systems.',
'Ascendancy patrols report contact on every front.',
'Coalition trade routes rerouted around Vespera Caldera.',
'Brood hive count unconfirmed β and climbing.',
'Joint operations still unauthorised past squad level.',
"Vex's broadcasts are back on the open band.",
"Renn's invoices outpace Command's replies.",
'Four homeworlds: three banners, one infestation.',
'Nordhall Frontline Shelf reports overlapping claims. Again.',
'Doctrine reminder: hold what you take, take it clean.',
];
function dailySitrep(){ return SITREPS[dayKey()%SITREPS.length]; }
const ORDERS=[
/* ---- general conduct of the war β always achievable through normal play */
{id:'win1', nm:'Take the field', ds:'Win 1 match', goal:1, stat:'wins', rw:{cores:120, boost:['xp','30m']}},
{id:'win3', nm:'Sustained offensive', ds:'Win 3 matches', goal:3, stat:'wins', rw:{cores:320, boost:['cores','1h']}},
{id:'play2', nm:'Deployment orders', ds:'Play 2 matches', goal:2, stat:'played', rw:{cores:90, boost:['res','30m']}},
{id:'kill400',nm:'Attrition', ds:'Destroy 400 units', goal:400, stat:'kills', rw:{cores:150, boost:['res','1h']}},
{id:'kill1500',nm:'Grinder', ds:'Destroy 1500 units', goal:1500,stat:'kills', rw:{cores:380, boost:['xp','1h']}},
{id:'build12',nm:'Engineering corps', ds:'Raise 12 structures', goal:12, stat:'built', rw:{cores:110, boost:['build','30m']}},
{id:'hard1', nm:'Against the odds', ds:'Win once on Hard', goal:1, stat:'hardWins', rw:{cores:400, boost:['cores','24h']}},
{id:'wild1', nm:'Calculated risk', ds:'Win a match with a wildcard active', goal:1, stat:'wcWins', rw:{cores:260, boost:['xp','1h']}},
{id:'purge', nm:'Sterilisation', ds:'Destroy 2 hives', goal:2, stat:'nests', rw:{cores:200, boost:['res','1h']}},
{id:'fast', nm:'Blitz', ds:'Win in under 8 minutes', goal:1, stat:'fastWins', rw:{cores:300, boost:['build','1h']}},
/* ---- named fronts β tied to a specific faction, same rivalry story.js tells */
{id:'vsLegion', nm:'Ascendancy front', ds:'Beat the Red Ascendancy', goal:1, stat:'winLegion', rw:{cores:190, boost:['xp','30m']}},
{id:'vsSyndicate', nm:'Coalition front', ds:'Beat the Syndicate Coalition', goal:1, stat:'winSyndicate', rw:{cores:190, boost:['res','30m']}},
{id:'vsHorde', nm:'Hive front', ds:'Beat the Umbral Brood', goal:1, stat:'winHorde', rw:{cores:190, boost:['build','30m']}},
{id:'vsNova', nm:'Frontline front', ds:'Beat Terran Frontline Command', goal:1, stat:'winNova', rw:{cores:190, boost:['xp','30m']}},
/* ---- held ground β first site of each homeworld, so a contract is completable
the day that planet opens rather than asking for a locked later region. ---- */
{id:'mapVanguard', nm:'Hold Parade Circumference', ds:'Win on Aelos Parade Circumference', goal:1, stat:'map_aelos_north_small', rw:{cores:150, boost:['xp','30m']}},
{id:'mapHighland', nm:'Hold Frostwake Grid', ds:'Win on Nordhall Frostwake Docks', goal:1, stat:'map_nordhall_isles_small', rw:{cores:150, boost:['res','30m']}},
{id:'mapIsles', nm:'Hold Buried Court', ds:'Win on Pyraeth Crown Pit', goal:1, stat:'map_pyraeth_crater_small', rw:{cores:150, boost:['build','30m']}},
{id:'mapCrater', nm:'Hold Caldera Nests', ds:'Win on Vespera Hatch Pit', goal:1, stat:'map_vespera_spire_small', rw:{cores:150, boost:['cores','30m']}},
/* ---- mission type β tied to the victory condition chosen at setup */
{id:'goalDom', nm:'Domination order', ds:'Win a Domination match', goal:1, stat:'goal_domination', rw:{cores:230, boost:['xp','1h']}},
{id:'goalPurge', nm:'Full Purge', ds:'Win a Hive Purge mission', goal:1, stat:'goal_purge', rw:{cores:230, boost:['res','1h']}},
{id:'goalSurv', nm:'Last Stand order', ds:'Win a Last Stand match', goal:1, stat:'goal_survival', rw:{cores:230, boost:['build','1h']}},
/* ---- named hazards β the harder, opt-in modifiers from Operations */
{id:'wcMoon', nm:'Blood Moon detail', ds:'Win a match during a Blood Moon', goal:1, stat:'wc_moon', rw:{cores:280, boost:['xp','1h']}},
{id:'wcTitan', nm:'Titan Rush detail', ds:'Win a match with Titan Rush active', goal:1, stat:'wc_titan', rw:{cores:320, boost:['cores','1h']}},
{id:'wcWild', nm:'Rampant Wildlife detail',ds:'Win a match with Rampant Wildlife active', goal:1, stat:'wc_wild', rw:{cores:320, boost:['build','1h']}},
];
function dailyState(){
META.daily=META.daily||{day:0,prog:{},claimed:{},streak:0,lastDay:0};
const d=dayKey();
if(META.daily.day!==d){
/* A new day. The streak survives only if yesterday was played; anything
longer than that resets it, which is what makes a streak mean something. */
const yest=dayKey(new Date(Date.now()-86400000));
META.daily.streak=(META.daily.lastDay===yest||META.daily.lastDay===d)?(META.daily.streak||0):0;
META.daily={day:d, prog:{}, claimed:{}, streak:META.daily.streak||0, lastDay:META.daily.lastDay||0};
metaSave();
}
return META.daily;
}
function todaysOrders(){
const d=dailyState().day;
const r=dayRand(d);
const pool=ORDERS.slice();
const out=[];
while(out.length<3&&pool.length) out.push(pool.splice((r()*pool.length)|0,1)[0]);
return out;
}
/* Called once at the end of every match with what actually happened. Runs
synchronously from metaGrant(), before newSkirmish() touches anything, so
the match globals below (AI.fac, curMap, goalSel, WC) still describe the
match that just ended β no separate hook into main.js/meta.js needed. */
function dailyRecord(res){
const st=dailyState();
const add=(k,v)=>{ if(v>0) st.prog[k]=(st.prog[k]||0)+v; };
add('played',1);
add('kills',res.kills|0);
add('built',res.built|0);
add('nests',res.nests|0);
if(res.win){
add('wins',1);
if(res.difficulty>=2) add('hardWins',1);
if(res.wildcards>0) add('wcWins',1);
if(res.seconds>0&&res.seconds<480) add('fastWins',1);
const fac=(typeof AI!=='undefined'&&AI.fac)||'';
if(fac==='legion') add('winLegion',1);
else if(fac==='syndicate') add('winSyndicate',1);
else if(fac==='horde') add('winHorde',1);
else if(fac==='nova') add('winNova',1);
const map=(typeof curMap!=='undefined'&&curMap)||'';
if(map) add('map_'+map,1);
const goal=(typeof goalSel!=='undefined'&&goalSel)||'';
if(goal) add('goal_'+goal,1);
const wc=(typeof WC!=='undefined'&&WC)||{};
/* OPMODS renamed Blood Moon to `dark` and Hiveworld to `swarm`; the
orders still ask for the live wildcard tokens (`moon`, `wild`) that
random picks and the sim actually set. Count both names or those
three flavour orders can never complete. */
const alias={dark:'moon',moon:'moon',swarm:'wild',wild:'wild',titan:'titan'};
for(const id in wc) if(wc[id]){
add('wc_'+id,1);
if(alias[id]&&alias[id]!==id) add('wc_'+alias[id],1);
}
st.lastDay=st.day;
if(!st.countedToday){ st.countedToday=1; st.streak=(st.streak||0)+1; }
}
/* The match earned these. Leaving them sitting on CLAIM meant a player
who never opened Daily Orders was paid nothing for finishing them. */
const granted=dailyGrantReady(true);
metaSave();
renderDaily();
DAILY_LAST={granted,ready:todaysOrders().filter(o=>orderDone(o)&&!orderClaimed(o))};
}
let DAILY_LAST={granted:[],ready:[]};
function dailyLast(){ return DAILY_LAST; }
function dailyGrantReady(quiet){
const granted=[];
for(const o of todaysOrders()){
if(orderDone(o)&&!orderClaimed(o)){
claimOrder(o,quiet);
granted.push(o);
}
}
return granted;
}
function orderDone(o){ return (dailyState().prog[o.stat]||0)>=o.goal; }
function orderClaimed(o){ return !!dailyState().claimed[o.id]; }
function claimOrder(o,quiet){
if(!orderDone(o)||orderClaimed(o)) return;
const st=dailyState();
st.claimed[o.id]=1;
/* The streak bonus rides on the reward rather than being its own currency:
one number to understand, and it grows where the player already looks. */
const sb=1+Math.min(0.5,(st.streak||0)*0.05);
const cores=Math.round(o.rw.cores*sb);
META.cores+=cores;
if(o.rw.boost){
const [k,dk]=o.rw.boost;
const dur=BOOST_DUR.find(d=>d.k===dk);
grantBoost(k,dur?dur.s:1800);
if(!quiet) toast('β '+cores+' cores Β· '+BOOSTS[k].em+' '+BOOSTS[k].nm+' for '+(dur?dur.nm.toLowerCase():'30 min'));
} else if(!quiet) toast('β '+cores+' cores');
metaSave(); renderMetaHead(); renderDaily();
if(!quiet){ sfx('level'); buzz(25); }
}
/* The menu badge: a dot only when something is actually claimable, so it is a
signal rather than decoration. */
function renderDailyDot(){
const dot=document.getElementById('dailyDot'); if(!dot) return;
const any=todaysOrders().some(o=>orderDone(o)&&!orderClaimed(o));
dot.classList.toggle('on',any);
}
function renderDaily(){
renderDailyDot();
const b2=document.getElementById('boostRow2');
if(b2){
const on=Object.keys(BOOSTS).filter(boostActive);
b2.innerHTML=on.length? on.map(k=>'<div class="boostChip">'
+(typeof itemArt==='function'?itemArt('bst_'+k,BOOSTS[k].em,18):'<span>'+BOOSTS[k].em+'</span>')
+BOOSTS[k].nm+' <b>'+fmtLeft(boostLeft(k))+'</b></div>').join('')
: '<div style="font-size:11px;color:#7fa8c6;font-weight:600">None active β complete an order to earn one</div>';
}
const g=document.getElementById('dailyList'); if(!g) return;
const st=dailyState();
const hd=document.getElementById('dailyHead');
if(hd){
const ms=new Date(); ms.setHours(24,0,0,0);
hd.innerHTML='<div style="font-size:11px;color:#7fa8c6;font-weight:700;letter-spacing:.02em;margin-bottom:5px">π° SITREP β '+dailySitrep()+'</div>'
+'<div>Resets in '+fmtLeft((ms-Date.now())/1000)
+(st.streak?' Β· π₯ '+st.streak+'-day streak (+'+Math.round(Math.min(50,st.streak*5))+'%)':'')+'</div>';
}
let h='';
for(const o of todaysOrders()){
const p=Math.min(o.goal,st.prog[o.stat]||0);
const done=p>=o.goal, claimed=orderClaimed(o);
const bk=o.rw.boost?o.rw.boost[0]:null;
h+='<div class="ordItem'+(claimed?' claimed':done?' done':'')+'" data-id="'+o.id+'">'
+'<div class="ordTx"><b>'+o.nm+'</b><span>'+o.ds+'</span>'
+'<div class="ordBarO"><div class="ordBarF" style="width:'+(p/o.goal*100)+'%"></div></div>'
+'<div class="ordRw">⬑ '+o.rw.cores+(bk?' · '+
(typeof itemArt==='function'?itemArt('bst_'+bk,BOOSTS[bk].em,14):BOOSTS[bk].em)+' '+BOOSTS[bk].nm+' '+
(BOOST_DUR.find(d=>d.k===o.rw.boost[1])||{nm:''}).nm:'')+'</div></div>'
+'<div class="ordAct">'+(claimed?'β':done?'CLAIM':p+'/'+o.goal)+'</div></div>';
}
g.innerHTML=h;
g.querySelectorAll('.ordItem').forEach(el=>{
const claim=()=>{
const o=todaysOrders().find(x=>x.id===el.dataset.id);
if(o) claimOrder(o);
};
if(typeof mfBindTap==='function') mfBindTap(el,claim); else el.addEventListener('click',claim);
});
renderBoosts();
if(typeof mfBindTabs==='function') mfBindTabs(document.getElementById('dailyScr'),'orders');
}
function initDaily(){
dailyState();
renderDaily(); renderBoosts();
/* The countdown and the booster clocks tick once a second β often enough to
feel live, rare enough to be free. */
setInterval(()=>{
const scr=document.getElementById('dailyScr');
if(scr&&scr.style.display==='flex') renderDaily();
renderBoosts(); renderDailyDot();
},1000);
}
|