WoD 额外装备信息统计

Displays number of extra stats your armor gives you.

// ==UserScript==// [WoD] Extra Equipment Stats
// Version 1.14, 2014-08-08
// Script aimed at players of World Of Dungeons. Displays number of extra stats your armor gives you.
//
// When you enter your attributes page, a new button will appear at the bottom of page.
// Pressing this button will fetch info about equipment your hero is wearing and display
// cumulative info (like total attack bonus, total defense bonus, etc...) at the bottom of the page.
//-----------------------------------------------------------------------------

//-----------------------------------------------------------------------------
// Changelog
// 1.14
// - fixing english Damage Sensitivity
//
// 1.13
// - updating german and french set related translations
// - "Created with" link added
//
// 1.12
// - first try at fixing set item boni multiple calculation
//
// 1.11
// - grant/downloadURL metadata added
//
// 1.10
// - fixed loading of equipment page related to change of url parameters
//
// 1.9
// - added german translation
// - fix, again :(, for "+hero level" parsing bug
//
// 1.8
// - fixed attribute bonus parsing for french ("Attribut" -> "Particularité")
//
// 1.7
// - fix in bonus addition and calculation introduced by modifications for french server
// - some fixes to french translations
// - internal optimization of some calculations
//
// 1.6
// - will use local storage instead of GM_g(s)etvalue if local storage is supported
// - rounding now executed without eval :)
// - now using JSON.parse/stringify (where available) instead of eval/toSource
// - items with ' in names now correctly displayed
//
// 1.5
// - Math.Round() doesn't exist, replaced by Math.round()
// - fixed parsing bug due to design changes
// - included french translation and allowed all wod sites to use since should not depend on language used.
//   (clan monument analysis not tested in french)
// - fix of HL replace when HL is included in HL_Per.
// - added Finargol and taitoune to contributor list, thanks for helping on french version
// - added check for existance of GM_ functions so Opera users can use it too (some functionality missing)
// - rounding down or to the nearest integer is now language dependent (to accomodate behavior of french server)
//
// 1.4
// - status message now displays equipment name
// - fixed parsing bug due to design changes
// - fixed small bug when (z) explanation was not shown in some cases
// - first (of many :() trial on including update notifier script
//
// 1.3
// - saving last calculated result and displaying immediately on page load
// - removed "!! " from "reset point required to remove" items
// - changed way items are displayed when multiple items contribute to single stat (no more button to press)
//
// 1.2
// - fix for "+hero level" parsing bug
//
// 1.1
// - button disabled while equipment info is being fetched/calculated to prevent doing it multiple times simultaneously
// - status text moved below button so it doesn't "jump" up/down when status is empty/not empty
// - removed usage count from names of consumable items
// - changed way how items that can be worn multiple times are displayed, if effect is "only when used with" it will be
//   displayed only once, if not it will be displayed once but with x2, x3, x4.... appended to item name (and effect multiplied naturally)
//   I.E.
//      if 3 song books ballad of evasion are worn it will show only once in bonus for the level of the skill area (with (a) modifier)
//      if 3 skulls are worn it will show only once as "skull x3" having spell defense bonus equal to 3
// - added calculation of current bonus/malus value based on current hero level
// - added clan monument stats into calculations
// - fixed problem with bonus for the effect of the skill caused by additional description being added above effect bonus table
//
// 1.0
// - initial release
//-----------------------------------------------------------------------------

// ==UserScript==
// @name            WoD 额外装备信息统计
// @icon            http://info.world-of-dungeons.org/wod/css/WOD.gif
// @namespace       tomy
// @description     Displays number of extra stats your armor gives you.
// @include         http*://*.world-of-dungeons.*/wod/spiel/hero/attributes.php*
// @author          Tomy
// @contributor     Finargol, taitoune, Mastermage
// @copyright       2010+, Tomy
// @grant           GM_getValue
// @grant           GM_setValue
// @modifier        Christophero
// @version         2022.11.15.1
// ==/UserScript==

var DEBUG = false;
var VER = "1.18";
var LOCAL_VAR_NAME = "WOD ARMOR STATS " + location.host;

var Equipment = false;
var Result = false;
var Attribs = false;
var Level = false;
var Effect = false;
var DamageTaken = false;
var Defense = false;
var Damage = false;
var Attack = false;
var SetArray = false;
var SetCollection = false;

var HeroLevel = undefined;

var KeyButton = null;

String.prototype.endsWith = function (suffix) {
  return this.indexOf(suffix, this.length - suffix.length) !== -1;
};

String.prototype.startsWith = function (prefix) {
  return this.indexOf(prefix) == 0;
};

String.prototype.removeRight = function (suffix) {
  if (!this.endsWith(suffix)) return String(this);
  return String(this).substring(0, this.length - suffix.length);
};

String.prototype.trim = function () {
  return this.replace(/^\s+|\s+$/g, "");
};

String.prototype.space_clear = function () {
  var tmp = this;
  while (tmp.indexOf("  ") !== -1) tmp = tmp.replace("  ", "");
  return tmp;
};

function isFunctionDefined(name) {
  return eval("typeof " + name + " == 'function'");
}

function supportsLocalStorage() {
  try {
    return "localStorage" in window && window["localStorage"] !== null;
  } catch (e) {
    return false;
  }
}

function stringify(data) {
  if (typeof JSON == "object" && typeof JSON.stringify == "function")
    return JSON.stringify(data);
  else return data.toSource();
}

function parse(data) {
  if (typeof JSON == "object" && typeof JSON.parse == "function")
    return JSON.parse(data);
  else return eval(data);
}

function getLocalValue(name, defValue) {
  var ret = defValue;
  if (supportsLocalStorage()) {
    try {
      ret = window.localStorage[name];
    } catch (e) {}
  }

  if (ret == defValue && isFunctionDefined("GM_getValue"))
    return GM_getValue(name, defValue);

  if (ret != defValue) ret = parse(ret);
  return ret;
}

function setLocalValue(name, value) {
  var tmp = stringify(value);
  if (supportsLocalStorage()) {
    try {
      window.localStorage[name] = tmp;
      return true;
    } catch (e) {}
  }

  if (isFunctionDefined("GM_setValue")) {
    GM_setValue(name, tmp);
    return true;
  }

  return false;
}

function DebugMsg(Data) {
  if (DEBUG) alert(JSON.stringify(Data, null, 4));
}

function SetStatus(text, index, max, after) {
  var tmpText = text;
  if (max > 0) text = text + " (" + index + "/" + max + ")";
  if (after != undefined) text += " - " + after;
  Result.innerHTML = "<h3>" + text + "<h3>";
}

function trim(data) {
  // Use ECMA-262 Edition 3 String and RegExp features
  data = data.replace(/[\t\n\r ]+/g, " ");
  if (data.charAt(0) == " ") data = data.substring(1, data.length);
  if (data.charAt(data.length - 1) == " ")
    data = data.substring(0, data.length - 1);
  return data;
}

var Contents = {
  en: {
    Button_Name: "Calculate additional equipment stats",
    Title: "<h1>Additional equipment statistics</h1>Last calculated on ",
    Fetch_Hero: "Fetching hero info",
    Fetch_Clan: "Fetching clan info",
    Fetch_Equipment: "Fetching equipment list",
    Fetch_Info: "Fetching equipment info",
    Armor_Bonus: "Armor bonus",
    Attr_Bonus: "Attribute bonus",
    Level_Bonus: "Bonus for the level of the skill",
    Effect_Bonus: "Bonus for the effect of the skill",
    Damage_Taken: "Damage sensitivity",
    Defense_Bonus: "Defense bonus",
    Damage_Bonus: "Damage bonus",
    Damage_BonusR: "Damage Bonus (r)",
    Attack_Bonus: "Attack bonus",
    Dungeon_Bonus: "Bonus on loot from Dungeons",
    Item: "Item",
    Attribute: "Attribute",
    Skill: "Skill",
    Modifier: "Modifier",
    Value: "Value",
    Used_With: " (a)",
    Dmg_With: " (z)",
    Per: "%",
    HL_Per: "% of your hero`s level",
    HL: "hero level",
    Mult: " x ",
    Attack_Type: "Attack Type",
    Dmg_Split: " / ",
    Damage_Type: "Damage Type",
    BonusR: "Bonus (r)",
    Owner_Effect: "Effect on the owner of this item",
    Race_Name: "race",
    Level: "Level",
    Adv_Disadv: "advantages - disadvantages",
    Monument: "monument",
    Clan_Has_Monument: "The clan owns the monument",
    Link: "Link ...",
    Item_Skill:
      "the penalty or bonus are active when the item is used with one of the above mentioned skills.",
    Effect_BonusTalent:
      "<p>Increase the bonus on the <b>effect</b>, <u>not</u> the <b>level</b> of the skill. The bonuses will be added to the amount of <b>damage</b> (when using that type of attack) or the amount of <b>healing</b> (when healing).</p>",
    Damage_Added: "additionally -  when damage of this type is dealt",
    Damage_Effect:
      "is added upon normal / good / critical hits as an additional <i>effect</i> to the weapon used.",
    Rounding: Math.floor,
    All_Hits: "by normal / good / critical hits",
    Details: "details",
    Set: "Set",
    Copyright: "Created with Extra Equipment Stats",
  },
  cn: {
    Button_Name: "Calculate additional equipment stats",
    Title: "<h1>Additional equipment statistics</h1>Last calculated on ",
    Fetch_Hero: "Fetching hero info",
    Fetch_Clan: "Fetching clan info",
    Fetch_Equipment: "Fetching equipment list",
    Fetch_Info: "Fetching equipment info",
    Armor_Bonus: "护甲奖励",
    Attr_Bonus: "属性奖励",
    Level_Bonus: "对技能等级的奖励",
    Effect_Bonus: "对技能效果的奖励",
    Damage_Taken: "对此种攻击方式,攻击类型伤害的脆弱性",
    Defense_Bonus: "防御奖励",
    Damage_Bonus: "伤害奖励",
    Damage_BonusR: "伤害奖励 (r)",
    Attack_Bonus: "攻击奖励",
    Dungeon_Bonus: "地城探险得到的物品掉落奖励",
    Item: "物品",
    Attribute: "属性",
    Skill: "技能",
    Modifier: "修正",
    Value: "Value",
    Used_With: " (a)",
    Dmg_With: " (z)",
    Per: "%",
    HL_Per: "%×英雄等级",
    HL: "英雄等级",
    Mult: " x ",
    Attack_Type: "攻击方式",
    Dmg_Split: " / ",
    Damage_Type: "伤害方式",
    BonusR: "奖励 (r)",
    Owner_Effect: "作用在物品持有者上的效果",
    Owner_Effect2: "作用在人物 XXX身上的奖励效果:",
    Race_Name: "种族",
    Level: "级别",
    Adv_Disadv: "职业特性",
    Monument: "纪念碑",
    Clan_Has_Monument: "联盟纪念碑",
    Link: "链接...",
    Item_Skill:
      "只有在该物品可以使用的技能下使用此物品时,才会有这些奖惩效果。",
    Effect_BonusTalent:
      "奖励技能的效果,不是技能的等级。此奖励会增加到伤害(当使用该种攻击方式时)或者治疗量(当治疗时)。",
    Damage_Added:
      "只有当已经形成该攻击方式,该类型的伤害后,所标注的伤害奖励才会附加上去,否则不会附加",
    Damage_Effect:
      "当普通/重击/致命时,(伤害)<i>效果</i>附加在使用的武器上。",
    Rounding: Math.floor,
    All_Hits: "普通/重击/致命",
    Details: "详细信息",
    Set: "套装",
    Copyright: "Created with Extra Equipment Stats",
  },
  fr: {
    Button_Name: "Calculer les stats",
    Title: "<h1>Stats d'équipement supplémentaires.</h1>Derniere mise a jour ",
    Fetch_Hero: "Recherche du héros",
    Fetch_Clan: "Recherche du clan",
    Fetch_Equipment: "Recherche de l'équipement",
    Fetch_Info: "Recherche des infos",
    Armor_Bonus: "Bonus d'armure",
    Attr_Bonus: "Bonus sur les particularités",
    Level_Bonus: "Bonus sur le rang de talents",
    Effect_Bonus: "Bonus sur l'effet de talents",
    Damage_Taken: "Bonus sur la sensibilité aux dégâts",
    Defense_Bonus: "Bonus sur les parades",
    Damage_Bonus: "Bonus sur les dégâts",
    Damage_BonusR: "Bonus de dégâts (r)",
    Attack_Bonus: "Bonus sur les attaques",
    Dungeon_Bonus: "Bonus de combat en donjon",
    Item: "Objet",
    Attribute: "Particularité",
    Skill: "Talent",
    Modifier: "Modificateur",
    Value: "Valeur",
    Used_With: " (a)",
    Dmg_With: " (z)",
    Per: "%",
    HL_Per: "% du niveau du héros",
    HL: "du niveau du héros",
    Mult: " x ",
    Attack_Type: "Sorte d'attaque",
    Dmg_Split: " / ",
    Damage_Type: "Sorte de dégâts",
    BonusR: "Bonus (r)",
    Owner_Effect: "Effets sur le propriétaire de l'objet",
    Race_Name: "Peuple",
    Level: "Niveau",
    Adv_Disadv: "Avantages et inconvénients",
    Monument: "monument",
    Clan_Has_Monument: "Le clan possede le monument",
    Link: "Lien ...",
    Item_Skill:
      "uniquement quand l'objet est utilisé en combination avec l'un des talents indiqués ci-dessus",
    Effect_BonusTalent:
      "<p>Le bonus sur l'<b>Effet</b> n'augmente pas le <b>Rang</b> du talent. Au lieu de ça, les bonus sont ajoutés aux <b>dommages</b> (pour le type 'Attaques') ou aux <b>soins</b> (pour le type 'Guérison').</p>",
    Damage_Added:
      "Supplémentaire - uniquement quand des dégâts de ce genre sont causés",
    Damage_Effect:
      "lors de coups normaux / complets / critiques, est ajouté a l'effet de l'arme utilisée.",
    Rounding: Math.round,
    All_Hits: "lors de succès normaux / complets / critiques",
    Details: "Détails",
    Set: "Set",
    Copyright: "Créé avec Extra Equipment Stats",
  },
  de: {
    Button_Name: "Berechne Ausrüstungs-Boni",
    Title: "<h1>Boni durch Ausrüstung.</h1>Zuletzt aktualisiert am: ",
    Fetch_Hero: "Durchsuche Helden...",
    Fetch_Clan: "Durchsuche Clan...",
    Fetch_Equipment: "Durchsuche Ausrüstung...",
    Fetch_Info: "Sammle Informationen...",
    Armor_Bonus: "Boni auf Rüstung",
    Attr_Bonus: "Boni auf Eigenschaften",
    Level_Bonus: "Boni auf den Rang von Fertigkeiten",
    Effect_Bonus: "Boni auf die Wirkung von Fertigkeiten",
    Damage_Taken: "Boni auf die Anfälligkeit gegen Schäden", //here the correct german text is: "Boni auf die <a href="https://world-of-dungeons.de/ency/Anf%C3%A4lligkeit">Anfälligkeit</a> gegen Schäden"
    Defense_Bonus: "Boni auf Paraden",
    Damage_Bonus: "Boni auf Schaden",
    Damage_BonusR: "Schadensbonus (r)",
    Attack_Bonus: "Boni auf Angriffe",
    Dungeon_Bonus: "Boni auf Beute aus Dungeonkämpfen",
    Item: "Gegenstand",
    Attribute: "Eigenschaft",
    Skill: "Fertigkeit",
    Modifier: "Modifikator",
    Value: "Wert", //not sure which text is asked for here
    Used_With: " (a)",
    Dmg_With: " (z)",
    Per: "%",
    HL_Per: "% der Heldenstufe",
    HL: "Heldenstufe",
    Mult: " x ",
    Attack_Type: "Angriffsart",
    Dmg_Split: " / ",
    Damage_Type: "Schadensart",
    BonusR: "Bonus (r)",
    Owner_Effect: "Auswirkungen auf den Besitzer des Gegenstands",
    Race_Name: "Volk",
    Level: "Heldenstufe",
    Adv_Disadv: "Vor- und Nachteile",
    Monument: "Monument",
    Clan_Has_Monument: "Der Clan besitzt das Monument",
    Link: "Link ...",
    Item_Skill:
      "nur, während der Gegenstand mit einer der oben genannten Fertigkeiten angewendet wird",
    Effect_BonusTalent:
      'Boni auf die <b>Wirkung</b> erhöhen <u>nicht</u> den <b>Rang</b> einer Fertigkeit. Stattdessen werden die Boni zur Höhe des <b>Schadens</b> (beim Typ "Angriff") oder der <b>Heilung</b> (beim Typ "Heilung") addiert.',
    Damage_Added:
      "zusätzlich - nur wenn schon Schaden dieser Art verursacht wird",
    Damage_Effect:
      "wird bei normalen / guten / kritischen Treffern zur <i>Wirkung</i> der benutzten Waffe addiert.",
    Rounding: Math.floor,
    All_Hits: "bei normalen / guten / kritischen Treffern",
    Details: "Details",
    Set: "Set",
    Copyright: "Erstellt mit Extra Equipment Stats",
  },
};

var MandatoryProps = {};

try {
  Main();
} catch (e) {
  alert("Main(): " + e);
}

// FUNCTIONS //////////////////////////////////////////////////////////////////

function Main() {
  // Language selection
  if (GetLocalContents() == null) return;

  MandatoryProps[Contents.Attr_Bonus] = [Contents.Attribute, Contents.Modifier];
  MandatoryProps[Contents.Level_Bonus] = [Contents.Skill, Contents.Modifier];
  MandatoryProps[Contents.Effect_Bonus] = [Contents.Skill, Contents.Modifier];
  MandatoryProps[Contents.Damage_Taken] = [
    Contents.Attack_Type,
    Contents.BonusR,
    Contents.Damage_Type,
  ];
  MandatoryProps[Contents.Attack_Bonus] = [
    Contents.Attack_Type,
    Contents.Modifier,
  ];
  MandatoryProps[Contents.Damage_Bonus] = [
    Contents.Attack_Type,
    Contents.Damage_BonusR,
    Contents.Damage_Type,
  ];
  MandatoryProps[Contents.Defense_Bonus] = [
    Contents.Attack_Type,
    Contents.Modifier,
  ];
  MandatoryProps[Contents.Armor_Bonus] = [];
  MandatoryProps[Contents.Dungeon_Bonus] = [];

  // Add buttons
  KeyButton = AddButtonBeforeHints(Contents.Button_Name, OnCountStat);

  if (KeyButton == null) return;
  var nHeroId = GetHiddenInfo(document, "session_hero_id", "");
  var tmp = getLocalValue(LOCAL_VAR_NAME + nHeroId, undefined);
  if (tmp != undefined && tmp.version == VER) {
    Result.innerHTML = tmp.text;
  }
}

// It will only add the first eligible button
// return: the node of the first eligible disabled button, or null if didn't find anyone
function AddButtonBeforeHints(ButtonText, ButtonFunct) {
  var allInputs = document.getElementsByTagName("div");
  for (var i = 0; i < allInputs.length; ++i) {
    if (
      allInputs[i].className == "hints on" ||
      allInputs[i].className == "hints off"
    ) {
      var newTable = document.createElement("table");
      var newTR = document.createElement("tr");
      var newTD = document.createElement("td");
      var resTR = document.createElement("tr");
      Result = document.createElement("td");
      var newButton = document.createElement("input");
      newButton.setAttribute("type", "button");
      newButton.setAttribute("class", "button");
      newButton.setAttribute("value", ButtonText);
      newButton.addEventListener("click", ButtonFunct, false);
      newTable.appendChild(newTR);
      newTR.appendChild(newTD);
      newTD.appendChild(newButton);
      newTable.appendChild(resTR);
      resTR.appendChild(Result);
      allInputs[i].parentNode.insertBefore(newTable, allInputs[i]);
      var newP1 = document.createElement("br");
      var newP2 = document.createElement("br");
      allInputs[i].parentNode.insertBefore(newP1, newTable);
      allInputs[i].parentNode.insertBefore(newP2, newTable);
      return newButton;
    }
  }
  return null;
}

// Choose contents of the corresponding language
// Contents: { "lang1": { "Name1": "Value1", ..., "NameN": "ValueN" }, ..., "langN": { ... } ... }
// return: Local contents, or null
// It will edit the input contents directly, so the returned object is not necessary
function GetLocalContents() {
  function GetLanguage() {
    var langText = null;
    var allMetas = document.getElementsByTagName("meta");
    for (var i = 0; i < allMetas.length; ++i) {
      if (allMetas[i].httpEquiv == "Content-Language") {
        langText = allMetas[i].content;
        break;
      }
    }
    return langText;
  }

  var lang = GetLanguage();
  if (lang == null) return null;

  if (Contents instanceof Object) {
    Contents = Contents[lang];
    return Contents;
  } else return null;
}

function Value(abs, per, hlper) {
  if (!abs) abs = 0;
  if (!per) per = 0;
  if (!hlper) hlper = 0;

  this.abs = abs;
  this.per = per;
  this.hlper = hlper;
}

Value.Parse = function (text) {
  if (!text || text.trim().length == 0) text = "";

  var hl = text.replace(Contents.Mult + Contents.HL, "00" + Contents.HL_Per);
  hl = hl.replace("+ " + Contents.HL, "+100" + Contents.HL_Per);
  hl = hl.replace("+" + Contents.HL, "+100" + Contents.HL_Per);
  hl = hl.replace("- " + Contents.HL, "-100" + Contents.HL_Per);
  hl = hl.replace("-" + Contents.HL, "-100" + Contents.HL_Per);
  hl = hl.replace(Contents.HL_Per, "HL");

  var abs = 0;
  var per = 0;
  var hlper = 0;

  var arr = hl.split(" ");
  var txt = "";
  for (var j = 0; j < arr.length; ++j) {
    if (arr[j].endsWith(Contents.Per))
      per = arr[j].removeRight(Contents.Per) * 1;
    else if (arr[j].endsWith("HL")) hlper = arr[j].removeRight("HL") * 1;
    else abs = arr[j] * 1;
  }

  return new Value(abs, per, hlper);
};

Value.prototype.Add = function (RightVal) {
  return new Value(
    this.abs + RightVal.abs,
    this.per + RightVal.per,
    this.hlper + RightVal.hlper
  );
};

Value.prototype.Mult = function (rhs) {
  return new Value(this.abs * rhs, this.per * rhs, this.hlper * rhs);
};

Value.prototype.Html = function (negative) {
  var txt = "";
  if (negative == undefined) negative = false;
  var mult = negative ? -1 : 1;

  if (this.abs != 0) {
    txt +=
      '<span class="gem_' +
      (this.abs * mult > 0 ? "bonus" : "malus") +
      '">' +
      (this.abs > 0 ? "+" + this.abs : this.abs) +
      "</span> ";
  }

  if (this.per != 0) {
    txt +=
      '<span class="gem_' +
      (this.per * mult > 0 ? "bonus" : "malus") +
      '">' +
      (this.per > 0
        ? "+" + this.per /*.toFixed(2)*/
        : this.per) /*.toFixed(2)*/ +
      Contents.Per +
      "</span> ";
  }

  if (this.hlper != 0) {
    if (Math.abs(this.hlper) == 100) {
      txt +=
        '<span class="gem_' +
        (this.hlper * mult > 0 ? "bonus" : "malus") +
        '">' +
        (this.hlper > 0 ? "+" : "-") +
        Contents.HL +
        "</span> ";
    } else if (Math.abs(this.hlper) % 100 == 0) {
      txt +=
        '<span class="gem_' +
        (this.hlper * mult > 0 ? "bonus" : "malus") +
        '">' +
        (this.hlper > 0 ? "+" : "") +
        Math.round(this.hlper / 100) +
        Contents.Mult +
        Contents.HL +
        "</span> ";
    } else {
      txt +=
        '<span class="gem_' +
        (this.hlper * mult > 0 ? "bonus" : "malus") +
        '">' +
        (this.hlper > 0 ? "+" : "") +
        this.hlper +
        Contents.HL_Per +
        "</span> ";
    }
  }

  if (txt.length == 0) txt = "0";

  return txt.trim();
};

Value.prototype.Calc = function (hero_level) {
  return new Value(
    Contents.Rounding(this.abs + (hero_level * this.hlper) / 100),
    this.per,
    0
  );
};

function Dmg(normal, good, critical) {
  if (!normal) normal = new Value();
  if (!good) good = new Value();
  if (!critical) critical = new Value();

  this.normal = normal;
  this.good = good;
  this.critical = critical;
}

Dmg.Parse = function (text) {
  if (!text || text.trim().length == 0) {
    return new Dmg();
  }

  var arr = text.split(Contents.Dmg_Split);
  return new Dmg(Value.Parse(arr[0]), Value.Parse(arr[1]), Value.Parse(arr[2]));
};

Dmg.prototype.Add = function (RightVal) {
  return new Dmg(
    this.normal.Add(RightVal.normal),
    this.good.Add(RightVal.good),
    this.critical.Add(RightVal.critical)
  );
};

Dmg.prototype.Mult = function (rhs) {
  return new Dmg(
    this.normal.Mult(rhs),
    this.good.Mult(rhs),
    this.critical.Mult(rhs)
  );
};

Dmg.prototype.Html = function (negative) {
  return (
    this.normal.Html(negative) +
    Contents.Dmg_Split +
    this.good.Html(negative) +
    Contents.Dmg_Split +
    this.critical.Html(negative)
  );
};

Dmg.prototype.Calc = function (hero_level) {
  return new Dmg(
    this.normal.Calc(hero_level),
    this.good.Calc(hero_level),
    this.critical.Calc(hero_level)
  );
};

function OnCountStat() {
  try {
    if (this.className == "button_disabled") return;
    else this.className = "button_disabled";

    Result.innerHTML = "";
    Attribs = new Object();
    Level = new Object();
    Effect = new Object();
    DamageTaken = new Object();
    Defense = new Object();
    Damage = new Object();
    Attack = new Object();
    Equipment = new Array();
    SetArray = new Array();
    SetCollection = new Object();

    var nHeroId = GetHiddenInfo(document, "session_hero_id", "");
    var nPlayerId = GetHiddenInfo(document, "session_player_id", "");

    GetHeroInfo(nHeroId, nPlayerId);
  } catch (e) {
    alert("OnCountStat(): " + e);
  }
}

function GetHeroInfo(heroID, playerID) {
  var XmlHttp = new XMLHttpRequest();

  XmlHttp.onreadystatechange = function () {
    try {
      if (XmlHttp.readyState == 4 && XmlHttp.status == 200) {
        var Page = document.createElement("div");
        Page.innerHTML = XmlHttp.responseText;
        ReadHeroInfo(Page);
        GetClanInfo(heroID, playerID);
      }
    } catch (e) {
      alert("XMLHttpRequest.onreadystatechange(): " + e);
    }
  };

  var URL =
    location.protocol +
    "//" +
    location.host +
    "/wod/spiel/hero/profile.php?id=" +
    heroID +
    "&session_hero_id=" +
    heroID +
    "&IS_POPUP=1";

  SetStatus(Contents.Fetch_Hero, 0, 0);

  XmlHttp.open("GET", URL, true);
  XmlHttp.send(null);
}

function ReadHeroInfo(Document) {
  var allTD = Document.getElementsByTagName("td");
  var url = "";
  for (var i = 0; i < allTD.length - 1; ++i) {
    if (allTD[i].innerHTML == Contents.Race_Name) {
      var tmpName = allTD[i + 1].textContent.trim().removeRight("*");
      var allA = allTD[i + 1].getElementsByTagName("a");
      if (allA.length == 1) {
        var href = allA[0].getAttribute("href");
        Equipment.push({
          id: 0,
          name: tmpName,
          link: href,
          count: 1,
          okH2: Contents.Adv_Disadv,
        });
      } else {
        alert("ReadHeroInfo failed " + allA.length);
      }
    } else if (allTD[i].innerHTML == Contents.Level) {
      HeroLevel = allTD[i + 1].textContent.trim();
    }
  }
}

function GetClanInfo(heroID, playerID) {
  var XmlHttp = new XMLHttpRequest();

  XmlHttp.onreadystatechange = function () {
    try {
      if (XmlHttp.readyState == 4 && XmlHttp.status == 200) {
        var Page = document.createElement("div");
        Page.innerHTML = XmlHttp.responseText;
        ReadClanInfo(Page);
        GetEquipment(heroID, playerID);
      }
    } catch (e) {
      alert("XMLHttpRequest.onreadystatechange(): " + e);
    }
  };

  var URL =
    location.protocol +
    "//" +
    location.host +
    "/wod/spiel/clan/clan.php?session_hero_id=" +
    heroID +
    "&IS_POPUP=1";

  SetStatus(Contents.Fetch_Clan, 0, 0);

  XmlHttp.open("GET", URL, true);
  XmlHttp.send(null);
}

function ReadClanInfo(Document) {
  var allH2 = Document.getElementsByTagName("h2");
  for (var j = 0; j < allH2.length; ++j) {
    if (trim(allH2[j].textContent) != Contents.Monument) continue;

    var firstSibling = allH2[j].nextSibling;
    var nextSibling = firstSibling.nextSibling;

    if (
      firstSibling.nodeName == "#text" &&
      nextSibling.nodeName == "A" &&
      trim(firstSibling.textContent) == Contents.Clan_Has_Monument
    ) {
      Equipment.push({
        id: 0,
        name: nextSibling.textContent,
        link: nextSibling.getAttribute("href"),
        count: 1,
        okH2: undefined,
      });
    }
  }
}

function GetHiddenInfo(Document, InfoName, DefaultValue) {
  var allInputs = Document.getElementsByTagName("input");
  for (var i = 0; i < allInputs.length; ++i) {
    if (
      allInputs[i].getAttribute("type") == "hidden" &&
      allInputs[i].name == InfoName
    )
      return allInputs[i].value;
  }
  return DefaultValue;
}

function GetEquipment(heroID, playerID) {
  var XmlHttp = new XMLHttpRequest();

  XmlHttp.onreadystatechange = function () {
    try {
      if (XmlHttp.readyState == 4 && XmlHttp.status == 200) {
        var Page = document.createElement("div");
        Page.innerHTML = XmlHttp.responseText;
        ReadEquipment(Page, heroID, playerID);
        GetItem(0, heroID, playerID);
      }
    } catch (e) {
      alert("XMLHttpRequest.onreadystatechange(): " + e);
    }
  };

  var URL =
    location.protocol +
    "//" +
    location.host +
    "/wod/spiel/hero/items.php" +
    "?view=gear" +
    "&session_hero_id=" +
    heroID;

  SetStatus(Contents.Fetch_Equipment, 0, 0);

  XmlHttp.open("GET", URL, true);
  XmlHttp.send(null);
}

function ReadEquipment(Document, heroID, playerID) {
  var allForms = Document.getElementsByTagName("form");

  for (var i = 0; i < allForms.length; ++i) {
    if (allForms[i].getAttribute("name") == "the_form") {
      var allTDs = allForms[i].getElementsByTagName("td");
      for (var k = 0; k < allTDs.length; ++k) {
        var allOptions = allTDs[k].getElementsByTagName("option");
        var allTable = allTDs[k].getElementsByTagName("table");
        if (allTable.length > 0 || allOptions.length == 0) continue;
        var lastID = undefined;
        var lastName = undefined;
        for (var j = 0; j < allOptions.length; ++j) {
          var tmpId = allOptions[j].getAttribute("value") * -1;
          if (tmpId > 0) lastID = tmpId;
          if (tmpId == 0) {
            var tmpName = allOptions[j].innerHTML;
            if (tmpName.charAt(tmpName.length - 1) == "!")
              tmpName = tmpName.substr(0, tmpName.length - 1);
            lastName = tmpName;
          }
        }
        if (lastID != undefined && lastName != undefined) {
          if (/[^\(]*\([0-9]*\/[0-9]*\)/.test(lastName)) {
            lastName = lastName.replace(/\([0-9]*\/[0-9]*\)/g, "");
          }
          if (lastName.startsWith("!! ")) lastName = lastName.substring(3);

          var found = false;
          //for (var z = 0; z < Equipment.length; ++z) {
          //  if (Equipment[z].name == lastName) {
          //      Equipment[z].count++;
          //      found = true;
          //      break;
          //  }
          //}
          if (!found)
            Equipment.push({
              id: lastID,
              name: lastName,
              link:
                "/wod/spiel/hero/item.php?item_instance_id=" +
                lastID +
                "&session_hero_id=" +
                heroID +
                "&session_player_id=" +
                playerID +
                "&is_popup=1",
              count: 1,
              okH2: Contents.Owner_Effect,
            });
        }
      }
    }
  }
}

function GetItem(index, heroID, playerID) {
  if (index == Equipment.length) {
    ReadSet(heroID);
    //GetSet(0, heroID);
    return;
  }

  var XmlHttp = new XMLHttpRequest();

  XmlHttp.onreadystatechange = function () {
    try {
      if (XmlHttp.readyState == 4 && XmlHttp.status == 200) {
        var Page = document.createElement("div");
        Page.innerHTML = XmlHttp.responseText;
        ParseItem(index, Page, false);
        GetItem(index + 1, heroID, playerID);
      }
    } catch (e) {
      alert("XMLHttpRequest.onreadystatechange(): " + e);
    }
  };

  var URL = location.protocol + "//" + location.host + Equipment[index].link;

  SetStatus(
    Contents.Fetch_Info,
    index + 1,
    Equipment.length,
    Equipment[index].name
  );

  XmlHttp.open("GET", URL, true);
  XmlHttp.send(null);
}

function CheckProperties(name, data, props) {
  var ret = true;

  for (var j = 0; j > props.length; ++j) {
    if (!data.hasOwnProperty(props[j])) {
      alert("Missing field + " + name + " - " + props[j]);
      ret = false;
    }
  }

  return ret;
}

function ReadSet(heroID) {
  var tmp_equipment_length = Equipment.length;
  // push sets into Equipments
  for (var j = 0; j < SetArray.length; j++) {
    Equipment.push({
      id: 0,
      name: SetArray[j],
      link:
        "/wod/spiel/hero/set.php" +
        "?name=" +
        SetArray[j] +
        "&simulate_item_count=" +
        SetCollection[SetArray[j]].count.toString() +
        "&is_popup=1",
      count: 1,
      okH2: Contents.Owner_Effect2,
    });
  }
  GetSet(tmp_equipment_length, heroID);
}

function GetSet(index, heroID) {
  if (index == Equipment.length) {
    DisplayResult(heroID);
    return;
  }

  var XmlHttp = new XMLHttpRequest();

  XmlHttp.onreadystatechange = function () {
    try {
      if (XmlHttp.readyState == 4 && XmlHttp.status == 200) {
        var Page = document.createElement("div");
        Page.innerHTML = XmlHttp.responseText;
        ParseItem(index, Page, true);
        GetSet(index + 1, heroID);
      }
    } catch (e) {
      alert("XMLHttpRequest.onreadystatechange(): " + e);
    }
  };

  var URL = location.protocol + "//" + location.host + Equipment[index].link;

  SetStatus(
    Contents.Fetch_Info,
    index + 1,
    Equipment.length,
    Equipment[index].name
  );

  XmlHttp.open("GET", URL, true);
  XmlHttp.send(null);
}

function CheckProperties(name, data, props) {
  var ret = true;

  for (var j = 0; j > props.length; ++j) {
    if (!data.hasOwnProperty(props[j])) {
      alert("Missing field + " + name + " - " + props[j]);
      ret = false;
    }
  }

  return ret;
}

function DeductSetValue(data, index, names, value, used_with, in_addition) {
  try {
    for (var mynamej in names) {
      //console.log(names[mynamej]);
      data = data[names[mynamej]];
    }
  } catch (e) {
    //console.log('extra_error '+SetArray[index] + ' '+ names[0] + ' ' + myname);
    return;
  }

  if (data == undefined) {
    //console.log('data undefined');
    return;
  }

  //console.log('extra deduct '+SetArray[index] + ' ' + names[0]);
  //console.log('extra deduct data len ' + data.length.toString());
  // data is now inner most array of {values, item, used_with, set_name}
  for (var j = 0; j < data.length; ++j) {
    var tmp_in_addition;
    if (data[j].hasOwnProperty("in_addition")) {
      tmp_in_addition = data[j]["in_addition"];
      //console.log(tmp_in_addition);
    } else {
      tmp_in_addition = false;
    }
    if (data[j].hasOwnProperty("used_with")) {
      tmp_used_with = data[j]["used_with"];
      //console.log(tmp_in_addition);
    } else {
      tmp_used_with = false;
    }
    //console.log('set name '+data[j]['set_name']);
    //console.log('item name ' + Equipment[data[j]['item']].name);
    if (
      data[j]["set_name"] == Equipment[index].name &&
      tmp_used_with == used_with &&
      tmp_in_addition == in_addition
    ) {
      //console.log('extra dect value '+SetArray[index] + '' + name[0]);
      data[j]["value"] = data[j]["value"].Add(value.Mult(-1));
    }
  }
}

function PushWithCreateSubarrays(data, names, value) {
  for (var j = 0; j < names.length; ++j) {
    if (!data.hasOwnProperty(names[j])) {
      if (j != names.length - 1) {
        data[names[j]] = new Object();
      } else {
        data[names[j]] = new Array();
      }
    }
    data = data[names[j]];
  }
  data.push(value);
}

function ParseItem(index, Document, spF) {
  var allChildren = undefined;
  var okH2 = Equipment[index].okH2;

  var setName = undefined;
  var allH2 = Document.getElementsByTagName("h2");
  if (allH2 != undefined) {
    for (var j = 0; j < allH2.length && setName == undefined; ++j) {
      if (trim(allH2[j].textContent) == Contents.Details) {
        var allTDs =
          allH2[j].nextSibling.nextSibling.getElementsByTagName("td");
        for (var k = 0; k < allTDs.length - 1 && setName == undefined; ++k) {
          if (trim(allTDs[k].textContent) == Contents.Set)
            setName = trim(allTDs[k + 1].textContent);
        }
      }
    }
  }

  if (setName != undefined && !spF) {
    if (SetCollection.hasOwnProperty(setName)) {
      SetCollection[setName].count++;
    } else {
      SetArray.push(setName);
      SetCollection[setName] = { count: 1 };
    }
  }

  if (okH2 != undefined) {
    for (var i = 0; i < allH2.length; ++i) {
      if (allH2[i].textContent == okH2) {
        allChildren = allH2[i].parentNode.childNodes;
        break;
      }
    }
  } else {
    allChildren = Document.getElementsByTagName("form")[0].childNodes;
  }

  if (allChildren != undefined) {
    var ok = okH2 == undefined;
    for (var j = 0; j < allChildren.length - 2; ++j) {
      if (allChildren[j].nodeName == "H2") {
        var tmpH2 = trim(allChildren[j].textContent);
        if (tmpH2.length > 0 && okH2 != undefined) {
          ok = tmpH2 == okH2;
        }
      } else if (
        ok &&
        allChildren[j].nodeName == "H3" &&
        (allChildren[j + 2].nodeName == "TABLE" ||
          (allChildren[j + 2].nodeName == "P" &&
            allChildren[j + 3].nodeName == "#text" &&
            allChildren[j + 4].nodeName == "TABLE"))
      ) {
        var name = trim(allChildren[j].textContent);
        var parsed = undefined;
        if (allChildren[j + 2].nodeName == "TABLE")
          parsed = ParseTable(allChildren[j + 2]);
        else parsed = ParseTable(allChildren[j + 4]);

        for (var k = 0; k < parsed.length; ++k) {
          if (
            MandatoryProps.hasOwnProperty(name) &&
            CheckProperties(name, parsed[k], MandatoryProps[name])
          ) {
            var set_name =
              setName != undefined && parsed[k].IsGem ? setName : undefined;
            if (name == Contents.Attr_Bonus) {
              if (parsed[k][Contents.Modifier].endsWith(Contents.Used_With)) {
                var tmp_names = [parsed[k][Contents.Attribute]];
                var tmp_value = Value.Parse(
                  parsed[k][Contents.Modifier].removeRight(Contents.Used_With)
                );
                var tmp_item = index;
                if (spF) {
                  DeductSetValue(
                    Attribs,
                    tmp_item,
                    tmp_names,
                    tmp_value,
                    true,
                    false
                  );
                }
                PushWithCreateSubarrays(Attribs, tmp_names, {
                  value: tmp_value,
                  item: index,
                  used_with: true,
                  set_name: set_name,
                });
              }
            } else if (name == Contents.Level_Bonus) {
              if (parsed[k][Contents.Modifier].endsWith(Contents.Used_With)) {
                var tmp_names = [parsed[k][Contents.Skill]];
                var tmp_value = Value.Parse(
                  parsed[k][Contents.Modifier].removeRight(Contents.Used_With)
                );
                var tmp_item = index;
                if (spF) {
                  DeductSetValue(
                    Level,
                    tmp_item,
                    tmp_names,
                    tmp_value,
                    true,
                    false
                  );
                }
                PushWithCreateSubarrays(Level, tmp_names, {
                  value: tmp_value,
                  item: index,
                  used_with: true,
                  set_name: set_name,
                });
              }
            } else if (name == Contents.Effect_Bonus) {
              var tmp_names = [parsed[k][Contents.Skill]];
              var tmp_value = Value.Parse(
                parsed[k][Contents.Modifier].removeRight(Contents.Used_With)
              );
              var tmp_item = index;
              var tmp_used_with = parsed[k][Contents.Modifier].endsWith(
                Contents.Used_With
              );
              if (spF) {
                DeductSetValue(
                  Effect,
                  tmp_item,
                  tmp_names,
                  tmp_value,
                  tmp_used_with,
                  false
                );
              }
              PushWithCreateSubarrays(Effect, tmp_names, {
                value: tmp_value,
                item: index,
                used_with: tmp_used_with,
                set_name: set_name,
              });
            } else if (name == Contents.Damage_Taken) {
              var tmp_names = [
                parsed[k][Contents.Damage_Type],
                parsed[k][Contents.Attack_Type].removeRight(Contents.Used_With),
              ];
              var tmp_value = Dmg.Parse(
                parsed[k][Contents.BonusR].removeRight(Contents.Dmg_With)
              );
              var tmp_item = index;
              var tmp_in_addition = parsed[k][Contents.BonusR].endsWith(
                Contents.Dmg_With
              );
              var tmp_used_with = parsed[k][Contents.Attack_Type].endsWith(
                Contents.Used_With
              );
              if (spF) {
                DeductSetValue(
                  DamageTaken,
                  tmp_item,
                  tmp_names,
                  tmp_value,
                  tmp_used_with,
                  tmp_in_addition
                );
              }
              PushWithCreateSubarrays(DamageTaken, tmp_names, {
                value: tmp_value,
                item: index,
                in_addition: tmp_in_addition,
                used_with: tmp_used_with,
                set_name: set_name,
              });
            } else if (name == Contents.Attack_Bonus) {
              var tmp_names = [parsed[k][Contents.Attack_Type]];
              var tmp_value = Value.Parse(
                parsed[k][Contents.Modifier].removeRight(Contents.Used_With)
              );
              var tmp_item = index;
              var tmp_used_with = parsed[k][Contents.Modifier].endsWith(
                Contents.Used_With
              );
              if (spF) {
                DeductSetValue(
                  Attack,
                  tmp_item,
                  tmp_names,
                  tmp_value,
                  tmp_used_with,
                  false
                );
              }
              PushWithCreateSubarrays(Attack, tmp_names, {
                value: tmp_value,
                item: index,
                used_with: tmp_used_with,
                set_name: set_name,
              });
            } else if (name == Contents.Damage_Bonus) {
              var tmp_names = [
                parsed[k][Contents.Damage_Type],
                parsed[k][Contents.Attack_Type].removeRight(Contents.Used_With),
              ];
              var tmp_value = Dmg.Parse(
                parsed[k][Contents.Damage_BonusR].removeRight(Contents.Dmg_With)
              );
              var tmp_item = index;
              var tmp_in_addition = parsed[k][Contents.Damage_BonusR].endsWith(
                Contents.Dmg_With
              );
              var tmp_used_with = parsed[k][Contents.Attack_Type].endsWith(
                Contents.Used_With
              );
              if (spF) {
                DeductSetValue(
                  Damage,
                  tmp_item,
                  tmp_names,
                  tmp_value,
                  tmp_used_with,
                  tmp_in_addition
                );
              }
              PushWithCreateSubarrays(Damage, tmp_names, {
                value: tmp_value,
                item: index,
                in_addition: tmp_in_addition,
                used_with: tmp_used_with,
                set_name: set_name,
              });
            } else if (name == Contents.Defense_Bonus) {
              var tmp_names = [parsed[k][Contents.Attack_Type]];
              var tmp_value = Value.Parse(
                parsed[k][Contents.Modifier].removeRight(Contents.Used_With)
              );
              var tmp_item = index;
              var tmp_used_with = parsed[k][Contents.Modifier].endsWith(
                Contents.Used_With
              );
              if (spF) {
                DeductSetValue(
                  Defense,
                  tmp_item,
                  tmp_names,
                  tmp_value,
                  tmp_used_with,
                  false
                );
              }
              PushWithCreateSubarrays(Defense, tmp_names, {
                value: tmp_value,
                item: index,
                used_with: tmp_used_with,
                set_name: set_name,
              });
            }
          } else {
            if (okH2 != undefined || name != Contents.Link)
              alert("Unknown stat: " + name);
          }
        }
        j += allChildren[j + 2].nodeName == "TABLE" ? 2 : 4;
      }
    }
  }
}

function AddTableRow(isHeader, index, data, center) {
  var td = "td";
  if (isHeader) td = "th";

  var ret = "";
  if (data.length > 0) {
    ret = '<tr class="' + (isHeader ? "header" : "row" + (index % 2)) + '">';
    for (var j = 0; j < data.length; ++j) {
      var centertxt = "";
      if (center != undefined) {
        if (
          (typeof center == "object" && center[j]) ||
          (typeof center == "boolean" && center)
        )
          centertxt = ' align="center"';
      }
      ret +=
        "<" +
        td +
        ' class="content_table"' +
        centertxt +
        ">" +
        data[j] +
        "</" +
        td +
        ">";
    }
    ret += "</tr>";
  }

  return ret;
}

function GetEquipmentName(index, multiples) {
  return (
    Equipment[index].name.replace("'", "'") +
    (multiples && Equipment[index].count > 1
      ? " x" + Equipment[index].count
      : "")
  );
}

function GetEquipmentHref(index, multiples) {
  return (
    "<a onclick=\"return wo('" +
    Equipment[index].link +
    '&IS_POPUP=1\');" target="_blank" class="item_usable" href="' +
    Equipment[index].link +
    '&IS_POPUP=1" id="___wodToolTip_UniqueId__1">' +
    GetEquipmentName(index, multiples) +
    "</a>"
  );
}

function AddTable(heroID, Where, Data, heading, headers, desc) {
  var display = false;
  var txt = "<h3>" + heading + "</h3>";
  if (desc != undefined) txt += desc;
  txt += '<table class="content_table"><tbody>';
  txt += AddTableRow(true, 0, headers);

  var j = 0;
  var havea = false;
  for (var k in Data) {
    var total = new Value();
    var disp = '<table style="font-size: 10px;">';
    var last_disp = "";
    var cnt = 0;
    var sets = new Array();
    for (var i = 0; i < Data[k].length; ++i) {
      if (Data[k][i].used_with) continue;
      //if (Data[k][i].set_name != undefined) {
      //  if (sets.indexOf(Data[k][i].set_name) != -1) continue;
      //  sets.push(Data[k][i].set_name);
      //}
      total = total.Add(
        Data[k][i].value.Mult(Equipment[Data[k][i].item].count)
      );
      disp +=
        "<tr><td>" +
        GetEquipmentHref(Data[k][i].item, true) +
        "</td><td>" +
        Data[k][i].value.Mult(Equipment[Data[k][i].item].count).Html() +
        "</td></tr>";
      last_disp = GetEquipmentHref(Data[k][i].item, true);
      cnt++;
    }
    disp += "</table>";

    if (cnt > 0) {
      txt += AddTableRow(
        false,
        j,
        [
          k,
          total.Html(),
          total.Calc(HeroLevel).Html(),
          cnt > 1 ? disp : last_disp,
        ],
        [false, true, true, false]
      );
      j++;
    }

    for (var i = 0; i < Data[k].length; ++i) {
      if (!Data[k][i].used_with) continue;
      txt += AddTableRow(
        false,
        j,
        [
          k,
          Data[k][i].value.Html() + "<sup>" + Contents.Used_With + "</sup>",
          Data[k][i].value.Calc(HeroLevel).Html() +
            "<sup>" +
            Contents.Used_With +
            "</sup>",
          GetEquipmentHref(Data[k][i].item, false),
        ],
        [false, true, true, false]
      );
      j++;
      havea = true;
    }
  }
  txt += "</tbody></table>";
  if (havea)
    txt += '<font size="-1"><sup>(a)</sup> ' + Contents.Item_Skill + "</font>";

  if (j > 0) Where.innerHTML += txt;
}

function AddTableEx(heroID, Where, Data, heading, headers, negative) {
  if (negative == undefined) negative = false;

  var display = false;
  var txt = "<h3>" + heading + '</h3><table class="content_table"><tbody>';
  txt += AddTableRow(true, 0, headers);

  var j = 0;
  var havea = false;
  var havez = false;
  for (var k in Data) {
    for (var l in Data[k]) {
      var total = new Dmg();
      var disp = '<table style="font-size: 10px;">';
      var last_disp = "";
      var cnt = 0;

      var sets = new Array();
      for (var i = 0; i < Data[k][l].length; ++i) {
        if (Data[k][l][i].used_with || Data[k][l][i].in_addition) continue;
        //if (Data[k][l][i].set_name != undefined) {
        //  if (sets.indexOf(Data[k][l][i].set_name) != -1) continue;
        //  sets.push(Data[k][l][i].set_name);
        //}
        total = total.Add(
          Data[k][l][i].value.Mult(Equipment[Data[k][l][i].item].count)
        );
        disp +=
          "<tr><td>" +
          GetEquipmentHref(Data[k][l][i].item, true) +
          "</td><td>" +
          Data[k][l][i].value
            .Mult(Equipment[Data[k][l][i].item].count)
            .Html(negative) +
          "</td></tr>";
        last_disp = GetEquipmentHref(Data[k][l][i].item, true);
        cnt++;
      }
      disp += "</table>";
      if (cnt > 0) {
        txt += AddTableRow(
          false,
          j,
          [
            k,
            l,
            total.Html(negative),
            total.Calc(HeroLevel).Html(negative),
            cnt > 1 ? disp : last_disp,
          ],
          [false, true, true, true, false]
        );
        j++;
      }

      total = new Dmg();
      disp = '<table style="font-size: 10px;">';
      last_disp = "";
      cnt = 0;
      sets = new Array();
      for (var i = 0; i < Data[k][l].length; ++i) {
        if (Data[k][l][i].used_with || !Data[k][l][i].in_addition) continue;
        if (Data[k][l][i].set_name != undefined) {
          if (sets.indexOf(Data[k][l][i].set_name) != -1) continue;
          sets.push(Data[k][l][i].set_name);
        }
        total = total.Add(
          Data[k][l][i].value.Mult(Equipment[Data[k][l][i].item].count)
        );
        disp +=
          "<tr><td>" +
          GetEquipmentHref(Data[k][l][i].item, true) +
          "</td><td>" +
          Data[k][l][i].value
            .Mult(Equipment[Data[k][l][i].item].count)
            .Html(negative) +
          "</td></tr>";
        last_disp = GetEquipmentHref(Data[k][l][i].item, true);
        cnt++;
      }
      disp += "</table>";
      if (cnt > 0) {
        txt += AddTableRow(
          false,
          j,
          [
            k,
            l,
            total.Html(negative) + "<sup>" + Contents.Dmg_With + "</sup>",
            total.Calc(HeroLevel).Html(negative) +
              "<sup>" +
              Contents.Dmg_With +
              "</sup>",
            cnt > 1 ? disp : last_disp,
          ],
          [false, true, true, true, false]
        );
        j++;
        havez = true;
      }
      for (var i = 0; i < Data[k][l].length; ++i) {
        if (!Data[k][l][i].used_with) continue;
        txt += AddTableRow(
          false,
          j,
          [
            k,
            l + "<sup>" + Contents.Used_With + "</sup>",
            Data[k][l][i].value.Html(negative) +
              (Data[k][l][i].in_addition
                ? "<sup>" + Contents.Dmg_With + "</sup>"
                : ""),
            Data[k][l][i].value.Calc(HeroLevel).Html(negative) +
              (Data[k][l][i].in_addition
                ? "<sup>" + Contents.Dmg_With + "</sup>"
                : ""),
            GetEquipmentHref(Data[k][l][i].item, false),
          ],
          [false, true, true, true, false]
        );
        j++;
        havea = true;
        havez = havez || Data[k][l][i].in_addition;
      }
    }
  }
  txt += "</tbody></table>";
  if (!negative) {
    txt +=
      '<font size="-1"><sup>(r)</sup> ' +
      Contents.Damage_Effect +
      "</font><br>";
    if (havea)
      txt +=
        '<font size="-1"><sup>(a)</sup> ' + Contents.Item_Skill + "</font><br>";
    if (havez)
      txt +=
        '<font size="-1"><sup>(z)</sup> ' + Contents.Damage_Added + "</font>";
  } else {
    txt += '<font size="-1"><sup>(r)</sup> ' + Contents.All_Hits + "</font>";
    if (havea)
      txt +=
        '<font size="-1"><sup>(a)</sup> ' + Contents.Item_Skill + "</font><br>";
    if (havez)
      txt +=
        '<font size="-1"><sup>(z)</sup> ' + Contents.Damage_Added + "</font>";
  }

  if (j > 0) Where.innerHTML += txt;
}

function DisplayResult(heroID) {
  Result.innerHTML =
    Contents.Title +
    new Date().toLocaleString() +
    "<br/>" +
    "<a target='_blank' href='https://raw.githubusercontent.com/tomy2105/wod/master/extra_equipment_stats.user.js'><span style='font-size: 9px;'>" +
    Contents.Copyright +
    " v" +
    VER +
    "</span></a>";

  AddTable(
    heroID,
    Result,
    Attribs,
    Contents.Attr_Bonus,
    [Contents.Attribute, Contents.Modifier, Contents.Value, Contents.Item],
    undefined
  );
  AddTable(
    heroID,
    Result,
    Level,
    Contents.Level_Bonus,
    [Contents.Skill, Contents.Modifier, Contents.Value, Contents.Item],
    undefined
  );
  AddTable(
    heroID,
    Result,
    Effect,
    Contents.Effect_Bonus,
    [Contents.Skill, Contents.Modifier, Contents.Value, Contents.Item],
    Contents.Effect_BonusTalent
  );
  AddTable(
    heroID,
    Result,
    Attack,
    Contents.Attack_Bonus,
    [Contents.Attack_Type, Contents.Modifier, Contents.Value, Contents.Item],
    undefined
  );
  AddTable(
    heroID,
    Result,
    Defense,
    Contents.Defense_Bonus,
    [Contents.Attack_Type, Contents.Modifier, Contents.Value, Contents.Item],
    undefined
  );

  AddTableEx(heroID, Result, Damage, Contents.Damage_Bonus, [
    Contents.Damage_Type,
    Contents.Attack_Type,
    Contents.Damage_BonusR,
    Contents.Value,
    Contents.Item,
  ]);
  AddTableEx(
    heroID,
    Result,
    DamageTaken,
    Contents.Damage_Taken,
    [
      Contents.Damage_Type,
      Contents.Attack_Type,
      Contents.BonusR,
      Contents.Value,
      Contents.Item,
    ],
    true
  );

  if (KeyButton.className == "button_disabled") KeyButton.className = "button";

  //setLocalValue(LOCAL_VAR_NAME + heroID, {text:Result.innerHTML, version:VER});
}

function ParseTable(Document) {
  var ret = new Array();
  var names = new Array();
  var allTRs = Document.getElementsByTagName("tr");
  for (var j = 0; j < allTRs.length; ++j) {
    var tr = allTRs[j];
    if (
      tr.getAttribute("class") == "content_table_header" ||
      tr.getAttribute("class") == "header"
    ) {
      var allTHs = tr.getElementsByTagName("th");
      for (var k = 0; k < allTHs.length; ++k) {
        names.push(allTHs[k].textContent.trim().space_clear());
      }
    } else {
      var allTDs = tr.getElementsByTagName("td");
      var row = new Object();
      var isGem = false;
      for (var k = 0; k < allTDs.length; ++k) {
        row[names[k]] = allTDs[k].textContent.trim().space_clear();
        if (allTDs[k].innerHTML.indexOf('by_gem">') != -1) {
          isGem = true;
        }
      }
      row["IsGem"] = isGem;
      ret.push(row);
    }
  }

  return ret;
}