// ==UserScript==
// @name        Live Percentile Ranges
// @namespace   E-Hentai
// @author      Superlatanium
// @version     1.1.0.s3
// @grant       none
// @match       *://*.hentaiverse.org/pages/showequip.php*
// @match       *://*.hentaiverse.org/equip/*
// @match       *://*.hentaiverse.org/isekai/equip/*
// ==/UserScript==
'use strict';

/*
How it works:

When Jenga's server on hvitems.niblseed.com gets sent an equipment link, its stats get parsed and it gets saved there.
The equipment ranges on the wiki are generated by searching through every equipment there and identifying the min/max number possible for each stat.
This script lets your browser send Superlatanium's server (at reasoningtheory.net) an equip type (such as Legendary Phase Robe),
and his server will then go through the Jenga's database to find the min/max for every stat. It takes a while to search through millions of equips to figure out a range.
The first time reasoningtheory is asked for an equip, it might take a minute or so for the results to come back (assuming there's no one else in the request queue).
But after that, the min and max for every stat will be stored on reasoningtheory directly, so further requests for that equip type will get responses very quickly.
You can see the current database here: https://reasoningtheory.net/viewranges

When your browser gets a range back, it saves it in your own local database, so server requests are only made if you don't already have the range info for an equip type.
Ranges on reasoningtheory and on your local database both expire after 2 weeks: after that, they both have to ask for fresh range data.
This ensures that the percentiles you get are up-to-date.

How to use:

Q to show Unforged Percentiles
W to show Forged Percentiles
E to show Absolute Percentiles
A to show Unforged Base
S to show Forged Base
D to show Fully Forged Base
Z to show Forged Scaled to your level
X to show Forged Scaled to a custom level
C to show Fully Forged Scaled to a custom level
F to show Forged Scaled to the equip's current level (the default display style of hentaiverse.org)
L to show equip's link for the forum ( [url=... )

If you want to have buttons for these functions rather than just keybinds, set mobile to true below.

You can change what summary information is displayed via "shortSummaryStatsConfig" and "extendedSummaryStatsConfig" below
On an equip page, if you want to see what your equip stats look like through the mins and maxes of a different quality, click the "Display Style" line and choose the quality you want
If you see an unforged and un-IWd equip with percentiles lower than 0 or higher than 100, update the server's database by clicking the "Display Style" line and then clicking "Send Ranges"
Clear the database on your own computer by clicking the "Display Style" line and clicking "Clear Local Database"

If you want to integrate Percentile Ranges into your own script, see the example and the comments at the very bottom:
**this script was designed to be modular**.
*/

const mobile = false;

//Set debug to false to disable PercentileRanges's console.log messages:
const debug = false;

//updateCheckDays is the number of days to check for updates. Set this to 0 to disable.
const updateCheckDays = 14;









//Some lmax of the server data are incorrect, so overwrite them with the correct values.
const manualRanges = {

  "Oak": {
    "Legendary": {
      "Magic Damage": {
        "all | all": {"max": 31.98}
      },
      "Holy EDB": {
        "Hallowed | Heimdall": {"max": 42.68}
      },
      "Elemental": {
        "all | all": {"max": 6.45}
      },
      "Divine": {
        "all | all": {"max": 6.45}
      },
      "Supportive": {
        "all | not!Earth-walker": {"max": 11.81}
      },
      "Counter-Resist": {
        "all | all": {"max": 13.58}
      },
      "Intelligence": {
        "all | all": {"max": 4.82}
      },
      "Wisdom": {
        "all | all": {"max": 7.22}
      }
    }
  },

  "Willow": {
    "Legendary": {
      "Magic Damage": {
        "all | Destruction": {"max": 51.71}
      },
      "Fire EDB": {
        "Fiery | all": {"max": 11.32}
      },
      "Cold EDB": {
        "Arctic | all": {"max": 11.32}
      },
      "Elec EDB": {
        "Shocking | all": {"max": 18.56}
      },
      "Wind EDB": {
        "Tempestuous | all": {"max": 18.56}
      },
      "Dark EDB": {
        "Demonic | all": {"max": 26.6}
      },
      "Elemental": {
        "all | all": {"max": 6.14}
      },
      "Forbidden": {
        "all | all": {"max": 6.14}
      },
      "Deprecating": {
        "all | not!Curse-weaver": {"max": 11.81}
      },
      "Counter-Resist": {
        "all | all": {"max": 13.58}
      },
      "Intelligence": {
        "all | all": {"max": 4.82}
      },
      "Wisdom": {
        "all | all": {"max": 7.22}
      }
    }
  },

  "Katalox": {
    "Legendary": {
      "Magic Damage": {
        "all | Destruction": {"max": 52.2},
        "all | not!Destruction": {"max": 32.39}
      },
      "Holy EDB": {
        "Hallowed | Heimdall": {"max": 37.84},
        "Hallowed | not!Heimdall": {"max": 21.76}
      },
      "Dark EDB": {
        "Demonic | Fenrir": {"max": 37.84},
        "Demonic | not!Fenrir": {"max": 21.76}
      },
      "Divine": {
        "all | Heaven-sent": {"max": 16.24},
        "all | not!Heaven-sent": {"max": 8.28}
      },
      "Forbidden": {
        "all | Demon-fiend": {"max": 16.24},
        "all | not!Demon-fiend": {"max": 8.28}
      },
      "Deprecating": {
        "all | all": {"max": 6.14}
      },
      "Intelligence": {
        "all | all": {"max": 7.22}
      },
      "Wisdom": {
        "all | all": {"max": 4.82}
      }
    }
  },

  "Redwood": {
    "Legendary": {
      "Magic Damage": {
        "all | Destruction": {"max": 51.71},
        "all | not!Destruction": {"max": 31.98}
      },
      "Fire EDB": {
        "Fiery | Surtr": {"max": 37.85},
        "Fiery | not!Surtr": {"max": 21.77}
      },
      "Cold EDB": {
        "Arctic | Niflheim": {"max": 37.85},
        "Arctic | not!Niflheim": {"max": 21.77}
      },
      "Elec EDB": {
        "Shocking | Mjolnir": {"max": 37.85},
        "Shocking | not!Mjolnir": {"max": 21.77}
      },
      "Wind EDB": {
        "Tempestuous | Freyr": {"max": 37.85},
        "Tempestuous | not!Freyr": {"max": 21.77}
      },
      "Elemental": {
        "all | Elementalist": {"max": 16.24},
        "all | not!Elementalist": {"max": 8.29}
      },
      "Supportive": {
        "all | all": {"max": 4.31}
      },
      "Deprecating": {
        "all | all": {"max": 4.31}
      },
      "Intelligence": {
        "all | all": {"max": 6.32}
      },
      "Wisdom": {
        "all | all": {"max": 6.32}
      }
    }
  },

  "Phase": {

    "Cap": {
      "Legendary": {
        "Magic Damage": {
          "Radiant | all": {"max": 4.23}
        },
        "Spell Crit Damage": {
          "Mystic | all": {"max": 3.91}
        },
        "Casting Speed": {
          "Charged | all": {"max": 3.47}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 3.61}
        },
        "Fire EDB": {
          "all | Surtr": {"max": 16.97}
        },
        "Cold EDB": {
          "all | Niflheim": {"max": 16.97}
        },
        "Elec EDB": {
          "all | Mjolnir": {"max": 16.97}
        },
        "Wind EDB": {
          "all | Freyr": {"max": 16.97}
        },
        "Holy EDB": {
          "all | Heimdall": {"max": 16.97}
        },
        "Dark EDB": {
          "all | Fenrir": {"max": 16.97}
        }
      }
    },

    "Robe": {
      "Legendary": {
        "Magic Damage": {
          "Radiant | all": {"max": 4.9}
        },
        "Spell Crit Damage": {
          "Mystic | all": {"max": 4.67}
        },
        "Casting Speed": {
          "Charged | all": {"max": 4.06}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 4.11}
        },
        "Fire EDB": {
          "all | Surtr": {"max": 20.18}
        },
        "Cold EDB": {
          "all | Niflheim": {"max": 20.18}
        },
        "Elec EDB": {
          "all | Mjolnir": {"max": 20.18}
        },
        "Wind EDB": {
          "all | Freyr": {"max": 20.18}
        },
        "Holy EDB": {
          "all | Heimdall": {"max": 20.18}
        },
        "Dark EDB": {
          "all | Fenrir": {"max": 20.18}
        }
      }
    },

    "Gloves": {
      "Legendary": {
        "Magic Damage": {
          "Radiant | all": {"max": 3.9}
        },
        "Spell Crit Damage": {
          "Mystic | all": {"max": 3.53}
        },
        "Casting Speed": {
          "Charged | all": {"max": 3.18}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 3.41}
        },
        "Fire EDB": {
          "all | Surtr": {"max": 15.36}
        },
        "Cold EDB": {
          "all | Niflheim": {"max": 15.36}
        },
        "Elec EDB": {
          "all | Mjolnir": {"max": 15.36}
        },
        "Wind EDB": {
          "all | Freyr": {"max": 15.36}
        },
        "Holy EDB": {
          "all | Heimdall": {"max": 15.36}
        },
        "Dark EDB": {
          "all | Fenrir": {"max": 15.36}
        }
      }
    },

    "Pants": {
      "Legendary": {
        "Magic Damage": {
          "Radiant | all": {"max": 4.56}
        },
        "Spell Crit Damage": {
          "Mystic | all": {"max": 4.29}
        },
        "Casting Speed": {
          "Charged | all": {"max": 3.77}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 3.91}
        },
        "Fire EDB": {
          "all | Surtr": {"max": 18.58}
        },
        "Cold EDB": {
          "all | Niflheim": {"max": 18.58}
        },
        "Elec EDB": {
          "all | Mjolnir": {"max": 18.58}
        },
        "Wind EDB": {
          "all | Freyr": {"max": 18.58}
        },
        "Holy EDB": {
          "all | Heimdall": {"max": 18.58}
        },
        "Dark EDB": {
          "all | Fenrir": {"max": 18.58}
        }
      }
    },

    "Shoes": {
      "Legendary": {
        "Magic Damage": {
          "Radiant | all": {"max": 3.57}
        },
        "Spell Crit Damage": {
          "Mystic | all": {"max": 3.15}
        },
        "Casting Speed": {
          "Charged | all": {"max": 2.89}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 3.11}
        },
        "Fire EDB": {
          "all | Surtr": {"max": 13.75}
        },
        "Cold EDB": {
          "all | Niflheim": {"max": 13.75}
        },
        "Elec EDB": {
          "all | Mjolnir": {"max": 13.75}
        },
        "Wind EDB": {
          "all | Freyr": {"max": 13.75}
        },
        "Holy EDB": {
          "all | Heimdall": {"max": 13.75}
        },
        "Dark EDB": {
          "all | Fenrir": {"max": 13.75}
        }
      }
    }

  },

  "Cotton": {

    "Cap": {
      "Legendary": {
        "Casting Speed": {
          "Charged | all": {"max": 3.47}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 3.61}
        },
        "Elemental": {
          "all | Elementalist": {"max": 8.29}
        },
        "Divine": {
          "all | Heaven-sent": {"max": 8.29}
        },
        "Forbidden": {
          "all | Demon-fiend": {"max": 8.29}
        },
        "Supportive": {
          "all | Earth-walker": {"max": 8.29}
        },
        "Deprecating": {
          "all | Curse-weaver": {"max": 8.29}
        }
      }
    },

    "Robe": {
      "Legendary": {
        "Casting Speed": {
          "Charged | all": {"max": 4.06}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 4.11}
        },
        "Elemental": {
          "all | Elementalist": {"max": 9.89}
        },
        "Divine": {
          "all | Heaven-sent": {"max": 9.89}
        },
        "Forbidden": {
          "all | Demon-fiend": {"max": 9.89}
        },
        "Supportive": {
          "all | Earth-walker": {"max": 9.89}
        },
        "Deprecating": {
          "all | Curse-weaver": {"max": 9.89}
        }
      }
    },

    "Gloves": {
      "Legendary": {
        "Casting Speed": {
          "Charged | all": {"max": 3.18}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 3.41}
        },
        "Elemental": {
          "all | Elementalist": {"max": 7.5}
        },
        "Divine": {
          "all | Heaven-sent": {"max": 7.5}
        },
        "Forbidden": {
          "all | Demon-fiend": {"max": 7.5}
        },
        "Supportive": {
          "all | Earth-walker": {"max": 7.5}
        },
        "Deprecating": {
          "all | Curse-weaver": {"max": 7.5}
        }
      }
    },

    "Pants": {
      "Legendary": {
        "Casting Speed": {
          "Charged | all": {"max": 3.77}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 3.91}
        },
        "Elemental": {
          "all | Elementalist": {"max": 9.09}
        },
        "Divine": {
          "all | Heaven-sent": {"max": 9.09}
        },
        "Forbidden": {
          "all | Demon-fiend": {"max": 9.09}
        },
        "Supportive": {
          "all | Earth-walker": {"max": 9.09}
        },
        "Deprecating": {
          "all | Curse-weaver": {"max": 9.09}
        }
      }
    },

    "Shoes": {
      "Legendary": {
        "Casting Speed": {
          "Charged | all": {"max": 2.89}
        },
        "Mana Conservation": {
          "Frugal | all": {"max": 3.11}
        },
        "Elemental": {
          "all | Elementalist": {"max": 6.7}
        },
        "Divine": {
          "all | Heaven-sent": {"max": 6.7}
        },
        "Forbidden": {
          "all | Demon-fiend": {"max": 6.7}
        },
        "Supportive": {
          "all | Earth-walker": {"max": 6.7}
        },
        "Deprecating": {
          "all | Curse-weaver": {"max": 6.7}
        }
      }
    }

  }

};

const PercentileRanges = (() => {
  function debuglog(str){
    if (!debug)
      return;
    const colonSplits = (new Error()).stack.split('\n')[1].split(':');
    console.log(colonSplits[colonSplits.length - 2] + ':' + colonSplits[colonSplits.length - 1] + ' :: ' + str);
  }

  const shortSummaryStatsConfig = [
    //1h
    [[/axe/, /club/, /rapier/, /shortsword/, /wakizashi/, /estoc/, /longsword/, /mace/, /katana/], ['ADB']],

    //Staff
    [[/staff/], ['MDB']],
    [[/hallowed.+staff/], ['Holy EDB']],
    [[/demonic.+staff/], ['Dark EDB']],
    [[/tempestuous.+staff/], ['Wind EDB']],
    [[/shocking.+staff/], ['Elec EDB']],
    [[/arctic.+staff/], ['Cold EDB']],
    [[/fiery.+staff/], ['Fire EDB']],

    //Shield
    [[/\sshield\s/, /buckler/], ['BLK']],

    //Cloth
    [[/cotton.+heaven-sent/], ['Divine Prof']],
    [[/cotton.+demon-fiend/], ['Forb Prof']],
    [[/cotton.+elementalist/], ['Elem Prof']],
    [[/cotton.+curse-weaver/], ['Depr Prof']],
    [[/cotton.+earth-walker/], ['Sup Prof']],
    [[/phase.+heimdall/], ['Holy EDB']],
    [[/phase.+fenrir/], ['Dark EDB']],
    [[/phase.+freyr/], ['Wind EDB']],
    [[/phase.+mjolnir/], ['Elec EDB']],
    [[/phase.+niflheim/], ['Cold EDB']],
    [[/phase.+surtr/], ['Fire EDB']],

    //Light
    [[/shade/], ['ADB']],

    //Heavy
    [[/power/], ['ADB']],
  ].map(line => ({'nameTests': line[0], 'statsToShow': line[1]}));


  const extendedSummaryStatsConfig = [
    //1h
    [[/axe/, /club/], ['ADB', 'Str', 'Dex', 'Agi']],
    [[/rapier/, /shortsword/, /wakizashi/], ['ADB', 'Parry', 'Str', 'Dex', 'Agi']],

    //2h
    [[/estoc/, /longsword/, /mace/, /katana/], ['ADB', 'Str', 'Dex', 'Agi']],

    //Staff
    [[/staff/], ['MDB', 'Int', 'Wis']],
    [[/hallowed.+staff/], ['Holy EDB', 'Divine Prof']],
    [[/demonic.+staff/], ['Dark EDB', 'Forb Prof']],
    [[/tempestuous.+staff/], ['Wind EDB', 'Elem Prof']],
    [[/shocking.+staff/], ['Elec EDB', 'Elem Prof']],
    [[/arctic.+staff/], ['Cold EDB', 'Elem Prof']],
    [[/fiery.+staff/], ['Fire EDB', 'Elem Prof']],
    [[/staff/], ['Depr Prof', 'CR', 'Burden']],

    //Shield
    [[/\sshield\s/, /buckler/], ['BLK', 'Pmit', 'Mmit', 'Str', 'Dex', 'End', 'Agi', 'Crus', 'Slas', 'Pier']],

    //Cloth
    [[/cotton.+heaven-sent/], ['Divine Prof']],
    [[/cotton.+demon-fiend/], ['Forb Prof']],
    [[/cotton.+elementalist/], ['Elem Prof']],
    [[/cotton.+curse-weaver/], ['Depr Prof']],
    [[/cotton.+earth-walker/], ['Sup Prof']],
    [[/phase.+heimdall/], ['Holy EDB']],
    [[/phase.+fenrir/], ['Dark EDB']],
    [[/phase.+freyr/], ['Wind EDB']],
    [[/phase.+mjolnir/], ['Elec EDB']],
    [[/phase.+niflheim/], ['Cold EDB']],
    [[/phase.+surtr/], ['Fire EDB']],
    [[/cotton/, /phase/], ['MDB', 'Int', 'Wis', 'Cast Speed', 'Mag CD', 'Evd', 'Agi', 'Pmit', 'Mmit', 'Crus']],

    //Light
    [[/leather/, /shade/], ['ADB', 'Attack Speed', 'Pmit', 'Mmit', 'Evd', 'Res', 'Str', 'Dex', 'End', 'Agi', 'Int', 'Wis', 'Crus', 'Slas', 'Pier']],
    [[/leather/], ['Burden']],
    [[/leather/, /shade/], ['Interf']],

    //Heavy
    [[/\splate/], ['BLK']],
    [[/power/], ['ADB']],
    [[/\splate/, /power/], ['Str', 'Dex', 'End', 'Pmit', 'Mmit', 'Phy CC', 'Phy CD', 'Crus', 'Slas', 'Pier', 'Burden', 'Interf']]
  ].map(line => ({'nameTests': line[0], 'statsToShow': line[1]}));

  const weightedValueConfig = [
  ].map(line => ({"tag": line[0], "nameTest": line[1], "statsWeight": Object.entries(line[2]).map(entry=>({"name":entry[0],"value":entry[1]}))}));

  // statNames: [abbreviated name, forging name, html name, base multiplier, level scaling factor]. Base multiplier is necessary for precise reverse forge calculations
  const statNames = [
    ['ADB', 'Physical Damage', 'Attack Damage', 0.0854, 250/15],
    ['Phy CC', 'Physical Crit Chance', 'Attack Crit Chance', 0.0105, 2000],
    ['Phy CD', null, 'Attack Crit Damage', 0.01, Infinity],
    ['Phy ACC', 'Physical Hit Chance', 'Attack Accuracy', 0.06069, 5000],
    ['Attack Speed', null, 'Attack Speed', 0.0481, Infinity],
    ['MDB', 'Magical Damage', 'Magic Damage', 0.082969, 250/11],
    ['Mag CC', 'Magical Crit Chance', 'Magic Crit Chance', 0.0114, 2000],
    ['Mag CD', null, 'Spell Crit Damage', 0.01, Infinity],
    ['Mag ACC', 'Magical Hit Chance', 'Magic Accuracy', 0.0491, 5000],
    ['Cast Speed', null, 'Casting Speed', 0.0489, Infinity],

    ['Str', 'Strength Bonus', 'Strength', 0.03, 250/7],
    ['Dex', 'Dexterity Bonus', 'Dexterity', 0.03, 250/7],
    ['End', 'Endurance Bonus', 'Endurance', 0.03, 250/7],
    ['Agi', 'Agility Bonus', 'Agility', 0.03, 250/7],
    ['Int', 'Intelligence Bonus', 'Intelligence', 0.03, 250/7],
    ['Wis', 'Wisdom Bonus', 'Wisdom', 0.03, 250/7],
    ['Evd', 'Evade Chance', 'Evade Chance', 0.025, 2000],
    ['Res', 'Resist Chance', 'Resist Chance', 0.0804, 2000],

    ['Pmit', 'Physical Defense', 'Physical Mitigation', 0.021, 2000],
    ['Mmit', 'Magical Defense', 'Magical Mitigation', 0.0201, 2000],
    ['BLK', 'Block Chance', 'Block Chance', 0.0998, 2000],
    ['Parry', 'Parry Chance', 'Parry Chance', 0.0894, 2000],
    ['Mana C', null, 'Mana Conservation', 0.1, Infinity],
    ['Crus', 'Crushing Mitigation', 'Crushing', 0.0155, Infinity],
    ['Slas', 'Slashing Mitigation', 'Slashing', 0.0153, Infinity],
    ['Pier', 'Piercing Mitigation', 'Piercing', 0.015, Infinity],
    ['Burden', null, 'Burden', 0, Infinity],
    ['Interf', null, 'Interference', 0, Infinity],

    ['Elem Prof', 'Elemental Proficiency', 'Elemental', 0.0306, 250/7],
    ['Divine Prof', 'Divine Proficiency', 'Divine', 0.0306, 250/7],
    ['Forb Prof', 'Forbidden Proficiency', 'Forbidden', 0.0306, 250/7],
    ['Depr Prof', 'Deprecating Proficiency', 'Deprecating', 0.0306, 250/7],
    ['Sup Prof', 'Supportive Proficiency', 'Supportive', 0.0306, 250/7],

    ['Holy EDB', 'Holy Spell Damage', 'Holy EDB', 0.0804, 200],
    ['Dark EDB', 'Dark Spell Damage', 'Dark EDB', 0.0804, 200],
    ['Wind EDB', 'Wind Spell Damage', 'Wind EDB', 0.0804, 200],
    ['Elec EDB', 'Elec Spell Damage', 'Elec EDB', 0.0804, 200],
    ['Cold EDB', 'Cold Spell Damage', 'Cold EDB', 0.0804, 200],
    ['Fire EDB', 'Fire Spell Damage', 'Fire EDB', 0.0804, 200],

    ['Holy MIT', 'Holy Mitigation', 'Holy MIT', 0.1, Infinity],
    ['Dark MIT', 'Dark Mitigation', 'Dark MIT', 0.1, Infinity],
    ['Wind MIT', 'Wind Mitigation', 'Wind MIT', 0.1, Infinity],
    ['Elec MIT', 'Elec Mitigation', 'Elec MIT', 0.1, Infinity],
    ['Cold MIT', 'Cold Mitigation', 'Cold MIT', 0.1, Infinity],
    ['Fire MIT', 'Fire Mitigation', 'Fire MIT', 0.1, Infinity],

    ['CR', null, 'Counter-Resist', 0.1, Infinity],
  ].map(statNameArray => ({'abbr': statNameArray[0], 'forgeName': statNameArray[1], 'htmlName': statNameArray[2], 'baseMultiplier': statNameArray[3], 'levelScalingFactor': statNameArray[4]}));
  const htmlMagicTypes = ['Holy', 'Dark', 'Wind', 'Elec', 'Cold', 'Fire'];

  const peerlessPXP = Object.entries({
    'axe':375, 'club':375, 'rapier':377, 'shortsword':377, 'wakizashi':378,
    'estoc':377, 'katana':375, 'longsword':375, 'mace':375,
    'katalox staff':368, 'oak staff':371, 'redwood staff':371, 'willow staff':371,
    'buckler':374, 'force shield':374, 'kite shield':374,
    'phase':377, 'cotton':377,
    'arcanist':421, 'shade':394, 'leather':393,
    'power':382, ' plate':377,
  });

  let ready = true;
  const equipList = [];
  let parsingDone = {};
  let delayedDone = null;
  let delayedstatusElm = null;
  const rangesNeeded = [];
  let usingThisDocument = false;

  function getDb(){
    const ranges = localStorage.EquipmentRanges ? JSON.parse(localStorage.EquipmentRanges) : {};
    mergeDeep(ranges, manualRanges);
    return ranges;
  }
  let EquipmentRanges = getDb();
  //Expose addEquipDocument:
  function addEquipDocument(equipDocument, eid, useQuality){
    usingThisDocument = true;
    parseEquip(equipDocument, eid, useQuality);
  }
  //Expose addEquipLink:
  function addEquipLink(link, useQuality){
    usingThisDocument = false;
    const eid = /\/equip\//.test(link) ? link.match(/equip\/(\d+)\//)[1] : link.match(/eid=(\d+)/)[1];
    parsingDone[eid] = false;
    get(link, equipDocument => {
      parseEquip(equipDocument, eid, useQuality);
      parsingDone[eid] = true;
      if (delayedDone && Object.keys(parsingDone).every(parsingEid => parsingDone[parsingEid] === true))
        run(delayedDone, delayedstatusElm);
    });
  }

  function getName(equipDocument){
    if (typeof equipDocument.body.children[1] === 'undefined')
      return 'No such item';
    const showequip = equipDocument.body.children[1];
    const nameDiv = showequip.children.length === 3 ?
                    showequip.children[0].children[0] :
                    showequip.children[1].children[0];
    const name = nameDiv.children.length === 3 ?
                 nameDiv.children[0].textContent + ' ' + nameDiv.children[2].textContent :
                 nameDiv.children[0].textContent;
    return name;
  }
  function getEquipInfo(equipDocument, eid, useQuality){
    const name = getName(equipDocument);
    if (name === 'No such item'){
      return {'name': 'No such item'};
    }
    const pageHtml = equipDocument.body.innerHTML;
    const matchD = pageHtml.match(/<div>([^&]+)\s(?:&nbsp; ){2}(?:Level\s([0-9]+|Unassigned)\s)?.+(Soulbound|Tradeable|Untradeable).+Condition:.+\((\d+)%.+Tier:\s(\d+)\s(?:\((\d+)\s\/\s(\d+))?/i);
    let matchN = name.match(/([\w-]+) ([\w-]*?) ?(Axe|Club|Rapier|Shortsword|Wakizashi|Dagger|Sword Chucks|Estoc|Longsword|Mace|Katana|Scythe|Oak|Redwood|Willow|Katalox|Ebony|Buckler|Kite|Force|Tower|Cotton|Phase|Gossamer|Silk|Leather|Shade|Kevlar|Dragon Hide|Plate|Power|Shield|Chainmail|Gold|Silver|Bronze|Diamond|Emerald|Prism|Platinum|Steel|Titanium|Iron) ?((?!of)\w*) ?((?=of)[\w- ]*|$)/i);
    const matchP = pageHtml.match(/Strength|Dexterity|Endurance|Agility|Wisdom|Intelligence/gi) || [];
    const matchO = /Flimsy|Fine|Bronze|Iron|Silver|Steel|Gold|Platinum|Titanium|Emerald|Sapphire|Diamond|Prism|trimmed|adorned|tipped|the Ox|the Raccoon|the Cheetah|the Turtle|the Fox|the Owl|Chucks|Ebony|Scythe|Dagger|Astral|Quintessential|Silk|Hide|Buckler of the Fleet|Cloth of the Fleet|Hulk|Aura|Stone-Skinned|Fire-eater|Frost-born|Thunder-child|Wind-waker|Thrice-blessed|Spirit-ward|Chainmail|Coif|Mitons|Hauberk|Chausses|Kevlar|Gossamer|Tower/i;
    const matchButcher = pageHtml.match(/Butcher\sLv.(\d)/);
    const matchSwiftStrike = pageHtml.match(/Swift\sStrike\sLv.(\d)/);
    const matchArchmage = pageHtml.match(/Archmage\sLv.(\d)/);
    const matchPenetrator = pageHtml.match(/Penetrator\sLv.(\d)/);
    if (matchN === null || matchN.length !== 6)
      matchN = ["%%%", "%%%", "%%%", "%%%", "%%%", "%%%"];

    const pabs = matchP.filter((item, pos, self) => {
      return self.indexOf(item) === pos;
    });

    const equipInfo = {
      eid: eid || '',
      name: name,
      nameLower: name.toLowerCase(),
      level: +matchD[2] || matchD[2] || matchD[3],
      tier: +matchD[5],
      totalPxpThisLevel: +matchD[7] || 0,
      quality: useQuality || matchN[1],
      prefix: matchN[2],
      slot: matchN[4],
      type: matchN[3],
      suffix: matchN[5],
      pabs,
      obsolete: name.match(matchO) ? true : false,
      forging: getForging(equipDocument), //forging: forging[htmlName] is object with keys forgeLevel, baseMultiplier, scalingFactor;
      butcher: matchButcher ? parseInt(matchButcher[1]) : 0,
      swiftStrike: matchSwiftStrike ? parseInt(matchSwiftStrike[1]) : 0,
      archmage: matchArchmage ? parseInt(matchArchmage[1]) : 0,
      penetrator: matchPenetrator ? parseInt(matchPenetrator[1]) : 0
    };
    if (/of\sthe\s/i.test(equipInfo.suffix))
      equipInfo.suffix = equipInfo.suffix.substring(7);
    else if (/^of\s/i.test(equipInfo.suffix))
      equipInfo.suffix = equipInfo.suffix.substring(3);
    const typesWithSlots = ['Cotton', 'Phase', 'Leather', 'Shade', 'Plate', 'Power'];
    if (!typesWithSlots.includes(equipInfo.type))
      equipInfo.slot = '';
    const forgingKeys = Object.keys(equipInfo.forging);
    equipInfo.maxForging = forgingKeys.length === 0 ? 0 : forgingKeys.reduce((maxForgeLevel, forgeObjKey) => Math.max(maxForgeLevel, equipInfo.forging[forgeObjKey].forgeLevel), 0);
    return equipInfo;
  }
  function calcPxp0(pxpN, n){
    let pxp0Est = 300;
    for (let i = 1; i < 15; i++){
      const sumPxpNextLevel = 1000 * (Math.pow(1 + pxp0Est / 1000, n + 1) - 1);
      const sumPxpThisLevel = 1000 * (Math.pow(1 + pxp0Est / 1000, n) - 1);
      const estimate = sumPxpNextLevel - sumPxpThisLevel;
      if (estimate > pxpN)
        pxp0Est -= 300 / Math.pow(2, i);
      else
        pxp0Est += 300 / Math.pow(2, i);
    }
    return Math.round(pxp0Est);
  }
  function getPxp0(item){
    if (item.tier === 0)
      return item.totalPxpThisLevel;
    if (item.tier === 10){ // use an average pxp0 for tier 10 equipment
      const pxp0 = (peerlessPXP.find(n=>item.nameLower.includes(n[0]))||[null,400])[1];
      const quality = item.name.split(' ')[0]; // item.quality changes when changing the range, so it needs to get the quality from its name.
      if (quality === 'Peerless')
        return pxp0;
      else if (quality === 'Legendary')
        return Math.round(pxp0 * 0.95);
      else if (quality === 'Magnificent')
        return Math.round(pxp0 * 0.89);
      else
        return Math.round(pxp0 * 0.8);
    }
    //tier is 1-9:
    return calcPxp0(item.totalPxpThisLevel, item.tier);
  }
  function reverseForgeMultiplierDamage(forgedBase, forgeLevelObj, pxp0, iwCoeff){
    const qualityBonus = ((pxp0 - 100) / 25) * forgeLevelObj.baseMultiplier;
    const forgeCoeff = 1 + 0.279575 * Math.log(0.1 * forgeLevelObj.forgeLevel + 1); // 0.278875
    const unforgedBase = (forgedBase - qualityBonus) / forgeCoeff / iwCoeff + qualityBonus; // iwCoeff for Butcher and Archmage
    return unforgedBase;
  }
  function reverseForgeMultiplierNondamage(forgedBase, forgeLevelObj, pxp0){
    const qualityBonus = ((pxp0 - 100) / 25) * forgeLevelObj.baseMultiplier;
    const forgeCoeff = 1 + 0.2 * Math.log(0.1 * forgeLevelObj.forgeLevel + 1);
    const unforgedBase = (forgedBase - qualityBonus) / forgeCoeff + qualityBonus;
    return unforgedBase;
  }
  function getForging(equipDocument){
    const forging = {};
    [ {'textContent':'Physical Damage Lv.0'}, {'textContent':'Magical Damage Lv.0'}, ...equipDocument.querySelectorAll('#eu > span') ].forEach(span => { // insert fake forge data: Physical Damage for Butcher and Magical Damage for Archmage
      const re = span.textContent.match(/(.+)\sLv\.(\d+)/);
      const statNameObj = statObjFromForgeName(re[1]);
      if (!statNameObj)
        debuglog('no statNameObj found for ' + re[1]);
      forging[statNameObj.htmlName] = {forgeLevel:parseInt(re[2]),
                                       baseMultiplier:statNameObj.baseMultiplier,
                                       scalingFactor:statNameObj.levelScalingFactor};
    });
    return forging;
  }
  function statObjFromForgeName(forgeName){
    return statNames.find(statNameObj => forgeName === statNameObj.forgeName);
  }
  function htmlNameFromAbbrevName(abbrevName){
    try{
      return statNames.find(statNameObj => abbrevName === statNameObj.abbr).htmlName;
    } catch (e) {
      debuglog('htmlNameFromAbbrevName missing ' + abbrevName);
    }
  }
  function abbrevNameFromHtmlName(htmlName){
    try {
      return statNames.find(statNameObj => htmlName === statNameObj.htmlName).abbr;
    } catch (e){
      debuglog('abbrevNameFromHtmlName missing ' + htmlName);
    }
  }
  function titleStrToBase(title){
    return parseFloat(title.substr(6));
  }
  function getEquipStats(equipDocument, equipInfo){
    const equipStats = [];
    const plusAndPercentNodes = [];
    equipDocument.querySelectorAll('div[title]').forEach(div => {
      const equipStatObj = {};
      const spans = div.querySelectorAll('span');
      if (spans[0].textContent !== '+') //+s currently in a text node here will be turned into spans shortly - this will let us parse the HTML again if the user asks for percentiles for a certain quality
        equipStatObj.span = spans[0];
      else
        equipStatObj.span = spans[1];
      const span = equipStatObj.span;
      if (/Damage/.test(span.textContent)){
        const damageAmount = span.textContent.match(/^\d+/);
        const extraStr = span.textContent.match(/[^\d]+$/);
        span.textContent = damageAmount;
        div.appendChild(equipDocument.createElement('span')).textContent = extraStr;
      }

      let htmlName;
      if (div.parentElement.parentElement.id === 'equip_extended')
        htmlName = 'Attack Damage';
      else {
        htmlName = div.childNodes[0].textContent.trim();
        if (/\+/.test(htmlName)) // "Elec +"
          htmlName = htmlName.substr(0, htmlName.length - 2);
      }
      if (htmlMagicTypes.includes(htmlName)) {
        if (div.parentElement.children[0].textContent === 'Damage Mitigations')
          htmlName += ' MIT';
        else
          htmlName += ' EDB';
      }



      if (isHtmlNameFromIW(htmlName, equipInfo))
        return; //throws out IW-only stats

      equipStatObj.htmlName = htmlName;
      equipStatObj.scaledValue = parseFloat(span.textContent);
      equipStatObj.baseForgedValue = titleStrToBase(div.title);
      equipStatObj.baseUnforgedValue = equipStatObj.baseForgedValue;

      const previousNode = span.previousSibling;
      if (/\+/.test(previousNode.textContent)){
        if (previousNode.tagName !== 'SPAN'){
          previousNode.textContent = span.previousSibling.textContent.slice(0, -1);
          const newPlusSpan = span.parentElement.insertBefore(equipDocument.createElement('span'), span);
          newPlusSpan.textContent = '+';
          plusAndPercentNodes.push(newPlusSpan);
        } else
            plusAndPercentNodes.push(previousNode);
      }
      if (span.parentElement.nextElementSibling && span.parentElement.nextElementSibling.textContent.trim() === '%')
        plusAndPercentNodes.push(span.parentElement.nextElementSibling);

      //reverse forging:
      if (htmlName === 'Attack Damage' || htmlName === 'Magic Damage'){
        //calculate Butcher and Archmage here
        const iwLevel = htmlName === 'Attack Damage' ? equipInfo.butcher : htmlName === 'Magic Damage' ? equipInfo.archmage : 0;
        const iwCoeff = 1 + 0.02 * iwLevel;
        if (equipInfo.forging[htmlName] || iwLevel)
          equipStatObj.baseUnforgedValue = reverseForgeMultiplierDamage(equipStatObj.baseUnforgedValue, equipInfo.forging[htmlName], equipInfo.pxp0, iwCoeff);
      } else if (equipInfo.forging[htmlName])
        equipStatObj.baseUnforgedValue = reverseForgeMultiplierNondamage(equipStatObj.baseUnforgedValue, equipInfo.forging[htmlName], equipInfo.pxp0);

      //reverse IW:
      if (htmlName === 'Counter-Resist' && equipInfo.penetrator)
        equipStatObj.baseUnforgedValue = equipStatObj.baseUnforgedValue - 4 * equipInfo.penetrator;
      else if (htmlName === 'Attack Speed' && equipInfo.swiftStrike)
        equipStatObj.baseUnforgedValue = equipStatObj.baseUnforgedValue - 1.92 * equipInfo.swiftStrike;

      equipStats.push(equipStatObj);
    });
    if (plusAndPercentNodes.length > 0)
      equipStats.plusAndPercentNodes = plusAndPercentNodes;
    return equipStats;
  }
  function isHtmlNameFromIW(htmlName, equipInfo){
    if (htmlName === 'HP Bonus' ||
        htmlName === 'MP Bonus' ||
        (htmlName === 'Holy MIT' && equipInfo.prefix !== 'Zircon') ||
        (htmlName === 'Dark MIT' && equipInfo.prefix !== 'Onyx') ||
        (htmlName === 'Wind MIT' && equipInfo.prefix !== 'Jade') ||
        (htmlName === 'Elec MIT' && equipInfo.prefix !== 'Amber') ||
        (htmlName === 'Cold MIT' && equipInfo.prefix !== 'Cobalt') ||
        (htmlName === 'Fire MIT' && equipInfo.prefix !== 'Ruby') ||

        (htmlName === 'Attack Crit Damage' && equipInfo.prefix !== 'Savage' && equipInfo.type !== 'Power') ||                                  //Fatality
        htmlName === 'Counter-Parry' ||                                                                                                        //Overpower
        (htmlName === 'Attack Speed' && equipInfo.type !== 'Wakizashi' && equipInfo.suffix !== 'Swiftness' && equipInfo.prefix !== 'Agile') || //Swift Strike

        (htmlName === 'Spell Crit Damage' && equipInfo.prefix !== 'Mystic' && equipInfo.type !== 'Phase') ||                                          //Annihilator
        (htmlName === 'Mana Conservation' && equipInfo.suffix !== 'Focus' && equipInfo.suffix !== 'Battlecaster' && equipInfo.prefix !== 'Frugal') || //Economizer
        (htmlName === 'Counter-Resist' && equipInfo.type !== 'Willow' && equipInfo.type !== 'Oak') ||                                                 //Penetrator
        (htmlName === 'Casting Speed' && equipInfo.prefix !== 'Charged')                                                                              //Spellweaver
    ){
      return true;
    }
    return false;
  }

  function parseEquip(equipDocument, eid, useQuality){
    const equipInfo = getEquipInfo(equipDocument, eid, useQuality);
    if (equipInfo.name === 'No such item'){
      equipInfo.level = 'No such item';
      return equipInfo;
    }
    equipInfo.infoStr = equipInfo.level;
    if (/(Shield\s)|(Buckler)/.test(equipInfo.name))
      equipInfo.infoStr += ', ' + equipInfo.pabs.map(pab => pab.substring(0, 3)).join(' ');
    if (equipInfo.tier > 0)
      equipInfo.infoStr += ', IW ' + equipInfo.tier;
    equipInfo.pxp0 = getPxp0(equipInfo);
    if (equipInfo.tier < 10)
      equipInfo.infoStr += ', PXP ' + equipInfo.pxp0;
    equipInfo.negativeInfoStr = '';
    if (equipInfo.maxForging > 0)
      equipInfo.infoStr += ', forged ' + equipInfo.maxForging;
    [['Magnificent', 'Mag'], ['Exquisite', 'Exq'], ['Superior', 'Sup']].forEach(qualityObj => {
      if (equipInfo.quality === qualityObj[0])
        equipInfo.infoStr += ', ' + qualityObj[1] + ' ranges: ';
    });
    const equipStats = getEquipStats(equipDocument, equipInfo);
    const htmlNames = equipStats.map(equipStat => equipStat.htmlName);
    const haveRanges = !!validateRanges(equipInfo.type, equipInfo.slot, equipInfo.quality, htmlNames);
    if (!haveRanges){
      if (equipInfo.type === '%%%' || equipInfo.obsolete){
        debuglog('Equip is too obsolete, not parsing');
        return;
      }
      rangesNeeded.push({type: equipInfo.type, slot: equipInfo.slot, quality: equipInfo.quality, statNames: htmlNames});
      ready = false;
    }
    equipList.push({'equipInfo': equipInfo, 'equipStats': equipStats});
  }
  function validateRanges(type, slot, quality, htmlNames){
    EquipmentRanges = getDb();
    /* EquipmentRanges: big multidimensional array of
       EquipmentRanges[type][slot][quality][htmlName] or EquipmentRanges[type][quality][htmlName], example:
       EquipmentRanges['Cotton']['Cap']['Legendary']['Attack Accuracy'] will contain [{prefix:'all',suffix:'all', min:3.89, max:4.62}] */
    if (!Object.keys(EquipmentRanges).includes(type))
      return false;
    if (slot && !Object.keys(EquipmentRanges[type]).includes(slot))
      return false;
    const equipsByQuality = slot ? EquipmentRanges[type][slot] : EquipmentRanges[type];
    if (equipsByQuality === false)
      return true; //bad equip types: don't need to ping reasoningtheory again
    if (quality === 'Peerless')
      quality = 'Legendary'; //when userscript asks reasoningtheory for Peerless info, reasoningtheory will return Leg info, and Leg info will go into userscript database
    if (!Object.keys(equipsByQuality).includes(quality))
      return false;
    if (!htmlNames.every(htmlName => {
      if (!Object.keys(equipsByQuality[quality]).includes(htmlName)){
        debuglog('for ' + type + ' ' + slot + ' ' + quality + ', EquipmentRanges DB is lacking htmlName ' + htmlName);
        return false;
      }
      return true;
    }))
      return false;
    if (updateCheckDays && Date.now()/1000 - (equipsByQuality[quality].lastCheck || 0) > 3600*24*updateCheckDays)
      return false;
    return !!equipsByQuality[quality];
  }

  //Expose run:
  function run(done, statusElm){
    if (!Object.keys(parsingDone).every(eid => parsingDone[eid] === true)){
      delayedDone = done;
      delayedstatusElm = statusElm;
      return;
    }
    if (ready){
      statusElm.textContent = 'Using local database...';
      debuglog('Using local database');
      makeResults(done, statusElm);
    } else {
      statusElm.textContent = '从在线数据库获取数据中...';
      debuglog('Getting ranges from reasoningtheory');
      updateDatabase(done, statusElm); //calls makeResults
    }
  }
  function updateDatabase(done, statusElm){
    post('https://reasoningtheory.net/gethvitems.php', 'rangesNeeded=' + JSON.stringify(rangesNeeded), newRangesStr => {
      let newRanges;
      try {
        newRanges = JSON.parse(newRangesStr);
        setLastCheck(newRanges, parseInt(Date.now()/1000));
      } catch (e){
        statusElm.textContent = 'Response Error: "' + newRangesStr + '"';
        return;
      }
      EquipmentRanges = getDb();
      mergeDeep(EquipmentRanges, newRanges);
      mergeDeep(EquipmentRanges, manualRanges);
      localStorage.EquipmentRanges = JSON.stringify(EquipmentRanges);
      makeResults(done, statusElm);
    }, statusElm);
  }
  function mergeDeep(target, source){
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key])
          Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else
        Object.assign(target, { [key]: source[key] });
    }
  }
  function setLastCheck(source, date) {
    for (const key in source) {
      if (isObject(source[key])) {
        setLastCheck(source[key], date);
      }
    }
    if (source.hasOwnProperty('lastUpdate')) {
        source.lastCheck = date;
    }
  }

  function isObject(item) {
    return (item && typeof item === 'object' && !Array.isArray(item));
  }

  function makeResults(done, statusElm){
    if (equipList.length === 0){
      debuglog('No equips to parse');
      statusElm.textContent = 'No equips to parse';
    }
    statusElm.textContent = 'Making results...';
    debuglog('Making results...');
    const singlePageResult = usingThisDocument && equipList.length === 1 && equipValid(equipList[0]);

    //equip has keys:
    //  equipInfo, object with name, level, type, slot, etc
    //  equipStats, array of objects; objects have keys: span, htmlName, scaledValue, baseForgedValue, baseUnforgedValue

    let results = {};
    equipList.forEach(equip => {
      const equipResult = {};

      equipResult.infos = equip.equipInfo;

      if (!equipValid(equip)){
        debuglog('Equip type not in database: ' + equip.equipInfo.name);
        equipResult.summaries = ['Unknown equip type', 'Unknown equip type'];
        results[equip.equipInfo.eid] = equipResult;
        return;
      }

      const { summary: extendedSummary, htmlNamePriorities, weightedValues } = makeSummaryAndPriorities(extendedSummaryStatsConfig, equip);
      equip.equipInfo.htmlNamePriorities = htmlNamePriorities;
      equipResult.summaries = {
        short: makeSummaryAndPriorities(shortSummaryStatsConfig, equip).summary,
        extended: extendedSummary,
        weighted: weightedValues
      };


      const equipPercentiles = {'unforged': {}, 'forged': {}};
      const equipBases = {'unforged': {}, 'forged': {}};
      const equipRanges = {};
      equip.equipStats.forEach(equipStat => {
        equipPercentiles.unforged[equipStat.htmlName] = getPercentile(equip.equipInfo, equipStat.htmlName, equipStat.baseUnforgedValue);
        equipPercentiles.forged[equipStat.htmlName] = getPercentile(equip.equipInfo, equipStat.htmlName, equipStat.baseForgedValue);
        equipBases.unforged[equipStat.htmlName] = equipStat.baseUnforgedValue;
        equipBases.forged[equipStat.htmlName] = equipStat.baseForgedValue;
      });
      equipResult.percentiles = equipPercentiles;
      equipResult.bases = equipBases;
      equipResult.ranges = equipRanges;


      if (singlePageResult){
        equipResult.transformations = makeTransformations(equip);
        results = equipResult;
      } else {
        results[equip.equipInfo.eid] = equipResult;
      }
    });



    ready = true;
    equipList.length = 0;
    rangesNeeded.length = 0;
    parsingDone = {};
    delayedDone = null;
    statusElm.textContent = 'Results finished';
    debuglog('Results finished');
    //"results", or "results[eid]": {infos, summaries, percentiles, bases, ranges, transformations}
    done(results);
  }

  //expose getRanges, used by DocumentInteractor
  function getRanges(equipInfo){
    let equipsByQuality;
    let qualityToUse;
    let statsArray;
    try {
      equipsByQuality = equipInfo.slot ? EquipmentRanges[equipInfo.type][equipInfo.slot] : EquipmentRanges[equipInfo.type];
      qualityToUse = equipInfo.quality === 'Peerless' ? 'Legendary' : equipInfo.quality;
      statsArray = equipsByQuality[qualityToUse];
      if (!statsArray)
        throw new Error();
    } catch (e){
      console.log(`In database, didn't find ${equipInfo.quality} ${equipInfo.type} ${equipInfo.slot}`);
      return false;
    }
    const matchingRanges = {};
    Object.keys(statsArray).forEach(htmlName => {
      const thisMatchingRange = getMatchingRange(statsArray[htmlName], equipInfo.prefix, equipInfo.suffix);
      if (!thisMatchingRange)
        return;
      matchingRanges[htmlName] = thisMatchingRange;
    });
    return matchingRanges;
  }
  function getMatchingRange(statArray, thisPrefix, thisSuffix){
    const thisKey = Object.keys(statArray).find(prefixSuffixStr => {
      //Keys are formatted like this to give each prefix-suffix combination a predictable key, rather than the key being a random array index. Useful for backend.
      const [prefixTest, suffixTest] = prefixSuffixStr.split(' | ');
      if (prefixTest !== 'all' && prefixTest !== thisPrefix && !prefixTest.includes('not!'))
        return false;
      else if (prefixTest.includes('not!') && prefixTest.split('!').indexOf(thisPrefix) !== -1)
        return false;
      else if (suffixTest !== 'all' && suffixTest !== thisSuffix && !suffixTest.includes('not!'))
        return false;
      else if (suffixTest.includes('not!') && suffixTest.split('!').indexOf(thisSuffix) !== -1)
        return false;
      return true;
    });
    if (!thisKey)
      return false;
    return statArray[thisKey];
  }
  function getPercentile(equipInfo, htmlName, baseValue){
    const equipsByQuality = equipInfo.slot ? EquipmentRanges[equipInfo.type][equipInfo.slot] : EquipmentRanges[equipInfo.type];
    const qualityToUse = equipInfo.quality === 'Peerless' ? 'Legendary' : equipInfo.quality;
    const statArray = equipsByQuality[qualityToUse][htmlName];
    const thisMatchingRange = getMatchingRange(statArray, equipInfo.prefix, equipInfo.suffix);
    if (!thisMatchingRange){
      debuglog('Failed to find a range for ' + equipInfo.type + ' ' + equipInfo.slot + ' ' + qualityToUse + ' ' + htmlName + ' in list ' + JSON.stringify(Object.keys(statArray)));
      return false;
    }
    //debuglog(equipInfo.name + ' htmlName: ' + htmlName + ' thisKey ' + thisKey);
    const percentile = Math.round(100 * (baseValue - thisMatchingRange.min) / (thisMatchingRange.max - thisMatchingRange.min));
    return percentile;
  }
  function getAbsolutePercentile(equipInfo, htmlName, baseValue){
    const equipsByQuality = equipInfo.slot ? EquipmentRanges[equipInfo.type][equipInfo.slot] : EquipmentRanges[equipInfo.type];
    const qualityToUse = 'Legendary';
    const statArray = equipsByQuality[qualityToUse][htmlName];
    const thisMatchingRange = getMatchingRange(statArray, equipInfo.prefix, equipInfo.suffix);
    if (!thisMatchingRange){
      debuglog('Failed to find a range for ' + equipInfo.type + ' ' + equipInfo.slot + ' ' + qualityToUse + ' ' + htmlName + ' in list ' + JSON.stringify(Object.keys(statArray)));
      return false;
    }
    //debuglog(equipInfo.name + ' htmlName: ' + htmlName + ' thisKey ' + thisKey);
    const diff = Math.round((baseValue-thisMatchingRange.max)*100)/100;
    const percentile = '<span>P' + (diff>0?'+'+diff.toFixed(2) : diff<0?diff.toFixed(2) : '') + '<br>' + (100 * baseValue / thisMatchingRange.max).toFixed(2) + '%</span>';
    return percentile;
  }
  function getFullyForgedBase(equip,equipStat){
    const equipInfo = equip.equipInfo;
    const htmlName = equipStat.htmlName;
    const forgeObj = statNames.find(statNameObj => htmlName === statNameObj.htmlName);
    if(!forgeObj.forgeName) {
      return;
    }
    let forgeFactor = 0.2,
        forgeLevel = 50,
        iwLevel = 0;
    if(htmlName === 'Attack Damage' || htmlName === 'Magic Damage') {
      forgeFactor = 0.279575; // 0.278875
      forgeLevel = 100;
      if(equipInfo.type === 'Phase' && equipInfo.prefix === 'Radiant') { // Magic Damage Lv.50: Radiant Phase
        forgeLevel = 50;
      }
      if(!equipInfo.slot && equipInfo.suffix === 'Slaughter') { // Butcher Lv.5: Weapon of Slaugher
        iwLevel = 5;
      }
      if(!equipInfo.slot && equipInfo.suffix === 'Destruction') { // Archmage Lv.5: Staff of Destruction
        //iwLevel = 5;
      }
    }
    const pxp0 = equipInfo.pxp0;
    const unforgedBase = equipStat.baseUnforgedValue;
    const qualityBonus = ((pxp0 - 100) / 25) * forgeObj.baseMultiplier;
    const forgeCoeff = 1 + forgeFactor * Math.log(0.1 * forgeLevel + 1);
    const iwCoeff = 1 + 0.02 * iwLevel;
    const forgedBase = (unforgedBase - qualityBonus) * forgeCoeff * iwCoeff + qualityBonus;
    return forgedBase;
  }
  function getWeightedValue(equip, htmlName, weight){
    const foundEquipStat = equip.equipStats.find(equipStat => equipStat.htmlName === htmlName);
    if (!foundEquipStat){
      //debuglog('[OK] on ' + equip.equipInfo.name + ', found no equipStat for "' + htmlName + '"');
      //return;
    }
    const equipInfo = equip.equipInfo;
    const equipsByQuality = equipInfo.slot ? EquipmentRanges[equipInfo.type][equipInfo.slot] : EquipmentRanges[equipInfo.type];
    const qualityToUse = 'Legendary';
    const statArray = equipsByQuality[qualityToUse][htmlName];
    const thisMatchingRange = getMatchingRange(statArray, equipInfo.prefix, equipInfo.suffix);
    if (!thisMatchingRange){
      debuglog('Failed to find a range for ' + equipInfo.type + ' ' + equipInfo.slot + ' ' + qualityToUse + ' ' + htmlName + ' in list ' + JSON.stringify(Object.keys(statArray)));
      return false;
    }
    //debuglog(equipInfo.name + ' htmlName: ' + htmlName + ' thisKey ' + thisKey);
    const baseValue = foundEquipStat ? foundEquipStat.baseUnforgedValue * weight : 0;
    const maxValue = thisMatchingRange.max * weight;
    return [baseValue,maxValue];
  }

  function equipValid(equip){
    const equipsByQuality = equip.equipInfo.slot ? EquipmentRanges[equip.equipInfo.type][equip.equipInfo.slot] : EquipmentRanges[equip.equipInfo.type];
    return !!equipsByQuality;
  }

  function makeSummaryAndPriorities(config, equip){
    let summary = equip.equipInfo.infoStr.toString();
    const htmlNamePriorities = [];
    config.forEach(line => {
      line.nameTests.forEach(nameTest => {
        if (!nameTest.test(equip.equipInfo.nameLower))
          return;
        line.statsToShow.forEach(abbrevName => {
          const htmlName = htmlNameFromAbbrevName(abbrevName);
          const foundEquipStat = equip.equipStats.find(equipStat => equipStat.htmlName === htmlName);
          if (!foundEquipStat){
            //debuglog('[OK] on ' + equip.equipInfo.name + ', found no equipStat for "' + htmlName + '"');
            return;
          }
          const percentile = getPercentile(equip.equipInfo, htmlName, foundEquipStat.baseUnforgedValue);
          if (percentile < 0)
            return; //lower-than-0 percentiles are not displayed in summary, but they're still accessible via "percentiles" object
          if (summary.includes('%') || !summary.includes('ranges'))
            summary += ', ';
          summary += abbrevNameFromHtmlName(htmlName) + ' ' + percentile + '%';
          htmlNamePriorities.push(htmlName);
        });
      });
    });
    const weightedValues = [];
    weightedValueConfig.forEach(line => {
      if(!line.nameTest.test(equip.equipInfo.nameLower)) {
        return;
      }
      const [baseValue,maxValue] = line.statsWeight.reduce((prev,entry) => {
        const [baseValue,maxValue] = getWeightedValue(equip, entry.name, entry.value);
        return [baseValue+prev[0],maxValue+prev[1]];
      }, [0,0]);
      weightedValues.push('['+line.tag+'] '+(100*baseValue/maxValue).toFixed(2)+'% ('+baseValue.toFixed(2)+' / '+maxValue.toFixed(2)+')');
    });
    return {summary, htmlNamePriorities, weightedValues};
  }

  function makeTransformations(equip){
    const transformations = {};
    const formatDiv = document.querySelector('#equip_extended').lastElementChild.appendChild(document.createElement('div'));
    formatDiv.style.cssText = 'margin-top:6px';
    formatDiv.textContent = 'Display style: Forged Scaled';
    const qualityToShow = equip.equipInfo.quality === 'Peerless' ? 'Legendary' : equip.equipInfo.quality;
    transformations.showUnforgedPercentiles = function(){
      equip.equipStats.forEach(equipStat => {
        equipStat.span.textContent = getPercentile(equip.equipInfo, equipStat.htmlName, equipStat.baseUnforgedValue) + '%';
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'none');
      formatDiv.textContent = '显示样式: 不计入强化属性的浮动值(按F返回) (' + qualityToShow + ')';
    };
    transformations.showForgedPercentiles = function(){
      equip.equipStats.forEach(equipStat => {
        equipStat.span.textContent = getPercentile(equip.equipInfo, equipStat.htmlName, equipStat.baseForgedValue) + '%';
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'none');
      formatDiv.textContent = '显示样式: 计入强化属性的浮动值 (' + qualityToShow + ')';
    };
    transformations.showAbsoltuePercentiles = function(){
      equip.equipStats.forEach(equipStat => {
        equipStat.span.innerHTML = getAbsolutePercentile(equip.equipInfo, equipStat.htmlName, equipStat.baseUnforgedValue);
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'none');
      formatDiv.textContent = '显示样式: 绝对浮动值';
    };
    transformations.showUnforgedBase = function(){
      equip.equipStats.forEach(equipStat => {
        equipStat.span.textContent = Math.round(equipStat.baseUnforgedValue * 100) / 100;
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'inline');
      formatDiv.textContent = '显示样式: 不含强化属性的基础属性';
    };
    transformations.showForgedBase = function(){
      equip.equipStats.forEach(equipStat => {
        equipStat.span.textContent = equipStat.baseForgedValue;
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'inline');
      formatDiv.textContent = '显示样式: 包含强化属性的基础属性';
    };
    transformations.showFullyForgedBase = function(){
      equip.equipStats.forEach(equipStat => {
        const forgedBase = getFullyForgedBase(equip,equipStat) || equipStat.baseForgedValue;
        equipStat.span.textContent = Math.round(forgedBase * 100) / 100;
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'inline');
      formatDiv.textContent = '显示样式: 最大强化后的基础属性';
      if(!equip.equipInfo.slot && equip.equipInfo.suffix === 'Slaughter') {
        formatDiv.append(document.createElement('br'),'(以 潜在能力-屠夫Lv.5 武器攻击伤害+10% 为基准)');
      }
      if(equip.equipInfo.type === 'Phase' && equip.equipInfo.prefix === 'Radiant') {
        formatDiv.append(document.createElement('br'),'(以基础魔法伤害强化 Lv.50 为基准)');
      }
    };
    transformations.showScaledDefault = function(){
      equip.equipStats.forEach(equipStat => {
        equipStat.span.textContent = equipStat.scaledValue;
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'inline');
      formatDiv.textContent = '显示样式: 当前属性';
    };
    transformations.showScaledMylevel = function(){
      formatDiv.textContent = '正在获取你的等级...';
      EquipmentRanges = getDb();
      if (EquipmentRanges.userLevelObj && EquipmentRanges.userLevelObj.level === 500){
        changeDisplay(500);
        return;
      }
      if (EquipmentRanges.userLevelObj && Date.now() - EquipmentRanges.userLevelObj.timestamp < 1000 * 60 * 60 * 24 * 7){ //7 days
        changeDisplay(parseInt(EquipmentRanges.userLevelObj.level));
        return;
      }
      //now, either !userLevelObj or it's out of date:
      const rootDomain = window.location.href.match(/^(.+\.org)/)[1];
      get(rootDomain, response => {
        formatDiv.textContent = 'Got response from root domain...';
        let level;
        try {
          level = parseInt(response.querySelector('#level_readout').children[0].children[0].textContent.match(/\d+/)[0]);
        } catch (e){
          formatDiv.textContent = "Couldn't parse the response";
          return;
        }
        const userLevelObj = {level, timestamp: Date.now()};
        EquipmentRanges.userLevelObj = userLevelObj;
        localStorage.EquipmentRanges = JSON.stringify(EquipmentRanges);
        changeDisplay(level);
      });
      function changeDisplay(myLevel){
        equip.equipStats.forEach(equipStat => {
          const levelScalingFactor = statNames.find(statNameObj => statNameObj.htmlName === equipStat.htmlName).levelScalingFactor;
          equipStat.span.textContent = Math.round(100 * (1 + myLevel / levelScalingFactor) * equipStat.baseForgedValue) / 100;
        });
        if (equip.equipStats.plusAndPercentNodes)
          equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'inline');
        formatDiv.textContent = '显示样式: 当前等级+当前强化状态下的属性 Lv' + myLevel;
      }
    };
    transformations.showScaledCustomlevel = function(){
      const customLevel = parseInt(prompt('输入目标等级:', EquipmentRanges.userLevelObj ? EquipmentRanges.userLevelObj.level : ''));
      if (!customLevel)
        return;
      equip.equipStats.forEach(equipStat => {
        const levelScalingFactor = statNames.find(statNameObj => statNameObj.htmlName === equipStat.htmlName).levelScalingFactor;
        equipStat.span.textContent = Math.round(100 * (1 + customLevel / levelScalingFactor) * equipStat.baseForgedValue) / 100;
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'inline');
      formatDiv.textContent = '显示样式: 目标等级+当前强化状态下的属性 Lv' + customLevel;
    };
    transformations.showFullyForgedScaled = function(){
      const customLevel = parseInt(prompt('输入目标等级:', EquipmentRanges.userLevelObj ? EquipmentRanges.userLevelObj.level : ''));
      if (!customLevel)
        return;
      equip.equipStats.forEach(equipStat => {
        const forgedBase = getFullyForgedBase(equip,equipStat) || equipStat.baseForgedValue;
        const levelScalingFactor = statNames.find(statNameObj => statNameObj.htmlName === equipStat.htmlName).levelScalingFactor;
        equipStat.span.textContent = Math.round(100 * (1 + customLevel / levelScalingFactor) * forgedBase) / 100;
      });
      if (equip.equipStats.plusAndPercentNodes)
        equip.equipStats.plusAndPercentNodes.forEach(node => node.style.display = 'inline');
      formatDiv.textContent = '显示样式: 目标等级+最大强化下的属性 Lv' + customLevel;
      if(!equip.equipInfo.slot && equip.equipInfo.suffix === 'Slaughter') {
        formatDiv.append(document.createElement('br'),'(以 潜在能力-屠夫Lv.5 武器攻击伤害+10% 为基准)');
      }
      if(equip.equipInfo.type === 'Phase' && equip.equipInfo.prefix === 'Radiant') {
        formatDiv.append(document.createElement('br'),'(以基础魔法伤害强化 Lv.50 为基准)');
      }
    };

    return transformations;
  }

  function get(url, done){
    const r = new XMLHttpRequest();
    r.open('GET', url, true);
    r.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
    r.responseType = 'document';
    r.onload = function () {
      if (r.readyState !== 4)
        return;
      if (r.status === 200){
        done(r.response);
      }
    };
    r.send();
  }
  function post(url, data, done, statusElm){
    const r = new XMLHttpRequest();
    r.open('POST', url, true);
    r.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
    r.onload = function () {
      if (r.readyState !== 4)
        return;
      if (r.status !== 200){
        statusElm.textContent = 'Response Error: Response code of "' + r.status + '"';
        return;
      }
      done(r.response);
    };
    r.onerror = function(){
      statusElm.textContent = 'Request Error: Response code of "' + r.status + '"';
    };
    r.send(data);
  }
  return {addEquipLink, addEquipDocument, run, getRanges};
})();
//end of PercentileRanges function























function DocumentInteractor(){
  if (!document.querySelector('#showequip'))
    return; // "No such item"
  if (window.innerHeight < 650) //this will only work on windows created via window.open, whose source is almost surely pressing "c" while hovering over an equip
    window.resizeBy(0, 650 - window.innerHeight);

  document.head.appendChild(document.createElement('style')).textContent = `
#equip_extended { height: initial !important; min-height: 400px !important; } /* so the forges of equipment are always inside the equipment box */

#summaryDiv {margin-top: 20px;}
#optionsDiv {display: none; margin-top: 10px; margin-bottom: 10px;}
option {border:0 !important; margin:0 !important; padding:0 !important;}
#linksDiv {width: 400px; margin:auto; padding-top: 10px;}
#linksDiv > a {float: left;}
#linksDiv > a:nth-child(2) {padding-left: 85px; padding-right: 55px;}
#linksDiv > button {padding: 0;}

#mobileButtons {margin-top: 20px; margin-left:auto; margin-right:auto; max-width:450px;}
#mobileButtons > span {display: inline-block; width: 120px; border: 1px solid; padding: 2px; margin: 2px;}
#mobileButtons > span:last-child {margin-top: 12px;}

#compareDiv {padding-top: 35px;}
#compareDiv > span:nth-child(1) {padding-right: 10px;}
#compareDiv > input:nth-child(2) {width: 500px;}

#checkboxContainer {width: 100px; margin: auto; }
#checkboxContainer > div {text-align: left;}
svg {width: 720px;}
g {pointer-events: all;}

.ex > div, .ep > div {height:auto !important}
.eq span > span {display:inline-block; vertical-align:middle; text-align:center}
  `;
  const summaryDiv = document.body.appendChild(document.createElement('div'));
  summaryDiv.id = 'summaryDiv';
  let baseQuality;
  const {optionsDiv, select, wikiLink, sendRangeDiv} = (function buildSelect(){
    const optionsDiv = document.body.appendChild(document.createElement('div'));
    optionsDiv.id = 'optionsDiv';
    const select = optionsDiv.appendChild(document.createElement('select'));
    ['Legendary', 'Magnificent', 'Exquisite', 'Superior'].forEach(quality => select.appendChild(document.createElement('option')).textContent = quality);
// 在select.onchange事件触发前检测#change-translate元素名是否为“中”，如果是则直接执行tryChangeQuality函数
  select.onchange = () => {
    const changeTranslateElement = document.querySelector('#change-translate');
    if (changeTranslateElement && changeTranslateElement.textContent.trim() === '中') {
      tryChangeQuality();
    } else {
      // 模拟按下Alt + A键
      const altAKeyEvent = new KeyboardEvent('keydown', {
        key: 'a',
        altKey: true,
      });
      document.dispatchEvent(altAKeyEvent);
      tryChangeQuality();
      // 在tryChangeQuality执行完成后再次模拟按下Alt + A键
      setTimeout(() => {
        document.dispatchEvent(altAKeyEvent);
      }, 0);
    }
  };
    select.onclick = clearSelection;

    const sendRangeDiv = optionsDiv.appendChild(document.createElement('div'));

    const linksDiv = optionsDiv.appendChild(document.createElement('div'));
    linksDiv.id = 'linksDiv';
    const serverLink = linksDiv.appendChild(document.createElement('a'));
    serverLink.target = '_blank';
    serverLink.href = 'https://reasoningtheory.net/viewranges';
    serverLink.textContent = '浮动范围数据库';
    const wikiLink = linksDiv.appendChild(document.createElement('a'));
    wikiLink.target = '_blank';
    wikiLink.href = 'https://ehwiki.org/wiki/Equipment_Ranges';
    wikiLink.textContent = 'Wiki浮动范围链接';

    const clearButton = linksDiv.appendChild(document.createElement('button'));
    clearButton.textContent = 'Clear Database';
    clearButton.onclick = () => { delete localStorage.EquipmentRanges; clearButton.textContent = 'done'; };

    return {optionsDiv, select, wikiLink, sendRangeDiv};
  })();

  if(window.location.href.match(/\/equip\//)) {
    // merge split texts of the equipment name
    const showequip = document.body.children[1];
    const nameDiv = showequip.children.length === 3 ? showequip.children[0].children[0] : showequip.children[1].children[0];
    let name = nameDiv.children[0].textContent;
    if(nameDiv.children.length === 3) {
      name += ' ' + nameDiv.children[2].textContent.replace(/^(Of|The)\b/,s=>s.toLowerCase());
      nameDiv.children[2].remove();
      nameDiv.children[1].remove();
      nameDiv.children[0].textContent = name;
      nameDiv.style.height = '18px';
    }
    // change the browser title to the name of the equipment, and add it to the browser history for searching
    const owner = document.getElementById('equip_extended').nextElementSibling.textContent.match(/Current Owner: (.+)/) ? RegExp.$1 : 'System';
    window.history.pushState(null,null);
    document.title = name + ' (' + owner + ') - The HentaiVerse';
  }

  let results;
  let compareResults = null;
  let elementsToRemoveOnReset = [];
  let svgDiv;
  addAndRunParser(null);
  //addAndRunParser('Legendary'); // at first, use L-ranges regardless of the actual quality
  function addAndRunParser(changeToQuality){
    summaryDiv.textContent = 'Checking...';
    PercentileRanges.addEquipDocument(document, null, changeToQuality);
    PercentileRanges.run(addResultsToDocument, summaryDiv);
  }
  function addResultsToDocument(resultsParam){
    if (!resultsParam.infos){
      summaryDiv.textContent = 'No data available for this equip type';
      return;
    }
    results = resultsParam;
    const {infos, summaries, percentiles, bases, ranges, transformations} = resultsParam;
    const type = infos.type;
    const slot =  infos.slot;

    if (type === 'Cotton' || type === 'Phase')
      wikiLink.href = 'https://ehwiki.org/wiki/Equipment_Ranges_Clothes#' + type + '_' + slot;
    else if (type === 'Leather' || type === 'Shade')
      wikiLink.href = 'https://ehwiki.org/wiki/Equipment_Ranges_Light#' + type + '_' + slot;
    else if (type === 'Plate' || type === 'Power')
      wikiLink.href = 'https://ehwiki.org/wiki/Equipment_Ranges_Heavy#' + type + '_' + slot;
    else
      wikiLink.href = 'https://ehwiki.org/wiki/Equipment_Ranges#' + infos.type;
    if (!baseQuality)
      baseQuality = infos.quality;
    summaryDiv.textContent = summaries.extended;
    summaries.weighted.forEach(text=>{summaryDiv.append(document.createElement('br'),text);});
    if (summaries.extended === 'Unknown equip type')
      return;
    const promptForumLink = (() => {
      const formattedLink = (() => {
        const hrefBase = 'https://hentaiverse.org/';
        const hrefIsk = window.location.href.includes('/isekai/') ? 'isekai/' : '';
        const hrefReg = window.location.href.match(/\/equip\/(\d+)\/(.{10})/) || window.location.href.match(/eid=(\d+)&key=(.{10})/);
        if (hrefReg)
          return hrefBase + hrefIsk + 'equip/' + hrefReg[1] + '/' + hrefReg[2];
        else
          return window.location.href;
      })();
      return () => window.prompt('Forum Link:', '[url=' + formattedLink + ']' + infos.name + '[/url]');
    })();
    const actions = [
      [81, transformations.showUnforgedPercentiles], //Q
      [87, transformations.showForgedPercentiles],   //W
      [69, transformations.showAbsoltuePercentiles], //E
      [65, transformations.showUnforgedBase],        //A
      [83, transformations.showForgedBase],          //S
      [68, transformations.showFullyForgedBase],     //D
      [90, transformations.showScaledMylevel],       //Z
      [88, transformations.showScaledCustomlevel],   //X
      [67, transformations.showFullyForgedScaled],   //C
      [70, transformations.showScaledDefault],       //F
      [76, promptForumLink],                         //L
    ].map(([keyCode, func]) => ({keyCode, func}));
    document.body.addEventListener('keydown', e => {
      if(e.ctrlKey || e.altKey || e.shiftKey)
        return;
      if (e.target.tagName === 'INPUT' && e.target.type !== 'checkbox')
        return;
      const action = actions.find(action => e.keyCode === action.keyCode);
      if (!action)
        return;
      action.func();
    });
    const displayStyleDiv = document.querySelector('#equip_extended').children[1].lastChild;
    displayStyleDiv.onclick = () => {
      selectElementText(summaryDiv);
      optionsDiv.style.display = 'block';
      svgDiv.style.display = 'block';
    };
    elementsToRemoveOnReset.push(displayStyleDiv);
    summaryDiv.onclick = displayStyleDiv.onclick;
    select.style.display = 'inline';
    const qualityToShow = infos.quality === 'Peerless' ? 'Legendary' : infos.quality;
    select.childNodes.forEach(option => {
      if (option.textContent === qualityToShow){
        option.selected = true;
        option.style.backgroundColor = '#00ef37';
      } else
        option.style.backgroundColor = null;
    });
    const percentilesAreForThisQuality = (baseQuality === infos.quality);
    const rangeShouldBeSent = (percentilesAreForThisQuality && infos.tier === 0 && infos.maxForging === 0 && !sendRangeDiv.textContent &&
                               Object.keys(percentiles.unforged).some(statName => {
                                                                        const percentile = percentiles.unforged[statName];
                                                                        return (percentile >= 101 || percentile <= -1);
                                                                      })
    );
    if (rangeShouldBeSent){
      sendRangeDiv.textContent = 'Send Range';
      sendRangeDiv.style.marginTop = '10px';
      sendRangeDiv.onclick = () => {
        sendRangeDiv.textContent = 'Sending Range...';
        sendRangeDiv.onclick = null;
        sendThisPageToHvitems(sendRangeDiv);
      };
    }
    if (mobile && !document.querySelector('#mobileButtons'))
      buildMobileButtons(transformations, promptForumLink);

    const compareDiv = optionsDiv.appendChild(document.createElement('div'));
    compareDiv.id = 'compareDiv';
    compareDiv.appendChild(document.createElement('span')).textContent = '装备对比:';
    const compareInput = compareDiv.appendChild(document.createElement('input'));
    compareInput.placeholder = 'https://hentaiverse.org/equip/105097442/5eb80fe368';
    elementsToRemoveOnReset.push(compareDiv);
    const compareButton = compareDiv.appendChild(document.createElement('button'));
    compareButton.textContent = 'Compare!';
    compareButton.addEventListener('click', () => {
      compareInput.style.backgroundColor = null;
      const newLink = compareInput.value.replace(/^.+\.org/, window.location.href.match(/^.+\.org/)); //CORS
      if (!newLink)
        return;
      if (newLink.includes(' :::') || (!/\/equip\//.test(newLink) && !/showequip\.php/.test(newLink))){
        compareButton.textContent = 'Invalid link';
        return;
      }
      compareButton.disabled = true;
      compareButton.textContent = 'Getting info...';
      PercentileRanges.addEquipLink(newLink);
      PercentileRanges.run(doCompare.bind(null, compareDiv, checkboxContainer), compareButton);
    });

    const checkboxHeader = optionsDiv.appendChild(document.createElement('h4'));
    checkboxHeader.textContent = '图形化显示浮动范围:';
    elementsToRemoveOnReset.push(checkboxHeader);
    const checkboxContainer = optionsDiv.appendChild(document.createElement('div'));
    checkboxContainer.id = 'checkboxContainer';
    elementsToRemoveOnReset.push(checkboxContainer);
    ['Legendary', 'Magnificent', 'Exquisite', 'Superior'].forEach(quality => {
      const checkboxDiv = checkboxContainer.appendChild(document.createElement('div'));
      const checkbox = checkboxDiv.appendChild(document.createElement('input'));
      checkbox.type = 'checkbox';
      if (quality === infos.quality || quality === 'Legendary' && infos.quality === 'Peerless'){
        checkbox.checked = true;
        checkbox.disabled = true;
      } else {
        const thisQualityEquipInfo = Object.assign(Object.assign({}, infos), {quality});
        if (PercentileRanges.getRanges(thisQualityEquipInfo) === false)
          checkbox.disabled = true;
      }
      checkbox.addEventListener('change', function(){
        buildGraphic(checkboxContainer);
      });
      checkboxDiv.appendChild(document.createTextNode(quality));
    });


    buildGraphic();

    transformations.showUnforgedPercentiles();
    //transformations.showAbsoltuePercentiles(); // sssss2
  }
  function tryChangeQuality(){
    const changeTo = select.options[select.selectedIndex].textContent;
    if (changeTo !== results.infos.quality)
      reset(changeTo);
  }

  function reset(changeToQuality){
    document.body.onkeydown = () => {};
    results.transformations.showScaledDefault();
    select.style.display = 'none';
    elementsToRemoveOnReset.forEach(elementToRemove => elementToRemove.remove());
    elementsToRemoveOnReset = [];
    compareResults = null;
    svgDiv.remove();
    addAndRunParser(changeToQuality);
  }

  function sendThisPageToHvitems(responseDiv){
    const url = window.location.href;
    const [, eid, key] = /\/equip\//.test(url) ?
                           url.match(/\/equip\/(\d+)\/([0-9a-f]+)/) :
                           url.match(/\.php\?eid=(\d+)&key=([0-9a-f]+)/);
    const rangeToSend = [{eid,key}];
    const r = new XMLHttpRequest();
    const data = new FormData();
    data.append('action', 'store');
    data.append('equipment', JSON.stringify(rangeToSend));
    r.open('POST' , 'https://hvitems.niblseed.com/');
    r.send(data);

    r.onload = function () {
      if (r.readyState !== 4 || r.status !== 200)
        return;
      if (r.response === '1')
        responseDiv.textContent = 'Done!';
      else
        responseDiv.textContent = 'Error, response was: "' + r.response + '"';
    };
  }
  function buildMobileButtons(transformations, promptForumLink){
    const buttonConfigs = [
      [transformations.showUnforgedPercentiles, 'Unforged Percentiles'],
      [transformations.showForgedPercentiles, 'Forged Percentiles'],
      [transformations.showAbsoltuePercentiles, 'Absolute Percentiles'],
      [transformations.showUnforgedBase, 'Unforged Base'],
      [transformations.showForgedBase, 'Forged Base'],
      [transformations.showFullyForgedBase, 'Fully Forged Base'],
      [transformations.showScaledMylevel, 'Scale to Your Level'],
      [transformations.showScaledCustomlevel, 'Scale to Custom Level'],
      [transformations.showFullyForgedScaled, 'Fully Forged Scaled'],
      [transformations.showScaledDefault, 'Scale to Default'],
      [promptForumLink, 'Forum Link']
    ];
    const mobileButtons = document.body.appendChild(document.createElement('div'));
    mobileButtons.id = 'mobileButtons';
    buttonConfigs.forEach((buttonConfig, buttonIndex) => {
      if (buttonIndex === 3 || buttonIndex === 6 || buttonIndex === 9)
        mobileButtons.appendChild(document.createElement('br'));
      const span = mobileButtons.appendChild(document.createElement('span'));
      span.textContent = buttonConfig[1];
      span.onclick = buttonConfig[0];
    });
  }

  function doCompare(compareDiv, checkboxContainer, compareResultsPacked){
    const [, compareInput, compareButton] = compareDiv.children;
    compareButton.disabled = false;
    const compareResultsUnpacked = Object.values(compareResultsPacked)[0];
    if (!compareResultsUnpacked || !compareResultsUnpacked.infos){
      if (!compareResultsUnpacked)
        compareButton.textContent = 'Equip parsing failed (obsolete? mistyped link?)';
      else
        compareButton.textContent = 'No data available for this equip type';
      compareResults = null;
      buildGraphic(checkboxContainer);
      return;
    }
    compareResults = compareResultsUnpacked;
    compareButton.textContent = 'Compare!';
    compareInput.value = compareResults.infos.name + ' ::: ' + compareInput.value;
    compareInput.style.backgroundColor = '#c9ffd1';
    buildGraphic(checkboxContainer);
  }

  //this function is called on addResultsToDocument, and also on quality checkbox change, and also on "Compare!"
  function buildGraphic(checkboxContainer){
    const {infos, summaries, percentiles, bases, transformations} = results;
    const nativeQuality = infos.quality === 'Peerless' ? 'Legendary' : infos.quality;
    const qualitiesToDisplay = !checkboxContainer ?
                               [nativeQuality] :
                               Array.from(checkboxContainer.children).reduce((accum, checkboxDiv) => {
                                 if (!checkboxDiv.children[0].checked)
                                   return accum;
                                 accum.push(checkboxDiv.childNodes[1].textContent);
                                 return accum;
                               }, []);
    const htmlNames = infos.htmlNamePriorities;
    const otherHtmlNamesWithoutDuplicates = Object.keys(bases.unforged).filter(htmlName => !htmlNames.includes(htmlName));
    Array.prototype.push.apply(htmlNames, otherHtmlNamesWithoutDuplicates);
    if (compareResults){
      const compareHtmlNamesWithoutDuplicates = Object.keys(compareResults.bases.unforged).filter(htmlName => !htmlNames.includes(htmlName));
      Array.prototype.push.apply(htmlNames, compareHtmlNamesWithoutDuplicates);
    }
    //rangesByQuality: Assoc array of objects like rangesByQuality[quality] = PercentileRanges.getRanges(thisQualityEquipInfo)
    const rangesByQuality = getRangesByQuality(infos);

    let pageJustLoaded = false;
    if (svgDiv)
      svgDiv.remove(); //might have already been called by reset(), but sometimes buildGraphic is called without reset()
    else
      pageJustLoaded = true;
    svgDiv = document.body.appendChild(document.createElement('div'));
    if (pageJustLoaded)
      svgDiv.style.display = 'none';
    svgDiv.id = 'svgDiv';
    const svg = svgDiv.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg"));
    addInnerHTML(svg, 'rect', 'x="0" y="0" width="100%" height="100%"     fill="#DFEBED">');
    const equipTypeStr = `${infos.type}, ${infos.slot ? infos.slot + ', ' : ''}${infos.quality} [${infos.prefix ? infos.prefix : ''} | ${infos.suffix ? infos.suffix : ''}]`;
    addInnerHTML(svg, 'text', `x="360" y="30" fill="black" font-size="16" font-weight="bolder" text-anchor="middle">Ranges for ${equipTypeStr}</text>`);

    const axisStart = 170;
    const axisWidth = 510;
    let y = 70;
    htmlNames.forEach(htmlName => {
      const [graphMin, graphMax] = (() => {
        let min, max;
        Object.keys(rangesByQuality).filter(quality => qualitiesToDisplay.includes(quality)).forEach(quality => {
          if (!Object.keys(rangesByQuality[quality]).includes(htmlName))
            return;
          const thisStatRange = rangesByQuality[quality][htmlName];
          if (!thisStatRange)
            return;
          if (!min || thisStatRange.min < min)
            min = Number(thisStatRange.min);
          if (!max || thisStatRange.max > max)
            max = Number(thisStatRange.max);
        });
        if (!max && compareResults && compareResults.bases.unforged[htmlName])
          return [0, compareResults.bases.unforged[htmlName]];
        return [min, max];
      })();
      if (!graphMax)
        return;
      function calcX(value){
        return axisWidth * (value - graphMin) / (graphMax - graphMin);
      }
      const {min, max} = rangesByQuality[nativeQuality][htmlName];
      function calcPercentile(value){
        return (value - min) / (max - min);
      }

      const g = svg.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "g"));
      const gBackground = addInnerHTML(g, 'rect', `x="0" y="${y-23}" width="720" height="${35 + (15 * qualitiesToDisplay.length)}" fill="none"/>`);
      g.addEventListener('mouseover', () => gBackground.setAttribute('fill', 'yellow'));
      g.addEventListener('mouseleave', () => gBackground.setAttribute('fill', 'none'));
      addInnerHTML(g, 'text', 'x="10" y="' + y + '" fill="blue">' + htmlName + '</text>');
      //Draw each quality range:
      let nativeQualityY;
      qualitiesToDisplay.forEach(quality => {
        const [thisQualityMin, thisQualityMax] = [rangesByQuality[quality][htmlName].min, rangesByQuality[quality][htmlName].max];
        addInnerHTML(g, 'text', `x="${axisStart - 62}" y="${y}" fill="blue" >${quality.slice(0, 3)}</text>`);
        if (thisQualityMax !== 0){ //range is from compareResults
          addInnerHTML(g, 'text', `x="${axisStart - 10}" y="${y}" fill="blue" text-anchor="end">${thisQualityMin}</text>`);
          addInnerHTML(g, 'rect', `x="${axisStart + calcX(thisQualityMin)}" y="${y - 6}" width="${calcX(thisQualityMax) - calcX(thisQualityMin)}" height="4" fill="black"/>`);
          addInnerHTML(g, 'text', `x="${axisStart + axisWidth + 10}" y="${y}" fill="blue">${thisQualityMax}</text>`);
        }
        if (quality === nativeQuality)
          nativeQualityY = y;
        y += 15;
      });

      const origX = drawStatCircle(bases.unforged[htmlName]);

      if (compareResults && compareResults.bases.unforged[htmlName]){
        const compareX = drawStatCircle(compareResults.bases.unforged[htmlName], true);
        const [lowerX, higherX] = [origX, compareX].sort((a, b) => a - b);
        //ensure the green or red will be drawable and not offscreen:
        if (higherX - lowerX > 12 && !((lowerX < axisStart && higherX < axisStart) || (lowerX > axisStart + axisWidth && higherX > axisStart + axisWidth))){
          const startX = restrictToGraphRange(lowerX + 6, axisStart, axisWidth);
          const endX = restrictToGraphRange(higherX - 6, axisStart, axisWidth);
          const width = endX - startX;
          addInnerHTML(g, 'rect', `x="${startX}" y="${nativeQualityY - 9}" width="${width}" height="10" fill="${origX > compareX ? 'red' : '#00ff11'}"/>`);
        }
      } else if (compareResults && !compareResults.bases.unforged[htmlName]){
        const endX = restrictToGraphRange(origX - 6, axisStart, axisWidth);
        const width = endX - axisStart;
        addInnerHTML(g, 'rect', `x="${axisStart}" y="${nativeQualityY - 9}" width="${width}" height="10" fill="red"/>`);
      }

      function drawStatCircle(unforgedStat, below){
        const y = nativeQualityY;
        if (!unforgedStat && min === 0){
          addInnerHTML(g, 'text', `x="${axisStart}" y="${y + (below ? 13 : (-13))}" stroke="#DFEBED" stroke-width="8" paint-order="stroke" fill="black" text-anchor="middle">0 = 0%</text>`);
          addInnerHTML(g, 'circle', `cx="${axisStart}" cy="${y - 4}" r="6" stroke="black" fill="${below ? '#d391ff' : 'yellow'}"/>`);
          return;
        }
        const accuratePercentile = (unforgedStat - min) / (max - min); //better than integers 0-100
        const statStrToDraw = (Math.round(unforgedStat * 100) / 100) + ' = ' + Math.round(accuratePercentile * 100) + '%';
        const statXLoc = axisStart + calcX(unforgedStat);
        const visibleX = Math.min(Math.max(statXLoc, 40), 680);
        addInnerHTML(g, 'text', `x="${visibleX}" y="${y + (below ? 13 : (-13))}" stroke="#DFEBED" stroke-width="8" paint-order="stroke" fill="black" text-anchor="middle">${statStrToDraw}</text>`);
        addInnerHTML(g, 'circle', `cx="${statXLoc}" cy="${y - 4}" r="6" stroke="black" fill="${below ? '#d391ff' : 'yellow'}"/>`);
        return statXLoc;
      }

      y += 35;
    });
    svg.style.height = y + 'px';
  }
  function getRangesByQuality(infos){
    const qualitiesToShow = {};
    document.querySelectorAll('#checkboxContainer input').forEach(checkbox => {
      const checkboxQuality = checkbox.nextSibling.textContent;
      if (!checkbox.checked)
        return;
      const thisQualityEquipInfo = Object.assign(Object.assign({}, infos), {quality: checkboxQuality});
      const qualityRange = PercentileRanges.getRanges(thisQualityEquipInfo);
      qualitiesToShow[checkboxQuality] = qualityRange;
    });
    return qualitiesToShow;
  }
  function addInnerHTML(parent, elmName, continuedString){ //adds elements without dereferencing variable/listener assignments for earlier elements in parent - unlike plain .innerHTML +=
    parent.appendChild(document.createElement(elmName)).outerHTML = '<' + elmName + ' ' + continuedString;
    return parent.lastChild;
  }

  function restrictToGraphRange(x, axisStart, axisWidth){
    if (x > axisStart + axisWidth)
      return axisStart + axisWidth;
    if (x < axisStart)
      return axisStart;
    return x;
  }

  function selectElementText(element){
    if (document.selection) {
      const range = document.body.createTextRange();
      range.moveToElementText(element);
      range.select();
    } else if (window.getSelection) {
      const range = document.createRange();
      range.selectNode(element);
      window.getSelection().addRange(range);
    }
  }
  function clearSelection(){
    if (document.selection)
      document.selection.empty();
    else if (window.getSelection)
      window.getSelection().removeAllRanges();
  }
}

DocumentInteractor();



/*
//Example, getting information from multiple equipments at once:

const statusElm = document.body.appendChild(document.createElement('div'));
statusElm.textContent = 'Ready';
const infoArea = document.body.appendChild(document.createElement('textarea'));
infoArea.style.width = '1300px';
infoArea.style.height = '100px';
PercentileRanges.addEquipLink('https://hentaiverse.org/equip/82053309/dde67e38ef');
PercentileRanges.addEquipLink('https://hentaiverse.org/equip/125808352/17b7a8e553');
PercentileRanges.addEquipLink('https://hentaiverse.org/equip/117993212/8204116269', 'Magnificent');
PercentileRanges.run(results => {
  [82053309, 125808352, 117993212].forEach(eid => {
    const result = results[eid];
    const {percentiles, bases, summaries, infos} = result;
    infoArea.value += infos.name + ' : ' + summaries.extended + '\n';
  })
}, statusElm);
*/

/*
`Results` is a big associative array:
If multiple equipments are added before .run, then results indexed by eid: result = results[eid]
Or, if .addEquipDocument is called, and it's the only equip added before .run, then `results` is simply the result of that equip, no eid involved

Each result is an associative array of:
  infos: associative array of properties pertaining to this equip: name, level, type, slot, etc. See getEquipInfo function.
  percentiles:
    percentiles.unforged contains unforged percentiles indexed by the htmlName of the stat. Eg "percentiles.unforged['Holy EDB'] === 70"
    percentiles.forged   contains   forged percentiles indexed by the htmlName of the stat
  bases:
    bases.unforged contains unforged base values indexed by the htmlName of the stat. Eg "percentiles.unforged['Holy EDB'] === 12.54"
    bases.forged   contains   forged base values indexed by the htmlName of the stat
  ranges:
    associative array of mins and maxes (base values), indexed by the htmlName of the stat
  summaries
    summaries.extended contains a string describing some relevant stats of the equipment(customizable at the top). Eg "summaries.extended === '500, Holy EDB 70%, Int 80%, Wis 29%, Evd 19%, Agi 44%, Pmit 84%, Mmit 46%'"
    summaries.short contains a string describing only the most relevant stats of the equipment (customizable at the top)
 transformations, contains functions used by the built-in DocumentInteractor to show info for each stat on keypress
*/







