/* MoonBite Badges — first-block celebration + earned-badge system. * * Self-contained: no libraries, no network calls. State lives in * localStorage under one key, scoped per mining address so a shared * machine can hold several miners' histories. * * Public API (all safe to call even if the DOM shelf is absent): * MoonBiteBadges.recordBlocks(sessionBlocks, chainHeight, address) * — call from the mining status poll; detects the first-ever block * (full-screen celebration) and awards later badges with a toast. * MoonBiteBadges.renderShelf(elId, address) — paint earned badges. */ (function () { 'use strict'; var STORE_KEY = 'mb_miner_profiles'; /* The block reward is consensus, not a constant to hardcode here: it halves on schedule, so the ceremony asks the server and degrades to a neutral label if the call fails. */ var REWARD_LABEL = 'MBITE'; try { fetch('/api/consensus').then(function (r) { return r.json(); }).then(function (c) { if (c && c.current_reward_coins) REWARD_LABEL = '+' + c.current_reward_coins + ' MBITE'; }).catch(function () {}); } catch (e) {} /* Badges are earned by verifiable local events only. Height tiers are * judged by the chain height when the miner's FIRST block landed — * being early is the one thing latecomers cannot copy. */ var BADGES = [ { id: 'first_bite', icon: 'I', name: 'First Bite', desc: 'Mined your first block', test: function (p) { return p.totalBlocks >= 1; } }, { id: 'genesis', icon: 'G', name: 'Genesis Miner', desc: 'First block before height 10,000', test: function (p) { return p.totalBlocks >= 1 && p.firstHeight < 10000; } }, { id: 'pioneer', icon: 'P', name: 'Pioneer', desc: 'First block before height 100,000', test: function (p) { return p.totalBlocks >= 1 && p.firstHeight < 100000; } }, { id: 'pre_halving', icon: 'H', name: 'Pre-Halving', desc: 'First block before the 1,000,000 halving', test: function (p) { return p.totalBlocks >= 1 && p.firstHeight < 1000000; } }, { id: 'ten', icon: '10', name: 'Ten Bites', desc: 'Mined 10 blocks', test: function (p) { return p.totalBlocks >= 10; } }, { id: 'hundred', icon: '100', name: 'Hundred Bites', desc: 'Mined 100 blocks', test: function (p) { return p.totalBlocks >= 100; } }, { id: 'week_streak', icon: '7', name: 'Seven Moons', desc: 'Mined on 7 different days', test: function (p) { return p.miningDays.length >= 7; } } ]; function loadAll() { try { return JSON.parse(localStorage.getItem(STORE_KEY)) || {}; } catch (e) { return {}; } } function saveAll(all) { localStorage.setItem(STORE_KEY, JSON.stringify(all)); } function profileFor(address) { var all = loadAll(); if (!all[address]) { all[address] = { totalBlocks: 0, sessionBase: 0, firstHeight: null, firstDate: null, miningDays: [], badges: [] }; saveAll(all); } return all[address]; } function saveProfile(address, p) { var all = loadAll(); all[address] = p; saveAll(all); } /* ---------------------------------------------------------------- record */ var lastSession = 0; function recordBlocks(sessionBlocks, chainHeight, address) { if (!address || typeof sessionBlocks !== 'number') return; var p = profileFor(address); // Session counter resets each run; only count increases. var delta = sessionBlocks - lastSession; if (sessionBlocks < lastSession) { delta = sessionBlocks; } // new session lastSession = sessionBlocks; if (delta <= 0) return; var hadNoBlocks = p.totalBlocks === 0; p.totalBlocks += delta; if (p.firstHeight === null) { p.firstHeight = Math.max(0, (chainHeight || 0) - delta + 1); p.firstDate = new Date().toISOString(); } var today = new Date().toISOString().slice(0, 10); if (p.miningDays.indexOf(today) === -1) p.miningDays.push(today); // Evaluate badges; collect the newly earned. var fresh = []; BADGES.forEach(function (b) { if (p.badges.indexOf(b.id) === -1 && b.test(p)) { p.badges.push(b.id); fresh.push(b); } }); saveProfile(address, p); if (hadNoBlocks) { /* Hand the block to /wall so it can offer a claim form there, rather than prompting for a handle mid-ceremony. */ try { localStorage.setItem('mb_last_block', JSON.stringify({ address: address, height: p.firstHeight, date: p.firstDate })); } catch (e) {} celebrateFirstBlock(p, address, fresh); } else { fresh.forEach(function (b, i) { setTimeout(function () { toast(b.icon + ' Badge earned: ' + b.name); }, i * 2200); }); } renderShelf('mbBadgeShelf', address); } /* ----------------------------------------------------------------- shelf */ function renderShelf(elId, address) { var el = document.getElementById(elId); if (!el) return; var p = address ? profileFor(address) : null; var earned = p ? p.badges : []; el.innerHTML = BADGES.map(function (b) { var got = earned.indexOf(b.id) !== -1; return '