feat: download drops — permanent % stat bonuses + rare timed buffs
Reworks qBitTorrz progression with a download-loot layer on top of the existing currency/Shop spine:
- Every completed download rolls loot: ~34% chance of a PERMANENT +% bonus to a stat
(Download Speed / Seed Income / Connected Seeds), with a rarity curve — small % common,
big % rare (Common..Legendary). Bigger torrents drop bigger bonuses (tierFactor).
- ~5% chance instead of a TIMED multiplier buff ('better server access': x2-x10 for 30-60s).
Same-stat buffs take the strongest (no runaway stacking); different stats combine.
- New Bonuses detail tab tracks accumulated %s, active buffs (live countdown), and a drop log;
rarity-colored toasts; active buffs surface in the event banner.
- Multipliers integrate into downMult/upMultGlobal/torrentSeeds. All loot resets on Migration.
- Relabeled the Upgrades dialog as the Shop (buy upgrades + unlock tiers). Robust sanitizeState
for the new fields. Also fixed a latent event-banner bug (inline display:none blocked all banners).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ecb0fd61e2
commit
7ec834944b
143
index.html
143
index.html
@ -233,6 +233,9 @@
|
||||
font-size:12px;max-width:330px;border-left:4px solid var(--green);animation:tin .25s ease;}
|
||||
.toast.warn{border-left-color:var(--orange);} .toast.bad{border-left-color:var(--red);}
|
||||
.toast.info{border-left-color:var(--blue);} .toast.prestige{border-left-color:#8a2fb0;}
|
||||
.toast.drop-common{border-left-color:#8a8a8a;} .toast.drop-uncommon{border-left-color:#4f9e3a;}
|
||||
.toast.drop-rare{border-left-color:#2f7fd0;} .toast.drop-epic{border-left-color:#8a2fb0;}
|
||||
.toast.drop-legendary{border-left-color:#d98a1f;box-shadow:0 4px 14px rgba(0,0,0,.35),0 0 0 1px #d98a1f;}
|
||||
.toast b{display:block;margin-bottom:2px;}
|
||||
@keyframes tin{from{transform:translateX(40px);opacity:0;}to{transform:none;opacity:1;}}
|
||||
|
||||
@ -240,6 +243,7 @@
|
||||
#event-banner{display:none;align-items:center;gap:10px;padding:4px 12px;font-weight:600;font-size:11px;flex:0 0 auto;}
|
||||
#event-banner.burst{display:flex;background:linear-gradient(#dff5d0,#cdeeb6);color:#2c5e16;border-bottom:1px solid #9fce7e;}
|
||||
#event-banner.throttle{display:flex;background:linear-gradient(#ffe0d6,#ffcbbb);color:#8a3115;border-bottom:1px solid #e7a78f;}
|
||||
#event-banner.buff{display:flex;background:linear-gradient(#efe1f7,#e2cdf0);color:#5e1d7e;border-bottom:1px solid #c79fe0;}
|
||||
.scroller{font-family:var(--mono);}
|
||||
::-webkit-scrollbar{width:14px;height:14px;}
|
||||
::-webkit-scrollbar-thumb{background:#c3c3c3;border:3px solid var(--bg);border-radius:8px;}
|
||||
@ -297,7 +301,7 @@
|
||||
<button class="tbtn" data-act="pause-all"><span class="ic">⏸️</span><span class="lb">Pause All</span></button>
|
||||
<button class="tbtn" id="tb-remove" data-act="remove-sel"><span class="ic">🗑️</span><span class="lb">Remove</span></button>
|
||||
<div class="tsep"></div>
|
||||
<button class="tbtn" data-act="open-store"><span class="ic">🛠️</span><span class="lb">Upgrades</span></button>
|
||||
<button class="tbtn" data-act="open-store"><span class="ic">🛒</span><span class="lb">Shop</span></button>
|
||||
<button class="tbtn" id="tb-prestige" data-act="open-prestige"><span class="ic">🛰️</span><span class="lb">Migrate</span></button>
|
||||
<div id="tb-stats">
|
||||
<div class="stat"><span class="k">Data</span><span class="v data" id="hud-data">0 B</span></div>
|
||||
@ -325,6 +329,7 @@
|
||||
<div id="detail">
|
||||
<div id="detail-tabs">
|
||||
<div class="dtab active" data-dtab="general">General</div>
|
||||
<div class="dtab" data-dtab="bonuses">Bonuses</div>
|
||||
<div class="dtab" data-dtab="trackers">Trackers</div>
|
||||
<div class="dtab" data-dtab="peers">Peers</div>
|
||||
<div class="dtab" data-dtab="irc">IRC / Swarm</div>
|
||||
@ -542,6 +547,11 @@ function freshState(){
|
||||
dtab:'general',
|
||||
sci:false,
|
||||
seenCatalog:{}, // catalog ids the player has seen in the Add dialog
|
||||
// download loot — completed downloads occasionally "contain" bonuses (reset on prestige)
|
||||
bonusPct:{down:0, income:0, seeds:0}, // accumulated permanent % bonuses per stat
|
||||
buffs:[], // active timed buffs {stat,mult,until,name,rarity}
|
||||
dropLog:[], // recent drop history
|
||||
drops:0, // lifetime-this-run drop count
|
||||
trackerList:defaultTrackers(), // announced trackers, each {url,seeds,peers,status}
|
||||
rssFilter:'*', // glob filter for RSS auto-download
|
||||
rssOn:true, // auto-download matching releases when rss is unlocked
|
||||
@ -565,11 +575,18 @@ const hasPerkUnlock = id => (G.perks[id]||0)>0;
|
||||
|
||||
function perkMult(id){ const p=PERK_BY_ID[id]; return Math.pow(p.mult, plvl(id)); }
|
||||
|
||||
// download-loot multipliers: permanent % bonuses + active timed buffs, per stat (down/income/seeds)
|
||||
function permMult(stat){ return 1 + ((G.bonusPct&&G.bonusPct[stat])||0)/100; }
|
||||
// strongest active buff per stat (no same-stat stacking → no runaway); different stats still combine
|
||||
function buffMult(stat){ let m=1; const n=now(); for(const b of (G.buffs||[])){ if(b.stat===stat && b.until>n && b.mult>m) m=b.mult; } return m; }
|
||||
function bonusMult(stat){ return permMult(stat)*buffMult(stat); }
|
||||
|
||||
function downMult(){
|
||||
let m=1;
|
||||
m*=Math.pow(UP_BY_ID.bandwidth.mult, lvl('bandwidth'));
|
||||
m*=Math.pow(UP_BY_ID.overclock.mult, lvl('overclock'));
|
||||
m*=perkMult('dpi');
|
||||
m*=bonusMult('down'); // download-loot bonus
|
||||
if(now()<G.burstUntil) m*=10;
|
||||
if(now()<G.throttleUntil && !hasUnlock('quantum')) m*=0.4;
|
||||
return m;
|
||||
@ -580,6 +597,7 @@ function upMultGlobal(){
|
||||
m*=Math.pow(UP_BY_ID.compress.mult, lvl('compress'));
|
||||
m*=Math.pow(UP_BY_ID.ratio.mult, lvl('ratio'));
|
||||
m*=perkMult('ghost');
|
||||
m*=bonusMult('income'); // download-loot bonus
|
||||
if(now()<G.throttleUntil && !hasUnlock('quantum')) m*=0.4;
|
||||
return m;
|
||||
}
|
||||
@ -616,7 +634,7 @@ function peerPrioBonus(t){ return (t&&t.dropLeechers?0.15:0)+(t&&t.prioSeeds?0.1
|
||||
function torrentSeeds(t){
|
||||
const c=CAT_BY_ID[t.cid];
|
||||
const base=(c?c.seeds:1)+baseSeedBonus()+trackerSeedBonus();
|
||||
return Math.max(1, Math.round(base*(1+peerPrioBonus(t))));
|
||||
return Math.max(1, Math.round(base*(1+peerPrioBonus(t))*bonusMult('seeds'))); // ×seeds loot bonus
|
||||
}
|
||||
function perPeerMult(){ return Math.pow(UP_BY_ID.perpeer.mult, lvl('perpeer')); }
|
||||
// Fraction of Max Line Speed actually achieved: min(1, seeds × bwPerPeer / line).
|
||||
@ -650,6 +668,7 @@ function now(){ return Date.now(); }
|
||||
function step(dt){ // dt seconds
|
||||
// events
|
||||
handleEvents();
|
||||
pruneBuffs();
|
||||
// per-torrent
|
||||
for(const t of G.torrents){
|
||||
if(t.status==='downloading'){
|
||||
@ -664,6 +683,7 @@ function step(dt){ // dt seconds
|
||||
} else if(t.downloaded >= c.size){
|
||||
t.downloaded = c.size; t.status='seeding'; t.completedAt=now();
|
||||
toast('info','Download complete', t.name.slice(0,40)+'<br>Now <b>seeding</b> for '+fmtSpd(torrentUpSpeed(t)));
|
||||
rollLoot(t); // the completed download may have "contained" a bonus
|
||||
}
|
||||
} else if(t.status==='seeding'){
|
||||
const sp=torrentUpSpeed(t);
|
||||
@ -711,6 +731,76 @@ function handleEvents(){
|
||||
|
||||
function rand(a,b){ return a + Math.random()*(b-a); }
|
||||
|
||||
/* ---- download loot / drops ------------------------------------------------
|
||||
Every COMPLETED download rolls for a bonus it "contained":
|
||||
- occasional PERMANENT % bonus to a stat (rarer = bigger), or
|
||||
- much rarer, a TIMED multiplier buff ("better server access"). Bigger torrents
|
||||
drop bigger bonuses (tierFactor). All loot resets on prestige. */
|
||||
const DROP = { timedChance:0.05, permChance:0.34 }; // per completion
|
||||
const RARITY = {
|
||||
Common: {c:'#8a8a8a', g:'•'},
|
||||
Uncommon: {c:'#4f9e3a', g:'◆'},
|
||||
Rare: {c:'#2f7fd0', g:'★'},
|
||||
Epic: {c:'#8a2fb0', g:'✦'},
|
||||
Legendary: {c:'#d98a1f', g:'✸'},
|
||||
};
|
||||
const STAT_LABEL = { down:'Download Speed', income:'Seed Income', seeds:'Connected Seeds' };
|
||||
const STAT_ICON = { down:'⬇️', income:'⬆️', seeds:'🌱' };
|
||||
function tierFactor(c){
|
||||
switch(c.req){
|
||||
case 'singularity': return 2.4; case 'darknet': return 2.0; case 'fiber': return 1.6;
|
||||
case 'private': return 1.3; case 'cat6': return 1.1; default: return 1.0;
|
||||
}
|
||||
}
|
||||
// permanent % bonus — small common, big rare (the rarity curve)
|
||||
function rollPermBonus(c){
|
||||
const r=Math.random(); let pct, rarity;
|
||||
if(r<0.56){ pct=rand(1,3); rarity='Common'; }
|
||||
else if(r<0.83){ pct=rand(4,7); rarity='Uncommon'; }
|
||||
else if(r<0.95){ pct=rand(8,15); rarity='Rare'; }
|
||||
else if(r<0.99){ pct=rand(16,28); rarity='Epic'; }
|
||||
else { pct=rand(30,55); rarity='Legendary'; }
|
||||
pct *= tierFactor(c);
|
||||
const sr=Math.random(); const stat = sr<0.40?'income' : sr<0.72?'down' : 'seeds';
|
||||
return { stat, pct:+pct.toFixed(1), rarity };
|
||||
}
|
||||
// timed buff — rarer, multipliers from better server access
|
||||
const BUFF_TABLE = [
|
||||
{p:0.50, stat:'seeds', mult:2, dur:30, name:'Private Tracker Invite', rarity:'Uncommon'},
|
||||
{p:0.28, stat:'seeds', mult:3, dur:45, name:'Seedbox Trial Access', rarity:'Rare'},
|
||||
{p:0.15, stat:'down', mult:5, dur:45, name:'Fiber Burst', rarity:'Epic'},
|
||||
{p:0.07, stat:'income', mult:10, dur:60, name:'0-Day Scene Access', rarity:'Legendary'},
|
||||
];
|
||||
function rollTimedBuff(){ let r=Math.random(),a=0; for(const b of BUFF_TABLE){ a+=b.p; if(r<a) return b; } return BUFF_TABLE[0]; }
|
||||
|
||||
function rollLoot(t, silent){
|
||||
const c=CAT_BY_ID[t.cid]; if(!c) return;
|
||||
const r=Math.random();
|
||||
if(r < DROP.timedChance){
|
||||
if(silent) return; // timed buffs are live-only
|
||||
grantTimedBuff(t, rollTimedBuff());
|
||||
} else if(r < DROP.timedChance + DROP.permChance){
|
||||
grantPermBonus(t, rollPermBonus(c), silent);
|
||||
}
|
||||
}
|
||||
function grantPermBonus(t, b, silent){
|
||||
G.bonusPct[b.stat] = (G.bonusPct[b.stat]||0) + b.pct;
|
||||
G.drops = (G.drops||0)+1;
|
||||
G.dropLog.unshift({ kind:'perm', name:CAT_BY_ID[t.cid].name, stat:b.stat, pct:b.pct, rarity:b.rarity });
|
||||
if(G.dropLog.length>40) G.dropLog.pop();
|
||||
if(!silent) toast('drop-'+b.rarity.toLowerCase(), RARITY[b.rarity].g+' '+b.rarity+' — +'+b.pct+'% '+STAT_LABEL[b.stat],
|
||||
'contained in '+CAT_BY_ID[t.cid].name.slice(0,34));
|
||||
}
|
||||
function grantTimedBuff(t, b){
|
||||
G.buffs.push({ stat:b.stat, mult:b.mult, until:now()+b.dur*1000, name:b.name, rarity:b.rarity });
|
||||
G.drops = (G.drops||0)+1;
|
||||
G.dropLog.unshift({ kind:'buff', name:CAT_BY_ID[t.cid].name, stat:b.stat, mult:b.mult, dur:b.dur, rarity:b.rarity, label:b.name });
|
||||
if(G.dropLog.length>40) G.dropLog.pop();
|
||||
toast('drop-'+b.rarity.toLowerCase(), '⚡ '+b.name+' — ×'+b.mult+' '+STAT_LABEL[b.stat]+' for '+b.dur+'s',
|
||||
'unpacked from '+CAT_BY_ID[t.cid].name.slice(0,30));
|
||||
}
|
||||
function pruneBuffs(){ const n=now(); if(G.buffs.some(b=>b.until<=n)) G.buffs=G.buffs.filter(b=>b.until>n); }
|
||||
|
||||
// glob like *.upg or *REPACK* -> case-insensitive regex
|
||||
function globToRe(glob){
|
||||
if(!glob||glob==='*'||glob.trim()==='') return /.*/i;
|
||||
@ -888,6 +978,7 @@ function doPrestige(){
|
||||
// reset run state — abandon local hardware & trackers, keep credits/perks
|
||||
G.data=0; G.runUp=0; G.torrents=[]; G.upgrades={}; G.sel=null;
|
||||
G.trackerList=defaultTrackers();
|
||||
G.bonusPct={down:0,income:0,seeds:0}; G.buffs=[]; G.dropLog=[]; G.drops=0; // loot resets on migration
|
||||
G.burstUntil=0;G.nextBurst=0;G.throttleUntil=0;G.nextThrottle=0;G.nextMagnet=0;
|
||||
toast('prestige','🛰️ Migration Complete','Local hardware scrubbed. Redeployed onto an encrypted Virtual Seedbox Cluster.<br>Earned <b>'+gain+' Crypto-Credits</b>.');
|
||||
ircSys('--- ISP throttle evaded. migrated to decentralized seedbox cluster. new node online ---');
|
||||
@ -1097,8 +1188,10 @@ function renderStatus(){
|
||||
`${dl} downloading · ${sd} seeding · earning ${fmtSpd(totalUpSpeed())} Data`;
|
||||
// event banner
|
||||
const eb=document.getElementById('event-banner');
|
||||
if(now()<G.burstUntil){ eb.className='burst'; eb.innerHTML='⚡ PEER SWARM BURST — download ×10 ('+Math.ceil((G.burstUntil-now())/1000)+'s)'; }
|
||||
else if(now()<G.throttleUntil && !hasUnlock('quantum')){ eb.className='throttle'; eb.innerHTML='🐌 ISP BANDWIDTH THROTTLING — speeds ×0.4 ('+Math.ceil((G.throttleUntil-now())/1000)+'s) — buy Quantum Encryption to immunize'; }
|
||||
const aBuffs=G.buffs.filter(b=>b.until>now());
|
||||
if(now()<G.burstUntil){ eb.className='burst'; eb.style.display='flex'; eb.innerHTML='⚡ PEER SWARM BURST — download ×10 ('+Math.ceil((G.burstUntil-now())/1000)+'s)'; }
|
||||
else if(now()<G.throttleUntil && !hasUnlock('quantum')){ eb.className='throttle'; eb.style.display='flex'; eb.innerHTML='🐌 ISP BANDWIDTH THROTTLING — speeds ×0.4 ('+Math.ceil((G.throttleUntil-now())/1000)+'s) — buy Quantum Encryption to immunize'; }
|
||||
else if(aBuffs.length){ const b=aBuffs[aBuffs.length-1]; eb.className='buff'; eb.style.display='flex'; eb.innerHTML='⚡ '+b.name.toUpperCase()+' — ×'+b.mult+' '+STAT_LABEL[b.stat]+' ('+Math.ceil((b.until-now())/1000)+'s)'+(aBuffs.length>1?' · +'+(aBuffs.length-1)+' more':''); }
|
||||
else { eb.className=''; eb.style.display='none'; }
|
||||
}
|
||||
|
||||
@ -1107,17 +1200,40 @@ function renderDetail(){
|
||||
document.querySelectorAll('.dtab').forEach(d=>d.classList.toggle('active',d.dataset.dtab===G.dtab));
|
||||
const body=document.getElementById('detail-body');
|
||||
const t=G.torrents.find(x=>x.uid===G.sel);
|
||||
if(!t && G.dtab!=='irc' && G.dtab!=='trackers'){
|
||||
if(!t && G.dtab!=='irc' && G.dtab!=='trackers' && G.dtab!=='bonuses'){
|
||||
body.innerHTML='<div class="det-empty">Select a torrent to see its details.</div>';
|
||||
if(G.dtab==='speed') drawSpeed();
|
||||
return;
|
||||
}
|
||||
if(G.dtab==='general'){ body.innerHTML=detGeneral(t); }
|
||||
else if(G.dtab==='bonuses'){ body.innerHTML=detBonuses(); }
|
||||
else if(G.dtab==='trackers'){ body.innerHTML=detTrackers(t); }
|
||||
else if(G.dtab==='peers'){ body.innerHTML=detPeers(t); }
|
||||
else if(G.dtab==='irc'){ body.innerHTML=detIrc(t); const log=document.getElementById('irc-log'); if(log) log.scrollTop=log.scrollHeight; }
|
||||
else if(G.dtab==='speed'){ body.innerHTML=detSpeed(t); drawSpeed(); }
|
||||
}
|
||||
function detBonuses(){
|
||||
const n=now();
|
||||
const rows=['down','income','seeds'].map(s=>{
|
||||
const perm=G.bonusPct[s]||0, bf=buffMult(s), tot=permMult(s)*bf;
|
||||
return `<div class="k">${STAT_ICON[s]} ${STAT_LABEL[s]}</div><div class="v">+${perm.toFixed(1)}% permanent${bf>1?` · <span class="col-up">×${bf.toFixed(2)} buff</span>`:''} → <b>×${tot.toFixed(2)}</b></div>`;
|
||||
}).join('');
|
||||
const active=G.buffs.filter(b=>b.until>n);
|
||||
const buffHtml = active.length ? active.map(b=>{
|
||||
const left=Math.max(0,(b.until-n)/1000);
|
||||
return `<div class="stall-row"><span style="flex:1"><b style="color:${RARITY[b.rarity].c}">${b.name}</b> — ×${b.mult} ${STAT_LABEL[b.stat]}</span><span class="col-up" style="font-family:var(--mono)">${left.toFixed(0)}s</span></div>`;
|
||||
}).join('') : '<div style="color:var(--muted)">None active. A rare few downloads contain temporary server-access multipliers.</div>';
|
||||
const log = G.dropLog.slice(0,16).map(d=>{
|
||||
const col=RARITY[d.rarity]?RARITY[d.rarity].c:'#888';
|
||||
if(d.kind==='buff') return `<div>⚡ <b style="color:${col}">${d.label}</b> ×${d.mult} ${STAT_LABEL[d.stat]} <span style="color:var(--muted)">— ${d.name.slice(0,30)}</span></div>`;
|
||||
return `<div><span style="color:${col}">${(RARITY[d.rarity]||{}).g||'•'} ${d.rarity}</span> +${d.pct}% ${STAT_LABEL[d.stat]} <span style="color:var(--muted)">— ${d.name.slice(0,30)}</span></div>`;
|
||||
}).join('') || '<div style="color:var(--muted)">No drops yet — complete downloads to find bonuses.</div>';
|
||||
return `<div style="margin-bottom:6px;color:var(--muted)">Completed downloads occasionally <b>contain bonuses</b> — small % boosts are common, big ones rare; a rare few hold temporary multipliers. <b>${G.drops}</b> found this run (resets on Migration).</div>
|
||||
<div class="kv" style="margin-bottom:8px">${rows}</div>
|
||||
<div style="font-weight:600;margin:4px 0 3px">⚡ Active Timed Buffs</div>${buffHtml}
|
||||
<div style="font-weight:600;margin:8px 0 3px">Recent Drops</div>
|
||||
<div style="max-height:84px;overflow:auto;border:1px solid var(--line);background:#fcfcfc;padding:5px;font-size:11px;line-height:1.5">${log}</div>`;
|
||||
}
|
||||
function detTrackers(t){
|
||||
const totSeeds=trackerSeedBonus();
|
||||
const cost=announceCost();
|
||||
@ -1381,7 +1497,7 @@ function renderStoreModal(){
|
||||
const ups=UPGRADES.filter(u=>u.tab===storeTab);
|
||||
const cards=ups.map(u=>upgradeCard(u)).join('');
|
||||
openModal(`
|
||||
<div class="modal-title">🛠️ Network Options & Upgrades<span class="x" data-act="close-modal">✕</span></div>
|
||||
<div class="modal-title">🛒 Shop — Network Upgrades & Tier Unlocks<span class="x" data-act="close-modal">✕</span></div>
|
||||
<div class="mtabs">${tabHtml}</div>
|
||||
<div class="modal-body"><div class="store-list" id="store-list">${cards}</div></div>
|
||||
<div class="modal-foot"><span style="margin-right:auto;color:var(--muted)">Data available: <b class="col-up" id="store-data">${fmt(G.data)}</b></span>
|
||||
@ -1491,7 +1607,8 @@ function openHelp(){
|
||||
<ol style="padding-left:20px">
|
||||
<li><b>➕ Add Torrent</b> → pick a release. It downloads at <code>min(Max Line Speed, Seeds × Bandwidth/Peer)</code>.</li>
|
||||
<li>At 100% a torrent flips to <b>🌱 Seeding</b> and generates <b>Data</b> — your currency — forever.</li>
|
||||
<li>Spend Data in <b>🛠️ Upgrades</b>: advance your <b>Network Hardware</b> tier (56k → Fiber → Telecom Trunk) to raise your line ceiling, plus storage slots and unlocks (Cat6 → Private → Fiber).</li>
|
||||
<li>Spend Data in the <b>🛒 Shop</b>: advance your <b>Network Hardware</b> tier (56k → Fiber → Telecom Trunk) to raise your line ceiling, plus storage slots and tier unlocks (Cat6 → Private → Fiber).</li>
|
||||
<li><b>🎁 Drops:</b> completed downloads occasionally <b>contain bonuses</b> — small permanent % stat boosts are common, big ones rare; a rare few hold <b>timed multipliers</b> (e.g. ×3 seeds from server access). Track them in the <b>Bonuses</b> tab. (Drops reset on Migration.)</li>
|
||||
<li><b>Peer-limited?</b> Open the <b>Trackers</b> tab and <b>Announce to New Trackers</b> — each adds connected seeds to every torrent, pushing your speed up toward the line cap. In <b>Peers</b>, tick <i>Drop Leechers</i> / <i>Prioritize Seeds</i> for +15% each on a specific file.</li>
|
||||
<li><b>📡 RSS</b> auto-downloads releases matching a glob filter; <b>announce-bot</b> drops pre-release <b>🧲 magnets</b> in the IRC tab — grab them to skip the slow start.</li>
|
||||
<li><b>💎 Rare</b> torrents stall at 99.9% — open <b>IRC / Swarm</b> and request the last piece from an archivist.</li>
|
||||
@ -1582,6 +1699,12 @@ function sanitizeState(){
|
||||
if(!G.upgrades||typeof G.upgrades!=='object'||Array.isArray(G.upgrades)) G.upgrades={};
|
||||
if(!G.perks||typeof G.perks!=='object'||Array.isArray(G.perks)) G.perks={};
|
||||
if(!G.seenCatalog||typeof G.seenCatalog!=='object'||Array.isArray(G.seenCatalog)) G.seenCatalog={};
|
||||
if(!G.bonusPct||typeof G.bonusPct!=='object'||Array.isArray(G.bonusPct)) G.bonusPct={down:0,income:0,seeds:0};
|
||||
['down','income','seeds'].forEach(s=>{ G.bonusPct[s]=num(G.bonusPct[s],0); });
|
||||
if(!Array.isArray(G.buffs)) G.buffs=[];
|
||||
G.buffs=G.buffs.filter(b=>b&&['down','income','seeds'].includes(b.stat)&&typeof b.until==='number'&&b.until>Date.now()&&num(b.mult,0)>0);
|
||||
if(!Array.isArray(G.dropLog)) G.dropLog=[];
|
||||
G.drops=Math.floor(num(G.drops,0));
|
||||
if(!Array.isArray(G.trackerList)||!G.trackerList.length) G.trackerList=defaultTrackers();
|
||||
G.trackerList=G.trackerList.filter(t=>t&&typeof t.url==='string').map(t=>({
|
||||
url:t.url, seeds:num(t.seeds,1), peers:num(t.peers,1), status:t.status||'working'}));
|
||||
@ -1634,7 +1757,7 @@ function simulateAway(dt){
|
||||
t.downloaded+=sp*sdt; G.totalDown+=sp*sdt;
|
||||
const c=CAT_BY_ID[t.cid];
|
||||
if(c.rare && t.downloaded>=c.size*0.999 && t.downloaded<c.size){ t.downloaded=c.size*0.999; t.status='stalled'; }
|
||||
else if(t.downloaded>=c.size){ t.downloaded=c.size; t.status='seeding'; t.completedAt=Date.now(); }
|
||||
else if(t.downloaded>=c.size){ t.downloaded=c.size; t.status='seeding'; t.completedAt=Date.now(); rollLoot(t, true); }
|
||||
} else if(t.status==='seeding'){
|
||||
const c=CAT_BY_ID[t.cid];
|
||||
const sp=c.up*upMultGlobal()+seedboxFlat();
|
||||
@ -1794,8 +1917,8 @@ function tick(){
|
||||
renderHud(); renderTable(); renderStatus();
|
||||
// sidebar counts change -> cheap re-render occasionally
|
||||
if(Math.floor(t/700)!==Math.floor((t-TICK_MS)/700)) renderSidebar();
|
||||
if(G.dtab!=='speed') { if(G.sel!=null && (G.dtab==='general')) renderDetail(); }
|
||||
else renderDetail();
|
||||
if(G.dtab==='speed'||G.dtab==='bonuses') renderDetail();
|
||||
else if(G.sel!=null && G.dtab==='general') renderDetail();
|
||||
if(G.dtab==='irc') { const log=document.getElementById('irc-log'); }
|
||||
// live modal numbers
|
||||
if(modalKind==='store'){ const d=document.getElementById('store-data'); if(d) d.textContent=fmt(G.data); updateStoreAfford(); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user