diff --git a/CHANGELOG.md b/CHANGELOG.md index 15ad94e..23b0ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## Unreleased + +### English +- **Two new achievements:** 🌋 **Trillion hash** (a share with difficulty over 1 trillion) and the 🏆 **Golden prize: block found!**, unlocked when a share passes the network difficulty. The golden prize sits on its own full-width row, shows its trophy even while locked and lights up in gold when won. + +### Português +- **Duas conquistas novas:** 🌋 **Um trilhão** (um share com dificuldade acima de 1 trilhão) e o 🏆 **Prêmio de ouro: bloco achado!**, que desbloqueia quando um share passa da dificuldade da rede. O prêmio de ouro fica numa linha inteira só para ele, mostra o troféu mesmo trancado e acende em dourado quando sai. + ## v0.6.0 — 2026-11-12 ### English diff --git a/src/luck.js b/src/luck.js index 3bd9b96..a5b82e0 100644 --- a/src/luck.js +++ b/src/luck.js @@ -73,17 +73,21 @@ const ACHIEVEMENTS = [ { id: 'diff_1k', icon: '🎯', pt: ['Clube dos 1.000', 'Um share com dificuldade acima de 1.000'], en: ['1,000 club', 'A share with difficulty over 1,000'] }, { id: 'diff_1m', icon: '🚀', pt: ['Um em um milhão', 'Um share com dificuldade acima de 1 milhão'], en: ['One in a million', 'A share with difficulty over 1 million'] }, { id: 'diff_1b', icon: '☄️', pt: ['Quase bloco (1 bi)', 'Um share com dificuldade acima de 1 bilhão'], en: ['Billionaire hash', 'A share with difficulty over 1 billion'] }, + { id: 'diff_1t', icon: '🌋', pt: ['Um trilhão', 'Um share com dificuldade acima de 1 trilhão'], en: ['Trillion hash', 'A share with difficulty over 1 trillion'] }, { id: 'shares_1k', icon: '📦', pt: ['Mil tentativas', '1.000 shares aceitos na fazenda'], en: ['A thousand tries', '1,000 accepted shares on the farm'] }, { id: 'shares_100k', icon: '🏭', pt: ['100 mil tentativas', '100.000 shares aceitos na fazenda'], en: ['100,000 tries', '100,000 accepted shares on the farm'] }, { id: 'luck_2x', icon: '🍀', pt: ['Dobro da sorte', 'Worker com sorte de pelo menos 2× o esperado'], en: ['Double luck', 'A worker with at least 2× expected luck'] }, { id: 'luck_10x', icon: '⚡', pt: ['Raio na cabeça (10×)', 'Worker com sorte de pelo menos 10× o esperado'], en: ['Lightning strike (10×)', 'A worker with at least 10× expected luck'] }, { id: 'week_nonstop', icon: '🛡️', pt: ['Uma semana sem parar', '7 dias seguidos minerando sem cair'], en: ['One week nonstop', '7 days in a row mining without stopping'] }, { id: 'underdog', icon: '🐢', pt: ['Troféu vovô', 'Líder da semana com menos de 10% do maior hashrate'], en: ['Underdog trophy', 'Weekly leader with under 10% of the top hashrate'] }, + // o prêmio de ouro: um share acima da dificuldade da rede é um bloco + { id: 'block', icon: '🏆', golden: true, pt: ['Prêmio de ouro: bloco achado!', 'Um share acima da dificuldade da rede: a fazenda achou um bloco'], en: ['Golden prize: block found!', 'A share above the network difficulty: the farm found a block'] }, ]; // Avalia quais conquistas já foram atingidas e preserva as já desbloqueadas em `saved`. function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) { const best = Number(ctx.best) || 0; + const netDiff = Number(ctx.networkDifficulty) || 0; const bestWorker = ctx.bestWorker && ctx.bestWorker !== '—' ? ctx.bestWorker : null; const totalShares = Number(ctx.totalShares) || 0; const workers = Array.isArray(ctx.workers) ? ctx.workers : []; @@ -104,12 +108,14 @@ function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) { diff_1k: () => (best >= 1e3 ? { worker: bestWorker } : null), diff_1m: () => (best >= 1e6 ? { worker: bestWorker } : null), diff_1b: () => (best >= 1e9 ? { worker: bestWorker } : null), + diff_1t: () => (best >= 1e12 ? { worker: bestWorker } : null), shares_1k: () => (totalShares >= 1e3 ? { worker: null } : null), shares_100k: () => (totalShares >= 1e5 ? { worker: null } : null), luck_2x: () => (topLuck && topLuck.factor >= 2 ? { worker: topLuck.name } : null), luck_10x: () => (topLuck && topLuck.factor >= 10 ? { worker: topLuck.name } : null), week_nonstop: () => (nonstop ? { worker: nonstop.name } : null), underdog: () => (isUnderdog ? { worker: leaderName } : null), + block: () => (netDiff > 0 && best >= netDiff ? { worker: bestWorker } : null), }; const map = {}; @@ -130,6 +136,7 @@ function evaluateAchievements(ctx = {}, saved = {}, now = Date.now()) { const list = ACHIEVEMENTS.map((a) => ({ id: a.id, icon: a.icon, + golden: !!a.golden, pt: a.pt, en: a.en, unlocked: !!map[a.id], diff --git a/src/painel.js b/src/painel.js index 2d1428a..cec94a0 100644 --- a/src/painel.js +++ b/src/painel.js @@ -1014,6 +1014,7 @@ function snapshot(internal = false) { const topRec = [...records].sort((a, b) => b.value - a.value)[0]; const ach = evaluateAchievements({ best, + networkDifficulty: pool.difficulty, bestWorker: topRec ? topRec.worker : null, totalShares: Math.max(proxyTotalShares, poolTotalShares), workers: view, @@ -1244,6 +1245,9 @@ const PAGE = ` .ach-i { font-size:18px; line-height:1.2; } .ach-t { font-size:13px; font-weight:600; color:var(--txt); } .ach.on .ach-t { color:var(--gold); } + .ach.golden { grid-column:1 / -1; border-style:dashed; } + .ach.golden.on { border:2px solid #f2a900; background:linear-gradient(90deg, rgba(242,169,0,.18), rgba(242,169,0,.04)); box-shadow:0 0 18px rgba(242,169,0,.35); } + .ach.golden.on .ach-t { color:#f2a900; font-size:15px; } .ach-d { font-size:11px; color:var(--dim); line-height:1.3; margin-top:2px; } .hist-card .chart { height:240px; } body.kiosk .luck-duo { flex:1; min-height:0; } @@ -2131,11 +2135,11 @@ function renderGamification(j) { const isEn = LOC !== 'pt-BR'; for (const a of list) { const box = document.createElement('div'); - box.className = 'ach' + (a.unlocked ? ' on' : ''); + box.className = 'ach' + (a.unlocked ? ' on' : '') + (a.golden ? ' golden' : ''); const pair = (isEn ? a.en : a.pt) || a.pt || ['', '']; const ico = document.createElement('span'); ico.className = 'ach-i'; - ico.textContent = a.unlocked ? a.icon : '🔒'; + ico.textContent = a.unlocked || a.golden ? a.icon : '🔒'; // o troféu de ouro aparece mesmo trancado, para dar vontade const txt = document.createElement('div'); const t = document.createElement('div'); t.className = 'ach-t'; diff --git a/tests/luck.test.js b/tests/luck.test.js index 99e2915..8605b26 100644 --- a/tests/luck.test.js +++ b/tests/luck.test.js @@ -227,3 +227,19 @@ test('cobertura de todas as conquistas e edge cases de evaluateAchievements', () assert.ok(nonArrayRes.map.first_share); assert.strictEqual(nonArrayRes.map.first_share.worker, null); }); + +test('conquistas: 1 trilhão e o prêmio de ouro (bloco = share acima da dificuldade da rede)', () => { + const net = 148.4e12, now = Date.now(); + const quase = evaluateAchievements({ best: 2e12, bestWorker: 'orangeone', networkDifficulty: net }, {}, now); + const ids = quase.newlyUnlocked.map((a) => a.id); + assert.ok(ids.includes('diff_1t')); + assert.ok(!ids.includes('block')); + const semRede = evaluateAchievements({ best: 2e14, bestWorker: 'orangeone' }, {}, now); // sem dificuldade conhecida, não inventa bloco + assert.ok(!semRede.newlyUnlocked.some((a) => a.id === 'block')); + const bloco = evaluateAchievements({ best: 150e12, bestWorker: 'orangeone', networkDifficulty: net }, quase.map, now); + assert.deepStrictEqual(bloco.newlyUnlocked.map((a) => a.id), ['block']); + assert.strictEqual(bloco.map.block.worker, 'orangeone'); + const g = bloco.list.find((a) => a.id === 'block'); + assert.ok(g.golden && g.unlocked); + assert.strictEqual(ACHIEVEMENTS[ACHIEVEMENTS.length - 1].id, 'block', 'o prêmio de ouro fica por último'); +});