MediaWiki:Common.js: Difference between revisions
Switched to use CSS class for passing arguments |
No edit summary |
||
| Line 4: | Line 4: | ||
$('#ca-view a').text('Read'); | $('#ca-view a').text('Read'); | ||
}); | }); | ||
// | // ========================================================================== | ||
mw.hook('wikipage.content').add(function($content) { | // 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}; | |||
return; | 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: [ {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} ], | |||
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; }); }); | |||
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'} | |||
]; | |||
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>' + | |||
'<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>'; | |||
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 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'); | |||
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'; | |||
if (id === 'combat'){ combatView.style.display = 'block'; calcCombat(); return; } | |||
if (id === 'maxhit'){ maxhitView.style.display = 'block'; calcMaxHit(); 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'); | |||
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%'; | |||
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%'; | |||
} | |||
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 ( | 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); }); | |||
selectTab('fishing'); | |||
}); | }); | ||