feat: expand drop stats + pre-rolled mystery (?) downloads
Builds on the download-drops system: - More drop stats: added Per-Peer Bandwidth, Drop Luck (raises future drop chance & rarity), and Migration Credits (boosts the next prestige payout) alongside Download Speed / Seed Income / Connected Seeds — six % stats, weighted. Plus a rare flat STORAGE drive drop (+1 slot). - The '?' mystery: loot is now PRE-ROLLED when a torrent is added and stored on the torrent, so a download that contains something shows a pulsing purple ? badge while it downloads, revealed on completion. Drop Luck (evaluated at pre-roll) scales drop chance (capped) and biases rarity. - Bonuses tab shows all six stats + storage; new drop kinds in the log; help updated. - Integrated: perPeerMult, maxSlots (bonusSlots), doPrestige payout (credits). Robust sanitize for the new fields, and hardened torrent sanitization (default status/peerSeed/loot) so a corrupt save with a torrent missing 'status' no longer crashes render. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7ec834944b
commit
7718f5bb6b
136
index.html
136
index.html
@ -96,6 +96,10 @@
|
|||||||
table.tl tbody tr:hover{background:var(--tbl-hover);}
|
table.tl tbody tr:hover{background:var(--tbl-hover);}
|
||||||
table.tl tbody tr.sel{background:var(--sel)!important;}
|
table.tl tbody tr.sel{background:var(--sel)!important;}
|
||||||
.name-cell{display:flex;align-items:center;gap:6px;}
|
.name-cell{display:flex;align-items:center;gap:6px;}
|
||||||
|
.myst{display:inline-flex;align-items:center;justify-content:center;min-width:14px;height:14px;
|
||||||
|
border-radius:3px;background:#8a2fb0;color:#fff;font-weight:800;font-size:10px;flex:0 0 auto;
|
||||||
|
animation:mystpulse 1.6s ease-in-out infinite;}
|
||||||
|
@keyframes mystpulse{0%,100%{opacity:.55;}50%{opacity:1;}}
|
||||||
.st-dot{width:10px;height:10px;border-radius:2px;flex:0 0 auto;}
|
.st-dot{width:10px;height:10px;border-radius:2px;flex:0 0 auto;}
|
||||||
.st-down{background:var(--blue);} .st-seed{background:var(--green);}
|
.st-down{background:var(--blue);} .st-seed{background:var(--green);}
|
||||||
.st-pause{background:#9a9a9a;} .st-stall{background:var(--orange);}
|
.st-pause{background:#9a9a9a;} .st-stall{background:var(--orange);}
|
||||||
@ -548,7 +552,8 @@ function freshState(){
|
|||||||
sci:false,
|
sci:false,
|
||||||
seenCatalog:{}, // catalog ids the player has seen in the Add dialog
|
seenCatalog:{}, // catalog ids the player has seen in the Add dialog
|
||||||
// download loot — completed downloads occasionally "contain" bonuses (reset on prestige)
|
// download loot — completed downloads occasionally "contain" bonuses (reset on prestige)
|
||||||
bonusPct:{down:0, income:0, seeds:0}, // accumulated permanent % bonuses per stat
|
bonusPct:{down:0, income:0, seeds:0, perpeer:0, luck:0, credits:0}, // permanent % per stat
|
||||||
|
bonusSlots:0, // flat extra storage slots from rare Storage drops
|
||||||
buffs:[], // active timed buffs {stat,mult,until,name,rarity}
|
buffs:[], // active timed buffs {stat,mult,until,name,rarity}
|
||||||
dropLog:[], // recent drop history
|
dropLog:[], // recent drop history
|
||||||
drops:0, // lifetime-this-run drop count
|
drops:0, // lifetime-this-run drop count
|
||||||
@ -622,7 +627,7 @@ function baseSeedBonus(){
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
function maxSlots(){
|
function maxSlots(){
|
||||||
return 3 + lvl('storage1')*1 + lvl('storage2')*1 + plvl('rep');
|
return 3 + lvl('storage1')*1 + lvl('storage2')*1 + plvl('rep') + (G.bonusSlots||0);
|
||||||
}
|
}
|
||||||
function usedSlots(){ return G.torrents.length; }
|
function usedSlots(){ return G.torrents.length; }
|
||||||
|
|
||||||
@ -636,7 +641,7 @@ function torrentSeeds(t){
|
|||||||
const base=(c?c.seeds:1)+baseSeedBonus()+trackerSeedBonus();
|
const base=(c?c.seeds:1)+baseSeedBonus()+trackerSeedBonus();
|
||||||
return Math.max(1, Math.round(base*(1+peerPrioBonus(t))*bonusMult('seeds'))); // ×seeds loot bonus
|
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')); }
|
function perPeerMult(){ return Math.pow(UP_BY_ID.perpeer.mult, lvl('perpeer')) * bonusMult('perpeer'); }
|
||||||
// Fraction of Max Line Speed actually achieved: min(1, seeds × bwPerPeer / line).
|
// Fraction of Max Line Speed actually achieved: min(1, seeds × bwPerPeer / line).
|
||||||
// Below SAT_SEEDS you are peer-limited; announce trackers / prioritise peers to saturate.
|
// Below SAT_SEEDS you are peer-limited; announce trackers / prioritise peers to saturate.
|
||||||
function downFrac(seeds){ return Math.min(1, seeds/SAT_SEEDS*perPeerMult()); }
|
function downFrac(seeds){ return Math.min(1, seeds/SAT_SEEDS*perPeerMult()); }
|
||||||
@ -683,7 +688,7 @@ function step(dt){ // dt seconds
|
|||||||
} else if(t.downloaded >= c.size){
|
} else if(t.downloaded >= c.size){
|
||||||
t.downloaded = c.size; t.status='seeding'; t.completedAt=now();
|
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)));
|
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
|
revealLoot(t); // reveal whatever the download "contained" (pre-rolled on add)
|
||||||
}
|
}
|
||||||
} else if(t.status==='seeding'){
|
} else if(t.status==='seeding'){
|
||||||
const sp=torrentUpSpeed(t);
|
const sp=torrentUpSpeed(t);
|
||||||
@ -732,11 +737,13 @@ function handleEvents(){
|
|||||||
function rand(a,b){ return a + Math.random()*(b-a); }
|
function rand(a,b){ return a + Math.random()*(b-a); }
|
||||||
|
|
||||||
/* ---- download loot / drops ------------------------------------------------
|
/* ---- download loot / drops ------------------------------------------------
|
||||||
Every COMPLETED download rolls for a bonus it "contained":
|
Loot is PRE-ROLLED the moment you add a torrent (stored on t.loot), so a download
|
||||||
- occasional PERMANENT % bonus to a stat (rarer = bigger), or
|
that "contains" something shows a ? while it downloads and is revealed on completion.
|
||||||
- much rarer, a TIMED multiplier buff ("better server access"). Bigger torrents
|
Kinds: a PERMANENT +% bonus to a stat (rarer = bigger), a rarer TIMED multiplier
|
||||||
drop bigger bonuses (tierFactor). All loot resets on prestige. */
|
buff, or a rare flat STORAGE drive (+1 slot). Drop Luck raises chance & rarity.
|
||||||
const DROP = { timedChance:0.05, permChance:0.34 }; // per completion
|
Stats: down/income/seeds (live), perpeer (saturate with fewer seeds), luck (better
|
||||||
|
future drops), credits (bigger next migration). All loot resets on prestige. */
|
||||||
|
const DROP = { storageChance:0.015, timedChance:0.05, permChance:0.34 }; // per completion (×luck, capped)
|
||||||
const RARITY = {
|
const RARITY = {
|
||||||
Common: {c:'#8a8a8a', g:'•'},
|
Common: {c:'#8a8a8a', g:'•'},
|
||||||
Uncommon: {c:'#4f9e3a', g:'◆'},
|
Uncommon: {c:'#4f9e3a', g:'◆'},
|
||||||
@ -744,43 +751,72 @@ const RARITY = {
|
|||||||
Epic: {c:'#8a2fb0', g:'✦'},
|
Epic: {c:'#8a2fb0', g:'✦'},
|
||||||
Legendary: {c:'#d98a1f', g:'✸'},
|
Legendary: {c:'#d98a1f', g:'✸'},
|
||||||
};
|
};
|
||||||
const STAT_LABEL = { down:'Download Speed', income:'Seed Income', seeds:'Connected Seeds' };
|
const STAT_LABEL = { down:'Download Speed', income:'Seed Income', seeds:'Connected Seeds',
|
||||||
const STAT_ICON = { down:'⬇️', income:'⬆️', seeds:'🌱' };
|
perpeer:'Per-Peer Bandwidth', luck:'Drop Luck', credits:'Migration Credits' };
|
||||||
|
const STAT_ICON = { down:'⬇️', income:'⬆️', seeds:'🌱', perpeer:'🔗', luck:'🍀', credits:'🛰️' };
|
||||||
|
const DROP_STATS = ['down','income','seeds','perpeer','luck','credits'];
|
||||||
function tierFactor(c){
|
function tierFactor(c){
|
||||||
switch(c.req){
|
switch(c.req){
|
||||||
case 'singularity': return 2.4; case 'darknet': return 2.0; case 'fiber': return 1.6;
|
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;
|
case 'private': return 1.3; case 'cat6': return 1.1; default: return 1.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// permanent % bonus — small common, big rare (the rarity curve)
|
function pickDropStat(){
|
||||||
function rollPermBonus(c){
|
const w=[['income',26],['down',22],['seeds',20],['perpeer',12],['luck',10],['credits',10]];
|
||||||
const r=Math.random(); let pct, rarity;
|
let tot=0; for(const x of w) tot+=x[1]; let r=Math.random()*tot;
|
||||||
|
for(const [s,wt] of w){ if(r<wt) return s; r-=wt; }
|
||||||
|
return 'income';
|
||||||
|
}
|
||||||
|
// permanent % bonus — small common, big rare; Drop Luck biases toward higher rarity
|
||||||
|
function rollPermBonus(c, luckMult){
|
||||||
|
let r=Math.random(); if(luckMult>1) r=Math.pow(r, 1/luckMult);
|
||||||
|
let pct, rarity;
|
||||||
if(r<0.56){ pct=rand(1,3); rarity='Common'; }
|
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.83){ pct=rand(4,7); rarity='Uncommon'; }
|
||||||
else if(r<0.95){ pct=rand(8,15); rarity='Rare'; }
|
else if(r<0.95){ pct=rand(8,15); rarity='Rare'; }
|
||||||
else if(r<0.99){ pct=rand(16,28); rarity='Epic'; }
|
else if(r<0.99){ pct=rand(16,28); rarity='Epic'; }
|
||||||
else { pct=rand(30,55); rarity='Legendary'; }
|
else { pct=rand(30,55); rarity='Legendary'; }
|
||||||
pct *= tierFactor(c);
|
pct *= tierFactor(c);
|
||||||
const sr=Math.random(); const stat = sr<0.40?'income' : sr<0.72?'down' : 'seeds';
|
return { stat:pickDropStat(), pct:+pct.toFixed(1), rarity };
|
||||||
return { stat, pct:+pct.toFixed(1), rarity };
|
|
||||||
}
|
}
|
||||||
// timed buff — rarer, multipliers from better server access
|
// timed buff — rarer, multipliers from better server access
|
||||||
const BUFF_TABLE = [
|
const BUFF_TABLE = [
|
||||||
{p:0.50, stat:'seeds', mult:2, dur:30, name:'Private Tracker Invite', rarity:'Uncommon'},
|
{p:0.34, 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.24, 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.18, stat:'perpeer',mult:3, dur:40, name:'Priority Routing', rarity:'Rare'},
|
||||||
{p:0.07, stat:'income', mult:10, dur:60, name:'0-Day Scene Access', rarity:'Legendary'},
|
{p:0.13, stat:'down', mult:5, dur:45, name:'Fiber Burst', rarity:'Epic'},
|
||||||
|
{p:0.07, stat:'income', mult:8, dur:60, name:'Mirror Server', rarity:'Epic'},
|
||||||
|
{p:0.04, stat:'income', mult:15, 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 rollTimedBuff(){ let r=Math.random(),a=0; for(const b of BUFF_TABLE){ a+=b.p; if(r<a) return b; } return BUFF_TABLE[BUFF_TABLE.length-1]; }
|
||||||
|
|
||||||
function rollLoot(t, silent){
|
// PRE-ROLL what a torrent contains, the moment it's added
|
||||||
const c=CAT_BY_ID[t.cid]; if(!c) return;
|
function prerollLoot(t){
|
||||||
|
const c=CAT_BY_ID[t.cid]; if(!c){ t.loot=null; return; }
|
||||||
|
const luck=permMult('luck');
|
||||||
|
let sP=DROP.storageChance*luck, tP=DROP.timedChance*luck, pP=DROP.permChance*luck;
|
||||||
|
const tot=sP+tP+pP; if(tot>0.85){ const k=0.85/tot; sP*=k; tP*=k; pP*=k; }
|
||||||
const r=Math.random();
|
const r=Math.random();
|
||||||
if(r < DROP.timedChance){
|
if(r<sP) t.loot={kind:'storage', rarity:'Epic'};
|
||||||
if(silent) return; // timed buffs are live-only
|
else if(r<sP+tP) t.loot=Object.assign({kind:'buff'}, rollTimedBuff());
|
||||||
grantTimedBuff(t, rollTimedBuff());
|
else if(r<sP+tP+pP) t.loot=Object.assign({kind:'perm'}, rollPermBonus(c, luck));
|
||||||
} else if(r < DROP.timedChance + DROP.permChance){
|
else t.loot=null;
|
||||||
grantPermBonus(t, rollPermBonus(c), silent);
|
}
|
||||||
|
// REVEAL pre-rolled loot on completion (silent = offline; timed buffs skipped offline)
|
||||||
|
function revealLoot(t, silent){
|
||||||
|
if(t.loot===undefined) prerollLoot(t); // legacy torrent (saved before pre-roll existed)
|
||||||
|
const L=t.loot; t.loot=null;
|
||||||
|
if(!L) return;
|
||||||
|
if(L.kind==='storage'){
|
||||||
|
G.bonusSlots=(G.bonusSlots||0)+1; G.drops=(G.drops||0)+1;
|
||||||
|
G.dropLog.unshift({kind:'storage', name:CAT_BY_ID[t.cid].name, rarity:L.rarity});
|
||||||
|
if(G.dropLog.length>40) G.dropLog.pop();
|
||||||
|
if(!silent) toast('drop-epic','✦ Storage Drive Found — +1 Slot','contained in '+CAT_BY_ID[t.cid].name.slice(0,30));
|
||||||
|
} else if(L.kind==='buff' && STAT_LABEL[L.stat] && isFinite(L.mult) && isFinite(L.dur)){
|
||||||
|
if(silent) return;
|
||||||
|
grantTimedBuff(t, L);
|
||||||
|
} else if(L.kind==='perm' && STAT_LABEL[L.stat] && isFinite(L.pct)){
|
||||||
|
grantPermBonus(t, L, silent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function grantPermBonus(t, b, silent){
|
function grantPermBonus(t, b, silent){
|
||||||
@ -876,6 +912,7 @@ function addTorrent(cid, silent, headstart){
|
|||||||
peerSeed:Math.floor(rand(20,400)),
|
peerSeed:Math.floor(rand(20,400)),
|
||||||
dropLeechers:false, prioSeeds:false,
|
dropLeechers:false, prioSeeds:false,
|
||||||
};
|
};
|
||||||
|
prerollLoot(t); // decide now what this download "contains" (shows ? while downloading)
|
||||||
G.torrents.push(t);
|
G.torrents.push(t);
|
||||||
if(!silent) toast('','Torrent added', c.name.slice(0,42));
|
if(!silent) toast('','Torrent added', c.name.slice(0,42));
|
||||||
renderAll();
|
renderAll();
|
||||||
@ -972,13 +1009,13 @@ function creditsFor(runUp){
|
|||||||
return Math.floor(Math.pow(runUp/1e9, 0.42));
|
return Math.floor(Math.pow(runUp/1e9, 0.42));
|
||||||
}
|
}
|
||||||
function doPrestige(){
|
function doPrestige(){
|
||||||
const gain=creditsFor(G.runUp);
|
const gain=Math.floor(creditsFor(G.runUp)*permMult('credits')); // Migration Credits drops boost the payout
|
||||||
if(gain<1){ toast('bad','Not ready to migrate','Move more data before the cluster is worth deploying. Need ~'+fmt(1e9)+' uploaded this run.'); return; }
|
if(gain<1){ toast('bad','Not ready to migrate','Move more data before the cluster is worth deploying. Need ~'+fmt(1e9)+' uploaded this run.'); return; }
|
||||||
G.credits+=gain; G.prestiges++;
|
G.credits+=gain; G.prestiges++;
|
||||||
// reset run state — abandon local hardware & trackers, keep credits/perks
|
// reset run state — abandon local hardware & trackers, keep credits/perks
|
||||||
G.data=0; G.runUp=0; G.torrents=[]; G.upgrades={}; G.sel=null;
|
G.data=0; G.runUp=0; G.torrents=[]; G.upgrades={}; G.sel=null;
|
||||||
G.trackerList=defaultTrackers();
|
G.trackerList=defaultTrackers();
|
||||||
G.bonusPct={down:0,income:0,seeds:0}; G.buffs=[]; G.dropLog=[]; G.drops=0; // loot resets on migration
|
G.bonusPct={down:0,income:0,seeds:0,perpeer:0,luck:0,credits:0}; G.bonusSlots=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;
|
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>.');
|
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 ---');
|
ircSys('--- ISP throttle evaded. migrated to decentralized seedbox cluster. new node online ---');
|
||||||
@ -1150,7 +1187,7 @@ function renderTable(){
|
|||||||
const ratio = t.downloaded>0? (t.uploaded/c.size): 0;
|
const ratio = t.downloaded>0? (t.uploaded/c.size): 0;
|
||||||
html+=`<tr data-uid="${t.uid}" class="${G.sel===t.uid?'sel':''}">
|
html+=`<tr data-uid="${t.uid}" class="${G.sel===t.uid?'sel':''}">
|
||||||
<td class="num">${i+1}</td>
|
<td class="num">${i+1}</td>
|
||||||
<td><div class="name-cell" title="${c.name}">${iconFor(t)} ${c.name}</div></td>
|
<td><div class="name-cell" title="${c.name}">${iconFor(t)}${t.loot&&t.status!=='seeding'?'<span class="myst" title="This download contains something…">?</span>':''} ${c.name}</div></td>
|
||||||
<td class="num">${fmt(c.size)}</td>
|
<td class="num">${fmt(c.size)}</td>
|
||||||
<td><div class="pbar ${seeding?'seed':''} ${stall?'stall':''}"><div class="fill" style="width:${pct.toFixed(2)}%"></div><div class="txt">${pct.toFixed(seeding?0:1)}%</div></div></td>
|
<td><div class="pbar ${seeding?'seed':''} ${stall?'stall':''}"><div class="fill" style="width:${pct.toFixed(2)}%"></div><div class="txt">${pct.toFixed(seeding?0:1)}%</div></div></td>
|
||||||
<td>${statusLabel(t)}</td>
|
<td>${statusLabel(t)}</td>
|
||||||
@ -1214,10 +1251,14 @@ function renderDetail(){
|
|||||||
}
|
}
|
||||||
function detBonuses(){
|
function detBonuses(){
|
||||||
const n=now();
|
const n=now();
|
||||||
const rows=['down','income','seeds'].map(s=>{
|
const note={ luck:'drop chance & rarity', credits:'next migration payout' };
|
||||||
|
const rows=DROP_STATS.map(s=>{
|
||||||
const perm=G.bonusPct[s]||0, bf=buffMult(s), tot=permMult(s)*bf;
|
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>`;
|
const right=(s==='luck'||s==='credits')
|
||||||
}).join('');
|
? `+${perm.toFixed(1)}% <span style="color:var(--muted)">${note[s]}</span>`
|
||||||
|
: `+${perm.toFixed(1)}%${bf>1?` · <span class="col-up">×${bf.toFixed(2)} buff</span>`:''} → <b>×${tot.toFixed(2)}</b>`;
|
||||||
|
return `<div class="k">${STAT_ICON[s]} ${STAT_LABEL[s]}</div><div class="v">${right}</div>`;
|
||||||
|
}).join('') + (G.bonusSlots>0?`<div class="k">💽 Storage Slots</div><div class="v">+${G.bonusSlots} from drives found</div>`:'');
|
||||||
const active=G.buffs.filter(b=>b.until>n);
|
const active=G.buffs.filter(b=>b.until>n);
|
||||||
const buffHtml = active.length ? active.map(b=>{
|
const buffHtml = active.length ? active.map(b=>{
|
||||||
const left=Math.max(0,(b.until-n)/1000);
|
const left=Math.max(0,(b.until-n)/1000);
|
||||||
@ -1225,14 +1266,15 @@ function detBonuses(){
|
|||||||
}).join('') : '<div style="color:var(--muted)">None active. A rare few downloads contain temporary server-access multipliers.</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 log = G.dropLog.slice(0,16).map(d=>{
|
||||||
const col=RARITY[d.rarity]?RARITY[d.rarity].c:'#888';
|
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>`;
|
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>`;
|
if(d.kind==='storage') return `<div><span style="color:${col}">✦ Storage</span> +1 Slot <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>';
|
}).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>
|
return `<div style="margin-bottom:6px;color:var(--muted)">Completed downloads occasionally <b>contain bonuses</b> — a <span class="myst">?</span> in the list means this one holds something. Small % boosts are common, big ones rare; a rare few hold timed multipliers or a storage drive. <b>${G.drops}</b> found this run (resets on Migration).</div>
|
||||||
<div class="kv" style="margin-bottom:8px">${rows}</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:4px 0 3px">⚡ Active Timed Buffs</div>${buffHtml}
|
||||||
<div style="font-weight:600;margin:8px 0 3px">Recent Drops</div>
|
<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>`;
|
<div style="max-height:80px;overflow:auto;border:1px solid var(--line);background:#fcfcfc;padding:5px;font-size:11px;line-height:1.5">${log}</div>`;
|
||||||
}
|
}
|
||||||
function detTrackers(t){
|
function detTrackers(t){
|
||||||
const totSeeds=trackerSeedBonus();
|
const totSeeds=trackerSeedBonus();
|
||||||
@ -1608,7 +1650,7 @@ function openHelp(){
|
|||||||
<li><b>➕ Add Torrent</b> → pick a release. It downloads at <code>min(Max Line Speed, Seeds × Bandwidth/Peer)</code>.</li>
|
<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>At 100% a torrent flips to <b>🌱 Seeding</b> and generates <b>Data</b> — your currency — forever.</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>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>🎁 Drops:</b> a download that "contains" something shows a <span class="myst">?</span> while it downloads, revealed when it completes. Small permanent <b>% stat boosts</b> (speed, income, seeds, per-peer, luck, credits) are common, big ones rare; a rare few hold <b>timed multipliers</b> (e.g. ×3 seeds) or a <b>storage drive</b> (+1 slot). Track it all 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>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>📡 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>
|
<li><b>💎 Rare</b> torrents stall at 99.9% — open <b>IRC / Swarm</b> and request the last piece from an archivist.</li>
|
||||||
@ -1699,10 +1741,11 @@ function sanitizeState(){
|
|||||||
if(!G.upgrades||typeof G.upgrades!=='object'||Array.isArray(G.upgrades)) G.upgrades={};
|
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.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.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};
|
if(!G.bonusPct||typeof G.bonusPct!=='object'||Array.isArray(G.bonusPct)) G.bonusPct={};
|
||||||
['down','income','seeds'].forEach(s=>{ G.bonusPct[s]=num(G.bonusPct[s],0); });
|
DROP_STATS.forEach(s=>{ G.bonusPct[s]=num(G.bonusPct[s],0); });
|
||||||
|
G.bonusSlots=Math.max(0,Math.floor(num(G.bonusSlots,0)));
|
||||||
if(!Array.isArray(G.buffs)) G.buffs=[];
|
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);
|
G.buffs=G.buffs.filter(b=>b&&DROP_STATS.includes(b.stat)&&typeof b.until==='number'&&b.until>Date.now()&&num(b.mult,0)>0);
|
||||||
if(!Array.isArray(G.dropLog)) G.dropLog=[];
|
if(!Array.isArray(G.dropLog)) G.dropLog=[];
|
||||||
G.drops=Math.floor(num(G.drops,0));
|
G.drops=Math.floor(num(G.drops,0));
|
||||||
if(!Array.isArray(G.trackerList)||!G.trackerList.length) G.trackerList=defaultTrackers();
|
if(!Array.isArray(G.trackerList)||!G.trackerList.length) G.trackerList=defaultTrackers();
|
||||||
@ -1712,12 +1755,19 @@ function sanitizeState(){
|
|||||||
G.rssOn=G.rssOn!==false;
|
G.rssOn=G.rssOn!==false;
|
||||||
if(!Array.isArray(G.torrents)) G.torrents=[];
|
if(!Array.isArray(G.torrents)) G.torrents=[];
|
||||||
G.torrents=G.torrents.filter(t=>t&&CAT_BY_ID[t.cid]);
|
G.torrents=G.torrents.filter(t=>t&&CAT_BY_ID[t.cid]);
|
||||||
|
const STATUSES=['downloading','seeding','stalled','paused-dl','paused-seed'];
|
||||||
for(const t of G.torrents){
|
for(const t of G.torrents){
|
||||||
const c=CAT_BY_ID[t.cid];
|
const c=CAT_BY_ID[t.cid];
|
||||||
t.downloaded=Math.min(c.size,num(t.downloaded,0));
|
t.downloaded=Math.min(c.size,num(t.downloaded,0));
|
||||||
t.uploaded=num(t.uploaded,0);
|
t.uploaded=num(t.uploaded,0);
|
||||||
t.dropLeechers=!!t.dropLeechers; t.prioSeeds=!!t.prioSeeds;
|
t.dropLeechers=!!t.dropLeechers; t.prioSeeds=!!t.prioSeeds;
|
||||||
if(typeof t.uid!=='number'||!isFinite(t.uid)) t.uid=G.seq++;
|
if(typeof t.uid!=='number'||!isFinite(t.uid)) t.uid=G.seq++;
|
||||||
|
if(!STATUSES.includes(t.status)) t.status = t.downloaded>=c.size ? 'seeding' : 'downloading';
|
||||||
|
if(typeof t.peerSeed!=='number'||!isFinite(t.peerSeed)) t.peerSeed=Math.floor(rand(20,400));
|
||||||
|
if(typeof t.name!=='string') t.name=c.name;
|
||||||
|
if(typeof t.completedAt!=='number') t.completedAt=0;
|
||||||
|
// pre-rolled loot must be null or a well-formed descriptor
|
||||||
|
if(t.loot!=null && (typeof t.loot!=='object' || !['perm','buff','storage'].includes(t.loot.kind))) t.loot=null;
|
||||||
}
|
}
|
||||||
if(!Array.isArray(G.irc)) G.irc=[];
|
if(!Array.isArray(G.irc)) G.irc=[];
|
||||||
if(typeof G.lastSave!=='number'||!isFinite(G.lastSave)) G.lastSave=Date.now();
|
if(typeof G.lastSave!=='number'||!isFinite(G.lastSave)) G.lastSave=Date.now();
|
||||||
@ -1757,7 +1807,7 @@ function simulateAway(dt){
|
|||||||
t.downloaded+=sp*sdt; G.totalDown+=sp*sdt;
|
t.downloaded+=sp*sdt; G.totalDown+=sp*sdt;
|
||||||
const c=CAT_BY_ID[t.cid];
|
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'; }
|
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(); rollLoot(t, true); }
|
else if(t.downloaded>=c.size){ t.downloaded=c.size; t.status='seeding'; t.completedAt=Date.now(); revealLoot(t, true); }
|
||||||
} else if(t.status==='seeding'){
|
} else if(t.status==='seeding'){
|
||||||
const c=CAT_BY_ID[t.cid];
|
const c=CAT_BY_ID[t.cid];
|
||||||
const sp=c.up*upMultGlobal()+seedboxFlat();
|
const sp=c.up*upMultGlobal()+seedboxFlat();
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user