MediaWiki:Common.js: Difference between revisions

From Winds Of Valen Wiki
Jump to navigation Jump to search
Automated page publish
gave the mobile menu some actual links
 
(15 intermediate revisions by the same user not shown)
Line 4: Line 4:
     $('#ca-view a').text('Read');
     $('#ca-view a').text('Read');
});
});
// ==========================================================================
// Minerva (mobile skin) main menu only ships Home/Random by default; it does
// not pull in MediaWiki:Sidebar the way Vector's portlets do. This injects
// the same section links (kept in sync with MediaWiki:Sidebar by hand) into
// the native menu markup so mobile visitors actually get real navigation
// instead of just Home/Random/Settings/About/Disclaimers.
(function () {
  function initMinervaNav() {
    try {
      if (!document.body || !document.body.classList.contains('skin-minerva')) return;
      var nav = document.getElementById('p-navigation');
      if (!nav || document.getElementById('wov-minerva-extra-nav')) return;
      var sections = [
        { heading: 'Guides', items: [
          ['Getting started', '/w/Getting_Started'],
          ['Interactive Map (Fullscreen)', 'https://jxlife.github.io/WindsOfValenMap/'],
          ['All skills', '/w/Skills'],
          ['All quests', '/w/Quests'],
          ['Combat guide', '/w/Combat']
        ]},
        { heading: 'Database', items: [
          ['Item database', '/w/Items'],
          ['NPC database', '/w/NPCs'],
          ['Set bonuses', '/w/Set_bonuses']
        ]},
        { heading: 'Skills', items: [
          ['Skill Calculator', '/w/Calculators'],
          ['Mining', '/w/Mining'],
          ['Smithing', '/w/Smithing'],
          ['Fishing', '/w/Fishing'],
          ['Potion Making', '/w/Potion_Making']
        ]},
        { heading: 'Tools', items: [
          ['About this wiki', '/w/Project:About'],
          ['Random page', '/w/Special:Random'],
          ['New files', '/w/Special:NewFiles'],
          ['Wanted pages', '/w/Special:WantedPages'],
          ['Wiki statistics', '/w/Special:Statistics']
        ]},
        { heading: 'Updates', items: [
          ['Recent changes', '/w/Special:RecentChanges']
        ]}
      ];
      var container = document.createElement('div');
      container.id = 'wov-minerva-extra-nav';
      sections.forEach(function (section) {
        var heading = document.createElement('div');
        heading.className = 'wov-minerva-nav-heading';
        heading.textContent = section.heading;
        container.appendChild(heading);
        var ul = document.createElement('ul');
        ul.className = 'toggle-list__list';
        section.items.forEach(function (item) {
          var li = document.createElement('li');
          li.className = 'toggle-list-item';
          var a = document.createElement('a');
          a.className = 'toggle-list-item__anchor';
          a.href = item[1];
          var label = document.createElement('span');
          label.className = 'toggle-list-item__label';
          label.textContent = item[0];
          a.appendChild(label);
          li.appendChild(a);
          ul.appendChild(li);
        });
        container.appendChild(ul);
      });
      nav.parentNode.insertBefore(container, nav.nextSibling);
    } catch (e) {}
  }
  if (document.body) {
    initMinervaNav();
  } else {
    document.addEventListener('DOMContentLoaded', initMinervaNav);
  }
})();
// ==========================================================================
// ==========================================================================
// Valen Ledger — Gathering & Combat XP Calculator
// Valen Ledger — Gathering & Combat XP Calculator
Line 24: Line 106:
     fishing: [ {name:'Minnow',xp:8},{name:'Blue Gill',xp:75},{name:'Common Trout',xp:75},{name:'Bass',xp:200},{name:'Perch',xp:225},{name:'Elder Trout',xp:500},{name:'Carp',xp:1250} ],
     fishing: [ {name:'Minnow',xp:8},{name:'Blue Gill',xp:75},{name:'Common Trout',xp:75},{name:'Bass',xp:200},{name:'Perch',xp:225},{name:'Elder Trout',xp:500},{name:'Carp',xp:1250} ],
     mining: [ {name:'Copper',xp:15},{name:'Tin',xp:15},{name:'Iron',xp:30},{name:'Coal',xp:80},{name:'Mithril',xp:150},{name:'Silver',xp:300},{name:'Gold',xp:350},{name:'Essence',xp:550} ],
     mining: [ {name:'Copper',xp:15},{name:'Tin',xp:15},{name:'Iron',xp:30},{name:'Coal',xp:80},{name:'Mithril',xp:150},{name:'Silver',xp:300},{name:'Gold',xp:350},{name:'Essence',xp:550} ],
     smithing: [ {name:'Iron Rod',xp:10},{name:'Large Mithril Rod',xp:275},{name:'Mithril Rod',xp:250},{name:'Steel Rod',xp:30},{name:'Iron Plate',xp:30},{name:'Large Steel Rod',xp:40},{name:'Silver Plate',xp:400},{name:'Silver Foil',xp:400},{name:'Gold Plate',xp:400},{name:'Steel Plate',xp:60},{name:'Large Steel Plate',xp:70},{name:'Large Mithril Plate',xp:550},{name:'Mithril Plate',xp:500},{name:'Golden Shield Frame',xp:1000},{name:'Small Ebony Rod',xp:3000},{name:'Large Ebony Rod',xp:3500},{name:'Ebony Rod',xp:3250},{name:'Small Ebony Plate',xp:6000},{name:'Ebony Plate',xp:6500},{name:'Large Ebony Plate',xp:7000},{name:'Duskknight Boot Left Sabaton',xp:10000},{name:'Duskknight Boot Right Sabaton',xp:10000},{name:'Duskknight Leg Tuille',xp:10000},{name:'Duskknight Leg Poleyn',xp:10000},{name:'Duskknight Body Pauldron',xp:10000},{name:'Duskknight Body Couter',xp:10000},{name:'Duskknight Body Rerebrace',xp:12000},{name:'Duskknight Body Vambrace',xp:12000},{name:'Duskknight Leg Tasset',xp:14000},{name:'Duskknight Boot Greave',xp:15000},{name:'Duskknight Leg Cuisses',xp:15000},{name:'Duskknight Helmet Face Plate',xp:15000},{name:'Duskknight Helmet Head Plate',xp:15000},{name:'Duskknight Helmet Spike',xp:15000},{name:'Duskknight Body Breastplate',xp:18000} ],
     smithing: [
      // Bronze
      {name:'Bronze Bar',xp:15},{name:'Bronze Helmet',xp:60},{name:'Bronze Platebody',xp:60},{name:'Bronze Platelegs',xp:120},{name:'Bronze Sword',xp:90},
      // Iron
      {name:'Iron Bar',xp:30},{name:'Iron Rod',xp:10},{name:'Iron Plate',xp:30},{name:'Iron Helmet',xp:120},{name:'Iron Platebody',xp:300},{name:'Iron Platelegs',xp:240},{name:'Iron Sword',xp:180},
      // Steel
      {name:'Steel Bar',xp:55},{name:'Steel Rod',xp:30},{name:'Large Steel Rod',xp:40},{name:'Steel Plate',xp:60},{name:'Large Steel Plate',xp:70},{name:'Steel Helmet',xp:220},{name:'Steel Platebody',xp:550},{name:'Steel Platelegs',xp:440},{name:'Steel Sword',xp:330},
      // Mithril
      {name:'Mithril Bar',xp:155},{name:'Large Mithril Rod',xp:275},{name:'Mithril Rod',xp:250},{name:'Large Mithril Plate',xp:550},{name:'Mithril Plate',xp:500},{name:'Mithril Helmet',xp:620},{name:'Mithril Platebody',xp:1550},{name:'Mithril Platelegs',xp:1240},{name:'Mithril Sword',xp:930},
      // Silver
      {name:'Silver Bar',xp:675},{name:'Silver Plate',xp:400},{name:'Silver Foil',xp:400},
      // Gold
      {name:'Gold Bar',xp:800},{name:'Gold Plate',xp:400},{name:'Golden Shield Frame',xp:1000},
      // Ebony
      {name:'Ebony Bar',xp:1800},{name:'Small Ebony Rod',xp:3000},{name:'Large Ebony Rod',xp:3500},{name:'Ebony Rod',xp:3250},{name:'Small Ebony Plate',xp:6000},{name:'Ebony Plate',xp:6500},{name:'Large Ebony Plate',xp:7000},
      // Duskknight
      {name:'Duskknight Boot Left Sabaton',xp:10000},{name:'Duskknight Boot Right Sabaton',xp:10000},{name:'Duskknight Leg Tuille',xp:10000},{name:'Duskknight Leg Poleyn',xp:10000},{name:'Duskknight Body Pauldron',xp:10000},{name:'Duskknight Body Couter',xp:10000},{name:'Duskknight Body Rerebrace',xp:12000},{name:'Duskknight Body Vambrace',xp:12000},{name:'Duskknight Leg Tasset',xp:14000},{name:'Duskknight Boot Greave',xp:15000},{name:'Duskknight Leg Cuisses',xp:15000},{name:'Duskknight Helmet Face Plate',xp:15000},{name:'Duskknight Helmet Head Plate',xp:15000},{name:'Duskknight Helmet Spike',xp:15000},{name:'Duskknight Body Breastplate',xp:18000}
    ],
     potionmaking: [ {name:'Fishing Potion',xp:500},{name:'Weak Health Potion',xp:500},{name:'Mining Potion',xp:1000},{name:'Shields Potion',xp:1500},{name:'Normal Health Potion',xp:1500},{name:'Attack Potion',xp:1500},{name:'Archery Potion',xp:1750},{name:'Magic Potion',xp:2000},{name:'Strong Health Potion',xp:3250},{name:'Strong Shields Potion',xp:6000} ]
     potionmaking: [ {name:'Fishing Potion',xp:500},{name:'Weak Health Potion',xp:500},{name:'Mining Potion',xp:1000},{name:'Shields Potion',xp:1500},{name:'Normal Health Potion',xp:1500},{name:'Attack Potion',xp:1500},{name:'Archery Potion',xp:1750},{name:'Magic Potion',xp:2000},{name:'Strong Health Potion',xp:3250},{name:'Strong Shields Potion',xp:6000} ]
   };
   };
   Object.keys(GATHER).forEach(function(k){ GATHER[k].sort(function(a,b){ return a.xp-b.xp; }); });
   Object.keys(GATHER).forEach(function(k){ if (k === 'smithing') return; GATHER[k].sort(function(a,b){ return a.xp-b.xp; }); });


   // Smithing crafting tree, sourced from the game's own recipe data (not fan-site numbers).
   // Smithing crafting tree, sourced from the game's own recipe data (not fan-site numbers).
Line 68: Line 167:
     'Duskknight Helmet Spike': [{name:'Large Ebony Rod', count:1},{name:'Silver Foil', count:1}],
     'Duskknight Helmet Spike': [{name:'Large Ebony Rod', count:1},{name:'Silver Foil', count:1}],
     'Duskknight Body Breastplate': [{name:'Large Ebony Plate', count:4},{name:'Silver Foil', count:8}],
     'Duskknight Body Breastplate': [{name:'Large Ebony Plate', count:4},{name:'Silver Foil', count:8}],
    'Bronze Bar': [{name:'Copper Ore', count:1},{name:'Tin Ore', count:1}],
    'Bronze Helmet': [{name:'Bronze Bar', count:4}],
    'Bronze Platebody': [{name:'Bronze Bar', count:10}],
    'Bronze Platelegs': [{name:'Bronze Bar', count:8}],
    'Bronze Sword': [{name:'Bronze Bar', count:6}],
     'Iron Bar': [{name:'Iron Ore', count:2}],
     'Iron Bar': [{name:'Iron Ore', count:2}],
    'Iron Helmet': [{name:'Iron Bar', count:4}],
    'Iron Platebody': [{name:'Iron Bar', count:10}],
    'Iron Platelegs': [{name:'Iron Bar', count:8}],
    'Iron Sword': [{name:'Iron Bar', count:6}],
     'Steel Bar': [{name:'Iron Ore', count:1},{name:'Coal Ore', count:1}],
     'Steel Bar': [{name:'Iron Ore', count:1},{name:'Coal Ore', count:1}],
    'Steel Helmet': [{name:'Steel Bar', count:4}],
    'Steel Platebody': [{name:'Steel Bar', count:10}],
    'Steel Platelegs': [{name:'Steel Bar', count:8}],
    'Steel Sword': [{name:'Steel Bar', count:6}],
     'Mithril Bar': [{name:'Mithril Ore', count:1},{name:'Coal Ore', count:2}],
     'Mithril Bar': [{name:'Mithril Ore', count:1},{name:'Coal Ore', count:2}],
    'Mithril Helmet': [{name:'Mithril Bar', count:4}],
    'Mithril Platebody': [{name:'Mithril Bar', count:10}],
    'Mithril Platelegs': [{name:'Mithril Bar', count:8}],
    'Mithril Sword': [{name:'Mithril Bar', count:6}],
     'Gold Bar': [{name:'Gold Ore', count:8}],
     'Gold Bar': [{name:'Gold Ore', count:8}],
     'Silver Bar': [{name:'Silver Ore', count:7}],
     'Silver Bar': [{name:'Silver Ore', count:7}],
Line 78: Line 194:
   // Smithing XP per item, for bars that aren't in the GATHER.smithing dropdown (they're an
   // Smithing XP per item, for bars that aren't in the GATHER.smithing dropdown (they're an
   // intermediate step, not a selectable training target) but still have their own recipe XP.
   // intermediate step, not a selectable training target) but still have their own recipe XP.
   var BAR_SMITHING_XP = { 'Iron Bar':30, 'Steel Bar':55, 'Mithril Bar':155, 'Gold Bar':800, 'Silver Bar':675, 'Ebony Bar':1800 };
   var BAR_SMITHING_XP = { 'Bronze Bar':15, 'Iron Bar':30, 'Steel Bar':55, 'Mithril Bar':155, 'Gold Bar':800, 'Silver Bar':675, 'Ebony Bar':1800 };
   function getSmithingXp(name){
   function getSmithingXp(name){
     var found = GATHER.smithing.filter(function(e){ return e.name === name; })[0];
     var found = GATHER.smithing.filter(function(e){ return e.name === name; })[0];
Line 85: Line 201:
     return null;
     return null;
   }
   }
  // Seconds per craft at the Anvil/Furnace, one entry per RECIPES key. Bar/Helmet/Platebody/
  // Platelegs/Sword durations for Bronze-Mithril come straight off the Smithing Guide tables;
  // everything else comes from the game's own recipe data.
  var CRAFT_DURATION = {
    'Bronze Bar':3, 'Bronze Helmet':6, 'Bronze Platebody':18, 'Bronze Platelegs':16, 'Bronze Sword':8,
    'Iron Bar':4, 'Iron Rod':15, 'Iron Plate':4, 'Iron Helmet':10, 'Iron Platebody':28, 'Iron Platelegs':24, 'Iron Sword':12,
    'Steel Bar':5, 'Steel Rod':3, 'Large Steel Rod':3, 'Steel Plate':5, 'Large Steel Plate':5, 'Steel Helmet':14, 'Steel Platebody':38, 'Steel Platelegs':32, 'Steel Sword':16,
    'Mithril Bar':6, 'Large Mithril Rod':13, 'Mithril Rod':13, 'Large Mithril Plate':22, 'Mithril Plate':22, 'Mithril Helmet':18, 'Mithril Platebody':48, 'Mithril Platelegs':40, 'Mithril Sword':20,
    'Silver Bar':10, 'Silver Plate':15, 'Silver Foil':15,
    'Gold Bar':15, 'Gold Plate':15, 'Golden Shield Frame':60,
    'Small Ebony Rod':40, 'Large Ebony Rod':40, 'Ebony Rod':40, 'Small Ebony Plate':60, 'Ebony Plate':60, 'Large Ebony Plate':60, 'Ebony Bar':20,
    'Duskknight Boot Left Sabaton':120, 'Duskknight Boot Right Sabaton':120, 'Duskknight Leg Tuille':120, 'Duskknight Leg Poleyn':120, 'Duskknight Body Pauldron':120, 'Duskknight Body Couter':120, 'Duskknight Body Rerebrace':120, 'Duskknight Body Vambrace':120, 'Duskknight Leg Tasset':120, 'Duskknight Boot Greave':120, 'Duskknight Leg Cuisses':120, 'Duskknight Helmet Face Plate':120, 'Duskknight Helmet Head Plate':120, 'Duskknight Helmet Spike':120, 'Duskknight Body Breastplate':120
  };


   // Walks RECIPES tier-by-tier from `qty` of `name`, merging same-name items within a tier.
   // Walks RECIPES tier-by-tier from `qty` of `name`, merging same-name items within a tier.
Line 345: Line 475:
       if (rawNames.length){
       if (rawNames.length){
         text += '\n\nTotal raw materials: ' + rawNames.map(function(n){ return fmt(breakdown.totals[n]) + ' ' + n; }).join(', ');
         text += '\n\nTotal raw materials: ' + rawNames.map(function(n){ return fmt(breakdown.totals[n]) + ' ' + n; }).join(', ');
      }
      var totalSeconds = breakdown.entries.reduce(function(sum, e){
        var dur = CRAFT_DURATION[e.name];
        return dur ? sum + e.qty * dur : sum;
      }, 0);
      if (totalSeconds > 0){
        text += '\nTotal craft time (Anvil/Furnace only, not counting ore gathering): ' + formatTime(totalSeconds);
       }
       }
       breakdownText.textContent = text;
       breakdownText.textContent = text;
Line 761: Line 898:
   selectTab('fishing');
   selectTab('fishing');
});
});
// ==========================================================================
// Mobile nav toggle — the sidebar (#mw-panel) renders inline far down the
// page on narrow screens, so add a fixed hamburger button that slides it in
// as an overlay drawer instead. CSS in Common.css keeps this hidden/inert
// on desktop widths (>=1000px).
// ==========================================================================
(function () {
  function initMobileNav() {
    var panel = document.getElementById('mw-panel');
    if (!panel || document.getElementById('mobile-nav-toggle')) return;
    var toggle = document.createElement('button');
    toggle.id = 'mobile-nav-toggle';
    toggle.type = 'button';
    toggle.setAttribute('aria-label', 'Toggle navigation menu');
    toggle.textContent = '\u2630 Menu';
    var backdrop = document.createElement('div');
    backdrop.id = 'mobile-nav-backdrop';
    document.body.insertBefore(backdrop, document.body.firstChild);
    document.body.insertBefore(toggle, document.body.firstChild);
    function closeNav() {
      panel.classList.remove('mobile-nav-open');
      backdrop.classList.remove('mobile-nav-open');
    }
    function toggleNav() {
      panel.classList.toggle('mobile-nav-open');
      backdrop.classList.toggle('mobile-nav-open');
    }
    toggle.addEventListener('click', toggleNav);
    backdrop.addEventListener('click', closeNav);
    panel.querySelectorAll('a').forEach(function (a) {
      a.addEventListener('click', closeNav);
    });
  }
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initMobileNav);
  } else {
    initMobileNav();
  }
})();

Latest revision as of 05:53, 1 September 2026

$(document).ready(function() {
    $('#ca-nstab-main a').text('Article');
    $('#ca-history a').text('History');
    $('#ca-view a').text('Read');
});

// ==========================================================================
// Minerva (mobile skin) main menu only ships Home/Random by default; it does
// not pull in MediaWiki:Sidebar the way Vector's portlets do. This injects
// the same section links (kept in sync with MediaWiki:Sidebar by hand) into
// the native menu markup so mobile visitors actually get real navigation
// instead of just Home/Random/Settings/About/Disclaimers.
(function () {
  function initMinervaNav() {
    try {
      if (!document.body || !document.body.classList.contains('skin-minerva')) return;
      var nav = document.getElementById('p-navigation');
      if (!nav || document.getElementById('wov-minerva-extra-nav')) return;

      var sections = [
        { heading: 'Guides', items: [
          ['Getting started', '/w/Getting_Started'],
          ['Interactive Map (Fullscreen)', 'https://jxlife.github.io/WindsOfValenMap/'],
          ['All skills', '/w/Skills'],
          ['All quests', '/w/Quests'],
          ['Combat guide', '/w/Combat']
        ]},
        { heading: 'Database', items: [
          ['Item database', '/w/Items'],
          ['NPC database', '/w/NPCs'],
          ['Set bonuses', '/w/Set_bonuses']
        ]},
        { heading: 'Skills', items: [
          ['Skill Calculator', '/w/Calculators'],
          ['Mining', '/w/Mining'],
          ['Smithing', '/w/Smithing'],
          ['Fishing', '/w/Fishing'],
          ['Potion Making', '/w/Potion_Making']
        ]},
        { heading: 'Tools', items: [
          ['About this wiki', '/w/Project:About'],
          ['Random page', '/w/Special:Random'],
          ['New files', '/w/Special:NewFiles'],
          ['Wanted pages', '/w/Special:WantedPages'],
          ['Wiki statistics', '/w/Special:Statistics']
        ]},
        { heading: 'Updates', items: [
          ['Recent changes', '/w/Special:RecentChanges']
        ]}
      ];

      var container = document.createElement('div');
      container.id = 'wov-minerva-extra-nav';

      sections.forEach(function (section) {
        var heading = document.createElement('div');
        heading.className = 'wov-minerva-nav-heading';
        heading.textContent = section.heading;
        container.appendChild(heading);

        var ul = document.createElement('ul');
        ul.className = 'toggle-list__list';
        section.items.forEach(function (item) {
          var li = document.createElement('li');
          li.className = 'toggle-list-item';
          var a = document.createElement('a');
          a.className = 'toggle-list-item__anchor';
          a.href = item[1];
          var label = document.createElement('span');
          label.className = 'toggle-list-item__label';
          label.textContent = item[0];
          a.appendChild(label);
          li.appendChild(a);
          ul.appendChild(li);
        });
        container.appendChild(ul);
      });

      nav.parentNode.insertBefore(container, nav.nextSibling);
    } catch (e) {}
  }
  if (document.body) {
    initMinervaNav();
  } else {
    document.addEventListener('DOMContentLoaded', initMinervaNav);
  }
})();
// ==========================================================================
// Valen Ledger — Gathering & Combat XP Calculator
// Mount point: <div id="valen-ledger"></div> on any wiki page.
// Numbers sourced from the game's own balance data (xp_table.json,
// gather_xp_table.json, combat_data.json in the desktop calculator app),
// not the fan-site numbers.
// ==========================================================================
mw.hook('wikipage.content').add(function ($content) {
  var mount = $content.find('#valen-ledger')[0];
  if (!mount || mount.dataset.vlInit) return;
  mount.dataset.vlInit = '1';

  var MAX_LEVEL = 100;

  var XP_TABLE = {1:0,2:74,3:160,4:258,5:371,6:500,7:649,8:820,9:1016,10:1241,11:1500,12:1797,13:2139,14:2531,15:2982,16:3500,17:4095,18:4778,19:5563,20:6464,21:7500,22:8690,23:10056,24:11626,25:13429,26:15500,27:17879,28:20612,29:23751,30:27358,31:31500,32:36258,33:41724,34:48003,35:55215,36:63500,37:73017,38:83949,39:96506,40:110930,41:127500,42:146533,43:168397,44:193512,45:222361,46:255500,47:293567,48:337294,49:387523,50:445222,51:511500,52:587634,53:675088,54:775547,55:890944,56:1023500,57:1175767,58:1350676,59:1551594,60:1782388,61:2047500,62:2352034,63:2701852,64:3103688,65:3565275,66:4095500,67:4704568,68:5404204,69:6207875,70:7131050,71:8191500,72:9409637,73:10808909,74:12416250,75:14262600,76:16383500,77:18819774,78:21618318,79:24833000,80:28525701,81:32767500,82:37640048,83:43237135,84:49666500,85:57051902,86:65535500,87:75280595,88:86474770,89:99333501,90:114104303,91:131071500,92:150561691,93:172950041,94:198667502,95:228209107,96:262143500,97:301123882,98:345900582,99:397335504,100:456418714};
  function xpForLevel(l){ l = Math.max(1, Math.min(MAX_LEVEL, l)); return XP_TABLE[l]; }

  var GATHER = {
    fishing: [ {name:'Minnow',xp:8},{name:'Blue Gill',xp:75},{name:'Common Trout',xp:75},{name:'Bass',xp:200},{name:'Perch',xp:225},{name:'Elder Trout',xp:500},{name:'Carp',xp:1250} ],
    mining: [ {name:'Copper',xp:15},{name:'Tin',xp:15},{name:'Iron',xp:30},{name:'Coal',xp:80},{name:'Mithril',xp:150},{name:'Silver',xp:300},{name:'Gold',xp:350},{name:'Essence',xp:550} ],
    smithing: [
      // Bronze
      {name:'Bronze Bar',xp:15},{name:'Bronze Helmet',xp:60},{name:'Bronze Platebody',xp:60},{name:'Bronze Platelegs',xp:120},{name:'Bronze Sword',xp:90},
      // Iron
      {name:'Iron Bar',xp:30},{name:'Iron Rod',xp:10},{name:'Iron Plate',xp:30},{name:'Iron Helmet',xp:120},{name:'Iron Platebody',xp:300},{name:'Iron Platelegs',xp:240},{name:'Iron Sword',xp:180},
      // Steel
      {name:'Steel Bar',xp:55},{name:'Steel Rod',xp:30},{name:'Large Steel Rod',xp:40},{name:'Steel Plate',xp:60},{name:'Large Steel Plate',xp:70},{name:'Steel Helmet',xp:220},{name:'Steel Platebody',xp:550},{name:'Steel Platelegs',xp:440},{name:'Steel Sword',xp:330},
      // Mithril
      {name:'Mithril Bar',xp:155},{name:'Large Mithril Rod',xp:275},{name:'Mithril Rod',xp:250},{name:'Large Mithril Plate',xp:550},{name:'Mithril Plate',xp:500},{name:'Mithril Helmet',xp:620},{name:'Mithril Platebody',xp:1550},{name:'Mithril Platelegs',xp:1240},{name:'Mithril Sword',xp:930},
      // Silver
      {name:'Silver Bar',xp:675},{name:'Silver Plate',xp:400},{name:'Silver Foil',xp:400},
      // Gold
      {name:'Gold Bar',xp:800},{name:'Gold Plate',xp:400},{name:'Golden Shield Frame',xp:1000},
      // Ebony
      {name:'Ebony Bar',xp:1800},{name:'Small Ebony Rod',xp:3000},{name:'Large Ebony Rod',xp:3500},{name:'Ebony Rod',xp:3250},{name:'Small Ebony Plate',xp:6000},{name:'Ebony Plate',xp:6500},{name:'Large Ebony Plate',xp:7000},
      // Duskknight
      {name:'Duskknight Boot Left Sabaton',xp:10000},{name:'Duskknight Boot Right Sabaton',xp:10000},{name:'Duskknight Leg Tuille',xp:10000},{name:'Duskknight Leg Poleyn',xp:10000},{name:'Duskknight Body Pauldron',xp:10000},{name:'Duskknight Body Couter',xp:10000},{name:'Duskknight Body Rerebrace',xp:12000},{name:'Duskknight Body Vambrace',xp:12000},{name:'Duskknight Leg Tasset',xp:14000},{name:'Duskknight Boot Greave',xp:15000},{name:'Duskknight Leg Cuisses',xp:15000},{name:'Duskknight Helmet Face Plate',xp:15000},{name:'Duskknight Helmet Head Plate',xp:15000},{name:'Duskknight Helmet Spike',xp:15000},{name:'Duskknight Body Breastplate',xp:18000}
    ],
    potionmaking: [ {name:'Fishing Potion',xp:500},{name:'Weak Health Potion',xp:500},{name:'Mining Potion',xp:1000},{name:'Shields Potion',xp:1500},{name:'Normal Health Potion',xp:1500},{name:'Attack Potion',xp:1500},{name:'Archery Potion',xp:1750},{name:'Magic Potion',xp:2000},{name:'Strong Health Potion',xp:3250},{name:'Strong Shields Potion',xp:6000} ]
  };
  Object.keys(GATHER).forEach(function(k){ if (k === 'smithing') return; GATHER[k].sort(function(a,b){ return a.xp-b.xp; }); });

  // Smithing crafting tree, sourced from the game's own recipe data (not fan-site numbers).
  // Each entry: item name -> array of {name, count} ingredients needed to craft ONE of that item.
  // Items with no entry here are raw/gathered materials (chain terminates).
  var RECIPES = {
    'Iron Rod': [{name:'Iron Bar', count:1}],
    'Large Mithril Rod': [{name:'Mithril Rod', count:2}],
    'Mithril Rod': [{name:'Mithril Bar', count:2}],
    'Steel Rod': [{name:'Steel Bar', count:1}],
    'Iron Plate': [{name:'Iron Bar', count:2}],
    'Large Steel Rod': [{name:'Steel Rod', count:2}],
    'Silver Plate': [{name:'Silver Bar', count:7}],
    'Silver Foil': [{name:'Silver Plate', count:1}],
    'Gold Plate': [{name:'Gold Bar', count:8}],
    'Steel Plate': [{name:'Steel Bar', count:2}],
    'Large Steel Plate': [{name:'Steel Plate', count:2}],
    'Large Mithril Plate': [{name:'Mithril Plate', count:4}],
    'Mithril Plate': [{name:'Mithril Bar', count:4}],
    'Golden Shield Frame': [{name:'Gold Plate', count:5},{name:'Gold Bar', count:3},{name:'Steel Rod', count:2},{name:'Steel Bar', count:2}],
    'Small Ebony Rod': [{name:'Ebony Bar', count:2}],
    'Large Ebony Rod': [{name:'Ebony Rod', count:4},{name:'Ebony Bar', count:3}],
    'Ebony Rod': [{name:'Small Ebony Rod', count:4},{name:'Ebony Bar', count:3}],
    'Small Ebony Plate': [{name:'Ebony Bar', count:4}],
    'Ebony Plate': [{name:'Small Ebony Plate', count:4},{name:'Ebony Bar', count:3}],
    'Large Ebony Plate': [{name:'Ebony Plate', count:4},{name:'Ebony Bar', count:3}],
    'Duskknight Boot Left Sabaton': [{name:'Ebony Plate', count:1},{name:'Silver Foil', count:2}],
    'Duskknight Boot Right Sabaton': [{name:'Ebony Plate', count:1},{name:'Silver Foil', count:2}],
    'Duskknight Leg Tuille': [{name:'Ebony Plate', count:1},{name:'Silver Foil', count:1}],
    'Duskknight Leg Poleyn': [{name:'Ebony Plate', count:1},{name:'Silver Foil', count:1}],
    'Duskknight Body Pauldron': [{name:'Ebony Plate', count:1},{name:'Silver Foil', count:1}],
    'Duskknight Body Couter': [{name:'Ebony Plate', count:1},{name:'Silver Foil', count:1}],
    'Duskknight Body Rerebrace': [{name:'Ebony Plate', count:2},{name:'Silver Foil', count:2}],
    'Duskknight Body Vambrace': [{name:'Ebony Plate', count:2},{name:'Silver Foil', count:2}],
    'Duskknight Leg Tasset': [{name:'Ebony Plate', count:3},{name:'Silver Foil', count:3}],
    'Duskknight Boot Greave': [{name:'Large Ebony Plate', count:1},{name:'Silver Foil', count:2}],
    'Duskknight Leg Cuisses': [{name:'Large Ebony Plate', count:2},{name:'Silver Foil', count:2}],
    'Duskknight Helmet Face Plate': [{name:'Large Ebony Plate', count:1},{name:'Silver Foil', count:2}],
    'Duskknight Helmet Head Plate': [{name:'Large Ebony Plate', count:1},{name:'Silver Foil', count:2}],
    'Duskknight Helmet Spike': [{name:'Large Ebony Rod', count:1},{name:'Silver Foil', count:1}],
    'Duskknight Body Breastplate': [{name:'Large Ebony Plate', count:4},{name:'Silver Foil', count:8}],
    'Bronze Bar': [{name:'Copper Ore', count:1},{name:'Tin Ore', count:1}],
    'Bronze Helmet': [{name:'Bronze Bar', count:4}],
    'Bronze Platebody': [{name:'Bronze Bar', count:10}],
    'Bronze Platelegs': [{name:'Bronze Bar', count:8}],
    'Bronze Sword': [{name:'Bronze Bar', count:6}],
    'Iron Bar': [{name:'Iron Ore', count:2}],
    'Iron Helmet': [{name:'Iron Bar', count:4}],
    'Iron Platebody': [{name:'Iron Bar', count:10}],
    'Iron Platelegs': [{name:'Iron Bar', count:8}],
    'Iron Sword': [{name:'Iron Bar', count:6}],
    'Steel Bar': [{name:'Iron Ore', count:1},{name:'Coal Ore', count:1}],
    'Steel Helmet': [{name:'Steel Bar', count:4}],
    'Steel Platebody': [{name:'Steel Bar', count:10}],
    'Steel Platelegs': [{name:'Steel Bar', count:8}],
    'Steel Sword': [{name:'Steel Bar', count:6}],
    'Mithril Bar': [{name:'Mithril Ore', count:1},{name:'Coal Ore', count:2}],
    'Mithril Helmet': [{name:'Mithril Bar', count:4}],
    'Mithril Platebody': [{name:'Mithril Bar', count:10}],
    'Mithril Platelegs': [{name:'Mithril Bar', count:8}],
    'Mithril Sword': [{name:'Mithril Bar', count:6}],
    'Gold Bar': [{name:'Gold Ore', count:8}],
    'Silver Bar': [{name:'Silver Ore', count:7}],
    'Ebony Bar': [{name:'Ebony Ore', count:7}]
  };

  // Smithing XP per item, for bars that aren't in the GATHER.smithing dropdown (they're an
  // intermediate step, not a selectable training target) but still have their own recipe XP.
  var BAR_SMITHING_XP = { 'Bronze Bar':15, 'Iron Bar':30, 'Steel Bar':55, 'Mithril Bar':155, 'Gold Bar':800, 'Silver Bar':675, 'Ebony Bar':1800 };
  function getSmithingXp(name){
    var found = GATHER.smithing.filter(function(e){ return e.name === name; })[0];
    if (found) return found.xp;
    if (BAR_SMITHING_XP.hasOwnProperty(name)) return BAR_SMITHING_XP[name];
    return null;
  }

  // Seconds per craft at the Anvil/Furnace, one entry per RECIPES key. Bar/Helmet/Platebody/
  // Platelegs/Sword durations for Bronze-Mithril come straight off the Smithing Guide tables;
  // everything else comes from the game's own recipe data.
  var CRAFT_DURATION = {
    'Bronze Bar':3, 'Bronze Helmet':6, 'Bronze Platebody':18, 'Bronze Platelegs':16, 'Bronze Sword':8,
    'Iron Bar':4, 'Iron Rod':15, 'Iron Plate':4, 'Iron Helmet':10, 'Iron Platebody':28, 'Iron Platelegs':24, 'Iron Sword':12,
    'Steel Bar':5, 'Steel Rod':3, 'Large Steel Rod':3, 'Steel Plate':5, 'Large Steel Plate':5, 'Steel Helmet':14, 'Steel Platebody':38, 'Steel Platelegs':32, 'Steel Sword':16,
    'Mithril Bar':6, 'Large Mithril Rod':13, 'Mithril Rod':13, 'Large Mithril Plate':22, 'Mithril Plate':22, 'Mithril Helmet':18, 'Mithril Platebody':48, 'Mithril Platelegs':40, 'Mithril Sword':20,
    'Silver Bar':10, 'Silver Plate':15, 'Silver Foil':15,
    'Gold Bar':15, 'Gold Plate':15, 'Golden Shield Frame':60,
    'Small Ebony Rod':40, 'Large Ebony Rod':40, 'Ebony Rod':40, 'Small Ebony Plate':60, 'Ebony Plate':60, 'Large Ebony Plate':60, 'Ebony Bar':20,
    'Duskknight Boot Left Sabaton':120, 'Duskknight Boot Right Sabaton':120, 'Duskknight Leg Tuille':120, 'Duskknight Leg Poleyn':120, 'Duskknight Body Pauldron':120, 'Duskknight Body Couter':120, 'Duskknight Body Rerebrace':120, 'Duskknight Body Vambrace':120, 'Duskknight Leg Tasset':120, 'Duskknight Boot Greave':120, 'Duskknight Leg Cuisses':120, 'Duskknight Helmet Face Plate':120, 'Duskknight Helmet Head Plate':120, 'Duskknight Helmet Spike':120, 'Duskknight Body Breastplate':120
  };

  // Walks RECIPES tier-by-tier from `qty` of `name`, merging same-name items within a tier.
  // Returns {entries: [{name,qty},...], totals: {rawItemName: qty,...}} or null if `name` has no recipe.
  // `entries` is a flat list in traversal order, one per item per tier (never combined onto one line).
  function buildMaterialBreakdown(name, qty){
    if (!RECIPES[name]) return null;
    var entries = [{name:name, qty:qty}];
    var totals = {};
    var currentTier = [{name:name, qty:qty}];
    while (true){
      var nextMap = {};
      var expandedAny = false;
      currentTier.forEach(function(e){
        var recipe = RECIPES[e.name];
        if (!recipe){ totals[e.name] = (totals[e.name] || 0) + e.qty; return; }
        expandedAny = true;
        recipe.forEach(function(ing){ nextMap[ing.name] = (nextMap[ing.name] || 0) + e.qty * ing.count; });
      });
      if (!expandedAny) break;
      var nextTier = Object.keys(nextMap).map(function(n){ return {name:n, qty:nextMap[n]}; });
      nextTier.forEach(function(e){ entries.push(e); });
      currentTier = nextTier;
    }
    return { entries: entries, totals: totals };
  }

  var MONSTERS = [
    {name:'Goblin',level:1,xp:50,area:'Broken Village',health:50,defence:1},{name:'Hen',level:1,xp:40,area:'Farmlands',health:40,defence:1},{name:'Rooster',level:1,xp:40,area:'Farmlands',health:40,defence:1},{name:'Cow',level:5,xp:105,area:'Farmlands',health:100,defence:5},{name:'Skeleton',level:6,xp:80,area:'Wheat Fields',health:75,defence:6},{name:'Bandit',level:10,xp:132,area:'Forest',health:120,defence:10},{name:'Skeleton Miner',level:13,xp:141,area:'Valen Gate / Grave Town',health:125,defence:13},{name:'Pirate Corsair',level:10,xp:132,area:'Valen Port',health:120,defence:5},{name:'Pirate Rigger',level:10,xp:132,area:'Valen Port',health:120,defence:5},{name:'Pirate Deckhand',level:10,xp:132,area:'Valen Port',health:120,defence:5},{name:'Goblin Villager',level:15,xp:229,area:'Goblin Village',health:200,defence:13},{name:'Skeleton Knight',level:26,xp:442,area:'West Mine',health:350,defence:20},{name:'Highwayman',level:28,xp:384,area:'West Mine',health:300,defence:23},{name:'Bandit Mercenary',level:30,xp:389,area:'Mercenary Camp',health:300,defence:25},{name:'Slum Bandit',level:30,xp:390,area:'Valen City',health:300,defence:20},{name:'Elf',level:37,xp:548,area:'Elven Haven',health:400,defence:30},{name:'Elf Scholar',level:37,xp:548,area:'Elven Haven',health:400,defence:20},{name:'Noble Skeleton',level:37,xp:548,area:'Valen City',health:400,defence:20},{name:'Cavern Goblin',level:37,xp:548,area:'West Mine Deep Cavern',health:400,defence:20},{name:'Skeleton Knight (Darklands)',level:67,xp:1670,area:'Darklands',health:1000,defence:40},{name:'Ashen Warrior',level:95,xp:2925,area:'West Mine Ashen Cavern',health:1500,defence:60},{name:'Ashen Archer',level:95,xp:2925,area:'West Mine Ashen Cavern',health:1500,defence:50},{name:'Ashen Mage',level:95,xp:2925,area:'West Mine Ashen Cavern',health:1500,defence:50},{name:'Goblin Berserker (Boss)',level:9,xp:273,area:'Broken Village',health:250,defence:6},{name:'Goblin Watcher (Boss)',level:9,xp:273,area:'Broken Village',health:250,defence:3},{name:'Pirate Captain (Boss)',level:25,xp:625,area:'Valen Port',health:500,defence:10},{name:'Bandit Leader (Boss)',level:25,xp:626,area:'Forest Alcove',health:500,defence:15},{name:'Goblin Chieftain (Boss)',level:40,xp:1400,area:'Goblin Village',health:1000,defence:18},{name:'Goblin General (Boss)',level:55,xp:1900,area:'General Cave',health:1500,defence:20},{name:'Bandit Mercenary Boss',level:74,xp:3480,area:'Mercenary Camp',health:2000,defence:27},{name:'Skeleton Pioneer (Boss)',level:90,xp:4750,area:'West Mine',health:2500,defence:30},{name:'Cavern Goblin Hunter (Boss)',level:107,xp:6105,area:'West Mine Deep Cavern',health:3000,defence:35},{name:'Fire Lich (Boss)',level:120,xp:7350,area:'Grave Town',health:3500,defence:35},{name:'Elf Warden (Boss)',level:120,xp:7350,area:'Elven Haven',health:3500,defence:35},{name:'The Burning King (Boss)',level:307,xp:30260,area:'West Mine Ashen Cavern',health:10000,defence:60}
  ];

  var COMBAT_EXP = {1:{total:0},2:{total:74},3:{total:160},4:{total:258},5:{total:371},6:{total:500},7:{total:649},8:{total:820},9:{total:1016},10:{total:1241},11:{total:1500},12:{total:1797},13:{total:2139},14:{total:2531},15:{total:2982},16:{total:3500},17:{total:4095},18:{total:4778},19:{total:5563},20:{total:6464},21:{total:7500},22:{total:8690},23:{total:10056},24:{total:11626},25:{total:13429},26:{total:15500},27:{total:17879},28:{total:20612},29:{total:23751},30:{total:27363},31:{total:31500},32:{total:36259},33:{total:41720},34:{total:47981},35:{total:55151},36:{total:63354},37:{total:72736},38:{total:83461},39:{total:95721},40:{total:109735},41:{total:125750},42:{total:144039},43:{total:164906},44:{total:188691},45:{total:215772},46:{total:246571},47:{total:281556},48:{total:321250},49:{total:366236},50:{total:417159},51:{total:474747},52:{total:539822},53:{total:613313},54:{total:696249},55:{total:789780},56:{total:895184},57:{total:1013881},58:{total:1147444},59:{total:1297613},60:{total:1466305},61:{total:1655628},62:{total:1867892},63:{total:2105628},64:{total:2371611},65:{total:2668884},66:{total:4095500},67:{total:4704568},68:{total:5404204},69:{total:6207875},70:{total:7131050},71:{total:8191500},72:{total:9409637},73:{total:10808909},74:{total:12416250},75:{total:14262600},76:{total:16383500},77:{total:18819774},78:{total:21618318},79:{total:24833000},80:{total:28525701},81:{total:32767500},82:{total:37640048},83:{total:43237135},84:{total:49666500},85:{total:57051902},86:{total:65535500},87:{total:75280595},88:{total:86474770},89:{total:99333501},90:{total:114104303},91:{total:131071500},92:{total:150561691},93:{total:172950041},94:{total:198667502},95:{total:228209107},96:{total:262143500},97:{total:301123982},98:{total:345900582},99:{total:397335504},100:{total:456418846}};

  // Max Hit formula (melee/archery/magic), confirmed against real game tooltips.
  var MAXHIT_CONST = {
    base_damage: 5.0,
    base_effective_level: 8,
    effective_level_damage_multiplier: 1.1,
    base_equipment_damage: 30.0,
    equipment_power_strength: 0.333333,
    damage_divider: 50.0
  };
  var MAXHIT_STYLES = {
    Attack:  { label:'Melee',   powerLabel:'Melee Power' },
    Archery: { label:'Archery', powerLabel:'Archery Power' },
    Magic:   { label:'Magic',   powerLabel:'Magic Power' }
  };
  function computeMaxHit(damageSkill, weaponDamage, attackSpeed, power){
    var c = MAXHIT_CONST;
    var effectiveAttackLevel = damageSkill + c.base_effective_level;
    var weaponDamageBonus = weaponDamage + c.base_equipment_damage;
    var flatDamageBonus = (power * c.equipment_power_strength) * attackSpeed;
    var maxHit = c.base_damage + (effectiveAttackLevel * c.effective_level_damage_multiplier) * (weaponDamageBonus + flatDamageBonus) / c.damage_divider;
    return { maxHit: maxHit, effectiveAttackLevel: effectiveAttackLevel, weaponDamageBonus: weaponDamageBonus, flatDamageBonus: flatDamageBonus };
  }

  function fmt(n){ return Math.round(n).toLocaleString('en-US'); }
  function fmt1(n){ return (Math.round(n*10)/10).toLocaleString('en-US'); }
  function clamp(v,lo,hi){ return Math.max(lo, Math.min(hi, v)); }

  if (!document.getElementById('valen-ledger-style')) {
    var style = document.createElement('style');
    style.id = 'valen-ledger-style';
    style.textContent =
      '#valen-ledger{font-family:"PT Serif",Georgia,serif;color:#3d2b1f;max-width:720px;margin:20px 0;}' +
      '#valen-ledger .vl-tabs{display:flex;gap:4px;flex-wrap:wrap;}' +
      '#valen-ledger .vl-tab{font-family:"Kingthings Petrock",serif;font-size:14px;letter-spacing:.03em;color:#5a4b38;background:#d8c39a;border:1px solid #5a4b38;border-bottom:none;padding:8px 14px 9px;border-radius:6px 6px 0 0;cursor:pointer;}' +
      '#valen-ledger .vl-tab.active{color:#f1e7d3;background:#3d2b1f;}' +
      '#valen-ledger .vl-card{background:#e2d1b4;border:1px solid #5a4b38;border-radius:0 6px 6px 6px;padding:20px;}' +
      '#valen-ledger .vl-field{display:flex;flex-direction:column;gap:5px;margin-bottom:14px;}' +
      '#valen-ledger .vl-row{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px;}' +
      '#valen-ledger label{font-size:11px;letter-spacing:.05em;text-transform:uppercase;color:#5a4b38;font-weight:bold;}' +
      '#valen-ledger select,#valen-ledger input[type=number]{font-family:"PT Serif",Georgia,serif;font-size:15px;color:#3d2b1f;background:#f1e7d3;border:1px solid #5a4b38;border-radius:4px;padding:7px 9px;width:100%;box-sizing:border-box;}' +
      '#valen-ledger input[type=number]{font-family:"Courier New",monospace;}' +
      '#valen-ledger .vl-hint{font-size:12.5px;color:#5a4b38;line-height:1.5;margin:2px 0 0;}' +
      '#valen-ledger .vl-xp-pair{display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-bottom:14px;}' +
      '#valen-ledger .vl-xp-box{background:#f1e7d3;border:1px solid #5a4b38;border-radius:4px;padding:8px 10px;}' +
      '#valen-ledger .vl-xp-box .vl-num{font-family:"Courier New",monospace;font-size:17px;}' +
      '#valen-ledger .vl-result{margin-top:6px;padding-top:16px;border-top:2px double #5a4b38;}' +
      '#valen-ledger .vl-result-head{font-family:"Kingthings Petrock",serif;font-size:13px;letter-spacing:.06em;text-transform:uppercase;color:#8f6420;margin:0 0 8px;}' +
      '#valen-ledger .vl-result-text{font-size:15px;line-height:1.7;white-space:pre-line;margin:0 0 12px;}' +
      '#valen-ledger .vl-result-text.done{color:#8f6420;font-weight:bold;}' +
      '#valen-ledger .vl-bar-track{height:9px;border-radius:5px;background:#c9b284;overflow:hidden;border:1px solid #5a4b38;}' +
      '#valen-ledger .vl-bar-fill{height:100%;background:#8f6420;width:0%;}' +
      '#valen-ledger .vl-stat-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-bottom:6px;}' +
      '#valen-ledger .vl-stat{background:#f1e7d3;border:1px solid #5a4b38;border-radius:4px;padding:10px 12px;}' +
      '#valen-ledger .vl-stat-label{font-size:10px;letter-spacing:.04em;text-transform:uppercase;color:#5a4b38;display:block;margin-bottom:5px;}' +
      '#valen-ledger .vl-stat-value{font-family:"Courier New",monospace;font-size:17px;}' +
      '#valen-ledger .vl-skill-grid{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;}' +
      '#valen-ledger .vl-skill-btn{font-family:"Kingthings Petrock",serif;font-size:12px;padding:6px 12px;border-radius:16px;border:1px solid #5a4b38;background:transparent;color:#5a4b38;cursor:pointer;}' +
      '#valen-ledger .vl-skill-btn.active{background:#8f6420;border-color:#8f6420;color:#f1e7d3;}' +
      '#valen-ledger .vl-skill-btn.health.active{background:#7a352c;border-color:#7a352c;}' +
      '#valen-ledger .vl-warn{color:#7a352c;font-size:14px;padding:12px 14px;border:1px solid #7a352c;border-radius:4px;background:rgba(122,53,44,.08);}' +
      '#valen-ledger .vl-side{margin-top:14px;}' +
      '@media(max-width:520px){#valen-ledger .vl-row,#valen-ledger .vl-xp-pair{grid-template-columns:1fr;}#valen-ledger .vl-stat-grid{grid-template-columns:1fr;}}';
    document.head.appendChild(style);
  }

  mount.innerHTML =
    '<nav class="vl-tabs" id="vl-tabs"></nav>' +
    '<div class="vl-card">' +
      '<div id="vl-gather-view">' +
        '<div class="vl-field"><label>Action / Item</label><select id="vl-action"></select></div>' +
        '<div class="vl-row">' +
          '<div class="vl-field"><label>Current Level</label><input id="vl-cur-lvl" type="number" min="1" max="100" value="15"></div>' +
          '<div class="vl-field"><label>Target Level</label><input id="vl-tgt-lvl" type="number" min="1" max="100" value="20"></div>' +
        '</div>' +
        '<div class="vl-xp-pair"><div class="vl-xp-box"><label>Current XP</label><div class="vl-num" id="vl-cur-xp">0</div></div><div class="vl-xp-box"><label>Target XP</label><div class="vl-num" id="vl-tgt-xp">0</div></div></div>' +
        '<div class="vl-result"><p class="vl-result-head">Reckoning</p><p class="vl-result-text" id="vl-gather-result">Pick a skill and an action to begin.</p><div class="vl-bar-track"><div class="vl-bar-fill" id="vl-gather-fill"></div></div></div>' +
        '<div class="vl-result" id="vl-breakdown-section" style="display:none;"><p class="vl-result-head">Material Breakdown</p><p class="vl-result-text" id="vl-breakdown-text"></p></div>' +
      '</div>' +
      '<div id="vl-combat-view" style="display:none;">' +
        '<div class="vl-field"><label>Skill Being Trained</label><div class="vl-skill-grid" id="vl-skill-grid"></div><p class="vl-hint" id="vl-combat-hint"></p></div>' +
        '<div class="vl-row"><div class="vl-field"><label id="vl-cur-label">Current Level</label><input id="vl-c-cur" type="number" min="1" max="100" value="1"></div><div class="vl-field"><label id="vl-req-label">Required Level</label><input id="vl-c-req" type="number" min="2" max="100" value="10"></div></div>' +
        '<div class="vl-field"><label id="vl-next-label">EXP Left For Next Level</label><input id="vl-c-next" type="number" min="0" value="74"></div>' +
        '<div class="vl-field" id="vl-secondary-field" style="display:none;"><label id="vl-secondary-lvl-label">Secondary Skill - Current Level (optional)</label><input id="vl-sec-lvl" type="number" min="1" max="100" placeholder="Leave blank to skip"><label id="vl-secondary-exp-label" style="margin-top:8px;">Secondary Skill - EXP Left (optional)</label><input id="vl-sec-exp" type="number" min="0" placeholder="e.g. 1234"></div>' +
        '<div class="vl-field"><label>Monster</label><select id="vl-monster"></select></div>' +
        '<div class="vl-field" id="vl-custom-field" style="display:none;"><label>Custom Monster Total XP</label><input id="vl-custom-xp" type="number" min="1" placeholder="e.g. 250"></div>' +
        '<div class="vl-row"><div class="vl-field"><label>Time Per Kill (min)</label><input id="vl-kill-min" type="number" min="0" value="0"></div><div class="vl-field"><label>Time Per Kill (sec)</label><input id="vl-kill-sec" type="number" min="0" max="59" value="15"></div></div>' +
        '<div class="vl-field" id="vl-health-field"><label>Health - Current Level (optional)</label><input id="vl-h-lvl" type="number" min="1" max="100" placeholder="Leave blank to skip"><label style="margin-top:8px;">Health - EXP Left (optional)</label><input id="vl-h-exp" type="number" min="0" placeholder="e.g. 1234"></div>' +
        '<div class="vl-result"><p class="vl-result-head">Reckoning</p><p class="vl-warn" id="vl-warn" style="display:none;"></p>' +
          '<div id="vl-results"><div class="vl-stat-grid"><div class="vl-stat"><span class="vl-stat-label">EXP Needed</span><span class="vl-stat-value" id="vl-r-exp">-</span></div><div class="vl-stat"><span class="vl-stat-label">Kills Required</span><span class="vl-stat-value" id="vl-r-kills">-</span></div><div class="vl-stat"><span class="vl-stat-label">Time Required</span><span class="vl-stat-value" id="vl-r-time">-</span></div></div><p class="vl-hint" id="vl-r-sub"></p></div>' +
          '<div class="vl-side" id="vl-sec-progress" style="display:none;"><p class="vl-hint" id="vl-sec-detail"></p><div class="vl-bar-track"><div class="vl-bar-fill" id="vl-sec-bar"></div></div></div>' +
          '<div class="vl-side" id="vl-health-progress" style="display:none;"><p class="vl-hint" id="vl-health-detail"></p><div class="vl-bar-track"><div class="vl-bar-fill" id="vl-health-bar"></div></div></div>' +
        '</div>' +
      '</div>' +
      '<div id="vl-maxhit-view" style="display:none;">' +
        '<div class="vl-field"><label>Combat Style</label><div class="vl-skill-grid" id="vl-maxhit-style-grid"></div></div>' +
        '<div class="vl-row">' +
          '<div class="vl-field"><label id="vl-mh-skill-label">Attack Level</label><input id="vl-mh-skill" type="number" min="1" max="100" value="50"></div>' +
          '<div class="vl-field"><label id="vl-mh-power-label">Melee Power</label><input id="vl-mh-power" type="number" min="0" value="0"></div>' +
        '</div>' +
        '<div class="vl-row">' +
          '<div class="vl-field"><label>Weapon Damage</label><input id="vl-mh-wepdmg" type="number" min="0" value="0"></div>' +
          '<div class="vl-field"><label>Attack Speed</label><input id="vl-mh-speed" type="number" min="0" step="0.1" value="2.5"></div>' +
        '</div>' +
        '<p class="vl-hint">Weapon Damage and Attack Speed come straight off your weapon\'s tooltip; the power stat comes from your equipped gear and also matches its tooltip.</p>' +
        '<div class="vl-result">' +
          '<p class="vl-result-head">Reckoning</p>' +
          '<div class="vl-stat-grid" style="grid-template-columns:1fr;"><div class="vl-stat"><span class="vl-stat-label">Max Hit</span><span class="vl-stat-value" id="vl-mh-result" style="font-size:24px;">-</span></div></div>' +
          '<p class="vl-hint" id="vl-mh-breakdown"></p>' +
        '</div>' +
      '</div>' +
      '<div id="vl-npcxphr-view" style="display:none;">' +
        '<p class="vl-warn" style="margin-bottom:14px;">Estimated model: this game has no confirmed hit-chance formula. Hit chance here is approximated as effective level divided by (effective level plus monster defence), clamped between 10% and 95%. Treat these numbers as a rough guide, not verified data like the rest of this wiki.</p>' +
        '<div class="vl-field"><label>Combat Style (for damage)</label><div class="vl-skill-grid" id="vl-nx-style-grid"></div></div>' +
        '<div class="vl-field"><label>Skill(s) Being Trained</label><div class="vl-skill-grid" id="vl-nx-skill-grid"></div><p class="vl-hint" id="vl-nx-hint"></p></div>' +
        '<div class="vl-row">' +
          '<div class="vl-field"><label id="vl-nx-skill-label">Attack Level</label><input id="vl-nx-skill" type="number" min="1" max="100" value="50"></div>' +
          '<div class="vl-field"><label id="vl-nx-power-label">Melee Power</label><input id="vl-nx-power" type="number" min="0" value="0"></div>' +
        '</div>' +
        '<div class="vl-row">' +
          '<div class="vl-field"><label>Weapon Damage</label><input id="vl-nx-wepdmg" type="number" min="0" value="0"></div>' +
          '<div class="vl-field"><label>Attack Speed</label><input id="vl-nx-speed" type="number" min="0" step="0.1" value="2.5"></div>' +
        '</div>' +
        '<div class="vl-field"><label>Monster</label><select id="vl-nx-monster"></select></div>' +
        '<div class="vl-result">' +
          '<p class="vl-result-head">Reckoning</p>' +
          '<div class="vl-stat-grid"><div class="vl-stat"><span class="vl-stat-label">Hit Chance</span><span class="vl-stat-value" id="vl-nx-hitchance">-</span></div><div class="vl-stat"><span class="vl-stat-label">Avg Dmg / Hit</span><span class="vl-stat-value" id="vl-nx-avgdmg">-</span></div><div class="vl-stat"><span class="vl-stat-label">Kills / Hour</span><span class="vl-stat-value" id="vl-nx-kph">-</span></div></div>' +
          '<div class="vl-stat-grid" style="grid-template-columns:1fr;margin-top:10px;"><div class="vl-stat"><span class="vl-stat-label" id="vl-nx-xph-label">XP / Hour</span><span class="vl-stat-value" id="vl-nx-xph" style="font-size:20px;">-</span></div></div>' +
          '<p class="vl-hint" id="vl-nx-sub"></p>' +
        '</div>' +
      '</div>' +
    '</div>';

  var TABS = [ {id:'fishing',label:'Fishing'},{id:'mining',label:'Mining'},{id:'smithing',label:'Smithing'},{id:'potionmaking',label:'Potion Making'},{id:'combat',label:'Combat'},{id:'maxhit',label:'Max Hit'},{id:'npcxphr',label:'NPC XP/hr'} ];
  var tabsEl = mount.querySelector('#vl-tabs');
  TABS.forEach(function(t){
    var b = document.createElement('button');
    b.type = 'button'; b.className = 'vl-tab'; b.textContent = t.label; b.dataset.tab = t.id;
    b.addEventListener('click', function(){ selectTab(t.id); });
    tabsEl.appendChild(b);
  });

  var state = { tab: 'fishing' };
  var gatherView = mount.querySelector('#vl-gather-view');
  var combatView = mount.querySelector('#vl-combat-view');
  var maxhitView = mount.querySelector('#vl-maxhit-view');
  var npcxphrView = mount.querySelector('#vl-npcxphr-view');

  function selectTab(id){
    state.tab = id;
    mount.querySelectorAll('.vl-tab').forEach(function(b){ b.classList.toggle('active', b.dataset.tab === id); });
    gatherView.style.display = 'none';
    combatView.style.display = 'none';
    maxhitView.style.display = 'none';
    npcxphrView.style.display = 'none';
    if (id === 'combat'){ combatView.style.display = 'block'; calcCombat(); return; }
    if (id === 'maxhit'){ maxhitView.style.display = 'block'; calcMaxHit(); return; }
    if (id === 'npcxphr'){ npcxphrView.style.display = 'block'; calcNpcXpHr(); return; }
    gatherView.style.display = 'block';
    populateActions();
    calcGather();
  }

  var actionSelect = mount.querySelector('#vl-action');
  var curLvlInput = mount.querySelector('#vl-cur-lvl');
  var tgtLvlInput = mount.querySelector('#vl-tgt-lvl');
  var curXpLabel = mount.querySelector('#vl-cur-xp');
  var tgtXpLabel = mount.querySelector('#vl-tgt-xp');
  var gatherResult = mount.querySelector('#vl-gather-result');
  var gatherFill = mount.querySelector('#vl-gather-fill');
  var breakdownSection = mount.querySelector('#vl-breakdown-section');
  var breakdownText = mount.querySelector('#vl-breakdown-text');

  function populateActions(){
    var entries = GATHER[state.tab] || [];
    actionSelect.innerHTML = '';
    entries.forEach(function(e, i){
      var opt = document.createElement('option');
      opt.value = String(i);
      opt.textContent = e.name + ' (' + fmt(e.xp) + ' xp)';
      actionSelect.appendChild(opt);
    });
  }

  function clampLevelInput(input){
    var v = parseInt(input.value, 10);
    if (isNaN(v)) v = 1;
    v = clamp(v, 1, MAX_LEVEL);
    input.value = v;
    return v;
  }

  function calcGather(){
    var entries = GATHER[state.tab] || [];
    if (entries.length === 0) return;
    var idx = parseInt(actionSelect.value, 10) || 0;
    var entry = entries[idx];
    var curLevel = clampLevelInput(curLvlInput);
    var tgtLevel = clampLevelInput(tgtLvlInput);
    var curXp = xpForLevel(curLevel);
    var tgtXp = xpForLevel(tgtLevel);
    curXpLabel.textContent = fmt(curXp);
    tgtXpLabel.textContent = fmt(tgtXp);
    if (tgtXp <= curXp){
      gatherResult.classList.add('done');
      gatherResult.textContent = 'Level ' + tgtLevel + ' calls for ' + fmt(tgtXp) + ' xp. You already have that at level ' + curLevel + ' (' + fmt(curXp) + ' xp). Nothing left to gather.';
      gatherFill.style.width = '100%';
      breakdownSection.style.display = 'none';
      return;
    }
    gatherResult.classList.remove('done');
    var needed = tgtXp - curXp;
    var actions = Math.ceil(needed / entry.xp);
    gatherResult.textContent = 'Level ' + curLevel + ' -> Level ' + tgtLevel + '\nNeed ' + fmt(needed) + ' more xp\n= ' + fmt(actions) + ' x ' + entry.name;
    gatherFill.style.width = '0%';

    var breakdown = state.tab === 'smithing' ? buildMaterialBreakdown(entry.name, actions) : null;
    if (!breakdown){
      breakdownSection.style.display = 'none';
    } else {
      breakdownSection.style.display = 'block';
      var text = breakdown.entries.map(function(e, i){
        var xp = getSmithingXp(e.name);
        var xpSuffix = xp ? ' (XP: ' + fmt(e.qty * xp) + ')' : '';
        return (i === 0 ? '' : '-> ') + fmt(e.qty) + ' ' + e.name + xpSuffix;
      }).join('\n');
      var rawNames = Object.keys(breakdown.totals);
      if (rawNames.length){
        text += '\n\nTotal raw materials: ' + rawNames.map(function(n){ return fmt(breakdown.totals[n]) + ' ' + n; }).join(', ');
      }
      var totalSeconds = breakdown.entries.reduce(function(sum, e){
        var dur = CRAFT_DURATION[e.name];
        return dur ? sum + e.qty * dur : sum;
      }, 0);
      if (totalSeconds > 0){
        text += '\nTotal craft time (Anvil/Furnace only, not counting ore gathering): ' + formatTime(totalSeconds);
      }
      breakdownText.textContent = text;
    }
  }

  actionSelect.addEventListener('change', calcGather);
  curLvlInput.addEventListener('input', calcGather);
  tgtLvlInput.addEventListener('input', calcGather);

  var selectedSkills = ['Attack'];
  var COMBAT_SKILLS = ['Attack','Archery','Magic','Defence','Evasion','Warding','Health'];
  var skillGrid = mount.querySelector('#vl-skill-grid');
  COMBAT_SKILLS.forEach(function(s){
    var b = document.createElement('button');
    b.type = 'button';
    b.className = 'vl-skill-btn' + (s === 'Attack' ? ' active' : '') + (s === 'Health' ? ' health' : '');
    b.textContent = s; b.dataset.skill = s;
    b.addEventListener('click', function(){ onSkillClick(s); });
    skillGrid.appendChild(b);
  });

  var combatHint = mount.querySelector('#vl-combat-hint');
  var combatCur = mount.querySelector('#vl-c-cur');
  var combatReq = mount.querySelector('#vl-c-req');
  var curLabel = mount.querySelector('#vl-cur-label');
  var reqLabel = mount.querySelector('#vl-req-label');
  var combatNext = mount.querySelector('#vl-c-next');
  var nextLabel = mount.querySelector('#vl-next-label');
  var secondaryField = mount.querySelector('#vl-secondary-field');
  var secLvlLabel = mount.querySelector('#vl-secondary-lvl-label');
  var secExpLabel = mount.querySelector('#vl-secondary-exp-label');
  var secLvl = mount.querySelector('#vl-sec-lvl');
  var secExp = mount.querySelector('#vl-sec-exp');
  var monsterSelect = mount.querySelector('#vl-monster');
  var customField = mount.querySelector('#vl-custom-field');
  var customXp = mount.querySelector('#vl-custom-xp');
  var killMin = mount.querySelector('#vl-kill-min');
  var killSec = mount.querySelector('#vl-kill-sec');
  var healthField = mount.querySelector('#vl-health-field');
  var healthLvl = mount.querySelector('#vl-h-lvl');
  var healthExp = mount.querySelector('#vl-h-exp');
  var warnEl = mount.querySelector('#vl-warn');
  var resultsEl = mount.querySelector('#vl-results');
  var rExp = mount.querySelector('#vl-r-exp');
  var rKills = mount.querySelector('#vl-r-kills');
  var rSub = mount.querySelector('#vl-r-sub');
  var rTime = mount.querySelector('#vl-r-time');
  var secProgress = mount.querySelector('#vl-sec-progress');
  var secDetail = mount.querySelector('#vl-sec-detail');
  var secBar = mount.querySelector('#vl-sec-bar');
  var healthProgress = mount.querySelector('#vl-health-progress');
  var healthDetail = mount.querySelector('#vl-health-detail');
  var healthBar = mount.querySelector('#vl-health-bar');

  function buildMonsterSelect(){
    monsterSelect.innerHTML = '';
    var areas = {};
    MONSTERS.forEach(function(m){ (areas[m.area] = areas[m.area] || []).push(m); });
    Object.keys(areas).forEach(function(area){
      var grp = document.createElement('optgroup');
      grp.label = area;
      areas[area].slice().sort(function(a,b){ return a.level-b.level; }).forEach(function(m){
        var opt = document.createElement('option');
        opt.value = String(m.xp);
        opt.textContent = m.name + ' (Lvl ' + m.level + ') - ' + fmt(m.xp) + ' XP';
        grp.appendChild(opt);
      });
      monsterSelect.appendChild(grp);
    });
    var customGrp = document.createElement('optgroup');
    customGrp.label = 'Other';
    var customOpt = document.createElement('option');
    customOpt.value = 'custom'; customOpt.textContent = 'Custom monster...';
    customGrp.appendChild(customOpt);
    monsterSelect.appendChild(customGrp);
  }

  function splitFractionsFor(skills){
    if (skills.length === 1 && skills[0] === 'Health') return {primary:0.25, secondary:0, health:0};
    if (skills.length === 1) return {primary:0.75, secondary:0, health:0.25};
    return {primary:0.375, secondary:0.375, health:0.25};
  }

  function findLevelForExp(total){
    var lvl = 1;
    for (var l = 1; l <= MAX_LEVEL; l++){ if (COMBAT_EXP[l].total <= total) lvl = l; else break; }
    return lvl;
  }

  function formatTime(totalSeconds){
    totalSeconds = Math.round(totalSeconds);
    var days = Math.floor(totalSeconds / 86400);
    var hours = Math.floor((totalSeconds % 86400) / 3600);
    var minutes = Math.floor((totalSeconds % 3600) / 60);
    var seconds = totalSeconds % 60;
    var parts = [];
    if (days) parts.push(days + 'd');
    if (hours || days) parts.push(hours + 'h');
    if (minutes || hours || days) parts.push(minutes + 'm');
    parts.push(seconds + 's');
    return parts.join(' ');
  }

  function clampLevel(input, fallback){
    var v = parseInt(input.value, 10);
    if (isNaN(v)) v = fallback;
    return v;
  }

  function updateHint(){
    if (selectedSkills.length === 2){
      combatHint.textContent = 'Dual training: 37.5% of each kill\'s EXP goes to ' + selectedSkills[0] + ' and ' + selectedSkills[1] + ', and 25% goes to Health.';
    } else if (selectedSkills[0] === 'Health'){
      combatHint.textContent = 'Training Health directly: 25% of every kill\'s EXP goes to Health.';
    } else {
      combatHint.textContent = '75% of every kill\'s EXP trains this skill. The other 25% always goes to Health.';
    }
    var primary = selectedSkills[0];
    curLabel.textContent = 'Current Level (' + primary + ')';
    reqLabel.textContent = 'Required Level (' + primary + ')';
    nextLabel.textContent = 'EXP Left For Next Level (' + primary + ')';
    if (selectedSkills.length === 2){
      var secondary = selectedSkills[1];
      secondaryField.style.display = 'block';
      secLvlLabel.textContent = secondary + ' - Current Level (optional)';
      secExpLabel.textContent = secondary + ' - EXP Left (optional)';
    } else {
      secondaryField.style.display = 'none';
    }
    healthField.style.display = selectedSkills.indexOf('Health') !== -1 ? 'none' : 'block';
  }

  function renderSide(levelInput, expInput, panel, detailEl, barEl, expGained, skillName, kills){
    if (!levelInput.value){ panel.style.display = 'none'; return; }
    var lvl = parseInt(levelInput.value, 10);
    if (!lvl || lvl < 1 || lvl > MAX_LEVEL){ panel.style.display = 'none'; return; }
    var curExp = COMBAT_EXP[lvl].total;
    if (expInput.value !== '' && COMBAT_EXP[lvl+1]){
      var expLeft = Math.max(0, parseFloat(expInput.value) || 0);
      curExp = Math.max(COMBAT_EXP[lvl].total, COMBAT_EXP[lvl+1].total - expLeft);
    }
    var newTotal = curExp + expGained;
    var newLevel = findLevelForExp(newTotal);
    var nextLvl = Math.min(newLevel + 1, MAX_LEVEL);
    var into = newTotal - COMBAT_EXP[newLevel].total;
    var forLevel = COMBAT_EXP[nextLvl] ? COMBAT_EXP[nextLvl].total - COMBAT_EXP[newLevel].total : 1;
    var pct = newLevel >= MAX_LEVEL ? 100 : Math.min(100, Math.round((into/forLevel)*100));
    panel.style.display = 'block';
    detailEl.textContent = newLevel >= MAX_LEVEL
      ? (skillName + ': already at max level.')
      : (skillName + ': level ' + lvl + ' -> ' + newLevel + ' (' + pct + '% into ' + nextLvl + '), +' + fmt(expGained) + ' xp over ' + fmt(kills) + ' kills.');
    barEl.style.width = pct + '%';
  }

  function calcCombat(){
    warnEl.style.display = 'none';
    resultsEl.style.display = 'block';

    function warn(msg){
      warnEl.textContent = msg; warnEl.style.display = 'block';
      resultsEl.style.display = 'none';
      secProgress.style.display = 'none';
      healthProgress.style.display = 'none';
    }

    var curLevel = clampLevel(combatCur, 0);
    var reqLevel = clampLevel(combatReq, 0);
    if (!curLevel || !reqLevel || curLevel < 1 || curLevel > MAX_LEVEL || reqLevel < 1 || reqLevel > MAX_LEVEL){ warn('Enter valid current and required levels (1-100).'); return; }
    if (reqLevel <= curLevel){ warn('Required level must be higher than current level.'); return; }

    var monsterXp;
    if (monsterSelect.value === 'custom') monsterXp = parseFloat(customXp.value);
    else monsterXp = parseFloat(monsterSelect.value);
    if (!monsterXp || monsterXp <= 0){ warn('Pick a monster or enter a custom XP value.'); return; }

    var mins = parseFloat(killMin.value) || 0;
    var secs = parseFloat(killSec.value) || 0;
    var timePerKill = mins * 60 + secs;
    if (timePerKill <= 0){ warn('Enter an average time to kill greater than 0.'); return; }

    var nextFull = COMBAT_EXP[curLevel+1] ? COMBAT_EXP[curLevel+1].total - COMBAT_EXP[curLevel].total : null;
    var expToNext = parseFloat(combatNext.value);
    if (nextFull !== null && (isNaN(expToNext) || expToNext < 0)){ warn('Enter the EXP left for your next level.'); return; }

    var totalNeeded;
    if (nextFull === null) totalNeeded = 0;
    else if (reqLevel === curLevel + 1) totalNeeded = Math.max(0, expToNext);
    else totalNeeded = Math.max(0, expToNext) + Math.max(0, COMBAT_EXP[reqLevel].total - COMBAT_EXP[curLevel+1].total);

    var fr = splitFractionsFor(selectedSkills);
    var primarySkill = selectedSkills[0];
    var secondarySkill = selectedSkills.length === 2 ? selectedSkills[1] : null;

    var primaryPerKill = monsterXp * fr.primary;
    var secondaryPerKill = monsterXp * fr.secondary;
    var healthPerKill = monsterXp * fr.health;

    var kills = totalNeeded === 0 ? 0 : Math.ceil(totalNeeded / primaryPerKill);
    var totalTime = kills * timePerKill;
    var secondaryGained = kills * secondaryPerKill;
    var healthGained = kills * healthPerKill;

    rExp.textContent = fmt(totalNeeded) + ' xp';
    rKills.textContent = fmt(kills);
    rSub.textContent = secondarySkill
      ? ('Each kill gives ' + primaryPerKill.toLocaleString(undefined,{maximumFractionDigits:1}) + ' xp to ' + primarySkill + ' and ' + secondarySkill + ' (of ' + fmt(monsterXp) + ' total)')
      : ('Each kill gives ' + primaryPerKill.toLocaleString(undefined,{maximumFractionDigits:1}) + ' xp to ' + primarySkill + ' (of ' + fmt(monsterXp) + ' total)');
    rTime.textContent = formatTime(totalTime);

    if (selectedSkills.indexOf('Health') !== -1) healthProgress.style.display = 'none';
    else renderSide(healthLvl, healthExp, healthProgress, healthDetail, healthBar, healthGained, 'Health', kills);

    if (!secondarySkill) secProgress.style.display = 'none';
    else renderSide(secLvl, secExp, secProgress, secDetail, secBar, secondaryGained, secondarySkill, kills);
  }

  function onSkillClick(skill){
    if (skill === 'Health'){ selectedSkills = ['Health']; }
    else if (selectedSkills.indexOf('Health') !== -1){ selectedSkills = [skill]; }
    else if (selectedSkills.indexOf(skill) !== -1){ if (selectedSkills.length > 1) selectedSkills = selectedSkills.filter(function(s){ return s !== skill; }); }
    else if (selectedSkills.length >= 2){ selectedSkills = [selectedSkills[1], skill]; }
    else { selectedSkills = selectedSkills.concat([skill]); }
    mount.querySelectorAll('.vl-skill-btn').forEach(function(b){ b.classList.toggle('active', selectedSkills.indexOf(b.dataset.skill) !== -1); });
    updateHint();
    calcCombat();
  }

  [combatCur, combatReq, combatNext, secLvl, secExp, customXp, killMin, killSec, healthLvl, healthExp].forEach(function(el){
    el.addEventListener('input', calcCombat);
  });

  buildMonsterSelect();
  monsterSelect.addEventListener('change', function(){
    customField.style.display = monsterSelect.value === 'custom' ? 'block' : 'none';
    calcCombat();
  });
  updateHint();

  // ------------- Max Hit -------------
  var maxhitStyle = 'Attack';
  var mhStyleGrid = mount.querySelector('#vl-maxhit-style-grid');
  Object.keys(MAXHIT_STYLES).forEach(function(styleKey){
    var b = document.createElement('button');
    b.type = 'button';
    b.className = 'vl-skill-btn' + (styleKey === maxhitStyle ? ' active' : '');
    b.textContent = MAXHIT_STYLES[styleKey].label;
    b.dataset.style = styleKey;
    b.addEventListener('click', function(){
      maxhitStyle = styleKey;
      mhStyleGrid.querySelectorAll('.vl-skill-btn').forEach(function(btn){ btn.classList.toggle('active', btn.dataset.style === styleKey); });
      mhSkillLabel.textContent = MAXHIT_STYLES[styleKey].label + ' Level';
      mhPowerLabel.textContent = MAXHIT_STYLES[styleKey].powerLabel;
      calcMaxHit();
    });
    mhStyleGrid.appendChild(b);
  });

  var mhSkillLabel = mount.querySelector('#vl-mh-skill-label');
  var mhPowerLabel = mount.querySelector('#vl-mh-power-label');
  var mhSkill = mount.querySelector('#vl-mh-skill');
  var mhPower = mount.querySelector('#vl-mh-power');
  var mhWepDmg = mount.querySelector('#vl-mh-wepdmg');
  var mhSpeed = mount.querySelector('#vl-mh-speed');
  var mhResult = mount.querySelector('#vl-mh-result');
  var mhBreakdown = mount.querySelector('#vl-mh-breakdown');

  function calcMaxHit(){
    var skillLevel = parseFloat(mhSkill.value) || 0;
    var power = parseFloat(mhPower.value) || 0;
    var wepDmg = parseFloat(mhWepDmg.value) || 0;
    var speed = parseFloat(mhSpeed.value) || 0;
    var r = computeMaxHit(skillLevel, wepDmg, speed, power);
    mhResult.textContent = fmt1(r.maxHit);
    mhBreakdown.textContent = 'Effective ' + MAXHIT_STYLES[maxhitStyle].label + ' level: ' + fmt1(r.effectiveAttackLevel) +
      ' | Weapon damage bonus: ' + fmt1(r.weaponDamageBonus) +
      ' | Flat damage bonus: ' + fmt1(r.flatDamageBonus);
  }

  [mhSkill, mhPower, mhWepDmg, mhSpeed].forEach(function(el){ el.addEventListener('input', calcMaxHit); });

  // ------------- NPC XP/hr -------------
  var nxStyle = 'Attack';
  var nxSelectedSkills = ['Attack'];
  var nxStyleGrid = mount.querySelector('#vl-nx-style-grid');
  Object.keys(MAXHIT_STYLES).forEach(function(styleKey){
    var b = document.createElement('button');
    b.type = 'button';
    b.className = 'vl-skill-btn' + (styleKey === nxStyle ? ' active' : '');
    b.textContent = MAXHIT_STYLES[styleKey].label;
    b.dataset.style = styleKey;
    b.addEventListener('click', function(){
      nxStyle = styleKey;
      nxStyleGrid.querySelectorAll('.vl-skill-btn').forEach(function(btn){ btn.classList.toggle('active', btn.dataset.style === styleKey); });
      calcNpcXpHr();
    });
    nxStyleGrid.appendChild(b);
  });

  var nxSkillGrid = mount.querySelector('#vl-nx-skill-grid');
  COMBAT_SKILLS.forEach(function(s){
    var b = document.createElement('button');
    b.type = 'button';
    b.className = 'vl-skill-btn' + (s === 'Attack' ? ' active' : '') + (s === 'Health' ? ' health' : '');
    b.textContent = s; b.dataset.skill = s;
    b.addEventListener('click', function(){ onNxSkillClick(s); });
    nxSkillGrid.appendChild(b);
  });

  var nxHint = mount.querySelector('#vl-nx-hint');
  var nxSkillLabel = mount.querySelector('#vl-nx-skill-label');
  var nxPowerLabel = mount.querySelector('#vl-nx-power-label');
  var nxSkill = mount.querySelector('#vl-nx-skill');
  var nxPower = mount.querySelector('#vl-nx-power');
  var nxWepDmg = mount.querySelector('#vl-nx-wepdmg');
  var nxSpeed = mount.querySelector('#vl-nx-speed');
  var nxMonsterSelect = mount.querySelector('#vl-nx-monster');
  var nxHitChance = mount.querySelector('#vl-nx-hitchance');
  var nxAvgDmg = mount.querySelector('#vl-nx-avgdmg');
  var nxKph = mount.querySelector('#vl-nx-kph');
  var nxXphLabel = mount.querySelector('#vl-nx-xph-label');
  var nxXph = mount.querySelector('#vl-nx-xph');
  var nxSub = mount.querySelector('#vl-nx-sub');

  function buildNxMonsterSelect(){
    nxMonsterSelect.innerHTML = '';
    var areas = {};
    MONSTERS.forEach(function(m, idx){ (areas[m.area] = areas[m.area] || []).push(idx); });
    Object.keys(areas).forEach(function(area){
      var grp = document.createElement('optgroup');
      grp.label = area;
      areas[area].slice().sort(function(a,b){ return MONSTERS[a].level - MONSTERS[b].level; }).forEach(function(idx){
        var m = MONSTERS[idx];
        var opt = document.createElement('option');
        opt.value = String(idx);
        opt.textContent = m.name + ' (Lvl ' + m.level + ', HP ' + fmt(m.health) + ', Def ' + m.defence + ')';
        grp.appendChild(opt);
      });
      nxMonsterSelect.appendChild(grp);
    });
  }

  function updateNxLabelsAndHint(){
    if (nxSelectedSkills.length === 2){
      nxHint.textContent = 'Dual training: 37.5% of each kill\'s EXP goes to ' + nxSelectedSkills[0] + ' and ' + nxSelectedSkills[1] + ', and 25% goes to Health.';
    } else if (nxSelectedSkills[0] === 'Health'){
      nxHint.textContent = 'Training Health directly: 25% of every kill\'s EXP goes to Health.';
    } else {
      nxHint.textContent = '75% of every kill\'s EXP trains this skill. The other 25% always goes to Health.';
    }
    nxSkillLabel.textContent = MAXHIT_STYLES[nxStyle].label + ' Level';
    nxPowerLabel.textContent = MAXHIT_STYLES[nxStyle].powerLabel;
    nxXphLabel.textContent = nxSelectedSkills[0] + ' XP / Hour';
  }

  function onNxSkillClick(skill){
    if (skill === 'Health'){ nxSelectedSkills = ['Health']; }
    else if (nxSelectedSkills.indexOf('Health') !== -1){ nxSelectedSkills = [skill]; }
    else if (nxSelectedSkills.indexOf(skill) !== -1){ if (nxSelectedSkills.length > 1) nxSelectedSkills = nxSelectedSkills.filter(function(s){ return s !== skill; }); }
    else if (nxSelectedSkills.length >= 2){ nxSelectedSkills = [nxSelectedSkills[1], skill]; }
    else { nxSelectedSkills = nxSelectedSkills.concat([skill]); }
    nxSkillGrid.querySelectorAll('.vl-skill-btn').forEach(function(b){ b.classList.toggle('active', nxSelectedSkills.indexOf(b.dataset.skill) !== -1); });
    updateNxLabelsAndHint();
    calcNpcXpHr();
  }

  function calcNpcXpHr(){
    var m = MONSTERS[parseInt(nxMonsterSelect.value, 10) || 0];
    if (!m){ return; }
    var skillLevel = parseFloat(nxSkill.value) || 0;
    var power = parseFloat(nxPower.value) || 0;
    var wepDmg = parseFloat(nxWepDmg.value) || 0;
    var speed = parseFloat(nxSpeed.value) || 0;
    if (speed <= 0){ nxSub.textContent = 'Enter an attack speed greater than 0.'; return; }

    var effectiveLevel = skillLevel + MAXHIT_CONST.base_effective_level;
    var hitChance = effectiveLevel / (effectiveLevel + m.defence);
    hitChance = Math.max(0.10, Math.min(0.95, hitChance));

    var mh = computeMaxHit(skillLevel, wepDmg, speed, power);
    var avgDamagePerHit = (mh.maxHit / 2) * hitChance;
    var dps = avgDamagePerHit / speed;
    var killsPerHour;
    if (dps <= 0) killsPerHour = 0;
    else {
      var timeToKillSeconds = m.health / dps;
      killsPerHour = 3600 / timeToKillSeconds;
    }

    var fr = splitFractionsFor(nxSelectedSkills);
    var xpPerKillPrimary = m.xp * fr.primary;
    var xpPerHourPrimary = killsPerHour * xpPerKillPrimary;

    nxHitChance.textContent = Math.round(hitChance * 100) + '%';
    nxAvgDmg.textContent = fmt1(avgDamagePerHit);
    nxKph.textContent = fmt1(killsPerHour);
    nxXph.textContent = fmt(xpPerHourPrimary) + ' xp';

    var subParts = [];
    if (nxSelectedSkills.length === 2){
      var xpPerHourSecondary = killsPerHour * (m.xp * fr.secondary);
      subParts.push(nxSelectedSkills[1] + ': ' + fmt(xpPerHourSecondary) + ' xp/hr');
    }
    if (fr.health > 0 && nxSelectedSkills.indexOf('Health') === -1){
      var xpPerHourHealth = killsPerHour * (m.xp * fr.health);
      subParts.push('Health: ' + fmt(xpPerHourHealth) + ' xp/hr');
    }
    nxSub.textContent = subParts.length ? subParts.join(' | ') : '';
  }

  [nxSkill, nxPower, nxWepDmg, nxSpeed].forEach(function(el){ el.addEventListener('input', calcNpcXpHr); });
  buildNxMonsterSelect();
  nxMonsterSelect.addEventListener('change', calcNpcXpHr);
  updateNxLabelsAndHint();

  selectTab('fishing');
});
// ==========================================================================
// Mobile nav toggle — the sidebar (#mw-panel) renders inline far down the
// page on narrow screens, so add a fixed hamburger button that slides it in
// as an overlay drawer instead. CSS in Common.css keeps this hidden/inert
// on desktop widths (>=1000px).
// ==========================================================================
(function () {
  function initMobileNav() {
    var panel = document.getElementById('mw-panel');
    if (!panel || document.getElementById('mobile-nav-toggle')) return;

    var toggle = document.createElement('button');
    toggle.id = 'mobile-nav-toggle';
    toggle.type = 'button';
    toggle.setAttribute('aria-label', 'Toggle navigation menu');
    toggle.textContent = '\u2630 Menu';

    var backdrop = document.createElement('div');
    backdrop.id = 'mobile-nav-backdrop';

    document.body.insertBefore(backdrop, document.body.firstChild);
    document.body.insertBefore(toggle, document.body.firstChild);

    function closeNav() {
      panel.classList.remove('mobile-nav-open');
      backdrop.classList.remove('mobile-nav-open');
    }
    function toggleNav() {
      panel.classList.toggle('mobile-nav-open');
      backdrop.classList.toggle('mobile-nav-open');
    }

    toggle.addEventListener('click', toggleNav);
    backdrop.addEventListener('click', closeNav);
    panel.querySelectorAll('a').forEach(function (a) {
      a.addEventListener('click', closeNav);
    });
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initMobileNav);
  } else {
    initMobileNav();
  }
})();