From 7718f5bb6bce8a016dd600d64fe7416b681fc907 Mon Sep 17 00:00:00 2001 From: monster Date: Wed, 17 Jun 2026 16:35:28 +1000 Subject: [PATCH] feat: expand drop stats + pre-rolled mystery (?) downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- index.html | 136 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 93 insertions(+), 43 deletions(-) diff --git a/index.html b/index.html index 34c82ab..8e22595 100644 --- a/index.html +++ b/index.html @@ -96,6 +96,10 @@ table.tl tbody tr:hover{background:var(--tbl-hover);} table.tl tbody tr.sel{background:var(--sel)!important;} .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-down{background:var(--blue);} .st-seed{background:var(--green);} .st-pause{background:#9a9a9a;} .st-stall{background:var(--orange);} @@ -548,7 +552,8 @@ function freshState(){ 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 + 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} dropLog:[], // recent drop history drops:0, // lifetime-this-run drop count @@ -622,7 +627,7 @@ function baseSeedBonus(){ return s; } 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; } @@ -636,7 +641,7 @@ function torrentSeeds(t){ const base=(c?c.seeds:1)+baseSeedBonus()+trackerSeedBonus(); 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). // Below SAT_SEEDS you are peer-limited; announce trackers / prioritise peers to saturate. 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){ t.downloaded = c.size; t.status='seeding'; t.completedAt=now(); toast('info','Download complete', t.name.slice(0,40)+'
Now seeding 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'){ const sp=torrentUpSpeed(t); @@ -732,11 +737,13 @@ 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 + Loot is PRE-ROLLED the moment you add a torrent (stored on t.loot), so a download + that "contains" something shows a ? while it downloads and is revealed on completion. + Kinds: a PERMANENT +% bonus to a stat (rarer = bigger), a rarer TIMED multiplier + buff, or a rare flat STORAGE drive (+1 slot). Drop Luck raises chance & rarity. + 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 = { Common: {c:'#8a8a8a', g:'•'}, Uncommon: {c:'#4f9e3a', g:'◆'}, @@ -744,43 +751,72 @@ const RARITY = { 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:'🌱' }; +const STAT_LABEL = { down:'Download Speed', income:'Seed Income', seeds:'Connected 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){ 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; +function pickDropStat(){ + const w=[['income',26],['down',22],['seeds',20],['perpeer',12],['luck',10],['credits',10]]; + let tot=0; for(const x of w) tot+=x[1]; let r=Math.random()*tot; + for(const [s,wt] of w){ if(r1) r=Math.pow(r, 1/luckMult); + 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 }; + return { stat:pickDropStat(), 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'}, + {p:0.34, stat:'seeds', mult:2, dur:30, name:'Private Tracker Invite', rarity:'Uncommon'}, + {p:0.24, stat:'seeds', mult:3, dur:45, name:'Seedbox Trial Access', rarity:'Rare'}, + {p:0.18, stat:'perpeer',mult:3, dur:40, name:'Priority Routing', rarity:'Rare'}, + {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(r0.85){ const k=0.85/tot; sP*=k; tP*=k; pP*=k; } 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); + if(r40) 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){ @@ -876,6 +912,7 @@ function addTorrent(cid, silent, headstart){ peerSeed:Math.floor(rand(20,400)), dropLeechers:false, prioSeeds:false, }; + prerollLoot(t); // decide now what this download "contains" (shows ? while downloading) G.torrents.push(t); if(!silent) toast('','Torrent added', c.name.slice(0,42)); renderAll(); @@ -972,13 +1009,13 @@ function creditsFor(runUp){ return Math.floor(Math.pow(runUp/1e9, 0.42)); } 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; } G.credits+=gain; G.prestiges++; // 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.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; toast('prestige','🛰️ Migration Complete','Local hardware scrubbed. Redeployed onto an encrypted Virtual Seedbox Cluster.
Earned '+gain+' Crypto-Credits.'); 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; html+=` ${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();