| ${i+1} |
- ${iconFor(t)} ${c.name} |
+ ${iconFor(t)}${t.loot&&t.status!=='seeding'?'?':''} ${c.name} |
${fmt(c.size)} |
${pct.toFixed(seeding?0:1)}% |
${statusLabel(t)} |
@@ -1214,10 +1251,14 @@ function renderDetail(){
}
function detBonuses(){
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;
- return `${STAT_ICON[s]} ${STAT_LABEL[s]}
+${perm.toFixed(1)}% permanent${bf>1?` · ×${bf.toFixed(2)} buff`:''} → ×${tot.toFixed(2)}
`;
- }).join('');
+ const right=(s==='luck'||s==='credits')
+ ? `+${perm.toFixed(1)}% ${note[s]}`
+ : `+${perm.toFixed(1)}%${bf>1?` · ×${bf.toFixed(2)} buff`:''} → ×${tot.toFixed(2)}`;
+ return `${STAT_ICON[s]} ${STAT_LABEL[s]}
${right}
`;
+ }).join('') + (G.bonusSlots>0?`💽 Storage Slots
+${G.bonusSlots} from drives found
`:'');
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);
@@ -1225,14 +1266,15 @@ function detBonuses(){
}).join('') : 'None active. A rare few downloads contain temporary server-access multipliers.
';
const log = G.dropLog.slice(0,16).map(d=>{
const col=RARITY[d.rarity]?RARITY[d.rarity].c:'#888';
- if(d.kind==='buff') return `⚡ ${d.label} ×${d.mult} ${STAT_LABEL[d.stat]} — ${d.name.slice(0,30)}
`;
- return `${(RARITY[d.rarity]||{}).g||'•'} ${d.rarity} +${d.pct}% ${STAT_LABEL[d.stat]} — ${d.name.slice(0,30)}
`;
+ if(d.kind==='buff') return `⚡ ${d.label} ×${d.mult} ${STAT_LABEL[d.stat]} — ${(d.name||'').slice(0,30)}
`;
+ if(d.kind==='storage') return `✦ Storage +1 Slot — ${(d.name||'').slice(0,30)}
`;
+ return `${(RARITY[d.rarity]||{}).g||'•'} ${d.rarity} +${d.pct}% ${STAT_LABEL[d.stat]} — ${(d.name||'').slice(0,30)}
`;
}).join('') || 'No drops yet — complete downloads to find bonuses.
';
- return `Completed downloads occasionally contain bonuses — small % boosts are common, big ones rare; a rare few hold temporary multipliers. ${G.drops} found this run (resets on Migration).
+ return `Completed downloads occasionally contain bonuses — a ? 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. ${G.drops} found this run (resets on Migration).
${rows}
⚡ Active Timed Buffs
${buffHtml}
Recent Drops
- ${log}
`;
+ ${log}
`;
}
function detTrackers(t){
const totSeeds=trackerSeedBonus();
@@ -1608,7 +1650,7 @@ function openHelp(){
➕ Add Torrent → pick a release. It downloads at min(Max Line Speed, Seeds × Bandwidth/Peer).
At 100% a torrent flips to 🌱 Seeding and generates Data — your currency — forever.
Spend Data in the 🛒 Shop: advance your Network Hardware tier (56k → Fiber → Telecom Trunk) to raise your line ceiling, plus storage slots and tier unlocks (Cat6 → Private → Fiber).
- 🎁 Drops: completed downloads occasionally contain bonuses — small permanent % stat boosts are common, big ones rare; a rare few hold timed multipliers (e.g. ×3 seeds from server access). Track them in the Bonuses tab. (Drops reset on Migration.)
+ 🎁 Drops: a download that "contains" something shows a ? while it downloads, revealed when it completes. Small permanent % stat boosts (speed, income, seeds, per-peer, luck, credits) are common, big ones rare; a rare few hold timed multipliers (e.g. ×3 seeds) or a storage drive (+1 slot). Track it all in the Bonuses tab. (Drops reset on Migration.)
Peer-limited? Open the Trackers tab and Announce to New Trackers — each adds connected seeds to every torrent, pushing your speed up toward the line cap. In Peers, tick Drop Leechers / Prioritize Seeds for +15% each on a specific file.
📡 RSS auto-downloads releases matching a glob filter; announce-bot drops pre-release 🧲 magnets in the IRC tab — grab them to skip the slow start.
💎 Rare torrents stall at 99.9% — open IRC / Swarm and request the last piece from an archivist.
@@ -1699,10 +1741,11 @@ 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(!G.bonusPct||typeof G.bonusPct!=='object'||Array.isArray(G.bonusPct)) G.bonusPct={};
+ 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=[];
- 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=[];
G.drops=Math.floor(num(G.drops,0));
if(!Array.isArray(G.trackerList)||!G.trackerList.length) G.trackerList=defaultTrackers();
@@ -1712,12 +1755,19 @@ function sanitizeState(){
G.rssOn=G.rssOn!==false;
if(!Array.isArray(G.torrents)) G.torrents=[];
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){
const c=CAT_BY_ID[t.cid];
t.downloaded=Math.min(c.size,num(t.downloaded,0));
t.uploaded=num(t.uploaded,0);
t.dropLeechers=!!t.dropLeechers; t.prioSeeds=!!t.prioSeeds;
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(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;
const c=CAT_BY_ID[t.cid];
if(c.rare && t.downloaded>=c.size*0.999 && 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'){
const c=CAT_BY_ID[t.cid];
const sp=c.up*upMultGlobal()+seedboxFlat();