MediaWiki:Gadget-dps-core.js

From Zeah RSPS - Wiki
Revision as of 08:16, 16 February 2022 by Soulgazer (talk | contribs) (Created page with "// <nowiki> // OSRS DPS Calculator // Version 1.0 // Made by Gau Cho // // // Thanks to Bitterkoekje, Elessar2, Gaz, Koekenpan, riblet15, TehKittyCat // import Gadget-dps-data.js // import Gadget-dps-data2.js →‎globals equipment, monster, $, rswiki, OO: →‎jshint es3: true: 'use strict'; //Debug var ticTime = performance.now(); function tic() { ticTime = performance.now(); } function toc() { var ticTocTime = performance.now() - ticTime; console.log(...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
// <nowiki>

// OSRS DPS Calculator
// Version 1.0
// Made by Gau Cho
//
//
// Thanks to Bitterkoekje, Elessar2, Gaz, Koekenpan, riblet15, TehKittyCat

// import Gadget-dps-data.js
// import Gadget-dps-data2.js

/* globals equipment, monster, $, rswiki, OO */
/* jshint es3: true */

'use strict';

//Debug
var ticTime = performance.now();
function tic() {
    ticTime = performance.now();
}
function toc() {
    var ticTocTime = performance.now() - ticTime;
    console.log('Milliseconds elapsed: ' + ticTocTime);
    return ticTocTime;
}
//End Debug

//'Chivalry': {AttackBonus:1.15, StrengthBonus:1.18, DefenceBonus:1.2, DrainSpeed:24, vi:0, TurnsOff:['Thick Skin','Burst of Strength','Clarity of Thought','Rock Skin','Superhuman Strength','Improved Reflexes','Steel Skin','Ultimate Strength','Incredible Reflexes','Piety','Rigour','Augury']},
equipment.prayers = {
    'Thick Skin': {DefenceBonus:1.05, DrainSpeed:3, vi:0, TurnsOff:['Rock Skin','Steel Skin','Chivalry','Piety','Rigour','Augury']},
    'Burst of Strength': {StrengthBonus:1.05, DrainSpeed:3, vi:0, TurnsOff:['Superhuman Strength','Ultimate Strength','Chivalry','Piety']},
    'Clarity of Thought': {AttackBonus:1.05, DrainSpeed:3, vi:0, TurnsOff:['Improved Reflexes','Incredible Reflexes','Chivalry','Piety']},
    'Sharp Eye': {RangedAttackBonus:1.05, RangedStrengthBonus:1.05, DrainSpeed:3, vi:0, TurnsOff:['Hawk Eye','Eagle Eye','Rigour']},
    'Mystic Will': {MagicBonus:1.05, DrainSpeed:3, vi:0, TurnsOff:['Mystic Lore','Mystic Might','Augury']},
    'Rock Skin': {DefenceBonus:1.1, DrainSpeed:6, vi:0, TurnsOff:['Thick Skin','Steel Skin','Chivalry','Piety','Rigour','Augury']},
    'Superhuman Strength': {StrengthBonus:1.1, DrainSpeed:6, vi:0, TurnsOff:['Burst of Strength','Ultimate Strength','Chivalry','Piety']},
    'Improved Reflexes': {AttackBonus:1.1, DrainSpeed:6, vi:0, TurnsOff:['Clarity of Thought','Incredible Reflexes','Chivalry','Piety']},
    'Rapid Restore': {DrainSpeed:1, vi:1, TurnsOff:[]},
    'Rapid Heal': {DrainSpeed:2, vi:1, TurnsOff:[]},
    'Protect Item': {DrainSpeed:2, vi:1, TurnsOff:[]},
    'Hawk Eye': {RangedAttackBonus:1.1, RangedStrengthBonus:1.1, DrainSpeed:6, vi:0, TurnsOff:['Sharp Eye','Eagle Eye','Rigour']},
    'Mystic Lore': {MagicBonus:1.1, DrainSpeed:6, vi:0, TurnsOff:['Mystic Will','Mystic Might','Augury']},
    'Steel Skin': {DefenceBonus:1.15, DrainSpeed:12, vi:0, TurnsOff:['Thick Skin','Rock Skin','Chivalry','Piety','Rigour','Augury']},
    'Ultimate Strength': {StrengthBonus:1.15, DrainSpeed:12, vi:0, TurnsOff:['Burst of Strength','Superhuman Strength','Chivalry','Piety']},
    'Incredible Reflexes': {AttackBonus:1.15, DrainSpeed:12, vi:0, TurnsOff:['Clarity of Thought','Improved Reflexes','Chivalry','Piety']},
    'Protect from Magic': {DrainSpeed:12, vi:1, TurnsOff:['Protect from Missiles','Protect from Melee','Retribution','Redemption','Smite']},
    'Protect from Missiles': {DrainSpeed:12, vi:1, TurnsOff:['Protect from Magic','Protect from Melee','Retribution','Redemption','Smite']},
    'Protect from Melee': {DrainSpeed:12, vi:1, TurnsOff:['Protect from Magic','Protect from Missiles','Retribution','Redemption','Smite']},
    'Eagle Eye': {RangedAttackBonus:1.15, RangedStrengthBonus:1.15, DrainSpeed:12, vi:2, TurnsOff:['Sharp Eye','Hawk Eye','Rigour']},
    'Mystic Might': {MagicBonus:1.15, DrainSpeed:12, vi:2, TurnsOff:['Mystic Will','Mystic Lore','Augury']},
    'Retribution': {DrainSpeed:3, vi:0, TurnsOff:['Protect from Magic','Protect from Missiles','Protect from Melee','Redemption','Smite']},
    'Redemption': {DrainSpeed:6, vi:0, TurnsOff:['Protect from Magic','Protect from Missiles','Protect from Melee','Retribution','Smite']},
    'Smite': {DrainSpeed:18, vi:0, TurnsOff:['Protect from Magic','Protect from Missiles','Protect from Melee','Retribution','Redemption']},
    'Preserve': {DrainSpeed:2, vi:1, TurnsOff:[]},
    'Chivalry': {AttackBonus:1.15, StrengthBonus:1.18, DefenceBonus:1.2, DrainSpeed:24, vi:0, TurnsOff:['Thick Skin','Burst of Strength','Clarity of Thought','Rock Skin','Superhuman Strength','Improved Reflexes','Steel Skin','Ultimate Strength','Incredible Reflexes','Piety','Rigour','Augury']},
    'Piety': {AttackBonus:1.2, StrengthBonus:1.23, DefenceBonus:1.25, DrainSpeed:24, vi:2, TurnsOff:['Thick Skin','Burst of Strength','Clarity of Thought','Rock Skin','Superhuman Strength','Improved Reflexes','Steel Skin','Ultimate Strength','Incredible Reflexes','Chivalry','Rigour','Augury']},
    'Rigour': {DefenceBonus:1.25, RangedAttackBonus:1.2, RangedStrengthBonus:1.23, DrainSpeed:24, vi:2, TurnsOff:['Thick Skin','Rock Skin','Steel Skin','Chivalry','Piety','Sharp Eye','Hawk Eye','Eagle Eye','Augury']},
    'Augury': {DefenceBonus:1.25, MagicBonus:1.25, DrainSpeed:24, vi:2, TurnsOff:['Thick Skin','Rock Skin','Steel Skin','Chivalry','Piety','Mystic Will','Mystic Lore','Mystic Might','Rigour']},
};

// 'Super combat potion':{AttackAdd:5,AttackMult:1.15,StrengthAdd:5,StrengthMult:1.15,DefenceAdd:5,DefenceMult:1.15,vi:2},
equipment.potions = {
    'Bastion potion':{DefenceAdd:5,DefenceMult:1.15,RangedAdd:4,RangedMult:1.1,vi:2,im:'Bastion potion(4)'},
    'Battlemage potion':{DefenceAdd:5,DefenceMult:1.15,MagicAdd:4,MagicMult:1,vi:2,im:'Battlemage potion(4)'},
    'Dragon battleaxe':{StrengthAdd:-1,StrengthMult:-1,vi:2,im:'Dragon battleaxe'},
    'Imbued heart':{MagicAdd:1,MagicMult:1.1,vi:2,im:'Imbued heart'},
    'Overload (+)':{AttackAdd:6,AttackMult:1.16,StrengthAdd:6,StrengthMult:1.16,DefenceAdd:6,DefenceMult:1.16,RangedAdd:6,RangedMult:1.16,MagicAdd:6,MagicMult:1.16,vi:2,im:'Overload (4) (Chambers of Xeric)'},
    'Overload (nightmare zone)':{AttackAdd:5,AttackMult:1.15,StrengthAdd:5,StrengthMult:1.15,DefenceAdd:5,DefenceMult:1.15,RangedAdd:5,RangedMult:1.15,MagicAdd:5,MagicMult:1.15,vi:2,im:'Overload (4)'},
    'Ranging potion':{RangedAdd:4,RangedMult:1.1,vi:2,im:'Ranging potion(4)'},
    'Saradomin brew':{AttackAdd:-2,AttackMult:0.9,StrengthAdd:-2,StrengthMult:0.9,DefenceAdd:2,DefenceMult:1.2,RangedAdd:-2,RangedMult:0.9,MagicAdd:-2,MagicMult:0.9,vi:2,im:'Saradomin brew(4)'},
    'Super combat potion':{AttackAdd:5,AttackMult:1.15,StrengthAdd:5,StrengthMult:1.15,DefenceAdd:5,DefenceMult:1.15,vi:2,im:'Super combat potion(4)'},
    'Super magic potion':{MagicAdd:5,MagicMult:1.15,vi:2,im:'Super magic potion (4)'},
    'Super ranging':{RangedAdd:5,RangedMult:1.15,vi:2,im:'Super ranging (4)'},
    'Xeric\'s aid (+)':{AttackAdd:-4,AttackMult:0.9,StrengthAdd:-4,StrengthMult:0.9,DefenceAdd:5,DefenceMult:1.2,RangedAdd:-4,RangedMult:0.9,MagicAdd:-4,MagicMult:0.9,vi:2,im:'Xeric\'s aid (4)'},
    'Zamorak brew':{AttackAdd:2,AttackMult:1.2,StrengthAdd:2,StrengthMult:1.12,DefenceAdd:-2,DefenceMult:0.9,vi:2,im:'Zamorak brew(4)'},
    'Attack potion':{AttackAdd:3,AttackMult:1.1,vi:1,im:'Attack potion(4)'},
    'Combat potion':{AttackAdd:3,AttackMult:1.1,StrengthAdd:3,StrengthMult:1.1,vi:1,im:'Combat potion(4)'},
    'Defence potion':{DefenceAdd:3,DefenceMult:1.1,vi:1,im:'Defence potion(4)'},
    'Elder potion (+)':{AttackAdd:6,AttackMult:1.16,StrengthAdd:6,StrengthMult:1.16,DefenceAdd:6,DefenceMult:1.16,vi:1,im:'Elder potion (4)'},
    'Elder potion':{AttackAdd:5,AttackMult:1.13,StrengthAdd:5,StrengthMult:1.13,DefenceAdd:5,DefenceMult:1.13,vi:1,im:'Elder potion (2)'},
    'Elder potion (-)':{AttackAdd:4,AttackMult:1.1,StrengthAdd:4,StrengthMult:1.1,DefenceAdd:4,DefenceMult:1.1,vi:1,im:'Elder potion (1)'},
    'Excalibur':{DefenceAdd:8,DefenceMult:1,vi:1,im:'Excalibur'},
    'Kodai potion (+)':{DefenceAdd:6,DefenceMult:1.16,MagicAdd:6,MagicMult:1.16,vi:1,im:'Kodai potion (4)'},
    'Kodai potion':{DefenceAdd:5,DefenceMult:1.13,MagicAdd:5,MagicMult:1.13,vi:1,im:'Kodai potion (2)'},
    'Kodai potion (-)':{DefenceAdd:4,DefenceMult:1.1,MagicAdd:4,MagicMult:1.1,vi:1,im:'Kodai potion (1)'},
    'Magic essence':{MagicAdd:3,MagicMult:1,vi:1,im:'Magic essence(4)'},
    'Magic potion':{MagicAdd:4,MagicMult:1,vi:1,im:'Magic potion(4)'},
    'Overload':{AttackAdd:5,AttackMult:1.13,StrengthAdd:5,StrengthMult:1.13,DefenceAdd:5,DefenceMult:1.13,RangedAdd:5,RangedMult:1.13,MagicAdd:5,MagicMult:1.13,vi:1,im:'Overload (2) (Chambers of Xeric)'},
    'Overload (-)':{AttackAdd:4,AttackMult:1.1,StrengthAdd:4,StrengthMult:1.1,DefenceAdd:4,DefenceMult:1.1,RangedAdd:4,RangedMult:1.1,MagicAdd:4,MagicMult:1.1,vi:1,im:'Overload (1) (Chambers of Xeric)'},
    'Strength potion':{StrengthAdd:3,StrengthMult:1.1,vi:1,im:'Strength potion(4)'},
    'Super attack':{AttackAdd:5,AttackMult:1.15,vi:1,im:'Super attack(4)'},
    'Super defence':{DefenceAdd:5,DefenceMult:1.15,vi:1,im:'Super defence(4)'},
    'Super restore':{AttackAdd:0,AttackMult:1,StrengthAdd:0,StrengthMult:1,DefenceAdd:0,DefenceMult:1,RangedAdd:0,RangedMult:1,MagicAdd:0,MagicMult:1,vi:1,im:'Super restore(4)'},
    'Super strength':{StrengthAdd:5,StrengthMult:1.15,vi:1,im:'Super strength(4)'},
    'Twisted potion (+)':{DefenceAdd:6,DefenceMult:1.16,RangedAdd:6,RangedMult:1.16,vi:1,im:'Twisted potion (4)'},
    'Twisted potion':{DefenceAdd:5,DefenceMult:1.13,RangedAdd:5,RangedMult:1.13,vi:1,im:'Twisted potion (2)'},
    'Twisted potion (-)':{DefenceAdd:4,DefenceMult:1.1,RangedAdd:4,RangedMult:1.1,vi:1,im:'Twisted potion (1)'},
    'Dragon pickaxe':{MiningAdd:3,MiningMult:1,vi:1,im:'Dragon pickaxe'},
    'Garden pie':{FarmingAdd:3,FarmingMult:1,vi:1,im:'Garden pie'},
};

// 'Blood Barrage': {attackSpeed:5, maxHit:29, fireSpell:false, vi:1, im:'Blood Barrage icon'},
equipment.spell = {
    'None': {aS:5, mh:0, vi:0, im:'Spellbook'},
    'Blood Barrage': {aS:5, mh:29, vi:1, im:'Blood Barrage icon'},
    'Blood Blitz': {aS:5, mh:25, vi:1, im:'Blood Blitz icon'},
    'Blood Burst': {aS:5, mh:21, vi:1, im:'Blood Burst icon'},
    'Blood Rush': {aS:5, mh:15, vi:1, im:'Blood Rush icon'},
    'Claws of Guthix': {aS:5, mh:20, smoke:1, vi:1, im:'Claws of Guthix icon'},
    'Claws of Guthix (charged)': {aS:5, mh:30, smoke:1, vi:1, im:'Claws of Guthix icon'},
    'Crumble Undead': {aS:5, mh:15, smoke:1, vi:1, im:'Crumble Undead icon'},
    'Earth Blast': {aS:5, mh:15, smoke:1, vi:1, im:'Earth Blast icon'},
    'Earth Bolt': {aS:5, mh:11, smoke:1, bolt:1, vi:1, im:'Earth Bolt icon'},
    'Earth Strike': {aS:5, mh:6, smoke:1, vi:1, im:'Earth Strike icon'},
    'Earth Surge': {aS:5, mh:23, smoke:1, vi:1, im:'Earth Surge icon'},
    'Earth Wave': {aS:5, mh:19, smoke:1, vi:1, im:'Earth Wave icon'},
    'Fire Blast': {aS:5, mh:16, fire:1, smoke:1, vi:1, im:'Fire Blast icon'},
    'Fire Bolt': {aS:5, mh:12, fire:1, bolt:1, smoke:1, vi:1, im:'Fire Bolt icon'},
    'Fire Strike': {aS:5, mh:8, fire:1, smoke:1, vi:1, im:'Fire Strike icon'},
    'Fire Surge': {aS:5, mh:24, fire:1, smoke:1, vi:1, im:'Fire Surge icon'},
    'Fire Wave': {aS:5, mh:20, fire:1, smoke:1, vi:1, im:'Fire Wave icon'},
    'Flames of Zamorak': {aS:5, mh:20, smoke:1, vi:1, im:'Flames of Zamorak icon'},
    'Flames of Zamorak (charged)': {aS:5, mh:30, smoke:1, vi:1, im:'Flames of Zamorak icon'},
    'Iban Blast': {aS:5, mh:25, smoke:1, vi:1, im:'Iban Blast icon'},
    'Ice Barrage': {aS:5, mh:30, vi:1, im:'Ice Barrage icon'},
    'Ice Blitz': {aS:5, mh:26, vi:1, im:'Ice Blitz icon'},
    'Ice Burst': {aS:5, mh:22, vi:1, im:'Ice Burst icon'},
    'Ice Rush': {aS:5, mh:16, vi:1, im:'Ice Rush icon'},
    'Magic Dart': {aS:5, mh:-1, smoke:1, vi:1, im:'Magic Dart icon'},
    'Saradomin Strike': {aS:5, mh:20, smoke:1, vi:1, im:'Saradomin Strike icon'},
    'Saradomin Strike (charged)': {aS:5, mh:30, smoke:1, vi:1, im:'Saradomin Strike icon'},
    'Shadow Barrage': {aS:5, mh:28, vi:1, im:'Shadow Barrage icon'},
    'Shadow Blitz': {aS:5, mh:24, vi:1, im:'Shadow Blitz icon'},
    'Shadow Burst': {aS:5, mh:18, vi:1, im:'Shadow Burst icon'},
    'Shadow Rush': {aS:5, mh:14, vi:1, im:'Shadow Rush icon'},
    'Smoke Barrage': {aS:5, mh:27, vi:1, im:'Smoke Barrage icon'},
    'Smoke Blitz': {aS:5, mh:23, vi:1, im:'Smoke Blitz icon'},
    'Smoke Burst': {aS:5, mh:17, vi:1, im:'Smoke Burst icon'},
    'Smoke Rush': {aS:5, mh:13, vi:1, im:'Smoke Rush icon'},
    'Water Blast': {aS:5, mh:14, smoke:1, vi:1, im:'Water Blast icon'},
    'Water Bolt': {aS:5, mh:10, smoke:1, bolt:1, vi:1, im:'Water Bolt icon'},
    'Water Strike': {aS:5, mh:4, smoke:1, vi:1, im:'Water Strike icon'},
    'Water Surge': {aS:5, mh:22, smoke:1, vi:1, im:'Water Surge icon'},
    'Water Wave': {aS:5, mh:18, smoke:1, vi:1, im:'Water Wave icon'},
    'Wind Blast': {aS:5, mh:13, smoke:1, vi:1, im:'Wind Blast icon'},
    'Wind Bolt': {aS:5, mh:9, smoke:1, bolt:1, vi:1, im:'Wind Bolt icon'},
    'Wind Strike': {aS:5, mh:2, smoke:1, vi:1, im:'Wind Strike icon'},
    'Wind Surge': {aS:5, mh:21, smoke:1, vi:1, im:'Wind Surge icon'},
    'Wind Wave': {aS:5, mh:17, smoke:1, vi:1, im:'Wind Wave icon'}
};

// 'axe': ['Slash - Accurate','Slash - Aggressive','Crush - Aggressive','Slash - Defensive','Magic - Spell'],
equipment.combatStyleLoadout = {
    'axe': ['Slash - Accurate','Slash - Aggressive','Crush - Aggressive','Slash - Defensive','Magic - Spell'],
    'banner': ['Stab - Accurate','Slash - Aggressive','Crush - Shared','Stab - Defensive','Magic - Spell'],
    'blaster': ['Magic - Spell'],
    'bludgeon': ['Crush - Aggressive','Crush - Aggressive','Crush - Aggressive','Magic - Spell'],
    'blunt': ['Crush - Accurate','Crush - Aggressive','Crush - Defensive','Magic - Spell'],
    'bow': ['Ranged - Accurate','Ranged - Rapid','Ranged - Longrange','Magic - Spell'],
    'bulwark': ['Crush - Accurate','Block - N/A'],
    'claws': ['Slash - Accurate','Slash - Aggressive','Stab - Shared','Slash - Defensive','Magic - Spell'],
    'crossbow': ['Ranged - Accurate','Ranged - Rapid','Ranged - Longrange','Magic - Spell'],
    'flamer': ['Slash - Aggressive','Ranged - Rapid','Magic - Accurate','Magic - Spell'],
    'grenade': ['Ranged - Accurate','Ranged - Rapid','Ranged - Longrange','Magic - Spell'],
    'gun': ['Crush - Aggressive','Magic - Spell'],
    'hacksword': ['Slash - Accurate','Slash - Aggressive','Stab - Shared','Slash - Defensive','Magic - Spell'],
    'heavysword': ['Slash - Accurate','Slash - Aggressive','Crush - Aggressive','Slash - Defensive','Magic - Spell'],
    'pickaxe': ['Stab - Accurate','Stab - Aggressive','Crush - Aggressive','Stab - Defensive','Magic - Spell'],
    'polearm': ['Stab - Shared','Slash - Aggressive','Stab - Defensive','Magic - Spell'],
    'polestaff': ['Crush - Accurate','Crush - Aggressive','Crush - Defensive','Magic - Spell'],
    'scythe': ['Slash - Accurate','Slash - Aggressive','Crush - Aggressive','Slash - Defensive','Magic - Spell'],
    'spear': ['Stab - Shared','Slash - Shared','Crush - Shared','Stab - Defensive','Magic - Spell'],
    'spiked': ['Crush - Accurate','Crush - Aggressive','Stab - Shared','Crush - Defensive','Magic - Spell'],
    'stabsword': ['Stab - Accurate','Stab - Aggressive','Slash - Aggressive','Stab - Defensive','Magic - Spell'],
    'staff': ['Crush - Accurate','Crush - Aggressive','Crush - Defensive','Magic - Spell','Magic - Defensive Spell'],
    'staff bladed': ['Stab - Accurate','Slash - Aggressive','Crush - Defensive','Magic - Spell','Magic - Defensive Spell'],
    'staff selfpowering': ['Magic - Accurate','Magic - Longrange','Magic - Spell'],
    'thrown': ['Ranged - Accurate','Ranged - Rapid','Ranged - Longrange','Magic - Spell'],
    'unarmed': ['Crush - Accurate','Crush - Aggressive','Crush - Defensive','Magic - Spell'],
    'whip': ['Slash - Accurate','Slash - Shared','Slash - Defensive','Magic - Spell'],
};

equipment.combatStyle = {
    'None': {im: 'Combat icon'}
};

//returns item name from id
equipment.lookupItemID = function(id, slot){
    for(var key in equipment[slot]){
        if(equipment[slot][key].id.includes(id)){
            return key;
        }
    }
    return false;
};

//returns [item object (obj), success (bool)] from item name:
equipment.getItem = function(name, slot){
    if(slot == 'spell' || slot == 'combatStyle') {
        name = utils.titleCase(name);
    } else {
        name = utils.itemCase(name);
    }
    if(name in equipment[slot]) {
        return [equipment[slot][name], true];
    } else {
        return [equipment[slot]['None'], false];
    }
};

//Override this prototype to update the function from osrs wiki's 2018-04-17T22:23:58Z version to the current version 2019-12-12T00:27:42Z
OO.ui.SelectWidget.prototype.getItemMatcher = function ( query, mode ) {
    var normalizeForMatching = this.constructor.static.normalizeForMatching,
        normalizedQuery = normalizeForMatching( query );
    mode = this.filterMode || mode; //Check filtermode since the old version of the widgets don't pass on filtermode
    if ( mode === true ) { // Support deprecated exact=true argument
        mode = 'exact';
    }
    return function ( item ) {
        var matchText = normalizeForMatching( item.getMatchText() );
        if ( normalizedQuery === '' ) {
            return mode !== 'exact'; // Empty string matches all, except if we are in 'exact' mode, where it doesn't match at all
        }
        switch ( mode ) {
        case 'exact':
            return matchText === normalizedQuery;
        case 'substring':
            return matchText.indexOf( normalizedQuery ) !== -1;
        default: //prefix
            return matchText.indexOf( normalizedQuery ) === 0;
        }
    };
};
OO.ui.SelectWidget.static.normalizeForMatching = function ( text ) {
    var normalized = text.trim().replace( /\s+/, ' ' ).toLowerCase();
    if ( normalized.normalize ) {
        normalized = normalized.normalize();
    }
    return normalized;
};

var updateEquipmentBonus = function(){
    var bonuses = ['at', 'al', 'ac', 'am', 'ar', 'dt', 'dl', 'dc', 'dm', 'dr', 'bs', 'br', 'bm', 'pr'];
    var slots = ['head','cape','neck','ammo','torso','legs','gloves','boots','ring','weapon',/*'combatStyle',*/'shield','blowpipe'/*,'spell'*/];
    bonuses.forEach(function(bonus) {
        var bonusvalue = 0;
        slots.forEach(function(slot) {
            if(typeof settings.loadout.slot.slotItem[slot][bonus] !== 'undefined') {
                bonusvalue += settings.loadout.slot.slotItem[slot][bonus];
            }
        });
        settings.loadout.slot.equipmentBonus[bonus] = bonusvalue;
        dpsForm.form.equipmentBonus.menu[bonus].setValue((bonusvalue<0?'':'+')+bonusvalue+(bonus==='bm'?'%':'')); //with sign
    });
};

var updateVisibleLevels = function(skills){
    skills.forEach(function(skill){
        settings.loadout.playerLevel.visible[skill] = settings.loadout.playerLevel.current[skill];
        settings.loadout.slot.potions.forEach(function(potion){
            var potionBoost;
            if(skill+'Add' in equipment.potions[potion]) {
                if(potion === 'Dragon battleaxe'){
                    potionBoost = Math.floor((Math.floor(settings.loadout.playerLevel.current.Magic/10)+Math.floor(settings.loadout.playerLevel.current.Defence/10)+Math.floor(settings.loadout.playerLevel.current.Ranged/10)+Math.floor(settings.loadout.playerLevel.current.Attack/10))/4)+10+settings.loadout.playerLevel.current.Strength;
                } else {
                    potionBoost = settings.loadout.playerLevel.current[skill]*equipment.potions[potion][skill+'Mult'] + equipment.potions[potion][skill+'Add'];
                }
                settings.loadout.playerLevel.visible[skill] = Math.max(settings.loadout.playerLevel.visible[skill], potionBoost);
            }
        });
        settings.loadout.playerLevel.visible[skill] = Math.floor(settings.loadout.playerLevel.visible[skill]);
        dpsForm.form.playerLevel.visible[skill].setValue(settings.loadout.playerLevel.visible[skill]);
    });
};

var setMonster = function(name){
    var saveSkill = function(setname,getname,defaultvalue,defaultsetting) {
        var val;
        if(typeof mobj[getname] === 'undefined') {
            val = defaultsetting;
        } else {
            val = mobj[getname];
        }
        settings.loadout.monster.current[setname] = val;
        settings.loadout.monster.visible[setname] = val;
        dpsForm.form.monster.menu['current'+setname].setValue(val);
        dpsForm.form.monster.menu['visible'+setname].setValue(val);
    };
    var saveVar = function(setname,getname,defaultvalue,defaultsetting,addplus,addperc) {
        var val;
        if(typeof mobj[getname] === 'undefined') {
            val = defaultsetting;
        } else {
            val = mobj[getname];
        }
        settings.loadout.monster[setname] = val;
        val = (addplus?(val<0?'':'+'):'')+val+(addperc?'%':'');
        dpsForm.form.monster.menu[setname].setValue(val);
        return;
    };
    
    dpsForm.monsterUnlockedToggleButton = false; //prevent recursion
    dpsForm.form.monster.levelToggle.setValue(false);

    var mobj;
    if(name in monster) {
        mobj = monster[name];
        saveSkill('Hitpoints','Hi','',1);
        saveSkill('Attack','At','',1);
        saveSkill('Strength','St','',1);
        saveSkill('Defence','De','',1);
        saveSkill('Ranged','Ra','',1);
        saveSkill('Magic','Ma','',1);
        saveVar('attackSpeed','aS','',4);
        saveVar('maxHit','mh','',0,false,false);
        saveVar('astab','at','',0,true,false);
        saveVar('aslash','al','',0,true,false);
        saveVar('acrush','ac','',0,true,false);
        saveVar('amagic','am','',0,true,false);
        saveVar('arange','ar','',0,true,false);
        saveVar('dstab','dt','',0,true,false);
        saveVar('dslash','dl','',0,true,false);
        saveVar('dcrush','dc','',0,true,false);
        saveVar('dmagic','dm','',0,true,false);
        saveVar('drange','dr','',0,true,false);
        saveVar('abns','ba','',0,true,false);
        saveVar('str','bs','',0,true,false);
        saveVar('rstr','br','',0,true,false);
        saveVar('mdmg','bm','',0,true,true);
        saveVar('xpBonus','xp',0,0,false,true);
        saveVar('immunePoison','ip',0,0,false,false);
        saveVar('immuneVenom','iv',0,0,false,false);
        saveVar('size','si',1,1,false,false);
        
        //combatType
        if(typeof mobj.cT === 'undefined') {
            settings.loadout.monster.combatType = [];
            dpsForm.form.monster.menu.combatType.clearItems();
        } else {
            dpsForm.form.monster.menu.combatType.clearItems();
            settings.loadout.monster.combatType = mobj.cT;
            settings.loadout.monster.combatType.forEach(function(item) {
                dpsForm.form.monster.menu.combatType.addTag(item);
            });
        }
        
        //attackStyle
        if(typeof mobj.aC === 'undefined') {
            settings.loadout.monster.attackStyle = 'Melee';
            dpsForm.form.monster.menu.attackStyle.setValue('Melee');
        } else {
            var styleChoices = ['Melee','Stab','Slash','Crush','Ranged','Magic','Magical melee','Magical ranged','Ranged magic'];
            styleChoices.forEach(function(choice) {
                if(mobj.aC.indexOf(choice,0) == 0) {
                    settings.loadout.monster.attackStyle = choice;
                    dpsForm.form.monster.menu.attackStyle.setValue(choice);
                }
            });
        }

        dpsForm.monsterUnlockedToggleButton = true;
        return mobj;
    } else {
        mobj = monster['None'];
        dpsForm.monsterUnlockedToggleButton = true;
        return mobj;
    }
};

var utils = {
    //Capitalize strings in the style of runescape items
    itemCase: function(val) {
        return val.charAt(0).toUpperCase() + val.slice(1).toLowerCase();
    },
    //Capitalize first letter of every word except of and (charged)
    titleCase: function(val) {
        val = val.replace(/\w+/g, function(word) {
            return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
        });
        val = val.replace(/ Of /g,' of '); //doesn't work for " Of Of ", or start/end of sentence but whatevs
        val = val.replace(/\(Charged\)/g,'(charged)');
        return val;
    },
    //only accepts digits and nothing else (positive ints only)
    isPositiveInt: function(val){
        return /^\d+$/.test(val);
    },
    //parses num, but returns def if NaN
    parseIntDefault: function(num,def){
        num = parseInt(num,10);
        return Number.isNaN(num)?def:num;
    },
    //parses num, but returns def if NaN
    parseFloatDefault: function(num,def){
        num = parseFloat(num);
        return Number.isNaN(num)?def:num;
    },
};

// https://runescape.wiki/w/Application_programming_interface#Old_School_Hiscores
var api = {
    dumpOrder: [null, 'Attack', 'Defence', 'Strength', 'Hitpoints', 'Ranged', 'Prayer', 'Magic', null, null, null, null, null, null, null, 'Mining', null, null, null, null, 'Farming'],
    lookup: function(rsn){
        var throwError = function(error) {
            dpsForm.form.playerLevel.rsnInputLayout.setErrors([error]);
            dpsForm.form.playerLevel.rsnInput.setFlags('invalid');
        };
        if(rsn.length > 0 && rsn.length < 13 && rsn.match(/^[a-zA-Z0-9 _]+$/) !== null) {
            dpsForm.form.playerLevel.rsnButton.setActive(true);
            dpsForm.form.playerLevel.rsnInputLayout.setErrors([]);
            dpsForm.form.playerLevel.rsnInput.setFlags({invalid:false});
            $.ajax({
                type: 'GET',
                url: '/cors/m=hiscore_oldschool/index_lite.ws?player='+rsn,
                dataType: 'text',
                error: function(jqXHR, textStatus, errorThrown) {
                    console.warn('Hiscores API call for '+rsn+'failed: ' + textStatus + '. ' + errorThrown);
                    if(errorThrown === 'Not Found') {
                        throwError('Display name not found! Please confirm the spelling!');
                    } else {
                        throwError('Failed to lookup this display name! Please check your internet connection or try again later.');
                    }
                    dpsForm.form.playerLevel.rsnButton.setActive(false);
                },
                success: function(data/*, textStatus, jqXHR*/) {
                    try {
                        data = data.split('\n');
                        for(var i = 0; i < api.dumpOrder.length; i++) {
                            if(api.dumpOrder[i]) {
                                var lvl_s = data[i].split(',', 2)[1];
                                var lvl_i = utils.parseIntDefault(lvl_s,1);
                                var skill = api.dumpOrder[i];
                                if(api.dumpOrder[i] === 'Prayer'||api.dumpOrder[i]==='Hitpoints'){
                                    if(settings.loadout.playerLevel.visible[skill] === settings.loadout.playerLevel.current[skill]){
                                        dpsForm.form.playerLevel.visible[skill].setValue(lvl_s);
                                        settings.loadout.playerLevel.visible[skill] = lvl_i;
                                    }
                                }
                                dpsForm.form.playerLevel.current[skill].setValue(lvl_s);
                                settings.loadout.playerLevel.current[skill] = lvl_i;
                            }
                        }
                        settings.save('playerLevel');
                    } catch (err) {
                        console.error('Unable to parse hiscores api data! '+err.message);
                        throwError('Error processing hiscore data! Please report this issue on the Wiki\'s Discord at https://discord.gg/runescapewiki!');
                    }
                    dpsForm.form.playerLevel.rsnButton.setActive(false);
                }
            });
        } else {
            throwError('Please enter valid a display name!');
        }
    },
};

var calc = {
    roll: {
        Roller: function(probability,roll,special){
            var _this = {
                'Prob':probability, //Percentage chance of this roll occurring
                'Roll':roll, //Value of the roll (e.g. attack roll or max hit)
                'Spec':special, //Special parameter:
                //  1 = 100% accurate
                //  2 = Dinh's
                //  4 = invalid parameters (e.g. Invalid Magic Dart)
                //  8 = Karil proc (2nd hitsplat with 50% damage)
                //  16 = Scythe hit 2
                //  32 = Scythe hit 3
                //  
            };
            return _this;
        },
        ApplyFunc: function(arr,args,func){
            console.log(args)
            for(var i=0; i<arr.length; i++){

                arr[i] = func(arr[i],args);
            }
            return arr;
        },
        ApplyMult: function(arr,mult){
            if(mult === 1){
                return arr;
            }
            for(var i=0; i<arr.length; i++){
                arr[i].Roll = Math.floor(arr[i].Roll*mult);
            }
            return arr;
        },
        ApplyAdd: function(arr,add){
            if(add === 0){
                return arr;
            }
            for(var i=0; i<arr.length; i++){
                arr[i].Roll = Math.floor(arr[i].Roll+add);
            }
            return arr;
        },
        ApplySplitMult: function(arr,splitarr){
            var newarr = [];
            splitarr.forEach(function(splitar){
                arr.forEach(function(ar){
                    newarr.push(calc.roll.Roller(ar.Prob*splitar.Prob,Math.floor(ar.Roll*splitar.Roll),ar.Spec|splitar.Spec));
                }); 
            });
            return newarr;
        },
        ApplySplitAdd: function(arr,splitarr){
            var newarr = [];
            splitarr.forEach(function(splitar){
                arr.forEach(function(ar){
                    newarr.push(calc.roll.Roller(ar.Prob*splitar.Prob,Math.floor(ar.Roll+splitar.Roll),ar.Spec|splitar.Spec));
                }); 
            });
            return newarr;
        },
    },
    check: {
        //Returns elite if wearing elite void, normal if wearing normal void, and 1 if not wearing void.
        MVoid: function(loadout, helmet, normal, elite){
            if(loadout.slot.label.head === helmet && loadout.slot.label.gloves === 'Void knight gloves' && (loadout.slot.label.torso === 'Elite void top' || loadout.slot.label.torso === 'Void knight top') && (loadout.slot.label.legs === 'Elite void robe' || loadout.slot.label.legs === 'Void knight robe')){
                if(loadout.slot.label.torso === 'Elite void top' && loadout.slot.label.legs === 'Elite void robe'){
                    return elite;
                }
                return normal;
            }
            return 1;
        },
        //Returns the multiplier if wearing salve/black mask. imbuedonly = true if the item needs to be imbued.
        //mask/salve/salve_e contain the multipliers for each case.
        MMaskSalve: function(loadout, imbuedonly, mask, salve, salve_e){
            var helmets;
            var amulets;
            var amulets_e;
            if(imbuedonly){
                helmets = ['Black mask (i)','Slayer helmet (i)','Black slayer helmet (i)','Green slayer helmet (i)','Red slayer helmet (i)','Purple slayer helmet (i)','Turquoise slayer helmet (i)','Hydra slayer helmet (i)','Twisted slayer helmet (i)'];
                amulets = ['Salve amulet(i)'];
                amulets_e = ['Salve amulet(ei)'];
            } else {
                helmets = ['Black mask','Black mask (i)','Slayer helmet','Slayer helmet (i)','Black slayer helmet','Black slayer helmet (i)','Green slayer helmet','Green slayer helmet (i)','Red slayer helmet','Red slayer helmet (i)','Purple slayer helmet','Purple slayer helmet (i)','Turquoise slayer helmet','Turquoise slayer helmet (i)','Hydra slayer helmet','Hydra slayer helmet (i)','Twisted slayer helmet','Twisted slayer helmet (i)'];
                amulets = ['Salve amulet','Salve amulet(i)'];
                amulets_e = ['Salve amulet (e)','Salve amulet(ei)'];
            }
            if(amulets_e.includes(loadout.slot.label.neck)){
                return salve_e;
            }
            if(amulets.includes(loadout.slot.label.neck)){
                return salve;
            }
            if(helmets.includes(loadout.slot.label.head) && loadout.monster.combatType.includes('slayer')){
                return mask;
            }
            return 1;
        },
        MSmokeStaff: function(loadout){
            if((loadout.slot.label.weapon === 'Smoke battlestaff' || loadout.slot.label.weapon === 'Mystic smoke staff') && loadout.slot.label.spell in equipment.spell && equipment.spell[loadout.slot.label.spell].smoke){
                return 1.1;
            }
            return 1;
        },
        MLightsword: function(loadout,darklight,silverlight){
            // TODO: Darklight, Silverlight, Silverlight (dyed)
            if(loadout.monster.combatType.includes('demon')){
                if(loadout.slot.label.weapon === 'Arclight'){
                    return 1.7;
                }
                if(loadout.slot.label.weapon === 'Darklight'){
                    return darklight;
                }
                if(loadout.slot.label.weapon === 'Silverlight' || loadout.slot.label.weapon === 'Silverlight (dyed)'){
                    return silverlight;
                }
                //Darklight
                //Silverlight
                //Silverlight (dyed)
            }
            return 1;
        },
        MLeafyBaxe: function(loadout){
            if(loadout.monster.combatType.includes('leafy')){
                if(loadout.slot.label.weapon === 'Leaf-bladed battleaxe'){
                    return 1.175;
                }
            }
            return 1;
        },
        MDhcb: function(loadout){
            if(loadout.monster.combatType.includes('dragon')){
                if(loadout.slot.label.weapon === 'Dragon hunter crossbow'){
                    return 1.3;
                }
            }
            return 1;
        },
        MDhl: function(loadout){
            if(loadout.monster.combatType.includes('dragon')){
                if(loadout.slot.label.weapon === 'Dragon hunter lance'){
                    return 1.2;
                }
            }
            return 1;
        },
        MHolyWater: function(loadout){
            if(loadout.monster.combatType.includes('demon')){
                if(loadout.slot.label.weapon === 'Holy water'){
                    return 1.6;
                }
            }
            return 1;
        },
        MWildyWeap: function(loadout, weapon, bonus){
            if(loadout.slot.wilderness && (loadout.slot.label.weapon === weapon || loadout.slot.label.weapon === weapon + ' (u)')){
                return bonus;
            }
            return 1;
        },
        MTbow: function(loadout,modifier){
            if(loadout.slot.label.weapon === 'Twisted bow'){
                var calc;
                if(modifier){ //maxhit modifier
                    calc = 250 + 100*loadout.monster.combatType.includes('raids');
                    calc = Math.min(calc,Math.max(loadout.monster.Magic,loadout.monster.am));
                    calc = Math.min(250,250+Math.trunc((10*3*calc/10-14)/100)-Math.trunc(Math.pow(3*calc/10-140,2)/100))/100;
                    return calc;   
                } else { //accuracy modifier
                    calc = 250 + 100*loadout.monster.combatType.includes('raids');
                    calc = Math.min(calc,Math.max(loadout.monster.Magic,loadout.monster.am));
                    calc = Math.min(140,140+Math.trunc((10*3*calc/10-10)/100)-Math.trunc(Math.pow(3*calc/10-100,2)/100))/100;
                    return calc;
                }
            }
            return 1;
        },
        MObbyArmour: function(loadout){
            //TODO does staff count as melee?
            if(loadout.slot.label.head === 'Obsidian helmet' && loadout.slot.label.torso === 'Obsidian platebody' && loadout.slot.label.legs === 'Obsidian platelegs' && (loadout.slot.label.weapon === 'Tzhaar-ket-om' || loadout.slot.label.weapon === 'Tzhaar-ket-om (t)' || loadout.slot.label.weapon === 'Tzhaar-ket-em' || loadout.slot.label.weapon === 'Toktz-xil-ak' || loadout.slot.label.weapon === 'Toktz-mej-tal' || loadout.slot.label.weapon === 'Toktz-xil-ek')){
                return 1.1;
            }
            return 1;
        },
        MObbyAmmy: function(loadout){
            //TODO does staff count as melee?
            if((loadout.slot.label.neck === 'Berserker necklace' || loadout.slot.label.neck === 'Berserker necklace (or)') && (loadout.slot.label.weapon === 'Tzhaar-ket-om' || loadout.slot.label.weapon === 'Tzhaar-ket-om (t)' || loadout.slot.label.weapon === 'Tzhaar-ket-em' || loadout.slot.label.weapon === 'Toktz-xil-ak' || loadout.slot.label.weapon === 'Toktz-mej-tal' || loadout.slot.label.weapon === 'Toktz-xil-ek')){
                return 1.2;
            }
            return 1;
        },
        MCrystalArmour: function(loadout,modifier){
            if(loadout.slot.label.weapon === 'Crystal bow'){
                var bonus = 0;
                if(loadout.slot.label.head === 'Crystal helm'){
                    bonus += 1;
                }
                if(loadout.slot.label.torso === 'Crystal body'){
                    bonus += 1;
                }
                if(loadout.slot.label.legs === 'Crystal legs'){
                    bonus += 1;
                }
                if(bonus === 3){
                    bonus = 5;
                }
                if(modifier){ //maxhit modifier
                    bonus = 1+bonus*0.03;
                } else { //accuracy modifier
                    bonus = 1+bonus*0.06;
                }
                return bonus;
            }
            return 1;
        },
        MChinchompa: function(loadout,stance2){
            if(loadout.slot.distance && (loadout.slot.label.weapon === 'Chinchompa' || loadout.slot.label.weapon === 'Red chinchompa' || loadout.slot.label.weapon === 'Black chinchompa')){
                if(stance2 === 'Accurate'){
                    if(loadout.slot.distance <= 3){
                        return 0;
                    } else if (loadout.slot.distance <= 6) {
                        return 0.25;
                    } else {
                        return 0.5;
                    }
                } else if(stance2 === 'Rapid'){
                    if(loadout.slot.distance <= 3){
                        return 0.25;
                    } else if (loadout.slot.distance <= 6) {
                        return 0;
                    } else {
                        return 0.25;
                    }
                } else if(stance2 === 'Longrange'){
                    if(loadout.slot.distance <= 3){
                        return 0.5;
                    } else if (loadout.slot.distance <= 6) {
                        return 0.25;
                    } else {
                        return 0;
                    }
                }
            }
            return 0;
        },
        HasGadder: function(loadout){
            return loadout.slot.label.weapon === 'Gadderhammer';
        },
        HasBrimstone: function(loadout){
            return loadout.slot.label.ring === 'Brimstone ring';
        },
        HasVestaLongsword: function(loadout){
            return loadout.slot.label.ring === 'Vesta\'s longsword';
        },
        HasKeris: function(loadout){
            return loadout.slot.label.weapon === 'Keris'||loadout.slot.label.weapon === 'Keris(p)'||loadout.slot.label.weapon === 'Keris(p+)'||loadout.slot.label.weapon === 'Keris(p++)';
        },
        HasVerac: function(loadout){
            if(loadout.slot.label.head === 'Verac\'s helm' && loadout.slot.label.torso === 'Verac\'s brassard' && loadout.slot.label.legs === 'Verac\'s plateskirt' && loadout.slot.label.weapon === 'Verac\'s flail'){
                if(loadout.slot.label.neck === 'Amulet of the damned'){
                    return 2;
                }
                return 1;
            }
            return 0;
        },
        HasAhrim: function(loadout){
            if(loadout.slot.label.head === 'Ahrim\'s hood' && loadout.slot.label.torso === 'Ahrim\'s robetop' && loadout.slot.label.legs === 'Ahrim\'s robeskirt' && loadout.slot.label.weapon === 'Ahrim\'s staff'){
                if(loadout.slot.label.neck === 'Amulet of the damned'){
                    return 2;
                }
                return 1;
            }
            return 0;
        },
        HasDharok: function(loadout){
            if(loadout.slot.label.head === 'Dharok\'s helm' && loadout.slot.label.torso === 'Dharok\'s platebody' && loadout.slot.label.legs === 'Dharok\'s platelegs' && loadout.slot.label.weapon === 'Dharok\'s greataxe'){
                if(loadout.slot.label.neck === 'Amulet of the damned'){
                    return 2;
                }
                return 1;
            }
            return 0;
        },
        HasKaril: function(loadout){
            if(loadout.slot.label.head === 'Karil\'s coif' && loadout.slot.label.torso === 'Karil\'s leathertop' && loadout.slot.label.legs === 'Karil\'s leatherskirt' && loadout.slot.label.weapon === 'Karil\'s crossbow'){
                if(loadout.slot.label.neck === 'Amulet of the damned'){
                    return 2;
                }
                return 1;
            }
            return 0;
        },
        MDharok: function(loadout){
            //TODO - add hitpoints
            if(calc.check.HasDharok(loadout)){
                if(true/*TODO*/){
                    return 1;
                }
                return 1 + Math.max(0,loadout.playerLevel.current.Hitpoints-loadout.playerLevel.visible.Hitpoints)*loadout.playerLevel.current.Hitpoints/10000;
            }
            return 1;
        },
        MGuardians: function(loadout){
            //TODO: Mining level
            //TODO: What happens if not using pickaxe?
            if(loadout.monster.name === 'Guardian (Chambers of Xeric, Challenge Mode)' || loadout.monster.name === 'Guardian (Chambers of Xeric)'){
                var weapon = Math.min(61,loadout.slot.slotItem.weapon.mL || 0);
                var level = Math.min(100,loadout.playerLevel.current.Mining || 1); //TODO
                return (50+level+weapon)/150;
            }
            return 1;
        },
        //TODO Check "ontask"
        MaxMagicDart: function(loadout){
            var staves = ['Slayer\'s staff','Slayer\'s staff (e)','Toxic staff of the dead','Toxic staff (uncharged)','Staff of the dead','Staff of balance','Staff of light'];
            if(staves.includes(loadout.slot.label.weapon)){
                if(loadout.slot.label.weapon === 'Slayer\'s staff (e)' && loadout.monster.combatType.includes('slayer')){
                    return Math.floor(13+loadout.playerLevel.visible.Magic/6);
                }
                return Math.floor(10+loadout.playerLevel.visible.Magic/10);
            }
            return 0;
        },
        MaxTrident: function(loadout){
            if(loadout.slot.label.weapon === 'Trident of the seas' || loadout.slot.label.weapon === 'Trident of the seas (e)'){
                return Math.max(1,Math.floor(loadout.playerLevel.visible.Magic/3 - 5));
            } else if(loadout.slot.label.weapon === 'Trident of the swamp' || loadout.slot.label.weapon === 'Trident of the swamp (e)' || loadout.slot.label.weapon === 'Uncharged toxic trident' || loadout.slot.label.weapon === 'Uncharged toxic trident (e)'){
                return Math.max(4,Math.floor(loadout.playerLevel.visible.Magic/3 - 2));
            } else if(loadout.slot.label.weapon === 'Sanguinesti staff' || loadout.slot.label.weapon === 'Sanguinesti staff (beta)'){
                return Math.max(5,Math.floor(loadout.playerLevel.visible.Magic/3 - 1));
            } else if(loadout.slot.label.weapon === 'Starter staff'){
                return 8;
            } else {
                return false;
            }
        },
        MaxSalamander: function(loadout){
            if(loadout.slot.label.weapon === 'Black salamander'){
                return Math.floor(0.5+loadout.playerLevel.visible.Magic*0.24375);
            }
            if(loadout.slot.label.weapon === 'Red salamander'){
                return Math.floor(0.5+loadout.playerLevel.visible.Magic*0.2203125);
            }
            if(loadout.slot.label.weapon === 'Orange salamander'){
                return Math.floor(0.5+loadout.playerLevel.visible.Magic*0.1921875);
            }
            if(loadout.slot.label.weapon === 'Swamp lizard'){
                return Math.floor(0.5+loadout.playerLevel.visible.Magic*0.1875);
            }
            return false;
        },
        MChaosGauntlet: function(loadout){
            if(loadout.slot.label.gloves === 'Chaos gauntlets' && loadout.slot.slotItem.spell.bolt){
                return 3;
            }
            return 0;
        },
        MTomeOfFire: function(loadout){
            if(loadout.slot.label.shield === 'Tome of fire' && loadout.slot.slotItem.spell.fire){
                return 1.5;
            }
            return 1;
        },
        MCastleWars: function(loadout){
            if(loadout.monster.combatType.includes('castle wars flagholder')){
                return 1.2;
            }
            return 1;
        },
    },

    naiveDPS: function(accarr,rollmin,rollmax,typeless,ticks){
        //TODO: Scythe
        var dps = 0;
        for(var i = 0; i < accarr.length; i++) {
            var hit;
            if(accarr[i].Spec & 8){
                hit = (rollmax[i].Roll + rollmin[i].Roll)/2 + Math.floor((rollmax[i].Roll + rollmin[i].Roll)/2)/2;
            } else {
                hit = (rollmax[i].Roll + rollmin[i].Roll)/2;
            }
            dps += accarr[i].Prob*accarr[i].Roll*hit+typeless;
        }
        return dps/ticks/0.6;
    },

    accuracy: function(aroll,droll){
        if(aroll>droll){
            return 1-(droll+2)/(2*aroll+2);
        }
        return aroll/(2*droll+2);
    },

    accuracies: function(arollers,drollers){
        var newarr = [];
        arollers.forEach(function(aroller){
            var roll;
            if(aroller.Spec & 1){ //guaranteed hit
                roll = 1;
            } else if (aroller.Spec & 6){ //Dinh's (2) or invalid (4)
                roll = 0;
            } else {
                roll = 0;
                drollers.forEach(function(droller){
                    roll += droller.Prob*calc.accuracy(aroller.Roll,droller.Roll);
                }); 
            }
            newarr.push(calc.roll.Roller(aroller.Prob,roll,aroller.Spec));
        });
        return newarr;
    },

    monsterDefence: function(loadout,style,spec){
        var roll;
        if(style === 'dmagic'){
            roll = [calc.roll.Roller(1,Math.floor((loadout.monster.visible.Magic+9)*(64+loadout.monster.dmagic)),0)];
            if(calc.check.HasBrimstone(loadout)){
                roll = calc.roll.ApplySplitMult(roll,[calc.roll.Roller(0.75,1,0),calc.roll.Roller(0.25,0.9,0)]);
            }
        } else {
            roll = [calc.roll.Roller(1,Math.floor((loadout.monster.visible.Defence+9)*(64+loadout.monster[style])),0)];
            if(spec && calc.check.HasVestaLongsword(loadout)){
                roll = calc.roll.ApplyMult(roll,0.25);
            }
        }
        return roll;

    },
    //Input: Loadout
    //Output: min roll, max roll, guaranteed typeless damage, attack tick duration
    playerMaxHit: function(loadout){
        //Todo: Ba level to min + max hit
        //Todo: Holy water
        //todo: Bolts
        //Todo: Clarify which dragons are fiery

        var res = loadout.slot.label.combatStyle.match(/^(.*) - (.*)$/);
        if(!(res && ['Stab','Slash','Crush','Ranged','Magic','Block'].includes(res[1]) && ['Accurate','Aggressive','Defensive','Shared','Rapid','Longrange','Spell','Defensive Spell'].includes(res[2]))){
            if(loadout.slot.combatStyleLoadout in equipment.combatStyleLoadout){
                res = equipment.combatStyleLoadout[loadout.slot.combatStyleLoadout][0].match(/^(.*) - (.*)$/);
            } else {
                res = 'Stab - Accurate'.match(/^(.*) - (.*)$/);
            }
        }
        var stance1 = res[1];
        var stance2 = res[2];

        var rollmax;
        var rollmin = [calc.roll.Roller(1,0,0)];
        var typeless = 0; //dmg even if miss
        var attackticks = loadout.slot.slotItem.weapon.aS;
        var args;
        var tomeoffire = 1;


        if(stance1 === 'Block'){
            rollmax = [calc.roll.Roller(1,0,2)];
        } else if(stance1 === 'Ranged'){
            rollmax = [calc.roll.Roller(1,loadout.playerLevel.visible.Ranged,0)];
            loadout.slot.prayers.forEach(function(prayer){
                rollmax = calc.roll.ApplyMult(rollmax,equipment.prayers[prayer].RangedStrengthBonus || 1);
            });
            rollmax = calc.roll.ApplyAdd(rollmax,8);
            if(stance2 === 'Accurate'){
                rollmax = calc.roll.ApplyAdd(rollmax,3);
            } else if(stance2 === 'Rapid'){
                attackticks -= 1;
            }
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MVoid(loadout,'Void ranger helm',1.1,1.125));
            rollmax[0].Roll = Math.floor(0.5+rollmax[0].Roll*(64+loadout.slot.equipmentBonus.br)/640); //cheating out of laziness
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MMaskSalve(loadout,true,1.15,1.15,1.2));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MDhcb(loadout));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MHolyWater(loadout));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MWildyWeap(loadout,'Craw\'s bow',1.5));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MTbow(loadout,true));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MCrystalArmour(loadout,true));
            if(calc.check.HasKaril(loadout)){
                rollmax = calc.roll.ApplySplitAdd(rollmax,[calc.roll.Roller(0.75,0,0),calc.roll.Roller(0.25,0,8)]);
                rollmin = calc.roll.ApplySplitAdd(rollmin,[calc.roll.Roller(0.75,0,0),calc.roll.Roller(0.25,0,0)]);
            }
        } else if(stance1 === 'Magic') {
            if(stance2 === 'Spell' || stance2 === 'Defensive Spell'){
                attackticks = loadout.slot.slotItem.spell.aS;
                if(loadout.slot.label.spell === 'Magic Dart'){
                    args = calc.check.MaxMagicDart(loadout);
                    rollmax = [calc.roll.Roller(1,args,args ? 0 : 4)];
                } else {
                    rollmax = [calc.roll.Roller(1,loadout.slot.slotItem.spell.mh,loadout.slot.slotItem.spell.mh ? 0 : 4)];
                    calc.roll.ApplyAdd(rollmax,calc.check.MChaosGauntlet(loadout));
                    tomeoffire = calc.check.MTomeOfFire(loadout);
                }
            } else {
                args = calc.check.MaxTrident(loadout);
                if(args === false){
                    args = calc.check.MaxSalamander(loadout);
                }
                if(args === false){
                    rollmax = [calc.roll.Roller(1,0,4)];
                } else {
                    rollmax = [calc.roll.Roller(1,args,0)];
                }
            }
            var mbns = loadout.slot.equipmentBonus.bm;
            var mask = 1;
            mbns += calc.check.MSmokeStaff(loadout);
            args = calc.check.MMaskSalve(loadout,true,0,1.15,1.2);
            if(args === 0){
                mask = 1.15;
            } else {
                args = args - 1;
            }
            mbns += args;
            mbns += calc.check.MWildyWeap(loadout,'Thammaron\'s sceptre',1.25)-1;
            mbns += calc.check.MVoid(loadout,'Void mage helm',1,1.025)-1;
            rollmax = calc.roll.ApplyMult(rollmax,mbns);
            rollmax = calc.roll.ApplyMult(rollmax,tomeoffire);
            rollmax = calc.roll.ApplyMult(rollmax,mask);
            if(calc.check.HasAhrim(loadout) === 2){
                rollmax = calc.roll.ApplySplitMult(rollmax,[calc.roll.Roller(0.75,1,0),calc.roll.Roller(0.25,1.3,0)]);
                rollmin = calc.roll.ApplySplitAdd(rollmin,[calc.roll.Roller(0.75,0,0),calc.roll.Roller(0.25,0,0)]);
            }
        } else {
            rollmax = [calc.roll.Roller(1,loadout.playerLevel.visible.Strength,0)];
            loadout.slot.prayers.forEach(function(prayer){
                rollmax = calc.roll.ApplyMult(rollmax,equipment.prayers[prayer].StrengthBonus || 1);
            });
            rollmax = calc.roll.ApplyAdd(rollmax,8);
            if(stance2 === 'Aggressive'){
                rollmax = calc.roll.ApplyAdd(rollmax,3);
            } else if(stance2 === 'Shared') {
                rollmax = calc.roll.ApplyAdd(rollmax,1);
            }
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MVoid(loadout,'Void melee helm',1.1,1.1));
            rollmax[0].Roll = Math.floor(0.5+rollmax[0].Roll*(64+loadout.slot.equipmentBonus.bs)/640); //cheating out of laziness
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MMaskSalve(loadout,true,7/6,7/6,1.2));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MLightsword(loadout,1.6,1.5/*???TODO???*/));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MLeafyBaxe(loadout));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MDhl(loadout));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MWildyWeap(loadout,'Viggora\'s chainmace',1.5));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MObbyArmour(loadout));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MObbyAmmy(loadout)); //which order with obsidian?
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MDharok(loadout));
            rollmax = calc.roll.ApplyMult(rollmax,calc.check.MGuardians(loadout));
            if(calc.check.HasGadder(loadout)){
                rollmax = calc.roll.ApplySplitMult(rollmax,[calc.roll.Roller(0.95,1.25,0),calc.roll.Roller(0.05,2,0)]);
                rollmin = calc.roll.ApplySplitAdd(rollmin,[calc.roll.Roller(0.95,0,0),calc.roll.Roller(0.05,0,0)]);
            }
            if(calc.check.HasKeris(loadout)){
                rollmax = calc.roll.ApplySplitMult(rollmax,[calc.roll.Roller(50/51,4/3,0),calc.roll.Roller(1/51,3,0)]);
                rollmin = calc.roll.ApplySplitAdd(rollmin,[calc.roll.Roller(50/51,0,0),calc.roll.Roller(1/51,0,0)]);
            }
            if(calc.check.HasVerac(loadout)){
                rollmax = calc.roll.ApplySplitAdd(rollmax,[calc.roll.Roller(0.75,0,0),calc.roll.Roller(0.25,1,1)]);
                rollmin = calc.roll.ApplySplitAdd(rollmin,[calc.roll.Roller(0.75,0,0),calc.roll.Roller(0.25,1,1)]);
            }
        }
        rollmax = calc.roll.ApplyMult(rollmax,calc.check.MCastleWars(loadout));
        //TODO: If ba, typeless += attack level

        return [rollmin,rollmax,typeless,attackticks];
    },
    playerAttack: function(loadout){
        //Todo:
        //  Raids parameter
        //  Wilderness parameter
        //  distance parameter
        //  Check if Twisted bow truncates 3*Magic/10
        //  Slayer ontask parameter
        //  Rune claw attack delay special attack
        //  Todo special attacks

        //Todo: bolt specials
        var res = loadout.slot.label.combatStyle.match(/^(.*) - (.*)$/);
        if(!(res && ['Stab','Slash','Crush','Ranged','Magic','Block'].includes(res[1]) && ['Accurate','Aggressive','Defensive','Shared','Rapid','Longrange','Spell','Defensive Spell'].includes(res[2]))){
            if(loadout.slot.combatStyleLoadout in equipment.combatStyleLoadout){
                res = equipment.combatStyleLoadout[loadout.slot.combatStyleLoadout][0].match(/^(.*) - (.*)$/);
            } else {
                res = 'Stab - Accurate'.match(/^(.*) - (.*)$/);
            }
        }
        var stance1 = res[1];
        var stance2 = res[2];

        var rollacc;
        var args;
        var defencestyle;

        if(stance1 === 'Block'){
            return [[calc.roll.Roller(1,0,2)],''];
        } else if(stance1 === 'Ranged'){
            defencestyle = 'drange';
            rollacc = [calc.roll.Roller(1,loadout.playerLevel.visible.Ranged,0)];
            loadout.slot.prayers.forEach(function(prayer){
                rollacc = calc.roll.ApplyMult(rollacc,equipment.prayers[prayer].RangedAttackBonus || 1);
            });
            rollacc = calc.roll.ApplyAdd(rollacc,8);
            if(stance2 === 'Accurate'){
                rollacc = calc.roll.ApplyAdd(rollacc,3);
            }
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MVoid(loadout,'Void ranger helm',1.1,1.1));
            rollacc = calc.roll.ApplyMult(rollacc,64+loadout.slot.equipmentBonus.ar);
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MMaskSalve(loadout,true,1.15,1.15,1.2));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MDhcb(loadout));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MWildyWeap(loadout,'Craw\'s bow',1.5));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MTbow(loadout,false));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MCrystalArmour(loadout,false));
            args = calc.check.MChinchompa(loadout,stance2);
            rollacc = calc.roll.ApplyFunc(rollacc,args,function(rollaccer_i,chin){
                rollaccer_i.Roll = rollaccer_i.Roll - Math.floor(rollaccer_i.Roll*chin);
                return rollaccer_i;
            });
            if(calc.check.HasKaril(loadout)){
                rollacc = calc.roll.ApplySplitAdd(rollacc,[calc.roll.Roller(0.75,0,0),calc.roll.Roller(0.25,0,8)]);
            }
        } else if(stance1 === 'Magic'){
            defencestyle = 'dmagic';
            rollacc = [calc.roll.Roller(1,loadout.playerLevel.visible.Magic,0)];
            loadout.slot.prayers.forEach(function(prayer){
                rollacc = calc.roll.ApplyMult(rollacc,equipment.prayers[prayer].MagicBonus || 1);
            });
            rollacc = calc.roll.ApplyAdd(rollacc,8);
            if(stance2 === 'Accurate'){
                rollacc = calc.roll.ApplyAdd(rollacc,3);
            }
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MVoid(loadout,'Void mage helm',1.45,1.45));
            rollacc = calc.roll.ApplyMult(rollacc,64+loadout.slot.equipmentBonus.am);
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MSmokeStaff(loadout));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MMaskSalve(loadout,true,1.15,1.15,1.2));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MWildyWeap(loadout,'Thammaron\'s sceptre',2));
            if(calc.check.HasAhrim(loadout) === 2){ //split to keep in line with max hit above
                rollacc = calc.roll.ApplySplitMult(rollacc,[calc.roll.Roller(0.75,1,0),calc.roll.Roller(0.25,1,0)]);
            }
        } else {
            rollacc = [calc.roll.Roller(1,loadout.playerLevel.visible.Attack,0)];
            loadout.slot.prayers.forEach(function(prayer){
                rollacc = calc.roll.ApplyMult(rollacc,equipment.prayers[prayer].AttackBonus || 1);
            });
            rollacc = calc.roll.ApplyAdd(rollacc,8);
            if(stance2 === 'Accurate'){
                rollacc = calc.roll.ApplyAdd(rollacc,3);
            } else if(stance2 === 'Shared') {
                rollacc = calc.roll.ApplyAdd(rollacc,1);
            }
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MVoid(loadout,'Void melee helm',1.1,1.1));
            if(stance1 === 'Stab'){
                defencestyle = 'dstab';
                rollacc = calc.roll.ApplyMult(rollacc,64+loadout.slot.equipmentBonus.at);
            } else if (stance1 === 'Slash'){
                defencestyle = 'dslash';
                rollacc = calc.roll.ApplyMult(rollacc,64+loadout.slot.equipmentBonus.al);
            } else {
                defencestyle = 'dcrush';
                rollacc = calc.roll.ApplyMult(rollacc,64+loadout.slot.equipmentBonus.ac);
            }
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MMaskSalve(loadout,true,7/6,7/6,1.2));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MLightsword(loadout,1,1));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MDhl(loadout));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MWildyWeap(loadout,'Viggora\'s chainmace',1.5));
            rollacc = calc.roll.ApplyMult(rollacc,calc.check.MObbyArmour(loadout));
            if(calc.check.HasGadder(loadout)){
                rollacc = calc.roll.ApplySplitMult(rollacc,[calc.roll.Roller(0.95,1,0),calc.roll.Roller(0.05,1,0)]);
            }
            if(calc.check.HasKeris(loadout)){
                rollacc = calc.roll.ApplySplitMult(rollacc,[calc.roll.Roller(50/51,1,0),calc.roll.Roller(1/51,1,0)]);
            }
            if(calc.check.HasVerac(loadout)){
                rollacc = calc.roll.ApplySplitMult(rollacc,[calc.roll.Roller(0.75,1,0),calc.roll.Roller(0.25,1,1)]);
            }
        }
        return [rollacc,defencestyle];
    },
};

// Users settings
var settings = {
    storedLoadouts: [],
    loadout: {},
    /*
    Object playerLevel
        string rsn

        Object current
            number Attack
            number Strength
            number Defence
            number Ranged
            number Magic
        
        Object visible
            number Attack
            number Strength
            number Defence
            number Ranged
            number Magic

        boolean forceLookup (force a hiscore lookup on page load, used when rsn is defined in query string)
    Object slot
        Object label
            string weapon
            string combatStyle
            string blowpipe
            string spell
            string head
            string cape
            string neck
            string ammo
            string torso
            string shield
            string legs
            string gloves
            string boots
            string ring
        
        array prayers
        array potions
        
        boolean visibleBlowpipe
        boolean visibleSpell
        boolean disabledShield
        string combatStyleLoadout (combat style options name e.g. 'unarmed')
        
        boolean wilderness TODO
        number distance TODO

        Object slotItem (equipment[slot].item for each slot)
            Object weapon
            Object combatStyle
            Object blowpipe
            Object spell
            Object head
            Object cape
            Object neck
            Object ammo
            Object torso
            Object shield
            Object legs
            Object gloves
            Object boots
            Object ring
            (there is no prayers or potions object)

        Object equipmentBonus
            number at
            number al
            number ac
            number am
            number ar
            number dt
            number dl
            number dc
            number dm
            number dr
            number bs
            number br
            number bm

            boolean manualLevel
    Object monster
        Object visible
            number Hitpoints
            number Attack
            number Strength
            number Defence
            number Ranged
            number Magic
        Object current
            number Hitpoints
            number Attack
            number Strength
            number Defence
            number Ranged
            number Magic
        string name
        array combatType
        string attackStyle
        number attackSpeed
        number maxHit
        number astab
        number aslash
        number acrush
        number amagic
        number arange
        number dstab
        number dslash
        number dcrush
        number dmagic
        number drange
        number abns
        number str
        number rstr
        number mdmg
        number xpBonus
        number immunePoison
        number immuneVenom

        
        boolean manualLevel
        */

    // Load from localStorage
    load: function(type) {
        if (!rswiki.hasLocalStorage()) {
            console.warn('Browser does not support localStorage');
            return;
        }

        switch (type) {
        case 'storedLoadouts':
            try {
                settings.storedLoadouts = JSON.parse(localStorage.getItem('rsw-dps-storedloadouts'));
            } catch (err) {
                settings.storedLoadouts = [];
                console.warn('Error loading storedLoadouts from localStorage');
            }
            if (settings.storedLoadouts === null) {
                settings.storedLoadouts = [];
            }
            console.log('Localstorage loaded - storedLoadouts');
            console.log(settings.storedLoadouts);
            return settings.storedLoadouts;

        case 'playerLevel':
            try {
                settings.loadout.playerLevel = JSON.parse(localStorage.getItem('rsw-dps-loadout-playerLevel'));
            } catch (err) {
                settings.loadout.playerLevel = {};
                console.warn('Error loading loadout from localStorage');
            }
            if (settings.loadout.playerLevel === null) {
                settings.loadout.playerLevel = {};
            }
            console.log('Localstorage loaded - loadout.playerLevel');
            console.log(settings.loadout.playerLevel);
            return settings.loadout.playerLevel;

        case 'slot':
            try {
                settings.loadout.slot = JSON.parse(localStorage.getItem('rsw-dps-loadout-slot'));
            } catch (err) {
                settings.loadout.slot = {};
                console.warn('Error loading loadout from localStorage');
            }
            if (settings.loadout.slot === null) {
                settings.loadout.slot = {};
            }
            console.log('Localstorage loaded - loadout.slot');
            console.log(settings.loadout.slot);
            return settings.loadout.slot;
        
        case 'monster':
            try {
                settings.loadout.monster = JSON.parse(localStorage.getItem('rsw-dps-loadout-monster'));
            } catch (err) {
                settings.loadout.monster = {};
                console.warn('Error loading loadout from localStorage');
            }
            if (settings.loadout.monster === null) {
                settings.loadout.monster = {};
            }
            console.log('Localstorage loaded - loadout.monster');
            console.log(settings.loadout.monster);
            return settings.loadout.monster;

        case 'loadout':
            settings.load('playerLevel');
            settings.load('slot');
            settings.load('monster');
            return true;
            
        case 'all':
            settings.load('storedLoadouts');
            settings.load('loadout');
            return true;

        default:
            console.error('Invalid localStorage type: '+type);
            return null;
        }
    },

    //Load from query string on page load
    loadQueryString: function() {
        var urlParams = {};
        var match;
        var pl  = /\+/g;
        var search = /([^&=]+)=?([^&]*)/g;
        var decode = function (s) { return decodeURIComponent(s.replace(pl, ' ')); };
        var query  = window.location.search.substring(1);
        while (match = search.exec(query)) { // eslint-disable-line no-cond-assign
            urlParams[decode(match[1])] = decode(match[2]);
        }
        
        var skill_iterable = ['Attack', 'Strength', 'Defence', 'Ranged', 'Magic'];
        skill_iterable.forEach(function(skill){
            if(skill in urlParams){
                settings.loadout.playerLevel.current[skill] = utils.parseIntDefault(urlParams[skill], 1);
                console.log('Loaded query string '+skill+' - '+settings.loadout.playerLevel.current[skill]);
            }
        });
        
        if('rsn' in urlParams){
            settings.loadout.playerLevel.rsn = urlParams.rsn;
            settings.loadout.playerLevel.forceLookup = true;
            console.log('Loaded query string rsn - ' +settings.loadout.playerLevel.rsn);
        }
        var slot_iterable = ['head','cape','neck','ammo','torso','legs','gloves','boots','ring','weapon','combatStyle','shield','blowpipe','spell']; //shield/blowpipe/spell/combatStyle after weapon
        slot_iterable.forEach(function(slot){
            if(slot in urlParams){
                if(slot == 'blowpipe' && !settings.loadout.slot.visibleBlowpipe) {
                    return;
                } else if(slot == 'shield' && settings.loadout.slot.disabledShield){
                    return;
                } else if(slot == 'spell' && !settings.loadout.slot.visibleSpell){
                    return;
                }
                var slot_id = urlParams[slot];
                if(utils.isPositiveInt(slot_id)){
                    var slot_name = equipment.lookupItemID(parseInt(slot_id), slot);
                    if(slot_name) {
                        slot_id = slot_name;
                    }
                }
                slot_id = utils.itemCase(slot_id);
                console.log('Loaded query string '+slot+' - ' +slot_id);
                settings.loadout.slot.label[slot] = slot_id;
                settings.loadout.slot.slotItem[slot] = equipment.getItem(slot_id, slot)[0];
                if(slot == 'weapon'){
                    settings.loadout.slot.disabledShield = settings.loadout.slot.slotItem[slot].sl == '2h';
                    if(settings.loadout.slot.disabledShield == true) {
                        settings.loadout.slot.label.shield = '';
                    }
                    settings.loadout.slot.visibleBlowpipe = slot_id == 'Toxic blowpipe';
                    if(settings.loadout.slot.visibleBlowpipe == false) {
                        settings.loadout.slot.label.blowpipe = '';
                    }
                    settings.loadout.slot.combatStyleLoadout = settings.loadout.slot.slotItem[slot].cS;
                } else if(slot == 'combatStyle') {
                    settings.loadout.slot.visibleSpell = slot_id == 'Magic - spell' || slot_id == 'Magic - defensive spell';
                    if(settings.loadout.slot.visibleSpell == false) {
                        settings.loadout.slot.label.spell = '';
                    }
                }
            }
        });
        settings.save('all');
    },
    
    // Save to localStorage
    save: function (type) {
        if (!rswiki.hasLocalStorage()) {
            console.warn('Browser does not support localStorage');
            return;
        }
        var ret;
        var stringified;
        var save;
        switch (type) {
        case 'storedLoadouts':
            stringified = JSON.stringify(settings.storedLoadouts);
            localStorage.setItem('rsw-dps-storedLoadouts', stringified);
            // Check for save
            try {
                ret = JSON.parse(localStorage.getItem('rsw-dps-storedLoadouts'));
            } catch (err) {
                console.warn('Error saving storedLoadouts to localStorage');
                return false;
            }
            console.log('Localstorage saved - storedLoadouts: ' + ret);
            return true;
            
        case 'playerLevel':
            stringified = JSON.stringify(settings.loadout.playerLevel);
            localStorage.setItem('rsw-dps-loadout-playerLevel', stringified);
            // Check for save
            try {
                ret = JSON.parse(localStorage.getItem('rsw-dps-loadout-playerLevel'));
            } catch (err) {
                console.warn('Error saving loadout.playerLevel to localStorage');
                return false;
            }
            console.log('Localstorage saved - loadout.playerLevel');
            return true;
            
        case 'slot':
            stringified = JSON.stringify(settings.loadout.slot);
            localStorage.setItem('rsw-dps-loadout-slot', stringified);
            // Check for save
            try {
                ret = JSON.parse(localStorage.getItem('rsw-dps-loadout-slot'));
            } catch (err) {
                console.warn('Error saving loadout.slot to localStorage');
                return false;
            }
            console.log('Localstorage saved - loadout.slot');
            return true;
            
        case 'monster':
            stringified = JSON.stringify(settings.loadout.monster);
            localStorage.setItem('rsw-dps-loadout-monster', stringified);
            // Check for save
            try {
                ret = JSON.parse(localStorage.getItem('rsw-dps-loadout-monster'));
            } catch (err) {
                console.warn('Error saving loadout.monster to localStorage');
                return false;
            }
            console.log('Localstorage saved - loadout.monster');
            return true;
            
        case 'loadout':
            save = settings.save('playerLevel');
            save = settings.save('slot') && save;
            save = settings.save('monster') && save;
            return save;

        case 'all':
            save = settings.save('storedLoadouts');
            save = settings.save('loadout') && save;
            return save;

        default:
            console.error('Invalid localStorage type '+type);
            return null;
        }
    }
};

var dpsForm = {
    // Placeholder for form elements
    form: {},
    
    // Create the form
    create: function () {
        var arr;
        /* format:
            Initialize setting variables
            Create widget
            Create event hooks*/
        
        // // // // // // //
        //  Display Name  //
        // // // // // // //
        if(settings.loadout.playerLevel == null) {
            settings.loadout.playerLevel = {};
        }
        if (settings.loadout.playerLevel.rsn == null) {
            settings.loadout.playerLevel.rsn = '';
        }
        dpsForm.form.playerLevel = {};

        dpsForm.form.playerLevel.rsnInput = new OO.ui.TextInputWidget({
            autocomplete: true,
            placeholder: 'Display name',
            id: 'dps-player-rsnInput',
            value: settings.loadout.playerLevel.rsn,
            icon: 'search'
        });
        dpsForm.form.playerLevel.rsnButton = new OO.ui.ButtonWidget({
            label: 'Hiscore Lookup',
            id: 'dps-player-rsnButton'
        });
        dpsForm.form.playerLevel.rsnInput.on('enter', dpsForm.rsnInputSubmit);
        dpsForm.form.playerLevel.rsnButton.on('click', dpsForm.rsnInputSubmit);

        dpsForm.form.playerLevel.rsnInputLayout = new OO.ui.ActionFieldLayout(
            dpsForm.form.playerLevel.rsnInput,
            dpsForm.form.playerLevel.rsnButton,
            {id: 'dps-player-rsnInput-layout'}
        );

        // // // // // // //
        // Player Skills  //
        // // // // // // //
        if(settings.loadout.playerLevel.current == null) {
            settings.loadout.playerLevel.current = {};
        }
        if(settings.loadout.playerLevel.visible == null) {
            settings.loadout.playerLevel.visible = {};
        }
        dpsForm.form.playerLevel.current = {};
        dpsForm.form.playerLevel.visible = {};
        dpsForm.form.playerLevel.skillIcon = {};
        dpsForm.form.playerLevel.slash = {};
        dpsForm.form.playerLevel.layout = {};

        var addSkill = function(skill, icon, width, height, visible){
            if(settings.loadout.playerLevel.current[skill] == null) {
                settings.loadout.playerLevel.current[skill] = 1;
            }
            dpsForm.form.playerLevel.current[skill] = new OO.ui.NumberInputWidget( {
                min: 0,
                max: 9999,
                step: 1,
                showButtons: false,
                value: settings.loadout.playerLevel.current[skill],
                classes: ['dps-player-skill-current','dps-skill-input'],
            });
            dpsForm.form.playerLevel.current[skill].on('change', dpsForm.playerLevelChange, [skill,visible]);
            dpsForm.form.playerLevel.skillIcon[skill] = new OO.ui.LabelWidget({
                label:$('<img alt="'+name+'" src="'+icon+'" data-file-width="'+width+'" data-file-height="'+height+'" width="'+width+'" height="'+height+'">'),
                classes: ['dps-player-skill-icon','dps-skill-icon'],
            });
            dpsForm.form.playerLevel.slash[skill] = new OO.ui.LabelWidget({
                label: '/',
                classes: ['dps-player-skill-slash','dps-skill-slash'],
            });
            if(settings.loadout.playerLevel.visible[skill] == null) {
                settings.loadout.playerLevel.visible[skill] = 1;
            }
            dpsForm.form.playerLevel.visible[skill] = new OO.ui.NumberInputWidget( {
                min: 0,
                max: 9999,
                step: 1,
                showButtons: false,
                disabled: (visible === 1),
                value: settings.loadout.playerLevel.visible[skill],
                classes: ['dps-player-skill-visible','dps-player-skill-visible-'+skill,'dps-skill-input'],
            });
            if(visible === 2){
                dpsForm.form.playerLevel.visible[skill].on('change', dpsForm.playerLevelChange, [skill,3]);
            }
            dpsForm.form.playerLevel.layout[skill] = new OO.ui.HorizontalLayout({
                items: [
                    dpsForm.form.playerLevel.skillIcon[skill],
                    dpsForm.form.playerLevel.visible[skill],
                    dpsForm.form.playerLevel.slash[skill],
                    dpsForm.form.playerLevel.current[skill],
                ],
                classes: ['dps-player-skill-layout','dps-skill-layout']
            });
        };
        if(settings.loadout.playerLevel.forceLookup) {
            settings.loadout.playerLevel.forceLookup = false;
            api.lookup(settings.loadout.playerLevel.rsn);
        }
        
        addSkill('Attack','/images/f/fe/Attack_icon.png?b4bce',25,25,1);
        addSkill('Strength','/images/1/1b/Strength_icon.png?e6e0c',16,20,1);
        addSkill('Defence','/images/b/b7/Defence_icon.png?ca0cd',17,19,1);
        addSkill('Ranged','/images/1/19/Ranged_icon.png?01b0e',23,23,1);
        addSkill('Magic','/images/5/5c/Magic_icon.png?334cf',25,23,1);
        addSkill('Prayer','/images/f/f2/Prayer_icon.png?ca0dc',23,23,2);
        addSkill('Hitpoints','/images/9/96/Hitpoints_icon.png?a4819',23,20,2);
        addSkill('Mining','/images/4/4a/Mining_icon.png?00870',23,23,1);
        addSkill('Farming','/images/f/fc/Farming_icon.png?558fa',25,23,1);

        dpsForm.form.playerLevel.skillLayout1 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.playerLevel.layout.Attack,
                dpsForm.form.playerLevel.layout.Strength,
                dpsForm.form.playerLevel.layout.Defence,
                dpsForm.form.playerLevel.layout.Magic,
                dpsForm.form.playerLevel.layout.Ranged,
            ],
            id: 'dps-player-skill-skillLayout1'
        });
        dpsForm.form.playerLevel.skillLayout2 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.playerLevel.layout.Hitpoints,
                dpsForm.form.playerLevel.layout.Prayer,
                dpsForm.form.playerLevel.layout.Mining,
                dpsForm.form.playerLevel.layout.Farming,
            ],
            id: 'dps-player-skill-skillLayout2'
        });

        // // // // // // // // // //
        // Boosts (Prayer/Potion)  //
        // // // // // // // // // //
        if(settings.loadout.slot == null) {
            settings.loadout.slot = {};
        }
        dpsForm.form.slot = {};

        if(settings.loadout.slot.prayers == null) {
            settings.loadout.slot.prayers = [];
        }
        arr = [];
        for(var item in equipment.prayers) {
            if(equipment.prayers[item].vi || true) { //Add all prayers for now
                arr.push({data:item,icon:'search'});
            }
        }
        dpsForm.form.slot.prayers = new OO.ui.MenuTagMultiselectWidget({
            allowArbitrary: false,
            inputPosition: 'outline',
            spellcheck: false,
            menu: {
                highlightOnFilter: true,
                hideOnChoose: false,
            },
            options: arr,
            classes: ['dps-slot-menu','prayers'],
        });
        dpsForm.form.slot.prayers.menu.filterMode = 'substring'; //For compatibility issues, the filterMode needs to be defined outside of the init as the current version of oojs.ui that the wiki has does not support filterMode
        settings.loadout.slot.prayers.forEach(function(prayer) {
            dpsForm.form.slot.prayers.addTag(prayer);
        });
        dpsForm.form.slot.prayers.on('change', dpsForm.prayerChange);
        
        if(settings.loadout.slot.potions == null) {
            settings.loadout.slot.potions = [];
        }
        arr = [];
        for(var potion in equipment.potions) {
            if(equipment.potions[potion].vi) { //only add if visible >= 1
                arr.push({data:potion});
            }
        }
        dpsForm.form.slot.potions = new OO.ui.MenuTagMultiselectWidget({
            allowArbitrary: false,
            inputPosition: 'outline',
            spellcheck: false,
            menu: {
                highlightOnFilter: true,
                hideOnChoose: false,
            },
            options: arr,
            classes: ['dps-slot-menu','potions'],
        });
        dpsForm.form.slot.potions.menu.filterMode = 'substring'; //For compatibility issues, the filterMode needs to be defined outside of the init as the current version of oojs.ui that the wiki has does not support filterMode
        settings.loadout.slot.potions.forEach(function(potion) {
            dpsForm.form.slot.potions.addTag(potion);
        });
        dpsForm.form.slot.potions.on('change', dpsForm.potionChange);

        // // // // // // // //
        //  Equipment slots  //
        // // // // // // // //
        if(settings.loadout.slot.label == null) {
            settings.loadout.slot.label = {};
        }
        if(settings.loadout.slot.slotItem == null) {
            settings.loadout.slot.slotItem = {};
        }
        if(settings.loadout.slot.visibleBlowpipe == null) {
            settings.loadout.slot.visibleBlowpipe = false;
        }
        if(settings.loadout.slot.visibleSpell == null) {
            settings.loadout.slot.visibleSpell = false;
        }
        if(settings.loadout.slot.disabledShield == null) {
            settings.loadout.slot.disabledShield = false;
        }
        dpsForm.form.slot.icon = {};
        dpsForm.form.slot.label = {};
        dpsForm.form.slot.layout = {};
        
        var addSlot = function(slot,label){
            if(settings.loadout.slot.label[slot] == null) {
                settings.loadout.slot.label[slot] = '';
            }
            if(settings.loadout.slot.slotItem[slot] == null) {
                settings.loadout.slot.slotItem[slot] = equipment.getItem('None',slot)[0];
            }
            var arr = [];
            for(var item in equipment[slot]) {
                if(equipment[slot][item].vi) { //only add if visible == 1
                    arr.push({data:item});
                }
            }
            dpsForm.form.slot[slot] = new OO.ui.ComboBoxInputWidget({
                autocomplete: false,
                required: false,
                options: arr,
                placeholder: label,
                value: settings.loadout.slot.label[slot],
                spellcheck: false,
                menu: {
                    filterFromInput: true,
                    autoHide: true,
                },
                classes: ['dps-slot-menu',slot],
            });
            dpsForm.form.slot[slot].menu.filterMode = 'substring'; //For compatibility issues, the filterMode needs to be defined outside of the init as the current version of oojs.ui that the wiki has does not support filterMode
            dpsForm.form.slot[slot].on('change', dpsForm.slotChange, [slot]);
            
            if(slot == 'spell' || slot == 'combatStyle') {
                arr = utils.titleCase(settings.loadout.slot.label[slot]);
            } else {
                arr = utils.itemCase(settings.loadout.slot.label[slot]);
            }
            if(settings.loadout.slot.slotItem[slot].vi){ //visible selections have a css sprite - Alternatively, could always load the non-sprite image on first load to make the initial page load faster(?)
                arr = $('<div class="dps-slot-icon '+slot+'"><img src="/images/4/47/Placeholder.png?1111" class="dps-img-'+slot+' dps-img-'+slot+'-'+arr.replace(/ /g,'_')+'"/></div>');
            } else { //invisible selections don't have a css sprite
                arr = $('<div class="dps-slot-icon '+slot+'"><img src="'+rswiki.getFileURLCached(settings.loadout.slot.slotItem[slot].im+'.png')+'"></div>');
            }
            dpsForm.form.slot.icon[slot] = new OO.ui.LabelWidget({
                label: arr
            });

            dpsForm.form.slot.layout[slot] = new OO.ui.HorizontalLayout({
                items: [
                    dpsForm.form.slot.icon[slot],
                    dpsForm.form.slot[slot],
                ]
            });
            
        };
        addSlot('head','Head');
        addSlot('cape','Cape');
        addSlot('neck','Neck');
        addSlot('ammo','Ammunition');
        addSlot('torso','Body');
        addSlot('shield','Shield');
        addSlot('legs','Legs');
        addSlot('gloves','Hand');
        addSlot('boots','Feet');
        addSlot('ring','Ring');
        addSlot('blowpipe','Dart');
        addSlot('spell','Spell');
        addSlot('weapon','Weapon');

        if(settings.loadout.slot.label.combatStyle == null) {
            settings.loadout.slot.label.combatStyle = 'Crush - Accurate';
        }
        if(settings.loadout.slot.slotItem.combatStyle == null) {
            settings.loadout.slot.slotItem.combatStyle = equipment.getItem('None','combatStyle')[0];
        }
        if(settings.loadout.slot.combatStyleLoadout == null) {
            settings.loadout.slot.combatStyleLoadout = 'unarmed';
        }
        dpsForm.form.slot.combatStyle = new OO.ui.DropdownInputWidget({
            classes: ['dps-slot-menu','combatStyle'],
            options: [{data:settings.loadout.slot.label.combatStyle}], //placeholder options - it's generated at the end of the function after the element is attached
            value: settings.loadout.slot.label.combatStyle
        });
        dpsForm.form.slot.combatStyle.on('change', dpsForm.slotChange, ['combatStyle']);

        dpsForm.form.slot.icon.combatStyle = new OO.ui.LabelWidget({
            label: $('<div class="dps-slot-icon combatStyle"><img src="'+rswiki.getFileURLCached(settings.loadout.slot.slotItem.combatStyle.im+'.png')+'"></div>')
        });

        dpsForm.form.slot.layout.combatStyle = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.slot.icon.combatStyle,
                dpsForm.form.slot.combatStyle,
            ]
        });

        dpsForm.form.slotLayout = new OO.ui.FieldsetLayout( {
            items: [
                dpsForm.form.slot.layout.weapon,   //includes 2h as well
                dpsForm.form.slot.layout.blowpipe,
                dpsForm.form.slot.layout.combatStyle,
                dpsForm.form.slot.layout.spell,
                dpsForm.form.slot.layout.head,
                dpsForm.form.slot.layout.cape,
                dpsForm.form.slot.layout.neck,
                dpsForm.form.slot.layout.ammo,
                dpsForm.form.slot.layout.torso,
                dpsForm.form.slot.layout.shield,
                dpsForm.form.slot.layout.legs,
                dpsForm.form.slot.layout.gloves,
                dpsForm.form.slot.layout.boots,
                dpsForm.form.slot.layout.ring,
            ],
            id: 'dps-slot-layout'
        });

        // // // // // // // //
        //  Equipment stats  //
        // // // // // // // //
        if(settings.loadout.slot.equipmentBonus == null) {
            settings.loadout.slot.equipmentBonus = {};
        }
        if(settings.loadout.slot.equipmentBonus.manualLevel == null) {
            settings.loadout.slot.equipmentBonus.manualLevel = false;
        }
        dpsForm.form.equipmentBonus = {};
        dpsForm.form.equipmentBonus.menu = {};
        dpsForm.form.equipmentBonus.layout = {};

        dpsForm.form.equipmentBonus.levelToggle = new OO.ui.ToggleSwitchWidget( {
            id: 'dps-equip-levelToggle',
            value: settings.loadout.slot.equipmentBonus.manualLevel
        });
        dpsForm.form.equipmentBonus.levelToggle.on('change', dpsForm.equipmentBonusToggleChange);
        
        dpsForm.form.equipmentBonus.levelToggleLayout = new OO.ui.FieldLayout(dpsForm.form.equipmentBonus.levelToggle, {
            label:'Manually override equipment stats',
            align:'left',
            id:'dps-equip-levelToggleLayout'
        });

        var addEquipmentBonusForm = function(name, icon, width, height, placeholder){
            if(settings.loadout.slot.equipmentBonus[name] == null) {
                settings.loadout.slot.equipmentBonus[name] = '';
            }
            dpsForm.form.equipmentBonus.menu[name] = new OO.ui.TextInputWidget( {
                autocomplete: false,
                value: (settings.loadout.slot.equipmentBonus[name]<0?'':'+')+settings.loadout.slot.equipmentBonus[name]+(name==='bm'?'%':''), //add + and %
                classes: ['dps-equip-menu',name],
                validate: /^[-+]?\d+$/
            });
            if(name === 'bm'){
                dpsForm.form.equipmentBonus.menu[name].setValidation(/^[-+]?\d+%?$/);
            }
            dpsForm.form.equipmentBonus.menu[name].on('change', dpsForm.equipmentBonusNumberChange, [name]);
            
            dpsForm.form.equipmentBonus.layout[name] = new OO.ui.FieldLayout(dpsForm.form.equipmentBonus.menu[name], {
                label:$('<img alt="'+name+'" src="'+icon+'" data-file-width="'+width+'" data-file-height="'+height+'" width="'+width+'" height="'+height+'">'),
                align:'left',
                classes: ['dps-equip-layout',name]
            });
        };
        addEquipmentBonusForm('at','/images/5/5c/White_dagger.png?51225',21,31,'Aggressive stab bonus');
        addEquipmentBonusForm('al','/images/8/8b/White_scimitar.png?3ef63',27,30,'Aggressive slash bonus');
        addEquipmentBonusForm('ac','/images/6/6a/White_warhammer.png?121eb',22,29,'Aggressive crush bonus');
        addEquipmentBonusForm('am','/images/5/5c/Magic_icon.png?334cf',25,23,'Aggressive magic bonus');
        addEquipmentBonusForm('ar','/images/1/19/Ranged_icon.png?01b0e',23,23,'Aggressive ranged bonus');
        addEquipmentBonusForm('dt','/images/5/5c/White_dagger.png?51225',21,31,'Defensive stab bonus');
        addEquipmentBonusForm('dl','/images/8/8b/White_scimitar.png?3ef63',27,30,'Defensive slash bonus');
        addEquipmentBonusForm('dc','/images/6/6a/White_warhammer.png?121eb',22,29,'Defensive crush bonus');
        addEquipmentBonusForm('dm','/images/5/5c/Magic_icon.png?334cf',25,23,'Defensive magic bonus');
        addEquipmentBonusForm('dr','/images/1/19/Ranged_icon.png?01b0e',23,23,'Defensive ranged bonus');
        addEquipmentBonusForm('bs','/images/1/1b/Strength_icon.png?e6e0c',16,20,'Strength bonus');
        addEquipmentBonusForm('br','/images/2/22/Ranged_Strength_icon.png?79763',26,25,'Ranged strength bonus');
        addEquipmentBonusForm('bm','/images/c/cc/Magic_Damage_icon.png?63a1a',28,28,'Magic strength bonus');
        addEquipmentBonusForm('pr','/images/f/f2/Prayer_icon.png?ca0dc',23,23,'Prayer bonus');

        dpsForm.form.equipmentBonus.bigLayout1 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.equipmentBonus.layout.at,
                dpsForm.form.equipmentBonus.layout.al,
                dpsForm.form.equipmentBonus.layout.ac,
                dpsForm.form.equipmentBonus.layout.am,
                dpsForm.form.equipmentBonus.layout.ar,
            ]
        });
        dpsForm.form.equipmentBonus.bigLayout2 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.equipmentBonus.layout.dt,
                dpsForm.form.equipmentBonus.layout.dl,
                dpsForm.form.equipmentBonus.layout.dc,
                dpsForm.form.equipmentBonus.layout.dm,
                dpsForm.form.equipmentBonus.layout.dr,
            ]
        });
        dpsForm.form.equipmentBonus.bigLayout3 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.equipmentBonus.layout.bs,
                dpsForm.form.equipmentBonus.layout.br,
                dpsForm.form.equipmentBonus.layout.bm,
                dpsForm.form.equipmentBonus.layout.pr,
            ]
        });




        // // // // // // // // //
        //  Monster Name/Toggle //
        // // // // // // // // //
        dpsForm.form.monster = {};

        if(settings.loadout.monster.name == null) {
            settings.loadout.monster.name = '';
        }
        
        arr = [];
        for(var mon in monster) {
            if(monster[mon].vi) {
                arr.push({data:mon});
            }
        }
        dpsForm.form.monster.name = new OO.ui.ComboBoxInputWidget({
            autocomplete: false,
            required: false,
            options: arr,
            placeholder: 'Monster name',
            value: settings.loadout.monster.name,
            spellcheck: false,
            menu: {
                filterFromInput: true,
                autoHide: true,
            }
        });
        dpsForm.form.monster.name.menu.filterMode = 'substring'; //For compatibility issues, the filterMode needs to be defined outside of the init as the current version of oojs.ui that the wiki has does not support filterMode
        dpsForm.form.monster.name.on('change', dpsForm.monsterNameChange);

        //Monster name selection > Layout
        dpsForm.form.monster.nameLayout = new OO.ui.FieldLayout(dpsForm.form.monster.name, {
            label:'Monster name'
        });

        // Manual monster - ToggleSwitch
        if(settings.loadout.monster.manualLevel == null) {
            settings.loadout.monster.manualLevel = false;
        }
        dpsForm.form.monster.levelToggle = new OO.ui.ToggleSwitchWidget( {
            id: 'dps-monster-levelToggle',
            value: settings.loadout.monster.manualLevel
        });
        dpsForm.form.monster.levelToggle.on('change', dpsForm.monsterToggleChange);
        
        // Manual monster > FieldLayout
        dpsForm.form.monster.levelToggleLayout = new OO.ui.FieldLayout(dpsForm.form.monster.levelToggle, {
            label:'Manually override monster stats',
            align:'left',
            id:'dps-monster-levelToggleLayout'
        });
        
        // // // // // // //
        //  Monster Skill //
        // // // // // // //

        if(settings.loadout.monster.current == null) {
            settings.loadout.monster.current = {};
        }
        if(settings.loadout.monster.visible == null) {
            settings.loadout.monster.visible = {};
        }
        dpsForm.form.monster.menu = {};
        dpsForm.form.monster.icon = {};
        dpsForm.form.monster.layout = {};
        dpsForm.form.monster.slash = {};
        // Monster PARAMETERS

        var addMonsterSkill = function(skill, icon, width, height){

            // Numeric parameter - NumberInput
            if(settings.loadout.monster.current[skill] == null) {
                settings.loadout.monster.current[skill] = 1;
            }
            if(settings.loadout.monster.visible[skill] == null) {
                settings.loadout.monster.visible[skill] = 1;
            }

            dpsForm.form.monster.icon[skill] = new OO.ui.LabelWidget({
                label:$('<img alt="'+skill+'" src="'+icon+'" data-file-width="'+width+'" data-file-height="'+height+'" width="'+width+'" height="'+height+'">'),
                classes: ['dps-monster-skill-icon','dps-skill-icon'],
            });

            dpsForm.form.monster.menu['current'+skill] = new OO.ui.NumberInputWidget({
                min: 0,
                max: 9999,
                step: 1,
                showButtons: false,
                value: settings.loadout.monster.current[skill],
                classes: ['dps-monster-skill-current','dps-skill-input'],
            });
            dpsForm.form.monster.menu['current'+skill].on('change', dpsForm.monsterSkillChange, [skill,2]);

            dpsForm.form.monster.slash[skill] = new OO.ui.LabelWidget({
                label: '/',
                classes: ['dps-monster-skill-slash','dps-skill-slash'],
            });

            dpsForm.form.monster.menu['visible'+skill] = new OO.ui.NumberInputWidget({
                min: 0,
                max: 9999,
                step: 1,
                showButtons: false,
                value: settings.loadout.monster.visible[skill],
                classes: ['dps-monster-skill-visible','dps-skill-input'],
            });
            dpsForm.form.monster.menu['visible'+skill].on('change', dpsForm.monsterSkillChange, [skill,3]);
            

            dpsForm.form.monster.layout[skill] = new OO.ui.HorizontalLayout({
                items: [
                    dpsForm.form.monster.icon[skill],
                    dpsForm.form.monster.menu['visible'+skill],
                    dpsForm.form.monster.slash[skill],
                    dpsForm.form.monster.menu['current'+skill],
                ],
                classes: ['dps-monster-skill-layout','dps-skill-layout']
            });
        };

        addMonsterSkill('Hitpoints','/images/9/96/Hitpoints_icon.png?a4819',23,20);
        addMonsterSkill('Attack','/images/f/fe/Attack_icon.png?b4bce',25,25);
        addMonsterSkill('Strength','/images/1/1b/Strength_icon.png?e6e0c',16,20);
        addMonsterSkill('Defence','/images/b/b7/Defence_icon.png?ca0cd',17,19);
        addMonsterSkill('Magic','/images/5/5c/Magic_icon.png?334cf',25,23);
        addMonsterSkill('Ranged','/images/1/19/Ranged_icon.png?01b0e',23,23);

        // // // // // // //
        // Monster Inputs //
        // // // // // // //

        var addMonsterForm = function(name, icon, width, height, placeholder, validate, addplus, addperc, defaultvalue){
            // Numeric parameter - NumberInput
            if(settings.loadout.monster[name] == null) {
                settings.loadout.monster[name] = defaultvalue;
            }
            dpsForm.form.monster.menu[name] = new OO.ui.TextInputWidget({
                autcomplete: false,
                value: (addplus?(settings.loadout.monster[name]<0?'':'+'):'')+settings.loadout.monster[name]+(addperc?'%':''), //add + and %
                validate: validate,
                classes: ['dps-monster-input','dps-monster-input-'+name],
            });
            dpsForm.form.monster.menu[name].on('change', dpsForm.monsterNumberChange, [name,defaultvalue]);
            
            // Numeric parameter > FieldLayout
            dpsForm.form.monster.layout[name] = new OO.ui.FieldLayout(dpsForm.form.monster.menu[name], {
                label:$('<img alt="'+name+'" src="'+icon+'" data-file-width="'+width+'" data-file-height="'+height+'" width="'+width+'" height="'+height+'">'),
                align:'left',
                classes: ['dps-monster-input-layout'],
            });
        };
        
        addMonsterForm('maxHit','/images/8/8f/Combat_icon.png?93d63',19,19,'Max hit',/^\d+$/,false,false,0); //#
        addMonsterForm('astab','/images/5/5c/White_dagger.png?51225',21,31,'Aggressive stab bonus',/^[-+]?\d+$/,true,false,0); //+-#
        addMonsterForm('aslash','/images/8/8b/White_scimitar.png?3ef63',27,30,'Aggressive slash bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('acrush','/images/6/6a/White_warhammer.png?121eb',22,29,'Aggressive crush bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('amagic','/images/5/5c/Magic_icon.png?334cf',25,23,'Aggressive magic bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('arange','/images/1/19/Ranged_icon.png?01b0e',23,23,'Aggressive ranged bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('dstab','/images/5/5c/White_dagger.png?51225',21,31,'Defensive stab bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('dslash','/images/8/8b/White_scimitar.png?3ef63',27,30,'Defensive slash bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('dcrush','/images/6/6a/White_warhammer.png?121eb',22,29,'Defensive crush bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('dmagic','/images/5/5c/Magic_icon.png?334cf',25,23,'Defensive magic bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('drange','/images/1/19/Ranged_icon.png?01b0e',23,23,'Defensive ranged bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('abns','/images/f/fe/Attack_icon.png?b4bce',25,25,'Attack bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('str','/images/1/1b/Strength_icon.png?e6e0c',16,20,'Strength bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('rstr','/images/2/22/Ranged_Strength_icon.png?79763',26,25,'Ranged strength bonus',/^[-+]?\d+$/,true,false,0);
        addMonsterForm('mdmg','/images/c/cc/Magic_Damage_icon.png?63a1a',28,28,'Magic strength bonus',/^[-+]?\d+%?$/,true,true,0); //+-#%
        addMonsterForm('xpBonus','/images/b/bd/Stats_icon.png?1b467',25,23,'XP bonus',/^\d*(0|0\.0|2\.5|5|5\.0|7\.5)%?$/,false,true,0); //Multiple of 2.5 +- %
        addMonsterForm('attackSpeed','/images/a/aa/Monster_attack_speed_1.png?fe7f2',32,10,'Attack speed',/^[1-9]\d?$/,false,false,4); //1-99
        addMonsterForm('size','/images/c/c2/Boxing_ring_icon.png?64787',27,22,'Tile size',/^[1-9]\d?$/,false,false,1);
        
        // attackStyle > DropdownWidget
        if(settings.loadout.monster.attackStyle == null) {
            settings.loadout.monster.attackStyle = 'Melee';
        }
        dpsForm.form.monster.menu.attackStyle = new OO.ui.DropdownInputWidget({
            placeholder: 'Attack style',
            options: [
                {data:'Melee'},
                {data:'Stab'},
                {data:'Slash'},
                {data:'Crush'},
                {data:'Ranged'},
                {data:'Magic'},
                {data:'Magical melee'},
                {data:'Magical ranged'},
                {data:'Ranged magic'},
            ],
            value: settings.loadout.monster.attackStyle,
        });
        dpsForm.form.monster.menu.attackStyle.on('change', dpsForm.monsterAttackStyleChange);
        
        // attackStyle > FieldLayout
        dpsForm.form.monster.layout.attackStyle = new OO.ui.FieldLayout(dpsForm.form.monster.menu.attackStyle, {
            label:$('<img alt="attackStyle" src="/images/8/8f/Combat_icon.png?93d63" data-file-width="19" data-file-height="19" width="19" height="19">'),
            align:'left',
        });
        
        // immunePoison > DropdownWidget
        if(settings.loadout.monster.immunePoison == null) {
            settings.loadout.monster.immunePoison = 0;
        }
        dpsForm.form.monster.menu.immunePoison = new OO.ui.DropdownInputWidget({
            placeholder: 'Poison immunity',
            options: [
                {data:0, label:'Not immune'},
                {data:1, label:'Immune'},
                {data:3, label:'Smoke spells only'},
            ],
            value: settings.loadout.monster.immunePoison,
            classes: ['dps-monster-input','dps-monster-input-immunePoison'],
        });
        dpsForm.form.monster.menu.immunePoison.dropdownWidget.menu.setIdealSize = function () { //Overriding another prototype to make the visible selection + the dropdown menu have different widths
            this.idealWidth = 150; //override the width
            if (!this.clipping) {
                this.$clippable.css({width:this.idealWidth});
            }
        };
        dpsForm.form.monster.menu.immunePoison.on('change', dpsForm.immunityChange, ['immunePoison']);
        
        // immunePoison > FieldLayout
        dpsForm.form.monster.layout.immunePoison = new OO.ui.FieldLayout(dpsForm.form.monster.menu.immunePoison, {
            label:$('<img alt="immunePoison" src="/images/c/ca/Poison_hitsplat.png?04b47" data-file-width="24" data-file-height="23" width="24" height="23">'),
            align:'left',
            classes: ['dps-monster-input-layout'],
        });
        
        // immuneVenom > DropdownWidget
        if(settings.loadout.monster.immuneVenom == null) {
            settings.loadout.monster.immuneVenom = 0;
        }
        dpsForm.form.monster.menu.immuneVenom = new OO.ui.DropdownInputWidget({
            placeholder: 'Poison immunity',
            options: [
                {data:0, label:'Not immune'},
                {data:1, label:'Immune'},
                {data:2, label:'Poisons instead'},
            ],
            value: settings.loadout.monster.immuneVenom,
            classes: ['dps-monster-input','dps-monster-input-immuneVenom'],
        });
        dpsForm.form.monster.menu.immuneVenom.dropdownWidget.menu.setIdealSize = function () { //Overriding another prototype to make the visible selection + the dropdown menu have different widths
            this.idealWidth = 150; //override the width
            if (!this.clipping) {
                this.$clippable.css({width:this.idealWidth});
            }
        };
        dpsForm.form.monster.menu.immuneVenom.on('change', dpsForm.immunityChange, ['immuneVenom']);
        
        // immuneVenom > FieldLayout
        dpsForm.form.monster.layout.immuneVenom = new OO.ui.FieldLayout(dpsForm.form.monster.menu.immuneVenom, {
            label:$('<img alt="immuneVenom" src="/images/3/37/Venom_hitsplat.png?8843e" data-file-width="24" data-file-height="23" width="24" height="23">'),
            align:'left',
            classes: ['dps-monster-input-layout'],
        });
        
        // combatType > MenuTagMultiselectWidget
        if(settings.loadout.monster.combatType == null) {
            settings.loadout.monster.combatType = [];
        }
        dpsForm.form.monster.menu.combatType = new OO.ui.MenuTagMultiselectWidget({
            allowArbitrary: false,
            inputPosition: 'outline',
            spellcheck: false,
            placeholder: 'Combat attributes',
            menu: {
                highlightOnFilter: true,
                hideOnChoose: false,
            },
            options: [
                {data:'demon'},
                {data:'dragon'},
                {data:'fiery'},
                {data:'kalphite'},
                {data:'leafy'},
                {data:'penance'},
                {data:'shade'},
                {data:'slayer'},
                {data:'undead'},
                {data:'vampyre1'},
                {data:'vampyre2'},
                {data:'vampyre3'},
                {data:'castle wars flagholder'},
            ],
        });
        settings.loadout.monster.combatType.forEach(function(item) {
            dpsForm.form.monster.menu.combatType.addTag(item);
        });
        dpsForm.form.monster.menu.combatType.on('change', dpsForm.monsterCombatTypeChange);
        
        dpsForm.form.monsterLayout1 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.monster.layout.Hitpoints,
                dpsForm.form.monster.layout.Attack,
                dpsForm.form.monster.layout.Strength,
                dpsForm.form.monster.layout.Defence,
                dpsForm.form.monster.layout.Magic,
                dpsForm.form.monster.layout.Ranged,
            ]
        });
        dpsForm.form.monsterLayout2 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.monster.layout.astab,
                dpsForm.form.monster.layout.aslash,
                dpsForm.form.monster.layout.acrush,
                dpsForm.form.monster.layout.amagic,
                dpsForm.form.monster.layout.arange,
            ]
        });
        dpsForm.form.monsterLayout3 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.monster.layout.dstab,
                dpsForm.form.monster.layout.dslash,
                dpsForm.form.monster.layout.dcrush,
                dpsForm.form.monster.layout.dmagic,
                dpsForm.form.monster.layout.drange,
            ]
        });
        dpsForm.form.monsterLayout4 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.monster.layout.abns,
                dpsForm.form.monster.layout.str,
                dpsForm.form.monster.layout.rstr,
                dpsForm.form.monster.layout.mdmg,
                dpsForm.form.monster.layout.immunePoison,
            ]
        });
        dpsForm.form.monsterLayout5 = new OO.ui.HorizontalLayout({
            items: [
                dpsForm.form.monster.layout.maxHit,
                dpsForm.form.monster.layout.xpBonus,
                dpsForm.form.monster.layout.attackSpeed,
                dpsForm.form.monster.layout.size,
                dpsForm.form.monster.layout.immuneVenom,
            ]
        });
        dpsForm.form.monsterLayout6 = new OO.ui.FieldsetLayout({
            items: [
                dpsForm.form.monster.layout.attackStyle,
                dpsForm.form.monster.menu.combatType,
            ]
        });

        // Button
        dpsForm.form.calculateButton =  new OO.ui.ButtonWidget( {
            label: 'CALCULATE',
            icon: 'die',
            title: 'Remove'
        });
        dpsForm.form.calculateButton.on('click', dpsForm.calculateButtonClicked);

        
        //add item_id to parameters(?) + all items
        
        $('.dps-main').empty()
            .append($('<tr><th class="dps-subheader infobox-subheader">Player</th></tr>'))
            .append($('<tr/>')
                .append($('<td/>')
                    .append(dpsForm.form.playerLevel.rsnInputLayout.$element)
                    .append(dpsForm.form.playerLevel.skillLayout1.$element)
                    .append(dpsForm.form.playerLevel.skillLayout2.$element)
                )
            ).append('<tr><th class="dps-subheader infobox-subheader">Boosts</th></tr>')
            .append($('<tr/>')
                .append($('<td/>')
                    .append(dpsForm.form.slot.potions.$element)
                    .append(dpsForm.form.slot.prayers.$element)
                )
            ).append('<tr><th class="dps-subheader infobox-subheader">Equipment</th></tr>')
            .append($('<tr/>')
                .append($('<td/>')
                    .append(dpsForm.form.slotLayout.$element)
                )
            ).append('<tr><th class="dps-subheader infobox-subheader">Equipment Stats</th></tr>')
            .append($('<tr/>')
                .append($('<td/>')
                    .append(dpsForm.form.equipmentBonus.levelToggleLayout.$element)
                    .append(dpsForm.form.equipmentBonus.bigLayout1.$element)
                    .append(dpsForm.form.equipmentBonus.bigLayout2.$element)
                    .append(dpsForm.form.equipmentBonus.bigLayout3.$element)
                )
            ).append('<tr><th class="dps-subheader infobox-subheader">Monster</th></tr>')
            .append($('<tr/>')
                .append($('<td/>')
                    .append(dpsForm.form.monster.nameLayout.$element)
                    .append(dpsForm.form.monster.levelToggleLayout.$element)
                    .append(dpsForm.form.monsterLayout1.$element)
                    .append(dpsForm.form.monsterLayout2.$element)
                    .append(dpsForm.form.monsterLayout3.$element)
                    .append(dpsForm.form.monsterLayout4.$element)
                    .append(dpsForm.form.monsterLayout5.$element)
                    .append(dpsForm.form.monsterLayout6.$element)
                )
            ).append('<tr><th class="dps-subheader infobox-subheader">Calculate</th></tr>')
            .append($('<tr/>')
                .append($('<td/>')
                    .append(dpsForm.form.calculateButton.$element)
                )
            ).append($('<tr class="dps-testOutput"/>'));
        //.after($(resultsContainer)).after($(resultsHeader));
        
        //Injecting things with jquery because it's easier than modifying OO.ui
        $('.dps-slot-menu.prayers > .oo-ui-tagMultiselectWidget-input > .oo-ui-inputWidget-input').attr('placeholder','Activate Prayer');
        $('.dps-slot-menu.potions > .oo-ui-tagMultiselectWidget-input > .oo-ui-inputWidget-input').attr('placeholder','Use Stat Boosts');

        var slots = ['head','cape','neck','ammo','torso','legs','gloves','boots','ring','weapon','shield','blowpipe','spell','potions','prayers'];
        slots.forEach(function(slot){
            new Image().src = rswiki.getFileURLCached('Combat Calculator '+slot+' sprite.png');
            $('.dps-slot-menu.'+slot+' > div.oo-ui-menuSelectWidget > div.oo-ui-menuOptionWidget > span.oo-ui-labelElement-label').each(function (){
                var pname = $(this).text();
                $(this).prepend($('<div class="dps-injectedIcon"><img src="/images/4/47/Placeholder.png?11111" class="dps-img-'+slot+' dps-img-'+slot+'-'+pname.replace(/ /g,'_')+'"/></div>'));
            });
        });

        //TODO: Build the dropdowns here to load the page faster

        //needs to be after elements added to page
        dpsForm.form.slot.layout.blowpipe.toggle(settings.loadout.slot.visibleBlowpipe);
        dpsForm.form.slot.layout.spell.toggle(settings.loadout.slot.visibleSpell);
        dpsForm.form.slot.shield.setDisabled(settings.loadout.slot.disabledShield);
        dpsForm.updateCombatStyleLoadout(settings.loadout.slot.combatStyleLoadout,settings.loadout.slot.label.combatStyle);
        dpsForm.equipmentBonusToggleChange(settings.loadout.slot.equipmentBonus.manualLevel);
        dpsForm.monsterToggleChange(settings.loadout.monster.manualLevel);
        updateVisibleLevels(['Attack','Strength','Defence','Magic','Ranged','Mining','Farming']);
        //Resave loadout to fix null parameters
        settings.save('loadout');
    },
    
    //When new rsn is submitted,
    //Lookup new skill level values.
    rsnInputSubmit: function() {
        if(dpsForm.form.playerLevel.rsnButton.isActive()){
            return;
        }
        console.log('rsnInputSubmit');
        var _rsn = dpsForm.form.playerLevel.rsnInput.getValue();
        api.lookup(_rsn);
        settings.loadout.playerLevel.rsn = _rsn;
        settings.save('playerLevel');
    },
    
    // When a player changes one of his current or visible skill levels,
    playerLevelChange: function(skill, type, value) {
        console.log('playerLevelChange: ' + skill + ' to ' + value);
        var lvl = utils.parseIntDefault(value,1);
        if(type===1){ //Main skill like Attack
            settings.loadout.playerLevel.current[skill] = lvl;
            updateVisibleLevels([skill]);
        } else if(type===2){ //Current Prayer/Hitpoints
            if(settings.loadout.playerLevel.current[skill] === settings.loadout.playerLevel.visible[skill]){
                settings.loadout.playerLevel.visible[skill] = lvl;
                dpsForm.form.playerLevel.visible[skill].setValue(value);
            }
            settings.loadout.playerLevel.current[skill] = lvl;
        } else if(type===3){ //Visible Prayer/Hitpoints
            settings.loadout.playerLevel.visible[skill] = lvl;
        }
        settings.save('playerLevel');
    },
    
    // When a player changes one of his equipments
    slotChange: function(slot, value) {
        var itemname;
        if(slot == 'spell' || slot == 'combatStyle') {
            itemname = utils.titleCase(value);
        } else {
            itemname = utils.itemCase(value);
        }
        console.log('slotChange: ' + slot + ' to ' + itemname);
        settings.loadout.slot.label[slot] = itemname;
        var ret = equipment.getItem(itemname, slot);
        settings.loadout.slot.slotItem[slot] = ret[0];
        if(ret[1]){
            dpsForm.form.slot[slot].setFlags({invalid:false});
        } else {
            dpsForm.form.slot[slot].setFlags('invalid'); //TODO: the flag gets insta-removed from the class for some unclear reason so it doesn't work. To investigate - probably need to set a validation function
        }
        if(settings.loadout.slot.slotItem[slot].vi){ //visible selections have a css sprite
            dpsForm.form.slot.icon[slot].setLabel($('<div class="dps-slot-icon '+slot+'"><img src="/images/4/47/Placeholder.png?11111" class="dps-img-'+slot+' dps-img-'+slot+'-'+itemname.replace(/ /g,'_')+'"/></div>'));
        } else { //invisible selections don't have a css sprite
            dpsForm.form.slot.icon[slot].setLabel($('<div class="dps-slot-icon '+slot+'"><img src="'+rswiki.getFileURLCached(settings.loadout.slot.slotItem[slot].im+'.png')+'"></div>'));
        }
        if(slot == 'weapon') {
            settings.loadout.slot.disabledShield = settings.loadout.slot.slotItem[slot].sl == '2h';
            dpsForm.form.slot.shield.setDisabled(settings.loadout.slot.disabledShield);
            if(settings.loadout.slot.disabledShield == true) {
                dpsForm.form.slot.shield.setValue('');
                settings.loadout.slot.label.shield = '';
            }
            settings.loadout.slot.visibleBlowpipe = itemname == 'Toxic blowpipe';
            dpsForm.form.slot.layout.blowpipe.toggle(settings.loadout.slot.visibleBlowpipe);
            if(settings.loadout.slot.visibleBlowpipe == false) {
                dpsForm.form.slot.blowpipe.setValue('');
                settings.loadout.slot.label.blowpipe = '';
            }
            settings.loadout.slot.combatStyleLoadout = settings.loadout.slot.slotItem[slot].cS;
            dpsForm.updateCombatStyleLoadout(settings.loadout.slot.combatStyleLoadout);
        }
        if(slot == 'combatStyle') {
            settings.loadout.slot.visibleSpell = itemname == 'Magic - Spell' || itemname == 'Magic - Defensive Spell';
            dpsForm.form.slot.layout.spell.toggle(settings.loadout.slot.visibleSpell);
            if(settings.loadout.slot.visibleSpell == false) {
                dpsForm.form.slot.spell.setValue('');
                settings.loadout.slot.label.spell = '';
            }
        } else if(!settings.loadout.slot.equipmentBonus.manualLevel) {
            updateEquipmentBonus();
        } else {
            dpsForm.form.equipmentBonus.levelToggle.setValue(false);
        }
        settings.save('slot');
    },
    
    //Update the options for combat style. The selected option will default to the first option if value is undefined
    loadedCombatStyleLoadout: '',
    updateCombatStyleLoadout: function(style, value) {
        if(style === dpsForm.loadedCombatStyleLoadout) {
            return;
        }
        dpsForm.loadedCombatStyleLoadout = style;

        var choices = equipment.combatStyleLoadout[style];
        value = value || choices[0];
        
        var arr = [];
        choices.forEach(function(choice) {
            arr.push({data:choice});
        });
        dpsForm.form.slot.combatStyle.setOptions(arr);
        
        var i = 0;
        $('.dps-slot-menu.combatStyle > .oo-ui-dropdownWidget > .oo-ui-menuSelectWidget >.oo-ui-labelElement >.oo-ui-labelElement-label').each(function (){
            $(this).prepend($('<div class="dps-injectedIcon"><img src="/images/4/47/Placeholder.png?11111" class="dps-img-combatStyle dps-img-combatStyle-'+style.replace(/ /g,'_')+'-'+i+'"/></div>'));
            i++;
        });

        dpsForm.form.slot.combatStyle.setValue(value);
        settings.loadout.slot.label.combatStyle = value;
        settings.save('slot');
    },
    
    // When a player selects a new prayer or changes order
    // TODO: Event does not fire if player clicks X to remove a value
    prayerChangeStop: false,
    prayerChange: function(datalist) {
        if(dpsForm.prayerChangeStop) {  //prevent recursion when turning off other prayers
            return;
        }
        dpsForm.prayerChangeStop = true;
        var lastElement = datalist.length-1;
        if(lastElement) { //skip if empty list
            equipment.prayers[datalist[lastElement].data].TurnsOff.forEach(function(prayer) {
                dpsForm.form.slot.prayers.removeTagByData(prayer);
            });
        }
        settings.loadout.slot.prayers = dpsForm.form.slot.prayers.getValue();
        dpsForm.prayerChangeStop = false;
        settings.save('slot');
    },
    
    // When a player selects a new potion or changes order
    // TODO: Event does not fire if player clicks X to remove a value
    potionChange: function() {
        settings.loadout.slot.potions = dpsForm.form.slot.potions.getValue();
        updateVisibleLevels(['Attack','Strength','Defence','Magic','Ranged','Mining','Farming']);
        settings.save('playerLevel');
        settings.save('slot');
    },
    
    // When manual equipmentBonus toggle is changed, update all equipment bonuses
    equipmentBonusToggleChange: function(value) {
        console.log('equipmentBonusToggleChange');
        settings.loadout.slot.equipmentBonus.manualLevel = value;
        for(var menu in dpsForm.form.equipmentBonus.menu){
            dpsForm.form.equipmentBonus.menu[menu].setDisabled(!value);
        }
        if (!value) {
            updateEquipmentBonus();
        }
        settings.save('slot');
    },

    // When a player changes equipmentBonus property
    equipmentBonusNumberChange: function(param, value) {
        settings.loadout.slot.equipmentBonus[param] = utils.parseIntDefault(value,0);
        settings.save('slot');
    },

    monsterUnlockedToggleButton: true, //prevent recursion
    monsterNameChange: function(value){
        console.log(value);
        setMonster(value);
        settings.loadout.monster.name = value;
        settings.save('monster');
    },

    // When manual monster toggle is changed, update monster stats
    monsterToggleChange: function(value) {
        console.log('monsterToggleChange');
        settings.loadout.monster.manualLevel = value;
        for(var menu in dpsForm.form.monster.menu){
            dpsForm.form.monster.menu[menu].setDisabled(!value);
        }
        if (!value && dpsForm.monsterUnlockedToggleButton) {
            setMonster(settings.loadout.monster.name);
        }
        settings.save('monster');
    },

    monsterSkillChange: function(skill, type, value) {
        var lvl = utils.parseIntDefault(value,1);
        if(type===2){ //Current
            if(settings.loadout.monster.current[skill] === settings.loadout.monster.visible[skill]){
                settings.loadout.monster.visible[skill] = lvl;
                dpsForm.form.monster.menu['visible'+skill].setValue(lvl);
            }
            settings.loadout.monster.current[skill] = lvl;
        } else if(type===3){ //Visible
            settings.loadout.monster.visible[skill] = lvl;
        }
        if(dpsForm.monsterUnlockedToggleButton) {
            settings.save('monster');
        }
    },

    monsterNumberChange: function(param, defaultvalue, value) {
        if(param == 'xpBonus') {
            settings.loadout.monster[param] = Math.round(utils.parseFloatDefault(value,defaultvalue)/2.5)*2.5;
            console.log('xpBonus'+settings.loadout.monster[param]);
        } else {
            settings.loadout.monster[param] = utils.parseIntDefault(value,defaultvalue);
        }
        if(dpsForm.monsterUnlockedToggleButton) {
            settings.save('monster');
        }
    },
    
    monsterCombatTypeChange: function() {
        settings.loadout.monster.combatType = dpsForm.form.monster.menu.combatType.getValue();
        if(dpsForm.monsterUnlockedToggleButton) {
            settings.save('monster');
        }
    },
    
    monsterAttackStyleChange: function() {
        settings.loadout.monster.attackStyle = dpsForm.form.monster.menu.attackStyle.getValue();
        if(dpsForm.monsterUnlockedToggleButton){
            settings.save('monster');
        }
    },
    
    immunityChange: function(slot, value) {
        //console.log('immunityChange'+value);
        settings.loadout.monster[slot] = value;
        if(dpsForm.monsterUnlockedToggleButton){
            settings.save('monster');
        }
    },
    
    calculateButtonClicked: function() {
        var args = calc.playerAttack(settings.loadout);
        var aroll = args[0];
        var dstyle = args[1];
        args = calc.playerMaxHit(settings.loadout);
        var drollmin = args[0];
        var drollmax = args[1];
        var dtypeless = args[2];
        var ticks = args[3];
        var droll = calc.monsterDefence(settings.loadout,dstyle,false);
        var acc = calc.accuracies(aroll,droll);
        var dps = calc.naiveDPS(acc,drollmin,drollmax,dtypeless,ticks);

        var output = 'CALCS:<br><table><tr><td>%</td><td>Defence roll</td></tr>';
        for(var i = 0; i < droll.length; i++) {
            output += '<tr><td>'+droll[i].Prob.toPrecision(5)*100+'%</td><td>'+droll[i].Roll+'</td></tr>';
        }
        output += '</table><br><table><tr><td>%</td><td>Attack roll</td><td>Hit</td><td>Accuracy</td><td>Special</td></tr>';
        for(i = 0; i < aroll.length; i++) {
            output += '<tr><td>'+aroll[i].Prob.toPrecision(5)*100+'%</td><td>'+aroll[i].Roll+'</td><td>'+(dtypeless+drollmin[i].Roll)+'-'+drollmax[i].Roll+'</td><td>'+acc[i].Roll.toPrecision(5)*100+'%</td><td>'+aroll[i].Spec+'</td></tr>';
        }
        output += '</table><br><br>DPS: '+dps.toPrecision(5);
        $('.dps-testOutput').empty().append(output);

    },
};

function init() {
    try {
        console.error('----------');
        console.log('Combat Calculator Gadget Startup');
        
        $('table.dps').empty();
        $('table.dps').append('<tr><th class="dps-header infobox-header">Combat Calculator</th></tr>');
        $('table.dps').append('<tr class="dps-main"><td>Loading combat calculator</td></tr>');
        tic()
        settings.load('all');
        settings.loadQueryString();
        dpsForm.create();
        toc()
    } catch(err) {
        console.error(err);
    }
}

$(init);
// </nowiki>>