Melvor Show Modifiers

Adds a button to show all your modifiers

// ==UserScript==
// @name        Melvor Show Modifiers
// @namespace   http://tampermonkey.net/
// @version     0.2.13
// @description Adds a button to show all your modifiers
// @author		GMiclotte
// @include		https://melvoridle.com/*
// @include		https://*.melvoridle.com/*
// @exclude		https://melvoridle.com/index.php
// @exclude		https://*.melvoridle.com/index.php
// @exclude		https://wiki.melvoridle.com/*
// @exclude		https://*.wiki.melvoridle.com/*
// @inject-into page
// @noframes
// @grant		none
// ==/UserScript==


((main) => {
    const script = document.createElement('script');
    script.textContent = `try { (${main})(); } catch (e) { console.log(e); }`;
    document.body.appendChild(script).parentNode.removeChild(script);
})(() => {

    class ShowModifiers {

        constructor(name, logName, check = true) {
            this.name = name;
            this.logName = logName;
            // increased - decreased
            this.creasedModifiers = {
                // modifiers that do not directly relate to skilling
                misc: [
                    'BankSpace',
                    'BankSpaceShop',
                ],
                // modifiers that relate to both combat and non-combat skilling
                skilling: [
                    'ChanceToPreservePotionCharge',
                    'ChanceToDoubleItemsGlobal',
                    'GPFromSales',
                    'GPGlobal',
                    'GlobalSkillXP',
                    'HiddenSkillLevel',
                    'PotionChargesFlat',
                    'SkillXP',
                    'SummoningChargePreservation',
                ],
                // modifiers that only relate to combat and are not classified in a finer group
                combat: [
                    'AttackRolls',
                    'ChanceToDoubleLootCombat',
                    'DamageToAllMonsters',
                    'DamageToBosses',
                    'DamageToCombatAreaMonsters',
                    'DamageToDungeonMonsters',
                    'GPFromMonsters',
                    'GPFromMonstersFlat',
                    'GlobalAccuracy',
                    'MaxHitFlat',
                    'MaxHitPercent',
                    'MaxHitpoints',
                    'MinHitBasedOnMaxHit',
                    'MonsterRespawnTimer',
                    'AttackInterval',
                    'AttackIntervalPercent',
                    'ChanceToApplyBurn',
                    'GPOnEnemyHit',
                    'BleedLifesteal',
                    'BurnLifesteal',
                    'PoisonLifesteal',
                    'FlatMinHit',
                    'DamageTaken',
                    'GlobalEvasion',
                ],
                // modifiers that relate to healing
                hitpoints: [
                    'AutoEatEfficiency',
                    'AutoEatHPLimit',
                    'AutoEatThreshold',
                    'FoodHealingValue',
                    'HPRegenFlat',
                    'HitpointRegeneration',
                    'Lifesteal',
                    'FlatMaxHitpoints',
                ],
                // modifiers that relate to defence
                defence: [
                    'DamageReduction',
                    'MagicEvasion',
                    'MeleeEvasion',
                    'RangedEvasion',
                    'ReflectDamage',
                    'FlatReflectDamage',
                    'RolledReflectDamage',
                    'DamageReductionPercent',
                ],
                // modifiers that relate to using melee attacks
                attack: [],
                strength: [],
                melee: [
                    'MeleeAccuracyBonus',
                    'MeleeStrengthBonus',
                    'MeleeMaxHit',
                    'MeleeLifesteal',
                    'MeleeCritChance',
                ],
                // modifiers that relate to using ranged attacks
                ranged: [
                    'AmmoPreservation',
                    'RangedAccuracyBonus',
                    'RangedStrengthBonus',
                    'RangedMaxHit',
                    'RangedLifesteal',
                    'RangedCritChance',
                ],
                // modifiers that relate to using magic attacks
                magic: [
                    'MagicAccuracyBonus',
                    'MagicDamageBonus',
                    'MinAirSpellDmg',
                    'MaxAirSpellDmg',
                    'MinEarthSpellDmg',
                    'MaxEarthSpellDmg',
                    'MinFireSpellDmg',
                    'MaxFireSpellDmg',
                    'MinWaterSpellDmg',
                    'MaxWaterSpellDmg',
                    'RunePreservation',
                    'MagicMaxHit',
                    'MagicLifesteal',
                    'MagicCritChance',
                ],
                // modifiers that relate to slayer tasks, areas, or monsters
                slayer: [
                    'DamageToSlayerAreaMonsters',
                    'DamageToSlayerTasks',
                    'SlayerAreaEffectNegationFlat',
                    'SlayerCoins',
                    'SlayerTaskLength',
                ],
                // modifiers that relate to prayer
                prayer: [
                    'ChanceToPreservePrayerPoints',
                    'FlatPrayerCostReduction',
                    'PrayerCost',
                ],
                // modifiers that apply to general non-combat skilling
                nonCombat: [
                    'ChanceToDoubleItemsSkill',
                    'SkillInterval',
                    'SkillIntervalPercent',
                    'ChanceAdditionalSkillResource',
                ],
                production: [
                    'GlobalPreservationChance',
                    'SkillPreservationChance',
                ],
                mastery: [
                    'GlobalMasteryXP',
                    'MasteryXP',
                ],
                // specific skills
                agility: [
                    'GPFromAgility',
                    'AgilityObstacleCost',
                ],
                altMagic: [
                    'AltMagicSkillXP',
                    'AltMagicRunePreservation',
                    'RunePreservation',
                ],
                astrology: [],
                cooking: [
                    'ChanceSuccessfulCook',
                    'ChancePerfectCookGlobal',
                    'ChancePerfectCookFire',
                    'ChancePerfectCookFurnace',
                    'ChancePerfectCookPot',
                ],
                crafting: [],
                farming: [
                    'ChanceDoubleHarvest',
                    'FarmingYield',
                    'AllotmentSeedCost',
                ],
                firemaking: [
                    'ChanceForDiamondFiremaking',
                    'GPFromFiremaking',
                ],
                fishing: [
                    'FishingSpecialChance',
                ],
                fletching: [],
                herblore: [
                    'ChanceRandomPotionHerblore',
                ],
                mining: [
                    'ChanceNoDamageMining',
                    'ChanceToDoubleOres',
                    'MiningNodeHP',
                ],
                runecrafting: [
                    'ChanceForElementalRune',
                    'ElementalRuneGain',
                    'AdditionalRunecraftCountRunes',
                ],
                smithing: [
                    'SeeingGoldChance',
                ],
                summoning: [
                    'SummoningMaxHit',
                ],
                nonCBSummoning: [
                    'SummoningShardCost',
                    'SummoningCreationCharges',
                ],
                thieving: [
                    'ChanceToDoubleLootThieving',
                    'GPFromThieving',
                    'GPFromThievingFlat',
                    'ThievingStealth',
                    'MinThievingGP',
                ],
                woodcutting: [
                    'BirdNestDropRate',
                ],
                // golbin raid
                golbinRaid: [],
                // aprilFools
                aprilFools: [],
                // modifiers that are not actually implemented in the game
                unimplemented: [
                    'MaxStamina',
                    'StaminaCost',
                    'StaminaPerObstacle',
                    'StaminaPreservationChance',
                ],
            }

            // unique modifiers, i.e. not in+de creased
            this.singletonModifiers = {
                misc: [
                    'autoSlayerUnlocked',
                    'dungeonEquipmentSwapping',
                    'increasedEquipmentSets',
                ],
                skilling: [
                    'allowSignetDrops',
                    'increasedMasteryPoolProgress',
                ],
                combat: [
                    'meleeProtection',
                    'rangedProtection',
                    'magicProtection',
                    'meleeImmunity',
                    'rangedImmunity',
                    'magicImmunity',
                    'otherStyleImmunity',
                    'curseImmunity',
                    'stunImmunity',
                    'sleepImmunity',
                    'burnImmunity',
                    'poisonImmunity',
                    'bleedImmunity',
                    'debuffImmunity',
                    'slowImmunity',
                    'frostBurnImmunity',
                    'masteryToken',
                    'autoEquipFoodUnlocked',
                    'autoSwapFoodUnlocked',
                    'masteryToken',
                    'freeProtectItem',
                    'globalEvasionHPScaling',
                    'increasedRebirthChance',
                    'decreasedDragonBreathDamage',
                    'increasedMeleeStunThreshold',
                    'increasedChanceToIncreaseStunDuration',
                    'increasedRuneProvision',
                    'increasedChanceToConvertSeedDrops',
                    'increasedAfflictionChance',
                    'increasedChanceToApplyPoison',
                    'increasedChanceToApplyFrostburn',
                    'increasedEndOfTurnHealing2',
                    'increasedEndOfTurnHealing3',
                    'increasedEndOfTurnHealing5',
                    'increasedTotalBleedDamage',
                    'increasedOnHitSlowMagnitude',
                    'increasedNonMagicPoisonChance',
                    'increasedFrostburn',
                    'increasedPoisonReflectChance',
                    'increasedBleedReflectChance',
                    'bonusCoalOnDungeonCompletion',
                    'bypassSlayerItems',
                    'itemProtection',
                    'autoLooting',
                    'autoBurying',
                    'increasedGPMultiplierPer1MGP',
                    'increasedGPMultiplierCap',
                    'increasedGPMultiplierMin',
                    'allowAttackAugmentingMagic',
                    'summoningSynergy_0_1',
                    'summoningSynergy_0_6',
                    'summoningSynergy_0_7',
                    'summoningSynergy_0_8',
                    'summoningSynergy_0_12',
                    'summoningSynergy_0_13',
                    'summoningSynergy_0_14',
                    'summoningSynergy_0_15',
                    'summoningSynergy_1_2',
                    'summoningSynergy_1_8',
                    'summoningSynergy_1_12',
                    'summoningSynergy_1_13',
                    'summoningSynergy_1_14',
                    'summoningSynergy_1_15',
                    'summoningSynergy_2_13',
                    'summoningSynergy_2_15',
                    'summoningSynergy_6_13',
                    'summoningSynergy_7_13',
                    'summoningSynergy_8_13',
                    'summoningSynergy_12_13',
                    'summoningSynergy_12_14',
                    'summoningSynergy_13_14',
                    'increasedChanceToPreserveFood',
                    'allowLootContainerStacking',
                    'infiniteLootContainer',
                ],
                hitpoints: [
                    'decreasedRegenerationInterval',
                ],
                defence: [],
                attack: [],
                strength: [],
                melee: [
                    'increasedMeleeStunChance',
                    'summoningSynergy_6_7',
                    'summoningSynergy_6_12',
                    'summoningSynergy_6_14',
                    'summoningSynergy_6_15',
                ],
                ranged: [
                    'summoningSynergy_7_8',
                    'summoningSynergy_7_12',
                    'summoningSynergy_7_14',
                    'summoningSynergy_7_15',
                ],
                magic: [
                    'increasedConfusion',
                    'increasedDecay',
                    'summoningSynergy_6_8',
                    'summoningSynergy_8_14',
                    'increasedMinNatureSpellDamageBasedOnMaxHit',
                    'increasedSurgeSpellAccuracy',
                    'increasedSurgeSpellMaxHit',
                    'increasedElementalEffectChance',
                ],
                slayer: [
                    'summoningSynergy_2_12',
                    'summoningSynergy_8_12',
                ],
                prayer: [
                    'increasedRedemptionPercent',
                    'increasedRedemptionThreshold',
                ],
                nonCombat: [
                    'increasedOffItemChance',
                    'doubleItemsSkill',
                ],
                production: [],
                mastery: [],
                // specific skills
                agility: [],
                altMagic: [],
                astrology: [
                    'increasedBaseStardustDropQty',
                ],
                cooking: [
                    'decreasedSecondaryFoodBurnChance',
                    'summoningSynergy_3_9',
                    'summoningSynergy_4_9',
                    'summoningSynergy_9_17',
                    'summoningSynergy_9_18',
                    'summoningSynergy_9_19',
                ],
                crafting: [
                    'summoningSynergy_5_16',
                    'summoningSynergy_9_16',
                    'summoningSynergy_10_16',
                    'summoningSynergy_16_17',
                    'summoningSynergy_16_18',],
                farming: [
                    'freeCompost',
                    'increasedCompostPreservationChance',
                ],
                firemaking: [
                    'freeBonfires',
                    'increasedFiremakingCoalChance',
                    'summoningSynergy_3_19',
                    'summoningSynergy_4_19',
                    'summoningSynergy_9_19',
                    'summoningSynergy_16_19',
                    'summoningSynergy_18_19',
                ],
                fishing: [
                    'summoningSynergy_3_5',
                    'summoningSynergy_4_5',
                    'summoningSynergy_5_9',
                    'summoningSynergy_5_18',],
                fletching: [],
                herblore: [],
                mining: [
                    'increasedMiningGemChance',
                    'doubleOresMining',
                    'increasedBonusCoalMining',
                    'summoningSynergy_4_5',
                    'summoningSynergy_4_10',
                    'summoningSynergy_4_16',
                    'summoningSynergy_4_17',
                    'summoningSynergy_4_18',
                ],
                runecrafting: [
                    'increasedRunecraftingEssencePreservation',
                    'summoningSynergy_3_10',
                    'summoningSynergy_5_10',
                    'summoningSynergy_10_17',
                    'summoningSynergy_10_18',
                    'summoningSynergy_10_19',],
                smithing: [
                    'decreasedSmithingCoalCost',
                    'summoningSynergy_5_17',
                    'summoningSynergy_9_17',
                    'summoningSynergy_10_17',
                    'summoningSynergy_17_18',
                    'summoningSynergy_17_19',
                ],
                summoning: [],
                nonCBSummoning: [],
                thieving: [
                    'increasedThievingSuccessRate',
                    'increasedThievingSuccessCap',
                    'summoningSynergy_3_11',
                    'summoningSynergy_4_11',
                    'summoningSynergy_5_11',
                    'summoningSynergy_9_11',
                    'summoningSynergy_10_11',
                    'summoningSynergy_11_16',
                    'summoningSynergy_11_17',
                    'summoningSynergy_11_18',
                    'summoningSynergy_11_19',
                ],
                woodcutting: [
                    'increasedTreeCutLimit',
                    'summoningSynergy_3_4',
                    'summoningSynergy_3_16',
                    'summoningSynergy_3_17',
                    'summoningSynergy_3_18',
                    'summoningSynergy_3_19',
                ],
                // golbin raid modifiers
                golbinRaid: [
                    'golbinRaidIncreasedMaximumAmmo',
                    'golbinRaidIncreasedMaximumRunes',
                    'golbinRaidIncreasedMinimumFood',
                    'golbinRaidIncreasedPrayerLevel',
                    'golbinRaidIncreasedPrayerPointsStart',
                    'golbinRaidIncreasedPrayerPointsWave',
                    'golbinRaidIncreasedStartingRuneCount',
                    'golbinRaidPassiveSlotUnlocked',
                    'golbinRaidPrayerUnlocked',
                    'golbinRaidStartingWeapon',
                    'golbinRaidWaveSkipCostReduction',
                ],
                // chaos mode modifiers
                aprilFools: [
                    'aprilFoolsIncreasedMovementSpeed',
                    'aprilFoolsDecreasedMovementSpeed',
                    'aprilFoolsIncreasedTeleportCost',
                    'aprilFoolsDecreasedTeleportCost',
                    'aprilFoolsIncreasedUpdateDelay',
                    'aprilFoolsDecreasedUpdateDelay',
                    'aprilFoolsIncreasedLemonGang',
                    'aprilFoolsDecreasedLemonGang',
                    'aprilFoolsIncreasedCarrotGang',
                    'aprilFoolsDecreasedCarrotGang',
                ],
                unimplemented: [],
            }

            if (check) {
                this.checkUnknownModifiers();
            }

            // map of relevant modifiers per tag
            this.relevantModifiers = {};

            // all
            this.relevantModifiers.all = this.getModifierNames(
                Object.getOwnPropertyNames(this.creasedModifiers),
                Object.getOwnPropertyNames(SKILLS).map(x => Number(x)),
            );

            // misc
            this.relevantModifiers.misc = this.getModifierNames(['misc'], []);

            // golbin raid
            this.relevantModifiers.golbin = this.getModifierNames(['golbinRaid'], []);

            // all combat
            this.relevantModifiers.combat = this.getModifierNames(
                [
                    'skilling',
                    'combat',
                ],
                [
                    Skills.Attack,
                    Skills.Strength,
                    Skills.Ranged,
                    Skills.Magic,
                    Skills.Defence,
                    Skills.Hitpoints,
                    Skills.Prayer,
                    Skills.Slayer,
                    Skills.Summoning,
                ],
            );

            // melee combat
            this.relevantModifiers.melee = this.getModifierNames(
                [
                    'skilling',
                    'combat',
                ],
                [
                    Skills.Attack,
                    Skills.Strength,
                    Skills.Defence,
                    Skills.Hitpoints,
                    Skills.Prayer,
                    Skills.Slayer,
                    Skills.Summoning,
                ],
            );

            // ranged combat
            this.relevantModifiers.ranged = this.getModifierNames(
                [
                    'skilling',
                    'combat',
                ],
                [
                    Skills.Ranged,
                    Skills.Defence,
                    Skills.Hitpoints,
                    Skills.Prayer,
                    Skills.Slayer,
                    Skills.Summoning,
                ],
            );

            // magic combat
            this.relevantModifiers.magic = this.getModifierNames(
                [
                    'skilling',
                    'combat',
                    'hitpoints',
                ],
                [
                    Skills.Magic,
                    Skills.Defence,
                    Skills.Hitpoints,
                    Skills.Prayer,
                    Skills.Slayer,
                    Skills.Summoning,
                ],
            );

            // slayer
            this.relevantModifiers.slayer = this.getModifierNames(
                [
                    'skilling',
                ],
                [
                    Skills.Slayer,
                ],
            );

            // gathering skills
            this.gatheringSkills = ['Woodcutting', 'Fishing', 'Mining', 'Thieving', 'Farming', 'Agility', 'Astrology'];
            this.gatheringSkills.forEach(name => {
                this.relevantModifiers[name] = this.getModifierNames(
                    [
                        'skilling',
                        'nonCombat',
                        'mastery',
                    ],
                    [
                        Skills[name]
                    ],
                );
                const lname = name.toLowerCase();
                if (this.creasedModifiers[lname] !== undefined) {
                    this.relevantModifiers[name].names.push(this.creasedModifiers[lname]);
                }
                if (this.singletonModifiers[lname] !== undefined) {
                    this.relevantModifiers[name].names.push(this.singletonModifiers[lname]);
                }
            });

            // production skills
            this.productionSkills = ['Firemaking', 'Cooking', 'Smithing', 'Fletching', 'Crafting', 'Runecrafting', 'Herblore', 'Summoning'];
            this.productionSkills.forEach(name => {
                const setNames = [
                    'skilling',
                    'nonCombat',
                    'production',
                    'mastery',
                ];
                if (name === 'Summoning') {
                    setNames.push('nonCBSummoning');
                }
                this.relevantModifiers[name] = this.getModifierNames(
                    setNames,
                    [
                        Skills[name]
                    ],
                );
            });

            // whatever alt magic is
            this.relevantModifiers.altMagic = this.getModifierNames(
                [
                    'skilling',
                    'nonCombat',
                    'altMagic',
                ],
                [],
            );

            // golbin raid
            this.relevantModifiers.golbinRaid = this.getModifierNames(
                ['golbinRaid'],
                [],
            );
        }

        log(...args) {
            console.log(`${this.logName}:`, ...args);
        }

        checkUnknownModifiers() {
            // list of known modifiers
            this.knownModifiers = {};
            for (const subset in this.creasedModifiers) {
                this.creasedModifiers[subset].forEach(modifier => {
                    this.knownModifiers[`increased${modifier}`] = true;
                    this.knownModifiers[`decreased${modifier}`] = true;
                });
            }
            for (const subset in this.singletonModifiers) {
                this.singletonModifiers[subset].forEach(modifier => {
                    this.knownModifiers[modifier] = true;
                });
            }

            // check for unknown modifiers
            const modifierNames = [
                ...Object.getOwnPropertyNames(player.modifiers),
                // player.modifiers.skillModifiers
                ...Object.getOwnPropertyNames(modifierData).filter(x => modifierData[x].isSkill),
            ];
            let hasUnknownModifiers = false;
            modifierNames.forEach(modifier => {
                if (modifier === 'skillModifiers') {
                    return;
                }
                if (this.knownModifiers[modifier]) {
                    return;
                }
                hasUnknownModifiers = true;
                this.log(`unknown modifier ${modifier}`);
            });
            if (!hasUnknownModifiers) {
                this.log('no unknown modifiers detected!')
            }

            // check for non-existent modifiers
            let hasNonExistentModifiers = false;
            for (const modifier in this.knownModifiers) {
                if (!modifierNames.includes(modifier)) {
                    hasNonExistentModifiers = true;
                    this.log(`non-existent modifier ${modifier}`);
                }
            }
            if (!hasNonExistentModifiers) {
                this.log('no non-existent modifiers detected!')
            }
        }

        getModifierNames(setNames, skillIDs) {
            // add skill based on skillID
            skillIDs.forEach(id => {
                if (!setNames.includes(Skills[id])) {
                    setNames.push(Skills[id].toLowerCase());
                }
            });
            // add melee based on att/str skillID
            if (skillIDs.includes(Skills.Attack) || skillIDs.includes(Skills.Strength)) {
                if (!setNames.includes('melee')) {
                    setNames.push('melee');
                }
            }

            // gather modifiers
            return {
                names: [...new Set([
                    ...setNames.map(name => this.creasedModifiers[name]).reduce((a, x) => [...a, ...x], []),
                    ...setNames.map(name => this.singletonModifiers[name]).reduce((a, x) => [...a, ...x], []),
                ])],
                skillIDs: skillIDs,
            };
        }

        printUniqueModifier(modifier, value, skillID) {
            if (!value) {
                return [];
            }
            // convert to array if required
            const valueToPrint = skillID !== undefined ? [skillID, value] : value;
            return [printPlayerModifier(modifier, valueToPrint)];
        }

        printDiffModifier(modifier, value, skillID = undefined) {
            // compute difference
            if (!value) {
                return [];
            }
            // store if value is positive or negative
            const positive = value > 0;
            // take absolute value
            let valueToPrint = positive ? value : -value;
            // convert to array if required
            valueToPrint = skillID !== undefined ? [skillID, valueToPrint] : valueToPrint;
            // print increased or decreased
            if (positive) {
                return [printPlayerModifier('increased' + modifier, valueToPrint)];
            }
            return [printPlayerModifier('decreased' + modifier, valueToPrint)];
        }

        getModifierValue(modifiers, modifier, skillID = undefined) {
            if (!this.isSkillModifier(modifier)) {
                return this.getSimpleModifier(modifiers, modifier);
            }
            return this.getSkillModifier(modifiers, modifier, skillID);
        }

        getSimpleModifier(modifiers, modifier) {
            // unique
            if (this.isUniqueModifier(modifier)) {
                return modifiers[modifier];
            }
            // creased
            let increased = modifiers['increased' + modifier];
            if (!increased) {
                increased = 0;
            }
            let decreased = modifiers['decreased' + modifier];
            if (!decreased) {
                decreased = 0;
            }
            return increased - decreased;
        }

        getSkillModifier(modifiers, modifier, skillID) {
            const skillModifiers = modifiers.skillModifiers ? modifiers.skillModifiers : modifiers;
            // unique
            if (this.isUniqueModifier(modifier)) {
                const map = this.skillModifierMapAux(skillModifiers, modifier);
                return this.skillModifierAux(map, skillID);
            }
            // creased
            const increased = this.skillModifierMapAux(skillModifiers, 'increased' + modifier);
            const decreased = this.skillModifierMapAux(skillModifiers, 'decreased' + modifier);
            return this.skillModifierAux(increased, skillID) - this.skillModifierAux(decreased, skillID);
        }

        skillModifierMapAux(map, skillID) {
            if (!map) {
                return [];
            }
            let tmp;
            if (map.constructor.name === 'Map') {
                tmp = map.get(skillID);
            } else {
                tmp = map[skillID];
            }
            return tmp ? tmp : [];
        }

        skillModifierAux(map, skillID) {
            if (!map || map.length === 0) {
                return 0;
            }
            if (map.constructor.name === 'Map') {
                const value = map.get(skillID);
                if (!value) {
                    return 0
                }
                return value;
            }
            return map.filter(x => x[0] === skillID)
                .map(x => x[1])
                .reduce((a, b) => a + b, 0);
        }

        printModifier(modifiers, modifier, skillIDs) {
            if (!this.isSkillModifier(modifier)) {
                const value = this.getSimpleModifier(modifiers, modifier);
                if (this.isUniqueModifier(modifier)) {
                    // unique
                    return this.printUniqueModifier(modifier, value);
                }
                // creased
                return this.printDiffModifier(modifier, value);
            }
            // skillModifiers
            return skillIDs.map(skillID => {
                const value = this.getSkillModifier(modifiers, modifier, skillID);
                if (this.isUniqueModifier(modifier)) {
                    // unique
                    return this.printUniqueModifier(modifier, value, skillID);
                }
                // creased
                return this.printDiffModifier(modifier, value, skillID);
            }).reduce((a, b) => a.concat(b), []);
        }

        isUniqueModifier(modifier) {
            return modifierData[modifier] !== undefined;
        }

        isSkillModifier(modifier) {
            if (this.isUniqueModifier(modifier)) {
                return modifierData[modifier].isSkill;
            }
            const data = modifierData['increased' + modifier];
            if (data === undefined) {
                // this.log(`Unknown modifier ${modifier}`);
                return false;
            }
            return data.isSkill;
        }

        printRelevantModifiers(modifiers, tag) {
            const relevantNames = this.relevantModifiers[tag].names;
            const skillIDs = this.relevantModifiers[tag].skillIDs;
            const toPrint = [];
            relevantNames.forEach(name => {
                this.printModifier(modifiers, name, skillIDs).forEach(result => toPrint.push(result));
            });
            return toPrint;
        }

        makeTagButton(tag, text, icon) {
            return '<div class="dropdown d-inline-block ml-2">'
                + '<button type="button" '
                + 'class="btn btn-sm btn-dual text-combat-smoke" '
                + 'id="page-header-modifiers" '
                + `onclick="window.${this.name}.replaceRelevantModifiersHtml(window.${this.name}.getModifiers(), '${text}', '${tag}');" `
                + 'aria-haspopup="true" '
                + 'aria-expanded="true">'
                + `<img class="skill-icon-xxs" src="${icon}">`
                + '</button>'
                + '</div>';
        }

        replaceRelevantModifiersHtml(modifiers, text, tag) {
            $('#show-modifiers').replaceWith(this.printRelevantModifiersHtml(modifiers, text, tag));
        }

        printRelevantModifiersHtml(modifiers, text, tag, id = 'show-modifiers') {
            let passives = `<div id="${id}"><br/>`;
            passives += `<h5 class=\"font-w400 font-size-sm mb-1\">${text}</h5><br/>`;
            this.printRelevantModifiers(modifiers, tag).forEach(toPrint => {
                passives += `<h5 class=\"font-w400 font-size-sm mb-1 ${toPrint[1]}\">${toPrint[0]}</h5>`;
            });
            passives += '</div>';
            return passives;
        }

        showRelevantModifiers(modifiers, text, tag = 'all') {
            let passives = `<h5 class=\"font-w600 font-size-sm mb-1 text-combat-smoke\">${text}</h5><h5 class=\"font-w600 font-size-sm mb-3 text-warning\"></h5>`;
            passives += `<h5 class="font-w600 font-size-sm mb-3 text-warning"><small>(Does not include non-modifier effects)</small></h5>`;
            passives += this.makeTagButton('all', 'All Modifiers', 'assets/media/main/completion_log.svg');
            passives += this.makeTagButton('golbinRaid', 'Golbin Raid', 'assets/media/main/raid_coins.svg');
            passives += this.makeTagButton('combat', 'Combat', 'assets/media/skills/combat/combat.svg');
            passives += this.makeTagButton('melee', 'Melee', 'assets/media/skills/attack/attack.svg');
            passives += this.makeTagButton('ranged', 'Ranged', 'assets/media/skills/ranged/ranged.svg');
            passives += this.makeTagButton('magic', 'Combat Magic', 'assets/media/skills/combat/spellbook.svg');
            passives += this.makeTagButton('slayer', 'Slayer', 'assets/media/skills/slayer/slayer.svg');
            passives += '<br/>';
            this.gatheringSkills.forEach(skill => passives += this.makeTagButton(skill, skill, `assets/media/skills/${skill.toLowerCase()}/${skill.toLowerCase()}.svg`));
            passives += '<br/>';
            this.productionSkills.forEach(skill => passives += this.makeTagButton(skill, skill, `assets/media/skills/${skill.toLowerCase()}/${skill.toLowerCase()}.svg`));
            passives += this.makeTagButton('altMagic', 'Alt. Magic', 'assets/media/skills/magic/magic.svg');
            passives += this.printRelevantModifiersHtml(modifiers, 'All Modifiers', tag);
            Swal.fire({
                html: passives,
            });
        }

        getModifiers() {
            return game.isGolbinRaid ? game.golbinRaid.player.modifiers : player.modifiers;
        }
    }

    function startShowModifiers() {
        const name = 'melvorShowModifiers';
        window[name] = new ShowModifiers(name, 'Show Modifiers');
        let modifierButton = () => {
            return '<div class="dropdown d-inline-block ml-2">'
                + '<button type="button" '
                + 'class="btn btn-sm btn-dual text-combat-smoke" '
                + 'id="page-header-modifiers" '
                + `onclick="window.${name}.showRelevantModifiers(window.${name}.getModifiers(), \'Active Modifiers\');" `
                + 'aria-haspopup="true" '
                + 'aria-expanded="true">'
                + `<img class="skill-icon-xxs" src="${getItemMedia(Items.Event_Clue_1)}">`
                + '</button>'
                + '</div>';
        }

        let node = document.getElementById('page-header-potions-dropdown').parentNode;
        node.parentNode.insertBefore($(modifierButton().trim())[0], node);
    }

    function loadScript() {
        if (typeof confirmedLoaded !== typeof undefined && confirmedLoaded) {
            // Only load script after game has opened
            clearInterval(scriptLoader);
            startShowModifiers();
        }
    }

    const scriptLoader = setInterval(loadScript, 200);
});