MediaWiki:Common.js: Difference between revisions
No edit summary |
gave the mobile menu some actual links |
||
| (18 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:' | 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). | |||
// 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 = [ | var MONSTERS = [ | ||
{name:'Goblin',level:1,xp:50,area:'Broken Village'},{name:'Hen',level:1,xp:40,area:'Farmlands'},{name:'Rooster',level:1,xp:40,area:'Farmlands'},{name:'Cow',level:5,xp:105,area:'Farmlands'},{name:'Skeleton',level:6,xp:80,area:'Wheat Fields'},{name:'Bandit',level:10,xp:132,area:'Forest'},{name:'Skeleton Miner',level:13,xp:141,area:'Valen Gate / Grave Town'},{name:'Pirate Corsair',level:10,xp:132,area:'Valen Port'},{name:'Pirate Rigger',level:10,xp:132,area:'Valen Port'},{name:'Pirate Deckhand',level:10,xp:132,area:'Valen Port'},{name:'Goblin Villager',level:15,xp:229,area:'Goblin Village'},{name:'Skeleton Knight',level:26,xp:442,area:'West Mine'},{name:'Highwayman',level:28,xp:384,area:'West Mine'},{name:'Bandit Mercenary',level:30,xp:389,area:'Mercenary Camp'},{name:'Slum Bandit',level:30,xp:390,area:'Valen City'},{name:'Elf',level:37,xp:548,area:'Elven Haven'},{name:'Elf Scholar',level:37,xp:548,area:'Elven Haven'},{name:'Noble Skeleton',level:37,xp:548,area:'Valen City'},{name:'Cavern Goblin',level:37,xp:548,area:'West Mine Deep Cavern'},{name:'Skeleton Knight (Darklands)',level:67,xp:1670,area:'Darklands'},{name:'Ashen Warrior',level:95,xp:2925,area:'West Mine Ashen Cavern'},{name:'Ashen Archer',level:95,xp:2925,area:'West Mine Ashen Cavern'},{name:'Ashen Mage',level:95,xp:2925,area:'West Mine Ashen Cavern'},{name:'Goblin Berserker (Boss)',level:9,xp:273,area:'Broken Village'},{name:'Goblin Watcher (Boss)',level:9,xp:273,area:'Broken Village'},{name:'Pirate Captain (Boss)',level:25,xp:625,area:'Valen Port'},{name:'Bandit Leader (Boss)',level:25,xp:626,area:'Forest Alcove'},{name:'Goblin Chieftain (Boss)',level:40,xp:1400,area:'Goblin Village'},{name:'Goblin General (Boss)',level:55,xp:1900,area:'General Cave'},{name:'Bandit Mercenary Boss',level:74,xp:3480,area:'Mercenary Camp'},{name:'Skeleton Pioneer (Boss)',level:90,xp:4750,area:'West Mine'},{name:'Cavern Goblin Hunter (Boss)',level:107,xp:6105,area:'West Mine Deep Cavern'},{name:'Fire Lich (Boss)',level:120,xp:7350,area:'Grave Town'},{name:'Elf Warden (Boss)',level:120,xp:7350,area:'Elven Haven'},{name:'The Burning King (Boss)',level:307,xp:30260,area:'West Mine Ashen Cavern'} | {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} | ||
]; | ]; | ||
| Line 111: | Line 323: | ||
'<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-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"><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>' + | ||
'<div id="vl-combat-view" style="display:none;">' + | '<div id="vl-combat-view" style="display:none;">' + | ||
| Line 142: | Line 355: | ||
'<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>' + | '<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>' + | '<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>' + | '</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'} ]; | 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'); | var tabsEl = mount.querySelector('#vl-tabs'); | ||
TABS.forEach(function(t){ | TABS.forEach(function(t){ | ||
| Line 159: | Line 392: | ||
var combatView = mount.querySelector('#vl-combat-view'); | var combatView = mount.querySelector('#vl-combat-view'); | ||
var maxhitView = mount.querySelector('#vl-maxhit-view'); | var maxhitView = mount.querySelector('#vl-maxhit-view'); | ||
var npcxphrView = mount.querySelector('#vl-npcxphr-view'); | |||
function selectTab(id){ | function selectTab(id){ | ||
| Line 166: | Line 400: | ||
combatView.style.display = 'none'; | combatView.style.display = 'none'; | ||
maxhitView.style.display = 'none'; | maxhitView.style.display = 'none'; | ||
npcxphrView.style.display = 'none'; | |||
if (id === 'combat'){ combatView.style.display = 'block'; calcCombat(); return; } | if (id === 'combat'){ combatView.style.display = 'block'; calcCombat(); return; } | ||
if (id === 'maxhit'){ maxhitView.style.display = 'block'; calcMaxHit(); return; } | if (id === 'maxhit'){ maxhitView.style.display = 'block'; calcMaxHit(); return; } | ||
if (id === 'npcxphr'){ npcxphrView.style.display = 'block'; calcNpcXpHr(); return; } | |||
gatherView.style.display = 'block'; | gatherView.style.display = 'block'; | ||
populateActions(); | populateActions(); | ||
| Line 180: | Line 416: | ||
var gatherResult = mount.querySelector('#vl-gather-result'); | var gatherResult = mount.querySelector('#vl-gather-result'); | ||
var gatherFill = mount.querySelector('#vl-gather-fill'); | var gatherFill = mount.querySelector('#vl-gather-fill'); | ||
var breakdownSection = mount.querySelector('#vl-breakdown-section'); | |||
var breakdownText = mount.querySelector('#vl-breakdown-text'); | |||
function populateActions(){ | function populateActions(){ | ||
| Line 215: | Line 453: | ||
gatherResult.textContent = 'Level ' + tgtLevel + ' calls for ' + fmt(tgtXp) + ' xp. You already have that at level ' + curLevel + ' (' + fmt(curXp) + ' xp). Nothing left to gather.'; | 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%'; | gatherFill.style.width = '100%'; | ||
breakdownSection.style.display = 'none'; | |||
return; | return; | ||
} | } | ||
| Line 222: | Line 461: | ||
gatherResult.textContent = 'Level ' + curLevel + ' -> Level ' + tgtLevel + '\nNeed ' + fmt(needed) + ' more xp\n= ' + fmt(actions) + ' x ' + entry.name; | gatherResult.textContent = 'Level ' + curLevel + ' -> Level ' + tgtLevel + '\nNeed ' + fmt(needed) + ' more xp\n= ' + fmt(actions) + ' x ' + entry.name; | ||
gatherFill.style.width = '0%'; | 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; | |||
} | |||
} | } | ||
| Line 498: | Line 761: | ||
[mhSkill, mhPower, mhWepDmg, mhSpeed].forEach(function(el){ el.addEventListener('input', calcMaxHit); }); | [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'); | 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(); | |||
} | |||
})(); | |||