MediaWiki:Script/Calculator.js : Différence entre versions

 
(2 révisions intermédiaires par le même utilisateur non affichées)
Ligne 36 : Ligne 36 :
 
function compareNumbers(a, b) {
 
function compareNumbers(a, b) {
 
   return b - a;
 
   return b - a;
 +
}
 +
 +
function isChecked(attribute) {
 +
  return attribute === "on";
 
}
 
}
  
Ligne 82 : Ligne 86 :
 
   var cell = row.insertCell();
 
   var cell = row.insertCell();
 
   cell.appendChild(deleteFightTemplate.cloneNode(true));
 
   cell.appendChild(deleteFightTemplate.cloneNode(true));
 +
}
 +
 +
function calcMeanDamages(damagesWeightedByType, totalCardinal) {
 +
  var sumDamages = 0;
 +
 +
  for (var damagesTypeName in damagesWeightedByType) {
 +
    if (damagesTypeName === "miss") {
 +
      continue;
 +
    }
 +
 +
    var damagesWeighted = damagesWeightedByType[damagesTypeName];
 +
 +
    for (var damages in damagesWeighted) {
 +
      sumDamages += damages * damagesWeighted[damages];
 +
    }
 +
  }
 +
 +
  return sumDamages / totalCardinal;
 
}
 
}
  
Ligne 229 : Ligne 251 :
  
 
   chart.data.missPercentage = scatterDataByType.miss;
 
   chart.data.missPercentage = scatterDataByType.miss;
 +
  chart.update();
 +
}
 +
 +
function addToBonusVariationChart(
 +
  damagesByBonus,
 +
  augmentationByBonus,
 +
  xLabel,
 +
  chart
 +
) {
 +
  chart.data.datasets[0].data = damagesByBonus;
 +
  chart.data.datasets[1].data = augmentationByBonus;
 +
  chart.options.scales.x.title.text = xLabel;
 
   chart.update();
 
   chart.update();
 
}
 
}
Ligne 645 : Ligne 679 :
  
 
function characterCreationListener(characters, battle) {
 
function characterCreationListener(characters, battle) {
   characters.characterCreation.addEventListener("submit", function (event) {
+
   var { characterCreation, saveButton, weaponCategory } = characters;
 +
 
 +
  characterCreation.addEventListener("submit", function (event) {
 
     event.preventDefault();
 
     event.preventDefault();
  
 
     if (characters.unsavedChanges) {
 
     if (characters.unsavedChanges) {
       saveCharacter(
+
       saveCharacter(characters.savedCharacters, characterCreation, battle);
        characters.savedCharacters,
+
       saveButtonGreen(saveButton);
        characters.characterCreation,
 
        battle
 
      );
 
       saveButtonGreen(characters.saveButton);
 
 
       characters.unsavedChanges = false;
 
       characters.unsavedChanges = false;
 
     }
 
     }
Ligne 662 : Ligne 694 :
 
     if (event.ctrlKey && event.key === "s") {
 
     if (event.ctrlKey && event.key === "s") {
 
       event.preventDefault();
 
       event.preventDefault();
       characters.saveButton.click();
+
       saveButton.click();
 +
    }
 +
  });
 +
 
 +
  weaponCategory.addEventListener("mouseover", function (event) {
 +
    label = event.target.closest("label");
 +
 
 +
    if (label) {
 +
      var tooltip = label.lastChild;
 +
 
 +
      if (tooltip.classList.contains("popContenu")) {
 +
        var tooltipRect = tooltip.getBoundingClientRect();
 +
        var modalRect = weaponCategory.getBoundingClientRect();
 +
 
 +
        if (tooltipRect.right > modalRect.right) {
 +
          tooltip.style.left = "-100%";
 +
        } else if (tooltipRect.left < modalRect.left) {
 +
          tooltip.style.left = "200%";
 +
        }
 +
      }
 
     }
 
     }
 
   });
 
   });
Ligne 866 : Ligne 917 :
  
 
     if (formElement.type === "checkbox") {
 
     if (formElement.type === "checkbox") {
       if (value === "on") {
+
       if (isChecked(value)) {
 
         formElement.checked = true;
 
         formElement.checked = true;
 
       }
 
       }
Ligne 1 182 : Ligne 1 233 :
 
     handleBonusVariation(characterCreation[selectedBonus], bonusVariation);
 
     handleBonusVariation(characterCreation[selectedBonus], bonusVariation);
 
   } else {
 
   } else {
 +
    var { minValue, maxValue } = bonusVariation;
 +
 +
    minValue.removeAttribute("min");
 +
    minValue.removeAttribute("max");
 +
 +
    maxValue.removeAttribute("min");
 +
    maxValue.removeAttribute("max");
 +
 
     hideElement(bonusVariation.container);
 
     hideElement(bonusVariation.container);
 
   }
 
   }
 
}
 
}
  
function handleBonusVariation(target, bonusVariation, isSelectedByUser) {
+
function getTargetContent(targetParent, targetName, isSkill) {
   var { inputDisplay, referenceValue, minValue, maxValue, step, container } =
+
   var targetContent = "";
    bonusVariation;
 
  
   var {
+
  if (targetParent.children.length <= 1) {
 +
    targetContent = targetParent.textContent;
 +
  } else if (targetName === "weaponUpgrade") {
 +
    targetContent = targetParent.children[1].textContent;
 +
  } else if (isSkill) {
 +
    var container = targetParent.children[1];
 +
 
 +
    for (var index = 1; index < container.children.length; index++) {
 +
      var element = container.children[index];
 +
 
 +
      if (element.checkVisibility()) {
 +
        targetContent += element.textContent;
 +
      }
 +
    }
 +
  } else {
 +
    for (var index = 1; index < targetParent.children.length; index++) {
 +
      var element = targetParent.children[index];
 +
 
 +
      if (element.checkVisibility()) {
 +
        targetContent += element.textContent;
 +
      }
 +
    }
 +
  }
 +
 
 +
  return targetContent;
 +
}
 +
 
 +
function handleBonusVariation(target, bonusVariation, isSelectedByUser) {
 +
  var { activation, inputDisplay, minValue, maxValue, container } =
 +
    bonusVariation;
 +
 
 +
   var {
 
     min: targetMin,
 
     min: targetMin,
 
     max: targetMax,
 
     max: targetMax,
Ligne 1 196 : Ligne 1 285 :
 
     value: targetValue,
 
     value: targetValue,
 
     parentElement: targetParent,
 
     parentElement: targetParent,
 +
    tagName,
 
   } = target;
 
   } = target;
  
   if (container.contains(target) || targetValue == 0) {
+
  targetMin = Number(targetMin);
     hideElement(container);
+
  targetMax = Number(targetMax);
 +
  targetValue = Number(targetValue);
 +
 
 +
  var isSkill = tagName === "SELECT";
 +
 
 +
   if (container.contains(target) || targetName == 0) {
 +
     if (!isSelectedByUser) {
 +
      hideElement(container);
 +
    }
 
     return;
 
     return;
 
   }
 
   }
  
   var targetContent;
+
   if (isSkill) {
 +
    var options = target.options;
  
  if (targetParent.children.length <= 1) {
+
     targetMin = options[0].value;
     targetContent = targetParent.textContent;
+
     targetMax = options[options.length - 1].value;
  } else {
 
     targetContent = targetParent.querySelector(
 
      "span:not(.tabber-noactive)"
 
    ).textContent;
 
 
   }
 
   }
 
  inputDisplay.value = targetContent;
 
  inputDisplay.style.width = targetContent.length * 0.55 + "em";
 
 
  referenceValue.min = targetMin;
 
  referenceValue.max = targetMax;
 
  
 
   minValue.min = targetMin;
 
   minValue.min = targetMin;
Ligne 1 224 : Ligne 1 313 :
 
   maxValue.min = targetMin;
 
   maxValue.min = targetMin;
 
   maxValue.max = targetMax;
 
   maxValue.max = targetMax;
 
  step.min = 1;
 
  step.max = targetMax - targetMin;
 
  
 
   if (isSelectedByUser) {
 
   if (isSelectedByUser) {
 
     var { input, tab } = bonusVariation;
 
     var { input, tab } = bonusVariation;
  
     targetValue = Number(targetValue);
+
     inputDisplay.value = getTargetContent(targetParent, targetName, isSkill);
 +
 
 
     input.value = targetName;
 
     input.value = targetName;
    referenceValue.value = targetValue;
 
 
     minValue.value = Math.max(targetValue - 10, targetMin);
 
     minValue.value = Math.max(targetValue - 10, targetMin);
 
     maxValue.value = Math.min(targetValue + 10, targetMax);
 
     maxValue.value = Math.min(targetValue + 10, targetMax);
     step.value = 1;
+
 
 +
     activation.checked = true;
  
 
     tab.click();
 
     tab.click();
Ligne 1 242 : Ligne 1 329 :
  
 
     input.dispatchEvent(new Event("change", { bubbles: true }));
 
     input.dispatchEvent(new Event("change", { bubbles: true }));
 +
  } else {
 +
    if (minValue.value < targetMin) {
 +
      minValue.value = targetMin;
 +
      minValue.dispatchEvent(new Event("change", { bubbles: true }));
 +
    }
 +
 +
    if (maxValue.value > targetMax) {
 +
      maxValue.value = targetMax;
 +
      maxValue.dispatchEvent(new Event("change", { bubbles: true }));
 +
    }
 
   }
 
   }
  
 +
  inputDisplay.style.width = inputDisplay.value.length * 0.55 + "em";
 
   showElement(container);
 
   showElement(container);
 
}
 
}
Ligne 1 305 : Ligne 1 403 :
 
   });
 
   });
  
   characterCreation.addEventListener("click", function (event) {
+
   function handleLongPress(target) {
     if (event.shiftKey || event.ctrlKey) {
+
     if (target.tagName !== "INPUT" && target.tagName !== "SELECT") {
       var target = event.target;
+
       target = target.querySelector("input");
 +
    }
  
       if (target.tagName !== "INPUT") {
+
    if (
         target = target.querySelector("input");
+
      !target ||
      }
+
       (target.type !== "number" &&
 +
        !target.classList.contains("skill-select") &&
 +
         target.name !== "weaponUpgrade")
 +
    ) {
 +
      return;
 +
    }
  
      if (!target || target.type !== "number") {
+
    handleBonusVariation(target, bonusVariation, true);
        return;
+
  }
      }
 
  
       handleBonusVariation(target, bonusVariation, true);
+
  characterCreation.addEventListener("click", function (event) {
 +
    if (event.shiftKey || event.ctrlKey) {
 +
       handleLongPress(event.target);
 
     }
 
     }
 +
  });
 +
 +
  var longPressTimer;
 +
 +
  characterCreation.addEventListener("touchstart", function (event) {
 +
    longPressTimer = setTimeout(function () {
 +
      handleLongPress(event.target);
 +
    }, 800);
 +
  });
 +
 +
  characterCreation.addEventListener("touchend", function () {
 +
    clearTimeout(longPressTimer);
 +
  });
 +
 +
  characterCreation.addEventListener("touchmove", function () {
 +
    clearTimeout(longPressTimer);
 
   });
 
   });
  
Ligne 1 586 : Ligne 1 707 :
 
function isStone(character) {
 
function isStone(character) {
 
   return character.race === 1;
 
   return character.race === 1;
 +
}
 +
 +
function isMeleeAttacker(monster) {
 +
  return monster.attack == 0;
 +
}
 +
 +
function isRangeAttacker(monster) {
 +
  return monster.attack == 1;
 +
}
 +
 +
function isMagicAttacker(monster) {
 +
  return monster.attack == 2;
 
}
 
}
  
Ligne 1 779 : Ligne 1 912 :
 
     minMagicAttackValue: minMagicAttackValue,
 
     minMagicAttackValue: minMagicAttackValue,
 
     maxMagicAttackValue: maxMagicAttackValue,
 
     maxMagicAttackValue: maxMagicAttackValue,
 +
    magicAttackValueAugmentation: getMagicAttackValueAugmentation(
 +
      minMagicAttackValue,
 +
      maxMagicAttackValue,
 +
      attacker.magicAttackValue
 +
    ),
 
     totalCardinal: totalCardinal,
 
     totalCardinal: totalCardinal,
 
     weights: calcWeights(minMagicAttackValue, maxMagicAttackValue, minInterval),
 
     weights: calcWeights(minMagicAttackValue, maxMagicAttackValue, minInterval),
Ligne 1 811 : Ligne 1 949 :
  
 
function calcDamageWithPrimaryBonuses(damages, bonusValues) {
 
function calcDamageWithPrimaryBonuses(damages, bonusValues) {
   damages = Math.floor(
+
   damages = Math.floor((damages * bonusValues.attackValueCoeff) / 100);
    (damages * bonusValues.attackValueCoeff) / 100 + bonusValues.adjustCoeff
 
  );
 
  
 
   damages += bonusValues.attackValueMarriage;
 
   damages += bonusValues.attackValueMarriage;
Ligne 1 908 : Ligne 2 044 :
 
     (damages * bonusValues.skillBonusByBonusCoeff) / 100
 
     (damages * bonusValues.skillBonusByBonusCoeff) / 100
 
   );
 
   );
 
+
 
   damages = Math.floor((tempDamages * bonusValues.tigerStrengthCoeff) / 100);
+
   damages = Math.floor(
 +
    (tempDamages * bonusValues.magicAttackValueCoeff) / 100 + 0.5
 +
  );
 +
  damages = Math.floor((damages * bonusValues.tigerStrengthCoeff) / 100);
  
 
   if (damagesType.criticalHit) {
 
   if (damagesType.criticalHit) {
Ligne 2 092 : Ligne 2 231 :
  
 
   var missPercentage = 0;
 
   var missPercentage = 0;
   var adjustCoeff = 0;
+
   var attackValueMeleeMagic = 0;
  var attackValuePercent = 0;
 
  var attackMeleeMagic = 0;
 
 
   var attackValueMarriage = 0;
 
   var attackValueMarriage = 0;
 
   var monsterResistanceMarriage = 0;
 
   var monsterResistanceMarriage = 0;
Ligne 2 113 : Ligne 2 250 :
 
   var tigerStrength = 0;
 
   var tigerStrength = 0;
 
   var blessingBonus = 0;
 
   var blessingBonus = 0;
 +
  var magicAttackValueMeleeMagic = 0;
 
   var criticalHitPercentage = attacker.criticalHit;
 
   var criticalHitPercentage = attacker.criticalHit;
 
   var piercingHitPercentage = attacker.piercingHit;
 
   var piercingHitPercentage = attacker.piercingHit;
Ligne 2 139 : Ligne 2 277 :
 
     }
 
     }
  
     attackValuePercent = attacker.attackValuePercent;
+
     attackValueMeleeMagic =
    attackMeleeMagic = attacker.attackMeleeMagic;
+
      attacker.attackValuePercent + Math.min(100, attacker.attackMeleeMagic);
  
 
     var weaponType = attacker.weapon.type;
 
     var weaponType = attacker.weapon.type;
Ligne 2 159 : Ligne 2 297 :
 
     }
 
     }
  
     if (attacker.whiteDragonElixir === "on") {
+
     if (isChecked(attacker.whiteDragonElixir)) {
 
       whiteDragonElixir = 10;
 
       whiteDragonElixir = 10;
 
     }
 
     }
Ligne 2 190 : Ligne 2 328 :
 
       criticalHitPercentage = 0;
 
       criticalHitPercentage = 0;
 
     } else {
 
     } else {
       if (attacker.isMarried === "on") {
+
       if (isChecked(attacker.isMarried)) {
         if (attacker.loveNecklace === "on") {
+
         if (isChecked(attacker.loveNecklace)) {
 
           attackValueMarriage = getMarriageBonusValue(
 
           attackValueMarriage = getMarriageBonusValue(
 
             attacker,
 
             attacker,
Ligne 2 199 : Ligne 2 337 :
 
         }
 
         }
  
         if (attacker.loveEarrings === "on") {
+
         if (isChecked(attacker.loveEarrings)) {
 
           criticalHitPercentage += getMarriageBonusValue(
 
           criticalHitPercentage += getMarriageBonusValue(
 
             attacker,
 
             attacker,
Ligne 2 207 : Ligne 2 345 :
 
         }
 
         }
  
         if (attacker.harmonyEarrings === "on") {
+
         if (isChecked(attacker.harmonyEarrings)) {
 
           piercingHitPercentage += getMarriageBonusValue(
 
           piercingHitPercentage += getMarriageBonusValue(
 
             attacker,
 
             attacker,
Ligne 2 216 : Ligne 2 354 :
 
       }
 
       }
  
       if (attacker.tigerStrength === "on") {
+
       if (isChecked(attacker.tigerStrength)) {
 
         tigerStrength = 40;
 
         tigerStrength = 40;
 
       }
 
       }
Ligne 2 252 : Ligne 2 390 :
 
       }
 
       }
  
       if (attacker.onYohara === "on") {
+
       if (isChecked(attacker.onYohara)) {
 
         var sungmaStrDifference = attacker.sungmaStr - attacker.sungmaStrMalus;
 
         var sungmaStrDifference = attacker.sungmaStr - attacker.sungmaStrMalus;
  
Ligne 2 272 : Ligne 2 410 :
 
     damageBonus = attacker.damageBonus;
 
     damageBonus = attacker.damageBonus;
  
     if (attacker.empireMalus === "on") {
+
     if (isChecked(attacker.empireMalus)) {
 
       empireMalus = 1;
 
       empireMalus = 1;
 
     }
 
     }
 
   } else {
 
   } else {
 
     if (isPC(victim)) {
 
     if (isPC(victim)) {
       if (victim.isMarried === "on") {
+
       if (isChecked(victim.isMarried)) {
         if (victim.harmonyBracelet === "on") {
+
         if (isChecked(victim.harmonyBracelet)) {
 
           monsterResistanceMarriage = getMarriageBonusValue(
 
           monsterResistanceMarriage = getMarriageBonusValue(
 
             victim,
 
             victim,
Ligne 2 286 : Ligne 2 424 :
 
         }
 
         }
  
         if (victim.harmonyNecklace === "on" && !skillType) {
+
         if (isChecked(victim.harmonyNecklace) && !skillType) {
 
           defenseMarriage = getMarriageBonusValue(
 
           defenseMarriage = getMarriageBonusValue(
 
             victim,
 
             victim,
Ligne 2 308 : Ligne 2 446 :
  
 
       if (!skillType) {
 
       if (!skillType) {
         if (attacker.attack == 0) {
+
         if (isMeleeAttacker(attacker)) {
 
           missPercentage = victim.meleeBlock;
 
           missPercentage = victim.meleeBlock;
 
           averageDamageResistance = victim.averageDamageResistance;
 
           averageDamageResistance = victim.averageDamageResistance;
 
           blessingBonus = calcBlessingBonus(constants.skillPowerTable, victim);
 
           blessingBonus = calcBlessingBonus(constants.skillPowerTable, victim);
         } else if (attacker.attack == 1) {
+
         } else if (isRangeAttacker(attacker)) {
 
           missPercentage = victim.arrowBlock;
 
           missPercentage = victim.arrowBlock;
 
           weaponDefense = victim.arrowDefense;
 
           weaponDefense = victim.arrowDefense;
 
           averageDamageResistance = victim.averageDamageResistance;
 
           averageDamageResistance = victim.averageDamageResistance;
 
           blessingBonus = calcBlessingBonus(constants.skillPowerTable, victim);
 
           blessingBonus = calcBlessingBonus(constants.skillPowerTable, victim);
         } else {
+
         } else if (isMagicAttacker(attacker)) {
 
           missPercentage = victim.arrowBlock;
 
           missPercentage = victim.arrowBlock;
 
           skillDamageResistance = victim.skillDamageResistance;
 
           skillDamageResistance = victim.skillDamageResistance;
Ligne 2 339 : Ligne 2 477 :
  
 
   if (isPC(victim)) {
 
   if (isPC(victim)) {
     if (!skillType && victim.biologist70 === "on") {
+
     if (!skillType && isChecked(victim.biologist70)) {
 
       defenseBoost = Math.floor((defenseBoost * 110) / 100);
 
       defenseBoost = Math.floor((defenseBoost * 110) / 100);
 
     }
 
     }
Ligne 2 362 : Ligne 2 500 :
 
     }
 
     }
  
     if (victim.steelDragonElixir === "on") {
+
     if (isChecked(victim.steelDragonElixir)) {
 
       steelDragonElixir = 10;
 
       steelDragonElixir = 10;
 
     }
 
     }
Ligne 2 368 : Ligne 2 506 :
  
 
   if (skillType === "magic") {
 
   if (skillType === "magic") {
     adjustCoeff = 0.5;
+
     attackValueMeleeMagic = 0;
     attackValuePercent = attacker.attackMagic;
+
     magicAttackValueMeleeMagic =
 +
      attacker.attackMagic + Math.min(100, attacker.attackMeleeMagic);
 
     attackValueMarriage = 0;
 
     attackValueMarriage = 0;
 
     defense = 0;
 
     defense = 0;
Ligne 2 386 : Ligne 2 525 :
 
     missPercentage: missPercentage,
 
     missPercentage: missPercentage,
 
     weaponBonusCoeff: 1,
 
     weaponBonusCoeff: 1,
    adjustCoeff: adjustCoeff,
+
     attackValueCoeff: 100 + attackValueMeleeMagic,
     attackValueCoeff:
 
      100 + attackValuePercent + Math.min(100, attackMeleeMagic),
 
 
     attackValueMarriage: attackValueMarriage,
 
     attackValueMarriage: attackValueMarriage,
 
     monsterResistanceMarriageCoeff: 100 - monsterResistanceMarriage,
 
     monsterResistanceMarriageCoeff: 100 - monsterResistanceMarriage,
Ligne 2 407 : Ligne 2 544 :
 
     weaponDefenseCoeff: 100 - weaponDefense,
 
     weaponDefenseCoeff: 100 - weaponDefense,
 
     blessingBonusCoeff: 100 - blessingBonus,
 
     blessingBonusCoeff: 100 - blessingBonus,
 +
    magicAttackValueCoeff: 100 + magicAttackValueMeleeMagic,
 
     extraPiercingHitCoeff: 5 * extraPiercingHitPercentage,
 
     extraPiercingHitCoeff: 5 * extraPiercingHitPercentage,
 
     averageDamageCoeff: 100 + averageDamage,
 
     averageDamageCoeff: 100 + averageDamage,
Ligne 2 557 : Ligne 2 695 :
 
   );
 
   );
  
   if (victim.class === "dragon" && victim.blessingOnself === "on") {
+
   if (victim.class === "dragon" && isChecked(ictim.blessingOnself)) {
 
     blessingBonus = floorMultiplication(blessingBonus, 1.1);
 
     blessingBonus = floorMultiplication(blessingBonus, 1.1);
 
   }
 
   }
Ligne 2 564 : Ligne 2 702 :
 
}
 
}
  
function getSkillFormula(battle, skillId, battleValues) {
+
function getSkillFormula(battle, skillId, battleValues, removeSkillVariation) {
 
   var { attacker, victim, attackFactor } = battleValues;
 
   var { attacker, victim, attackFactor } = battleValues;
 
   var skillPowerTable = battle.constants.skillPowerTable;
 
   var skillPowerTable = battle.constants.skillPowerTable;
Ligne 3 144 : Ligne 3 282 :
 
           skillFormula = function (atk, variation) {
 
           skillFormula = function (atk, variation) {
 
             return floorMultiplication(
 
             return floorMultiplication(
               1.8 * atk + (atk + 6 * dex + variation + 3 * str + lv) * skillPower,
+
               1.8 * atk +
 +
                (atk + 6 * dex + variation + 3 * str + lv) * skillPower,
 
               1
 
               1
 
             );
 
             );
Ligne 3 166 : Ligne 3 305 :
 
     if (improvedByBonus) {
 
     if (improvedByBonus) {
 
       skillInfo.skillBonusByBonus = attacker["skillBonus" + skillId];
 
       skillInfo.skillBonusByBonus = attacker["skillBonus" + skillId];
 +
    }
 +
 +
    if (removeSkillVariation) {
 +
      var averageVariation = (skillInfo.range[0] + skillInfo.range[0]) / 2;
 +
      skillInfo.range = [averageVariation, averageVariation];
 
     }
 
     }
 
   } else {
 
   } else {
Ligne 3 208 : Ligne 3 352 :
  
 
   updateBattleValues(battleValues, skillFormula, skillInfo);
 
   updateBattleValues(battleValues, skillFormula, skillInfo);
 +
}
 +
 +
function calcMagicAttackValueAugmentation(
 +
  magicAttackValueWeapon,
 +
  magicAttackValueBonus
 +
) {
 +
  if (magicAttackValueBonus) {
 +
    return Math.max(
 +
      1,
 +
      0.0025056 *
 +
        magicAttackValueBonus ** 0.602338 *
 +
        magicAttackValueWeapon ** 1.20476
 +
    );
 +
  }
 +
  return 0;
 +
}
 +
 +
function getMagicAttackValueAugmentation(
 +
  minMagicAttackValue,
 +
  maxMagicAttackValue,
 +
  magicAttackValueBonus
 +
) {
 +
  var magicAttackValueAugmentation = [];
 +
 +
  for (
 +
    var magicAttackValue = minMagicAttackValue;
 +
    magicAttackValue <= maxMagicAttackValue;
 +
    magicAttackValue++
 +
  ) {
 +
    magicAttackValueAugmentation.push(
 +
      calcMagicAttackValueAugmentation(magicAttackValue, magicAttackValueBonus)
 +
    );
 +
  }
 +
 +
  return magicAttackValueAugmentation;
 
}
 
}
  
Ligne 3 385 : Ligne 3 564 :
 
function calcMagicSkillDamages(battleValues) {
 
function calcMagicSkillDamages(battleValues) {
 
   var {
 
   var {
     attackValues: { minMagicAttackValue, maxMagicAttackValue, weights },
+
     attackValues: {
 +
      minMagicAttackValue,
 +
      maxMagicAttackValue,
 +
      magicAttackValueAugmentation,
 +
      weights,
 +
    },
 
     bonusValues,
 
     bonusValues,
 
     damagesTypeCombinaison,
 
     damagesTypeCombinaison,
Ligne 3 409 : Ligne 3 593 :
 
       magicAttackValue++
 
       magicAttackValue++
 
     ) {
 
     ) {
       var weight =
+
      var index = magicAttackValue - minMagicAttackValue;
        weights[magicAttackValue - minMagicAttackValue] * damagesType.weight;
+
       var weight = weights[index] * damagesType.weight;
  
 
       for (
 
       for (
Ligne 3 417 : Ligne 3 601 :
 
         variation++
 
         variation++
 
       ) {
 
       ) {
         var rawDamages = skillFormula(magicAttackValue, variation);
+
         var rawDamages = skillFormula(
 +
          magicAttackValue + magicAttackValueAugmentation[index],
 +
          variation
 +
        );
  
 
         if (savedDamages.hasOwnProperty(rawDamages)) {
 
         if (savedDamages.hasOwnProperty(rawDamages)) {
Ligne 3 462 : Ligne 3 649 :
 
}
 
}
  
function calcDamages(attacker, victim, attackType, battle) {
+
function calcDamages(
 +
  attacker,
 +
  victim,
 +
  attackType,
 +
  battle,
 +
  removeSkillVariation
 +
) {
 
   var damagesCalculator, skillId, skillType;
 
   var damagesCalculator, skillId, skillType;
  
Ligne 3 486 : Ligne 3 679 :
  
 
   if (skillId) {
 
   if (skillId) {
     getSkillFormula(battle, skillId, battleValues);
+
     getSkillFormula(battle, skillId, battleValues, removeSkillVariation);
 
   }
 
   }
  
Ligne 3 497 : Ligne 3 690 :
 
     totalCardinal: totalCardinal,
 
     totalCardinal: totalCardinal,
 
     possibleDamagesCount: possibleDamagesCount,
 
     possibleDamagesCount: possibleDamagesCount,
 +
    skillType: skillType,
 
   };
 
   };
 
}
 
}
  
function changeMonsterValues(monster, instance, attacker) {
+
function damagesWithoutVariation(
   switch (instance) {
+
  attacker,
     case "SungMahiTower":
+
  victim,
      var sungMahiFloor = 1;
+
  attackType,
      var sungMahiStep = 1;
+
  battle,
      var rawDefense = 120;
+
  characters
 +
) {
 +
   startDamagesTime = performance.now();
 +
 
 +
  var {
 +
     damagesWeightedByType,
 +
    totalCardinal,
 +
    possibleDamagesCount,
 +
    skillType,
 +
  } = calcDamages(attacker, victim, attackType, battle);
 +
 
 +
  endDamagesTime = performance.now();
 +
 
 +
  possibleDamagesCount = displayResults(
 +
    possibleDamagesCount,
 +
    totalCardinal,
 +
    damagesWeightedByType,
 +
    battle,
 +
    attacker.name,
 +
    victim.name
 +
  );
  
      if (isPC(attacker)) {
+
  endDisplayTime = performance.now();
        sungMahiFloor = attacker.sungMahiFloor;
 
        sungMahiStep = attacker.sungMahiStep;
 
      }
 
  
      if (monster.rank === 5) {
+
  displayFightInfo(
        monster.level = 121;
+
    possibleDamagesCount,
        monster.dex = 75;
+
    endDamagesTime - startDamagesTime,
        rawDefense += 1;
+
    endDisplayTime - endDamagesTime,
      } else if (monster.rank === 6) {
+
    battle
        monster.level = 123;
+
  );
        monster.dex = 75;
+
  addPotentialErrorInformation(
        rawDefense += 1;
+
    battle.errorInformation,
      } else {
+
    attacker,
        monster.level = 120;
+
    victim,
        monster.dex = 68;
+
    skillType,
      }
+
    characters
      monster.vit = 100;
+
  );
      monster.rawDefense = rawDefense + (sungMahiStep - 1) * 6;
+
 
      monster.fistDefense = 0;
+
  hideElement(battle.bonusVariationResultContainer);
      monster.swordDefense = 0;
+
  showElement(battle.fightResultContainer);
      monster.twoHandedSwordDefense = 0;
 
      monster.daggerDefense = 0;
 
      monster.bellDefense = 0;
 
      monster.fanDefense = 0;
 
      monster.arrowDefense = 0;
 
      monster.clawDefense = 0;
 
      monster.magicResistance = 0;
 
      monster.fireResistance = -20;
 
  }
 
 
}
 
}
  
function createWeapon(weaponVnum) {
+
function damagesWithVariation(
   var weapon = weaponData[weaponVnum];
+
  attacker,
   var weaponName = weapon[0];
+
  victim,
 +
  attackType,
 +
  battle,
 +
  entity,
 +
  entityVariation
 +
) {
 +
  startTime = performance.now();
 +
   var damagesByBonus = [];
 +
   var augmentationByBonus = [];
 +
  var {
 +
    bonusVariationMinValue: minVariation,
 +
    bonusVariationMaxValue: maxVariation,
 +
  } = entity;
 +
  var step = Math.ceil((maxVariation - minVariation + 1) / 500);
 +
  var simulationCount = 0;
 +
  var simulationTime;
 +
 
 +
  for (
 +
    var bonusValue = minVariation;
 +
    bonusValue <= maxVariation;
 +
    bonusValue += step
 +
  ) {
 +
    entity[entityVariation] = bonusValue;
  
  return {
+
    var { damagesWeightedByType, totalCardinal } = calcDamages(
    name: weaponName,
+
      copyObject(attacker),
    type: weapon[1],
+
      copyObject(victim),
    minAttackValue: weapon[2][2],
+
      attackType,
    maxAttackValue: weapon[2][3],
+
      battle,
    minMagicAttackValue: weapon[2][0],
+
      true
    maxMagicAttackValue: weapon[2][1],
+
     );
    upgrades: weapon[3],
 
     isSerpent: isValueInArray("serpent", weaponName.toLowerCase()),
 
  };
 
}
 
  
function createMonster(monsterVnum, attacker) {
+
    var meanDamages = calcMeanDamages(damagesWeightedByType, totalCardinal);
  var monsterAttributes = monsterData[monsterVnum];
 
  
  var monster = {
+
    if (bonusValue === minVariation) {
    name: monsterAttributes[36],
+
      var firstDamages = Math.max(meanDamages, 1e-3);
    rank: monsterAttributes[0],
+
     }
    race: monsterAttributes[1],
+
 
    attack: monsterAttributes[2],
+
     damagesByBonus.push({ x: bonusValue, y: meanDamages });
    level: monsterAttributes[3],
+
     augmentationByBonus.push({
     type: monsterAttributes[4],
+
      x: bonusValue,
    str: monsterAttributes[5],
+
      y: meanDamages / firstDamages - 1,
     dex: monsterAttributes[6],
+
     });
    vit: monsterAttributes[7],
+
     simulationCount++;
     int: monsterAttributes[8],
+
   }
    minAttackValue: monsterAttributes[9],
 
    maxAttackValue: monsterAttributes[10],
 
     rawDefense: monsterAttributes[11],
 
     criticalHit: monsterAttributes[12],
 
    piercingHit: monsterAttributes[13],
 
    fistDefense: monsterAttributes[14],
 
    swordDefense: monsterAttributes[15],
 
    twoHandedSwordDefense: monsterAttributes[16],
 
    daggerDefense: monsterAttributes[17],
 
    bellDefense: monsterAttributes[18],
 
    fanDefense: monsterAttributes[19],
 
    arrowDefense: monsterAttributes[20],
 
    clawDefense: monsterAttributes[21],
 
    fireResistance: monsterAttributes[22],
 
    lightningResistance: monsterAttributes[23],
 
    magicResistance: monsterAttributes[24],
 
    windResistance: monsterAttributes[25],
 
    lightningBonus: monsterAttributes[26],
 
    fireBonus: monsterAttributes[27],
 
    iceBonus: monsterAttributes[28],
 
    windBonus: monsterAttributes[29],
 
    earthBonus: monsterAttributes[30],
 
    darknessBonus: monsterAttributes[31],
 
    darknessResistance: monsterAttributes[32],
 
    iceResistance: monsterAttributes[33],
 
    earthResistance: monsterAttributes[34],
 
    damageMultiplier: monsterAttributes[35],
 
   };
 
  
   // monster.instance = 0;
+
   endTime = performance.now();
  
   // if (attacker && monster.instance === 0) {
+
   battle.damagesByBonus = damagesByBonus.concat(entityVariation);
  //  changeMonsterValues(monster, "SungMahiTower", attacker);
 
  // }
 
  
   monster.defense = monster.rawDefense + monster.level + monster.vit;
+
   addToBonusVariationChart(
 +
    damagesByBonus,
 +
    augmentationByBonus,
 +
    entity.bonusVariationDisplay,
 +
    battle.bonusVariationChart
 +
  );
  
   return monster;
+
   simulationCount = battle.numberFormats.default.format(simulationCount);
}
+
  simulationTime = battle.numberFormats.second.format(
 +
    (endTime - startTime) / 1000
 +
  );
  
function addPotentialErrorInformation(
+
   battle.simulationCounter.textContent = simulationCount;
   errorInformation,
+
   battle.simulationTime.textContent = simulationTime;
   attacker,
 
  victim,
 
  characters
 
) {
 
  for (var error of Object.values(errorInformation)) {
 
    hideElement(error);
 
  }
 
  
   if (isPC(attacker)) {
+
   hideElement(battle.fightResultContainer);
    if (isRiding(attacker)) {
+
  showElement(battle.bonusVariationResultContainer);
      if (attacker.horsePoint === 0) {
 
        showElement(errorInformation["horse-level"]);
 
      }
 
      showElement(errorInformation["horse-stat"]);
 
    } else if (isPolymorph(attacker)) {
 
      if (attacker.polymorphPoint === 0) {
 
        showElement(errorInformation["polymorph-level"]);
 
      }
 
  
      if (
+
  if (
        (attacker.polymorphPoint <= 39 && attacker.attackValuePercent <= 199) ||
+
    isChecked(attacker.bonusVariationActivation) &&
        (attacker.polymorphPoint === 40 && attacker.attackValuePercent <= 299)
+
    isChecked(victim.bonusVariationActivation)
      ) {
+
  ) {
        showElement(errorInformation["polymorph-bonus"]);
+
    showElement(battle.errorInformation["attacker-victim-variation"]);
      }
 
    }
 
 
   } else {
 
   } else {
     showElement(errorInformation["monster-attacker"]);
+
     hideElement(battle.errorInformation["attacker-victim-variation"]);
 
   }
 
   }
 +
}
  
  if (isPC(victim)) {
+
function changeMonsterValues(monster, instance, attacker) {
    if (isRiding(victim)) {
+
  switch (instance) {
      showElement(errorInformation["horse-stat"]);
+
    case "SungMahiTower":
    } else if (isPolymorph(victim)) {
+
      var sungMahiFloor = 1;
      if (attacker.polymorphPoint === 0) {
+
      var sungMahiStep = 1;
         showElement(errorInformation["polymorph-level"]);
+
      var rawDefense = 120;
 +
 
 +
      if (isPC(attacker)) {
 +
        sungMahiFloor = attacker.sungMahiFloor;
 +
         sungMahiStep = attacker.sungMahiStep;
 
       }
 
       }
      showElement(errorInformation["polymorph-defense"]);
 
    }
 
  }
 
  
  if (characters.unsavedChanges) {
+
      if (monster.rank === 5) {
    showElement(errorInformation["save"]);
+
        monster.level = 121;
 +
        monster.dex = 75;
 +
        rawDefense += 1;
 +
      } else if (monster.rank === 6) {
 +
        monster.level = 123;
 +
        monster.dex = 75;
 +
        rawDefense += 1;
 +
      } else {
 +
        monster.level = 120;
 +
        monster.dex = 68;
 +
      }
 +
      monster.vit = 100;
 +
      monster.rawDefense = rawDefense + (sungMahiStep - 1) * 6;
 +
      monster.fistDefense = 0;
 +
      monster.swordDefense = 0;
 +
      monster.twoHandedSwordDefense = 0;
 +
      monster.daggerDefense = 0;
 +
      monster.bellDefense = 0;
 +
      monster.fanDefense = 0;
 +
      monster.arrowDefense = 0;
 +
      monster.clawDefense = 0;
 +
      monster.magicResistance = 0;
 +
      monster.fireResistance = -20;
 
   }
 
   }
 
}
 
}
  
function reduceChartPointsListener(battle) {
+
function createWeapon(weaponVnum) {
   var {
+
   var weapon = weaponData[weaponVnum];
    reduceChartPoints,
+
   var weaponName = weapon[0];
    numberFormats: { second: numberFormat },
 
    displayTime,
 
   } = battle;
 
  
   reduceChartPoints.addEventListener("change", function () {
+
   return {
     var startDisplayTime = performance.now();
+
     name: weaponName,
     var scatterDataByType = battle.scatterDataByType;
+
     type: weapon[1],
     var {
+
     minAttackValue: weapon[2][2],
      chart,
+
    maxAttackValue: weapon[2][3],
      maxPoints,
+
    minMagicAttackValue: weapon[2][0],
      chart: {
+
    maxMagicAttackValue: weapon[2][1],
        data: { datasets },
+
    upgrades: weapon[3],
      },
+
    isSerpent: isValueInArray("serpent", weaponName.toLowerCase()),
    } = battle.damagesChart;
+
  };
    var addAnimations = false;
+
}
  
    for (var index = 0; index < datasets.length; index++) {
+
function createMonster(monsterVnum, attacker) {
      var dataset = datasets[index];
+
  var monsterAttributes = monsterData[monsterVnum];
      var scatterData = scatterDataByType[dataset.name];
 
  
      if (dataset.canBeReduced && reduceChartPoints.checked) {
+
  var monster = {
        dataset.data = aggregateDamages(scatterData, maxPoints);
+
    name: monsterAttributes[36],
        addAnimations = true;
+
    rank: monsterAttributes[0],
      } else {
+
    race: monsterAttributes[1],
        dataset.data = scatterData;
+
    attack: monsterAttributes[2],
      }
+
    level: monsterAttributes[3],
     }
+
    type: monsterAttributes[4],
     handleChartAnimations(chart, addAnimations);
+
    str: monsterAttributes[5],
     chart.update();
+
    dex: monsterAttributes[6],
 +
    vit: monsterAttributes[7],
 +
    int: monsterAttributes[8],
 +
    minAttackValue: monsterAttributes[9],
 +
    maxAttackValue: monsterAttributes[10],
 +
    rawDefense: monsterAttributes[11],
 +
    criticalHit: monsterAttributes[12],
 +
    piercingHit: monsterAttributes[13],
 +
    fistDefense: monsterAttributes[14],
 +
    swordDefense: monsterAttributes[15],
 +
    twoHandedSwordDefense: monsterAttributes[16],
 +
    daggerDefense: monsterAttributes[17],
 +
    bellDefense: monsterAttributes[18],
 +
    fanDefense: monsterAttributes[19],
 +
    arrowDefense: monsterAttributes[20],
 +
    clawDefense: monsterAttributes[21],
 +
    fireResistance: monsterAttributes[22],
 +
    lightningResistance: monsterAttributes[23],
 +
    magicResistance: monsterAttributes[24],
 +
    windResistance: monsterAttributes[25],
 +
    lightningBonus: monsterAttributes[26],
 +
    fireBonus: monsterAttributes[27],
 +
    iceBonus: monsterAttributes[28],
 +
    windBonus: monsterAttributes[29],
 +
    earthBonus: monsterAttributes[30],
 +
    darknessBonus: monsterAttributes[31],
 +
    darknessResistance: monsterAttributes[32],
 +
     iceResistance: monsterAttributes[33],
 +
     earthResistance: monsterAttributes[34],
 +
     damageMultiplier: monsterAttributes[35],
 +
  };
  
    displayTime.textContent = numberFormat.format(
+
  // monster.instance = 0;
      (performance.now() - startDisplayTime) / 1000
 
    );
 
  });
 
}
 
  
function downloadRawDataListener(battle) {
+
  // if (attacker && monster.instance === 0) {
   var button = document.getElementById("download-raw-data");
+
   //  changeMonsterValues(monster, "SungMahiTower", attacker);
   var fileType = "text/csv;charset=utf-8;";
+
   // }
  var filename = "raw_damages.csv";
 
  
   button.addEventListener("click", function (e) {
+
   monster.defense = monster.rawDefense + monster.level + monster.vit;
    var damagesWeightedByType = battle.damagesWeightedByType;
 
  
    if (!damagesWeightedByType) {
+
  return monster;
      return;
+
}
    }
 
  
    var csvContent = "damages,probabilities,damagesType\n";
+
function addPotentialErrorInformation(
 +
  errorInformation,
 +
  attacker,
 +
  victim,
 +
  skillType,
 +
  characters
 +
) {
 +
  for (var error of Object.values(errorInformation)) {
 +
    hideElement(error);
 +
  }
  
     for (var damagesType in damagesWeightedByType) {
+
  if (isPC(attacker)) {
       var damagesWeighted = damagesWeightedByType[damagesType];
+
    if (isRiding(attacker)) {
 +
      if (attacker.horsePoint === 0) {
 +
        showElement(errorInformation["horse-level"]);
 +
      }
 +
      showElement(errorInformation["horse-stat"]);
 +
     } else if (isPolymorph(attacker)) {
 +
       if (attacker.polymorphPoint === 0) {
 +
        showElement(errorInformation["polymorph-level"]);
 +
      }
  
       for (var damages in damagesWeighted) {
+
       if (
         csvContent +=
+
        (attacker.polymorphPoint <= 39 && attacker.attackValuePercent <= 199) ||
          damages + "," + damagesWeighted[damages] + "," + damagesType + "\n";
+
         (attacker.polymorphPoint === 40 && attacker.attackValuePercent <= 299)
 +
      ) {
 +
        showElement(errorInformation["polymorph-bonus"]);
 
       }
 
       }
 
     }
 
     }
 
+
    if (skillType === "magic") {
     downloadData(csvContent, fileType, filename);
+
      if (attacker.magicAttackValue) {
   });
+
        showElement(errorInformation["magic-attack-value-bonus"]);
 +
      }
 +
      if (victim.magicResistance) {
 +
        showElement(errorInformation["magic-resistance"]);
 +
      }
 +
    }
 +
  } else {
 +
    showElement(errorInformation["monster-attacker"]);
 +
    if (isMagicAttacker(attacker) && victim.magicResistance) {
 +
      showElement(errorInformation["magic-resistance"]);
 +
    }
 +
  }
 +
 
 +
  if (isPC(victim)) {
 +
     if (isRiding(victim)) {
 +
      showElement(errorInformation["horse-stat"]);
 +
    } else if (isPolymorph(victim)) {
 +
      if (attacker.polymorphPoint === 0) {
 +
        showElement(errorInformation["polymorph-level"]);
 +
      }
 +
      showElement(errorInformation["polymorph-defense"]);
 +
    }
 +
   }
 +
 
 +
  if (characters.unsavedChanges) {
 +
    showElement(errorInformation["save"]);
 +
  }
 
}
 
}
  
function displayResults(
+
function reduceChartPointsListener(battle) {
   possibleDamagesCount,
+
   var {
  totalCardinal,
+
    reduceChartPointsContainer,
  damagesWeightedByType,
+
    reduceChartPoints,
   battle,
+
    numberFormats: { second: numberFormat },
   attackerName,
+
    displayTime,
  victimName
+
   } = battle;
) {
+
 
  var [
+
   reduceChartPoints.addEventListener("change", function () {
     meanDamages,
+
    var startDisplayTime = performance.now();
     minDamages,
+
     var scatterDataByType = battle.scatterDataByType;
    maxDamages,
+
     var {
    scatterDataByType,
+
      chart,
    possibleDamagesCount,
+
      maxPoints,
    uniqueDamagesCount,
+
      chart: {
  ] = prepareDamagesData(
+
        data: { datasets },
    damagesWeightedByType,
+
      },
     possibleDamagesCount,
+
     } = battle.damagesChart;
     totalCardinal
+
     var addAnimations = false;
  );
 
  
  addToDamagesChart(
+
    for (var index = 0; index < datasets.length; index++) {
    scatterDataByType,
+
      var dataset = datasets[index];
    battle.damagesChart,
+
      var scatterData = scatterDataByType[dataset.name];
    battle.reduceChartPoints.checked
 
  );
 
  updateDamagesChartDescription(
 
    battle.uniqueDamagesCounters,
 
    uniqueDamagesCount,
 
    battle.numberFormats.default
 
  );
 
  displayFightResults(
 
    battle,
 
    attackerName,
 
    victimName,
 
    meanDamages,
 
    minDamages,
 
    maxDamages
 
  );
 
  battle.damagesWeightedByType = damagesWeightedByType;
 
  battle.scatterDataByType = scatterDataByType;
 
  
   return possibleDamagesCount;
+
      if (dataset.canBeReduced && reduceChartPoints.checked) {
 +
        dataset.data = aggregateDamages(scatterData, maxPoints);
 +
        addAnimations = true;
 +
      } else {
 +
        dataset.data = scatterData;
 +
      }
 +
    }
 +
 
 +
    handleChartAnimations(chart, addAnimations);
 +
    chart.update();
 +
 
 +
    displayTime.textContent = numberFormat.format(
 +
      (performance.now() - startDisplayTime) / 1000
 +
    );
 +
   });
 +
 
 +
  reduceChartPointsContainer.addEventListener("pointerup", function (event) {
 +
    if (event.pointerType === "mouse") {
 +
      reduceChartPoints.click();
 +
    }
 +
  });
 
}
 
}
  
function displayFightResults(
+
function downloadRawDataListener(battle) {
  battle,
+
   var { downLoadRawData, downLoadRawDataVariation } = battle;
  attackerName,
+
   var fileType = "text/csv;charset=utf-8;";
  victimName,
 
  meanDamages,
 
  minDamages,
 
  maxDamages
 
) {
 
   var {
 
    tableResultFight,
 
    tableResultHistory,
 
    attackTypeSelection,
 
    savedFights,
 
    numberFormats: { default: numberFormat },
 
    deleteFightTemplate,
 
   } = battle;
 
  
   showElement(tableResultFight.parentElement);
+
   downLoadRawData.addEventListener("click", function () {
  hideElement(tableResultHistory.rows[1]);
+
    var damagesWeightedByType = battle.damagesWeightedByType;
 +
    var filename = "raw_damages.csv";
 +
    var csvContent = "damage,probabilities,damageType\n";
  
  var valuesToDisplay = [
+
    for (var damagesType in damagesWeightedByType) {
    attackerName,
+
      var damagesWeighted = damagesWeightedByType[damagesType];
    victimName,
 
    attackTypeSelection.options[attackTypeSelection.selectedIndex].textContent,
 
    meanDamages,
 
    minDamages,
 
    maxDamages,
 
  ];
 
  
  savedFights.push(valuesToDisplay);
+
      for (var damages in damagesWeighted) {
  updateSavedFights(savedFights);
+
        csvContent +=
 +
          damages + "," + damagesWeighted[damages] + "," + damagesType + "\n";
 +
      }
 +
    }
  
  editTableResultRow(tableResultFight.rows[1], valuesToDisplay, numberFormat);
+
    downloadData(csvContent, fileType, filename);
   addRowToTableResultHistory(
+
   });
    tableResultHistory,
 
    valuesToDisplay,
 
    deleteFightTemplate,
 
    numberFormat
 
  );
 
}
 
  
function displayFightInfo(
+
  downLoadRawDataVariation.addEventListener("click", function () {
  possibleDamagesCount,
+
    var damagesByBonus = battle.damagesByBonus;
  damagesTime,
+
    var damagesByBonusLength = damagesByBonus.length;
  displayTime,
+
    var filename = "damages_variation.csv";
  battle
 
) {
 
  var container = battle.possibleDamagesCounter.parentElement;
 
  
  if (possibleDamagesCount <= 1) {
+
    if (!damagesByBonusLength) {
    hideElement(container);
+
      return;
    return;
+
     }
  } else {
 
     showElement(container);
 
  }
 
  
  possibleDamagesCount =
+
     var csvContent =
     battle.numberFormats.default.format(possibleDamagesCount);
+
      damagesByBonus[damagesByBonusLength - 1] + ",averageDamage\n";
  damagesTime = battle.numberFormats.second.format(damagesTime / 1000);
 
  displayTime = battle.numberFormats.second.format(displayTime / 1000);
 
  
  battle.possibleDamagesCounter.textContent = possibleDamagesCount;
+
    for (var index = 0; index < damagesByBonusLength - 1; index++) {
  battle.damagesTime.textContent = damagesTime;
+
      var row = damagesByBonus[index];
  battle.displayTime.textContent = displayTime;
 
}
 
  
function isPseudoSaved(characters, pseudo) {
+
      csvContent += row.x + "," + row.y + "\n";
  return characters.savedCharacters.hasOwnProperty(pseudo);
 
}
 
 
 
function createBattle(characters, battle) {
 
  battle.battleForm.addEventListener("submit", function (event) {
 
    event.preventDefault();
 
 
 
    startDamagesTime = performance.now();
 
 
 
    // auto save
 
    if (characters.unsavedChanges) {
 
      characters.saveButton.click();
 
 
     }
 
     }
  
     var battleInfo = new FormData(event.target);
+
     downloadData(csvContent, fileType, filename);
    var attackerName = battleInfo.get("attacker");
+
  });
    var attackType = battleInfo.get("attackTypeSelection");
+
}
    var victimName = battleInfo.get("victim");
 
    var attackerVariation;
 
    var victimVariation;
 
  
    if (!attackerName && !attackType && !victimName) {
+
function displayResults(
      return;
+
  possibleDamagesCount,
     }
+
  totalCardinal,
 
+
  damagesWeightedByType,
     if (isPseudoSaved(characters, attackerName)) {
+
  battle,
      var attacker = copyObject(characters.savedCharacters[attackerName]);
+
  attackerName,
      attackerVariation = attacker.bonusVariation;
+
  victimName
     } else {
+
) {
      var attacker = createMonster(attackerName);
+
  var [
    }
+
    meanDamages,
 +
    minDamages,
 +
    maxDamages,
 +
     scatterDataByType,
 +
    possibleDamagesCount,
 +
     uniqueDamagesCount,
 +
  ] = prepareDamagesData(
 +
    damagesWeightedByType,
 +
    possibleDamagesCount,
 +
     totalCardinal
 +
  );
  
     if (isPseudoSaved(characters, victimName)) {
+
  addToDamagesChart(
      var victim = copyObject(characters.savedCharacters[victimName]);
+
    scatterDataByType,
      victimVariation = victim.bonusVariation;
+
     battle.damagesChart,
     } else {
+
    battle.reduceChartPoints.checked
      var victim = createMonster(victimName, attacker);
+
  );
    }
+
  updateDamagesChartDescription(
 +
    battle.uniqueDamagesCounters,
 +
    uniqueDamagesCount,
 +
    battle.numberFormats.default
 +
  );
 +
  displayFightResults(
 +
    battle,
 +
     attackerName,
 +
    victimName,
 +
    meanDamages,
 +
    minDamages,
 +
    maxDamages
 +
  );
 +
  battle.damagesWeightedByType = damagesWeightedByType;
 +
  battle.scatterDataByType = scatterDataByType;
  
    if (attackerVariation && attacker.hasOwnProperty(attackerVariation)) {
+
  return possibleDamagesCount;
      // console.log(attacker[attackerVariation]);
+
}
    } else if (victimVariation && victim.hasOwnProperty(victimVariation)) {
 
      // console.log(victim[victimVariation]);
 
    }
 
  
    var { damagesWeightedByType, totalCardinal, possibleDamagesCount } =
+
function displayFightResults(
      calcDamages(attacker, victim, attackType, battle);
+
  battle,
 
+
  attackerName,
    endDamagesTime = performance.now();
+
  victimName,
 
+
  meanDamages,
    possibleDamagesCount = displayResults(
+
  minDamages,
      possibleDamagesCount,
+
  maxDamages
      totalCardinal,
+
) {
      damagesWeightedByType,
+
  var {
      battle,
+
    tableResultFight,
      attacker.name,
+
    tableResultHistory,
      victim.name
+
    attackTypeSelection,
    );
+
    savedFights,
 +
    numberFormats: { default: numberFormat },
 +
    deleteFightTemplate,
 +
  } = battle;
 +
 
 +
  hideElement(tableResultHistory.rows[1]);
 +
 
 +
  var valuesToDisplay = [
 +
    attackerName,
 +
    victimName,
 +
    attackTypeSelection.options[attackTypeSelection.selectedIndex].textContent,
 +
    meanDamages,
 +
    minDamages,
 +
    maxDamages,
 +
  ];
  
    endDisplayTime = performance.now();
+
  savedFights.push(valuesToDisplay);
 +
  updateSavedFights(savedFights);
  
    displayFightInfo(
+
  editTableResultRow(tableResultFight.rows[1], valuesToDisplay, numberFormat);
      possibleDamagesCount,
+
  addRowToTableResultHistory(
      endDamagesTime - startDamagesTime,
+
    tableResultHistory,
      endDisplayTime - endDamagesTime,
+
    valuesToDisplay,
      battle
+
    deleteFightTemplate,
    );
+
     numberFormat
    addPotentialErrorInformation(
+
   );
      battle.errorInformation,
 
      attacker,
 
      victim,
 
      characters
 
    );
 
     showElement(battle.fightResultContainer);
 
   });
 
 
}
 
}
  
function createMapping() {
+
function displayFightInfo(
   mapping = {
+
   possibleDamagesCount,
    typeFlag: [
+
  damagesTime,
      "animalBonus", // 0
+
  displayTime,
      "humanBonus", // 1
+
  battle
      "orcBonus", // 2
+
) {
      "mysticBonus", // 3
+
  var container = battle.possibleDamagesCounter.parentElement;
      "undeadBonus", // 4
+
 
      "insectBonus", // 5
+
  if (possibleDamagesCount <= 1) {
      "desertBonus", // 6
+
     hideElement(container);
      "devilBonus", // 7
+
     return;
     ],
+
  } else {
     raceBonus: {
+
     showElement(container);
      warrior: "warriorBonus",
+
  }
      sura: "suraBonus",
+
 
      ninja: "ninjaBonus",
+
  possibleDamagesCount =
      shaman: "shamanBonus",
+
     battle.numberFormats.default.format(possibleDamagesCount);
      lycan: "lycanBonus",
+
  damagesTime = battle.numberFormats.second.format(damagesTime / 1000);
    },
+
  displayTime = battle.numberFormats.second.format(displayTime / 1000);
     raceResistance: {
+
 
      warrior: "warriorResistance",
+
  battle.possibleDamagesCounter.textContent = possibleDamagesCount;
      sura: "suraResistance",
+
  battle.damagesTime.textContent = damagesTime;
      ninja: "ninjaResistance",
+
  battle.displayTime.textContent = displayTime;
      shaman: "shamanResistance",
+
}
      lycan: "lycanResistance",
+
 
     },
+
function isPseudoSaved(characters, pseudo) {
    defenseWeapon: [
+
  return characters.savedCharacters.hasOwnProperty(pseudo);
      "swordDefense", // 0
+
}
      "daggerDefense", // 1
+
 
      "arrowDefense", // 2
+
function createBattle(characters, battle) {
      "twoHandedSwordDefense", // 3
+
  battle.battleForm.addEventListener("submit", function (event) {
      "bellDefense", // 4
+
    event.preventDefault();
      "clawDefense", // 5
+
 
      "fanDefense", // 6
+
    // auto save
      "swordDefense", // 7
+
    if (characters.unsavedChanges) {
      "fistDefense", // 8
+
       characters.saveButton.click();
    ],
+
     }
    breakWeapon: [
+
 
      "breakSwordDefense", // 0
+
     var battleInfo = new FormData(event.target);
      "breakDaggerDefense", // 1
+
    var attackerName = battleInfo.get("attacker");
      "breakArrowDefense", // 2
+
    var attackType = battleInfo.get("attackTypeSelection");
      "breakTwoHandedSwordDefense", // 3
+
    var victimName = battleInfo.get("victim");
      "breakBellDefense", // 4
+
    var attackerVariation;
      "breakClawDefense", // 5
+
    var victimVariation;
      "breakFanDefense", // 6
+
 
       "breakSwordDefense", // 7
+
     if (!attackerName && !attackType && !victimName) {
     ],
+
       return;
     elementBonus: [
+
    }
      "fireBonus", // 0
+
 
      "iceBonus", // 1
+
    if (isPseudoSaved(characters, attackerName)) {
      "windBonus", // 2
+
       var attacker = copyObject(characters.savedCharacters[attackerName]);
      "lightningBonus", // 3
+
       attackerVariation = attacker.bonusVariation;
      "earthBonus", // 4
+
     } else {
      "darknessBonus", // 5
+
      var attacker = createMonster(attackerName);
     ],
+
    }
    elementResistance: [
 
       "fireResistance", // 0
 
      "iceResistance", // 1
 
      "windResistance", // 2
 
      "lightningResistance", // 3
 
       "earthResistance", // 4
 
       "darknessResistance", // 5
 
     ],
 
  };
 
  return mapping;
 
}
 
  
function createConstants() {
+
    if (isPseudoSaved(characters, victimName)) {
  var constants = {
+
      var victim = copyObject(characters.savedCharacters[victimName]);
     polymorphPowerTable: [
+
      victimVariation = victim.bonusVariation;
       10, 11, 11, 12, 13, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 26, 27,
+
    } else {
       29, 31, 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 59, 62, 66, 70, 74, 79,
+
      var victim = createMonster(victimName, attacker);
       84, 89, 94, 100, 0,
+
    }
     ],
+
 
     skillPowerTable: [
+
    if (
       0, 0.05, 0.06, 0.08, 0.1, 0.12, 0.14, 0.16, 0.18, 0.2, 0.22, 0.24, 0.26,
+
      isChecked(attacker.bonusVariationActivation) &&
       0.28, 0.3, 0.32, 0.34, 0.36, 0.38, 0.4, 0.5, 0.52, 0.54, 0.56, 0.58, 0.6,
+
      attacker.hasOwnProperty(attackerVariation) &&
       0.63, 0.66, 0.69, 0.72, 0.82, 0.85, 0.88, 0.91, 0.94, 0.98, 1.02, 1.06,
+
      attacker.bonusVariationMinValue < attacker.bonusVariationMaxValue
       1.1, 1.15, 1.25,
+
     ) {
 +
       damagesWithVariation(
 +
        attacker,
 +
        victim,
 +
        attackType,
 +
        battle,
 +
        attacker,
 +
        attackerVariation
 +
      );
 +
    } else if (
 +
      isChecked(victim.bonusVariationActivation) &&
 +
      victim.hasOwnProperty(victimVariation) &&
 +
      victim.bonusVariationMinValue < victim.bonusVariationMaxValue
 +
    ) {
 +
      damagesWithVariation(
 +
        attacker,
 +
        victim,
 +
        attackType,
 +
        battle,
 +
        victim,
 +
        victimVariation
 +
       );
 +
    } else {
 +
       damagesWithoutVariation(attacker, victim, attackType, battle, characters);
 +
     }
 +
  });
 +
}
 +
 
 +
function createMapping() {
 +
  mapping = {
 +
     typeFlag: [
 +
       "animalBonus", // 0
 +
      "humanBonus", // 1
 +
      "orcBonus", // 2
 +
       "mysticBonus", // 3
 +
      "undeadBonus", // 4
 +
      "insectBonus", // 5
 +
       "desertBonus", // 6
 +
       "devilBonus", // 7
 
     ],
 
     ],
     marriageTable: {
+
     raceBonus: {
       harmonyEarrings: [4, 5, 6, 8],
+
       warrior: "warriorBonus",
       loveEarrings: [4, 5, 6, 8],
+
       sura: "suraBonus",
       harmonyBracelet: [4, 5, 6, 8],
+
       ninja: "ninjaBonus",
       loveNecklace: [20, 25, 30, 40],
+
       shaman: "shamanBonus",
       harmonyNecklace: [12, 16, 20, 30],
+
       lycan: "lycanBonus",
 
     },
 
     },
     allowedWeaponsPerRace: {
+
     raceResistance: {
       warrior: [0, 3, 8],
+
       warrior: "warriorResistance",
       ninja: [0, 1, 2, 8],
+
       sura: "suraResistance",
       sura: [0, 7, 8],
+
       ninja: "ninjaResistance",
       shaman: [4, 6, 8],
+
       shaman: "shamanResistance",
       lycan: [5, 8],
+
       lycan: "lycanResistance",
 
     },
 
     },
     translation: {
+
     defenseWeapon: [
       fr: {
+
       "swordDefense", // 0
        damages: "Dégâts",
+
      "daggerDefense", // 1
        percentage: "Pourcentage",
+
      "arrowDefense", // 2
        miss: "Miss",
+
      "twoHandedSwordDefense", // 3
        normalHit: "Coup classique",
+
      "bellDefense", // 4
        criticalHit: "Coup critique",
+
      "clawDefense", // 5
        piercingHit: "Coup perçant",
+
      "fanDefense", // 6
        criticalPiercingHit: "Coup critique perçant",
+
      "swordDefense", // 7
        damagesRepartition: "Distribution des dégâts",
+
       "fistDefense", // 8
       },
+
    ],
      en: {
+
    breakWeapon: [
        damages: "Damages",
+
      "breakSwordDefense", // 0
        percentage: "Percentage",
+
      "breakDaggerDefense", // 1
        miss: "Miss",
+
      "breakArrowDefense", // 2
        normalHit: "Normal hit",
+
      "breakTwoHandedSwordDefense", // 3
        criticalHit: "Critical hit",
+
      "breakBellDefense", // 4
        piercingHit: "Piercing hit",
+
       "breakClawDefense", // 5
        criticalPiercingHit: "Critical piercing hit",
+
      "breakFanDefense", // 6
        damagesRepartition: "Damages repartition",
+
      "breakSwordDefense", // 7
       },
+
    ],
      tr: {
+
    elementBonus: [
        damages: "Hasar",
+
      "fireBonus", // 0
        percentage: "Yüzde",
+
      "iceBonus", // 1
        miss: "Miss Vuruş",
+
      "windBonus", // 2
        normalHit: "Düz Vuruş",
+
       "lightningBonus", // 3
        criticalHit: "Kritik Vuruş",
+
      "earthBonus", // 4
        piercingHit: "Delici Vuruş",
+
      "darknessBonus", // 5
        criticalPiercingHit: "Kritikli Delici Vuruş",
+
    ],
        damagesRepartition: "Hasar Dağılımı",
+
    elementResistance: [
       },
+
      "fireResistance", // 0
      ro: {
+
      "iceResistance", // 1
        damages: "Daune",
+
      "windResistance", // 2
        percentage: "Procent",
+
       "lightningResistance", // 3
        miss: "Miss",
+
      "earthResistance", // 4
        normalHit: "Lovitura normala",
+
      "darknessResistance", // 5
        criticalHit: "Lovitura critica",
+
     ],
        piercingHit: "Lovitura patrunzatoare",
+
   };
        criticalPiercingHit: "Lovitura critica si patrunzatoare",
+
   return mapping;
        damagesRepartition: "Distribuția daunelor",
 
       },
 
      de: {
 
        damages: "Schäden",
 
        percentage: "Prozentsatz",
 
        miss: "Verfehlen",
 
        normalHit: "Normaler Treffer",
 
        criticalHit: "Kritischer Treffer",
 
        piercingHit: "Durchdringender Treffer",
 
        criticalPiercingHit: "Kritischer durchdringender Treffer",
 
        damagesRepartition: "",
 
      },
 
     },
 
   };
 
   return constants;
 
 
}
 
}
  
function initResultTableHistory(battle) {
+
function createConstants() {
   var {
+
   var constants = {
     tableResultHistory,
+
     polymorphPowerTable: [
    savedFights,
+
      10, 11, 11, 12, 13, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 26, 27,
     deleteFightTemplate,
+
      29, 31, 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 59, 62, 66, 70, 74, 79,
     numberFormats: { default: numberFormat },
+
      84, 89, 94, 100, 0,
  } = battle;
+
     ],
  var startIndex = 3;
+
     skillPowerTable: [
 
+
      0, 0.05, 0.06, 0.08, 0.1, 0.12, 0.14, 0.16, 0.18, 0.2, 0.22, 0.24, 0.26,
  if (savedFights.length) {
+
       0.28, 0.3, 0.32, 0.34, 0.36, 0.38, 0.4, 0.5, 0.52, 0.54, 0.56, 0.58, 0.6,
    hideElement(tableResultHistory.rows[1]);
+
       0.63, 0.66, 0.69, 0.72, 0.82, 0.85, 0.88, 0.91, 0.94, 0.98, 1.02, 1.06,
 
+
       1.1, 1.15, 1.25,
    for (var savedFight of savedFights) {
+
    ],
       addRowToTableResultHistory(
+
    marriageTable: {
        tableResultHistory,
+
      harmonyEarrings: [4, 5, 6, 8],
        savedFight,
+
      loveEarrings: [4, 5, 6, 8],
        deleteFightTemplate,
+
       harmonyBracelet: [4, 5, 6, 8],
        numberFormat
+
      loveNecklace: [20, 25, 30, 40],
       );
+
      harmonyNecklace: [12, 16, 20, 30],
    }
+
     },
  }
+
     allowedWeaponsPerRace: {
 
+
       warrior: [0, 3, 8],
  tableResultHistory.addEventListener("click", function (event) {
+
       ninja: [0, 1, 2, 8],
    var deleteButton = event.target.closest(".svg-icon-delete");
+
      sura: [0, 7, 8],
 
+
      shaman: [4, 6, 8],
    if (deleteButton) {
+
      lycan: [5, 8],
      var row = deleteButton.closest("tr");
+
    },
 
+
    translation: {
       if (row) {
+
      fr: {
        savedFights.splice(row.rowIndex - startIndex, 1);
+
         damages: "Dégâts",
        updateSavedFights(savedFights);
+
         percentage: "Pourcentage",
 
+
         miss: "Miss",
        row.remove();
+
         normalHit: "Coup classique",
 
+
         criticalHit: "Coup critique",
        if (tableResultHistory.rows.length === startIndex) {
+
         piercingHit: "Coup perçant",
          showElement(tableResultHistory.rows[1]);
+
         criticalPiercingHit: "Coup critique perçant",
        }
+
         damagesRepartition: "Distribution des dégâts",
       }
+
         averageDamages: "Dégâts moyens",
    }
+
         damagesAugmentation: "Augmentation des dégâts",
  });
+
         bonusVariationTitle: [
}
+
           "Évolution des dégâts moyens",
 
+
           "par rapport à la valeur d'un bonus",
function initChart(battle, chartSource) {
+
        ],
  var { translation, reduceChartPointsContainer, reduceChartPoints } = battle;
+
      },
 
+
      en: {
  function createChart() {
+
         damages: "Damage",
     var percentFormat = battle.numberFormats.percent;
+
        percentage: "Percentage",
     var customPlugins = {
+
         miss: "Miss",
       id: "customPlugins",
+
         normalHit: "Normal Hit",
       afterDraw(chart) {
+
        criticalHit: "Critical Hit",
        var missPercentage = chart.data.missPercentage;
+
        piercingHit: "Piercing Hit",
 
+
        criticalPiercingHit: "Critical Piercing Hit",
        if (!missPercentage) {
+
         damagesRepartition: "Damage Repartition",
          return;
+
         averageDamages: "Average Damage",
        }
+
         damagesAugmentation: "Damage Augmentation",
 
+
         bonusVariationTitle: [
        var {
+
          "Evolution of Average Damage",
          ctx,
+
          "Relative to a Bonus Value",
          chartArea: { top, right },
+
         ],
         } = chart;
 
         ctx.save();
 
         var text =
 
          translation.miss + " : " + percentFormat.format(missPercentage);
 
         var padding = 4;
 
         var fontSize = 14;
 
 
 
         ctx.font = fontSize + "px Helvetica Neue";
 
 
 
         var textWidth = ctx.measureText(text).width;
 
         var xPosition = right - textWidth - 5;
 
         var yPosition = top + 5;
 
 
 
         ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
 
         ctx.fillRect(
 
           xPosition - padding,
 
           yPosition - padding,
 
          textWidth + 2 * padding,
 
          fontSize + 2 * padding
 
         );
 
 
 
         ctx.strokeStyle = "red";
 
         ctx.strokeRect(
 
          xPosition - padding,
 
          yPosition - padding,
 
          textWidth + 2 * padding,
 
          fontSize + 2 * padding
 
         );
 
 
 
         ctx.fillStyle = "#666";
 
         ctx.textBaseline = "top";
 
         ctx.fillText(text, xPosition, yPosition + 1);
 
 
 
         ctx.restore();
 
 
       },
 
       },
    };
+
      tr: {
 
+
        damages: "Hasar",
    Chart.register(customPlugins);
+
        percentage: "Yüzde",
 
+
        miss: "Miss Vuruş",
    var ctx = battle.plotDamages.getContext("2d");
+
        normalHit: "Düz Vuruş",
    var maxLabelsInTooltip = 10;
+
        criticalHit: "Kritik Vuruş",
    var nullLabelText = " ...";
+
        piercingHit: "Delici Vuruş",
 
+
        criticalPiercingHit: "Kritikli Delici Vuruş",
    var chart = new Chart(ctx, {
+
        damagesRepartition: "Hasar Dağılımı",
      type: "scatter",
+
        averageDamages: "Ortalama Hasar",
      data: {
+
         damagesAugmentation: "Ortalama Hasar Artışı",
         missPercentage: 0,
+
         bonusVariationTitle: [
         datasets: [],
+
          "Bir bonusun değerine kıyasla",
 +
          "Ortalama Hasar Çizelgesi",
 +
        ],
 
       },
 
       },
       options: {
+
       ro: {
         responsive: true,
+
         damages: "Daune",
         maintainAspectRatio: false,
+
         percentage: "Procent",
         plugins: {
+
         miss: "Miss",
          legend: {
+
        normalHit: "Lovitura normala",
            display: true,
+
        criticalHit: "Lovitura critica",
            onClick: function (e, legendItem, legend) {
+
        piercingHit: "Lovitura patrunzatoare",
              var currentIndex = legendItem.datasetIndex;
+
        criticalPiercingHit: "Lovitura critica si patrunzatoare",
              var ci = legend.chart;
+
        damagesRepartition: "Distribuția daunelor",
              var isCurrentDatasetVisible = ci.isDatasetVisible(currentIndex);
+
        averageDamages: "Media damageului",
              var datasets = ci.data.datasets;
+
        damagesAugmentation: "Damage imbunatatit",
              var hideReducePoints = true;
+
        bonusVariationTitle: [
              var isReducePointsChecked = reduceChartPoints.checked;
+
          "Evolutia mediei damageului",
 
+
          "relativ la o valoare bonus",
              datasets[currentIndex].hidden = isCurrentDatasetVisible;
+
        ],
              legendItem.hidden = isCurrentDatasetVisible;
+
      },
 
+
      de: {
              for (var index in datasets) {
+
        damages: "Schäden",
                if (
+
        percentage: "Prozentsatz",
                  ci.isDatasetVisible(index) &&
+
        miss: "Verfehlen",
                  datasets[index].canBeReduced
+
        normalHit: "Normaler Treffer",
                ) {
+
        criticalHit: "Kritischer Treffer",
                  showElement(reduceChartPointsContainer);
+
        piercingHit: "Durchdringender Treffer",
                  hideReducePoints = false;
+
        criticalPiercingHit: "Kritischer durchdringender Treffer",
                  break;
+
        damagesRepartition: "Schadensverteilung",
                }
+
        averageDamages: "Durchschnittlicher Schaden",
              }
+
        damagesAugmentation: "Schadenserhöhung",
 
+
        bonusVariationTitle: [
              if (hideReducePoints) {
+
          "Entwicklung des durchschnittlichen Schadens",
                hideElement(reduceChartPointsContainer);
+
          "im Verhältnis zu einem Bonus",
                handleChartAnimations(ci, true);
+
        ],
              } else {
+
      },
                handleChartAnimations(ci, isReducePointsChecked);
+
      pt: {
              }
+
        damages: "Dano",
 
+
        percentage: "Percentagem",
              ci.update();
+
        miss: "Miss",
            },
+
        normalHit: "Dano normal",
          },
+
        criticalHit: "Dano crítico",
          title: {
+
        piercingHit: "Dano perfurante",
            display: true,
+
        criticalPiercingHit: "Dano crítico perfurante",
            text: translation.damagesRepartition,
+
        damagesRepartition: "Repartição de dano",
            font: {
+
        averageDamages: "Dano médio",
              size: 20,
+
        damagesAugmentation: "Aumento de dano",
            },
+
        bonusVariationTitle: ["Evolução do dano médio", "relativo a um bónus"],
          },
+
      },
          tooltip: {
+
      // es: {
            callbacks: {
+
      //  damages: "Daño",
              label: function (context) {
+
      //  percentage: "Porcentaje",
                if (context.label === null) {
+
      //  miss: "Miss",
                  return nullLabelText;
+
      //  normalHit: "Daño normal",
                }
+
      //  criticalHit: "Daño crítico",
 
+
      //  piercingHit: "Daño perforante",
                var xValue = battle.numberFormats.default.format(
+
      //  criticalPiercingHit: "Daño crítico perforante",
                  context.parsed.x
+
      //  damagesRepartition: "Repartición de daños",
                );
+
      //  averageDamages: "Daño medio",
                var yValue = battle.numberFormats.percent.format(
+
      //  damagesAugmentation: "Aumento de daño",
                  context.parsed.y
+
      //  bonusVariationTitle: ["Evolución del daño medio", "Relativo a una bonificación"]
                );
+
      // },
 
+
    },
                label =
+
  };
                  " " +
+
  return constants;
                  context.dataset.label +
+
}
                  " : (" +
+
 
                  xValue +
+
function initResultTableHistory(battle) {
                  ", " +
+
  var {
                  yValue +
+
    tableResultHistory,
                  ")";
+
    savedFights,
 
+
    deleteFightTemplate,
                return label;
+
    numberFormats: { default: numberFormat },
              },
+
  } = battle;
              beforeBody: function (tooltipItems) {
+
  var startIndex = 3;
                if (tooltipItems.length > maxLabelsInTooltip + 1) {
+
 
                  tooltipItems.splice(maxLabelsInTooltip + 1);
+
  if (savedFights.length) {
                  tooltipItems[maxLabelsInTooltip].label = null;
+
    hideElement(tableResultHistory.rows[1]);
                }
+
 
              },
+
    for (var savedFight of savedFights) {
            },
+
      addRowToTableResultHistory(
            caretPadding: 10,
+
        tableResultHistory,
          },
+
        savedFight,
        },
+
        deleteFightTemplate,
        scales: {
+
        numberFormat
          x: {
+
      );
            type: "linear",
+
    }
            position: "bottom",
+
  }
            title: {
+
 
              display: true,
+
  tableResultHistory.addEventListener("click", function (event) {
              text: translation.damages,
+
    var deleteButton = event.target.closest(".svg-icon-delete");
              font: {
+
 
                size: 16,
+
    if (deleteButton) {
              },
+
      var row = deleteButton.closest("tr");
            },
+
 
          },
+
      if (row) {
          y: {
+
        savedFights.splice(row.rowIndex - startIndex, 1);
            title: {
+
        updateSavedFights(savedFights);
              display: true,
+
 
              text: translation.percentage,
+
        row.remove();
              font: {
+
 
                size: 16,
+
        if (tableResultHistory.rows.length === startIndex) {
              },
+
          showElement(tableResultHistory.rows[1]);
            },
+
        }
            ticks: {
+
      }
              format: {
+
    }
                style: "percent",
+
  });
              },
+
}
            },
+
 
          },
+
function initDamagesChart(battle) {
        },
+
  var { translation, reduceChartPointsContainer, reduceChartPoints } = battle;
        elements: {
+
  var percentFormat = battle.numberFormats.percent;
          point: {
+
  var customPlugins = {
            borderWidth: 1,
+
    id: "customPlugins",
            radius: 3,
+
    afterDraw(chart) {
            hitRadius: 3,
+
      var missPercentage = chart.data.missPercentage;
            hoverRadius: 6,
+
 
            hoverBorderWidth: 2,
+
      if (!missPercentage) {
          },
+
         return;
         },
+
       }
       },
+
 
    });
+
      var {
 
+
         ctx,
    var datasetsStyle = [
+
         chartArea: { top, right },
      {
+
      } = chart;
         name: "normalHit",
+
      ctx.save();
         canBeReduced: false,
+
      var text =
         label: translation.normalHit,
+
         translation.miss + " : " + percentFormat.format(missPercentage);
        backgroundColor: "rgba(75, 192, 192, 0.2)",
+
      var padding = 4;
        borderColor: "rgba(75, 192, 192, 1)",
+
      var fontSize = 14;
      },
+
 
       {
+
       ctx.font = fontSize + "px Helvetica Neue";
        name: "piercingHit",
+
 
        canBeReduced: false,
+
      var textWidth = ctx.measureText(text).width;
        label: translation.piercingHit,
+
      var xPosition = right - textWidth - 5;
        backgroundColor: "rgba(192, 192, 75, 0.2)",
+
      var yPosition = top + 5;
        borderColor: "rgba(192, 192, 75, 1)",
+
 
       },
+
      ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
      {
+
       ctx.fillRect(
         name: "criticalHit",
+
         xPosition - padding,
         canBeReduced: false,
+
         yPosition - padding,
         label: translation.criticalHit,
+
         textWidth + 2 * padding,
         backgroundColor: "rgba(192, 75, 192, 0.2)",
+
         fontSize + 2 * padding
         borderColor: "rgba(192, 75, 192, 1)",
+
      );
      },
+
 
       {
+
      ctx.strokeStyle = "red";
        name: "criticalPiercingHit",
+
      ctx.strokeRect(
        canBeReduced: false,
+
        xPosition - padding,
        label: translation.criticalPiercingHit,
+
         yPosition - padding,
        backgroundColor: "rgba(75, 75, 192, 0.2)",
+
        textWidth + 2 * padding,
        borderColor: "rgba(75, 75, 192, 1)",
+
        fontSize + 2 * padding
      },
+
      );
    ];
+
 
    battle.damagesChart = {
+
       ctx.fillStyle = "#666";
      chart: chart,
+
      ctx.textBaseline = "top";
      datasetsStyle: datasetsStyle,
+
      ctx.fillText(text, xPosition, yPosition + 1);
       maxPoints: 500,
+
 
       reduceChartPointsContainer: reduceChartPointsContainer,
+
      ctx.restore();
     };
+
    },
  }
+
  };
 
+
 
  loadScript(chartSource, createChart);
+
  Chart.register(customPlugins);
}
+
 
 
+
  var ctx = battle.plotDamages.getContext("2d");
function attackSelectonListener(
+
  var maxLabelsInTooltip = 10;
   characters,
+
  var nullLabelText = " ...";
   attackerSelection,
+
 
   attackTypeSelection
+
  var chart = new Chart(ctx, {
) {
+
    type: "scatter",
   attackerSelection.addEventListener("change", function (event) {
+
    data: {
     var attackerName = event.target.value;
+
       missPercentage: 0,
 
+
       datasets: [],
     if (isPseudoSaved(characters, attackerName)) {
+
     },
       var attacker = characters.savedCharacters[attackerName];
+
    options: {
 +
      responsive: true,
 +
      maintainAspectRatio: false,
 +
      plugins: {
 +
        legend: {
 +
          display: true,
 +
          onHover: function (e) {
 +
            e.native.target.style.cursor = "pointer";
 +
          },
 +
          onLeave: function (e) {
 +
            e.native.target.style.cursor = "default";
 +
          },
 +
          onClick: function (e, legendItem, legend) {
 +
            var currentIndex = legendItem.datasetIndex;
 +
            var ci = legend.chart;
 +
            var isCurrentDatasetVisible = ci.isDatasetVisible(currentIndex);
 +
            var datasets = ci.data.datasets;
 +
            var hideReducePoints = true;
 +
            var isReducePointsChecked = reduceChartPoints.checked;
 +
 
 +
            datasets[currentIndex].hidden = isCurrentDatasetVisible;
 +
            legendItem.hidden = isCurrentDatasetVisible;
 +
 
 +
            for (var index in datasets) {
 +
              if (ci.isDatasetVisible(index) && datasets[index].canBeReduced) {
 +
                showElement(reduceChartPointsContainer);
 +
                hideReducePoints = false;
 +
                break;
 +
              }
 +
            }
 +
 
 +
            if (hideReducePoints) {
 +
              hideElement(reduceChartPointsContainer);
 +
              handleChartAnimations(ci, true);
 +
            } else {
 +
              handleChartAnimations(ci, isReducePointsChecked);
 +
            }
 +
 
 +
            ci.update();
 +
          },
 +
        },
 +
        title: {
 +
          display: true,
 +
          text: translation.damagesRepartition,
 +
          font: {
 +
            size: 20,
 +
          },
 +
        },
 +
        tooltip: {
 +
          callbacks: {
 +
            label: function (context) {
 +
              if (context.label === null) {
 +
                return nullLabelText;
 +
              }
 +
 
 +
              var xValue = battle.numberFormats.default.format(
 +
                context.parsed.x
 +
              );
 +
              var yValue = battle.numberFormats.percent.format(
 +
                context.parsed.y
 +
              );
 +
 
 +
              label =
 +
                " " +
 +
                context.dataset.label +
 +
                " : (" +
 +
                xValue +
 +
                ", " +
 +
                yValue +
 +
                ")";
 +
 
 +
              return label;
 +
            },
 +
            beforeBody: function (tooltipItems) {
 +
              if (tooltipItems.length > maxLabelsInTooltip + 1) {
 +
                tooltipItems.splice(maxLabelsInTooltip + 1);
 +
                tooltipItems[maxLabelsInTooltip].label = null;
 +
              }
 +
            },
 +
          },
 +
          caretPadding: 10,
 +
        },
 +
      },
 +
      scales: {
 +
        x: {
 +
          type: "linear",
 +
          position: "bottom",
 +
          title: {
 +
            display: true,
 +
            text: translation.damages,
 +
            font: {
 +
              size: 16,
 +
            },
 +
          },
 +
        },
 +
        y: {
 +
          title: {
 +
            display: true,
 +
            text: translation.percentage,
 +
            font: {
 +
              size: 16,
 +
            },
 +
          },
 +
          ticks: {
 +
            format: {
 +
              style: "percent",
 +
            },
 +
          },
 +
        },
 +
      },
 +
      elements: {
 +
        point: {
 +
          borderWidth: 1,
 +
          radius: 3,
 +
          hitRadius: 3,
 +
          hoverRadius: 6,
 +
          hoverBorderWidth: 2,
 +
        },
 +
      },
 +
    },
 +
  });
 +
 
 +
  var datasetsStyle = [
 +
    {
 +
      name: "normalHit",
 +
      canBeReduced: false,
 +
      label: translation.normalHit,
 +
      backgroundColor: "rgba(75, 192, 192, 0.2)",
 +
      borderColor: "rgba(75, 192, 192, 1)",
 +
    },
 +
    {
 +
      name: "piercingHit",
 +
      canBeReduced: false,
 +
      label: translation.piercingHit,
 +
      backgroundColor: "rgba(192, 192, 75, 0.2)",
 +
      borderColor: "rgba(192, 192, 75, 1)",
 +
    },
 +
    {
 +
      name: "criticalHit",
 +
      canBeReduced: false,
 +
      label: translation.criticalHit,
 +
      backgroundColor: "rgba(192, 75, 192, 0.2)",
 +
      borderColor: "rgba(192, 75, 192, 1)",
 +
    },
 +
    {
 +
      name: "criticalPiercingHit",
 +
      canBeReduced: false,
 +
      label: translation.criticalPiercingHit,
 +
      backgroundColor: "rgba(75, 75, 192, 0.2)",
 +
      borderColor: "rgba(75, 75, 192, 1)",
 +
    },
 +
  ];
 +
  battle.damagesChart = {
 +
    chart: chart,
 +
    datasetsStyle: datasetsStyle,
 +
    maxPoints: 500,
 +
    reduceChartPointsContainer: reduceChartPointsContainer,
 +
  };
 +
}
 +
 
 +
function initBonusVariationChart(battle) {
 +
  var translation = battle.translation;
 +
 
 +
  var ctx = battle.plotBonusVariation.getContext("2d");
 +
 
 +
  var chart = new Chart(ctx, {
 +
    type: "line",
 +
    data: {
 +
      datasets: [
 +
        {
 +
          label: translation.averageDamages,
 +
          backgroundColor: "rgba(75, 192, 192, 0.2)",
 +
          borderColor: "rgba(75, 192, 192, 1)",
 +
          fill: true,
 +
        },
 +
        {
 +
          label: translation.damagesAugmentation,
 +
          backgroundColor: "rgba(192, 192, 75, 0.2)",
 +
          borderColor: "rgba(192, 192, 75, 1)",
 +
          hidden: true,
 +
          yTicksFormat: { style: "percent" },
 +
          fill: true,
 +
        },
 +
      ],
 +
    },
 +
    options: {
 +
      responsive: true,
 +
      maintainAspectRatio: false,
 +
      plugins: {
 +
        legend: {
 +
          display: true,
 +
          onHover: function (e) {
 +
            e.native.target.style.cursor = "pointer";
 +
          },
 +
          onLeave: function (e) {
 +
            e.native.target.style.cursor = "default";
 +
          },
 +
          onClick: function (e, legendItem, legend) {
 +
            var currentIndex = legendItem.datasetIndex;
 +
            var ci = legend.chart;
 +
            var datasets = ci.data.datasets;
 +
            var isCurrentDatasetVisible = ci.isDatasetVisible(currentIndex);
 +
            var yAxis = ci.options.scales.y;
 +
 
 +
            var otherIndex = currentIndex === 0 ? 1 : 0;
 +
            var visibleDataset = isCurrentDatasetVisible
 +
              ? datasets[otherIndex]
 +
              : datasets[currentIndex];
 +
 
 +
            datasets[currentIndex].hidden = isCurrentDatasetVisible;
 +
            datasets[otherIndex].hidden = !isCurrentDatasetVisible;
 +
 
 +
            yAxis.title.text = visibleDataset.label;
 +
            yAxis.ticks.format = visibleDataset.yTicksFormat;
 +
 
 +
            ci.update();
 +
          },
 +
        },
 +
        title: {
 +
          display: true,
 +
          text: translation.bonusVariationTitle,
 +
          font: {
 +
            size: 18,
 +
          },
 +
        },
 +
        tooltip: {
 +
          caretPadding: 10,
 +
        },
 +
      },
 +
      scales: {
 +
        x: {
 +
          type: "linear",
 +
          position: "bottom",
 +
          title: {
 +
            display: true,
 +
            text: "Bonus",
 +
            font: {
 +
              size: 16,
 +
            },
 +
          },
 +
          ticks: {
 +
            callback: function (value) {
 +
              if (Number.isInteger(value)) {
 +
                return Number(value);
 +
              }
 +
            },
 +
          },
 +
        },
 +
        y: {
 +
          title: {
 +
            display: true,
 +
            text: translation.averageDamages,
 +
            font: {
 +
              size: 16,
 +
            },
 +
          },
 +
        },
 +
      },
 +
      elements: {
 +
        point: {
 +
          borderWidth: 1,
 +
          radius: 3,
 +
          hitRadius: 3,
 +
          hoverRadius: 6,
 +
          hoverBorderWidth: 2,
 +
        },
 +
      },
 +
    },
 +
  });
 +
 
 +
  battle.bonusVariationChart = chart;
 +
}
 +
 
 +
function attackSelectonListener(
 +
   characters,
 +
   attackerSelection,
 +
   attackTypeSelection
 +
) {
 +
   attackerSelection.addEventListener("change", function (event) {
 +
     var attackerName = event.target.value;
 +
 
 +
     if (isPseudoSaved(characters, attackerName)) {
 +
       var attacker = characters.savedCharacters[attackerName];
 
       filterAttackTypeSelection(attacker, attackTypeSelection);
 
       filterAttackTypeSelection(attacker, attackTypeSelection);
 
     } else {
 
     } else {
Ligne 4 392 : Ligne 4 907 :
 
     bonusVariation: {
 
     bonusVariation: {
 
       tab: document.getElementById("Variation"),
 
       tab: document.getElementById("Variation"),
 +
      activation: document.getElementById("bonus-variation-activation"),
 
       input: document.getElementById("bonus-variation"),
 
       input: document.getElementById("bonus-variation"),
 
       inputDisplay: document.getElementById("bonus-variation-display"),
 
       inputDisplay: document.getElementById("bonus-variation-display"),
 
       container: document.getElementById("bonus-variation-range"),
 
       container: document.getElementById("bonus-variation-range"),
      referenceValue: document.getElementById("bonus-variation-reference"),
 
 
       minValue: document.getElementById("bonus-variation-min-value"),
 
       minValue: document.getElementById("bonus-variation-min-value"),
 
       maxValue: document.getElementById("bonus-variation-max-value"),
 
       maxValue: document.getElementById("bonus-variation-max-value"),
      step: document.getElementById("bonus-variation-step"),
 
 
     },
 
     },
 
   };
 
   };
  
   delete characters.newCharacterTemplate.dataset.click;
+
   characters.bonusVariation.inputDisplay.setAttribute("readonly", "");
  
 
   for (var [pseudo, character] of Object.entries(getSavedCharacters())) {
 
   for (var [pseudo, character] of Object.entries(getSavedCharacters())) {
Ligne 4 424 : Ligne 4 938 :
 
     damagesWeightedByType: {},
 
     damagesWeightedByType: {},
 
     scatterDataByType: {},
 
     scatterDataByType: {},
 +
    damagesByBonus: [],
 
     tableResultFight: document.getElementById("result-table-fight"),
 
     tableResultFight: document.getElementById("result-table-fight"),
 
     tableResultHistory: document.getElementById("result-table-history"),
 
     tableResultHistory: document.getElementById("result-table-history"),
Ligne 4 430 : Ligne 4 945 :
 
     errorInformation: {},
 
     errorInformation: {},
 
     fightResultContainer: document.getElementById("fight-result-container"),
 
     fightResultContainer: document.getElementById("fight-result-container"),
 +
    downLoadRawData: document.getElementById("download-raw-data"),
 +
    downLoadRawDataVariation: document.getElementById(
 +
      "download-raw-data-variation"
 +
    ),
 +
    bonusVariationResultContainer: document.getElementById(
 +
      "bonus-variation-result-container"
 +
    ),
 
     reduceChartPointsContainer: document.getElementById(
 
     reduceChartPointsContainer: document.getElementById(
 
       "reduce-chart-points-container"
 
       "reduce-chart-points-container"
Ligne 4 435 : Ligne 4 957 :
 
     reduceChartPoints: document.getElementById("reduce-chart-points"),
 
     reduceChartPoints: document.getElementById("reduce-chart-points"),
 
     plotDamages: document.getElementById("plot-damages"),
 
     plotDamages: document.getElementById("plot-damages"),
 +
    plotBonusVariation: document.getElementById("plot-bonus-variation"),
 
     uniqueDamagesCounters: document.querySelectorAll(".unique-damages-counter"),
 
     uniqueDamagesCounters: document.querySelectorAll(".unique-damages-counter"),
 
     possibleDamagesCounter: document.getElementById("possible-damages-counter"),
 
     possibleDamagesCounter: document.getElementById("possible-damages-counter"),
 
     damagesTime: document.getElementById("damages-time"),
 
     damagesTime: document.getElementById("damages-time"),
 
     displayTime: document.getElementById("display-time"),
 
     displayTime: document.getElementById("display-time"),
 +
    simulationCounter: document.getElementById("simulation-counter"),
 +
    simulationTime: document.getElementById("simulation-time"),
 
     numberFormats: {
 
     numberFormats: {
 
       default: new Intl.NumberFormat(undefined, {
 
       default: new Intl.NumberFormat(undefined, {
Ligne 4 466 : Ligne 4 991 :
 
   );
 
   );
 
   initResultTableHistory(battle);
 
   initResultTableHistory(battle);
   initChart(battle, chartSource);
+
   loadScript(chartSource, function () {
 +
    initDamagesChart(battle);
 +
    initBonusVariationChart(battle);
 +
  });
 
   reduceChartPointsListener(battle);
 
   reduceChartPointsListener(battle);
 
   downloadRawDataListener(battle);
 
   downloadRawDataListener(battle);
  
   var errorElements = document
+
   var errorElements = document.querySelectorAll("[data-error]");
    .getElementById("error-information")
 
    .querySelectorAll("li[data-error]");
 
  
 
   for (var index = 0; index < errorElements.length; index++) {
 
   for (var index = 0; index < errorElements.length; index++) {

Version actuelle datée du 20 novembre 2024 à 17:07

function showElement(element) {
  element.classList.remove("tabber-noactive");
}

function hideElement(element) {
  element.classList.add("tabber-noactive");
}

function removeAccent(str) {
  return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
}

function toNormalForm(str) {
  return removeAccent(str)
    .replace("-", " ")
    .replace(/[^a-zA-Z0-9 ]/g, "")
    .toLowerCase();
}

function pseudoFormat(str) {
  return removeAccent(str).replace(/[^A-Za-z0-9 \(\)\+_-]+/g, "");
}

function isValueInArray(value, array) {
  return array.indexOf(value) !== -1;
}

function copyObject(object) {
  var copy = {};
  for (var key in object) {
    copy[key] = object[key];
  }
  return copy;
}

function compareNumbers(a, b) {
  return b - a;
}

function isChecked(attribute) {
  return attribute === "on";
}

function floorMultiplication(firstFactor, secondFactor) {
  return Math.floor((firstFactor * secondFactor).toFixed(8));
}

function truncateNumber(number, precision) {
  return Math.floor(number * 10 ** precision) / 10 ** precision;
}

function addKeyValue(object, key, value) {
  if (object.hasOwnProperty(key)) {
    object[key] += value;
  } else {
    object[key] = value;
  }
}

function editTableResultRow(row, valuesToDisplay, numberFormat) {
  row.innerHTML = "";
  for (var index = 0; index < valuesToDisplay.length; index++) {
    var cell = row.insertCell();
    var textContent;

    if (index >= 3) {
      textContent = numberFormat.format(valuesToDisplay[index]);
    } else {
      textContent = valuesToDisplay[index];
    }

    cell.textContent = textContent;
  }

  return row;
}

function addRowToTableResultHistory(
  tableResultHistory,
  valuesToDisplay,
  deleteFightTemplate,
  numberFormat
) {
  var row = tableResultHistory.insertRow();
  editTableResultRow(row, valuesToDisplay, numberFormat);
  var cell = row.insertCell();
  cell.appendChild(deleteFightTemplate.cloneNode(true));
}

function calcMeanDamages(damagesWeightedByType, totalCardinal) {
  var sumDamages = 0;

  for (var damagesTypeName in damagesWeightedByType) {
    if (damagesTypeName === "miss") {
      continue;
    }

    var damagesWeighted = damagesWeightedByType[damagesTypeName];

    for (var damages in damagesWeighted) {
      sumDamages += damages * damagesWeighted[damages];
    }
  }

  return sumDamages / totalCardinal;
}

function prepareDamagesData(
  damagesWeightedByType,
  possibleDamagesCountTemp,
  totalCardinal
) {
  var minDamages = Infinity;
  var maxDamages = 0;
  var scatterDataByType = {};
  var sumDamages = 0;
  var possibleDamagesCount = 0;
  var uniqueDamagesCount = 0;

  for (var damagesTypeName in damagesWeightedByType) {
    if (damagesTypeName === "miss") {
      scatterDataByType.miss = damagesWeightedByType.miss;
      possibleDamagesCount++;
      uniqueDamagesCount++;
      continue;
    }

    var firstIteration = true;
    var damagesWeighted = damagesWeightedByType[damagesTypeName];
    var scatterData = [];
    scatterDataByType[damagesTypeName] = scatterData;

    for (var damages in damagesWeighted) {
      damages = +damages;

      if (firstIteration) {
        if (damages < minDamages) {
          minDamages = damages;
        }
        firstIteration = false;
      }

      var weight = damagesWeighted[damages];
      var probability = weight / totalCardinal;

      sumDamages += damages * weight;
      damagesWeighted[damages] = probability;
      scatterData.push({ x: damages, y: probability });
    }

    var scatterDataLength = scatterData.length;

    possibleDamagesCount += possibleDamagesCountTemp;
    uniqueDamagesCount += scatterDataLength;

    if (damages > maxDamages) {
      maxDamages = damages;
    }
  }

  if (minDamages === Infinity) {
    minDamages = 0;
  }

  return [
    sumDamages / totalCardinal,
    minDamages,
    maxDamages,
    scatterDataByType,
    possibleDamagesCount,
    uniqueDamagesCount,
  ];
}

function aggregateDamages(scatterData, maxPoints) {
  var dataLength = scatterData.length;
  var remainingData = dataLength;
  var aggregateScatterData = [];

  for (var groupIndex = 0; groupIndex < maxPoints; groupIndex++) {
    var groupLength = Math.floor(remainingData / (maxPoints - groupIndex));
    var startIndex = dataLength - remainingData;
    var aggregateDamages = 0;
    var aggregateProbability = 0;

    for (var index = startIndex; index < startIndex + groupLength; index++) {
      var { x: damages, y: probability } = scatterData[index];
      aggregateDamages += damages * probability;
      aggregateProbability += probability;
    }

    aggregateScatterData.push({
      x: aggregateDamages / aggregateProbability,
      y: aggregateProbability,
    });

    remainingData -= groupLength;
  }

  return aggregateScatterData;
}

function addToDamagesChart(
  scatterDataByType,
  damagesChart,
  isReducePointsChecked
) {
  var { chart, datasetsStyle, maxPoints, reduceChartPointsContainer } =
    damagesChart;
  var isFirstDataset = true;
  var datasets = chart.data.datasets;

  datasets.length = 0;

  for (var index = 0; index < datasetsStyle.length; index++) {
    var dataset = copyObject(datasetsStyle[index]);

    if (!scatterDataByType.hasOwnProperty(dataset.name)) {
      continue;
    }

    var scatterData = scatterDataByType[dataset.name];
    var canBeReduced = scatterData.length > 2 * maxPoints;

    dataset.hidden = !isFirstDataset;
    dataset.canBeReduced = canBeReduced;

    if (canBeReduced && isReducePointsChecked) {
      dataset.data = aggregateDamages(scatterData, maxPoints);
    } else {
      dataset.data = scatterData;
    }

    if (isFirstDataset) {
      isFirstDataset = false;

      if (canBeReduced) {
        showElement(reduceChartPointsContainer);

        if (!isReducePointsChecked) {
          handleChartAnimations(chart, false);
        }
      } else {
        hideElement(reduceChartPointsContainer);
        handleChartAnimations(chart, true);
      }
    }

    datasets.push(dataset);
  }

  chart.data.missPercentage = scatterDataByType.miss;
  chart.update();
}

function addToBonusVariationChart(
  damagesByBonus,
  augmentationByBonus,
  xLabel,
  chart
) {
  chart.data.datasets[0].data = damagesByBonus;
  chart.data.datasets[1].data = augmentationByBonus;
  chart.options.scales.x.title.text = xLabel;
  chart.update();
}

function handleChartAnimations(chart, addAnimations) {
  chart.options.animation = addAnimations;
  chart.options.animations.colors = addAnimations;
  chart.options.animations.x = addAnimations;
  chart.options.transitions.active.animation.duration = addAnimations * 1000;
}

function updateDamagesChartDescription(
  uniqueDamagesCounters,
  uniqueDamagesCount,
  formatNumber
) {
  uniqueDamagesCounters.forEach(function (element) {
    if (uniqueDamagesCount <= 1) {
      hideElement(element.parentElement);
    } else {
      showElement(element.parentElement);
      element.textContent = formatNumber.format(uniqueDamagesCount);
    }
  });
}

function getMonsterName(monsterVnum) {
  var monsterAttributes = monsterData[monsterVnum];
  return monsterAttributes[monsterAttributes.length - 1];
}

function filterClass(selectedRace, classChoice, selectValueIsChanged = false) {
  showElement(classChoice.parentElement);

  for (var option of classChoice.options) {
    if (option.getAttribute("data-race") === selectedRace) {
      if (!selectValueIsChanged) {
        classChoice.value = option.value;
        selectValueIsChanged = true;
      }
      showElement(option);
    } else {
      hideElement(option);
    }
  }
  if (selectedRace == "lycan") {
    hideElement(classChoice.parentElement);
  }
}

function filterWeapon(
  selectedRace,
  weaponElement,
  weaponCategory,
  allowedWeaponsPerRace,
  selectValueIsChanged = false
) {
  var allowedWeapons = allowedWeaponsPerRace[selectedRace];

  if (!selectValueIsChanged) {
    var weaponType = weaponData[weaponElement.value][1];

    if (!isValueInArray(weaponType, allowedWeapons)) {
      weaponElement.value = 0;
    }
  }

  var children = weaponCategory.children;

  for (var index = 0; index < children.length; index++) {
    var child = children[index];

    if (isValueInArray(index, allowedWeapons)) {
      showElement(child);
    } else {
      hideElement(child);
    }
  }
}

function getSelectedWeapon(weaponCategory) {
  return weaponCategory.querySelector("input[type='radio']:checked");
}

function handleWeaponDisplay(weaponDisplay, newWeapon, weaponVnum) {
  var newImage = newWeapon.nextElementSibling.cloneNode();
  var newText = document.createElement("span");
  var oldImage = weaponDisplay.firstChild;
  var oldText = oldImage.nextElementSibling;
  var weaponName = createWeapon(weaponVnum).name;

  if (weaponVnum == 0) {
    newText.textContent = " " + weaponName + " ";
  } else {
    var weaponLink = document.createElement("a");
    weaponLink.href = mw.util.getUrl(weaponName);
    weaponLink.title = weaponName;
    weaponLink.textContent = weaponName;

    newText.appendChild(document.createTextNode(" "));
    newText.appendChild(weaponLink);
    newText.appendChild(document.createTextNode(" "));
  }

  weaponDisplay.replaceChild(newImage, oldImage);
  weaponDisplay.replaceChild(newText, oldText);
}

function filterUpgrade(
  selectedRace,
  weaponUpgrade,
  weaponVnum,
  randomAttackValue,
  randomMagicAttackValue,
  currentUpgrade
) {
  var weapon = createWeapon(weaponVnum);

  if (weapon.isSerpent) {
    showElement(randomAttackValue);

    if (selectedRace === "sura" || selectedRace === "shaman") {
      showElement(randomMagicAttackValue);
    }
  } else {
    hideElement(randomAttackValue);
    hideElement(randomMagicAttackValue);
  }

  var upgradeNumber = weapon.upgrades.length;

  if (upgradeNumber <= 1) {
    hideElement(weaponUpgrade.parentElement);
  } else {
    showElement(weaponUpgrade.parentElement);
  }

  weaponUpgrade.innerHTML = "";

  for (var upgrade = 0; upgrade < upgradeNumber; upgrade++) {
    var option = document.createElement("option");
    option.value = upgrade;
    option.textContent = "+" + upgrade;
    weaponUpgrade.appendChild(option);
  }
  if (currentUpgrade === undefined) {
    option.selected = true;
  } else {
    weaponUpgrade.value = currentUpgrade;
    currentUpgrade = undefined;
  }
}

function filterState(selectedState, polymorphMonster) {
  if (selectedState === "polymorph") {
    showElement(polymorphMonster.parentElement);
  } else {
    hideElement(polymorphMonster.parentElement);
  }
}

function filterCheckbox(checkbox, element) {
  if (checkbox.checked) {
    showElement(element);
  } else {
    hideElement(element);
  }
}

function filterSkills(selectedClass, skillElementsToFilter) {
  for (var element of skillElementsToFilter) {
    if (isValueInArray(selectedClass, element.dataset.class)) {
      showElement(element);
    } else {
      hideElement(element);
    }
  }
}

function filterAttackTypeSelection(attacker, attackTypeSelection) {
  var attackerClass = attacker.class;
  var selectedOption =
    attackTypeSelection.options[attackTypeSelection.selectedIndex];
  var attackerIsNotPolymorph = !isPolymorph(attacker);

  for (var index = 2; index < attackTypeSelection.options.length; index++) {
    var option = attackTypeSelection.options[index];
    var optionClass = option.dataset.class;
    var optionValue = option.value;

    if (
      attackerIsNotPolymorph &&
      attacker[optionValue] &&
      (attackerClass === optionClass ||
        (optionValue.startsWith("horseSkill") &&
          isRiding(attacker) &&
          (!optionClass || isValueInArray(attackerClass, optionClass))))
    ) {
      showElement(option);
    } else {
      hideElement(option);

      if (selectedOption === option) {
        attackTypeSelection.selectedIndex = 0;
      }
    }
  }
}

function filterAttackTypeSelectionMonster(attackTypeSelection) {
  for (var index = 2; index < attackTypeSelection.options.length; index++) {
    hideElement(attackTypeSelection.options[index]);
  }

  if (attackTypeSelection.selectedIndex !== 1) {
    attackTypeSelection.selectedIndex = 0;
  }
}

function filterForm(characters, battle) {
  var characterCreation = characters.characterCreation;
  var allowedWeaponsPerRace = battle.constants.allowedWeaponsPerRace;

  characterCreation.addEventListener("change", function (event) {
    var target = event.target;
    var targetName = target.name;

    switch (targetName) {
      case "race":
        var selectedRace = target.value;
        var classChoice = characterCreation.class;
        var weaponElement = characterCreation.weapon;

        filterClass(selectedRace, classChoice);
        filterWeapon(
          selectedRace,
          weaponElement,
          characters.weaponCategory,
          allowedWeaponsPerRace
        );

        var newWeapon = getSelectedWeapon(characters.weaponCategory);
        handleWeaponDisplay(
          characters.weaponDisplay,
          newWeapon,
          weaponElement.value
        );
        filterUpgrade(
          selectedRace,
          characterCreation.weaponUpgrade,
          weaponElement.value,
          characters.randomAttackValue,
          characters.randomMagicAttackValue
        );
        filterSkills(classChoice.value, characters.skillElementsToFilter);

        if (characterCreation.name.value === battle.attackerSelection.value) {
          battle.resetAttackType = true;
        }
        break;
      case "class":
        filterSkills(target.value, characters.skillElementsToFilter);

        if (characterCreation.name.value === battle.attackerSelection.value) {
          battle.resetAttackType = true;
        }
        break;
      case "weapon":
        handleWeaponDisplay(
          characters.weaponDisplay,
          target,
          characterCreation.weapon.value
        );
        filterUpgrade(
          characterCreation.race.value,
          characterCreation.weaponUpgrade,
          target.value,
          characters.randomAttackValue,
          characters.randomMagicAttackValue
        );
        break;
      case "state":
        filterState(target.value, characterCreation.polymorphMonster);
        if (characterCreation.name.value === battle.attackerSelection.value) {
          battle.resetAttackType = true;
        }
        break;
      case "lowRank":
        filterCheckbox(target, characterCreation.playerRank.parentElement);
        break;
      case "isBlessed":
        filterCheckbox(target, characters.blessingCreation);
        break;
      case "onYohara":
        filterCheckbox(target, characters.yoharaCreation);
        break;
      case "isMarried":
        filterCheckbox(target, characters.marriageCreation);
        break;
    }

    if (
      targetName.startsWith("attackSkill") ||
      targetName.startsWith("horseSkill")
    ) {
      battle.resetAttackType = true;
    }
  });
}

function addUniquePseudo(characterDataObject, savedCharactersPseudo) {
  var characterPseudo = String(characterDataObject.name);
  var originalPseudo = characterPseudo;
  var count = 0;

  var regex = /(.*)(\d)$/;
  var match = characterPseudo.match(regex);

  if (match) {
    originalPseudo = match[1];
    count = match[2];
  }

  while (isValueInArray(characterPseudo, savedCharactersPseudo)) {
    characterPseudo = originalPseudo + count;
    count++;
  }

  characterDataObject.name = characterPseudo;
  return [characterDataObject, characterPseudo];
}

function convertToNumber(value) {
  var valueNumber = Number(value);
  return isNaN(valueNumber) ? value : valueNumber;
}

function getLocalStorageValue(key, defaultValue) {
  var storedValue = localStorage.getItem(key);

  if (storedValue) {
    return JSON.parse(storedValue);
  }

  return defaultValue;
}

function getSavedCharacters() {
  return getLocalStorageValue("savedCharactersCalculator", {});
}

function getSavedMonsters() {
  return getLocalStorageValue("savedMonstersCalculator", []).filter(function (
    num
  ) {
    return !isNaN(Number(num));
  });
}

function getSavedFights() {
  return getLocalStorageValue("savedFightsCalculator", []);
}

function saveToLocalStorage(key, value) {
  localStorage.setItem(key, JSON.stringify(value));
}

function updateSavedCharacters(savedCharacters) {
  saveToLocalStorage("savedCharactersCalculator", savedCharacters);
}

function updateSavedMonsters(savedMonsters) {
  saveToLocalStorage("savedMonstersCalculator", savedMonsters);
}

function updateSavedFights(savedFights) {
  saveToLocalStorage("savedFightsCalculator", savedFights);
}

function saveCharacter(
  savedCharacters,
  characterCreation,
  battle,
  newCharacter,
  characterDataObject
) {
  if (!characterDataObject) {
    var characterData = new FormData(characterCreation);
    var characterDataObject = {};

    characterData.forEach(function (value, key) {
      characterDataObject[key] = convertToNumber(value);
    });
  }

  savedCharacters[characterDataObject.name] = characterDataObject;
  updateSavedCharacters(savedCharacters);

  if (newCharacter) {
    addBattleChoice(battle, characterDataObject.name);
  }

  if (battle.resetAttackType) {
    filterAttackTypeSelection(characterDataObject, battle.attackTypeSelection);
    battle.resetAttackType = false;
  }
}

function saveButtonGreen(saveButton) {
  saveButton.classList.remove("unsaved-character");
}

function saveButtonOrange(saveButton) {
  saveButton.classList.add("unsaved-character");
}

function characterCreationListener(characters, battle) {
  var { characterCreation, saveButton, weaponCategory } = characters;

  characterCreation.addEventListener("submit", function (event) {
    event.preventDefault();

    if (characters.unsavedChanges) {
      saveCharacter(characters.savedCharacters, characterCreation, battle);
      saveButtonGreen(saveButton);
      characters.unsavedChanges = false;
    }
  });

  document.addEventListener("keydown", function (event) {
    if (event.ctrlKey && event.key === "s") {
      event.preventDefault();
      saveButton.click();
    }
  });

  weaponCategory.addEventListener("mouseover", function (event) {
    label = event.target.closest("label");

    if (label) {
      var tooltip = label.lastChild;

      if (tooltip.classList.contains("popContenu")) {
        var tooltipRect = tooltip.getBoundingClientRect();
        var modalRect = weaponCategory.getBoundingClientRect();

        if (tooltipRect.right > modalRect.right) {
          tooltip.style.left = "-100%";
        } else if (tooltipRect.left < modalRect.left) {
          tooltip.style.left = "200%";
        }
      }
    }
  });
}

function downloadData(content, type, filename) {
  var link = document.createElement("a");
  var blob = new Blob([content], { type: type });
  var blobURL = URL.createObjectURL(blob);

  link.href = blobURL;
  link.download = filename;
  document.body.appendChild(link);

  link.click();
  document.body.removeChild(link);
  URL.revokeObjectURL(blobURL);
}

function uploadCharacter(
  selectedFiles,
  characters,
  characterTemplate,
  charactersContainer,
  battle
) {
  var selectFilesLength = selectedFiles.length;

  for (var fileIndex = 0; fileIndex < selectFilesLength; fileIndex++) {
    var selectedFile = selectedFiles[fileIndex];

    if (selectedFile.type === "text/plain") {
      var reader = new FileReader();
      reader.onload = function (e) {
        var fileContent = e.target.result;
        try {
          var characterDataObject = JSON.parse(fileContent);

          if (characterDataObject.hasOwnProperty("name")) {
            var characterPseudo = String(characterDataObject.name);

            hideElement(characters.characterCreation);
            characterPseudo = validPseudo(characterPseudo);

            [characterDataObject, characterPseudo] = addUniquePseudo(
              characterDataObject,
              Object.keys(characters.savedCharacters)
            );
            var selectedCharacter = handleNewCharacter(
              characters,
              characterTemplate,
              charactersContainer,
              battle,
              characterPseudo
            )[0];

            if (selectFilesLength === 1) {
              updateForm(
                characterDataObject,
                characters.characterCreation,
                characters,
                selectedCharacter,
                battle
              );
            }

            saveCharacter(
              characters.savedCharacters,
              characters.characterCreation,
              battle,
              true,
              characterDataObject
            );
          }
        } catch (error) {
          console.log(error);
          if (error.name === "TypeError") {
            // delete the character
          }
        }
      };
      reader.readAsText(selectedFile);
    }
  }
}

function handleUploadCharacter(
  characters,
  characterTemplate,
  charactersContainer,
  battle
) {
  var characterInput = characters.characterInput;
  var dropZone = characters.dropZone;

  characterInput.accept = ".txt";
  characterInput.multiple = true;
  dropZone.setAttribute("tabindex", "0");

  dropZone.addEventListener("click", function () {
    characterInput.click();
  });

  dropZone.addEventListener("dragover", function (event) {
    event.preventDefault();
    dropZone.classList.add("drop-zone--dragover");
  });

  ["dragleave", "dragend"].forEach(function (type) {
    dropZone.addEventListener(type, function () {
      dropZone.classList.remove("drop-zone--dragover");
    });
  });

  dropZone.addEventListener("drop", function (event) {
    event.preventDefault();
    uploadCharacter(
      event.dataTransfer.files,
      characters,
      characterTemplate,
      charactersContainer,
      battle
    );
    dropZone.classList.remove("drop-zone--dragover");
  });

  characterInput.addEventListener("change", function (event) {
    uploadCharacter(
      event.target.files,
      characters,
      characterTemplate,
      charactersContainer,
      battle
    );
  });
}

function deleteCharacter(characters, pseudo, element, battle) {
  battle.battleForm.reset();
  delete characters.savedCharacters[pseudo];
  element.remove();

  updateSavedCharacters(characters.savedCharacters);
  removeBattleChoice(battle, pseudo);

  if (
    !Object.keys(characters.savedCharacters).length ||
    characters.characterCreation.name.value === pseudo
  ) {
    saveButtonGreen(characters.saveButton);
    characters.unsavedChanges = false;
    hideElement(characters.characterCreation);
    showElement(characters.characterCreation.previousElementSibling);
  }
}

function deleteMonster(characters, monsterVnum, element, battle) {
  battle.battleForm.reset();
  characters.savedMonsters.splice(
    characters.savedMonsters.indexOf(monsterVnum),
    1
  );

  if (element) {
    element.remove();
  }

  updateSavedMonsters(characters.savedMonsters);
  removeBattleChoice(battle, monsterVnum);
}

function handleStyle(characters, selectedElement) {
  var currentCharacter = characters.currentCharacter;

  if (currentCharacter) {
    currentCharacter.classList.remove("selected-character");
  }

  selectedElement.classList.add("selected-character");
  characters.currentCharacter = selectedElement;
}

function updateForm(
  formData,
  characterCreation,
  characters,
  selectedElement,
  battle
) {
  saveButtonGreen(characters.saveButton);
  hideElement(characterCreation.previousElementSibling);
  showElement(characterCreation);
  handleStyle(characters, selectedElement);

  characterCreation.reset();

  for (var [name, value] of Object.entries(formData)) {
    var formElement = characterCreation[name];

    if (!formElement) {
      continue;
    }

    if (formElement.type === "checkbox") {
      if (isChecked(value)) {
        formElement.checked = true;
      }
    } else {
      formElement.value = value;
    }
  }
  var selectedRace = characterCreation.race.value;
  var classChoice = characterCreation.class;
  var weaponElement = characterCreation.weapon;

  filterClass(selectedRace, classChoice, true);
  filterWeapon(
    selectedRace,
    weaponElement,
    characters.weaponCategory,
    battle.constants.allowedWeaponsPerRace,
    true
  );

  var newWeapon = getSelectedWeapon(characters.weaponCategory);

  handleWeaponDisplay(characters.weaponDisplay, newWeapon, weaponElement.value);
  filterUpgrade(
    selectedRace,
    characterCreation.weaponUpgrade,
    weaponElement.value,
    characters.randomAttackValue,
    characters.randomMagicAttackValue,
    formData.weaponUpgrade
  );
  filterState(
    characterCreation.state.value,
    characterCreation.polymorphMonster
  );
  filterCheckbox(
    characterCreation.lowRank,
    characterCreation.playerRank.parentElement
  );
  filterCheckbox(characterCreation.onYohara, characters.yoharaCreation);
  filterCheckbox(characterCreation.isBlessed, characters.blessingCreation);
  filterCheckbox(characterCreation.isMarried, characters.marriageCreation);
  filterSkills(classChoice.value, characters.skillElementsToFilter);
  handleBonusVariationUpdate(characterCreation, characters.bonusVariation);
}

function handleClickOnCharacter(
  spanInput,
  target,
  characters,
  characterElement,
  battle,
  edition
) {
  var displayedPseudo = characters.characterCreation.name.value;
  var pseudo = spanInput.dataset.name;

  if (edition) {
    if (!characters.unsavedChanges) {
      updateForm(
        characters.savedCharacters[pseudo],
        characters.characterCreation,
        characters,
        characterElement,
        battle
      );
    } else if (displayedPseudo === pseudo) {
      // pass
    } else {
      var result = confirm(
        "Voulez-vous continuer ? Les dernières modifications ne seront pas sauvegardées."
      );

      if (result) {
        updateForm(
          characters.savedCharacters[pseudo],
          characters.characterCreation,
          characters,
          characterElement,
          battle
        );
        characters.unsavedChanges = false;
      }
    }
  } else {
    if (target.tagName === "path") {
      target = target.parentElement;
    }

    switch (target.dataset.icon) {
      case "duplicate":
        if (!characters.unsavedChanges) {
          addNewCharacter(
            characters,
            characters.newCharacterTemplate,
            characters.charactersContainer,
            battle,
            pseudo
          );
        } else {
          var result = confirm(
            "Voulez-vous continuer ? Les dernières modifications ne seront pas sauvegardées."
          );

          if (result) {
            addNewCharacter(
              characters,
              characters.newCharacterTemplate,
              characters.charactersContainer,
              battle,
              pseudo
            );
            saveButtonGreen(characters.saveButton);
            characters.unsavedChanges = false;
          }
        }
        break;

      case "download":
        var character = characters.savedCharacters[pseudo];
        downloadData(
          JSON.stringify(character),
          "text/plain",
          character.name + ".txt"
        );
        break;

      case "delete":
        var result = confirm(
          "Voulez-vous vraiment supprimer définitivement le personnage " +
            pseudo +
            " ?"
        );
        if (result) {
          deleteCharacter(characters, pseudo, characterElement, battle);
        }
        break;
    }
  }
}

function handleNewCharacter(
  characters,
  characterTemplate,
  charactersContainer,
  battle,
  pseudo
) {
  var newCharacterTemplate = characterTemplate.cloneNode(true);
  var spanInput = newCharacterTemplate.querySelector("span.input");

  newCharacterTemplate.setAttribute("tabindex", "0");
  charactersContainer.appendChild(newCharacterTemplate);

  if (pseudo) {
    spanInput.textContent = pseudo;
    spanInput.setAttribute("data-name", pseudo);
  }

  newCharacterTemplate.addEventListener("click", function (event) {
    var target = event.target;

    if (target.tagName === "path" || target.tagName === "svg") {
      handleClickOnCharacter(
        spanInput,
        target,
        characters,
        newCharacterTemplate,
        battle
      );
    } else {
      handleClickOnCharacter(
        spanInput,
        null,
        characters,
        newCharacterTemplate,
        battle,
        true
      );
    }
  });

  newCharacterTemplate.addEventListener("keydown", function (event) {
    if (event.keyCode === 13) {
      event.target.click();
    }
  });

  return [newCharacterTemplate, spanInput];
}

function validPseudo(pseudo) {
  var newPseudo = pseudoFormat(pseudo);

  if (!newPseudo) {
    return "Pseudo";
  }

  return newPseudo;
}

function addNewCharacter(
  characters,
  characterTemplate,
  charactersContainer,
  battle,
  pseudoToDuplicate
) {
  function editAndSetCharacterPseudoInput(selectedCharacter, spanInput) {
    var maxPseudoLength = 20;

    var selection = window.getSelection();
    var range = document.createRange();

    if (pseudoToDuplicate) {
      spanInput.textContent = pseudoToDuplicate;
    }

    spanInput.contentEditable = true;
    spanInput.focus();
    range.selectNodeContents(spanInput);
    selection.removeAllRanges();
    selection.addRange(range);

    function pseudoValidation() {
      var characterPseudo = validPseudo(spanInput.textContent);
      var characterDataObject = { name: characterPseudo };

      if (pseudoToDuplicate) {
        characterDataObject = copyObject(
          characters.savedCharacters[pseudoToDuplicate]
        );
        characterDataObject.name = characterPseudo;
      }

      [characterDataObject, characterPseudo] = addUniquePseudo(
        characterDataObject,
        Object.keys(characters.savedCharacters)
      );

      selection.removeAllRanges();
      spanInput.contentEditable = false;
      spanInput.textContent = characterPseudo;
      spanInput.setAttribute("data-name", characterPseudo);

      updateForm(
        characterDataObject,
        characters.characterCreation,
        characters,
        selectedCharacter,
        battle
      );
      saveCharacter(
        characters.savedCharacters,
        characters.characterCreation,
        battle,
        true
      );
    }

    function handleMaxLength(event) {
      if (spanInput.textContent.length > maxPseudoLength) {
        spanInput.textContent = spanInput.textContent.slice(0, maxPseudoLength);
        range.setStart(spanInput.childNodes[0], maxPseudoLength);
        selection.removeAllRanges();
        selection.addRange(range);
      }
    }

    function handleBlur() {
      spanInput.removeEventListener("blur", handleBlur);
      spanInput.removeEventListener("input", handleMaxLength);
      pseudoValidation();
    }

    function handleKeyDown(event) {
      if (event.key === "Enter") {
        event.preventDefault();

        spanInput.removeEventListener("keydown", handleKeyDown);
        spanInput.removeEventListener("blur", handleBlur);
        spanInput.removeEventListener("input", handleMaxLength);

        pseudoValidation();
      }
    }

    spanInput.addEventListener("input", handleMaxLength);
    spanInput.addEventListener("keydown", handleKeyDown);
    spanInput.addEventListener("blur", handleBlur);
  }

  hideElement(characters.characterCreation);
  var [selectedCharacter, spanInput] = handleNewCharacter(
    characters,
    characterTemplate,
    charactersContainer,
    battle
  );

  editAndSetCharacterPseudoInput(selectedCharacter, spanInput);
}

function handleFocus() {
  var tooltipLinks = document.querySelectorAll("div.tooltip a");
  tooltipLinks.forEach(function (link) {
    link.setAttribute("tabindex", -1);
  });
}

function handleBonusVariationUpdate(characterCreation, bonusVariation) {
  var selectedBonus = characterCreation.bonusVariation.value;

  if (characterCreation.hasOwnProperty(selectedBonus)) {
    handleBonusVariation(characterCreation[selectedBonus], bonusVariation);
  } else {
    var { minValue, maxValue } = bonusVariation;

    minValue.removeAttribute("min");
    minValue.removeAttribute("max");

    maxValue.removeAttribute("min");
    maxValue.removeAttribute("max");

    hideElement(bonusVariation.container);
  }
}

function getTargetContent(targetParent, targetName, isSkill) {
  var targetContent = "";

  if (targetParent.children.length <= 1) {
    targetContent = targetParent.textContent;
  } else if (targetName === "weaponUpgrade") {
    targetContent = targetParent.children[1].textContent;
  } else if (isSkill) {
    var container = targetParent.children[1];

    for (var index = 1; index < container.children.length; index++) {
      var element = container.children[index];

      if (element.checkVisibility()) {
        targetContent += element.textContent;
      }
    }
  } else {
    for (var index = 1; index < targetParent.children.length; index++) {
      var element = targetParent.children[index];

      if (element.checkVisibility()) {
        targetContent += element.textContent;
      }
    }
  }

  return targetContent;
}

function handleBonusVariation(target, bonusVariation, isSelectedByUser) {
  var { activation, inputDisplay, minValue, maxValue, container } =
    bonusVariation;

  var {
    min: targetMin,
    max: targetMax,
    name: targetName,
    value: targetValue,
    parentElement: targetParent,
    tagName,
  } = target;

  targetMin = Number(targetMin);
  targetMax = Number(targetMax);
  targetValue = Number(targetValue);

  var isSkill = tagName === "SELECT";

  if (container.contains(target) || targetName == 0) {
    if (!isSelectedByUser) {
      hideElement(container);
    }
    return;
  }

  if (isSkill) {
    var options = target.options;

    targetMin = options[0].value;
    targetMax = options[options.length - 1].value;
  }

  minValue.min = targetMin;
  minValue.max = targetMax;

  maxValue.min = targetMin;
  maxValue.max = targetMax;

  if (isSelectedByUser) {
    var { input, tab } = bonusVariation;

    inputDisplay.value = getTargetContent(targetParent, targetName, isSkill);

    input.value = targetName;
    minValue.value = Math.max(targetValue - 10, targetMin);
    maxValue.value = Math.min(targetValue + 10, targetMax);

    activation.checked = true;

    tab.click();
    tab.scrollIntoView(true);

    input.dispatchEvent(new Event("change", { bubbles: true }));
  } else {
    if (minValue.value < targetMin) {
      minValue.value = targetMin;
      minValue.dispatchEvent(new Event("change", { bubbles: true }));
    }

    if (maxValue.value > targetMax) {
      maxValue.value = targetMax;
      maxValue.dispatchEvent(new Event("change", { bubbles: true }));
    }
  }

  inputDisplay.style.width = inputDisplay.value.length * 0.55 + "em";
  showElement(container);
}

function characterManagement(characters, battle) {
  var {
    newCharacterTemplate: characterTemplate,
    charactersContainer,
    addNewCharacterButton,
    saveButton,
    characterCreation,
    bonusVariation,
  } = characters;

  Object.keys(characters.savedCharacters).forEach(function (pseudo) {
    handleNewCharacter(
      characters,
      characterTemplate,
      charactersContainer,
      battle,
      pseudo
    );
  });

  addNewCharacterButton.addEventListener("click", function (event) {
    if (!characters.unsavedChanges) {
      addNewCharacter(
        characters,
        characterTemplate,
        charactersContainer,
        battle
      );
    } else {
      var result = confirm(
        "Voulez-vous continuer ? Les dernières modifications ne seront pas sauvegardées."
      );

      if (result) {
        addNewCharacter(
          characters,
          characterTemplate,
          charactersContainer,
          battle
        );
        saveButtonGreen(saveButton);
        characters.unsavedChanges = false;
      }
    }
  });

  handleUploadCharacter(
    characters,
    characterTemplate,
    charactersContainer,
    battle
  );

  characterCreation.addEventListener("change", function () {
    saveButtonOrange(saveButton);
    characters.unsavedChanges = true;
  });

  function handleLongPress(target) {
    if (target.tagName !== "INPUT" && target.tagName !== "SELECT") {
      target = target.querySelector("input");
    }

    if (
      !target ||
      (target.type !== "number" &&
        !target.classList.contains("skill-select") &&
        target.name !== "weaponUpgrade")
    ) {
      return;
    }

    handleBonusVariation(target, bonusVariation, true);
  }

  characterCreation.addEventListener("click", function (event) {
    if (event.shiftKey || event.ctrlKey) {
      handleLongPress(event.target);
    }
  });

  var longPressTimer;

  characterCreation.addEventListener("touchstart", function (event) {
    longPressTimer = setTimeout(function () {
      handleLongPress(event.target);
    }, 800);
  });

  characterCreation.addEventListener("touchend", function () {
    clearTimeout(longPressTimer);
  });

  characterCreation.addEventListener("touchmove", function () {
    clearTimeout(longPressTimer);
  });

  filterForm(characters, battle);
  characterCreationListener(characters, battle);
  handleFocus();

  window.addEventListener("beforeunload", function (event) {
    if (characters.unsavedChanges) {
      event.preventDefault();
      return "";
    }
  });
}

function handleNewMonster(
  characters,
  monsterTemplate,
  monstersContainer,
  battle,
  monsterVnum,
  monsterList
) {
  var newMonsterTemplate = monsterTemplate.cloneNode(true);
  var spanInput = newMonsterTemplate.querySelector("span.input");
  var deleteSvg = newMonsterTemplate.querySelector("svg");
  var monsterName = getMonsterName(monsterVnum);

  var link = document.createElement("a");
  link.href = mw.util.getUrl(monsterName);
  link.title = monsterName;
  link.textContent = monsterName;

  spanInput.appendChild(link);
  monstersContainer.appendChild(newMonsterTemplate);

  newMonsterTemplate.setAttribute("tabindex", "0");
  newMonsterTemplate.setAttribute("data-name", monsterVnum);
  monstersContainer.appendChild(newMonsterTemplate);

  deleteSvg.addEventListener("click", function (event) {
    deleteMonster(characters, monsterVnum, newMonsterTemplate, battle);
    var inputMonster = monsterList.querySelector(
      "input[name='" + monsterVnum + "']"
    );
    inputMonster.checked = false;
  });
}

function monsterManagement(characters, battle) {
  function handleDropdown(searchMonster, monsterList) {
    searchMonster.addEventListener("focus", function (event) {
      showElement(monsterList);
    });

    document.addEventListener("mousedown", function (event) {
      var target = event.target;
      if (!monsterList.contains(target) && !searchMonster.contains(target)) {
        hideElement(monsterList);
      }
    });
  }

  function addMonsterNames(monsterList) {
    var lastMonsterAttributeIndex = monsterData[101].length - 1;

    for (var monsterVnum in monsterData) {
      var li = document.createElement("li");
      var label = document.createElement("label");
      var input = document.createElement("input");
      var textNode = document.createTextNode(
        monsterData[monsterVnum][lastMonsterAttributeIndex]
      );

      label.htmlFor = "monster" + monsterVnum;
      input.id = "monster" + monsterVnum;
      input.type = "checkbox";

      input.name = monsterVnum;

      label.appendChild(input);
      label.appendChild(textNode);
      li.appendChild(label);
      monsterList.appendChild(li);
    }
  }

  function filterNames(searchMonster, monsterList) {
    var debounceTimer;

    searchMonster.addEventListener("input", function (event) {
      clearTimeout(debounceTimer);
      debounceTimer = setTimeout(function () {
        var value = toNormalForm(event.target.value);
        for (var element of monsterList.children) {
          if (!isValueInArray(value, toNormalForm(element.textContent))) {
            hideElement(element);
          } else {
            showElement(element);
          }
        }
      }, 500);
    });
  }

  var monsterTemplate = characters.newMonsterTemplate;
  var monstersContainer = characters.monstersContainer;
  var monsterList = characters.monsterList;
  var searchMonster = characters.searchMonster;
  var monsterListForm = characters.monsterListForm;

  document
    .getElementById("monster-link")
    .querySelector("a")
    .setAttribute("target", "_blank");

  handleDropdown(searchMonster, monsterList);
  addMonsterNames(monsterList, characters.monsterListTemplate);
  filterNames(searchMonster, monsterList);

  characters.savedMonsters.slice().forEach(function (monsterVnum) {
    var inputMonster = monsterList.querySelector(
      "input[name='" + monsterVnum + "']"
    );

    if (inputMonster) {
      handleNewMonster(
        characters,
        monsterTemplate,
        monstersContainer,
        battle,
        monsterVnum,
        monsterList
      );
      inputMonster.checked = true;
    } else {
      deleteMonster(characters, monsterVnum, null, battle);
    }
  });

  monsterListForm.addEventListener("submit", function (event) {
    event.preventDefault();
  });

  monsterListForm.addEventListener("change", function (event) {
    var target = event.target;
    var monsterVnum = target.name;

    if (monsterVnum === "search-monster") {
      return;
    }

    if (target.checked) {
      handleNewMonster(
        characters,
        monsterTemplate,
        monstersContainer,
        battle,
        monsterVnum,
        monsterList
      );

      characters.savedMonsters.push(monsterVnum);
      updateSavedMonsters(characters.savedMonsters);
      addBattleChoice(battle, monsterVnum, true);
    } else {
      var currentMonsterTemplate = monstersContainer.querySelector(
        "[data-name='" + monsterVnum + "']"
      );
      deleteMonster(characters, monsterVnum, currentMonsterTemplate, battle);
    }
  });

  addEventListener("storage", function (event) {
    if (event.key === "newMonsterCalculator") {
      var monsterVnum = Number(event.newValue);

      if (!monsterVnum) {
        return;
      }

      var inputMonster = monsterList.querySelector(
        "input[name='" + Math.abs(monsterVnum) + "']"
      );

      if (inputMonster) {
        if (
          (monsterVnum > 0 && !inputMonster.checked) ||
          (monsterVnum < 0 && inputMonster.checked)
        ) {
          inputMonster.click();
        }
      }
    }
  });
}

function removeBattleChoice(battle, name) {
  var battleSelects = [battle.attackerSelection, battle.victimSelection];

  battleSelects.forEach(function (battleSelect) {
    for (
      var optionIndex = 0;
      optionIndex < battleSelect.options.length;
      optionIndex++
    ) {
      if (battleSelect.options[optionIndex].value === name) {
        battleSelect.remove(optionIndex);
        break;
      }
    }
  });
}

function addBattleChoice(battle, name, isMonster = false) {
  function createOption(text, vnum) {
    var option = document.createElement("option");
    option.textContent = text;
    option.value = vnum;

    if (!isMonster) {
      option.classList.add("notranslate");
    }

    return option;
  }

  var vnum = name;

  if (isMonster) {
    name = getMonsterName(name);
  }

  if (isMonster && monsterData[vnum][1]) {
    // pass
  } else {
    battle.attackerSelection.appendChild(createOption(name, vnum));
  }

  battle.victimSelection.appendChild(createOption(name, vnum));
}

function updateBattleChoice(characters, battle) {
  var keys = Object.keys(characters.savedCharacters);

  for (var index = 0; index < keys.length; index++) {
    var pseudo = keys[index];
    addBattleChoice(battle, pseudo);
  }

  characters.savedMonsters.forEach(function (monsterVnum) {
    addBattleChoice(battle, monsterVnum, true);
  });
}

function isPC(character) {
  if (character.race === 0 || character.race === 1) {
    return false;
  }
  return true;
}

function isBoss(character) {
  return character.race === 0 && character.rank >= 5;
}

function isStone(character) {
  return character.race === 1;
}

function isMeleeAttacker(monster) {
  return monster.attack == 0;
}

function isRangeAttacker(monster) {
  return monster.attack == 1;
}

function isMagicAttacker(monster) {
  return monster.attack == 2;
}

function isMagicClass(character) {
  return character.race === "shaman" || character.class === "black_magic";
}

function isDispell(character, skillId) {
  return character.class === "weaponary" && skillId === 6;
}

function isPolymorph(character) {
  return character.state === "polymorph";
}

function isRiding(character) {
  return character.state === "horse";
}

function isBow(weapon) {
  return weapon.type === 2;
}

function calcAttackFactor(attacker, victim) {
  function calcCoeffK(dex, level) {
    return Math.min(90, Math.floor((2 * dex + level) / 3));
  }

  var K1 = calcCoeffK(attacker.polymorphDex, attacker.level);
  var K2 = calcCoeffK(victim.polymorphDex, attacker.level);

  var AR = (K1 + 210) / 300;
  var ER = (((2 * K2 + 5) / (K2 + 95)) * 3) / 10;

  return truncateNumber(AR - ER, 8);
}

function calcMainAttackValue(attacker) {
  var leadership = 0;
  var rawWeaponAttackValue = 0;

  if (isPC(attacker)) {
    var weaponUpgrades = attacker.weapon.upgrades;
    var maxUpgrade = weaponUpgrades.length - 1;

    if (attacker.hasOwnProperty("weaponUpgrade")) {
      rawWeaponAttackValue =
        weaponUpgrades[Math.min(attacker.weaponUpgrade, maxUpgrade)];

      // rare bug when weaponUpgrade is deleted
    } else {
      console.log("Warming: weaponUpgrade is missing.");
      rawWeaponAttackValue = weaponUpgrades[maxUpgrade];
    }

    leadership = attacker.leadership;
  }

  return 2 * (attacker.level + rawWeaponAttackValue) + leadership;
}

function calcStatAttackValue(character) {
  switch (character.race) {
    case "warrior":
    case "sura":
      return 2 * character.str;
    case "ninja":
      return Math.floor((1 / 4) * (character.str + 7 * character.dex));
    case "shaman":
      return Math.floor((1 / 3) * (5 * character.int + character.dex));
    case "lycan":
      return character.vit + 2 * character.dex;
    default:
      return 2 * character.str;
  }
}

function calcSecondaryAttackValue(attacker) {
  var attackValueOther = 0;

  var minAttackValue = 0;
  var maxAttackValue = 0;

  var minAttackValueSlash = 0;
  var maxAttackValueSlash = 0;

  var attackerWeapon = attacker.weapon;

  if (isPC(attacker)) {
    if (attackerWeapon.isSerpent) {
      var rawAttackValue = attackerWeapon.upgrades[attacker.weaponUpgrade];

      minAttackValue = attacker.minAttackValueRandom - rawAttackValue;
      maxAttackValue = attacker.maxAttackValueRandom - rawAttackValue;

      minAttackValue = Math.max(0, minAttackValue);
      maxAttackValue = Math.max(minAttackValue, maxAttackValue);
    } else {
      minAttackValue = attackerWeapon.minAttackValue;
      maxAttackValue = attackerWeapon.maxAttackValue;
    }

    minAttackValueSlash = Math.min(
      attacker.minAttackValueSlash,
      attacker.maxAttackValueSlash
    );
    maxAttackValueSlash = Math.max(
      attacker.minAttackValueSlash,
      attacker.maxAttackValueSlash
    );

    attackValueOther += attacker.attackValue;

    if (isBow(attackerWeapon) && !isPolymorph(attacker)) {
      attackValueOther += 25;
    }
  } else {
    minAttackValue = attacker.minAttackValue;
    maxAttackValue = attacker.maxAttackValue;
  }

  minAttackValue += attacker.minAttackValuePolymorph;
  maxAttackValue += attacker.maxAttackValuePolymorph;

  attackValueOther += attacker.statAttackValue;
  attackValueOther += attacker.horseAttackValue;

  var weaponInterval = maxAttackValue - minAttackValue + 1;
  var slashInterval = maxAttackValueSlash - minAttackValueSlash + 1;

  var totalCardinal = weaponInterval * slashInterval * 1_000_000;
  var minInterval = Math.min(weaponInterval, slashInterval);

  minAttackValue += minAttackValueSlash;
  maxAttackValue += maxAttackValueSlash;

  return {
    minAttackValue: minAttackValue,
    maxAttackValue: maxAttackValue,
    attackValueOther: attackValueOther,
    totalCardinal: totalCardinal,
    weights: calcWeights(minAttackValue, maxAttackValue, minInterval),
    possibleDamagesCount: maxAttackValue - minAttackValue + 1,
  };
}

function calcMagicAttackValue(attacker) {
  var minMagicAttackValue = 0;
  var maxMagicAttackValue = 0;

  var minMagicAttackValueSlash = 0;
  var maxMagicAttackValueSlash = 0;

  var attackerWeapon = attacker.weapon;

  if (attackerWeapon.isSerpent) {
    minMagicAttackValue = attacker.minMagicAttackValueRandom;
    maxMagicAttackValue = attacker.maxMagicAttackValueRandom;

    maxMagicAttackValue = Math.max(minMagicAttackValue, maxMagicAttackValue);
  } else {
    var rawWeaponAttackValue = attackerWeapon.upgrades[attacker.weaponUpgrade];

    if (!rawWeaponAttackValue) {
      rawWeaponAttackValue = 0;
    }

    minMagicAttackValue =
      attackerWeapon.minMagicAttackValue + rawWeaponAttackValue;
    maxMagicAttackValue =
      attackerWeapon.maxMagicAttackValue + rawWeaponAttackValue;
  }

  minMagicAttackValueSlash = Math.min(
    attacker.minMagicAttackValueSlash,
    attacker.maxMagicAttackValueSlash
  );
  maxMagicAttackValueSlash = Math.max(
    attacker.minMagicAttackValueSlash,
    attacker.maxMagicAttackValueSlash
  );

  var weaponInterval = maxMagicAttackValue - minMagicAttackValue + 1;
  var slashInterval = maxMagicAttackValueSlash - minMagicAttackValueSlash + 1;

  var totalCardinal = weaponInterval * slashInterval * 1_000_000;
  var minInterval = Math.min(weaponInterval, slashInterval);

  minMagicAttackValue += minMagicAttackValueSlash;
  maxMagicAttackValue += maxMagicAttackValueSlash;

  return {
    minMagicAttackValue: minMagicAttackValue,
    maxMagicAttackValue: maxMagicAttackValue,
    magicAttackValueAugmentation: getMagicAttackValueAugmentation(
      minMagicAttackValue,
      maxMagicAttackValue,
      attacker.magicAttackValue
    ),
    totalCardinal: totalCardinal,
    weights: calcWeights(minMagicAttackValue, maxMagicAttackValue, minInterval),
    possibleDamagesCount: maxMagicAttackValue - minMagicAttackValue + 1,
  };
}

function getPolymorphPower(polymorphPoint, polymorphPowerTable) {
  return polymorphPowerTable[polymorphPoint];
}

function getSkillPower(skillPoint, skillPowerTable) {
  return skillPowerTable[skillPoint];
}

function getMarriageBonusValue(character, marriageTable, itemName) {
  var index;
  var lovePoint = character.lovePoint;

  if (lovePoint < 65) {
    index = 0;
  } else if (lovePoint < 80) {
    index = 1;
  } else if (lovePoint < 100) {
    index = 2;
  } else {
    index = 3;
  }

  return marriageTable[itemName][index];
}

function calcDamageWithPrimaryBonuses(damages, bonusValues) {
  damages = Math.floor((damages * bonusValues.attackValueCoeff) / 100);

  damages += bonusValues.attackValueMarriage;

  damages = Math.floor(
    (damages * bonusValues.monsterResistanceMarriageCoeff) / 100
  );
  damages = Math.floor((damages * bonusValues.monsterResistanceCoeff) / 100);

  damages += Math.floor((damages * bonusValues.typeBonusCoeff) / 100);
  damages +=
    Math.floor((damages * bonusValues.raceBonusCoeff) / 100) -
    Math.floor((damages * bonusValues.raceResistanceCoeff) / 100);
  damages += Math.floor((damages * bonusValues.stoneBonusCoeff) / 100);
  damages += Math.floor((damages * bonusValues.monsterBonusCoeff) / 100);

  var elementBonusCoeff = bonusValues.elementBonusCoeff;

  damages +=
    Math.trunc((damages * elementBonusCoeff[0]) / 10000) +
    Math.trunc((damages * elementBonusCoeff[1]) / 10000) +
    Math.trunc((damages * elementBonusCoeff[2]) / 10000) +
    Math.trunc((damages * elementBonusCoeff[3]) / 10000) +
    Math.trunc((damages * elementBonusCoeff[4]) / 10000) +
    Math.trunc((damages * elementBonusCoeff[5]) / 10000);

  damages = Math.floor(damages * bonusValues.damageMultiplier);

  return damages;
}

function calcDamageWithSecondaryBonuses(
  damages,
  bonusValues,
  damagesType,
  minPiercingDamages,
  damagesWithPrimaryBonuses
) {
  damages = Math.floor(damages * bonusValues.magicResistanceCoeff);
  damages = Math.trunc((damages * bonusValues.weaponDefenseCoeff) / 100);
  damages = Math.floor((damages * bonusValues.tigerStrengthCoeff) / 100);
  damages = Math.floor((damages * bonusValues.blessingBonusCoeff) / 100);

  if (damagesType.criticalHit) {
    damages *= 2;
  }

  if (damagesType.piercingHit) {
    damages += bonusValues.defenseBoost + Math.min(0, minPiercingDamages);
    damages += Math.floor(
      (damagesWithPrimaryBonuses * bonusValues.extraPiercingHitCoeff) / 1000
    );
  }

  damages = Math.floor((damages * bonusValues.averageDamageCoeff) / 100);
  damages = Math.floor(
    (damages * bonusValues.averageDamageResistanceCoeff) / 100
  );
  damages = Math.floor(
    (damages * bonusValues.skillDamageResistanceCoeff) / 100
  );

  damages = Math.floor((damages * bonusValues.rankBonusCoeff) / 100);
  damages = Math.max(0, damages + bonusValues.defensePercent);
  damages += Math.min(
    300,
    Math.floor((damages * bonusValues.damageBonusCoeff) / 100)
  );
  damages = Math.floor((damages * bonusValues.empireMalusCoeff) / 10);
  damages = Math.floor((damages * bonusValues.sungMaStrBonusCoeff) / 10000);
  damages -= Math.floor(damages * bonusValues.sungmaStrMalusCoeff);

  damages = Math.floor((damages * bonusValues.whiteDragonElixirCoeff) / 100);
  damages = Math.floor((damages * bonusValues.steelDragonElixirCoeff) / 100);

  return damages;
}

function calcSkillDamageWithSecondaryBonuses(
  damages,
  bonusValues,
  damagesType,
  minPiercingDamages
) {
  damages = Math.floor(damages * bonusValues.magicResistanceCoeff);
  damages = Math.trunc((damages * bonusValues.weaponDefenseCoeff) / 100);

  damages -= bonusValues.defense;

  damages = floorMultiplication(damages, bonusValues.skillWardCoeff);
  damages = floorMultiplication(damages, bonusValues.skillBonusCoeff);

  var tempDamages = Math.floor(
    (damages * bonusValues.skillBonusByBonusCoeff) / 100
  );
  
  damages = Math.floor(
    (tempDamages * bonusValues.magicAttackValueCoeff) / 100 + 0.5
  );
  damages = Math.floor((damages * bonusValues.tigerStrengthCoeff) / 100);

  if (damagesType.criticalHit) {
    damages *= 2;
  }

  if (damagesType.piercingHit) {
    damages += bonusValues.defenseBoost + Math.min(0, minPiercingDamages);
    damages += Math.floor(
      (tempDamages * bonusValues.extraPiercingHitCoeff) / 1000
    );
  }

  damages = Math.floor((damages * bonusValues.skillDamageCoeff) / 100);
  damages = Math.floor(
    (damages * bonusValues.skillDamageResistanceCoeff) / 100
  );
  damages = Math.floor((damages * bonusValues.rankBonusCoeff) / 100);

  damages = Math.max(0, damages + bonusValues.defensePercent);
  damages += Math.min(
    300,
    Math.floor((damages * bonusValues.damageBonusCoeff) / 100)
  );
  damages = Math.floor((damages * bonusValues.empireMalusCoeff) / 10);
  damages = Math.floor((damages * bonusValues.sungMaStrBonusCoeff) / 10000);
  damages -= Math.floor(damages * bonusValues.sungmaStrMalusCoeff);

  damages = Math.floor((damages * bonusValues.whiteDragonElixirCoeff) / 100);
  damages = Math.floor((damages * bonusValues.steelDragonElixirCoeff) / 100);

  return damages;
}

function computePolymorphPoint(attacker, victim, polymorphPowerTable) {
  attacker.statAttackValue = 0;

  attacker.polymorphDex = attacker.dex;
  victim.polymorphDex = victim.dex;

  attacker.minAttackValuePolymorph = 0;
  attacker.maxAttackValuePolymorph = 0;

  if (isPC(attacker) && isPolymorph(attacker)) {
    var polymorphPowerPct =
      getPolymorphPower(attacker.polymorphPoint, polymorphPowerTable) / 100;
    var polymorphMonster = createMonster(attacker.polymorphMonster);

    var polymorphStr = floorMultiplication(
      polymorphPowerPct,
      polymorphMonster.str
    );

    attacker.polymorphDex += floorMultiplication(
      polymorphPowerPct,
      polymorphMonster.dex
    );

    attacker.minAttackValuePolymorph = floorMultiplication(
      polymorphPowerPct,
      polymorphMonster.minAttackValue
    );
    attacker.maxAttackValuePolymorph = floorMultiplication(
      polymorphPowerPct,
      polymorphMonster.maxAttackValue
    );

    if (!attacker.weapon) {
      attacker.maxAttackValuePolymorph += 1;
    }

    attacker.attackValue = 0;

    if (isMagicClass(attacker)) {
      attacker.statAttackValue = 2 * (polymorphStr + attacker.int);
    } else {
      attacker.statAttackValue = 2 * (polymorphStr + attacker.str);
    }
  } else {
    attacker.statAttackValue = calcStatAttackValue(attacker);
  }
}

function computeHorse(attacker) {
  attacker.horseAttackValue = 0;

  if (isPC(attacker) && isRiding(attacker)) {
    var horseConstant = 30;

    if (attacker.class === "weaponary") {
      horseConstant = 60;
    }

    attacker.horseAttackValue = floorMultiplication(
      2 * attacker.level + attacker.statAttackValue,
      attacker.horsePoint / horseConstant
    );
  }
}

function getRankBonus(attacker) {
  if (attacker.lowRank !== "on") {
    return 0;
  }

  switch (attacker.playerRank) {
    case "aggressive":
      return 1;
    case "fraudulent":
      return 2;
    case "malicious":
      return 3;
    case "cruel":
      return 5;
  }

  return 0;
}

function calcElementCoeffPvP(elementBonus, mapping, attacker, victim) {
  var minElementMalus = 0;
  var maxDifference = 0;
  var savedElementDifferences = [];
  var elementBonusIndex = 0;

  for (var index = 0; index < elementBonus.length; index++) {
    if (!attacker[mapping.elementBonus[index]]) {
      continue;
    }

    var elementDifference =
      attacker[mapping.elementBonus[index]] -
      victim[mapping.elementResistance[index]];

    if (elementDifference >= 0) {
      elementBonus[elementBonusIndex] = 10 * elementDifference;
      minElementMalus -= elementDifference;
      maxDifference = Math.max(maxDifference, elementDifference);
      elementBonusIndex++;
    } else {
      savedElementDifferences.push(elementDifference);
    }
  }

  if (!savedElementDifferences.length) {
    return;
  }

  minElementMalus += maxDifference;
  savedElementDifferences.sort(compareNumbers);

  for (var index = 0; index < savedElementDifferences.length; index++) {
    var elementDifference = savedElementDifferences[index];

    elementBonus[elementBonusIndex + index] =
      10 * Math.max(minElementMalus, elementDifference);

    minElementMalus = Math.min(
      0,
      Math.max(minElementMalus, minElementMalus - elementDifference)
    );
  }
}

function skillChanceReduction(value) {
  if (value <= 9) {
    return Math.floor(value / 2);
  }
  return 5 + Math.floor((value - 10) / 4);
}

function magicResistanceToCoeff(magicResistance) {
  if (magicResistance) {
    return 2000 / (6 * magicResistance + 1000) - 1;
  }
  return 1;
}

function createBattleValues(attacker, victim, battle, skillType) {
  var { mapping, constants } = battle;
  var calcAttackValues;

  var missPercentage = 0;
  var attackValueMeleeMagic = 0;
  var attackValueMarriage = 0;
  var monsterResistanceMarriage = 0;
  var monsterResistance = 0;
  var typeBonus = 0;
  var raceBonus = 0;
  var raceResistance = 0;
  var stoneBonus = 0;
  var monsterBonus = 0;
  var elementBonus = [0, 0, 0, 0, 0, 0]; // fire, ice, lightning, earth, darkness, wind, order doesn't matter
  var defenseMarriage = 0;
  var damageMultiplier = 1;
  var useDamages = 1;
  var defense = victim.defense;
  var defenseBoost = defense;
  var magicResistance = 0;
  var weaponDefense = 0;
  var tigerStrength = 0;
  var blessingBonus = 0;
  var magicAttackValueMeleeMagic = 0;
  var criticalHitPercentage = attacker.criticalHit;
  var piercingHitPercentage = attacker.piercingHit;
  var extraPiercingHitPercentage = Math.max(0, piercingHitPercentage - 100);
  var averageDamage = 0;
  var averageDamageResistance = 0;
  var skillDamage = 0;
  var skillDamageResistance = 0;
  var rankBonus = 0;
  var defensePercent = 0;
  var damageBonus = 0;
  var empireMalus = 0;
  var sungMaStrBonus = 0;
  var sungmaStrMalus = 0;
  var whiteDragonElixir = 0;
  var steelDragonElixir = 0;

  computePolymorphPoint(attacker, victim, constants.polymorphPowerTable);
  computeHorse(attacker);

  if (isPC(attacker)) {
    if (weaponData.hasOwnProperty(attacker.weapon)) {
      attacker.weapon = createWeapon(attacker.weapon);
    } else {
      attacker.weapon = createWeapon(0);
    }

    attackValueMeleeMagic =
      attacker.attackValuePercent + Math.min(100, attacker.attackMeleeMagic);

    var weaponType = attacker.weapon.type;

    if (skillType && attacker.class === "archery") {
      if (weaponType !== 2) {
        useDamages = 0;
        weaponType = 2;
      }
      defense = 0;
    }

    var weaponDefenseName = mapping.defenseWeapon[weaponType];
    var weaponDefenseBreakName = mapping.breakWeapon[weaponType];

    if (victim.hasOwnProperty(weaponDefenseName)) {
      weaponDefense = victim[weaponDefenseName];
    }

    if (isChecked(attacker.whiteDragonElixir)) {
      whiteDragonElixir = 10;
    }

    if (isPC(victim)) {
      if (!skillType) {
        if (weaponType === 2 && !isPolymorph(attacker)) {
          missPercentage = victim.arrowBlock;
        } else {
          missPercentage = victim.meleeBlock;
        }
        missPercentage +=
          victim.meleeArrowBlock -
          (missPercentage * victim.meleeArrowBlock) / 100;

        blessingBonus = calcBlessingBonus(constants.skillPowerTable, victim);
        averageDamageResistance = victim.averageDamageResistance;
      }

      typeBonus = Math.max(1, attacker.humanBonus - victim.humanResistance);
      raceBonus = attacker[mapping.raceBonus[victim.race]];
      raceResistance = victim[mapping.raceResistance[attacker.race]];

      calcElementCoeffPvP(elementBonus, mapping, attacker, victim);

      if (weaponType !== 2 && attacker.hasOwnProperty(weaponDefenseBreakName)) {
        weaponDefense -= attacker[weaponDefenseBreakName];
      }

      criticalHitPercentage = 0;
    } else {
      if (isChecked(attacker.isMarried)) {
        if (isChecked(attacker.loveNecklace)) {
          attackValueMarriage = getMarriageBonusValue(
            attacker,
            constants.marriageTable,
            "loveNecklace"
          );
        }

        if (isChecked(attacker.loveEarrings)) {
          criticalHitPercentage += getMarriageBonusValue(
            attacker,
            constants.marriageTable,
            "loveEarrings"
          );
        }

        if (isChecked(attacker.harmonyEarrings)) {
          piercingHitPercentage += getMarriageBonusValue(
            attacker,
            constants.marriageTable,
            "harmonyEarrings"
          );
        }
      }

      if (isChecked(attacker.tigerStrength)) {
        tigerStrength = 40;
      }

      for (var index = 0; index < elementBonus.length; index++) {
        var elementBonusName = mapping.elementBonus[index];
        var elementResistanceName = mapping.elementResistance[index];

        if (attacker[elementBonusName] && victim[elementBonusName]) {
          elementBonus[index] =
            50 * (attacker[elementBonusName] - victim[elementResistanceName]);
        } else {
          elementBonus[index] = 5 * attacker[elementBonusName];
        }
      }

      var victimType = victim.type;

      if (victimType !== -1) {
        typeBonus = attacker[mapping.typeFlag[victimType]];
      }

      monsterBonus = attacker.monsterBonus;

      if (isStone(victim)) {
        stoneBonus = attacker.stoneBonus;
      }

      if (isBoss(victim)) {
        if (skillType) {
          skillDamage += attacker.skillBossDamage;
        } else {
          averageDamage += attacker.bossDamage;
        }
      }

      if (isChecked(attacker.onYohara)) {
        var sungmaStrDifference = attacker.sungmaStr - attacker.sungmaStrMalus;

        if (sungmaStrDifference >= 0) {
          sungMaStrBonus = sungmaStrDifference;
        } else {
          sungmaStrMalus = 0.5;
        }
      }
    }

    if (skillType) {
      skillDamage += attacker.skillDamage;
    } else {
      averageDamage += attacker.averageDamage;
    }

    rankBonus = getRankBonus(attacker);
    damageBonus = attacker.damageBonus;

    if (isChecked(attacker.empireMalus)) {
      empireMalus = 1;
    }
  } else {
    if (isPC(victim)) {
      if (isChecked(victim.isMarried)) {
        if (isChecked(victim.harmonyBracelet)) {
          monsterResistanceMarriage = getMarriageBonusValue(
            victim,
            constants.marriageTable,
            "harmonyBracelet"
          );
        }

        if (isChecked(victim.harmonyNecklace) && !skillType) {
          defenseMarriage = getMarriageBonusValue(
            victim,
            constants.marriageTable,
            "harmonyNecklace"
          );
        }
      }

      monsterResistance = victim.monsterResistance;

      for (var index = 0; index < elementBonus.length; index++) {
        var elementBonusName = mapping.elementBonus[index];
        var elementResistanceName = mapping.elementResistance[index];

        if (attacker[elementBonusName]) {
          elementBonus[index] =
            80 * (attacker[elementBonusName] - victim[elementResistanceName]);
        }
      }

      if (!skillType) {
        if (isMeleeAttacker(attacker)) {
          missPercentage = victim.meleeBlock;
          averageDamageResistance = victim.averageDamageResistance;
          blessingBonus = calcBlessingBonus(constants.skillPowerTable, victim);
        } else if (isRangeAttacker(attacker)) {
          missPercentage = victim.arrowBlock;
          weaponDefense = victim.arrowDefense;
          averageDamageResistance = victim.averageDamageResistance;
          blessingBonus = calcBlessingBonus(constants.skillPowerTable, victim);
        } else if (isMagicAttacker(attacker)) {
          missPercentage = victim.arrowBlock;
          skillDamageResistance = victim.skillDamageResistance;
          magicResistance = victim.magicResistance;
        }

        missPercentage +=
          victim.meleeArrowBlock -
          (missPercentage * victim.meleeArrowBlock) / 100;
      }
    }

    typeBonus = 1;
    damageMultiplier = attacker.damageMultiplier;
  }

  if (skillType) {
    criticalHitPercentage = skillChanceReduction(criticalHitPercentage);
    piercingHitPercentage = skillChanceReduction(piercingHitPercentage);
  }

  if (isPC(victim)) {
    if (!skillType && isChecked(victim.biologist70)) {
      defenseBoost = Math.floor((defenseBoost * 110) / 100);
    }

    criticalHitPercentage = Math.max(
      0,
      criticalHitPercentage - victim.criticalHitResistance
    );
    piercingHitPercentage = Math.max(
      0,
      piercingHitPercentage - victim.piercingHitResistance
    );

    if (skillType) {
      skillDamageResistance = victim.skillDamageResistance;
    }

    if (isMagicClass(victim)) {
      defensePercent = (-2 * victim.magicDefense * victim.defensePercent) / 100;
    } else {
      defensePercent = (-2 * defenseBoost * victim.defensePercent) / 100;
    }

    if (isChecked(victim.steelDragonElixir)) {
      steelDragonElixir = 10;
    }
  }

  if (skillType === "magic") {
    attackValueMeleeMagic = 0;
    magicAttackValueMeleeMagic =
      attacker.attackMagic + Math.min(100, attacker.attackMeleeMagic);
    attackValueMarriage = 0;
    defense = 0;
    if (!isDispell(attacker, 6)) {
      magicResistance = victim.magicResistance;
    }
    weaponDefense = 0;
    calcAttackValues = calcMagicAttackValue;
  } else {
    calcAttackValues = calcSecondaryAttackValue;
  }

  missPercentage = Math.min(100, missPercentage);

  var bonusValues = {
    missPercentage: missPercentage,
    weaponBonusCoeff: 1,
    attackValueCoeff: 100 + attackValueMeleeMagic,
    attackValueMarriage: attackValueMarriage,
    monsterResistanceMarriageCoeff: 100 - monsterResistanceMarriage,
    monsterResistanceCoeff: 100 - monsterResistance,
    typeBonusCoeff: typeBonus,
    raceBonusCoeff: raceBonus,
    raceResistanceCoeff: raceResistance,
    stoneBonusCoeff: stoneBonus,
    monsterBonusCoeff: monsterBonus,
    elementBonusCoeff: elementBonus,
    damageMultiplier: damageMultiplier,
    useDamages: useDamages,
    defense: defense,
    defenseBoost: defenseBoost,
    defenseMarriage: defenseMarriage,
    tigerStrengthCoeff: 100 + tigerStrength,
    magicResistanceCoeff: magicResistanceToCoeff(magicResistance),
    weaponDefenseCoeff: 100 - weaponDefense,
    blessingBonusCoeff: 100 - blessingBonus,
    magicAttackValueCoeff: 100 + magicAttackValueMeleeMagic,
    extraPiercingHitCoeff: 5 * extraPiercingHitPercentage,
    averageDamageCoeff: 100 + averageDamage,
    averageDamageResistanceCoeff: 100 - Math.min(99, averageDamageResistance),
    skillDamageCoeff: 100 + skillDamage,
    skillDamageResistanceCoeff: 100 - Math.min(99, skillDamageResistance),
    rankBonusCoeff: 100 + rankBonus,
    defensePercent: Math.floor(defensePercent),
    damageBonusCoeff: damageBonus,
    empireMalusCoeff: 10 - empireMalus,
    sungMaStrBonusCoeff: 10000 + sungMaStrBonus,
    sungmaStrMalusCoeff: sungmaStrMalus,
    whiteDragonElixirCoeff: 100 + whiteDragonElixir,
    steelDragonElixirCoeff: 100 - steelDragonElixir,
  };

  criticalHitPercentage = Math.min(criticalHitPercentage, 100);
  piercingHitPercentage = Math.min(piercingHitPercentage, 100);

  var damagesTypeCombinaison = [
    {
      criticalHit: false,
      piercingHit: false,
      weight:
        (100 - criticalHitPercentage) *
        (100 - piercingHitPercentage) *
        (100 - missPercentage),
      name: "normalHit",
    },
    {
      criticalHit: true,
      piercingHit: false,
      weight:
        criticalHitPercentage *
        (100 - piercingHitPercentage) *
        (100 - missPercentage),
      name: "criticalHit",
    },
    {
      criticalHit: false,
      piercingHit: true,
      weight:
        (100 - criticalHitPercentage) *
        piercingHitPercentage *
        (100 - missPercentage),
      name: "piercingHit",
    },
    {
      criticalHit: true,
      piercingHit: true,
      weight:
        criticalHitPercentage * piercingHitPercentage * (100 - missPercentage),
      name: "criticalPiercingHit",
    },
  ];

  return {
    attacker: attacker,
    victim: victim,
    attackFactor: calcAttackFactor(attacker, victim),
    mainAttackValue: calcMainAttackValue(attacker),
    attackValues: calcAttackValues(attacker),
    bonusValues: bonusValues,
    damagesTypeCombinaison: damagesTypeCombinaison,
  };
}

function updateBattleValues(battleValues, skillFormula, skillInfo) {
  var weaponBonus = 0;
  var skillWard = 0;
  var skillBonus = 0;
  var skillBonusByBonus = 0;
  var { attacker: attacker, bonusValues: bonusValues } = battleValues;
  var {
    range: [minVariation, maxVariation],
  } = skillInfo;
  var variationLength = maxVariation - minVariation + 1;

  if (skillInfo.hasOwnProperty("weaponBonus")) {
    var [weaponType, weaponBonusValue] = skillInfo.weaponBonus;

    if (weaponType === attacker.weapon.type) {
      weaponBonus = weaponBonusValue;
    }
  }

  if (skillInfo.skillBonus) {
    skillBonus = skillInfo.skillBonus;
  }

  if (skillInfo.skillWard) {
    skillWard = skillInfo.skillWard;
  }

  if (skillInfo.skillBonusByBonus) {
    skillBonusByBonus = skillInfo.skillBonusByBonus;
  }

  if (skillInfo.removeWeaponReduction) {
    bonusValues.weaponDefenseCoeff = 100;
  }

  bonusValues.weaponBonusCoeff = 100 + weaponBonus;
  bonusValues.skillWardCoeff = 1 - skillWard / 100;
  bonusValues.skillBonusCoeff = 1 + skillBonus / 100;
  bonusValues.skillBonusByBonusCoeff = 100 + skillBonusByBonus;

  battleValues.skillFormula = skillFormula;
  battleValues.skillRange = skillInfo.range;
  battleValues.attackValues.totalCardinal *= variationLength;
  battleValues.attackValues.possibleDamagesCount *= variationLength;
}

function calcWeights(minValue, maxValue, minInterval) {
  var firstWeightLimit = minValue + minInterval - 1;
  var lastWeightsLimit = maxValue - minInterval + 1;
  var weights = [];

  for (var value = minValue; value < firstWeightLimit; value++) {
    weights.push(value - minValue + 1);
  }

  for (var value = firstWeightLimit; value <= lastWeightsLimit; value++) {
    weights.push(minInterval);
  }

  for (var value = lastWeightsLimit + 1; value <= maxValue; value++) {
    weights.push(maxValue - value + 1);
  }

  return weights;
}

function calcBlessingBonus(skillPowerTable, victim) {
  if (victim.isBlessed !== "on") {
    return 0;
  }

  var int = victim.intBlessing;
  var dex = victim.dexBlessing;
  var skillPower = getSkillPower(victim["skillBlessing"], skillPowerTable);

  if (!skillPower) {
    return 0;
  }

  var blessingBonus = floorMultiplication(
    ((int * 0.3 + 5) * (2 * skillPower + 0.5) + 0.3 * dex) / (skillPower + 2.3),
    1
  );

  if (victim.class === "dragon" && isChecked(ictim.blessingOnself)) {
    blessingBonus = floorMultiplication(blessingBonus, 1.1);
  }

  return blessingBonus;
}

function getSkillFormula(battle, skillId, battleValues, removeSkillVariation) {
  var { attacker, victim, attackFactor } = battleValues;
  var skillPowerTable = battle.constants.skillPowerTable;

  var skillFormula;
  var skillInfo = { range: [0, 0] };

  var { class: attackerClass, level: lv, vit, str, int, dex } = attacker;

  if (skillId <= 9) {
    var skillPower = getSkillPower(
      attacker["attackSkill" + skillId],
      skillPowerTable
    );

    var improvedBySkillBonus = false;
    var improvedByBonus = false;

    if (attackerClass === "body") {
      switch (skillId) {
        // Triple lacération
        case 1:
          skillFormula = function (atk) {
            return floorMultiplication(
              1.1 * atk + (0.5 * atk + 1.5 * str) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Moulinet à l'épée
        case 2:
          skillFormula = function (atk) {
            return floorMultiplication(
              3 * atk + (0.8 * atk + 5 * str + 3 * dex + vit) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Accélération
        case 5:
          skillFormula = function (atk) {
            return floorMultiplication(
              2 * atk + (atk + dex * 3 + str * 7 + vit) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Volonté de vivre
        case 6:
          skillFormula = function (atk) {
            return floorMultiplication(
              (3 * atk + (atk + 1.5 * str) * skillPower) * 1.07,
              1
            );
          };
          break;
        // Tremblement de terre
        case 9:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              3 * atk +
                (0.9 * atk + variation + 5 * str + 3 * dex + lv) * skillPower,
              1
            );
          };
          skillInfo.range = [1, 1000];
          break;
      }
    } else if (attackerClass === "mental") {
      switch (skillId) {
        // Attaque de l'esprit
        case 1:
          skillFormula = function (atk) {
            return floorMultiplication(
              2.3 * atk + (4 * atk + 4 * str + vit) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Attaque de la paume
        case 2:
          skillFormula = function (atk) {
            return floorMultiplication(
              2.3 * atk + (3 * atk + 4 * str + 3 * vit) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Charge
        case 3:
          skillFormula = function (atk) {
            return floorMultiplication(
              2 * atk + (2 * atk + 2 * dex + 2 * vit + 4 * str) * skillPower,
              1
            );
          };
          break;
        // Coup d'épée
        case 5:
          skillFormula = function (atk) {
            return floorMultiplication(
              2 * atk + (atk + 3 * dex + 5 * str + vit) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Orbe de l'épée
        case 6:
          skillFormula = function (atk) {
            return floorMultiplication(
              (2 * atk + (2 * atk + 2 * dex + 2 * vit + 4 * str) * skillPower) *
                1.1,
              1
            );
          };
          break;
        // Tremblement de terre
        case 9:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              3 * atk +
                (0.9 * atk + variation + 5 * str + 3 * dex + lv) * skillPower,
              1
            );
          };
          skillInfo.range = [1, 1000];
          break;
      }
    } else if (attackerClass === "blade_fight") {
      switch (skillId) {
        // Embuscade
        case 1:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              atk + (1.2 * atk + variation + 4 * dex + 4 * str) * skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [1, 50];
          skillInfo.range = [500, 700];
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Attaque rapide
        case 2:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              atk + (1.6 * atk + variation + 7 * dex + 7 * str) * skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [1, 35];
          skillInfo.range = [200, 300];
          improvedByBonus = true;
          break;
        // Dague filante
        case 3:
          skillFormula = function (atk) {
            return floorMultiplication(
              2 * atk + (0.5 * atk + 9 * dex + 7 * str) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Brume empoisonnée
        case 5:
          skillFormula = function (atk) {
            return floorMultiplication(
              2 * lv + (atk + 3 * str + 18 * dex) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Poison insidieux
        case 6:
          skillFormula = function (atk) {
            return floorMultiplication(
              (2 * lv + (atk + 3 * str + 18 * dex) * skillPower) * 1.1,
              1
            );
          };
          break;
        // Étoiles brillantes
        case 9:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              atk + (1.7 * atk + variation + 6 * dex + 5 * lv) * skillPower,
              1
            );
          };
          skillInfo.range = [1, 1000];
          break;
      }
    } else if (attackerClass === "archery") {
      switch (skillId) {
        // Tir à répétition
        // case 1:
        //   skillFormula = function (atk) {
        //     return floorMultiplication(
        //       atk + 0.2 * atk * Math.floor(2 + 6 * skillPower) + (0.8 * atk + 8 * dex * attackFactor + 2 * int) * skillPower,
        //       1
        //     );
        //   };
        //   improvedByBonus = true;
        //   break;
        // Pluie de flèches
        case 2:
          skillFormula = function (atk) {
            return floorMultiplication(
              atk + (1.7 * atk + 5 * dex + str) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Flèche de feu
        case 3:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              1.5 * atk + (2.6 * atk + 0.9 * int + variation) * skillPower,
              1
            );
          };
          skillInfo.range = [100, 300];
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Foulée de plume
        case 4:
          skillFormula = function (atk) {
            return floorMultiplication(
              (3 * dex + 200 + 2 * str + 2 * int) * skillPower,
              1
            );
          };
          skillInfo.removeWeaponReduction = true;
          break;
        // Flèche empoisonnée
        case 5:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              atk +
                (1.4 * atk + variation + 7 * dex + 4 * str + 4 * int) *
                  skillPower,
              1
            );
          };
          skillInfo.range = [100, 200];
          improvedByBonus = true;
          break;
        // Coup étincelant
        case 6:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              (atk +
                (1.2 * atk + variation + 6 * dex + 3 * str + 3 * int) *
                  skillPower) *
                1.2,
              1
            );
          };
          skillInfo.range = [100, 200];
          improvedByBonus = true;
          break;
        // Tir tempête
        case 9:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              1.9 * atk + (2.6 * atk + variation) * skillPower,
              1
            );
          };
          skillInfo.range = [1, 1000];
          break;
      }
    } else if (attackerClass === "weaponary") {
      switch (skillId) {
        // Toucher brûlant
        case 1:
          skillFormula = function (atk) {
            return floorMultiplication(
              atk +
                2 * lv +
                2 * int +
                (2 * atk + 4 * str + 14 * int) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Tourbillon du dragon
        case 2:
          skillFormula = function (atk) {
            return floorMultiplication(
              1.1 * atk +
                2 * lv +
                2 * int +
                (1.5 * atk + str + 12 * int) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Contre-sort
        case 6:
          skillFormula = function (mav, variation) {
            return floorMultiplication(
              40 +
                5 * lv +
                2 * int +
                (10 * int + 7 * mav + variation) * attackFactor * skillPower,
              1
            );
          };
          skillInfo.range = [50, 100];
          break;
        // Coup démoniaque
        case 9:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              1.9 * atk + (2.6 * atk + variation) * skillPower,
              1
            );
          };
          skillInfo.range = [1, 1000];
          break;
      }
    } else if (attackerClass === "black_magic") {
      switch (skillId) {
        // Attaque des ténèbres
        case 1:
          skillFormula = function (mav, variation) {
            return floorMultiplication(
              40 +
                5 * lv +
                2 * int +
                (13 * int + 6 * mav + variation) * attackFactor * skillPower,
              1
            );
          };
          skillInfo.range = [50, 100];
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Attaque de flammes
        // case 2:
        //   skillFormula = function (mav, variation) {
        //     return floorMultiplication(
        //       5 * lv + 2 * int + (7 * int + 8 * mav + 4 * str + 2 * vit + variation) * skillPower,
        //       1
        //     );
        //   };
        //   skillInfo.range = [180, 100];
        //   improvedByBonus = true;
        //   break;
        // Esprit de flammes
        case 3:
          skillFormula = function (mav, variation) {
            return floorMultiplication(
              30 +
                2 * lv +
                2 * int +
                (7 * int + 6 * mav + variation) * attackFactor * skillPower,
              1
            );
          };
          skillInfo.range = [200, 500];
          break;
        // Frappe de l'esprit
        // case 5:
        //   skillFormula = function (mav, variation) {
        //     return floorMultiplication(
        //       40 + 2 * lv + 2 * int + (2 * vit + 2 * dex + 13 * int + 6 * mav + variation) * attackFactor * skillPower,
        //       1
        //     );
        //   };
        //   skillInfo.range = [180, 200];
        //   break;
        // Orbe des ténèbres
        case 6:
          skillFormula = function (mav) {
            return floorMultiplication(
              120 +
                6 * lv +
                (5 * vit + 5 * dex + 29 * int + 9 * mav) *
                  attackFactor *
                  skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Vague mortelle
        case 9:
          skillFormula = function (mav, variation) {
            return floorMultiplication(
              120 +
                6 * lv +
                (5 * vit + 5 * dex + 30 * int + variation + 9 * mav) *
                  attackFactor *
                  skillPower,
              1
            );
          };
          skillInfo.range = [1, 1000];
          break;
      }
    } else if (attackerClass === "dragon") {
      switch (skillId) {
        // Talisman volant
        case 1:
          skillFormula = function (mav) {
            return floorMultiplication(
              70 +
                5 * lv +
                (18 * int + 7 * str + 5 * mav + 50) * attackFactor * skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [4, 10];
          improvedByBonus = true;
          break;
        // Dragon chassant
        case 2:
          skillFormula = function (mav) {
            return floorMultiplication(
              60 +
                5 * lv +
                (16 * int + 6 * dex + 6 * mav + 120) *
                  attackFactor *
                  skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [4, 10];
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Rugissement du dragon
        case 3:
          skillFormula = function (mav) {
            return floorMultiplication(
              70 +
                3 * lv +
                (20 * int + 3 * str + 10 * mav + 100) *
                  attackFactor *
                  skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [4, 10];
          improvedByBonus = true;
          break;
        // Météore
        case 9:
          skillFormula = function (mav, variation) {
            return floorMultiplication(
              120 +
                6 * lv +
                (5 * vit + 5 * dex + 30 * int + variation + 9 * mav) *
                  attackFactor *
                  skillPower,
              1
            );
          };
          skillInfo.range = [1, 1000];
          break;
      }
    } else if (attackerClass === "heal") {
      switch (skillId) {
        // Jet de foudre
        case 1:
          skillFormula = function (mav, variation) {
            return floorMultiplication(
              60 +
                5 * lv +
                (8 * int + 2 * dex + 8 * mav + variation) *
                  attackFactor *
                  skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [6, 10];
          skillInfo.range = [5 * int, 15 * int];
          improvedByBonus = true;
          break;
        // Invocation de foudre
        case 2:
          skillFormula = function (mav, variation) {
            return floorMultiplication(
              40 +
                4 * lv +
                (13 * int + 2 * str + 10 * mav + variation) *
                  attackFactor *
                  skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [6, 10];
          skillInfo.range = [5 * int, 16 * int];
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Griffe de foudre
        case 3:
          skillFormula = function (mav, variation) {
            return floorMultiplication(
              50 +
                5 * lv +
                (8 * int + 2 * str + 8 * mav + variation) *
                  attackFactor *
                  skillPower,
              1
            );
          };
          skillInfo.range = [1, 800];
          improvedByBonus = true;
          break;
      }
    } else if (attackerClass === "lycan") {
      switch (skillId) {
        // Déchiqueter
        // case 1:
        //   skillFormula = function (atk) {
        //     return floorMultiplication(
        //       1.1 * atk + (0.3 * atk + 1.5 * str) * skillPower,
        //       1
        //     );
        //   };
        //   skillInfo.weaponBonus = [5, 54];
        //   improvedByBonus = true;
        //   break;
        // Souffle de loup
        case 2:
          skillFormula = function (atk) {
            return floorMultiplication(
              2 * atk + (atk + 3 * dex + 5 * str + vit) * skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [5, 35];
          improvedByBonus = true;
          improvedBySkillBonus = true;
          break;
        // Bond de loup
        case 3:
          skillFormula = function (atk) {
            return floorMultiplication(
              atk + (1.6 * atk + 200 + 7 * dex + 7 * str) * skillPower,
              1
            );
          };
          skillInfo.weaponBonus = [5, 35];
          improvedByBonus = true;
          break;
        // Griffe de loup
        case 4:
          skillFormula = function (atk) {
            return floorMultiplication(
              3 * atk + (0.8 * atk + 6 * str + 2 * dex + vit) * skillPower,
              1
            );
          };
          improvedByBonus = true;
          break;
        // Tempête cinglante
        case 9:
          skillFormula = function (atk, variation) {
            return floorMultiplication(
              1.8 * atk +
                (atk + 6 * dex + variation + 3 * str + lv) * skillPower,
              1
            );
          };
          skillInfo.range = [1, 1000];
          break;
      }
    }
    if (improvedBySkillBonus) {
      skillInfo.skillBonus =
        16 * getSkillPower(attacker.skillBonus, skillPowerTable);

      var skillWardChoice = victim.skillWardChoice;

      if (skillWardChoice && skillWardChoice === attackerClass) {
        skillInfo.skillWard =
          24 * getSkillPower(victim.skillWard, skillPowerTable);
      }
    }

    if (improvedByBonus) {
      skillInfo.skillBonusByBonus = attacker["skillBonus" + skillId];
    }

    if (removeSkillVariation) {
      var averageVariation = (skillInfo.range[0] + skillInfo.range[0]) / 2;
      skillInfo.range = [averageVariation, averageVariation];
    }
  } else {
    var skillPower = getSkillPower(
      attacker["horseSkill" + skillId],
      skillPowerTable
    );

    switch (skillId) {
      // Combat équestre
      case 137:
        skillFormula = function (atk) {
          return floorMultiplication(atk + 2 * atk * skillPower, 1);
        };
        break;
      // Charge à cheval
      case 138:
        skillFormula = function (atk) {
          return floorMultiplication(
            2.4 * (200 + 1.5 * lv) + 600 * skillPower,
            1
          );
        };
        break;
      // Vague de Pouvoir
      case 139:
        skillFormula = function (atk) {
          return floorMultiplication(
            2 * (200 + 1.5 * lv) + 600 * skillPower,
            1
          );
        };
        break;
      // Grêle de flèches
      case 140:
        skillFormula = function (atk) {
          return floorMultiplication(atk + 2 * atk * skillPower, 1);
        };
        break;
    }
  }

  updateBattleValues(battleValues, skillFormula, skillInfo);
}

function calcMagicAttackValueAugmentation(
  magicAttackValueWeapon,
  magicAttackValueBonus
) {
  if (magicAttackValueBonus) {
    return Math.max(
      1,
      0.0025056 *
        magicAttackValueBonus ** 0.602338 *
        magicAttackValueWeapon ** 1.20476
    );
  }
  return 0;
}

function getMagicAttackValueAugmentation(
  minMagicAttackValue,
  maxMagicAttackValue,
  magicAttackValueBonus
) {
  var magicAttackValueAugmentation = [];

  for (
    var magicAttackValue = minMagicAttackValue;
    magicAttackValue <= maxMagicAttackValue;
    magicAttackValue++
  ) {
    magicAttackValueAugmentation.push(
      calcMagicAttackValueAugmentation(magicAttackValue, magicAttackValueBonus)
    );
  }

  return magicAttackValueAugmentation;
}

function calcPhysicalDamages(battleValues) {
  var {
    attackFactor,
    mainAttackValue,
    attackValues: { minAttackValue, maxAttackValue, attackValueOther, weights },
    bonusValues,
    damagesTypeCombinaison,
  } = battleValues;

  var damagesWeightedByType = {};

  if (bonusValues.missPercentage) {
    damagesWeightedByType.miss = bonusValues.missPercentage / 100;
  }

  for (var damagesType of damagesTypeCombinaison) {
    if (!damagesType.weight) {
      continue;
    }

    var damagesWeighted = {};
    damagesWeightedByType[damagesType.name] = damagesWeighted;

    for (
      var attackValue = minAttackValue;
      attackValue <= maxAttackValue;
      attackValue++
    ) {
      var weight = weights[attackValue - minAttackValue] * damagesType.weight;

      var secondaryAttackValue = 2 * attackValue + attackValueOther;
      var rawDamages =
        mainAttackValue +
        floorMultiplication(attackFactor, secondaryAttackValue);

      var damagesWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
        rawDamages,
        bonusValues
      );

      var minPiercingDamages =
        damagesWithPrimaryBonuses -
        bonusValues.defenseBoost +
        bonusValues.defenseMarriage;

      if (minPiercingDamages <= 2) {
        for (var damages = 1; damages <= 5; damages++) {
          var finalDamages = calcDamageWithSecondaryBonuses(
            damages,
            bonusValues,
            damagesType,
            minPiercingDamages,
            damagesWithPrimaryBonuses
          );

          addKeyValue(damagesWeighted, finalDamages, weight / 5);
        }
      } else {
        var finalDamages = calcDamageWithSecondaryBonuses(
          minPiercingDamages,
          bonusValues,
          damagesType,
          minPiercingDamages,
          damagesWithPrimaryBonuses
        );

        addKeyValue(damagesWeighted, finalDamages, weight);
      }
    }
  }

  return damagesWeightedByType;
}

function calcPhysicalSkillDamages(battleValues) {
  var {
    attackFactor,
    mainAttackValue,
    attackValues: { minAttackValue, maxAttackValue, attackValueOther, weights },
    bonusValues,
    damagesTypeCombinaison,
    skillFormula,
    skillRange: [minVariation, maxVariation],
  } = battleValues;

  var damagesWeightedByType = {};

  for (var damagesType of damagesTypeCombinaison) {
    if (!damagesType.weight) {
      continue;
    }

    var damagesWeighted = {};
    var savedDamages = {};

    damagesWeightedByType[damagesType.name] = damagesWeighted;

    for (
      var attackValue = minAttackValue;
      attackValue <= maxAttackValue;
      attackValue++
    ) {
      var weight = weights[attackValue - minAttackValue] * damagesType.weight;

      var secondaryAttackValue = 2 * attackValue + attackValueOther;
      var rawDamages =
        mainAttackValue +
        floorMultiplication(attackFactor, secondaryAttackValue);

      var damagesWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
        rawDamages,
        bonusValues
      );

      for (
        var variation = minVariation;
        variation <= maxVariation;
        variation++
      ) {
        if (damagesWithPrimaryBonuses <= 2) {
          for (var damages = 1; damages <= 5; damages++) {
            damages *= bonusValues.useDamages;

            var damagesWithFormula = skillFormula(damages, variation);

            damagesWithFormula = Math.floor(
              (damagesWithFormula * bonusValues.weaponBonusCoeff) / 100
            );

            var finalDamages = calcSkillDamageWithSecondaryBonuses(
              damagesWithFormula,
              bonusValues,
              damagesType,
              damagesWithPrimaryBonuses
            );

            addKeyValue(damagesWeighted, finalDamages, weight / 5);
          }
        } else {
          damagesWithPrimaryBonuses *= bonusValues.useDamages;

          var damagesWithFormula = skillFormula(
            damagesWithPrimaryBonuses,
            variation
          );

          if (savedDamages.hasOwnProperty(damagesWithFormula)) {
            var finalDamages = savedDamages[damagesWithFormula];
            damagesWeighted[finalDamages] += weight;
            continue;
          }

          var finalDamages = Math.floor(
            (damagesWithFormula * bonusValues.weaponBonusCoeff) / 100
          );

          finalDamages = calcSkillDamageWithSecondaryBonuses(
            finalDamages,
            bonusValues,
            damagesType,
            damagesWithPrimaryBonuses
          );

          savedDamages[damagesWithFormula] = finalDamages;
          addKeyValue(damagesWeighted, finalDamages, weight);
        }
      }
    }
  }

  return damagesWeightedByType;
}

function calcMagicSkillDamages(battleValues) {
  var {
    attackValues: {
      minMagicAttackValue,
      maxMagicAttackValue,
      magicAttackValueAugmentation,
      weights,
    },
    bonusValues,
    damagesTypeCombinaison,
    skillFormula,
    skillRange: [minVariation, maxVariation],
  } = battleValues;

  var damagesWeightedByType = {};

  for (var damagesType of damagesTypeCombinaison) {
    if (!damagesType.weight) {
      continue;
    }

    var damagesWeighted = {};
    var savedDamages = {};

    damagesWeightedByType[damagesType.name] = damagesWeighted;

    for (
      var magicAttackValue = minMagicAttackValue;
      magicAttackValue <= maxMagicAttackValue;
      magicAttackValue++
    ) {
      var index = magicAttackValue - minMagicAttackValue;
      var weight = weights[index] * damagesType.weight;

      for (
        var variation = minVariation;
        variation <= maxVariation;
        variation++
      ) {
        var rawDamages = skillFormula(
          magicAttackValue + magicAttackValueAugmentation[index],
          variation
        );

        if (savedDamages.hasOwnProperty(rawDamages)) {
          var finalDamages = savedDamages[rawDamages];
          damagesWeighted[finalDamages] += weight;
          continue;
        }

        var damagesWithPrimaryBonuses = Math.floor(
          (rawDamages * bonusValues.weaponBonusCoeff) / 100
        );

        damagesWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
          damagesWithPrimaryBonuses,
          bonusValues
        );

        if (damagesWithPrimaryBonuses <= 2) {
          for (var damages = 1; damages <= 5; damages++) {
            var finalDamages = calcSkillDamageWithSecondaryBonuses(
              damages,
              bonusValues,
              damagesType,
              damagesWithPrimaryBonuses
            );
            addKeyValue(damagesWeighted, finalDamages, weight / 5);
          }
        } else {
          var finalDamages = calcSkillDamageWithSecondaryBonuses(
            damagesWithPrimaryBonuses,
            bonusValues,
            damagesType,
            damagesWithPrimaryBonuses
          );

          savedDamages[rawDamages] = finalDamages;
          addKeyValue(damagesWeighted, finalDamages, weight);
        }
      }
    }
  }

  return damagesWeightedByType;
}

function calcDamages(
  attacker,
  victim,
  attackType,
  battle,
  removeSkillVariation
) {
  var damagesCalculator, skillId, skillType;

  if (attackType === "physical") {
    damagesCalculator = calcPhysicalDamages;
  } else if (attackType.startsWith("attackSkill")) {
    skillId = Number(attackType.split("attackSkill")[1]);

    if (isMagicClass(attacker) || isDispell(attacker, skillId)) {
      skillType = "magic";
      damagesCalculator = calcMagicSkillDamages;
    } else {
      skillType = "physical";
      damagesCalculator = calcPhysicalSkillDamages;
    }
  } else if (attackType.startsWith("horseSkill")) {
    skillType = "physical";
    skillId = Number(attackType.split("horseSkill")[1]);
    damagesCalculator = calcPhysicalSkillDamages;
  }

  var battleValues = createBattleValues(attacker, victim, battle, skillType);

  if (skillId) {
    getSkillFormula(battle, skillId, battleValues, removeSkillVariation);
  }

  var {
    attackValues: { totalCardinal, possibleDamagesCount },
  } = battleValues;

  return {
    damagesWeightedByType: damagesCalculator(battleValues),
    totalCardinal: totalCardinal,
    possibleDamagesCount: possibleDamagesCount,
    skillType: skillType,
  };
}

function damagesWithoutVariation(
  attacker,
  victim,
  attackType,
  battle,
  characters
) {
  startDamagesTime = performance.now();

  var {
    damagesWeightedByType,
    totalCardinal,
    possibleDamagesCount,
    skillType,
  } = calcDamages(attacker, victim, attackType, battle);

  endDamagesTime = performance.now();

  possibleDamagesCount = displayResults(
    possibleDamagesCount,
    totalCardinal,
    damagesWeightedByType,
    battle,
    attacker.name,
    victim.name
  );

  endDisplayTime = performance.now();

  displayFightInfo(
    possibleDamagesCount,
    endDamagesTime - startDamagesTime,
    endDisplayTime - endDamagesTime,
    battle
  );
  addPotentialErrorInformation(
    battle.errorInformation,
    attacker,
    victim,
    skillType,
    characters
  );

  hideElement(battle.bonusVariationResultContainer);
  showElement(battle.fightResultContainer);
}

function damagesWithVariation(
  attacker,
  victim,
  attackType,
  battle,
  entity,
  entityVariation
) {
  startTime = performance.now();
  var damagesByBonus = [];
  var augmentationByBonus = [];
  var {
    bonusVariationMinValue: minVariation,
    bonusVariationMaxValue: maxVariation,
  } = entity;
  var step = Math.ceil((maxVariation - minVariation + 1) / 500);
  var simulationCount = 0;
  var simulationTime;

  for (
    var bonusValue = minVariation;
    bonusValue <= maxVariation;
    bonusValue += step
  ) {
    entity[entityVariation] = bonusValue;

    var { damagesWeightedByType, totalCardinal } = calcDamages(
      copyObject(attacker),
      copyObject(victim),
      attackType,
      battle,
      true
    );

    var meanDamages = calcMeanDamages(damagesWeightedByType, totalCardinal);

    if (bonusValue === minVariation) {
      var firstDamages = Math.max(meanDamages, 1e-3);
    }

    damagesByBonus.push({ x: bonusValue, y: meanDamages });
    augmentationByBonus.push({
      x: bonusValue,
      y: meanDamages / firstDamages - 1,
    });
    simulationCount++;
  }

  endTime = performance.now();

  battle.damagesByBonus = damagesByBonus.concat(entityVariation);

  addToBonusVariationChart(
    damagesByBonus,
    augmentationByBonus,
    entity.bonusVariationDisplay,
    battle.bonusVariationChart
  );

  simulationCount = battle.numberFormats.default.format(simulationCount);
  simulationTime = battle.numberFormats.second.format(
    (endTime - startTime) / 1000
  );

  battle.simulationCounter.textContent = simulationCount;
  battle.simulationTime.textContent = simulationTime;

  hideElement(battle.fightResultContainer);
  showElement(battle.bonusVariationResultContainer);

  if (
    isChecked(attacker.bonusVariationActivation) &&
    isChecked(victim.bonusVariationActivation)
  ) {
    showElement(battle.errorInformation["attacker-victim-variation"]);
  } else {
    hideElement(battle.errorInformation["attacker-victim-variation"]);
  }
}

function changeMonsterValues(monster, instance, attacker) {
  switch (instance) {
    case "SungMahiTower":
      var sungMahiFloor = 1;
      var sungMahiStep = 1;
      var rawDefense = 120;

      if (isPC(attacker)) {
        sungMahiFloor = attacker.sungMahiFloor;
        sungMahiStep = attacker.sungMahiStep;
      }

      if (monster.rank === 5) {
        monster.level = 121;
        monster.dex = 75;
        rawDefense += 1;
      } else if (monster.rank === 6) {
        monster.level = 123;
        monster.dex = 75;
        rawDefense += 1;
      } else {
        monster.level = 120;
        monster.dex = 68;
      }
      monster.vit = 100;
      monster.rawDefense = rawDefense + (sungMahiStep - 1) * 6;
      monster.fistDefense = 0;
      monster.swordDefense = 0;
      monster.twoHandedSwordDefense = 0;
      monster.daggerDefense = 0;
      monster.bellDefense = 0;
      monster.fanDefense = 0;
      monster.arrowDefense = 0;
      monster.clawDefense = 0;
      monster.magicResistance = 0;
      monster.fireResistance = -20;
  }
}

function createWeapon(weaponVnum) {
  var weapon = weaponData[weaponVnum];
  var weaponName = weapon[0];

  return {
    name: weaponName,
    type: weapon[1],
    minAttackValue: weapon[2][2],
    maxAttackValue: weapon[2][3],
    minMagicAttackValue: weapon[2][0],
    maxMagicAttackValue: weapon[2][1],
    upgrades: weapon[3],
    isSerpent: isValueInArray("serpent", weaponName.toLowerCase()),
  };
}

function createMonster(monsterVnum, attacker) {
  var monsterAttributes = monsterData[monsterVnum];

  var monster = {
    name: monsterAttributes[36],
    rank: monsterAttributes[0],
    race: monsterAttributes[1],
    attack: monsterAttributes[2],
    level: monsterAttributes[3],
    type: monsterAttributes[4],
    str: monsterAttributes[5],
    dex: monsterAttributes[6],
    vit: monsterAttributes[7],
    int: monsterAttributes[8],
    minAttackValue: monsterAttributes[9],
    maxAttackValue: monsterAttributes[10],
    rawDefense: monsterAttributes[11],
    criticalHit: monsterAttributes[12],
    piercingHit: monsterAttributes[13],
    fistDefense: monsterAttributes[14],
    swordDefense: monsterAttributes[15],
    twoHandedSwordDefense: monsterAttributes[16],
    daggerDefense: monsterAttributes[17],
    bellDefense: monsterAttributes[18],
    fanDefense: monsterAttributes[19],
    arrowDefense: monsterAttributes[20],
    clawDefense: monsterAttributes[21],
    fireResistance: monsterAttributes[22],
    lightningResistance: monsterAttributes[23],
    magicResistance: monsterAttributes[24],
    windResistance: monsterAttributes[25],
    lightningBonus: monsterAttributes[26],
    fireBonus: monsterAttributes[27],
    iceBonus: monsterAttributes[28],
    windBonus: monsterAttributes[29],
    earthBonus: monsterAttributes[30],
    darknessBonus: monsterAttributes[31],
    darknessResistance: monsterAttributes[32],
    iceResistance: monsterAttributes[33],
    earthResistance: monsterAttributes[34],
    damageMultiplier: monsterAttributes[35],
  };

  // monster.instance = 0;

  // if (attacker && monster.instance === 0) {
  //   changeMonsterValues(monster, "SungMahiTower", attacker);
  // }

  monster.defense = monster.rawDefense + monster.level + monster.vit;

  return monster;
}

function addPotentialErrorInformation(
  errorInformation,
  attacker,
  victim,
  skillType,
  characters
) {
  for (var error of Object.values(errorInformation)) {
    hideElement(error);
  }

  if (isPC(attacker)) {
    if (isRiding(attacker)) {
      if (attacker.horsePoint === 0) {
        showElement(errorInformation["horse-level"]);
      }
      showElement(errorInformation["horse-stat"]);
    } else if (isPolymorph(attacker)) {
      if (attacker.polymorphPoint === 0) {
        showElement(errorInformation["polymorph-level"]);
      }

      if (
        (attacker.polymorphPoint <= 39 && attacker.attackValuePercent <= 199) ||
        (attacker.polymorphPoint === 40 && attacker.attackValuePercent <= 299)
      ) {
        showElement(errorInformation["polymorph-bonus"]);
      }
    }
    if (skillType === "magic") {
      if (attacker.magicAttackValue) {
        showElement(errorInformation["magic-attack-value-bonus"]);
      }
      if (victim.magicResistance) {
        showElement(errorInformation["magic-resistance"]);
      }
    }
  } else {
    showElement(errorInformation["monster-attacker"]);
    if (isMagicAttacker(attacker) && victim.magicResistance) {
      showElement(errorInformation["magic-resistance"]);
    }
  }

  if (isPC(victim)) {
    if (isRiding(victim)) {
      showElement(errorInformation["horse-stat"]);
    } else if (isPolymorph(victim)) {
      if (attacker.polymorphPoint === 0) {
        showElement(errorInformation["polymorph-level"]);
      }
      showElement(errorInformation["polymorph-defense"]);
    }
  }

  if (characters.unsavedChanges) {
    showElement(errorInformation["save"]);
  }
}

function reduceChartPointsListener(battle) {
  var {
    reduceChartPointsContainer,
    reduceChartPoints,
    numberFormats: { second: numberFormat },
    displayTime,
  } = battle;

  reduceChartPoints.addEventListener("change", function () {
    var startDisplayTime = performance.now();
    var scatterDataByType = battle.scatterDataByType;
    var {
      chart,
      maxPoints,
      chart: {
        data: { datasets },
      },
    } = battle.damagesChart;
    var addAnimations = false;

    for (var index = 0; index < datasets.length; index++) {
      var dataset = datasets[index];
      var scatterData = scatterDataByType[dataset.name];

      if (dataset.canBeReduced && reduceChartPoints.checked) {
        dataset.data = aggregateDamages(scatterData, maxPoints);
        addAnimations = true;
      } else {
        dataset.data = scatterData;
      }
    }

    handleChartAnimations(chart, addAnimations);
    chart.update();

    displayTime.textContent = numberFormat.format(
      (performance.now() - startDisplayTime) / 1000
    );
  });

  reduceChartPointsContainer.addEventListener("pointerup", function (event) {
    if (event.pointerType === "mouse") {
      reduceChartPoints.click();
    }
  });
}

function downloadRawDataListener(battle) {
  var { downLoadRawData, downLoadRawDataVariation } = battle;
  var fileType = "text/csv;charset=utf-8;";

  downLoadRawData.addEventListener("click", function () {
    var damagesWeightedByType = battle.damagesWeightedByType;
    var filename = "raw_damages.csv";
    var csvContent = "damage,probabilities,damageType\n";

    for (var damagesType in damagesWeightedByType) {
      var damagesWeighted = damagesWeightedByType[damagesType];

      for (var damages in damagesWeighted) {
        csvContent +=
          damages + "," + damagesWeighted[damages] + "," + damagesType + "\n";
      }
    }

    downloadData(csvContent, fileType, filename);
  });

  downLoadRawDataVariation.addEventListener("click", function () {
    var damagesByBonus = battle.damagesByBonus;
    var damagesByBonusLength = damagesByBonus.length;
    var filename = "damages_variation.csv";

    if (!damagesByBonusLength) {
      return;
    }

    var csvContent =
      damagesByBonus[damagesByBonusLength - 1] + ",averageDamage\n";

    for (var index = 0; index < damagesByBonusLength - 1; index++) {
      var row = damagesByBonus[index];

      csvContent += row.x + "," + row.y + "\n";
    }

    downloadData(csvContent, fileType, filename);
  });
}

function displayResults(
  possibleDamagesCount,
  totalCardinal,
  damagesWeightedByType,
  battle,
  attackerName,
  victimName
) {
  var [
    meanDamages,
    minDamages,
    maxDamages,
    scatterDataByType,
    possibleDamagesCount,
    uniqueDamagesCount,
  ] = prepareDamagesData(
    damagesWeightedByType,
    possibleDamagesCount,
    totalCardinal
  );

  addToDamagesChart(
    scatterDataByType,
    battle.damagesChart,
    battle.reduceChartPoints.checked
  );
  updateDamagesChartDescription(
    battle.uniqueDamagesCounters,
    uniqueDamagesCount,
    battle.numberFormats.default
  );
  displayFightResults(
    battle,
    attackerName,
    victimName,
    meanDamages,
    minDamages,
    maxDamages
  );
  battle.damagesWeightedByType = damagesWeightedByType;
  battle.scatterDataByType = scatterDataByType;

  return possibleDamagesCount;
}

function displayFightResults(
  battle,
  attackerName,
  victimName,
  meanDamages,
  minDamages,
  maxDamages
) {
  var {
    tableResultFight,
    tableResultHistory,
    attackTypeSelection,
    savedFights,
    numberFormats: { default: numberFormat },
    deleteFightTemplate,
  } = battle;

  hideElement(tableResultHistory.rows[1]);

  var valuesToDisplay = [
    attackerName,
    victimName,
    attackTypeSelection.options[attackTypeSelection.selectedIndex].textContent,
    meanDamages,
    minDamages,
    maxDamages,
  ];

  savedFights.push(valuesToDisplay);
  updateSavedFights(savedFights);

  editTableResultRow(tableResultFight.rows[1], valuesToDisplay, numberFormat);
  addRowToTableResultHistory(
    tableResultHistory,
    valuesToDisplay,
    deleteFightTemplate,
    numberFormat
  );
}

function displayFightInfo(
  possibleDamagesCount,
  damagesTime,
  displayTime,
  battle
) {
  var container = battle.possibleDamagesCounter.parentElement;

  if (possibleDamagesCount <= 1) {
    hideElement(container);
    return;
  } else {
    showElement(container);
  }

  possibleDamagesCount =
    battle.numberFormats.default.format(possibleDamagesCount);
  damagesTime = battle.numberFormats.second.format(damagesTime / 1000);
  displayTime = battle.numberFormats.second.format(displayTime / 1000);

  battle.possibleDamagesCounter.textContent = possibleDamagesCount;
  battle.damagesTime.textContent = damagesTime;
  battle.displayTime.textContent = displayTime;
}

function isPseudoSaved(characters, pseudo) {
  return characters.savedCharacters.hasOwnProperty(pseudo);
}

function createBattle(characters, battle) {
  battle.battleForm.addEventListener("submit", function (event) {
    event.preventDefault();

    // auto save
    if (characters.unsavedChanges) {
      characters.saveButton.click();
    }

    var battleInfo = new FormData(event.target);
    var attackerName = battleInfo.get("attacker");
    var attackType = battleInfo.get("attackTypeSelection");
    var victimName = battleInfo.get("victim");
    var attackerVariation;
    var victimVariation;

    if (!attackerName && !attackType && !victimName) {
      return;
    }

    if (isPseudoSaved(characters, attackerName)) {
      var attacker = copyObject(characters.savedCharacters[attackerName]);
      attackerVariation = attacker.bonusVariation;
    } else {
      var attacker = createMonster(attackerName);
    }

    if (isPseudoSaved(characters, victimName)) {
      var victim = copyObject(characters.savedCharacters[victimName]);
      victimVariation = victim.bonusVariation;
    } else {
      var victim = createMonster(victimName, attacker);
    }

    if (
      isChecked(attacker.bonusVariationActivation) &&
      attacker.hasOwnProperty(attackerVariation) &&
      attacker.bonusVariationMinValue < attacker.bonusVariationMaxValue
    ) {
      damagesWithVariation(
        attacker,
        victim,
        attackType,
        battle,
        attacker,
        attackerVariation
      );
    } else if (
      isChecked(victim.bonusVariationActivation) &&
      victim.hasOwnProperty(victimVariation) &&
      victim.bonusVariationMinValue < victim.bonusVariationMaxValue
    ) {
      damagesWithVariation(
        attacker,
        victim,
        attackType,
        battle,
        victim,
        victimVariation
      );
    } else {
      damagesWithoutVariation(attacker, victim, attackType, battle, characters);
    }
  });
}

function createMapping() {
  mapping = {
    typeFlag: [
      "animalBonus", // 0
      "humanBonus", // 1
      "orcBonus", // 2
      "mysticBonus", // 3
      "undeadBonus", // 4
      "insectBonus", // 5
      "desertBonus", // 6
      "devilBonus", // 7
    ],
    raceBonus: {
      warrior: "warriorBonus",
      sura: "suraBonus",
      ninja: "ninjaBonus",
      shaman: "shamanBonus",
      lycan: "lycanBonus",
    },
    raceResistance: {
      warrior: "warriorResistance",
      sura: "suraResistance",
      ninja: "ninjaResistance",
      shaman: "shamanResistance",
      lycan: "lycanResistance",
    },
    defenseWeapon: [
      "swordDefense", // 0
      "daggerDefense", // 1
      "arrowDefense", // 2
      "twoHandedSwordDefense", // 3
      "bellDefense", // 4
      "clawDefense", // 5
      "fanDefense", // 6
      "swordDefense", // 7
      "fistDefense", // 8
    ],
    breakWeapon: [
      "breakSwordDefense", // 0
      "breakDaggerDefense", // 1
      "breakArrowDefense", // 2
      "breakTwoHandedSwordDefense", // 3
      "breakBellDefense", // 4
      "breakClawDefense", // 5
      "breakFanDefense", // 6
      "breakSwordDefense", // 7
    ],
    elementBonus: [
      "fireBonus", // 0
      "iceBonus", // 1
      "windBonus", // 2
      "lightningBonus", // 3
      "earthBonus", // 4
      "darknessBonus", // 5
    ],
    elementResistance: [
      "fireResistance", // 0
      "iceResistance", // 1
      "windResistance", // 2
      "lightningResistance", // 3
      "earthResistance", // 4
      "darknessResistance", // 5
    ],
  };
  return mapping;
}

function createConstants() {
  var constants = {
    polymorphPowerTable: [
      10, 11, 11, 12, 13, 13, 14, 15, 16, 17, 18, 19, 20, 22, 23, 24, 26, 27,
      29, 31, 33, 35, 37, 39, 41, 44, 46, 49, 52, 55, 59, 62, 66, 70, 74, 79,
      84, 89, 94, 100, 0,
    ],
    skillPowerTable: [
      0, 0.05, 0.06, 0.08, 0.1, 0.12, 0.14, 0.16, 0.18, 0.2, 0.22, 0.24, 0.26,
      0.28, 0.3, 0.32, 0.34, 0.36, 0.38, 0.4, 0.5, 0.52, 0.54, 0.56, 0.58, 0.6,
      0.63, 0.66, 0.69, 0.72, 0.82, 0.85, 0.88, 0.91, 0.94, 0.98, 1.02, 1.06,
      1.1, 1.15, 1.25,
    ],
    marriageTable: {
      harmonyEarrings: [4, 5, 6, 8],
      loveEarrings: [4, 5, 6, 8],
      harmonyBracelet: [4, 5, 6, 8],
      loveNecklace: [20, 25, 30, 40],
      harmonyNecklace: [12, 16, 20, 30],
    },
    allowedWeaponsPerRace: {
      warrior: [0, 3, 8],
      ninja: [0, 1, 2, 8],
      sura: [0, 7, 8],
      shaman: [4, 6, 8],
      lycan: [5, 8],
    },
    translation: {
      fr: {
        damages: "Dégâts",
        percentage: "Pourcentage",
        miss: "Miss",
        normalHit: "Coup classique",
        criticalHit: "Coup critique",
        piercingHit: "Coup perçant",
        criticalPiercingHit: "Coup critique perçant",
        damagesRepartition: "Distribution des dégâts",
        averageDamages: "Dégâts moyens",
        damagesAugmentation: "Augmentation des dégâts",
        bonusVariationTitle: [
          "Évolution des dégâts moyens",
          "par rapport à la valeur d'un bonus",
        ],
      },
      en: {
        damages: "Damage",
        percentage: "Percentage",
        miss: "Miss",
        normalHit: "Normal Hit",
        criticalHit: "Critical Hit",
        piercingHit: "Piercing Hit",
        criticalPiercingHit: "Critical Piercing Hit",
        damagesRepartition: "Damage Repartition",
        averageDamages: "Average Damage",
        damagesAugmentation: "Damage Augmentation",
        bonusVariationTitle: [
          "Evolution of Average Damage",
          "Relative to a Bonus Value",
        ],
      },
      tr: {
        damages: "Hasar",
        percentage: "Yüzde",
        miss: "Miss Vuruş",
        normalHit: "Düz Vuruş",
        criticalHit: "Kritik Vuruş",
        piercingHit: "Delici Vuruş",
        criticalPiercingHit: "Kritikli Delici Vuruş",
        damagesRepartition: "Hasar Dağılımı",
        averageDamages: "Ortalama Hasar",
        damagesAugmentation: "Ortalama Hasar Artışı",
        bonusVariationTitle: [
          "Bir bonusun değerine kıyasla",
          "Ortalama Hasar Çizelgesi",
        ],
      },
      ro: {
        damages: "Daune",
        percentage: "Procent",
        miss: "Miss",
        normalHit: "Lovitura normala",
        criticalHit: "Lovitura critica",
        piercingHit: "Lovitura patrunzatoare",
        criticalPiercingHit: "Lovitura critica si patrunzatoare",
        damagesRepartition: "Distribuția daunelor",
        averageDamages: "Media damageului",
        damagesAugmentation: "Damage imbunatatit",
        bonusVariationTitle: [
          "Evolutia mediei damageului",
          "relativ la o valoare bonus",
        ],
      },
      de: {
        damages: "Schäden",
        percentage: "Prozentsatz",
        miss: "Verfehlen",
        normalHit: "Normaler Treffer",
        criticalHit: "Kritischer Treffer",
        piercingHit: "Durchdringender Treffer",
        criticalPiercingHit: "Kritischer durchdringender Treffer",
        damagesRepartition: "Schadensverteilung",
        averageDamages: "Durchschnittlicher Schaden",
        damagesAugmentation: "Schadenserhöhung",
        bonusVariationTitle: [
          "Entwicklung des durchschnittlichen Schadens",
          "im Verhältnis zu einem Bonus",
        ],
      },
      pt: {
        damages: "Dano",
        percentage: "Percentagem",
        miss: "Miss",
        normalHit: "Dano normal",
        criticalHit: "Dano crítico",
        piercingHit: "Dano perfurante",
        criticalPiercingHit: "Dano crítico perfurante",
        damagesRepartition: "Repartição de dano",
        averageDamages: "Dano médio",
        damagesAugmentation: "Aumento de dano",
        bonusVariationTitle: ["Evolução do dano médio", "relativo a um bónus"],
      },
      // es: {
      //   damages: "Daño",
      //   percentage: "Porcentaje",
      //   miss: "Miss",
      //   normalHit: "Daño normal",
      //   criticalHit: "Daño crítico",
      //   piercingHit: "Daño perforante",
      //   criticalPiercingHit: "Daño crítico perforante",
      //   damagesRepartition: "Repartición de daños",
      //   averageDamages: "Daño medio",
      //   damagesAugmentation: "Aumento de daño",
      //   bonusVariationTitle: ["Evolución del daño medio", "Relativo a una bonificación"]
      // },
    },
  };
  return constants;
}

function initResultTableHistory(battle) {
  var {
    tableResultHistory,
    savedFights,
    deleteFightTemplate,
    numberFormats: { default: numberFormat },
  } = battle;
  var startIndex = 3;

  if (savedFights.length) {
    hideElement(tableResultHistory.rows[1]);

    for (var savedFight of savedFights) {
      addRowToTableResultHistory(
        tableResultHistory,
        savedFight,
        deleteFightTemplate,
        numberFormat
      );
    }
  }

  tableResultHistory.addEventListener("click", function (event) {
    var deleteButton = event.target.closest(".svg-icon-delete");

    if (deleteButton) {
      var row = deleteButton.closest("tr");

      if (row) {
        savedFights.splice(row.rowIndex - startIndex, 1);
        updateSavedFights(savedFights);

        row.remove();

        if (tableResultHistory.rows.length === startIndex) {
          showElement(tableResultHistory.rows[1]);
        }
      }
    }
  });
}

function initDamagesChart(battle) {
  var { translation, reduceChartPointsContainer, reduceChartPoints } = battle;
  var percentFormat = battle.numberFormats.percent;
  var customPlugins = {
    id: "customPlugins",
    afterDraw(chart) {
      var missPercentage = chart.data.missPercentage;

      if (!missPercentage) {
        return;
      }

      var {
        ctx,
        chartArea: { top, right },
      } = chart;
      ctx.save();
      var text =
        translation.miss + " : " + percentFormat.format(missPercentage);
      var padding = 4;
      var fontSize = 14;

      ctx.font = fontSize + "px Helvetica Neue";

      var textWidth = ctx.measureText(text).width;
      var xPosition = right - textWidth - 5;
      var yPosition = top + 5;

      ctx.fillStyle = "rgba(255, 0, 0, 0.2)";
      ctx.fillRect(
        xPosition - padding,
        yPosition - padding,
        textWidth + 2 * padding,
        fontSize + 2 * padding
      );

      ctx.strokeStyle = "red";
      ctx.strokeRect(
        xPosition - padding,
        yPosition - padding,
        textWidth + 2 * padding,
        fontSize + 2 * padding
      );

      ctx.fillStyle = "#666";
      ctx.textBaseline = "top";
      ctx.fillText(text, xPosition, yPosition + 1);

      ctx.restore();
    },
  };

  Chart.register(customPlugins);

  var ctx = battle.plotDamages.getContext("2d");
  var maxLabelsInTooltip = 10;
  var nullLabelText = " ...";

  var chart = new Chart(ctx, {
    type: "scatter",
    data: {
      missPercentage: 0,
      datasets: [],
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        legend: {
          display: true,
          onHover: function (e) {
            e.native.target.style.cursor = "pointer";
          },
          onLeave: function (e) {
            e.native.target.style.cursor = "default";
          },
          onClick: function (e, legendItem, legend) {
            var currentIndex = legendItem.datasetIndex;
            var ci = legend.chart;
            var isCurrentDatasetVisible = ci.isDatasetVisible(currentIndex);
            var datasets = ci.data.datasets;
            var hideReducePoints = true;
            var isReducePointsChecked = reduceChartPoints.checked;

            datasets[currentIndex].hidden = isCurrentDatasetVisible;
            legendItem.hidden = isCurrentDatasetVisible;

            for (var index in datasets) {
              if (ci.isDatasetVisible(index) && datasets[index].canBeReduced) {
                showElement(reduceChartPointsContainer);
                hideReducePoints = false;
                break;
              }
            }

            if (hideReducePoints) {
              hideElement(reduceChartPointsContainer);
              handleChartAnimations(ci, true);
            } else {
              handleChartAnimations(ci, isReducePointsChecked);
            }

            ci.update();
          },
        },
        title: {
          display: true,
          text: translation.damagesRepartition,
          font: {
            size: 20,
          },
        },
        tooltip: {
          callbacks: {
            label: function (context) {
              if (context.label === null) {
                return nullLabelText;
              }

              var xValue = battle.numberFormats.default.format(
                context.parsed.x
              );
              var yValue = battle.numberFormats.percent.format(
                context.parsed.y
              );

              label =
                " " +
                context.dataset.label +
                " : (" +
                xValue +
                ", " +
                yValue +
                ")";

              return label;
            },
            beforeBody: function (tooltipItems) {
              if (tooltipItems.length > maxLabelsInTooltip + 1) {
                tooltipItems.splice(maxLabelsInTooltip + 1);
                tooltipItems[maxLabelsInTooltip].label = null;
              }
            },
          },
          caretPadding: 10,
        },
      },
      scales: {
        x: {
          type: "linear",
          position: "bottom",
          title: {
            display: true,
            text: translation.damages,
            font: {
              size: 16,
            },
          },
        },
        y: {
          title: {
            display: true,
            text: translation.percentage,
            font: {
              size: 16,
            },
          },
          ticks: {
            format: {
              style: "percent",
            },
          },
        },
      },
      elements: {
        point: {
          borderWidth: 1,
          radius: 3,
          hitRadius: 3,
          hoverRadius: 6,
          hoverBorderWidth: 2,
        },
      },
    },
  });

  var datasetsStyle = [
    {
      name: "normalHit",
      canBeReduced: false,
      label: translation.normalHit,
      backgroundColor: "rgba(75, 192, 192, 0.2)",
      borderColor: "rgba(75, 192, 192, 1)",
    },
    {
      name: "piercingHit",
      canBeReduced: false,
      label: translation.piercingHit,
      backgroundColor: "rgba(192, 192, 75, 0.2)",
      borderColor: "rgba(192, 192, 75, 1)",
    },
    {
      name: "criticalHit",
      canBeReduced: false,
      label: translation.criticalHit,
      backgroundColor: "rgba(192, 75, 192, 0.2)",
      borderColor: "rgba(192, 75, 192, 1)",
    },
    {
      name: "criticalPiercingHit",
      canBeReduced: false,
      label: translation.criticalPiercingHit,
      backgroundColor: "rgba(75, 75, 192, 0.2)",
      borderColor: "rgba(75, 75, 192, 1)",
    },
  ];
  battle.damagesChart = {
    chart: chart,
    datasetsStyle: datasetsStyle,
    maxPoints: 500,
    reduceChartPointsContainer: reduceChartPointsContainer,
  };
}

function initBonusVariationChart(battle) {
  var translation = battle.translation;

  var ctx = battle.plotBonusVariation.getContext("2d");

  var chart = new Chart(ctx, {
    type: "line",
    data: {
      datasets: [
        {
          label: translation.averageDamages,
          backgroundColor: "rgba(75, 192, 192, 0.2)",
          borderColor: "rgba(75, 192, 192, 1)",
          fill: true,
        },
        {
          label: translation.damagesAugmentation,
          backgroundColor: "rgba(192, 192, 75, 0.2)",
          borderColor: "rgba(192, 192, 75, 1)",
          hidden: true,
          yTicksFormat: { style: "percent" },
          fill: true,
        },
      ],
    },
    options: {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        legend: {
          display: true,
          onHover: function (e) {
            e.native.target.style.cursor = "pointer";
          },
          onLeave: function (e) {
            e.native.target.style.cursor = "default";
          },
          onClick: function (e, legendItem, legend) {
            var currentIndex = legendItem.datasetIndex;
            var ci = legend.chart;
            var datasets = ci.data.datasets;
            var isCurrentDatasetVisible = ci.isDatasetVisible(currentIndex);
            var yAxis = ci.options.scales.y;

            var otherIndex = currentIndex === 0 ? 1 : 0;
            var visibleDataset = isCurrentDatasetVisible
              ? datasets[otherIndex]
              : datasets[currentIndex];

            datasets[currentIndex].hidden = isCurrentDatasetVisible;
            datasets[otherIndex].hidden = !isCurrentDatasetVisible;

            yAxis.title.text = visibleDataset.label;
            yAxis.ticks.format = visibleDataset.yTicksFormat;

            ci.update();
          },
        },
        title: {
          display: true,
          text: translation.bonusVariationTitle,
          font: {
            size: 18,
          },
        },
        tooltip: {
          caretPadding: 10,
        },
      },
      scales: {
        x: {
          type: "linear",
          position: "bottom",
          title: {
            display: true,
            text: "Bonus",
            font: {
              size: 16,
            },
          },
          ticks: {
            callback: function (value) {
              if (Number.isInteger(value)) {
                return Number(value);
              }
            },
          },
        },
        y: {
          title: {
            display: true,
            text: translation.averageDamages,
            font: {
              size: 16,
            },
          },
        },
      },
      elements: {
        point: {
          borderWidth: 1,
          radius: 3,
          hitRadius: 3,
          hoverRadius: 6,
          hoverBorderWidth: 2,
        },
      },
    },
  });

  battle.bonusVariationChart = chart;
}

function attackSelectonListener(
  characters,
  attackerSelection,
  attackTypeSelection
) {
  attackerSelection.addEventListener("change", function (event) {
    var attackerName = event.target.value;

    if (isPseudoSaved(characters, attackerName)) {
      var attacker = characters.savedCharacters[attackerName];
      filterAttackTypeSelection(attacker, attackTypeSelection);
    } else {
      filterAttackTypeSelectionMonster(attackTypeSelection);
    }
  });
}

function getTranslation(translation) {
  var userLanguage = navigator.language;
  var langToUse = "en";

  for (var lang in translation) {
    if (userLanguage.startsWith(lang)) {
      langToUse = lang;
      break;
    }
  }

  return translation[langToUse];
}

function createDamageCalculatorInformation(chartSource) {
  var characters = {
    unsavedChanges: false,
    savedCharacters: {},
    currentCharacter: null,
    savedMonsters: getSavedMonsters(),
    characterCreation: document.getElementById("character-creation"),
    addNewCharacterButton: document.getElementById("add-new-character"),
    dropZone: document.getElementById("character-drop-zone"),
    characterInput: document.getElementById("character-input"),
    newCharacterTemplate: document.getElementById("new-character-template")
      .children[0],
    charactersContainer: document.getElementById("characters-container"),
    newMonsterTemplate: document.getElementById("new-monster-template")
      .children[0],
    monstersContainer: document.getElementById("monsters-container"),
    monsterListForm: document.getElementById("monster-list-form"),
    searchMonster: document.getElementById("search-monster"),
    monsterList: document.getElementById("monster-list"),
    saveButton: document.getElementById("save-character"),
    weaponCategory: document.getElementById("weapon-category"),
    weaponDisplay: document.getElementById("weapon-display"),
    randomAttackValue: document.getElementById("random-attack-value"),
    randomMagicAttackValue: document.getElementById(
      "random-magic-attack-value"
    ),
    yoharaCreation: document.getElementById("yohara-creation"),
    blessingCreation: document.getElementById("blessing-creation"),
    marriageCreation: document.getElementById("marriage-creation"),
    bonusVariation: {
      tab: document.getElementById("Variation"),
      activation: document.getElementById("bonus-variation-activation"),
      input: document.getElementById("bonus-variation"),
      inputDisplay: document.getElementById("bonus-variation-display"),
      container: document.getElementById("bonus-variation-range"),
      minValue: document.getElementById("bonus-variation-min-value"),
      maxValue: document.getElementById("bonus-variation-max-value"),
    },
  };

  characters.bonusVariation.inputDisplay.setAttribute("readonly", "");

  for (var [pseudo, character] of Object.entries(getSavedCharacters())) {
    characters.savedCharacters[pseudo] = character;
  }

  var skillContainer = document.getElementById("skill-container");
  characters.skillElementsToFilter =
    skillContainer.querySelectorAll("[data-class]");

  var mapping = createMapping();
  var constants = createConstants();

  var battle = {
    resetAttackType: false,
    savedFights: getSavedFights(),
    battleForm: document.getElementById("create-battle"),
    attackerSelection: document.getElementById("attacker-selection"),
    attackTypeSelection: document.getElementById("attack-type-selection"),
    victimSelection: document.getElementById("victim-selection"),
    damagesWeightedByType: {},
    scatterDataByType: {},
    damagesByBonus: [],
    tableResultFight: document.getElementById("result-table-fight"),
    tableResultHistory: document.getElementById("result-table-history"),
    deleteFightTemplate: document.getElementById("delete-fight-template")
      .children[0],
    errorInformation: {},
    fightResultContainer: document.getElementById("fight-result-container"),
    downLoadRawData: document.getElementById("download-raw-data"),
    downLoadRawDataVariation: document.getElementById(
      "download-raw-data-variation"
    ),
    bonusVariationResultContainer: document.getElementById(
      "bonus-variation-result-container"
    ),
    reduceChartPointsContainer: document.getElementById(
      "reduce-chart-points-container"
    ),
    reduceChartPoints: document.getElementById("reduce-chart-points"),
    plotDamages: document.getElementById("plot-damages"),
    plotBonusVariation: document.getElementById("plot-bonus-variation"),
    uniqueDamagesCounters: document.querySelectorAll(".unique-damages-counter"),
    possibleDamagesCounter: document.getElementById("possible-damages-counter"),
    damagesTime: document.getElementById("damages-time"),
    displayTime: document.getElementById("display-time"),
    simulationCounter: document.getElementById("simulation-counter"),
    simulationTime: document.getElementById("simulation-time"),
    numberFormats: {
      default: new Intl.NumberFormat(undefined, {
        minimumFractionDigits: 0,
        maximumFractionDigits: 1,
      }),
      percent: new Intl.NumberFormat(undefined, {
        style: "percent",
        maximumFractionDigits: 3,
      }),
      second: new Intl.NumberFormat(undefined, {
        style: "unit",
        unit: "second",
        unitDisplay: "long",
        maximumFractionDigits: 3,
      }),
    },
    mapping: mapping,
    constants: constants,
    translation: getTranslation(constants.translation),
  };

  attackSelectonListener(
    characters,
    battle.attackerSelection,
    battle.attackTypeSelection
  );
  initResultTableHistory(battle);
  loadScript(chartSource, function () {
    initDamagesChart(battle);
    initBonusVariationChart(battle);
  });
  reduceChartPointsListener(battle);
  downloadRawDataListener(battle);

  var errorElements = document.querySelectorAll("[data-error]");

  for (var index = 0; index < errorElements.length; index++) {
    var errorElement = errorElements[index];
    battle.errorInformation[errorElement.dataset.error] = errorElement;
  }

  return [characters, battle];
}

function loadScript(src, callback) {
  var script = document.createElement("script");
  script.src = src;

  function onComplete() {
    if (script.parentNode) {
      script.parentNode.removeChild(script);
    }
    if (callback) {
      callback();
    }
  }

  document.head.appendChild(script);

  script.onload = onComplete;
  script.onerror = onComplete;
}

function loadStyle(src) {
  var link = document.createElement("link");
  link.href = src;
  link.rel = "stylesheet";

  document.head.appendChild(link);
}

function loading() {
  var mainContainer = document.getElementById("hide-all");
  var loadingAnimation = document.getElementById("loading-animation");

  mainContainer.classList.remove("tabber-noactive");
  loadingAnimation.classList.add("tabber-noactive");
}

(function () {
  var javascriptSource =
    "/index.php?title=Utilisateur:Ankhseram/Calculator.js&action=raw&ctype=text/javascript";
  var cssSource =
    "/index.php?title=Utilisateur:Ankhseram/Style.css&action=raw&ctype=text/css";
  var chartSource = "https://cdn.jsdelivr.net/npm/chart.js";

  loadStyle(cssSource);

  function main() {
    var [characters, battle] = createDamageCalculatorInformation(chartSource);

    characterManagement(characters, battle);
    monsterManagement(characters, battle);

    updateBattleChoice(characters, battle);
    createBattle(characters, battle);

    loading();
  }
  loadScript(javascriptSource, main);
})();