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

(v0.19)
 
(17 révisions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
function showElement(element) {
 
  element.classList.remove("tabber-noactive");
 
}
 
 
function hideElement(element) {
 
  element.classList.add("tabber-noactive");
 
}
 
 
 
function removeAccent(str) {
 
function removeAccent(str) {
   return str
+
   return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
    .normalize("NFD")
 
    .replace(/[\u0300-\u036f]/g, "")
 
    .toLowerCase();
 
 
}
 
}
  
function toNormalForm(str) {
+
function pseudoFormat(str) {
   return removeAccent(str).replace(/[^a-zA-Z0-9 ]/g, "");
+
   return removeAccent(str).replace(/[^A-Za-z0-9 \(\)\+_-]+/g, "");
 
}
 
}
  
 
function isValueInArray(value, array) {
 
function isValueInArray(value, array) {
 
   return array.indexOf(value) !== -1;
 
   return array.indexOf(value) !== -1;
 +
}
 +
 +
function splitFirst(value, delimiter) {
 +
  var parts = value.split(delimiter);
 +
  var first = parts[0];
 +
  var rest = parts.slice(1).join(delimiter);
 +
  return [first, rest];
 
}
 
}
  
Ligne 32 : Ligne 28 :
 
function compareNumbers(a, b) {
 
function compareNumbers(a, b) {
 
   return b - a;
 
   return b - a;
 +
}
 +
 +
function isChecked(attribute) {
 +
  return attribute === "on";
 
}
 
}
  
Ligne 38 : Ligne 38 :
 
}
 
}
  
function floorMultiplicationWithNegative(firstFactor, secondFactor) {
+
function truncateNumber(number, precision) {
   if (secondFactor < 0) {
+
   return Math.floor(number * 10 ** precision) / 10 ** precision;
    return -floorMultiplication(firstFactor, -secondFactor);
 
  } else {
 
    return floorMultiplication(firstFactor, secondFactor);
 
  }
 
 
}
 
}
  
function numberDisplay(number, precision) {
+
function newChangeEvent() {
   return (Math.round(number * 10 ** precision) / 10 ** precision)
+
   return new Event("change", { bubbles: true });
    .toString()
 
    .replace(".", ",");
 
 
}
 
}
  
Ligne 60 : Ligne 54 :
 
}
 
}
  
function addRowToTableResult(tableResult, value) {
+
function openTargetTab(target) {
   var newRow = tableResult.insertRow(-1);
+
   var tabberContainer = target.closest(".tabber-container");
  var firstCell = newRow.insertCell(0);
 
  
   firstCell.textContent = value;
+
   if (!tabberContainer) {
   firstCell.colSpan = 2;
+
    return;
 +
   }
  
   newRow.style.fontWeight = "bold";
+
   var [buttonsContainer, tabsContainer] = tabberContainer.children;
}
+
  var buttons = buttonsContainer.children;
 +
  var tabs = tabsContainer.children;
  
function addToTableResult(tableResult, damagesWeighted, minMaxDamages) {
+
  for (var index = 0; index < tabs.length; index++) {
  var firstIteration = true;
+
    var tab = tabs[index];
 
+
     if (tab.contains(target) && !tab.checkVisibility()) {
  for (var damages in damagesWeighted) {
+
       buttons[index].click();
     if (firstIteration && minMaxDamages) {
+
       break;
       damages = parseInt(damages);
 
      if (damages < minMaxDamages.min) {
 
        minMaxDamages.min = damages;
 
       }
 
      firstIteration = false;
 
 
     }
 
     }
 +
  }
 +
}
  
    var newRow = tableResult.insertRow(-1);
+
function openTargetCollapsible(target) {
    var firstCell = newRow.insertCell(0);
+
  var collapsible = target.closest(".improved-collapsible");
  
     firstCell.textContent = damages;
+
  if (!collapsible) {
 +
     return;
 +
  }
  
    var secondCell = newRow.insertCell(1);
+
  var span = collapsible.firstElementChild;
    secondCell.textContent =
 
      numberDisplay(damagesWeighted[damages] * 100, 3) + " %";
 
  }
 
  
  damages = parseInt(damages);
+
   if (!span.classList.contains("mw-collapsible-toggle-expanded")) {
   if (minMaxDamages && damages > minMaxDamages.max) {
+
     span.click();
     minMaxDamages.max = damages;
 
 
   }
 
   }
 
}
 
}
  
function clearTableResult(tableResult) {
+
function editTableResultRow(row, valuesToDisplay, numberFormat) {
   var tableHeaderRowCount = 1;
+
   row.innerHTML = "";
   var rowCount = tableResult.rows.length;
+
   for (var index = 0; index < valuesToDisplay.length; index++) {
 +
    var cell = row.insertCell();
 +
    var textContent;
  
  for (var rowIndex = tableHeaderRowCount; rowIndex < rowCount; rowIndex++) {
+
    if (index >= 3) {
    tableResult.deleteRow(tableHeaderRowCount);
+
      textContent = numberFormat.format(valuesToDisplay[index]);
 +
    } else {
 +
      textContent = valuesToDisplay[index];
 +
    }
 +
 
 +
    cell.textContent = textContent;
 
   }
 
   }
 +
 +
  return row;
 
}
 
}
  
function getMonsterName(monsterVnum) {
+
function addRowToTableResultHistory(
   var monsterAttributes = monsterData[monsterVnum];
+
  tableResultHistory,
   return monsterAttributes[monsterAttributes.length - 1];
+
  valuesToDisplay,
 +
  deleteFightTemplate,
 +
  numberFormat
 +
) {
 +
   var row = tableResultHistory.insertRow();
 +
  editTableResultRow(row, valuesToDisplay, numberFormat);
 +
  var cell = row.insertCell();
 +
   cell.appendChild(deleteFightTemplate.cloneNode(true));
 
}
 
}
  
function filterClass(selectedRace, classChoice, selectValueIsChanged = false) {
+
function calcMeanDamage(damageWeightedByType, totalCardinal) {
   showElement(classChoice.parentElement);
+
   var sumDamage = 0;
 +
 
 +
  for (var damageTypeName in damageWeightedByType) {
 +
    if (damageTypeName === "miss") {
 +
      continue;
 +
    }
 +
 
 +
    var damageWeighted = damageWeightedByType[damageTypeName];
  
  for (var option of classChoice.options) {
+
    for (var damage in damageWeighted) {
    if (option.getAttribute("data-race") === selectedRace) {
+
       sumDamage += damage * damageWeighted[damage];
       if (!selectValueIsChanged) {
 
        classChoice.value = option.value;
 
        selectValueIsChanged = true;
 
      }
 
      showElement(option);
 
    } else {
 
      hideElement(option);
 
 
     }
 
     }
 
   }
 
   }
   if (selectedRace == "lycan") {
+
 
    hideElement(classChoice.parentElement);
+
   return sumDamage / totalCardinal;
  }
 
 
}
 
}
  
function filterWeapon(
+
function prepareDamageData(damageWeightedByType, attackValues) {
   selectedRace,
+
   var totalCardinal = attackValues.totalCardinal;
   weapon,
+
  var minDamage = Infinity;
   weaponCategory,
+
  var maxDamage = 0;
   selectValueIsChanged = false
+
  var scatterDataByType = {};
) {
+
  var sumDamage = 0;
  var allowedWeaponsPerRace = {
+
  var possibleDamageCount = 0;
     warrior: [0, 3, 8],
+
   var possibleDamageCountTemp = 0;
     ninja: [0, 1, 2, 8],
+
   var uniqueDamageCount = 0;
     sura: [0, 7, 8],
+
 
     shaman: [4, 6, 8],
+
   for (var damageTypeName in damageWeightedByType) {
     lycan: [5, 8],
+
    if (damageTypeName === "miss") {
  };
+
      scatterDataByType.miss = damageWeightedByType.miss;
  var allowedWeapons = allowedWeaponsPerRace[selectedRace];
+
      possibleDamageCount++;
 +
      uniqueDamageCount++;
 +
      continue;
 +
    } else if (
 +
      (damageTypeName === "criticalHit" ||
 +
        damageTypeName === "criticalPiercingHit") &&
 +
      attackValues.isPlayerVsPlayer
 +
    ) {
 +
      possibleDamageCountTemp =
 +
        attackValues.possibleDamageCount * attackValues.weapon.interval;
 +
    } else {
 +
      possibleDamageCountTemp = attackValues.possibleDamageCount;
 +
    }
 +
 
 +
     var firstIteration = true;
 +
     var damageWeighted = damageWeightedByType[damageTypeName];
 +
     var scatterData = [];
 +
     scatterDataByType[damageTypeName] = scatterData;
 +
 
 +
     for (var damage in damageWeighted) {
 +
      damage = +damage;
 +
 
 +
      if (firstIteration) {
 +
        if (damage < minDamage) {
 +
          minDamage = damage;
 +
        }
 +
        firstIteration = false;
 +
      }
  
  if (!selectValueIsChanged) {
+
      var weight = damageWeighted[damage];
    var weaponType = weaponData[weapon.value][1];
+
      var probability = weight / totalCardinal;
  
    if (!isValueInArray(weaponType, allowedWeapons)) {
+
      sumDamage += damage * weight;
      weapon.value = 0;
+
      damageWeighted[damage] = probability;
 +
      scatterData.push({ x: damage, y: probability });
 
     }
 
     }
  }
 
  
  var children = weaponCategory.children;
+
    var scatterDataLength = scatterData.length;
  
  for (var index = 0; index < children.length; index++) {
+
    possibleDamageCount += possibleDamageCountTemp;
     var child = children[index];
+
     uniqueDamageCount += scatterDataLength;
  
     if (isValueInArray(index, allowedWeapons)) {
+
     if (damage > maxDamage) {
       showElement(child);
+
       maxDamage = damage;
    } else {
 
      hideElement(child);
 
 
     }
 
     }
 
   }
 
   }
}
 
  
function getSelectedWeapon(weaponCategory) {
+
  if (minDamage === Infinity) {
   return weaponCategory.querySelector("input[type='radio']:checked");
+
    minDamage = 0;
}
+
   }
 +
 
 +
  attackValues.possibleDamageCount = possibleDamageCount;
  
function handleWeaponDisplay(weaponDisplay, newWeapon, weaponValue) {
+
  return [
   var newImage = newWeapon.nextElementSibling.cloneNode();
+
    sumDamage / totalCardinal,
  var newText = document.createElement("span");
+
    minDamage,
  var oldImage = weaponDisplay.firstChild;
+
    maxDamage,
  var oldText = oldImage.nextElementSibling;
+
    scatterDataByType,
  var weaponName = weaponData[weaponValue][0];
+
    uniqueDamageCount,
 +
  ];
 +
}
 +
 
 +
function aggregateDamage(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 aggregateDamage = 0;
 +
    var aggregateProbability = 0;
 +
 
 +
    for (var index = startIndex; index < startIndex + groupLength; index++) {
 +
      var { x: damage, y: probability } = scatterData[index];
 +
      aggregateDamage += damage * probability;
 +
      aggregateProbability += probability;
 +
    }
  
  if (weaponValue == 0) {
+
    aggregateScatterData.push({
    newText.textContent = " " + weaponName + " ";
+
      x: aggregateDamage / aggregateProbability,
  } else {
+
      y: aggregateProbability,
     var weaponLink = document.createElement("a");
+
     });
    weaponLink.href = mw.util.getUrl(weaponName);
 
    weaponLink.title = weaponName;
 
    weaponLink.textContent = weaponName;
 
  
     newText.appendChild(document.createTextNode(" "));
+
     remainingData -= groupLength;
    newText.appendChild(weaponLink);
 
    newText.appendChild(document.createTextNode(" "));
 
 
   }
 
   }
  
   weaponDisplay.replaceChild(newImage, oldImage);
+
   return aggregateScatterData;
  weaponDisplay.replaceChild(newText, oldText);
 
 
}
 
}
  
function filterUpgrade(
+
function addToDamageChart(
   selectedRace,
+
   scatterDataByType,
   weaponUpgrade,
+
   damageChart,
   weaponValue,
+
   isReducePointsChecked
  randomAttackValue,
 
  randomMagicAttackValue,
 
  currentUpgrade
 
 
) {
 
) {
   var weapon = weaponData[weaponValue];
+
   var { chart, datasetsStyle, maxPoints, reduceChartPointsContainer } =
 +
    damageChart;
 +
  var isFirstDataset = true;
 +
  var datasets = chart.data.datasets;
 +
 
 +
  datasets.length = 0;
  
   if (isValueInArray("serpent", weapon[0].toLowerCase())) {
+
   for (var index = 0; index < datasetsStyle.length; index++) {
     showElement(randomAttackValue);
+
     var dataset = copyObject(datasetsStyle[index]);
  
     if (selectedRace === "sura" || selectedRace === "shaman") {
+
     if (!scatterDataByType.hasOwnProperty(dataset.name)) {
       showElement(randomMagicAttackValue);
+
       continue;
 
     }
 
     }
  } else {
 
    hideElement(randomAttackValue);
 
    hideElement(randomMagicAttackValue);
 
  }
 
 
  var upgradeNumber = weapon[3].length;
 
  
  if (upgradeNumber <= 1) {
+
    var scatterData = scatterDataByType[dataset.name];
    hideElement(weaponUpgrade.parentElement);
+
     var canBeReduced = scatterData.length > 2 * maxPoints;
  } else {
 
     showElement(weaponUpgrade.parentElement);
 
  
     weaponUpgrade.innerHTML = "";
+
     dataset.hidden = !isFirstDataset;
 +
    dataset.canBeReduced = canBeReduced;
  
     for (var upgrade = 0; upgrade < upgradeNumber; upgrade++) {
+
     if (canBeReduced && isReducePointsChecked) {
       var option = document.createElement("option");
+
       dataset.data = aggregateDamage(scatterData, maxPoints);
      option.value = upgrade;
 
      option.textContent = "+" + upgrade;
 
      weaponUpgrade.appendChild(option);
 
    }
 
    if (currentUpgrade === undefined) {
 
      option.selected = true;
 
 
     } else {
 
     } else {
       weaponUpgrade.value = currentUpgrade;
+
       dataset.data = scatterData;
      currentUpgrade = undefined;
 
 
     }
 
     }
  }
 
}
 
  
function filterState(selectedState, polymorphMonster) {
+
    if (isFirstDataset) {
  if (selectedState === "polymorph") {
+
      isFirstDataset = false;
    showElement(polymorphMonster.parentElement);
+
 
  } else {
+
      if (canBeReduced) {
    hideElement(polymorphMonster.parentElement);
+
        showElement(reduceChartPointsContainer);
  }
 
}
 
  
function filterCheckbox(checkbox, element) {
+
        if (!isReducePointsChecked) {
  if (checkbox.checked) {
+
          handleChartAnimations(chart, false);
    showElement(element);
+
        }
  } else {
+
      } else {
     hideElement(element);
+
        hideElement(reduceChartPointsContainer);
 +
        handleChartAnimations(chart, true);
 +
      }
 +
     }
 +
 
 +
    datasets.push(dataset);
 
   }
 
   }
 +
 +
  chart.data.missPercentage = scatterDataByType.miss;
 +
  chart.update();
 
}
 
}
  
function filterSkills(selectedClass, skillElementsToFilter) {
+
function addToBonusVariationChart(
   for (var element of skillElementsToFilter) {
+
  damageByBonus,
     if (isValueInArray(selectedClass, element.dataset.class)) {
+
  augmentationByBonus,
       showElement(element);
+
  xLabel,
 +
  chart
 +
) {
 +
   chart.data.datasets[0].data = damageByBonus;
 +
  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 updateDamageChartDescription(
 +
  uniqueDamageCounters,
 +
  uniqueDamageCount,
 +
  formatNumber
 +
) {
 +
  uniqueDamageCounters.forEach(function (element) {
 +
     if (uniqueDamageCount <= 1) {
 +
       hideElement(element.parentElement);
 
     } else {
 
     } else {
       hideElement(element);
+
       showElement(element.parentElement);
 +
      element.textContent = formatNumber.format(uniqueDamageCount);
 
     }
 
     }
   }
+
   });
 
}
 
}
  
function filterAttackTypeSelection(attacker, attackTypeSelection) {
+
function getMonsterName(monsterVnum) {
   var attackerClass = attacker.class;
+
   var monsterAttributes = monsterData[monsterVnum];
   var selectedOption =
+
   return monsterAttributes[monsterAttributes.length - 1];
    attackTypeSelection.options[attackTypeSelection.selectedIndex];
+
}
  
   for (var index = 2; index < attackTypeSelection.options.length; index++) {
+
function filterClass(selectedRace, classChoice, selectValueIsChanged = false) {
     var option = attackTypeSelection.options[index];
+
   for (var radioNode of classChoice) {
    var optionClass = option.dataset.class;
+
     var radioGrandParent = radioNode.parentElement.parentElement;
    var optionValue = option.value;
 
  
     if (
+
     if (radioNode.getAttribute("data-race") === selectedRace) {
      (optionClass &&
+
       if (!selectValueIsChanged) {
        optionClass === attackerClass &&
+
         radioNode.checked = true;
        attacker[optionValue] &&
+
         selectValueIsChanged = true;
        !isPolymorph(attacker)) ||
+
      }
       (optionValue.startsWith("horseSkill") &&
+
       showElement(radioGrandParent);
         isRiding(attacker) &&
 
         attacker[optionValue])
 
    ) {
 
       showElement(option);
 
 
     } else {
 
     } else {
       hideElement(option);
+
       hideElement(radioGrandParent);
 
 
      if (selectedOption === option) {
 
        attackTypeSelection.selectedIndex = 0;
 
      }
 
 
     }
 
     }
 
   }
 
   }
 
}
 
}
  
function filterAttackTypeSelectionMonster(attackTypeSelection) {
+
function filterWeapon(
   for (var index = 2; index < attackTypeSelection.options.length; index++) {
+
  selectedRace,
    hideElement(attackTypeSelection.options[index]);
+
  weaponElement,
 +
  weaponCategory,
 +
  allowedWeaponsPerRace,
 +
  selectValueIsChanged = false
 +
) {
 +
   var allowedWeapons = allowedWeaponsPerRace[selectedRace];
 +
 
 +
  if (!selectValueIsChanged) {
 +
    var weaponType = createWeapon(weaponElement.value).type;
 +
 
 +
    if (!isValueInArray(weaponType, allowedWeapons)) {
 +
      weaponElement.value = 0;
 +
    }
 
   }
 
   }
  
   if (attackTypeSelection.selectedIndex !== 1) {
+
   var children = weaponCategory.children;
     attackTypeSelection.selectedIndex = 0;
+
 
 +
  for (var index = 0; index < children.length; index++) {
 +
    var child = children[index];
 +
 
 +
    if (isValueInArray(index, allowedWeapons)) {
 +
      showElement(child);
 +
     } else {
 +
      hideElement(child);
 +
    }
 
   }
 
   }
 
}
 
}
  
function filterForm(characters, battle) {
+
function changePolymorphValues(characterCreation, monsterVnum, monsterImage) {
   var characterCreation = characters.characterCreation;
+
   var { polymorphMonster, polymorphMonsterImage } = characterCreation;
 +
 
 +
  polymorphMonster.value = monsterVnum;
 +
  polymorphMonsterImage.value = monsterImage;
  
   characterCreation.addEventListener("change", function (event) {
+
   polymorphMonster.dispatchEvent(newChangeEvent());
    var target = event.target;
+
}
    var targetName = target.name;
 
  
    switch (targetName) {
+
function resetImageFromWiki(image) {
      case "race":
+
  image.removeAttribute("srcset");
        var selectedRace = target.value;
+
  image.removeAttribute("data-file-width");
        var classChoice = characterCreation.class;
+
  image.removeAttribute("data-file-height");
        var weapon = characterCreation.weapon;
+
}
  
        filterClass(selectedRace, classChoice);
+
function handleImageFromWiki(image, newSrc) {
        filterWeapon(selectedRace, weapon, characters.weaponCategory);
+
  image.src = newSrc;
 +
  image.alt = newSrc.split("/").pop();
 +
}
  
        var newWeapon = getSelectedWeapon(characters.weaponCategory);
+
function handlePolymorphDisplay(polymorphDisplay, monsterVnum, monsterSrc) {
        handleWeaponDisplay(characters.weaponDisplay, newWeapon, weapon.value);
+
  var oldImage = polymorphDisplay.firstChild;
        filterUpgrade(
+
  var oldLink = oldImage.nextElementSibling;
          selectedRace,
+
  var monsterName = getMonsterName(monsterVnum);
          characterCreation.weaponUpgrade,
+
  var newLink = createWikiLink(monsterName);
          weapon.value,
 
          characters.randomAttackValue,
 
          characters.randomMagicAttackValue
 
        );
 
        filterSkills(classChoice.value, characters.skillElementsToFilter);
 
  
        if (characterCreation.name.value === battle.attackerSelection.value) {
+
  resetImageFromWiki(oldImage);
          battle.resetAttackType = true;
+
  handleImageFromWiki(oldImage, monsterSrc);
        }
 
        break;
 
      case "class":
 
        filterSkills(target.value, characters.skillElementsToFilter);
 
  
        if (characterCreation.name.value === battle.attackerSelection.value) {
+
  polymorphDisplay.replaceChild(newLink, oldLink);
          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 (
+
function createWikiLink(pageName) {
      targetName.startsWith("attackSkill") ||
+
  var wikiLink = document.createElement("a");
      targetName.startsWith("horseSkill")
 
    ) {
 
      battle.resetAttackType = true;
 
    }
 
  });
 
}
 
  
function getSavedCharacters() {
+
  wikiLink.href = mw.util.getUrl(pageName);
   var savedCharacters = localStorage.getItem("savedCharactersCalculator");
+
   wikiLink.title = pageName;
 +
  wikiLink.textContent = pageName;
  
   if (savedCharacters) {
+
   return wikiLink;
    return JSON.parse(savedCharacters);
 
  }
 
  return {};
 
 
}
 
}
  
function getSavedMonsters() {
+
function getSelectedWeapon(weaponCategory) {
   var savedMonsters = localStorage.getItem("savedMonstersCalculator");
+
   return weaponCategory.querySelector("input[type='radio']:checked");
 
 
  if (savedMonsters) {
 
    return JSON.parse(savedMonsters).filter(function (num) {
 
      return !isNaN(Number(num));
 
    });
 
  }
 
  return [];
 
 
}
 
}
  
function addUniquePseudo(characterDataObject, savedCharactersPseudo) {
+
function handleWeaponDisplay(
   var characterPseudo = characterDataObject.name;
+
  weaponCategory,
   var originalPseudo = characterPseudo;
+
  weaponDisplay,
   var count = 0;
+
   weaponVnum,
 +
   newWeapon
 +
) {
 +
   var newWeapon = newWeapon || getSelectedWeapon(weaponCategory);
  
   var regex = /(.*)(\d)$/;
+
   var newImage = newWeapon.nextElementSibling;
   var match = characterPseudo.match(regex);
+
  var newText = document.createElement("span");
 +
   var oldImage = weaponDisplay.firstChild;
 +
  var oldText = oldImage.nextElementSibling;
 +
  var weaponName = newImage.nextElementSibling.dataset.o;
  
   if (match) {
+
   if (weaponVnum == 0) {
     originalPseudo = match[1];
+
     newText.textContent = weaponName;
     count = match[2];
+
  } else {
 +
     var weaponLink = createWikiLink(weaponName);
 +
    newText.appendChild(weaponLink);
 
   }
 
   }
  
   while (isValueInArray(characterPseudo, savedCharactersPseudo)) {
+
   weaponDisplay.replaceChild(newImage.cloneNode(), oldImage);
    characterPseudo = originalPseudo + count;
+
   weaponDisplay.replaceChild(newText, oldText);
    count++;
 
   }
 
 
 
  characterDataObject.name = characterPseudo;
 
  return [characterDataObject, characterPseudo];
 
 
}
 
}
  
function convertToNumber(value) {
+
function filterUpgrade(
   var valueNumber = Number(value);
+
  weaponUpgrade,
   return isNaN(valueNumber) ? value : valueNumber;
+
  weaponVnum,
}
+
  randomAttackValue,
 +
  randomMagicAttackValue
 +
) {
 +
   var weapon = createWeapon(weaponVnum);
 +
   var currentUpgrade = Number(weaponUpgrade.value);
 +
  var weaponUpgradeChildren = weaponUpgrade.children;
  
function updateSavedCharacters(savedCharacters) {
+
  if (weapon.isSerpent) {
  localStorage.setItem(
+
     showElement(randomAttackValue);
     "savedCharactersCalculator",
 
    JSON.stringify(savedCharacters)
 
  );
 
}
 
  
function updateSavedMonsters(savedMonsters) {
+
    if (weapon.isMagic) {
  localStorage.setItem(
+
      showElement(randomMagicAttackValue);
     "savedMonstersCalculator",
+
     }
     JSON.stringify(savedMonsters)
+
  } else {
  );
+
     hideElement(randomAttackValue);
}
+
    hideElement(randomMagicAttackValue);
 +
  }
  
function saveCharacter(
+
  if (weapon.maxUpgrade < 1) {
  savedCharacters,
+
    hideElement(weaponUpgrade.parentElement);
  characterCreation,
+
  } else {
  battle,
+
     showElement(weaponUpgrade.parentElement);
  newCharacter,
+
  }
  characterDataObject
 
) {
 
  if (!characterDataObject) {
 
     var characterData = new FormData(characterCreation);
 
    var characterDataObject = {};
 
  
    characterData.forEach(function (value, key) {
+
  for (var upgrade = 0; upgrade <= weapon.maxUpgrade; upgrade++) {
      characterDataObject[key] = convertToNumber(value);
+
    showElement(weaponUpgradeChildren[upgrade]);
    });
 
 
   }
 
   }
  
   savedCharacters[characterDataObject.name] = characterDataObject;
+
   for (
  updateSavedCharacters(savedCharacters);
+
    var upgrade = weapon.maxUpgrade + 1;
 
+
    upgrade < weaponUpgradeChildren.length;
   if (newCharacter) {
+
    upgrade++
     addBattleChoice(battle, characterDataObject.name);
+
   ) {
 +
     hideElement(weaponUpgradeChildren[upgrade]);
 
   }
 
   }
  
   if (battle.resetAttackType) {
+
   if (currentUpgrade > weapon.maxUpgrade) {
     filterAttackTypeSelection(characterDataObject, battle.attackTypeSelection);
+
     weaponUpgrade.value = weapon.maxUpgrade;
    battle.resetAttackType = false;
 
 
   }
 
   }
 
}
 
}
  
function saveButtonGreen(saveButton) {
+
function filterCheckbox(checkbox, element) {
   saveButton.classList.remove("unsaved-character");
+
   if (checkbox.checked) {
 +
    showElement(element);
 +
  } else {
 +
    hideElement(element);
 +
  }
 
}
 
}
  
function saveButtonOrange(saveButton) {
+
function filterSkills(selectedClass, skillElementsToFilter) {
   saveButton.classList.add("unsaved-character");
+
   for (var element of skillElementsToFilter) {
 +
    if (isValueInArray(selectedClass, element.dataset.class)) {
 +
      showElement(element);
 +
    } else {
 +
      hideElement(element);
 +
    }
 +
  }
 
}
 
}
  
function characterCreationListener(characters, battle) {
+
function filterForm(characters, battle) {
   characters.characterCreation.addEventListener("submit", function (event) {
+
   var { saveButton, characterCreation, toggleSiblings } = characters;
     event.preventDefault();
+
  var allowedWeaponsPerRace = battle.constants.allowedWeaponsPerRace;
 +
  var battleChoice = battle.battleChoice;
 +
 
 +
  characterCreation.addEventListener("change", function (event) {
 +
     var target = event.target;
 +
    var targetName = target.name;
  
     if (characters.unsavedChanges) {
+
     saveButtonOrange(saveButton);
      saveCharacter(
+
    characters.unsavedChanges = true;
        characters.savedCharacters,
 
        characters.characterCreation,
 
        battle
 
      );
 
      saveButtonGreen(characters.saveButton);
 
      characters.unsavedChanges = false;
 
    }
 
  });
 
  
  document.addEventListener("keydown", function (event) {
+
    switch (targetName) {
    if (event.ctrlKey && event.key === "s") {
+
      case "race":
      event.preventDefault();
+
        var selectedRace = target.value;
      characters.saveButton.click();
+
        var classChoice = characterCreation.class;
    }
+
        var weaponElement = characterCreation.weapon;
  });
+
 
}
+
        filterClass(selectedRace, classChoice);
 +
        filterWeapon(
 +
          selectedRace,
 +
          weaponElement,
 +
          characters.weaponCategory,
 +
          allowedWeaponsPerRace
 +
        );
 +
        handleWeaponDisplay(
 +
          characters.weaponCategory,
 +
          characters.weaponDisplay,
 +
          weaponElement.value
 +
        );
 +
        filterUpgrade(
 +
          characterCreation.weaponUpgrade,
 +
          weaponElement.value,
 +
          characters.randomAttackValue,
 +
          characters.randomMagicAttackValue
 +
        );
 +
        filterSkills(classChoice.value, characters.skillElementsToFilter);
 +
        handleBonusVariationUpdate(
 +
          characterCreation,
 +
          characters.bonusVariation,
 +
          true
 +
        );
  
function downloadCharacter(character) {
+
        battleChoice.resetAttackType = true;
  var content = JSON.stringify(character);
+
        break;
  var link = document.createElement("a");
+
      case "class":
  var blob = new Blob([content], { type: "text/plain" });
+
        filterSkills(target.value, characters.skillElementsToFilter);
  var blobURL = URL.createObjectURL(blob);
+
        handleBonusVariationUpdate(
 +
          characterCreation,
 +
          characters.bonusVariation,
 +
          true
 +
        );
  
  link.href = blobURL;
+
        battleChoice.resetAttackType = true;
  link.download = character.name + ".txt";
+
        break;
  link.click();
+
      case "weapon":
  URL.revokeObjectURL(blobURL);
+
        var weaponElement = characterCreation.weapon;
}
 
  
function uploadCharacter(
+
        handleWeaponDisplay(
  selectedFiles,
+
          characters.weaponCategory,
  characters,
+
          characters.weaponDisplay,
  characterTemplate,
+
          weaponElement.value,
  charactersContainer,
+
          target
  battle
+
        );
) {
+
        filterUpgrade(
  var selectFilesLength = selectedFiles.length;
+
          characterCreation.weaponUpgrade,
 +
          target.value,
 +
          characters.randomAttackValue,
 +
          characters.randomMagicAttackValue
 +
        );
 +
        handleBonusVariationUpdate(
 +
          characterCreation,
 +
          characters.bonusVariation
 +
        );
 +
        break;
 +
      case "isRiding":
 +
        battleChoice.resetAttackType = true;
 +
        break;
 +
      case "isPolymorph":
 +
        battleChoice.resetAttackType = true;
 +
        break;
 +
    }
  
  for (var fileIndex = 0; fileIndex < selectFilesLength; fileIndex++) {
+
    if (toggleSiblings.hasOwnProperty(targetName)) {
    var selectedFile = selectedFiles[fileIndex];
+
      filterCheckbox(target, toggleSiblings[targetName]);
 +
    }
  
     if (selectedFile.type === "text/plain") {
+
     if (
       var reader = new FileReader();
+
      targetName.startsWith("attackSkill") ||
      reader.onload = function (e) {
+
       targetName.startsWith("horseSkill")
        var fileContent = e.target.result;
+
    ) {
        try {
+
      battleChoice.resetAttackType = true;
          var characterDataObject = JSON.parse(fileContent);
+
    }
          var characterPseudo = characterDataObject.name;
+
  });
 +
}
  
          if (characterPseudo) {
+
function addUniquePseudo(characterDataObject, savedCharactersPseudo) {
            hideElement(characters.characterCreation);
+
  var characterPseudo = String(characterDataObject.name);
            characterPseudo = validPseudo(characterPseudo);
+
  var originalPseudo = characterPseudo;
            [characterDataObject, characterPseudo] = addUniquePseudo(
+
  var count = 0;
              characterDataObject,
+
 
              Object.keys(characters.savedCharacters)
+
  var regex = /(.*)(\d)$/;
            );
+
  var match = characterPseudo.match(regex);
            var selectedCharacter = handleNewCharacter(
 
              characters,
 
              characterTemplate,
 
              charactersContainer,
 
              battle,
 
              characterPseudo
 
            )[0];
 
  
            if (selectFilesLength === 1) {
+
  if (match) {
              updateForm(
+
    originalPseudo = match[1];
                characterDataObject,
+
    count = match[2];
                characters.characterCreation,
+
  }
                characters,
 
                selectedCharacter
 
              );
 
            }
 
  
            saveCharacter(
+
  while (isValueInArray(characterPseudo, savedCharactersPseudo)) {
              characters.savedCharacters,
+
    characterPseudo = originalPseudo + count;
              characters.characterCreation,
+
    count++;
              battle,
 
              true,
 
              characterDataObject
 
            );
 
          }
 
        } catch (error) {
 
          if (error.name === "TypeError") {
 
            // delete the character
 
          }
 
        }
 
      };
 
      reader.readAsText(selectedFile);
 
    }
 
 
   }
 
   }
 +
 +
  characterDataObject.name = characterPseudo;
 +
  return [characterDataObject, characterPseudo];
 
}
 
}
  
function handleUploadCharacter(
+
function convertToNumber(value) {
  characters,
+
   var valueNumber = Number(value);
  characterTemplate,
+
   return isNaN(valueNumber) ? value : valueNumber;
  charactersContainer,
+
}
  battle
 
) {
 
   var characterInput = characters.characterInput;
 
   var dropZone = characters.dropZone;
 
  
  characterInput.accept = ".txt";
+
function getLocalStorageValue(key, defaultValue) {
   characterInput.multiple = true;
+
   var storedValue = localStorage.getItem(key);
  dropZone.setAttribute("tabindex", "0");
 
  
   dropZone.addEventListener("click", function () {
+
   if (storedValue) {
     characterInput.click();
+
     return JSON.parse(storedValue);
   });
+
   }
  
   dropZone.addEventListener("dragover", function (event) {
+
   return defaultValue;
    event.preventDefault();
+
}
    dropZone.classList.add("drop-zone--dragover");
 
  });
 
  
  ["dragleave", "dragend"].forEach(function (type) {
+
function getSavedCharacters() {
    dropZone.addEventListener(type, function () {
+
  return getLocalStorageValue("savedCharactersCalculator", {});
      dropZone.classList.remove("drop-zone--dragover");
+
}
    });
 
  });
 
  
  dropZone.addEventListener("drop", function (event) {
+
function getSavedMonsters() {
    event.preventDefault();
+
  var savedMonsters = getLocalStorageValue("savedMonstersCalculator", {});
    uploadCharacter(
 
      event.dataTransfer.files,
 
      characters,
 
      characterTemplate,
 
      charactersContainer,
 
      battle
 
    );
 
    dropZone.classList.remove("drop-zone--dragover");
 
  });
 
  
   characterInput.addEventListener("change", function (event) {
+
   if (Array.isArray(savedMonsters)) {
     uploadCharacter(
+
     return {};
      event.target.files,
+
   }
      characters,
 
      characterTemplate,
 
      charactersContainer,
 
      battle
 
    );
 
   });
 
}
 
  
function deleteCharacter(characters, pseudo, element, battle) {
+
  var filteredMonsters = {};
  battle.battleForm.reset();
 
  delete characters.savedCharacters[pseudo];
 
  element.remove();
 
  
   updateSavedCharacters(characters.savedCharacters);
+
   for (var vnum in savedMonsters) {
   removeBattleChoice(battle, pseudo);
+
    if (
 +
      String(Number(vnum)) === vnum &&
 +
      savedMonsters[vnum].hasOwnProperty("category")
 +
    ) {
 +
      filteredMonsters[vnum] = savedMonsters[vnum];
 +
    }
 +
   }
  
   if (
+
   updateSavedMonsters(filteredMonsters);
    !Object.keys(characters.savedCharacters).length ||
+
 
    characters.characterCreation.name.value === pseudo
+
   return filteredMonsters;
   ) {
 
    saveButtonGreen(characters.saveButton);
 
    characters.unsavedChanges = false;
 
    hideElement(characters.characterCreation);
 
    showElement(characters.characterCreation.previousElementSibling);
 
  }
 
 
}
 
}
  
function deleteMonster(characters, monsterVnum, element, battle) {
+
function getSavedFights() {
   battle.battleForm.reset();
+
   return getLocalStorageValue("savedFightsCalculator", []);
  characters.savedMonsters.splice(
+
}
    characters.savedMonsters.indexOf(monsterVnum),
 
    1
 
  );
 
  
  if (element) {
+
function saveToLocalStorage(key, value) {
    element.remove();
+
  localStorage.setItem(key, JSON.stringify(value));
  }
+
}
  
  updateSavedMonsters(characters.savedMonsters);
+
function updateSavedCharacters(savedCharacters) {
   removeBattleChoice(battle, monsterVnum);
+
   saveToLocalStorage("savedCharactersCalculator", savedCharacters);
 
}
 
}
  
function handleStyle(characters, selectedElement) {
+
function updateSavedMonsters(savedMonsters) {
   var currentCharacter = characters.currentCharacter;
+
   saveToLocalStorage("savedMonstersCalculator", savedMonsters);
 +
}
  
  if (currentCharacter) {
+
function updateSavedFights(savedFights) {
    currentCharacter.classList.remove("selected-character");
+
   saveToLocalStorage("savedFightsCalculator", savedFights);
   }
 
 
 
  selectedElement.classList.add("selected-character");
 
  characters.currentCharacter = selectedElement;
 
 
}
 
}
  
function updateForm(formData, characterCreation, characters, selectedElement) {
+
function saveCharacter(
   saveButtonGreen(characters.saveButton);
+
  savedCharacters,
  hideElement(characterCreation.previousElementSibling);
+
  characterCreation,
  showElement(characterCreation);
+
  battle,
  handleStyle(characters, selectedElement);
+
  newCharacter,
 
+
  characterDataObject
   characterCreation.reset();
+
) {
 +
   if (!characterDataObject) {
 +
    var characterData = new FormData(characterCreation);
 +
    var characterDataObject = {};
 +
 
 +
    characterData.forEach(function (value, key) {
 +
      characterDataObject[key] = convertToNumber(value);
 +
    });
 +
  }
 +
 
 +
   var characterPseudo = characterDataObject.name;
 +
  var { battleChoice } = battle;
  
   for (var [name, value] of Object.entries(formData)) {
+
   savedCharacters[characterPseudo] = characterDataObject;
    var formElement = characterCreation[name];
+
  updateSavedCharacters(savedCharacters);
  
     if (!formElement) {
+
  if (newCharacter) {
       continue;
+
     addBattleChoice(battleChoice, characterPseudo, characterDataObject);
    }
+
  } else {
 +
    updateBattleChoiceImage(
 +
       battleChoice,
 +
      characterPseudo,
 +
      characterDataObject.race
 +
    );
 +
  }
  
    if (formElement.type === "checkbox") {
+
  if (battleChoice.resetAttackType) {
      if (value === "on") {
+
    if (isCharacterSelected(characterPseudo, battleChoice.attacker.selected)) {
         formElement.checked = true;
+
      filterAttackTypeSelectionCharacter(
       }
+
         characterDataObject,
    } else {
+
        battleChoice.attackType
      formElement.value = value;
+
       );
 
     }
 
     }
 +
    battleChoice.resetAttackType = false;
 
   }
 
   }
  var selectedRace = characterCreation.race.value;
+
}
  var classChoice = characterCreation.class;
 
  var weapon = characterCreation.weapon;
 
  
  filterClass(selectedRace, classChoice, true);
+
function saveButtonGreen(saveButton) {
   filterWeapon(selectedRace, weapon, characters.weaponCategory, true);
+
   saveButton.classList.remove("unsaved-character");
 +
}
  
  var newWeapon = getSelectedWeapon(characters.weaponCategory);
+
function saveButtonOrange(saveButton) {
 
+
   saveButton.classList.add("unsaved-character");
  handleWeaponDisplay(characters.weaponDisplay, newWeapon, weapon.value);
 
  filterUpgrade(
 
    selectedRace,
 
    characterCreation.weaponUpgrade,
 
    weapon.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);
 
 
}
 
}
  
function handleClickOnCharacter(
+
function characterCreationListener(characters, battle) {
  spanInput,
+
   var { characterCreation, saveButton, weaponCategory } = characters;
  target,
 
  characters,
 
  characterElement,
 
  battle,
 
  edition
 
) {
 
   var displayedPseudo = characters.characterCreation.name.value;
 
  var pseudo = spanInput.dataset.name;
 
  
   if (edition) {
+
   characterCreation.addEventListener("submit", handleSubmitForm);
    if (!characters.unsavedChanges) {
+
  characterCreation.addEventListener("invalid", handleInvalidInput, true);
      updateForm(
+
  document.addEventListener("keydown", handleSaveShortcut);
        characters.savedCharacters[pseudo],
+
  weaponCategory.addEventListener("mouseover", handleTooltipOverflow);
        characters.characterCreation,
 
        characters,
 
        characterElement
 
      );
 
    } else if (displayedPseudo === pseudo) {
 
      // pass
 
    } else {
 
      var result = confirm(
 
        "Voulez-vous continuer ? Les dernières modifications ne seront pas sauvegardées."
 
      );
 
  
      if (result) {
+
  function handleSubmitForm(event) {
        updateForm(
+
    event.preventDefault();
          characters.savedCharacters[pseudo],
+
 
          characters.characterCreation,
+
    if (characters.unsavedChanges) {
          characters,
+
      saveCharacter(characters.savedCharacters, characterCreation, battle);
          characterElement
+
      saveButtonGreen(saveButton);
        );
+
      characters.unsavedChanges = false;
        characters.unsavedChanges = false;
 
      }
 
 
     }
 
     }
   } else {
+
   }
     if (target.tagName === "path") {
+
 
       target = target.parentElement;
+
  function handleInvalidInput(event) {
 +
    var target = event.target;
 +
 
 +
     if (target.checkVisibility()) {
 +
       return;
 
     }
 
     }
  
     switch (target.dataset.icon) {
+
     var autoCorrectInput = target.closest(".auto-correct-input");
      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) {
+
    if (
            addNewCharacter(
+
      autoCorrectInput &&
              characters,
+
      autoCorrectInput.classList.contains("tabber-noactive")
              characters.newCharacterTemplate,
+
    ) {
              characters.charactersContainer,
+
      target.value = target.defaultValue;
              battle,
+
      return;
              pseudo
+
    }
            );
+
 
            saveButtonGreen(characters.saveButton);
+
    openTargetTab(target);
            characters.unsavedChanges = false;
+
    openTargetCollapsible(target);
          }
+
  }
        }
+
 
        break;
+
  function handleSaveShortcut(event) {
 +
    if (event.ctrlKey && event.key === "s") {
 +
      event.preventDefault();
 +
      saveButton.click();
 +
    }
 +
  }
 +
 
 +
  function handleTooltipOverflow(event) {
 +
    var label = event.target.closest("label");
 +
 
 +
    if (label) {
 +
      var tooltip = label.lastChild;
  
       case "download":
+
       if (tooltip.classList.contains("popContenu")) {
         downloadCharacter(characters.savedCharacters[pseudo]);
+
         var tooltipRect = tooltip.getBoundingClientRect();
         break;
+
         var modalRect = weaponCategory.getBoundingClientRect();
  
      case "delete":
+
         if (tooltipRect.right > modalRect.right) {
         var result = confirm(
+
           tooltip.style.left = "-100%";
           "Voulez-vous vraiment supprimer définitivement le personnage " +
+
         } else if (tooltipRect.left < modalRect.left) {
            pseudo +
+
           tooltip.style.left = "200%";
            " ?"
 
        );
 
         if (result) {
 
           deleteCharacter(characters, pseudo, characterElement, battle);
 
 
         }
 
         }
        break;
+
      }
 
     }
 
     }
 
   }
 
   }
 
}
 
}
  
function handleNewCharacter(
+
function downloadData(content, type, filename) {
   characters,
+
  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,
 
   characterTemplate,
 
   charactersContainer,
 
   charactersContainer,
   battle,
+
   battle
  pseudo
 
 
) {
 
) {
   var newCharacterTemplate = characterTemplate.cloneNode(true);
+
   var selectFilesLength = selectedFiles.length;
   var spanInput = newCharacterTemplate.querySelector("span.input");
+
 
 +
   for (var fileIndex = 0; fileIndex < selectFilesLength; fileIndex++) {
 +
    var selectedFile = selectedFiles[fileIndex];
  
  newCharacterTemplate.setAttribute("tabindex", "0");
+
    if (selectedFile.type === "text/plain") {
  charactersContainer.appendChild(newCharacterTemplate);
+
      var reader = new FileReader();
 +
      reader.onload = function (e) {
 +
        var fileContent = e.target.result;
 +
        try {
 +
          var characterDataObject = JSON.parse(fileContent);
  
  if (pseudo) {
+
          if (characterDataObject.hasOwnProperty("name")) {
    spanInput.textContent = pseudo;
+
            var characterPseudo = String(characterDataObject.name);
    spanInput.setAttribute("data-name", pseudo);
 
  }
 
  
  newCharacterTemplate.addEventListener("click", function (event) {
+
            hideElement(characters.characterCreation);
    var target = event.target;
+
            characterPseudo = validPseudo(characterPseudo);
  
    if (target.tagName === "path" || target.tagName === "svg") {
+
            [characterDataObject, characterPseudo] = addUniquePseudo(
      handleClickOnCharacter(
+
              characterDataObject,
        spanInput,
+
              Object.keys(characters.savedCharacters)
        target,
+
            );
        characters,
+
            var selectedCharacter = handleNewCharacter(
        newCharacterTemplate,
+
              characters,
        battle
+
              characterTemplate,
      );
+
              charactersContainer,
    } else {
+
              battle,
      handleClickOnCharacter(
+
              characterPseudo
        spanInput,
+
            )[0];
        null,
 
        characters,
 
        newCharacterTemplate,
 
        battle,
 
        true
 
      );
 
    }
 
  });
 
  
  newCharacterTemplate.addEventListener("keydown", function (event) {
+
            if (selectFilesLength === 1) {
    if (event.keyCode === 13) {
+
              updateForm(
      event.target.click();
+
                characterDataObject,
    }
+
                characters.characterCreation,
  });
+
                characters,
 +
                selectedCharacter,
 +
                battle
 +
              );
 +
            }
  
  return [newCharacterTemplate, spanInput];
+
            saveCharacter(
}
+
              characters.savedCharacters,
 
+
              characters.characterCreation,
function validPseudo(pseudo) {
+
              battle,
  var newPseudo = pseudo.replace(/[^A-Za-z0-9]+/g, "");
+
              true,
 
+
              characterDataObject
  if (!newPseudo) {
+
            );
     return "Pseudo";
+
          }
 +
        } catch (error) {
 +
          console.log(error);
 +
          if (error.name === "TypeError") {
 +
            // delete the character
 +
          }
 +
        }
 +
      };
 +
      reader.readAsText(selectedFile);
 +
     }
 
   }
 
   }
 
  return newPseudo;
 
 
}
 
}
  
function addNewCharacter(
+
function handleUploadCharacter(
 
   characters,
 
   characters,
 
   characterTemplate,
 
   characterTemplate,
 
   charactersContainer,
 
   charactersContainer,
   battle,
+
   battle
  pseudoToDuplicate
 
 
) {
 
) {
   function editAndSetCharacterPseudoInput(selectedCharacter, spanInput) {
+
   var characterInput = characters.characterInput;
    var maxPseudoLength = 15;
+
  var dropZone = characters.dropZone;
  
    var selection = window.getSelection();
+
  characterInput.accept = ".txt";
    var range = document.createRange();
+
  characterInput.multiple = true;
 +
  dropZone.setAttribute("tabindex", "0");
  
    if (pseudoToDuplicate) {
+
  dropZone.addEventListener("click", function () {
      spanInput.textContent = pseudoToDuplicate;
+
    characterInput.click();
    }
+
  });
  
    spanInput.contentEditable = true;
+
  dropZone.addEventListener("dragover", function (event) {
    spanInput.focus();
+
     event.preventDefault();
     range.selectNodeContents(spanInput);
+
     dropZone.classList.add("drop-zone--dragover");
     selection.removeAllRanges();
+
  });
    selection.addRange(range);
 
  
     function pseudoValidation() {
+
  ["dragleave", "dragend"].forEach(function (type) {
       var characterPseudo = validPseudo(spanInput.textContent);
+
     dropZone.addEventListener(type, function () {
      var characterDataObject = { name: characterPseudo };
+
       dropZone.classList.remove("drop-zone--dragover");
 +
    });
 +
  });
  
      if (pseudoToDuplicate) {
+
  dropZone.addEventListener("drop", function (event) {
        characterDataObject = copyObject(
+
    event.preventDefault();
          characters.savedCharacters[pseudoToDuplicate]
+
    uploadCharacter(
        );
+
      event.dataTransfer.files,
        characterDataObject.name = characterPseudo;
+
      characters,
      }
+
      characterTemplate,
 +
      charactersContainer,
 +
      battle
 +
    );
 +
    dropZone.classList.remove("drop-zone--dragover");
 +
  });
 +
 
 +
  characterInput.addEventListener("change", function (event) {
 +
    uploadCharacter(
 +
      event.target.files,
 +
      characters,
 +
      characterTemplate,
 +
      charactersContainer,
 +
      battle
 +
    );
 +
  });
 +
}
  
      [characterDataObject, characterPseudo] = addUniquePseudo(
+
function deleteCharacter(characters, pseudo, element, battle) {
        characterDataObject,
+
  delete characters.savedCharacters[pseudo];
        Object.keys(characters.savedCharacters)
+
  element.remove();
      );
 
  
      selection.removeAllRanges();
+
  updateSavedCharacters(characters.savedCharacters);
      spanInput.contentEditable = false;
+
  removeBattleChoice(battle.battleChoice, pseudo, "character");
      spanInput.textContent = characterPseudo;
 
      spanInput.setAttribute("data-name", characterPseudo);
 
  
      updateForm(
+
  if (
        characterDataObject,
+
    !Object.keys(characters.savedCharacters).length ||
        characters.characterCreation,
+
    characters.characterCreation.name.value === pseudo
        characters,
+
  ) {
        selectedCharacter
+
    saveButtonGreen(characters.saveButton);
      );
+
    characters.unsavedChanges = false;
      saveCharacter(
+
    hideElement(characters.characterCreation);
        characters.savedCharacters,
+
    showElement(characters.characterCreation.previousElementSibling);
        characters.characterCreation,
+
  }
        battle,
+
}
        true
 
      );
 
    }
 
  
    function handleMaxLength(event) {
+
function deleteMonster(characters, battle, monsterVnum) {
      if (spanInput.textContent.length > maxPseudoLength) {
+
  var monsterElements = characters.monsterElements;
        spanInput.textContent = spanInput.textContent.slice(0, maxPseudoLength);
+
  var monsterType = characters.savedMonsters[monsterVnum].category;
        range.setStart(spanInput.childNodes[0], maxPseudoLength);
 
        selection.removeAllRanges();
 
        selection.addRange(range);
 
      }
 
    }
 
  
    function handleBlur() {
+
  if (monsterElements.hasOwnProperty(monsterVnum)) {
      spanInput.removeEventListener("blur", handleBlur);
+
    monsterElements[monsterVnum].remove();
      spanInput.removeEventListener("input", handleMaxLength);
+
    delete monsterElements[monsterVnum];
      pseudoValidation();
+
  }
    }
 
  
    function handleKeyDown(event) {
+
  delete characters.savedMonsters[monsterVnum];
      if (event.key === "Enter") {
 
        event.preventDefault();
 
  
        spanInput.removeEventListener("keydown", handleKeyDown);
+
  updateSavedMonsters(characters.savedMonsters);
        spanInput.removeEventListener("blur", handleBlur);
+
  removeBattleChoice(battle.battleChoice, monsterVnum, monsterType);
        spanInput.removeEventListener("input", handleMaxLength);
+
}
  
        pseudoValidation();
+
function handleStyle(characters, selectedElement) {
      }
+
  var currentCharacter = characters.currentCharacter;
    }
 
  
    spanInput.addEventListener("input", handleMaxLength);
+
  if (currentCharacter) {
     spanInput.addEventListener("keydown", handleKeyDown);
+
     currentCharacter.classList.remove("selected-character");
    spanInput.addEventListener("blur", handleBlur);
 
 
   }
 
   }
  
   hideElement(characters.characterCreation);
+
   selectedElement.classList.add("selected-character");
   var [selectedCharacter, spanInput] = handleNewCharacter(
+
   characters.currentCharacter = selectedElement;
    characters,
 
    characterTemplate,
 
    charactersContainer,
 
    battle
 
  );
 
 
 
  editAndSetCharacterPseudoInput(selectedCharacter, spanInput);
 
 
}
 
}
  
function handleFocus() {
+
function updateForm(
   var tooltipLinks = document.querySelectorAll("div.tooltip a");
+
  formData,
   tooltipLinks.forEach(function (link) {
+
  characterCreation,
    link.setAttribute("tabindex", -1);
+
  characters,
   });
+
  selectedElement,
}
+
  battle
 +
) {
 +
   saveButtonGreen(characters.saveButton);
 +
   hideElement(characterCreation.previousElementSibling);
 +
  showElement(characterCreation);
 +
   handleStyle(characters, selectedElement);
  
function characterManagement(characters, battle) {
+
  characterCreation.reset();
  var characterTemplate = characters.newCharacterTemplate;
 
  var charactersContainer = characters.charactersContainer;
 
  
   Object.keys(characters.savedCharacters).forEach(function (pseudo) {
+
   for (var [name, value] of Object.entries(formData)) {
     handleNewCharacter(
+
     var formElement = characterCreation[name];
      characters,
 
      characterTemplate,
 
      charactersContainer,
 
      battle,
 
      pseudo
 
    );
 
  });
 
  
  characters.addNewCharacterButton.addEventListener("click", function (event) {
+
     if (!formElement) {
     if (!characters.unsavedChanges) {
+
       continue;
       addNewCharacter(
+
     }
        characters,
 
        characterTemplate,
 
        charactersContainer,
 
        battle
 
      );
 
     } else {
 
      var result = confirm(
 
        "Voulez-vous continuer ? Les dernières modifications ne seront pas sauvegardées."
 
      );
 
  
      if (result) {
+
    if (formElement.type === "checkbox") {
        addNewCharacter(
+
      if (isChecked(value)) {
          characters,
+
         formElement.checked = true;
          characterTemplate,
 
          charactersContainer,
 
          battle
 
        );
 
        saveButtonGreen(characters.saveButton);
 
         characters.unsavedChanges = false;
 
 
       }
 
       }
 +
    } else {
 +
      formElement.value = value;
 
     }
 
     }
   });
+
   }
 +
  var selectedRace = characterCreation.race.value;
 +
  var classChoice = characterCreation.class;
 +
  var weaponElement = characterCreation.weapon;
  
   handleUploadCharacter(
+
   filterClass(selectedRace, classChoice, true);
     characters,
+
  filterWeapon(
     characterTemplate,
+
    selectedRace,
     charactersContainer,
+
    weaponElement,
     battle
+
    characters.weaponCategory,
 +
    battle.constants.allowedWeaponsPerRace,
 +
    true
 +
  );
 +
  handlePolymorphDisplay(
 +
    characters.polymorphDisplay,
 +
    characterCreation.polymorphMonster.value,
 +
    characterCreation.polymorphMonsterImage.value
 +
  );
 +
  handleWeaponDisplay(
 +
     characters.weaponCategory,
 +
     characters.weaponDisplay,
 +
     weaponElement.value
 +
  );
 +
  filterUpgrade(
 +
    characterCreation.weaponUpgrade,
 +
    weaponElement.value,
 +
    characters.randomAttackValue,
 +
     characters.randomMagicAttackValue
 
   );
 
   );
 
+
   for (var [targetName, sibling] of Object.entries(characters.toggleSiblings)) {
   characters.characterCreation.addEventListener("change", function () {
+
     filterCheckbox(characterCreation[targetName], sibling);
     saveButtonOrange(characters.saveButton);
+
   }
    characters.unsavedChanges = true;
+
   filterSkills(classChoice.value, characters.skillElementsToFilter);
   });
+
   handleBonusVariationUpdate(characterCreation, characters.bonusVariation);
 
 
   filterForm(characters, battle);
 
  characterCreationListener(characters, battle);
 
   handleFocus();
 
 
 
  window.addEventListener("beforeunload", function (event) {
 
    if (characters.unsavedChanges) {
 
      event.preventDefault();
 
      event.returnValue = "";
 
      return "";
 
    }
 
  });
 
 
}
 
}
  
function handleNewMonster(
+
function handleClickOnCharacter(
 +
  spanInput,
 +
  target,
 
   characters,
 
   characters,
   monsterTemplate,
+
   characterElement,
  monstersContainer,
 
 
   battle,
 
   battle,
   monsterVnum,
+
   edition
  monsterList
 
 
) {
 
) {
   var newMonsterTemplate = monsterTemplate.cloneNode(true);
+
   var displayedPseudo = characters.characterCreation.name.value;
  var spanInput = newMonsterTemplate.querySelector("span.input");
+
   var pseudo = spanInput.dataset.name;
   var deleteSvg = newMonsterTemplate.querySelector("svg");
 
  var monsterName = getMonsterName(monsterVnum);
 
  
   var link = document.createElement("a");
+
   if (edition) {
  link.href = mw.util.getUrl(monsterName);
+
    if (!characters.unsavedChanges) {
  link.title = monsterName;
+
      updateForm(
  link.textContent = monsterName;
+
        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."
 +
      );
  
  spanInput.appendChild(link);
+
      if (result) {
   monstersContainer.appendChild(newMonsterTemplate);
+
        updateForm(
 +
          characters.savedCharacters[pseudo],
 +
          characters.characterCreation,
 +
          characters,
 +
          characterElement,
 +
          battle
 +
        );
 +
        characters.unsavedChanges = false;
 +
      }
 +
    }
 +
   } else {
 +
    if (target.tagName === "path") {
 +
      target = target.parentElement;
 +
    }
  
  newMonsterTemplate.setAttribute("tabindex", "0");
+
    switch (target.dataset.icon) {
  newMonsterTemplate.setAttribute("data-name", monsterVnum);
+
      case "duplicate":
  monstersContainer.appendChild(newMonsterTemplate);
+
        if (!characters.unsavedChanges) {
 
+
          addNewCharacter(
  deleteSvg.addEventListener("click", function (event) {
+
            characters,
    deleteMonster(characters, monsterVnum, newMonsterTemplate, battle);
+
            characters.newCharacterTemplate,
    var inputMonster = monsterList.querySelector(
+
            characters.charactersContainer,
      "input[name='" + monsterVnum + "']"
+
            battle,
    );
+
            pseudo
    inputMonster.checked = false;
+
          );
  });
+
        } else {
}
+
          var result = confirm(
 +
            "Voulez-vous continuer ? Les dernières modifications ne seront pas sauvegardées."
 +
          );
  
function monsterManagement(characters, battle) {
+
          if (result) {
  function handleDropdown(searchMonster, monsterList) {
+
            addNewCharacter(
    searchMonster.addEventListener("focus", function (event) {
+
              characters,
      showElement(monsterList);
+
              characters.newCharacterTemplate,
    });
+
              characters.charactersContainer,
 +
              battle,
 +
              pseudo
 +
            );
 +
            saveButtonGreen(characters.saveButton);
 +
            characters.unsavedChanges = false;
 +
          }
 +
        }
 +
        break;
  
    document.addEventListener("mousedown", function (event) {
+
      case "download":
      var target = event.target;
+
        var character = characters.savedCharacters[pseudo];
      if (!monsterList.contains(target) && !searchMonster.contains(target)) {
+
        downloadData(
        hideElement(monsterList);
+
          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 addMonsterNames(monsterList) {
+
function handleNewCharacter(
    var lastMonsterAttributeIndex = monsterData[101].length - 1;
+
  characters,
 +
  characterTemplate,
 +
  charactersContainer,
 +
  battle,
 +
  pseudo
 +
) {
 +
  var newCharacterTemplate = characterTemplate.cloneNode(true);
 +
  var spanInput = newCharacterTemplate.querySelector("span.input");
  
    for (var monsterVnum in monsterData) {
+
  newCharacterTemplate.setAttribute("tabindex", "0");
      var li = document.createElement("li");
+
  charactersContainer.appendChild(newCharacterTemplate);
      var label = document.createElement("label");
 
      var input = document.createElement("input");
 
      var textNode = document.createTextNode(
 
        monsterData[monsterVnum][lastMonsterAttributeIndex]
 
      );
 
  
      label.htmlFor = "monster" + monsterVnum;
+
  if (pseudo) {
      input.id = "monster" + monsterVnum;
+
    spanInput.textContent = pseudo;
      input.type = "checkbox";
+
    spanInput.setAttribute("data-name", pseudo);
 
 
      input.name = monsterVnum;
 
 
 
      label.appendChild(input);
 
      label.appendChild(textNode);
 
      li.appendChild(label);
 
      monsterList.appendChild(li);
 
    }
 
 
   }
 
   }
  
   function filterNames(searchMonster, monsterList) {
+
   newCharacterTemplate.addEventListener("click", function (event) {
     var debounceTimer;
+
     var target = event.target;
  
     searchMonster.addEventListener("input", function (event) {
+
     if (target.tagName === "path" || target.tagName === "svg") {
      clearTimeout(debounceTimer);
+
      handleClickOnCharacter(
      debounceTimer = setTimeout(function () {
+
        spanInput,
        var value = toNormalForm(event.target.value);
+
        target,
        for (var element of monsterList.children) {
+
        characters,
          if (!isValueInArray(value, toNormalForm(element.textContent))) {
+
        newCharacterTemplate,
            hideElement(element);
+
        battle
          } else {
+
       );
            showElement(element);
+
     } else {
          }
+
       handleClickOnCharacter(
        }
+
        spanInput,
      }, 500);
+
        null,
    });
 
  }
 
 
 
  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,
 
         characters,
         monsterTemplate,
+
         newCharacterTemplate,
        monstersContainer,
 
 
         battle,
 
         battle,
         monsterVnum,
+
         true
        monsterList
 
 
       );
 
       );
      inputMonster.checked = true;
 
    } else {
 
      deleteMonster(characters, monsterVnum, null, battle);
 
 
     }
 
     }
 
   });
 
   });
  
   monsterListForm.addEventListener("submit", function (event) {
+
   newCharacterTemplate.addEventListener("keydown", function (event) {
     event.preventDefault();
+
     if (event.keyCode === 13) {
 +
      event.target.click();
 +
    }
 
   });
 
   });
  
   monsterListForm.addEventListener("change", function (event) {
+
   return [newCharacterTemplate, spanInput];
    var target = event.target;
+
}
    var monsterVnum = target.name;
 
  
    if (monsterVnum === "search-monster") {
+
function validPseudo(pseudo) {
      return;
+
  var newPseudo = pseudoFormat(pseudo);
    }
 
  
    if (target.checked) {
+
  if (!newPseudo) {
      handleNewMonster(
+
    return "Pseudo";
        characters,
+
  }
        monsterTemplate,
 
        monstersContainer,
 
        battle,
 
        monsterVnum,
 
        monsterList
 
      );
 
  
      characters.savedMonsters.push(monsterVnum);
+
  return newPseudo;
      updateSavedMonsters(characters.savedMonsters);
+
}
      addBattleChoice(battle, monsterVnum, true);
 
    } else {
 
      var currentMonsterTemplate = monstersContainer.querySelector(
 
        "[data-name='" + monsterVnum + "']"
 
      );
 
      deleteMonster(characters, monsterVnum, currentMonsterTemplate, battle);
 
    }
 
  });
 
  
   addEventListener("storage", function (event) {
+
function addNewCharacter(
    if (event.key === "newMonsterCalculator") {
+
  characters,
      var monsterVnum = Number(event.newValue);
+
  characterTemplate,
 +
   charactersContainer,
 +
  battle,
 +
  pseudoToDuplicate
 +
) {
 +
  function editAndSetCharacterPseudoInput(selectedCharacter, spanInput) {
 +
    var maxPseudoLength = 20;
  
      if (!monsterVnum) {
+
    var selection = window.getSelection();
        return;
+
    var range = document.createRange();
      }
 
  
      var inputMonster = monsterList.querySelector(
+
    if (pseudoToDuplicate) {
        "input[name='" + Math.abs(monsterVnum) + "']"
+
      spanInput.textContent = pseudoToDuplicate;
      );
 
 
 
      if (inputMonster) {
 
        if (
 
          (monsterVnum > 0 && !inputMonster.checked) ||
 
          (monsterVnum < 0 && inputMonster.checked)
 
        ) {
 
          inputMonster.click();
 
        }
 
      }
 
 
     }
 
     }
  });
 
}
 
  
function removeBattleChoice(battle, name) {
+
    spanInput.contentEditable = true;
  var battleSelects = [battle.attackerSelection, battle.victimSelection];
+
    spanInput.focus();
 +
    range.selectNodeContents(spanInput);
 +
    selection.removeAllRanges();
 +
    selection.addRange(range);
  
  battleSelects.forEach(function (battleSelect) {
+
    function pseudoValidation() {
    for (
+
      var characterPseudo = validPseudo(spanInput.textContent);
       var optionIndex = 0;
+
       var characterDataObject = { name: characterPseudo };
      optionIndex < battleSelect.options.length;
+
 
       optionIndex++
+
       if (pseudoToDuplicate) {
    ) {
+
        characterDataObject = copyObject(
      if (battleSelect.options[optionIndex].value === name) {
+
          characters.savedCharacters[pseudoToDuplicate]
         battleSelect.remove(optionIndex);
+
         );
         break;
+
         characterDataObject.name = characterPseudo;
 
       }
 
       }
    }
 
  });
 
}
 
  
function addBattleChoice(battle, name, isMonster = false) {
+
      [characterDataObject, characterPseudo] = addUniquePseudo(
  function createOption(text, vnum) {
+
        characterDataObject,
    var option = document.createElement("option");
+
        Object.keys(characters.savedCharacters)
    option.textContent = text;
+
      );
    option.value = vnum;
 
  
    if (!isMonster) {
+
      selection.removeAllRanges();
       option.classList.add("notranslate");
+
      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
 +
      );
 
     }
 
     }
  
     return option;
+
     function handleMaxLength(event) {
  }
+
      if (spanInput.textContent.length > maxPseudoLength) {
 
+
        spanInput.textContent = spanInput.textContent.slice(0, maxPseudoLength);
  var vnum = name;
+
        range.setStart(spanInput.childNodes[0], maxPseudoLength);
 +
        selection.removeAllRanges();
 +
        selection.addRange(range);
 +
      }
 +
    }
  
  if (isMonster) {
+
    function handleBlur() {
    name = getMonsterName(name);
+
      spanInput.removeEventListener("blur", handleBlur);
  }
+
      spanInput.removeEventListener("input", handleMaxLength);
 +
      pseudoValidation();
 +
    }
  
  if (isMonster && monsterData[vnum][1]) {
+
    function handleKeyDown(event) {
    // pass
+
      if (event.key === "Enter") {
  } else {
+
        event.preventDefault();
    battle.attackerSelection.appendChild(createOption(name, vnum));
 
  }
 
  
  battle.victimSelection.appendChild(createOption(name, vnum));
+
        spanInput.removeEventListener("keydown", handleKeyDown);
}
+
        spanInput.removeEventListener("blur", handleBlur);
 +
        spanInput.removeEventListener("input", handleMaxLength);
  
function updateBattleChoice(characters, battle) {
+
        pseudoValidation();
  var keys = Object.keys(characters.savedCharacters);
+
      }
 +
    }
  
  for (var index = 0; index < keys.length; index++) {
+
    spanInput.addEventListener("input", handleMaxLength);
    var pseudo = keys[index];
+
    spanInput.addEventListener("keydown", handleKeyDown);
     addBattleChoice(battle, pseudo);
+
     spanInput.addEventListener("blur", handleBlur);
 
   }
 
   }
  
   characters.savedMonsters.forEach(function (monsterVnum) {
+
   hideElement(characters.characterCreation);
     addBattleChoice(battle, monsterVnum, true);
+
  var [selectedCharacter, spanInput] = handleNewCharacter(
   });
+
     characters,
}
+
    characterTemplate,
 +
    charactersContainer,
 +
    battle
 +
   );
  
function isPC(character) {
+
   editAndSetCharacterPseudoInput(selectedCharacter, spanInput);
   if (character.race === 0 || character.race === 1) {
 
    return false;
 
  }
 
  return true;
 
 
}
 
}
  
function isBoss(character) {
+
function handleFocus() {
   return character.race === 0 && character.rank >= 5;
+
   var tooltipLinks = document.querySelectorAll("div.tooltip a");
 +
  tooltipLinks.forEach(function (link) {
 +
    link.setAttribute("tabindex", -1);
 +
  });
 
}
 
}
  
function isStone(character) {
+
function resetBonusVariation(bonusVariation) {
   return character.race === 1;
+
   function resetInput(input) {
 +
    input.removeAttribute("min");
 +
    input.removeAttribute("max");
 +
    input.defaultValue = 0;
 +
    input.value = input.defaultValue;
 +
  }
 +
  var { minValue, maxValue, checkbox, container, disabledText, selectedText } =
 +
    bonusVariation;
 +
 
 +
  resetInput(minValue);
 +
  resetInput(maxValue);
 +
  showElement(disabledText);
 +
  hideElement(selectedText);
 +
  hideElement(container);
 +
  checkbox.checked = false;
 
}
 
}
  
function isMagicClass(character) {
+
function handleBonusVariationUpdate(
   return character.race === "shaman" || character.class === "black_magic";
+
  characterCreation,
}
+
  bonusVariation,
 +
  resetSkill
 +
) {
 +
   var selectedBonus = characterCreation.bonusVariation.value;
 +
  var displayName = characterCreation.bonusVariationName.value;
 +
 
 +
  if (
 +
    resetSkill &&
 +
    (selectedBonus.startsWith("attackSkill") ||
 +
      selectedBonus.startsWith("horseSkill") ||
 +
      selectedBonus.startsWith("skillBonus"))
 +
  ) {
 +
    resetBonusVariation(bonusVariation);
 +
    return;
 +
  }
  
function isPolymorph(character) {
+
  if (
   return character.state === "polymorph";
+
    characterCreation.hasOwnProperty(selectedBonus) &&
 +
    selectedBonus != 0 &&
 +
    displayName != 0
 +
  ) {
 +
    handleBonusVariation(
 +
      characterCreation[selectedBonus],
 +
      bonusVariation,
 +
      displayName
 +
    );
 +
   } else {
 +
    resetBonusVariation(bonusVariation);
 +
  }
 
}
 
}
  
function isRiding(character) {
+
function getTargetContent(targetParent, targetName, isSkill) {
   return character.state === "horse";
+
   var targetContent = "";
}
 
  
function isBow(weapon) {
+
  if (targetParent.children.length <= 1) {
   return weapon[1] === 2;
+
    targetContent = targetParent.textContent;
}
+
   } else if (targetName === "weaponUpgrade") {
 +
    targetContent = targetParent.children[1].textContent;
 +
  } else if (targetName === "level") {
 +
    targetContent = targetParent.textContent.replace("Lv", "").trim();
 +
  } else if (isSkill) {
 +
    var container = targetParent.children[1];
  
function calcAttackFactor(attacker, victim) {
+
    for (var index = 0; index < container.children.length; index++) {
  function calcCoeffK(dex, level) {
+
      var element = container.children[index];
    return Math.min(90, Math.floor((2 * dex + level) / 3));
 
  }
 
  
  var K1 = calcCoeffK(attacker.polymorphDex, attacker.level);
+
      if (element.checkVisibility()) {
   var K2 = calcCoeffK(victim.polymorphDex, attacker.level);
+
        targetContent += element.textContent;
 +
      }
 +
    }
 +
   } else {
 +
    for (var index = 1; index < targetParent.children.length; index++) {
 +
      var element = targetParent.children[index];
  
  var AR = (K1 + 210) / 300;
+
      if (element.checkVisibility()) {
   var ER = (((2 * K2 + 5) / (K2 + 95)) * 3) / 10;
+
        targetContent += element.textContent;
 +
      }
 +
    }
 +
   }
  
   return AR - ER;
+
   return targetContent.trim();
 
}
 
}
  
function calcMainAttackValue(attacker, attackerWeapon) {
+
function handleBonusVariation(target, bonusVariation, displayName) {
   var leadership = 0;
+
   var {
   var rawWeaponAttackValue = 0;
+
    minValue,
 +
    maxValue,
 +
    checkbox,
 +
    container,
 +
    disabledText,
 +
    selectedText,
 +
    displaySpan,
 +
   } = bonusVariation;
  
   if (isPC(attacker)) {
+
   var {
     var rawWeaponAttackValue = attackerWeapon[3][attacker.weaponUpgrade];
+
     min: targetMin,
 +
    max: targetMax,
 +
    name: targetName,
 +
    value: targetValue,
 +
    parentElement: targetParent,
 +
    tagName,
 +
  } = target;
  
    if (!rawWeaponAttackValue) {
+
  targetMin = Number(targetMin);
      rawWeaponAttackValue = 0;
+
  targetMax = Number(targetMax);
    }
+
  targetValue = Number(targetValue);
  
    leadership = attacker.leadership;
+
  var isSkill = tagName === "SELECT";
  }
 
  
   return 2 * (attacker.level + rawWeaponAttackValue) + leadership;
+
   if (isSkill) {
}
+
    var options = target.options;
  
function calcStatAttackValue(character) {
+
     targetMin = options[0].value;
  switch (character.race) {
+
     targetMax = options[options.length - 1].value;
     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, attackerWeapon) {
+
  minValue.min = targetMin;
   var attackValueOther = 0;
+
  minValue.max = targetMax;
 +
  minValue.defaultValue = targetMin;
 +
 
 +
  maxValue.min = targetMin;
 +
  maxValue.max = targetMax;
 +
   maxValue.defaultValue = targetMin;
  
   var minAttackValue = 0;
+
   hideElement(disabledText);
   var maxAttackValue = 0;
+
   showElement(selectedText);
  
   var minAttackValueSlash = 0;
+
   if (displayName) {
  var maxAttackValueSlash = 0;
+
    minValue.value = Math.min(Math.max(minValue.value, targetMin), targetMax);
 +
    maxValue.value = Math.max(Math.min(maxValue.value, targetMax), targetMin);
  
   if (isPC(attacker)) {
+
    displaySpan.textContent = displayName;
     if (isValueInArray("serpent", attackerWeapon[0].toLowerCase())) {
+
   } else {
      var rawAttackValue = attackerWeapon[3][attacker.weaponUpgrade];
+
     var { input, inputName, tabButton } = bonusVariation;
 +
    var targetContent = getTargetContent(targetParent, targetName, isSkill);
  
      minAttackValue = attacker.minAttackValueRandom - rawAttackValue;
+
    input.value = targetName;
      maxAttackValue = attacker.maxAttackValueRandom - rawAttackValue;
+
    inputName.value = targetContent;
 +
    displaySpan.textContent = targetContent;
  
      minAttackValue = Math.max(0, minAttackValue);
+
    minValue.value = Math.max(targetValue - 10, targetMin);
      maxAttackValue = Math.max(minAttackValue, maxAttackValue);
+
    maxValue.value = Math.min(targetValue + 10, targetMax);
    } else {
 
      minAttackValue = attackerWeapon[2][2];
 
      maxAttackValue = attackerWeapon[2][3];
 
    }
 
  
     minAttackValueSlash = Math.min(
+
     showElement(container);
      attacker.minAttackValueSlash,
+
     checkbox.checked = true;
      attacker.maxAttackValueSlash
 
    );
 
     maxAttackValueSlash = Math.max(
 
      attacker.minAttackValueSlash,
 
      attacker.maxAttackValueSlash
 
    );
 
  
     attackValueOther += attacker.attackValue;
+
     tabButton.click();
 +
    tabButton.scrollIntoView(true);
  
     if (isBow(attackerWeapon) && !isPolymorph(attacker)) {
+
     input.dispatchEvent(newChangeEvent());
      attackValueOther += 25;
 
    }
 
  } else {
 
    minAttackValue = attacker.minAttackValue;
 
    maxAttackValue = attacker.maxAttackValue;
 
 
   }
 
   }
 +
}
  
   minAttackValue += attacker.minAttackValuePolymorph;
+
function characterManagement(characters, battle) {
   maxAttackValue += attacker.maxAttackValuePolymorph;
+
   var {
 +
    newCharacterTemplate: characterTemplate,
 +
    charactersContainer,
 +
    addNewCharacterButton,
 +
    saveButton,
 +
    characterCreation,
 +
    bonusVariation,
 +
   } = characters;
  
   attackValueOther += attacker.statAttackValue;
+
   Object.keys(characters.savedCharacters).forEach(function (pseudo) {
   attackValueOther += attacker.horseAttackValue;
+
    handleNewCharacter(
 +
      characters,
 +
      characterTemplate,
 +
      charactersContainer,
 +
      battle,
 +
      pseudo
 +
    );
 +
   });
  
   var weaponInterval = maxAttackValue - minAttackValue + 1;
+
   addNewCharacterButton.addEventListener("click", function (event) {
   var slashInterval = maxAttackValueSlash - minAttackValueSlash + 1;
+
    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;
 +
      }
 +
    }
 +
   });
  
   var totalCardinal = weaponInterval * slashInterval * 10000;
+
   handleUploadCharacter(
   var minInterval = Math.min(weaponInterval, slashInterval);
+
    characters,
 +
    characterTemplate,
 +
    charactersContainer,
 +
    battle
 +
   );
  
   minAttackValue += minAttackValueSlash;
+
   function handleLongPress(target) {
  maxAttackValue += maxAttackValueSlash;
+
    if (target.tagName !== "INPUT" && target.tagName !== "SELECT") {
 +
      target = target.querySelector("input");
 +
    }
  
  return [
+
    if (
    minAttackValue,
+
      !target ||
    maxAttackValue,
+
      (target.type !== "number" &&
    attackValueOther,
+
        !target.classList.contains("skill-select") &&
    minInterval,
+
        target.name !== "weaponUpgrade") ||
     totalCardinal,
+
      target.classList.contains("disabled-variation")
  ];
+
     ) {
}
+
      return;
 +
    }
  
function calcMagicAttackValue(attacker, attackerWeapon) {
+
    handleBonusVariation(target, bonusVariation, null);
  var minMagicAttackValue = 0;
+
   }
   var maxMagicAttackValue = 0;
 
  
   var minMagicAttackValueSlash = 0;
+
   characterCreation.addEventListener("click", function (event) {
  var maxMagicAttackValueSlash = 0;
+
     if (event.shiftKey || event.ctrlKey) {
 
+
      handleLongPress(event.target);
  if (isValueInArray("serpent", attackerWeapon[0].toLowerCase())) {
 
     minMagicAttackValue = attacker.minMagicAttackValueRandom;
 
    maxMagicAttackValue = attacker.maxMagicAttackValueRandom;
 
 
 
    maxMagicAttackValue = Math.max(minMagicAttackValue, maxMagicAttackValue);
 
  } else {
 
    var rawWeaponAttackValue = attackerWeapon[3][attacker.weaponUpgrade];
 
 
 
    if (!rawWeaponAttackValue) {
 
      rawWeaponAttackValue = 0;
 
 
     }
 
     }
 +
  });
  
    minMagicAttackValue = attackerWeapon[2][0] + rawWeaponAttackValue;
+
  var longPressTimer;
    maxMagicAttackValue = attackerWeapon[2][1] + rawWeaponAttackValue;
 
  }
 
  
   minMagicAttackValueSlash = Math.min(
+
   characterCreation.addEventListener("touchstart", function (event) {
     attacker.minMagicAttackValueSlash,
+
     longPressTimer = setTimeout(function () {
    attacker.maxMagicAttackValueSlash
+
      handleLongPress(event.target);
  );
+
     }, 800);
  maxMagicAttackValueSlash = Math.max(
+
   });
     attacker.minMagicAttackValueSlash,
 
    attacker.maxMagicAttackValueSlash
 
   );
 
  
   var weaponInterval = maxMagicAttackValue - minMagicAttackValue + 1;
+
   characterCreation.addEventListener("touchend", function () {
   var slashInterval = maxMagicAttackValueSlash - minMagicAttackValueSlash + 1;
+
    clearTimeout(longPressTimer);
 +
   });
  
   var totalCardinal = weaponInterval * slashInterval * 10000;
+
   characterCreation.addEventListener("touchmove", function () {
   var minInterval = Math.min(weaponInterval, slashInterval);
+
    clearTimeout(longPressTimer);
 +
   });
  
   minMagicAttackValue += minMagicAttackValueSlash;
+
   filterForm(characters, battle);
   maxMagicAttackValue += maxMagicAttackValueSlash;
+
   characterCreationListener(characters, battle);
 
+
   handleFocus();
   return [minMagicAttackValue, maxMagicAttackValue, minInterval, totalCardinal];
 
}
 
  
function getPolymorphPower(polymorphPoint, polymorphPowerTable) {
+
  window.addEventListener("beforeunload", function (event) {
   return polymorphPowerTable[polymorphPoint];
+
    if (characters.unsavedChanges) {
 +
      event.preventDefault();
 +
      return "";
 +
    }
 +
   });
 
}
 
}
  
function getSkillPower(skillPoint, skillPowerTable) {
+
function addMonsterElement(characters, battle, monsterVnum, iframeInfo) {
   return skillPowerTable[skillPoint];
+
   var { monsterTemplate, monstersContainer } = characters;
}
+
  var monsterElement = monsterTemplate.cloneNode(true);
 +
  var spanInput = monsterElement.querySelector("span.input");
 +
  var deleteSvg = monsterElement.querySelector("svg");
 +
  var monsterName = getMonsterName(monsterVnum);
 +
  var link = createWikiLink(monsterName);
  
function getMarriageBonusValue(character, marriageTable, itemName) {
+
  monsterElement.setAttribute("tabindex", "0");
   var index;
+
  spanInput.appendChild(link);
   var lovePoint = character.lovePoint;
+
   monstersContainer.appendChild(monsterElement);
 +
   characters.monsterElements[monsterVnum] = monsterElement;
  
   if (lovePoint < 65) {
+
   deleteSvg.addEventListener("click", function () {
    index = 0;
+
     var monster = characters.savedMonsters[monsterVnum];
  } else if (lovePoint < 80) {
+
     iframeInfo[monster.category].shouldBeUpdated = true;
    index = 1;
 
  } else if (lovePoint < 100) {
 
     index = 2;
 
  } else {
 
     index = 3;
 
  }
 
  
   return marriageTable[itemName][index];
+
    deleteMonster(characters, battle, monsterVnum);
 +
   });
 
}
 
}
  
function calcDamageWithPrimaryBonuses(damages, battleValues) {
+
function addNewMonster(
   damages = floorMultiplication(
+
  characters,
    damages * battleValues.attackValueCoeff + battleValues.adjustCoeff,
+
   battle,
    1
+
   monsterVnum,
   );
+
   monsterImage,
   damages += battleValues.attackValueMarriage;
+
   iframeInfo,
   damages = floorMultiplication(
+
  category
    damages,
+
) {
    battleValues.monsterResistanceMarriageCoeff
+
   if (isValueInArray(monsterVnum, Object.keys(characters.savedMonsters)))
  );
+
    return;
   damages = floorMultiplication(damages, battleValues.monsterResistanceCoeff);
 
  damages = floorMultiplication(damages, battleValues.typeBonusCoeff);
 
  damages +=
 
    floorMultiplication(damages, battleValues.raceBonusCoeff) -
 
    floorMultiplication(damages, battleValues.raceResistanceCoeff);
 
  damages = floorMultiplication(damages, battleValues.stoneBonusCoeff);
 
  damages = floorMultiplication(damages, battleValues.monsterBonusCoeff);
 
  
   var elementDamages = 0;
+
   var newMonster = {
  for (var elementBonusCoeff of battleValues.elementBonusCoeff) {
+
     image: monsterImage,
     elementDamages += floorMultiplicationWithNegative(
+
     category: category,
      damages,
+
   };
      elementBonusCoeff
 
     );
 
   }
 
  damages += elementDamages;
 
  
   damages = floorMultiplication(damages, battleValues.damageMultiplier);
+
   characters.savedMonsters[monsterVnum] = newMonster;
  
   return damages;
+
   addMonsterElement(characters, battle, monsterVnum, iframeInfo);
 +
  updateSavedMonsters(characters.savedMonsters);
 +
  addBattleChoice(battle.battleChoice, monsterVnum, newMonster, true);
 
}
 
}
  
function calcDamageWithSecondaryBonuses(
+
function addButtonsToCards(characters, iframeDoc, iframeInfo, category) {
  damages,
+
   var buttonTemplates = characters.monsterButtonTemplates.children[0];
  battleValues,
+
   var cardToEdit = iframeDoc.getElementById("cards-container").children;
  damagesType,
+
   var { nameToVnum } = iframeInfo;
  minPiercingDamages,
+
   var vnumToButtons = iframeInfo[category].vnumToButtons;
  damagesWithPrimaryBonuses
 
) {
 
   damages = floorMultiplication(damages, battleValues.magicResistanceCoeff);
 
   damages = floorMultiplication(damages, battleValues.weaponDefenseCoeff);
 
   damages = floorMultiplication(damages, battleValues.tigerStrengthCoeff);
 
   damages = floorMultiplication(damages, battleValues.blessingBonusCoeff);
 
  
   if (damagesType.criticalHit) {
+
   for (var cardIndex = 0; cardIndex < cardToEdit.length; cardIndex++) {
     damages *= 2;
+
     var card = cardToEdit[cardIndex];
  }
+
    var cardName = card.querySelector("[data-name]").firstChild.title;
 +
    var buttonTemplatesClone = buttonTemplates.cloneNode(true);
  
  if (damagesType.piercingHit) {
+
     cardName = cardName.replace(/\s/g, " ");
     damages += battleValues.defense + Math.min(0, minPiercingDamages);
 
    damages += floorMultiplication(
 
      damagesWithPrimaryBonuses,
 
      battleValues.extraPiercingHitCoeff
 
    );
 
  }
 
  
  damages = floorMultiplication(damages, battleValues.averageDamageCoeff);
+
    if (!nameToVnum.hasOwnProperty(cardName)) {
  damages = floorMultiplication(
+
      continue;
    damages,
+
     }
    battleValues.averageDamageResistanceCoeff
 
  );
 
  damages = floorMultiplication(
 
    damages,
 
     battleValues.skillDamageResistanceCoeff
 
  );
 
  
  damages = floorMultiplication(damages, battleValues.rankBonusCoeff);
+
     var monsterVnum = nameToVnum[cardName];
  damages = Math.max(0, damages + battleValues.defensePercent);
 
  damages += Math.min(
 
    300,
 
     floorMultiplication(damages, battleValues.damageBonusCoeff)
 
  );
 
  damages = floorMultiplication(damages, battleValues.empireMalusCoeff);
 
  damages = floorMultiplication(damages, battleValues.sungMaStrBonusCoeff);
 
  damages -= floorMultiplication(damages, battleValues.sungmaStrMalusCoeff);
 
  
  damages = floorMultiplication(damages, battleValues.whiteDragonElixirCoeff);
+
    buttonTemplatesClone.dataset.monsterId = monsterVnum;
  damages = floorMultiplication(damages, battleValues.steelDragonElixirCoeff);
+
    card.lastElementChild.appendChild(buttonTemplatesClone);
 
+
    vnumToButtons[monsterVnum] = buttonTemplatesClone.children;
   return damages;
+
   }
 
}
 
}
  
function calcSkillDamageWithSecondaryBonuses(
+
function updateiFrameButtons(characters, iframeInfo, category) {
  damages,
+
   var addedMonsters = Object.keys(characters.savedMonsters);
  battleValues,
+
   var { currentiFrameIsMonster } = iframeInfo;
   damagesType,
+
   var vnumToButtons = iframeInfo[category].vnumToButtons;
   minPiercingDamages
+
   var isPolymorphModal = category === "monster" && !currentiFrameIsMonster;
) {
 
   damages = floorMultiplication(damages, battleValues.magicResistanceCoeff);
 
   damages = floorMultiplication(damages, battleValues.weaponDefenseCoeff);
 
  
   damages -= battleValues.defense;
+
   for (var monsterVnum in vnumToButtons) {
 +
    var [addButton, deleteButton, selectButton] = vnumToButtons[monsterVnum];
  
  damages = floorMultiplication(damages, battleValues.skillWardCoeff);
+
    if (isPolymorphModal) {
  damages = floorMultiplication(damages, battleValues.skillBonusCoeff);
+
      hideElement(addButton);
  damages = floorMultiplication(damages, battleValues.skillBonusByBonusCoeff);
+
      hideElement(deleteButton);
  damages = floorMultiplication(damages, battleValues.tigerStrengthCoeff);
+
      showElement(selectButton);
 +
      continue;
 +
    }
  
  if (damagesType.criticalHit) {
+
    if (isValueInArray(monsterVnum, addedMonsters)) {
     damages *= 2;
+
      hideElement(addButton);
 +
      showElement(deleteButton);
 +
      hideElement(selectButton);
 +
     } else {
 +
      showElement(addButton);
 +
      hideElement(deleteButton);
 +
      hideElement(selectButton);
 +
    }
 
   }
 
   }
  
   if (damagesType.piercingHit) {
+
   iframeInfo.lastiFrameIsMonster = currentiFrameIsMonster;
    damages +=
+
}
      battleValues.piercingHitDefense + Math.min(0, minPiercingDamages);
+
 
 +
function getMonsterImage(buttonsContainer) {
 +
  var elder = buttonsContainer.parentElement.firstElementChild;
 +
  var image = elder.querySelector("img");
 +
 
 +
  if (image) {
 +
    return image.getAttribute("src") || "";
 
   }
 
   }
  
   damages = floorMultiplication(damages, battleValues.skillDamageCoeff);
+
   return "";
  damages = floorMultiplication(
+
}
    damages,
 
    battleValues.skillDamageResistanceCoeff
 
  );
 
  
  damages = floorMultiplication(damages, battleValues.rankBonusCoeff);
+
function handleiFrame(iframeInfo, category) {
   damages = Math.max(0, damages + battleValues.defensePercent);
+
   var { characters, battle } = iframeInfo;
   damages += Math.min(
+
   var iframeInfoCategory = iframeInfo[category];
    300,
+
  var { iframe, pageName, vnumToButtons } = iframeInfoCategory;
    floorMultiplication(damages, battleValues.damageBonusCoeff)
+
   var loadingAnimation = iframeInfoCategory.iframe.previousElementSibling;
  );
 
  damages = floorMultiplication(damages, battleValues.empireMalusCoeff);
 
   damages = floorMultiplication(damages, battleValues.sungMaStrBonusCoeff);
 
  damages -= floorMultiplication(damages, battleValues.sungmaStrMalusCoeff);
 
  
   damages = floorMultiplication(damages, battleValues.whiteDragonElixirCoeff);
+
   iframe.src = mw.util.getUrl(pageName);
  damages = floorMultiplication(damages, battleValues.steelDragonElixirCoeff);
 
  
   return damages;
+
   iframe.addEventListener("load", function () {
}
+
    var iframeDoc = this.contentDocument || this.contentWindow.document;
 +
    var iframeBody = iframeDoc.body;
 +
    var content = iframeDoc.getElementById("show-after-loading");
  
function computePolymorphPoint(attacker, victim, polymorphPowerTable) {
+
    iframeBody.firstElementChild.replaceWith(content);
  attacker.statAttackValue = 0;
 
  
  attacker.polymorphDex = attacker.dex;
+
    iframeBody.style.background = "transparent";
  victim.polymorphDex = victim.dex;
+
    iframeBody.style.paddingRight = "10px";
  
  attacker.minAttackValuePolymorph = 0;
+
    addButtonsToCards(characters, iframeDoc, iframeInfo, category);
  attacker.maxAttackValuePolymorph = 0;
+
    updateiFrameButtons(characters, iframeInfo, category);
  
  if (isPC(attacker) && isPolymorph(attacker) && polymorphPowerTable) {
+
     iframeInfoCategory.loadIsFinished = true;
     var polymorphPowerPct =
 
      getPolymorphPower(attacker.polymorphPoint, polymorphPowerTable) / 100;
 
    var polymorphMonster = createMonster(attacker.polymorphMonster);
 
  
     var polymorphStr = floorMultiplication(
+
     hideElement(loadingAnimation);
      polymorphPowerPct,
+
     showElement(iframe);
      polymorphMonster.str
 
     );
 
  
     attacker.polymorphDex += floorMultiplication(
+
     iframeDoc.addEventListener("click", handleButtonClick);
      polymorphPowerPct,
 
      polymorphMonster.dex
 
    );
 
  
     attacker.minAttackValuePolymorph = floorMultiplication(
+
     function handleButtonClick(event) {
       polymorphPowerPct,
+
       var target = event.target;
       polymorphMonster.minAttackValue
+
       var link = target.closest("a");
    );
+
      var buttonsContainer = target.closest(".handle-monster");
    attacker.maxAttackValuePolymorph = floorMultiplication(
 
      polymorphPowerPct,
 
      polymorphMonster.maxAttackValue
 
    );
 
  
    if (!attacker.weapon) {
+
      if (link) {
      attacker.maxAttackValuePolymorph += 1;
+
        event.preventDefault();
    }
+
        window.open(link.href, "_blank");
 +
      } else if (buttonsContainer) {
 +
        var { monsterId: monsterVnum } = buttonsContainer.dataset;
  
    attacker.attackValue = 0;
+
        // polymorph iframe
 +
        if (category === "monster" && !iframeInfo.currentiFrameIsMonster) {
 +
          var monsterImage = getMonsterImage(buttonsContainer);
 +
          changePolymorphValues(
 +
            characters.characterCreation,
 +
            monsterVnum,
 +
            monsterImage
 +
          );
 +
          handlePolymorphDisplay(
 +
            characters.polymorphDisplay,
 +
            monsterVnum,
 +
            monsterImage
 +
          );
 +
          iframe.dispatchEvent(newChangeEvent());
 +
        } else {
 +
          var addedMonsters = Object.keys(characters.savedMonsters);
 +
          var [addButton, deleteButton] = vnumToButtons[monsterVnum];
  
    if (isMagicClass(attacker)) {
+
          if (isValueInArray(monsterVnum, addedMonsters)) {
      attacker.statAttackValue = 2 * (polymorphStr + attacker.int);
+
            deleteMonster(characters, battle, monsterVnum);
    } else {
+
            showElement(addButton);
      attacker.statAttackValue = 2 * (polymorphStr + attacker.str);
+
            hideElement(deleteButton);
 +
          } else {
 +
            var monsterImage = getMonsterImage(buttonsContainer);
 +
            addNewMonster(
 +
              characters,
 +
              battle,
 +
              monsterVnum,
 +
              monsterImage,
 +
              iframeInfo,
 +
              category
 +
            );
 +
            hideElement(addButton);
 +
            showElement(deleteButton);
 +
          }
 +
        }
 +
      }
 
     }
 
     }
   } else {
+
   });
    attacker.statAttackValue = calcStatAttackValue(attacker);
 
  }
 
 
}
 
}
  
function computeHorse(attacker) {
+
function getNameToVnumMapping() {
   attacker.horseAttackValue = 0;
+
   var nameToVnum = {};
  
   if (isPC(attacker) && isRiding(attacker)) {
+
   for (var monsterVnum in monsterData) {
     var horseConstant = 30;
+
     nameToVnum[monsterData[monsterVnum][monsterData[monsterVnum].length - 1]] =
 +
      monsterVnum;
 +
  }
  
    if (attacker.class === "weaponary") {
+
  return nameToVnum;
      horseConstant = 60;
+
}
    }
 
  
    attacker.horseAttackValue = floorMultiplication(
+
function monsterManagement(characters, battle) {
      2 * attacker.level + attacker.statAttackValue,
+
  var { monsteriFrame, stoneiFrame } = characters;
      attacker.horsePoint / horseConstant
+
  var monsterVnums = Object.keys(monsterData);
    );
 
  }
 
}
 
  
function getRankBonus(attacker) {
+
  var iframeInfo = {
  if (attacker.lowRank !== "on") {
+
    lastiFrameIsMonster: false,
     return 0;
+
    currentiFrameIsMonster: false,
   }
+
    characters: characters,
 +
    battle: battle,
 +
    nameToVnum: getNameToVnumMapping(),
 +
    monster: {
 +
      isLoaded: false,
 +
      loadIsFinished: false,
 +
      shouldBeUpdated: false,
 +
      vnumToButtons: {},
 +
      pageName: "Monstres",
 +
      iframe: monsteriFrame,
 +
    },
 +
    stone: {
 +
      isLoaded: false,
 +
      loadIsFinished: false,
 +
      shouldBeUpdated: false,
 +
      vnumToButtons: {},
 +
      pageName: "Pierres Metin",
 +
      iframe: stoneiFrame,
 +
     },
 +
   };
  
   switch (attacker.playerRank) {
+
   document.addEventListener("modalOpen", function (event) {
     case "aggressive":
+
     var modalName = event.detail.name;
      return 1;
 
    case "fraudulent":
 
      return 2;
 
    case "malicious":
 
      return 3;
 
    case "cruel":
 
      return 5;
 
  }
 
  
  return 0;
+
    if (modalName === "monster" || modalName === "stone") {
}
+
      handleModal(event.target, modalName);
 +
    }
 +
  });
  
function calcElementCoeffPvP(elementBonus, mapping, attacker, victim) {
+
  function handleModal(target, modalName) {
  var minElementMalus = 0;
+
    var iframeInfoCategory = iframeInfo[modalName];
  var maxDifference = 0;
 
  var savedElementDifferences = [];
 
  var elementBonusIndex = 0;
 
  
  for (var index = 0; index < elementBonus.length; index++) {
+
    if (modalName === "monster") {
    if (!attacker[mapping.elementBonus[index]]) {
+
      var isMonsteriFrame = target.id === "add-new-monster";
       continue;
+
      iframeInfo.currentiFrameIsMonster = isMonsteriFrame;
 +
 
 +
      if (
 +
        iframeInfoCategory.loadIsFinished &&
 +
        ((isMonsteriFrame && !iframeInfo.lastiFrameIsMonster) ||
 +
          (!isMonsteriFrame && iframeInfo.lastiFrameIsMonster))
 +
      ) {
 +
        iframeInfoCategory.shouldBeUpdated = true;
 +
       }
 
     }
 
     }
  
     var elementDifference =
+
     if (!iframeInfoCategory.isLoaded) {
       attacker[mapping.elementBonus[index]] -
+
       handleiFrame(iframeInfo, modalName);
       victim[mapping.elementResistance[index]];
+
       iframeInfoCategory.isLoaded = true;
 +
    }
  
     if (elementDifference >= 0) {
+
     if (
       elementBonus[elementBonusIndex] = elementDifference / 1000;
+
       iframeInfoCategory.loadIsFinished &&
       minElementMalus -= elementDifference;
+
       iframeInfoCategory.shouldBeUpdated
       maxDifference = Math.max(maxDifference, elementDifference);
+
    ) {
       elementBonusIndex++;
+
       updateiFrameButtons(characters, iframeInfo, modalName);
    } else {
+
       iframeInfoCategory.shouldBeUpdated = false;
      savedElementDifferences.push(elementDifference);
 
 
     }
 
     }
 
   }
 
   }
  
   if (!savedElementDifferences.length) {
+
   Object.keys(characters.savedMonsters)
    return;
+
    .slice()
  }
+
    .forEach(function (monsterVnum) {
 
+
      if (isValueInArray(monsterVnum, monsterVnums)) {
  minElementMalus += maxDifference;
+
        addMonsterElement(characters, battle, monsterVnum, iframeInfo);
  savedElementDifferences.sort(compareNumbers);
+
      } else {
 +
        deleteMonster(characters, battle, monsterVnum);
 +
      }
 +
    });
 +
}
  
  for (var index = 0; index < savedElementDifferences.length; index++) {
+
function hideAttackType(container, input, attackType) {
    var elementDifference = savedElementDifferences[index];
+
  hideElement(container);
  
     elementBonus[elementBonusIndex + index] =
+
  if (input.checked) {
      Math.max(minElementMalus, elementDifference) / 1000;
+
     var defaultInput = attackType.defaultInput;
  
     minElementMalus = Math.min(
+
     input.checked = false;
      0,
+
    defaultInput.checked = true;
      Math.max(minElementMalus, minElementMalus - elementDifference)
+
    defaultInput.dispatchEvent(newChangeEvent());
    );
 
 
   }
 
   }
 
}
 
}
  
function skillChanceReduction(value) {
+
function filterAttackTypeSelectionCharacter(attacker, attackType) {
   if (value <= 9) {
+
   var attackerClass = attacker.class;
     return Math.floor(value / 2);
+
  var attackerIsNotPolymorph = !isPolymorph(attacker);
 +
  var { elements: attackTypeElements } = attackType;
 +
 
 +
  for (var index = 0; index < attackTypeElements.length; index++) {
 +
    var { container, input, inputClass, inputValue } =
 +
      attackTypeElements[index];
 +
 
 +
    if (
 +
      attackerIsNotPolymorph &&
 +
      attacker[inputValue] &&
 +
      (attackerClass === inputClass ||
 +
        (inputValue.startsWith("horseSkill") &&
 +
          isRiding(attacker) &&
 +
          (!inputClass || isValueInArray(attackerClass, inputClass))))
 +
    ) {
 +
      showElement(container);
 +
     } else {
 +
      hideAttackType(container, input, attackType);
 +
    }
 
   }
 
   }
  return 5 + Math.floor((value - 10) / 4);
 
 
}
 
}
  
function magicResistanceToCoeff(magicResistance) {
+
function filterAttackTypeSelectionMonster(attackType) {
   if (magicResistance) {
+
  var attackTypeElements = attackType.elements;
     return 2000 / (6 * magicResistance + 1000) - 1;
+
 
 +
  for (var index = 0; index < attackTypeElements.length; index++) {
 +
    var { container, input } = attackTypeElements[index];
 +
    hideAttackType(container, input, attackType);
 +
  }
 +
}
 +
 
 +
function removeBattleElement(battleChoice, nameOrVnum, category, type) {
 +
  var elements = battleChoice[category][type].elements;
 +
 
 +
   if (elements.hasOwnProperty(nameOrVnum)) {
 +
     elements[nameOrVnum].container.remove();
 +
    delete elements[nameOrVnum];
 +
    battleChoice[category][type].count--;
 
   }
 
   }
  return 1;
 
 
}
 
}
  
function createPhysicalBattleValues(
+
function removeBattleChoice(battleChoice, nameOrVnum, type) {
   attacker,
+
   battleChoice.categories.forEach(function (category) {
   attackerWeapon,
+
    if (category === "attacker" && type === "stone") {
   victim,
+
      return;
   mapping,
+
    }
   polymorphPowerTable,
+
    var selected = battleChoice[category].selected;
   marriageTable,
+
 
   skillPowerTable
+
    if (selected) {
 +
      var { type: selectedType, nameOrVnum: selectedNameOrVnum } =
 +
        parseTypeAndName(selected);
 +
 
 +
      if (nameOrVnum === selectedNameOrVnum && type === selectedType) {
 +
        resetBattleChoiceButton(battleChoice, category);
 +
        battleChoice[category].selected = null;
 +
      }
 +
    }
 +
 
 +
    removeBattleElement(battleChoice, nameOrVnum, category, type);
 +
    updateBattleChoiceText(battleChoice, category, type);
 +
   });
 +
}
 +
 
 +
function addBattleElement(
 +
   battleChoice,
 +
   pseudoOrVnum,
 +
   characterOrMonster,
 +
   category,
 +
   isMonster
 
) {
 
) {
   var missPercentage = 0;
+
   var { template, raceToImage } = battleChoice;
   var attackValuePercent = 0;
+
   var battleChoiceCategory = battleChoice[category];
   var attackMeleeMagic = 0;
+
   var templateClone = template.cloneNode(true);
   var attackValueMarriage = 0;
+
   var label = templateClone.firstElementChild;
   var monsterResistanceMarriage = 0;
+
   var [input, image, span] = label.children;
   var monsterResistance = 0;
+
   var imageSrc;
   var typeBonus = 0;
+
   var name;
   var raceBonus = 0;
+
   var type;
  var raceResistance = 0;
+
 
   var stoneBonus = 0;
+
   if (isMonster) {
  var monsterBonus = 0;
+
    type = characterOrMonster.category;
  var elementBonus = [0, 0, 0, 0, 0, 0]; // fire, ice, lightning, earth, darkness, wind, order doesn't matter
+
 
  var defenseMarriage = 0;
+
    if (type === "stone" && category === "attacker") {
  var damageMultiplier = 1;
+
      return;
  var magicResistance = 0;
+
    }
  var weaponDefense = 0;
+
    imageSrc = characterOrMonster.image;
  var tigerStrength = 0;
+
    name = getMonsterName(pseudoOrVnum);
  var blessingBonus = 0;
+
   } else {
  var criticalHitPercentage = attacker.criticalHit;
+
    type = "character";
  var criticalHitPercentageMarriage = 0;
+
    imageSrc = raceToImage[characterOrMonster.race];
  var piercingHitPercentage = attacker.piercingHit;
+
    name = pseudoOrVnum;
  var piercingHitPercentageMarriage = 0;
+
    label.classList.add("notranslate");
  var extraPiercingHitPercentage = Math.max(0, piercingHitPercentage - 100);
+
   }
   var averageDamage = 0;
 
  var averageDamageResistance = 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, polymorphPowerTable);
+
   var value = type + "-" + pseudoOrVnum;
   computeHorse(attacker);
+
   var id = category + "-" + value;
 +
  var currentBattleChoice = battleChoiceCategory[type];
  
   if (isPC(attacker)) {
+
   label.setAttribute("for", id);
    attackValuePercent = attacker.attackValuePercent;
+
  input.value = value;
    attackMeleeMagic = attacker.attackMeleeMagic;
+
  input.id = id;
 +
  input.name = category;
 +
  span.textContent = name;
  
    var weaponType = attackerWeapon[1];
+
  handleImageFromWiki(image, imageSrc);
  
    var weaponDefenseName = mapping.defenseWeapon[weaponType];
+
  currentBattleChoice.container.appendChild(templateClone);
    var weaponDefenseBreakName = mapping.breakWeapon[weaponType];
+
  currentBattleChoice.count++;
 +
  currentBattleChoice.elements[pseudoOrVnum] = {
 +
    container: templateClone,
 +
    image: image,
 +
    name: name,
 +
  };
  
    if (victim.hasOwnProperty(weaponDefenseName)) {
+
  updateBattleChoiceText(battleChoice, category, type);
      weaponDefense = victim[weaponDefenseName];
+
}
    }
 
  
     if (attacker.whiteDragonElixir === "on") {
+
function addBattleChoice(
       whiteDragonElixir = 10;
+
  battleChoice,
 +
  pseudoOrVnum,
 +
  characterOrMonster,
 +
  isMonster = false
 +
) {
 +
  addBattleElement(
 +
    battleChoice,
 +
    pseudoOrVnum,
 +
    characterOrMonster,
 +
    "attacker",
 +
     isMonster
 +
  );
 +
  addBattleElement(
 +
    battleChoice,
 +
    pseudoOrVnum,
 +
    characterOrMonster,
 +
    "victim",
 +
    isMonster
 +
  );
 +
}
 +
 
 +
function updateBattleChoiceImage(battleChoice, characterName, newRace) {
 +
  var imageSrc = battleChoice.raceToImage[newRace];
 +
 
 +
  battleChoice.categories.forEach(function (category) {
 +
    handleImageFromWiki(
 +
       battleChoice[category].character.elements[characterName].image,
 +
      imageSrc
 +
    );
 +
 
 +
    var selected = battleChoice[category].selected;
 +
 
 +
    if (isCharacterSelected(characterName, selected)) {
 +
      updateBattleChoiceButton(battleChoice, category, selected);
 
     }
 
     }
 +
  });
 +
}
  
    if (isPC(victim)) {
+
function updateBattleChoiceText(battleChoice, category, type) {
      if (weaponType === 2 && !isPolymorph(attacker)) {
+
  var { count, container } = battleChoice[category][type];
        missPercentage = victim.arrowBlock;
+
  var parentContainer = container.parentElement;
      } else {
 
        missPercentage = victim.meleeBlock;
 
      }
 
  
      missPercentage +=
+
  if (count === 0) {
        victim.meleeArrowBlock -
+
    hideElement(parentContainer);
        (missPercentage * victim.meleeArrowBlock) / 100;
+
    showElement(parentContainer.nextElementSibling);
 +
  } else if (count === 1) {
 +
    showElement(parentContainer);
 +
    hideElement(parentContainer.nextElementSibling);
 +
  }
 +
}
  
      typeBonus = Math.max(1, attacker.humanBonus - victim.humanResistance);
+
function updateBattleChoice(characters, battleChoice) {
      raceBonus = attacker[mapping.raceBonus[victim.race]];
+
  var templateImage = battleChoice.template.querySelector("img");
      raceResistance = victim[mapping.raceResistance[attacker.race]];
+
  var attackerImage = battleChoice.attacker.buttonContent.querySelector("img");
 +
  var victimImage = battleChoice.victim.buttonContent.querySelector("img");
  
      calcElementCoeffPvP(elementBonus, mapping, attacker, victim);
+
  resetImageFromWiki(templateImage);
 +
  resetImageFromWiki(attackerImage);
 +
  resetImageFromWiki(victimImage);
  
      if (weaponType !== 2 && attacker.hasOwnProperty(weaponDefenseBreakName)) {
+
  for (var [pseudo, character] of Object.entries(characters.savedCharacters)) {
        weaponDefense -= attacker[weaponDefenseBreakName];
+
    addBattleChoice(battleChoice, pseudo, character);
      }
+
  }
  
      criticalHitPercentage = 0;
+
  for (var [vnum, monster] of Object.entries(characters.savedMonsters)) {
      blessingBonus = calcBlessingBonus(skillPowerTable, victim);
+
    addBattleChoice(battleChoice, vnum, monster, true);
      averageDamageResistance = victim.averageDamageResistance;
+
  }
    } else {
+
}
      if (attacker.isMarried === "on") {
 
        if (attacker.loveNecklace === "on") {
 
          attackValueMarriage = getMarriageBonusValue(
 
            attacker,
 
            marriageTable,
 
            "loveNecklace"
 
          );
 
        }
 
  
        if (attacker.loveEarrings === "on") {
+
function updateBattleChoiceButton(battleChoice, category, data) {
          criticalHitPercentageMarriage = getMarriageBonusValue(
+
  var battleChoiceCategory = battleChoice[category];
            attacker,
+
  var { defaultButtonContent, buttonContent } = battleChoiceCategory;
            marriageTable,
+
  var [buttonImage, buttonSpan] = buttonContent.firstElementChild.children;
            "loveEarrings"
+
  var { type, nameOrVnum } = parseTypeAndName(data);
          );
+
  var { name, image } = battleChoiceCategory[type].elements[nameOrVnum];
        }
 
  
        if (attacker.harmonyEarrings === "on") {
+
  hideElement(defaultButtonContent);
          piercingHitPercentageMarriage = getMarriageBonusValue(
+
  showElement(buttonContent);
            attacker,
 
            marriageTable,
 
            "harmonyEarrings"
 
          );
 
        }
 
      }
 
  
      if (attacker.tigerStrength === "on") {
+
  buttonSpan.textContent = name;
        tigerStrength = 40;
+
  buttonImage.src = image.src;
      }
+
  battleChoiceCategory.selected = data;
 +
}
  
      for (var index = 0; index < elementBonus.length; index++) {
+
function resetBattleChoiceButton(battleChoice, category) {
        var elementBonusName = mapping.elementBonus[index];
+
  var { defaultButtonContent, buttonContent } = battleChoice[category];
        var elementResistanceName = mapping.elementResistance[index];
 
  
        if (attacker[elementBonusName] && victim[elementBonusName]) {
+
  showElement(defaultButtonContent);
          elementBonus[index] =
+
  hideElement(buttonContent);
            (attacker[elementBonusName] - victim[elementResistanceName]) / 200;
 
        } else {
 
          elementBonus[index] = attacker[elementBonusName] / 2000;
 
        }
 
      }
 
  
      var victimType = victim.type;
+
  if (category === "attacker") {
 +
    filterAttackTypeSelectionMonster(battleChoice.attackType);
 +
  }
 +
}
  
      if (victimType !== -1) {
+
function isPC(character) {
        typeBonus = attacker[mapping.typeFlag[victimType]];
+
  if (character.race === 0 || character.race === 1) {
      }
+
    return false;
 +
  }
 +
  return true;
 +
}
  
      monsterBonus = attacker.monsterBonus;
+
function isBoss(character) {
 +
  return character.race === 0 && character.rank >= 5;
 +
}
  
      if (isStone(victim)) {
+
function isStone(character) {
        stoneBonus = attacker.stoneBonus;
+
  return character.race === 1;
      }
+
}
 +
 
 +
function isMeleeAttacker(monster) {
 +
  return monster.attack == 0;
 +
}
  
      if (isBoss(victim)) {
+
function isRangeAttacker(monster) {
        averageDamage += attacker.bossDamage;
+
  return monster.attack == 1;
      }
+
}
  
      if (attacker.onYohara === "on") {
+
function isMagicAttacker(monster) {
        var sungmaStrDifference = attacker.sungmaStr - attacker.sungmaStrMalus;
+
  return monster.attack == 2;
 +
}
  
        if (sungmaStrDifference >= 0) {
+
function isMagicClass(character) {
          sungMaStrBonus = sungmaStrDifference;
+
  return character.race === "shaman" || character.class === "black_magic";
        } else {
+
}
          sungmaStrMalus = 0.5;
 
        }
 
      }
 
    }
 
  
    averageDamage += attacker.averageDamage;
+
function isDispell(character, skillId) {
    rankBonus = getRankBonus(attacker);
+
  return character.class === "weaponary" && skillId === 6;
    damageBonus = attacker.damageBonus;
+
}
  
    if (attacker.empireMalus === "on") {
+
function isPolymorph(character) {
      empireMalus = 10;
+
   return isChecked(character.isPolymorph);
    }
+
}
   } else {
 
    if (isPC(victim)) {
 
      if (victim.isMarried === "on") {
 
        if (victim.harmonyBracelet === "on") {
 
          monsterResistanceMarriage = getMarriageBonusValue(
 
            victim,
 
            marriageTable,
 
            "harmonyBracelet"
 
          );
 
        }
 
  
        if (victim.harmonyNecklace === "on") {
+
function isRiding(character) {
          defenseMarriage = getMarriageBonusValue(
+
  return isChecked(character.isRiding);
            victim,
+
}
            marriageTable,
 
            "harmonyNecklace"
 
          );
 
        }
 
      }
 
  
      monsterResistance = victim.monsterResistance;
+
function isBow(weaponType) {
 +
  return weaponType === 2;
 +
}
  
      for (var index = 0; index < elementBonus.length; index++) {
+
function calcAttackFactor(attacker, victim) {
        var elementBonusName = mapping.elementBonus[index];
+
  function calcCoeffK(dex, level) {
        var elementResistanceName = mapping.elementResistance[index];
+
    return Math.min(90, Math.floor((2 * dex + level) / 3));
 +
  }
  
        if (attacker[elementBonusName]) {
+
  var K1 = calcCoeffK(attacker.polymorphDex, attacker.level);
          elementBonus[index] =
+
  var K2 = calcCoeffK(victim.polymorphDex, attacker.level);
            (attacker[elementBonusName] - victim[elementResistanceName]) / 125;
 
        }
 
      }
 
  
      if (attacker.attack == 0) {
+
  var AR = (K1 + 210) / 300;
        missPercentage = victim.meleeBlock;
+
  var ER = (((2 * K2 + 5) / (K2 + 95)) * 3) / 10;
        averageDamageResistance = victim.averageDamageResistance;
 
        blessingBonus = calcBlessingBonus(skillPowerTable, victim);
 
      } else if (attacker.attack == 1) {
 
        missPercentage = victim.arrowBlock;
 
        weaponDefense = victim.arrowDefense;
 
        averageDamageResistance = victim.averageDamageResistance;
 
        blessingBonus = calcBlessingBonus(skillPowerTable, victim);
 
      } else {
 
        missPercentage = victim.arrowBlock;
 
        skillDamageResistance = victim.skillDamageResistance;
 
        magicResistance = victim.magicResistance;
 
      }
 
  
      missPercentage +=
+
  return truncateNumber(AR - ER, 8);
        victim.meleeArrowBlock -
+
}
        (missPercentage * victim.meleeArrowBlock) / 100;
 
    }
 
  
    typeBonus = 1;
+
function calcMainAttackValue(attacker) {
    damageMultiplier = attacker.damageMultiplier;
+
  var leadership = 0;
  }
+
  var weaponGrowth = 0;
  
   if (isPC(victim)) {
+
   if (isPC(attacker)) {
     if (victim.biologist70 === "on") {
+
     weaponGrowth = attacker.weapon.growth;
      victim.defense = floorMultiplication(victim.defense, 1.1);
+
     leadership = attacker.leadership;
     }
+
  }
    criticalHitPercentage = Math.max(
 
      0,
 
      criticalHitPercentage - victim.criticalHitResistance
 
    );
 
    piercingHitPercentage = Math.max(
 
      0,
 
      piercingHitPercentage - victim.piercingHitResistance
 
    );
 
  
    if (isMagicClass(victim)) {
+
  return 2 * (attacker.level + weaponGrowth) + leadership;
      defensePercent = (-2 * victim.magicDefense * victim.defensePercent) / 100;
+
}
    } else {
 
      defensePercent = (-2 * victim.defense * victim.defensePercent) / 100;
 
    }
 
  
     if (victim.steelDragonElixir === "on") {
+
function calcStatAttackValue(character) {
       steelDragonElixir = 10;
+
  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;
 
   }
 
   }
 +
}
  
  missPercentage = Math.min(100, missPercentage);
+
function calcSecondaryAttackValue(attacker, isPlayerVsPlayer) {
 +
  var attackValueOther = 0;
  
   var battleValues = {
+
   var minAttackValue = 0;
    missPercentage: missPercentage,
+
   var maxAttackValue = 0;
    adjustCoeff: 0,
 
    attackValueCoeff:
 
      1 + (attackValuePercent + Math.min(100, attackMeleeMagic)) / 100,
 
    attackValueMarriage: attackValueMarriage,
 
    monsterResistanceMarriageCoeff: 1 - monsterResistanceMarriage / 100,
 
    monsterResistanceCoeff: 1 - monsterResistance / 100,
 
    typeBonusCoeff: 1 + typeBonus / 100,
 
    raceBonusCoeff: raceBonus / 100,
 
    raceResistanceCoeff: raceResistance / 100,
 
    monsterBonusCoeff: 1 + monsterBonus / 100,
 
    stoneBonusCoeff: 1 + stoneBonus / 100,
 
    elementBonusCoeff: elementBonus,
 
    damageMultiplier: damageMultiplier,
 
    defense: victim.defense,
 
    defenseMarriage: defenseMarriage,
 
    magicResistanceCoeff: magicResistanceToCoeff(magicResistance),
 
    weaponDefenseCoeff: 1 - weaponDefense / 100,
 
    tigerStrengthCoeff: 1 + tigerStrength / 100,
 
    blessingBonusCoeff: 1 - blessingBonus / 100,
 
    extraPiercingHitCoeff: extraPiercingHitPercentage / 200,
 
    averageDamageCoeff: 1 + averageDamage / 100,
 
    averageDamageResistanceCoeff:
 
      1 - Math.min(99, averageDamageResistance) / 100,
 
    skillDamageResistanceCoeff: 1 - Math.min(99, skillDamageResistance) / 100,
 
    rankBonusCoeff: 1 + rankBonus / 100,
 
    defensePercent: Math.floor(defensePercent),
 
    damageBonusCoeff: damageBonus / 100,
 
    empireMalusCoeff: 1 - empireMalus / 100,
 
    sungMaStrBonusCoeff: 1 + sungMaStrBonus / 10000,
 
    sungmaStrMalusCoeff: sungmaStrMalus,
 
    whiteDragonElixirCoeff: 1 + whiteDragonElixir / 100,
 
    steelDragonElixirCoeff: 1 - steelDragonElixir / 100,
 
   };
 
  
   criticalHitPercentage = Math.min(
+
   var minWeaponAttackValue = 0;
    criticalHitPercentage + criticalHitPercentageMarriage,
+
   var maxWeaponAttackValue = 0;
    100
 
  );
 
   piercingHitPercentage = Math.min(
 
    piercingHitPercentage + piercingHitPercentageMarriage,
 
    100
 
  );
 
  
   battleValues.damagesTypeCombinaison = [
+
   var minAttackValueSlash = 0;
    {
+
   var maxAttackValueSlash = 0;
      criticalHit: false,
 
      piercingHit: false,
 
      weight:
 
        (100 - criticalHitPercentage) *
 
        (100 - piercingHitPercentage) *
 
        (100 - missPercentage),
 
      name: "Coup classique",
 
    },
 
    {
 
      criticalHit: true,
 
      piercingHit: false,
 
      weight:
 
        criticalHitPercentage *
 
        (100 - piercingHitPercentage) *
 
        (100 - missPercentage),
 
      name: "Coup critique",
 
    },
 
    {
 
      criticalHit: false,
 
      piercingHit: true,
 
      weight:
 
        (100 - criticalHitPercentage) *
 
        piercingHitPercentage *
 
        (100 - missPercentage),
 
      name: "Coup perçant",
 
    },
 
    {
 
      criticalHit: true,
 
      piercingHit: true,
 
      weight:
 
        criticalHitPercentage * piercingHitPercentage * (100 - missPercentage),
 
      name: "Coup critique + coup perçant",
 
    },
 
   ];
 
  
   return battleValues;
+
   if (isPC(attacker)) {
}
+
    var { type, isSerpent, minAttackValue, maxAttackValue, growth } =
 +
      attacker.weapon;
  
function createSkillBattleValues(
+
    if (isSerpent) {
  attacker,
+
      minAttackValue = Math.max(0, attacker.minAttackValueRandom - growth);
  attackerWeapon,
+
      maxAttackValue = Math.max(
  victim,
+
        minAttackValue,
  mapping,
+
        attacker.maxAttackValueRandom - growth
  marriageTable,
+
      );
  magicSkill
+
    }
) {
 
  var adjustCoeff = 0;
 
  var attackValuePercent = 0;
 
  var attackMeleeMagic = 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 damageMultiplier = 1;
 
  var useDamages = 1;
 
  var defense = victim.defense;
 
  var magicResistance = 0;
 
  var weaponDefense = 0;
 
  var tigerStrength = 0;
 
  var criticalHitPercentage = attacker.criticalHit;
 
  var piercingHitPercentage = attacker.piercingHit;
 
  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);
+
    minWeaponAttackValue = minAttackValue + growth;
  computeHorse(attacker);
+
    maxWeaponAttackValue = maxAttackValue + growth;
  
  if (isPC(attacker)) {
+
    minAttackValueSlash = Math.min(
    attackValuePercent = attacker.attackValuePercent;
+
      attacker.minAttackValueSlash,
     attackMeleeMagic = attacker.attackMeleeMagic;
+
      attacker.maxAttackValueSlash
 
+
    );
     var weaponType = attackerWeapon[1];
+
     maxAttackValueSlash = Math.max(
 +
      attacker.minAttackValueSlash,
 +
      attacker.maxAttackValueSlash
 +
     );
  
     if (attacker.class === "archery") {
+
     attackValueOther += attacker.attackValue;
      if (weaponType !== 2) {
+
 
        useDamages = 0;
+
    if (isBow(type) && !isPolymorph(attacker)) {
        weaponType = 2;
+
       attackValueOther += 25;
      }
 
       defense = 0;
 
 
     }
 
     }
 +
  } else {
 +
    minAttackValue = attacker.minAttackValue;
 +
    maxAttackValue = attacker.maxAttackValue;
 +
  }
  
    var weaponDefenseName = mapping.defenseWeapon[weaponType];
+
  minAttackValue += attacker.minAttackValuePolymorph;
    var weaponDefenseBreakName = mapping.breakWeapon[weaponType];
+
  maxAttackValue += attacker.maxAttackValuePolymorph;
  
    if (victim.hasOwnProperty(weaponDefenseName)) {
+
  attackValueOther += attacker.statAttackValue;
      weaponDefense = victim[weaponDefenseName];
+
  attackValueOther += attacker.horseAttackValue;
    }
 
  
    if (attacker.whiteDragonElixir === "on") {
+
  var weaponInterval = maxAttackValue - minAttackValue + 1;
      whiteDragonElixir = 10;
+
  var slashInterval = maxAttackValueSlash - minAttackValueSlash + 1;
    }
 
  
    if (isPC(victim)) {
+
  var totalCardinal = weaponInterval * slashInterval * 1_000_000;
      typeBonus = Math.max(1, attacker.humanBonus - victim.humanResistance);
+
  var minInterval = Math.min(weaponInterval, slashInterval);
      raceBonus = attacker[mapping.raceBonus[victim.race]];
 
      raceResistance = victim[mapping.raceResistance[attacker.race]];
 
 
 
      calcElementCoeffPvP(elementBonus, mapping, attacker, victim);
 
  
      if (weaponType !== 2 && attacker.hasOwnProperty(weaponDefenseBreakName)) {
+
  minAttackValue += minAttackValueSlash;
        weaponDefense -= attacker[weaponDefenseBreakName];
+
  maxAttackValue += maxAttackValueSlash;
      }
 
  
      criticalHitPercentage = 0;
+
  return {
     } else {
+
    minAttackValue: minAttackValue,
      if (attacker.isMarried === "on") {
+
    maxAttackValue: maxAttackValue,
        if (attacker.loveNecklace === "on") {
+
    attackValueOther: attackValueOther,
          attackValueMarriage = getMarriageBonusValue(
+
     totalCardinal: totalCardinal,
            attacker,
+
    weights: calcWeights(minAttackValue, maxAttackValue, minInterval),
            marriageTable,
+
    possibleDamageCount: maxAttackValue - minAttackValue + 1,
            "loveNecklace"
+
    weapon: {
          );
+
      minAttackValue: minWeaponAttackValue,
        }
+
      maxAttackValue: maxWeaponAttackValue,
 +
      interval: weaponInterval,
 +
    },
 +
    isPlayerVsPlayer: isPlayerVsPlayer,
 +
  };
 +
}
  
        if (attacker.loveEarrings === "on") {
+
function calcMagicAttackValue(attacker, isPlayerVsPlayer) {
          criticalHitPercentage += getMarriageBonusValue(
+
  var minMagicAttackValueSlash = 0;
            attacker,
+
  var maxMagicAttackValueSlash = 0;
            marriageTable,
 
            "loveEarrings"
 
          );
 
        }
 
  
        if (attacker.harmonyEarrings === "on") {
+
  var minWeaponAttackValue = 0;
          piercingHitPercentage += getMarriageBonusValue(
+
  var maxWeaponAttackValue = 0;
            attacker,
 
            marriageTable,
 
            "harmonyEarrings"
 
          );
 
        }
 
      }
 
  
      if (attacker.tigerStrength === "on") {
+
  var {
        tigerStrength = 40;
+
    isSerpent,
      }
+
    minMagicAttackValue,
 +
    maxMagicAttackValue,
 +
    minAttackValue,
 +
    maxAttackValue,
 +
    growth,
 +
  } = attacker.weapon;
  
      for (var index = 0; index < elementBonus.length; index++) {
+
  if (isSerpent) {
        var elementBonusName = mapping.elementBonus[index];
+
    minMagicAttackValue = Math.max(0, attacker.minMagicAttackValueRandom);
        var elementResistanceName = mapping.elementResistance[index];
+
    maxMagicAttackValue = Math.max(
 +
      minMagicAttackValue,
 +
      attacker.maxMagicAttackValueRandom
 +
    );
 +
    minAttackValue = Math.max(0, attacker.minAttackValueRandom - growth);
 +
    maxAttackValue = Math.max(
 +
      minAttackValue,
 +
      attacker.maxAttackValueRandom - growth
 +
    );
 +
  } else {
 +
    minMagicAttackValue += growth;
 +
    maxMagicAttackValue += growth;
 +
  }
  
        if (attacker[elementBonusName] && victim[elementBonusName]) {
+
  minWeaponAttackValue = minAttackValue + growth;
          elementBonus[index] =
+
  maxWeaponAttackValue = maxAttackValue + growth;
            (attacker[elementBonusName] - victim[elementResistanceName]) / 200;
 
        } else {
 
          elementBonus[index] = attacker[elementBonusName] / 2000;
 
        }
 
      }
 
  
      var victimType = victim.type;
+
  minMagicAttackValueSlash = Math.min(
 +
    attacker.minMagicAttackValueSlash,
 +
    attacker.maxMagicAttackValueSlash
 +
  );
 +
  maxMagicAttackValueSlash = Math.max(
 +
    attacker.minMagicAttackValueSlash,
 +
    attacker.maxMagicAttackValueSlash
 +
  );
  
      if (victimType !== -1) {
+
  var weaponInterval = maxMagicAttackValue - minMagicAttackValue + 1;
        typeBonus = attacker[mapping.typeFlag[victimType]];
+
  var slashInterval = maxMagicAttackValueSlash - minMagicAttackValueSlash + 1;
      }
 
  
      monsterBonus = attacker.monsterBonus;
+
  var totalCardinal = weaponInterval * slashInterval * 1_000_000;
 +
  var minInterval = Math.min(weaponInterval, slashInterval);
  
      if (isStone(victim)) {
+
  minMagicAttackValue += minMagicAttackValueSlash;
        stoneBonus = attacker.stoneBonus;
+
  maxMagicAttackValue += maxMagicAttackValueSlash;
      }
 
  
       if (isBoss(victim)) {
+
  return {
        skillDamage += attacker.skillBossDamage;
+
    minMagicAttackValue: minMagicAttackValue,
      }
+
    maxMagicAttackValue: maxMagicAttackValue,
 +
    magicAttackValueAugmentation: getMagicAttackValueAugmentation(
 +
      minMagicAttackValue,
 +
      maxMagicAttackValue,
 +
       attacker.magicAttackValue
 +
    ),
 +
    totalCardinal: totalCardinal,
 +
    weights: calcWeights(minMagicAttackValue, maxMagicAttackValue, minInterval),
 +
    possibleDamageCount: maxMagicAttackValue - minMagicAttackValue + 1,
 +
    weapon: {
 +
      minAttackValue: minWeaponAttackValue,
 +
      maxAttackValue: maxWeaponAttackValue,
 +
      interval: maxWeaponAttackValue - minWeaponAttackValue + 1,
 +
    },
 +
    isPlayerVsPlayer: isPlayerVsPlayer,
 +
  };
 +
}
  
      if (attacker.onYohara === "on") {
+
function getPolymorphPower(polymorphPoint, polymorphPowerTable) {
        var sungmaStrDifference = attacker.sungmaStr - attacker.sungmaStrMalus;
+
  return polymorphPowerTable[polymorphPoint];
 +
}
  
        if (sungmaStrDifference >= 0) {
+
function getSkillPower(skillPoint, skillPowerTable) {
          sungMaStrBonus = sungmaStrDifference;
+
  return skillPowerTable[skillPoint];
        } else {
+
}
          sungmaStrMalus = 0.5;
 
        }
 
      }
 
    }
 
  
    skillDamage += attacker.skillDamage;
+
function getMarriageBonusValue(character, marriageTable, itemName) {
    rankBonus = getRankBonus(attacker);
+
  var index;
    damageBonus = attacker.damageBonus;
+
  var lovePoint = character.lovePoint;
  
     if (attacker.empireMalus === "on") {
+
  if (lovePoint < 65) {
      empireMalus = 10;
+
     index = 0;
     }
+
  } else if (lovePoint < 80) {
 +
    index = 1;
 +
  } else if (lovePoint < 100) {
 +
     index = 2;
 
   } else {
 
   } else {
     if (isPC(victim)) {
+
     index = 3;
      if (victim.isMarried === "on" && victim.harmonyBracelet === "on") {
+
  }
        monsterResistanceMarriage = getMarriageBonusValue(
+
 
          victim,
+
  return marriageTable[itemName][index];
          marriageTable,
+
}
          "harmonyBracelet"
 
        );
 
      }
 
  
      monsterResistance = victim.monsterResistance;
+
function calcDamageWithPrimaryBonuses(damage, bonusValues) {
 +
  damage = Math.floor((damage * bonusValues.attackValueCoeff) / 100);
  
      for (var index = 0; index < elementBonus.length; index++) {
+
  damage += bonusValues.attackValueMarriage;
        var elementBonusName = mapping.elementBonus[index];
 
        var elementResistanceName = mapping.elementResistance[index];
 
  
        if (attacker[elementBonusName]) {
+
  damage = Math.floor(
          elementBonus[index] =
+
    (damage * bonusValues.monsterResistanceMarriageCoeff) / 100
            (attacker[elementBonusName] - victim[elementResistanceName]) / 125;
+
  );
        }
+
  damage = Math.floor((damage * bonusValues.monsterResistanceCoeff) / 100);
      }
+
 
    }
+
  damage += Math.floor((damage * bonusValues.typeBonusCoeff) / 100);
 +
  damage +=
 +
    Math.floor((damage * bonusValues.raceBonusCoeff) / 100) -
 +
    Math.floor((damage * bonusValues.raceResistanceCoeff) / 100);
 +
  damage += Math.floor((damage * bonusValues.stoneBonusCoeff) / 100);
 +
  damage += Math.floor((damage * bonusValues.monsterBonusCoeff) / 100);
 +
 
 +
  var elementBonusCoeff = bonusValues.elementBonusCoeff;
 +
 
 +
  damage +=
 +
    Math.trunc((damage * elementBonusCoeff[0]) / 10000) +
 +
    Math.trunc((damage * elementBonusCoeff[1]) / 10000) +
 +
    Math.trunc((damage * elementBonusCoeff[2]) / 10000) +
 +
    Math.trunc((damage * elementBonusCoeff[3]) / 10000) +
 +
    Math.trunc((damage * elementBonusCoeff[4]) / 10000) +
 +
    Math.trunc((damage * elementBonusCoeff[5]) / 10000);
 +
 
 +
  damage = Math.floor(damage * bonusValues.damageMultiplier);
 +
 
 +
  return damage;
 +
}
  
     typeBonus = 1;
+
function calcFinalDamage(
     damageMultiplier = attacker.damageMultiplier;
+
  damage,
 +
  bonusValues,
 +
  damageType,
 +
  minPiercingDamage,
 +
  tempDamage
 +
) {
 +
  if (damageType.isPiercingHit) {
 +
     damage += bonusValues.defenseBoost + Math.min(0, minPiercingDamage);
 +
     damage += Math.floor(
 +
      (tempDamage * bonusValues.extraPiercingHitCoeff) / 1000
 +
    );
 
   }
 
   }
  
   criticalHitPercentage = skillChanceReduction(criticalHitPercentage);
+
   damage = Math.floor((damage * bonusValues.averageDamageCoeff) / 100);
   piercingHitPercentage = skillChanceReduction(piercingHitPercentage);
+
  damage = Math.floor(
 +
    (damage * bonusValues.averageDamageResistanceCoeff) / 100
 +
  );
 +
   damage = Math.floor((damage * bonusValues.skillDamageCoeff) / 100);
 +
  damage = Math.floor((damage * bonusValues.skillDamageResistanceCoeff) / 100);
 +
  damage = Math.floor((damage * bonusValues.rankBonusCoeff) / 100);
 +
 
 +
  if (bonusValues.useDarkProtection) {
 +
    var { darkProtectionPoint, darkProtectionSp } = bonusValues;
  
  if (isPC(victim)) {
+
    var damageReduction = Math.floor(damage / 3);
     criticalHitPercentage = Math.max(
+
     var spAbsorption = Math.floor(
       0,
+
       (damageReduction * darkProtectionPoint) / 100
      criticalHitPercentage - victim.criticalHitResistance
 
 
     );
 
     );
    piercingHitPercentage = Math.max(
 
      0,
 
      piercingHitPercentage - victim.piercingHitResistance
 
    );
 
    skillDamageResistance = victim.skillDamageResistance;
 
  
     if (isMagicClass(victim)) {
+
     if (spAbsorption <= darkProtectionSp) {
       defensePercent = (-2 * victim.magicDefense * victim.defensePercent) / 100;
+
       damage -= damageReduction;
 
     } else {
 
     } else {
       defensePercent = (-2 * victim.defense * victim.defensePercent) / 100;
+
       damage -= Math.floor((darkProtectionSp * 100) / darkProtectionPoint);
    }
 
 
 
    if (victim.steelDragonElixir === "on") {
 
      steelDragonElixir = 10;
 
 
     }
 
     }
 
   }
 
   }
  
   if (magicSkill) {
+
   damage = Math.max(0, damage + bonusValues.defensePercent);
    adjustCoeff = 0.5;
+
  damage += Math.min(
    attackValuePercent = attacker.attackMagic;
+
     300,
     attackValueMarriage = 0;
+
     Math.floor((damage * bonusValues.damageBonusCoeff) / 100)
     defense = 0;
+
  );
    magicResistance = victim.magicResistance;
+
  damage = Math.floor((damage * bonusValues.empireMalusCoeff) / 10);
    weaponDefense = 0;
+
  damage = Math.floor((damage * bonusValues.sungMaStrBonusCoeff) / 10000);
   }
+
   damage -= Math.floor(damage * bonusValues.sungmaStrMalusCoeff);
  
   var battleValues = {
+
   damage = Math.floor((damage * bonusValues.whiteDragonElixirCoeff) / 100);
    weaponBonusCoeff: 1,
+
  damage = Math.floor((damage * bonusValues.steelDragonElixirCoeff) / 100);
    adjustCoeff: adjustCoeff,
 
    attackValueCoeff:
 
      1 + (attackValuePercent + Math.min(100, attackMeleeMagic)) / 100,
 
    attackValueMarriage: attackValueMarriage,
 
    monsterResistanceMarriageCoeff: 1 - monsterResistanceMarriage / 100,
 
    monsterResistanceCoeff: 1 - monsterResistance / 100,
 
    typeBonusCoeff: 1 + typeBonus / 100,
 
    raceBonusCoeff: raceBonus / 100,
 
    raceResistanceCoeff: raceResistance / 100,
 
    monsterBonusCoeff: 1 + monsterBonus / 100,
 
    stoneBonusCoeff: 1 + stoneBonus / 100,
 
    elementBonusCoeff: elementBonus,
 
    damageMultiplier: damageMultiplier,
 
    useDamages: useDamages,
 
    defense: defense,
 
    tigerStrengthCoeff: 1 + tigerStrength / 100,
 
    piercingHitDefense: victim.defense,
 
    magicResistanceCoeff: magicResistanceToCoeff(magicResistance),
 
    weaponDefenseCoeff: 1 - weaponDefense / 100,
 
    skillDamageCoeff: 1 + skillDamage / 100,
 
    skillDamageResistanceCoeff: 1 - Math.min(99, skillDamageResistance) / 100,
 
    rankBonusCoeff: 1 + rankBonus / 100,
 
    defensePercent: Math.floor(defensePercent),
 
    damageBonusCoeff: damageBonus / 100,
 
    empireMalusCoeff: 1 - empireMalus / 100,
 
    sungMaStrBonusCoeff: 1 + sungMaStrBonus / 10000,
 
    sungmaStrMalusCoeff: sungmaStrMalus,
 
    whiteDragonElixirCoeff: 1 + whiteDragonElixir / 100,
 
    steelDragonElixirCoeff: 1 - steelDragonElixir / 100,
 
  };
 
  
   criticalHitPercentage = Math.min(criticalHitPercentage, 100);
+
   return damage;
  piercingHitPercentage = Math.min(piercingHitPercentage, 100);
+
}
  
   battleValues.damagesTypeCombinaison = [
+
function saveFinalDamage(
    {
+
   damage,
      criticalHit: false,
+
  bonusValues,
      piercingHit: false,
+
  damageType,
      weight: (100 - criticalHitPercentage) * (100 - piercingHitPercentage),
+
  weapon,
      name: "Coup classique",
+
  minPiercingDamage,
    },
+
  damageWithPrimaryBonuses,
    {
+
  damageWeighted,
      criticalHit: true,
+
  weight
      piercingHit: false,
+
) {
      weight: criticalHitPercentage * (100 - piercingHitPercentage),
+
  damage = Math.floor(damage * bonusValues.magicResistanceCoeff);
      name: "Coup critique",
+
  damage = Math.trunc((damage * bonusValues.weaponDefenseCoeff) / 100);
    },
+
  damage = Math.floor((damage * bonusValues.tigerStrengthCoeff) / 100);
    {
+
  damage = Math.floor((damage * bonusValues.berserkBonusCoeff) / 100);
      criticalHit: false,
+
  damage = Math.floor((damage * bonusValues.fearBonusCoeff) / 100);
      piercingHit: true,
+
   damage = Math.floor((damage * bonusValues.blessingBonusCoeff) / 100);
      weight: (100 - criticalHitPercentage) * piercingHitPercentage,
 
      name: "Coup perçant",
 
    },
 
    {
 
      criticalHit: true,
 
      piercingHit: true,
 
      weight: criticalHitPercentage * piercingHitPercentage,
 
      name: "Coup critique + coup perçant",
 
    },
 
   ];
 
  
   return battleValues;
+
   const isCriticalHit = damageType.isCriticalHit;
}
 
  
function updateBattleValues(battleValues, skillInfo, attackerWeapon) {
+
  if (isCriticalHit && bonusValues.isPlayerVsPlayer) {
  var weaponBonus = 0;
+
    for (
  var skillWard = 0;
+
      let weaponAttackValue = weapon.minAttackValue;
  var skillBonus = 0;
+
      weaponAttackValue <= weapon.maxAttackValue;
  var skillBonusByBonus = 0;
+
      weaponAttackValue++
 +
    ) {
 +
      const criticalDamage = damage + 2 * weaponAttackValue;
 +
      const finalDamage = calcFinalDamage(
 +
        criticalDamage,
 +
        bonusValues,
 +
        damageType,
 +
        minPiercingDamage,
 +
        damageWithPrimaryBonuses
 +
      );
  
  if (skillInfo.hasOwnProperty("weaponBonus")) {
+
      addKeyValue(damageWeighted, finalDamage, weight / weapon.interval);
     var [weaponType, weaponBonusValue] = skillInfo.weaponBonus;
+
     }
 
+
  } else {
     if (weaponType === attackerWeapon[1]) {
+
     if (isCriticalHit) {
       weaponBonus = weaponBonusValue;
+
       damage *= 2;
 
     }
 
     }
  }
 
  
  if (skillInfo.skillBonus) {
+
    damage = calcFinalDamage(
     skillBonus = skillInfo.skillBonus;
+
      damage,
  }
+
      bonusValues,
 +
      damageType,
 +
      minPiercingDamage,
 +
      damageWithPrimaryBonuses
 +
     );
  
  if (skillInfo.skillWard) {
+
    addKeyValue(damageWeighted, damage, weight);
    skillWard = skillInfo.skillWard;
 
 
   }
 
   }
 
  if (skillInfo.skillBonusByBonus) {
 
    skillBonusByBonus = skillInfo.skillBonusByBonus;
 
  }
 
 
  if (skillInfo.removeWeaponReduction) {
 
    battleValues.weaponDefenseCoeff = 1;
 
  }
 
 
  battleValues.weaponBonusCoeff = 1 + weaponBonus / 100;
 
  battleValues.skillWardCoeff = 1 - skillWard / 100;
 
  battleValues.skillBonusCoeff = 1 + skillBonus / 100;
 
  battleValues.skillBonusByBonusCoeff = 1 + skillBonusByBonus / 100;
 
 
}
 
}
  
function calcPhysicalDamages(
+
function saveFinalSkillDamage(
   attacker,
+
   damage,
   attackerWeapon,
+
   bonusValues,
   victim,
+
   damageType,
   tableResult,
+
   weapon,
   mapping,
+
   minPiercingDamage,
   constants
+
   damageWeighted,
 +
  weight,
 +
  savedCriticalDamage
 
) {
 
) {
   var battleValues = createPhysicalBattleValues(
+
   damage = Math.floor(damage * bonusValues.magicResistanceCoeff);
    attacker,
+
  damage = Math.trunc((damage * bonusValues.weaponDefenseCoeff) / 100);
    attackerWeapon,
 
    victim,
 
    mapping,
 
    constants.polymorphPowerTable,
 
    constants.marriageTable,
 
    constants.skillPowerTable
 
  );
 
  
   var sumDamages = 0;
+
   damage -= bonusValues.defense;
  var minMaxDamages = { min: Infinity, max: 0 };
 
  clearTableResult(tableResult);
 
  
   var attackFactor = calcAttackFactor(attacker, victim);
+
   damage = floorMultiplication(damage, bonusValues.skillWardCoeff);
   var mainAttackValue = calcMainAttackValue(attacker, attackerWeapon);
+
   damage = floorMultiplication(damage, bonusValues.skillBonusCoeff);
  var [
 
    minAttackValue,
 
    maxAttackValue,
 
    attackValueOther,
 
    minInterval,
 
    totalCardinal,
 
  ] = calcSecondaryAttackValue(attacker, attackerWeapon);
 
  
   totalCardinal *= 100;
+
   const tempDamage = Math.floor(
 +
    (damage * bonusValues.skillBonusByBonusCoeff) / 100
 +
  );
  
   if (battleValues.missPercentage) {
+
   damage = Math.floor(
    addRowToTableResult(tableResult, "Miss");
+
     (tempDamage * bonusValues.magicAttackValueCoeff) / 100 + 0.5
     addToTableResult(tableResult, { 0: battleValues.missPercentage / 100 });
+
   );
   }
+
   damage = Math.floor((damage * bonusValues.tigerStrengthCoeff) / 100);
 
 
  var lastWeightsLimit = maxAttackValue - minInterval + 1;
 
   var firstWeightLimit = minAttackValue + minInterval - 1;
 
  
   for (var damagesType of battleValues.damagesTypeCombinaison) {
+
   const isCriticalHit = damageType.isCriticalHit;
    if (!damagesType.weight) {
 
      0;
 
      continue;
 
    }
 
  
     var damagesWeighted = {};
+
  if (isCriticalHit && bonusValues.isPlayerVsPlayer) {
     addRowToTableResult(tableResult, damagesType.name);
+
     const { minAttackValue, maxAttackValue, interval } = weapon;
 +
     const criticalWeight = weight / interval;
  
 
     for (
 
     for (
       var attackValue = minAttackValue;
+
       let weaponAttackValue = minAttackValue;
       attackValue <= maxAttackValue;
+
       weaponAttackValue <= maxAttackValue;
       attackValue++
+
       weaponAttackValue++
 
     ) {
 
     ) {
       var weight;
+
       const criticalDamage = damage + 2 * weaponAttackValue;
  
       if (attackValue > lastWeightsLimit) {
+
       if (
         weight = maxAttackValue - attackValue + 1;
+
        savedCriticalDamage &&
       } else if (attackValue < firstWeightLimit) {
+
        savedCriticalDamage.hasOwnProperty(criticalDamage) &&
         weight = attackValue - minAttackValue + 1;
+
         minPiercingDamage >= 0
      } else {
+
       ) {
         weight = minInterval;
+
         const savedDamage = savedCriticalDamage[criticalDamage];
 +
        damageWeighted[savedDamage] += criticalWeight;
 +
         continue;
 
       }
 
       }
  
       var secondaryAttackValue = 2 * attackValue + attackValueOther;
+
       const finalDamage = calcFinalDamage(
      var rawDamages =
+
         criticalDamage,
         mainAttackValue +
+
         bonusValues,
         floorMultiplication(attackFactor, secondaryAttackValue);
+
         damageType,
      var damagesWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
+
         minPiercingDamage,
         rawDamages,
+
         tempDamage
         battleValues
+
       );
      );
 
 
 
      var minPiercingDamages =
 
        damagesWithPrimaryBonuses -
 
        battleValues.defense +
 
         battleValues.defenseMarriage;
 
 
 
       if (minPiercingDamages <= 2) {
 
        for (var damages = 1; damages <= 5; damages++) {
 
          var finalDamages = calcDamageWithSecondaryBonuses(
 
            damages,
 
            battleValues,
 
            damagesType,
 
            minPiercingDamages,
 
            damagesWithPrimaryBonuses
 
          );
 
  
          addKeyValue(
+
      addKeyValue(damageWeighted, finalDamage, criticalWeight);
            damagesWeighted,
+
      if (savedCriticalDamage) {
            finalDamages,
+
         savedCriticalDamage[criticalDamage] = finalDamage;
            (weight * damagesType.weight) / (5 * totalCardinal)
 
          );
 
          sumDamages += (finalDamages * weight * damagesType.weight) / 5;
 
        }
 
      } else {
 
         var finalDamages = calcDamageWithSecondaryBonuses(
 
          minPiercingDamages,
 
          battleValues,
 
          damagesType,
 
          minPiercingDamages,
 
          damagesWithPrimaryBonuses
 
        );
 
 
 
        addKeyValue(
 
          damagesWeighted,
 
          finalDamages,
 
          (weight * damagesType.weight) / totalCardinal
 
        );
 
        sumDamages += finalDamages * weight * damagesType.weight;
 
 
       }
 
       }
 +
    }
 +
  } else {
 +
    if (isCriticalHit) {
 +
      damage *= 2;
 
     }
 
     }
  
     addToTableResult(tableResult, damagesWeighted, minMaxDamages);
+
     damage = calcFinalDamage(
  }
+
      damage,
 +
      bonusValues,
 +
      damageType,
 +
      minPiercingDamage,
 +
      tempDamage
 +
    );
 +
 
 +
    addKeyValue(damageWeighted, damage, weight);
  
  if (minMaxDamages.min === Infinity) {
+
     return damage;
     minMaxDamages.min = 0;
 
 
   }
 
   }
 
  return [sumDamages / totalCardinal, minMaxDamages];
 
 
}
 
}
  
function calcBlessingBonus(skillPowerTable, victim) {
+
function computePolymorphPoint(attacker, victim, polymorphPowerTable) {
   if (victim.isBlessed !== "on") {
+
   attacker.polymorphDex = attacker.dex;
    return 0;
+
   victim.polymorphDex = victim.dex;
   }
 
  
   var int = victim.intBlessing;
+
   attacker.minAttackValuePolymorph = 0;
   var dex = victim.dexBlessing;
+
   attacker.maxAttackValuePolymorph = 0;
  var skillPower = getSkillPower(victim["skillBlessing"], skillPowerTable);
 
  
   if (!skillPower) {
+
   if (isPC(attacker) && isPolymorph(attacker)) {
     return 0;
+
     var polymorphPowerPct =
  }
+
      getPolymorphPower(attacker.polymorphPoint, polymorphPowerTable) / 100;
 +
    var polymorphMonster = createMonster(attacker.polymorphMonster, null, true);
  
  var blessingBonus = floorMultiplication(
+
    var polymorphStr = floorMultiplication(
    ((int * 0.3 + 5) * (2 * skillPower + 0.5) + 0.3 * dex) / (skillPower + 2.3),
+
      polymorphPowerPct,
     1
+
      polymorphMonster.str
  );
+
     );
  
  if (victim.class === "dragon" && victim.blessingOnself === "on") {
+
    attacker.polymorphDex += floorMultiplication(
    blessingBonus = floorMultiplication(blessingBonus, 1.1);
+
      polymorphPowerPct,
  }
+
      polymorphMonster.dex
 +
    );
  
  return blessingBonus;
+
    attacker.minAttackValuePolymorph = floorMultiplication(
}
+
      polymorphPowerPct,
 +
      polymorphMonster.minAttackValue
 +
    );
 +
    attacker.maxAttackValuePolymorph = floorMultiplication(
 +
      polymorphPowerPct,
 +
      polymorphMonster.maxAttackValue
 +
    );
  
function getSkillFormula(
+
    if (!attacker.weapon) {
  skillPowerTable,
+
      attacker.maxAttackValuePolymorph += 1;
  skillId,
+
    }
  attacker,
 
  attackFactor,
 
  victim
 
) {
 
  var skillFormula;
 
  var skillInfo = {};
 
  
  var attackerClass = attacker.class;
+
    attacker.attackValue = 0;
  var lv = attacker.level;
 
  var vit = attacker.vit;
 
  var str = attacker.str;
 
  var int = attacker.int;
 
  var dex = attacker.dex;
 
  
  if (skillId <= 9) {
+
    if (isMagicClass(attacker)) {
    var skillPower = getSkillPower(
+
      attacker.statAttackValue = 2 * (polymorphStr + attacker.int);
       attacker["attackSkill" + skillId],
+
    } else {
      skillPowerTable
+
       attacker.statAttackValue = 2 * (polymorphStr + attacker.str);
    );
+
    }
 +
  }
 +
}
 +
 
 +
function computeHorse(attacker) {
 +
  attacker.horseAttackValue = 0;
  
     var improvedBySkillBonus = false;
+
  if (isPC(attacker) && isRiding(attacker) && !isPolymorph(attacker)) {
     var improvedByBonus = false;
+
     var horseConstant = 30;
 +
 
 +
    if (attacker.class === "weaponary") {
 +
      horseConstant = 60;
 +
     }
 +
 
 +
    attacker.horseAttackValue = floorMultiplication(
 +
      2 * attacker.level + attacker.statAttackValue,
 +
      attacker.horsePoint / horseConstant
 +
    );
 +
  }
 +
}
  
    if (attackerClass === "body") {
+
function getRankBonus(attacker) {
      switch (skillId) {
+
  if (!isChecked(attacker.lowRank)) {
        // Triple lacération
+
    return 0;
        case 1:
+
  }
          skillFormula = function (atk) {
+
 
            return floorMultiplication(
+
  switch (attacker.rank) {
              1.1 * atk + (0.5 * atk + 1.5 * str) * skillPower,
+
    case "aggressive":
              1
+
      return 1;
            );
+
    case "fraudulent":
          };
+
      return 2;
          improvedByBonus = true;
+
    case "malicious":
          break;
+
      return 3;
        // Moulinet à l'épée
+
    case "cruel":
        case 2:
+
      return 5;
          skillFormula = function (atk) {
+
  }
            return floorMultiplication(
+
 
              3 * atk + (0.8 * atk + 5 * str + 3 * dex + vit) * skillPower,
+
  return 0;
              1
+
}
            );
+
 
          };
+
function calcElementCoeffPvP(elementBonus, mapping, attacker, victim) {
          improvedByBonus = true;
+
  var minElementMalus = 0;
          improvedBySkillBonus = true;
+
  var maxDifference = 0;
          break;
+
  var savedElementDifferences = [];
        // Accélération
+
  var elementBonusIndex = 0;
        case 5:
+
 
          skillFormula = function (atk) {
+
  for (var index = 0; index < elementBonus.length; index++) {
            return floorMultiplication(
+
    if (!attacker[mapping.elementBonus[index]]) {
              2 * atk + (atk + dex * 3 + str * 7 + vit) * skillPower,
+
      continue;
              1
+
    }
            );
+
 
          };
+
    var elementDifference =
          improvedByBonus = true;
+
      attacker[mapping.elementBonus[index]] -
          break;
+
      victim[mapping.elementResistance[index]];
        // Volonté de vivre
+
 
        case 6:
+
    if (elementDifference >= 0) {
          skillFormula = function (atk) {
+
      elementBonus[elementBonusIndex] = 10 * elementDifference;
            return floorMultiplication(
+
      minElementMalus -= elementDifference;
              (3 * atk + (atk + 1.5 * str) * skillPower) * 1.07,
+
      maxDifference = Math.max(maxDifference, elementDifference);
              1
+
      elementBonusIndex++;
            );
+
    } else {
          };
+
      savedElementDifferences.push(elementDifference);
          break;
+
    }
        case 9:
+
  }
          skillFormula = function (atk) {
+
 
            return floorMultiplication(
+
  if (!savedElementDifferences.length) {
              3 * atk +
+
    return;
                (0.9 * atk + 500.5 + 5 * str + 3 * dex + lv) * skillPower,
+
  }
              1
+
 
            );
+
  minElementMalus += maxDifference;
          };
+
  savedElementDifferences.sort(compareNumbers);
          break;
+
 
      }
+
  for (var index = 0; index < savedElementDifferences.length; index++) {
    } else if (attackerClass === "mental") {
+
    var elementDifference = savedElementDifferences[index];
      switch (skillId) {
+
 
        // Attaque de l'esprit
+
    elementBonus[elementBonusIndex + index] =
        case 1:
+
      10 * Math.max(minElementMalus, elementDifference);
          skillFormula = function (atk) {
+
 
            return floorMultiplication(
+
    minElementMalus = Math.min(
              2.3 * atk + (4 * atk + 4 * str + vit) * skillPower,
+
      0,
              1
+
      Math.max(minElementMalus, minElementMalus - elementDifference)
            );
+
    );
          };
+
  }
          improvedByBonus = true;
+
}
          improvedBySkillBonus = true;
+
 
          break;
+
function calcCriticalHitChance(criticalHitPercentage) {
        // Attaque de la paume
+
  if (criticalHitPercentage <= 9) {
        case 2:
+
    return Math.floor((criticalHitPercentage + 5) / 5);
          skillFormula = function (atk) {
+
  }
            return floorMultiplication(
+
  return Math.floor((criticalHitPercentage + 5) / 6);
              2.3 * atk + (3 * atk + 4 * str + 3 * vit) * skillPower,
+
}
              1
+
 
            );
+
function calcCriticalSkillChance(criticalHitPercentage) {
          };
+
  if (criticalHitPercentage === 0) {
          improvedByBonus = true;
+
    return 0;
          break;
+
  } else if (criticalHitPercentage <= 9) {
        // Charge
+
    return Math.floor((criticalHitPercentage + 7) / 3);
        case 3:
+
  }
          skillFormula = function (atk) {
+
  return Math.floor((criticalHitPercentage + 5) / 3);
            return floorMultiplication(
+
}
              2 * atk + (2 * atk + 2 * dex + 2 * vit + 4 * str) * skillPower,
+
 
              1
+
function calcPiercingSkillChance(piercingHitPercentage) {
            );
+
  if (piercingHitPercentage <= 9) {
          };
+
    return Math.floor(piercingHitPercentage / 2);
          break;
+
  }
        // Coup d'épée
+
  return 5 + Math.floor((piercingHitPercentage - 10) / 4);
        case 5:
+
}
          skillFormula = function (atk) {
+
 
            return floorMultiplication(
+
function magicResistanceToCoeff(magicResistance) {
              2 * atk + (atk + 3 * dex + 5 * str + vit) * skillPower,
+
  if (magicResistance) {
              1
+
    return 2000 / (6 * magicResistance + 1000) - 1;
            );
+
  }
          };
+
  return 1;
          improvedByBonus = true;
+
}
          break;
+
 
        // Orbe de l'épée
+
function createBattleValues(attacker, victim, battle, skillType) {
        case 6:
+
  var {
          skillFormula = function (atk) {
+
    mapping,
            return floorMultiplication(
+
    constants: { polymorphPowerTable, skillPowerTable, marriageTable },
              (2 * atk + (2 * atk + 2 * dex + 2 * vit + 4 * str) * skillPower) *
+
  } = battle;
                1.1,
+
  var calcAttackValues;
              1
+
 
            );
+
  var missPercentage = 0;
          };
+
  var attackValueMeleeMagic = 0;
          break;
+
  var attackValueMarriage = 0;
        // Tremblement de terre
+
  var monsterResistanceMarriage = 0;
        case 9:
+
  var monsterResistance = 0;
          skillFormula = function (atk) {
+
  var typeBonus = 0;
            return floorMultiplication(
+
  var raceBonus = 0;
              3 * atk +
+
  var raceResistance = 0;
                (0.9 * atk + 500.5 + 5 * str + 3 * dex + lv) * skillPower,
+
  var stoneBonus = 0;
              1
+
  var monsterBonus = 0;
            );
+
  var elementBonus = [0, 0, 0, 0, 0, 0]; // fire, ice, lightning, earth, darkness, wind, order doesn't matter
          };
+
  var defenseMarriage = 0;
          break;
+
  var damageMultiplier = 1;
 +
  var useDamage = 1;
 +
  var defense = victim.defense;
 +
  var defenseBoost = defense;
 +
  var magicResistance = 0;
 +
  var weaponDefense = 0;
 +
  var tigerStrength = 0;
 +
  var berserkBonus = 0;
 +
  var blessingBonus = 0;
 +
  var fearBonus = 0;
 +
  var magicAttackValueMeleeMagic = 0;
 +
  var criticalHitPercentage = attacker.criticalHit;
 +
  var isPlayerVsPlayer = false;
 +
  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 useDarkProtection = false;
 +
  var darkProtectionPoint = 0;
 +
  var defensePercent = 0;
 +
  var damageBonus = 0;
 +
  var empireMalus = 0;
 +
  var sungMaStrBonus = 0;
 +
  var sungmaStrMalus = 0;
 +
  var whiteDragonElixir = 0;
 +
  var steelDragonElixir = 0;
 +
 
 +
  attacker.statAttackValue = calcStatAttackValue(attacker);
 +
 
 +
  computePolymorphPoint(attacker, victim, polymorphPowerTable);
 +
  computeHorse(attacker);
 +
 
 +
  if (isPC(attacker)) {
 +
    if (weaponData.hasOwnProperty(attacker.weapon)) {
 +
      attacker.weapon = createWeapon(attacker.weapon);
 +
    } else {
 +
      attacker.weapon = createWeapon(0);
 +
    }
 +
 
 +
    attacker.weapon.getValues(attacker.weaponUpgrade);
 +
 
 +
    attackValueMeleeMagic =
 +
      attacker.attackValuePercent + Math.min(100, attacker.attackMeleeMagic);
 +
 
 +
    var weaponType = attacker.weapon.type;
 +
 
 +
    if (skillType && attacker.class === "archery") {
 +
      if (weaponType !== 2) {
 +
        useDamage = 0;
 +
        weaponType = 2;
 
       }
 
       }
     } else if (attackerClass === "blade_fight") {
+
      defense = 0;
       switch (skillId) {
+
     }
        // Embuscade
+
 
        case 1:
+
    var weaponDefenseName = mapping.defenseWeapon[weaponType];
          skillFormula = function (atk) {
+
    var weaponDefenseBreakName = mapping.breakWeapon[weaponType];
            return floorMultiplication(
+
 
              atk + (1.2 * atk + 600 + 4 * dex + 4 * str) * skillPower,
+
    if (victim.hasOwnProperty(weaponDefenseName)) {
              1
+
       weaponDefense = victim[weaponDefenseName];
            );
+
    }
          };
+
 
           skillInfo.weaponBonus = [1, 50];
+
    if (isChecked(attacker.whiteDragonElixir)) {
          improvedByBonus = true;
+
      whiteDragonElixir = 10;
           improvedBySkillBonus = true;
+
    }
           break;
+
 
         // Attaque rapide
+
    if (isPC(victim)) {
         case 2:
+
      isPlayerVsPlayer = true;
          skillFormula = function (atk) {
+
 
            return floorMultiplication(
+
      if (!skillType) {
              atk + (1.6 * atk + 250 + 7 * dex + 7 * str) * skillPower,
+
        if (weaponType === 2 && !isPolymorph(attacker)) {
              1
+
          missPercentage = victim.arrowBlock;
            );
+
        } else {
          };
+
           missPercentage = victim.meleeBlock;
          skillInfo.weaponBonus = [1, 35];
+
        }
          improvedByBonus = true;
+
        missPercentage +=
          break;
+
           victim.meleeArrowBlock -
        // Dague filante
+
           (missPercentage * victim.meleeArrowBlock) / 100;
        case 3:
+
 
          skillFormula = function (atk) {
+
         criticalHitPercentage = calcCriticalHitChance(criticalHitPercentage);
            return floorMultiplication(
+
         berserkBonus = calcBerserkBonus(skillPowerTable, victim);
              2 * atk + (0.5 * atk + 9 * dex + 7 * str) * skillPower,
+
        blessingBonus = calcBlessingBonus(skillPowerTable, victim);
              1
+
        fearBonus = calcFearBonus(skillPowerTable, victim);
            );
+
        averageDamageResistance = victim.averageDamageResistance;
          };
+
      }
          improvedByBonus = true;
+
 
          break;
+
      typeBonus = Math.max(1, attacker.humanBonus - victim.humanResistance);
        // Brume empoisonnée
+
      raceBonus = attacker[mapping.raceBonus[victim.race]];
        case 5:
+
      raceResistance = victim[mapping.raceResistance[attacker.race]];
          skillFormula = function (atk) {
+
 
            return floorMultiplication(
+
      calcElementCoeffPvP(elementBonus, mapping, attacker, victim);
              2 * lv + (atk + 3 * str + 18 * dex) * skillPower,
+
 
              1
+
      if (weaponType !== 2 && attacker.hasOwnProperty(weaponDefenseBreakName)) {
             );
+
        weaponDefense -= attacker[weaponDefenseBreakName];
          };
+
      }
          improvedByBonus = true;
+
    } else {
           break;
+
      if (isChecked(attacker.isMarried)) {
         // Poison insidieux
+
        if (isChecked(attacker.loveNecklace)) {
         case 6:
+
          attackValueMarriage = getMarriageBonusValue(
           skillFormula = function (atk) {
+
             attacker,
             return floorMultiplication(
+
            marriageTable,
              (2 * lv + (atk + 3 * str + 18 * dex) * skillPower) * 1.1,
+
            "loveNecklace"
              1
+
           );
            );
+
         }
          };
+
 
          break;
+
         if (isChecked(attacker.loveEarrings)) {
         // Étoiles brillantes
+
           criticalHitPercentage += getMarriageBonusValue(
        case 9:
+
             attacker,
           skillFormula = function (atk) {
+
            marriageTable,
             return floorMultiplication(
+
            "loveEarrings"
              atk + (1.7 * atk + 500.5 + 6 * dex + 5 * lv) * skillPower,
+
          );
              1
+
        }
            );
+
 
          };
+
         if (isChecked(attacker.harmonyEarrings)) {
          break;
+
           piercingHitPercentage += getMarriageBonusValue(
 +
             attacker,
 +
            marriageTable,
 +
            "harmonyEarrings"
 +
          );
 +
        }
 
       }
 
       }
    } else if (attackerClass === "archery") {
+
 
      switch (skillId) {
+
      if (isChecked(attacker.tigerStrength)) {
         // Tir à répétition
+
         tigerStrength = 40;
        // case 1:
+
      }
        //  skillFormula = function (atk) {
+
 
        //    return floorMultiplication(
+
       for (var index = 0; index < elementBonus.length; index++) {
        //       atk + 0.2 * atk * Math.floor(2 + 6 * skillPower) + (0.8 * atk + 8 * dex * attackFactor + 2 * int) * skillPower,
+
         var elementBonusName = mapping.elementBonus[index];
         //      1
+
         var elementResistanceName = mapping.elementResistance[index];
        //    );
+
 
         //  };
+
         if (attacker[elementBonusName] && victim[elementBonusName]) {
        //  improvedByBonus = true;
+
           elementBonus[index] =
        //  break;
+
             50 * (attacker[elementBonusName] - victim[elementResistanceName]);
         // Pluie de flèches
+
        } else {
        case 2:
+
           elementBonus[index] = 5 * attacker[elementBonusName];
           skillFormula = function (atk) {
+
        }
             return floorMultiplication(
+
      }
              atk + (1.7 * atk + 5 * dex + str) * skillPower,
+
 
              1
+
      var victimType = victim.type;
            );
+
 
          };
+
      if (victimType !== -1) {
           improvedByBonus = true;
+
        typeBonus = attacker[mapping.typeFlag[victimType]];
          break;
+
      }
        // Flèche de feu
+
 
        case 3:
+
      monsterBonus = attacker.monsterBonus;
          skillFormula = function (atk) {
+
 
            return floorMultiplication(
+
      if (isStone(victim)) {
              1.5 * atk + (2.6 * atk + 0.9 * int + 200) * skillPower,
+
        stoneBonus = attacker.stoneBonus;
              1
+
      }
            );
+
 
          };
+
      if (isBoss(victim)) {
          improvedByBonus = true;
+
        if (skillType) {
          improvedBySkillBonus = true;
+
          skillDamage += attacker.skillBossDamage;
          break;
+
        } else {
        // Foulée de plume
+
           averageDamage += attacker.bossDamage;
        case 4:
+
         }
          skillFormula = function (atk) {
+
      }
            return floorMultiplication(
+
 
              (3 * dex + 200 + 2 * str + 2 * int) * skillPower,
+
      if (isChecked(attacker.onYohara)) {
              1
+
        var sungmaStrDifference = attacker.sungmaStr - attacker.sungmaStrMalus;
            );
+
 
          };
+
        if (sungmaStrDifference >= 0) {
          skillInfo.removeWeaponReduction = true;
+
           sungMaStrBonus = sungmaStrDifference;
          break;
+
         } else {
        // Flèche empoisonnée
+
           sungmaStrMalus = 0.5;
        case 5:
+
        }
          skillFormula = function (atk) {
 
            return floorMultiplication(
 
              atk +
 
                (1.4 * atk + 150 + 7 * dex + 4 * str + 4 * int) * skillPower,
 
              1
 
            );
 
          };
 
           improvedByBonus = true;
 
          break;
 
         // Coup étincelant
 
        case 6:
 
          skillFormula = function (atk) {
 
            return floorMultiplication(
 
              (atk +
 
                (1.2 * atk + 150 + 6 * dex + 3 * str + 3 * int) * skillPower) *
 
                1.2,
 
              1
 
            );
 
           };
 
          improvedByBonus = true;
 
          break;
 
         // Tir tempête
 
        case 9:
 
           skillFormula = function (atk) {
 
            return floorMultiplication(
 
              1.9 * atk + (2.6 * atk + 500.5) * skillPower,
 
              1
 
            );
 
          };
 
          break;
 
 
       }
 
       }
     } else if (attackerClass === "weaponary") {
+
     }
       switch (skillId) {
+
 
        // Toucher brûlant
+
    if (skillType) {
         case 1:
+
      skillDamage += attacker.skillDamage;
           skillFormula = function (atk) {
+
    } else {
             return floorMultiplication(
+
      averageDamage += attacker.averageDamage;
              atk +
+
    }
                2 * lv +
+
 
                2 * int +
+
    rankBonus = getRankBonus(attacker);
                (2 * atk + 4 * str + 14 * int) * skillPower,
+
    damageBonus = attacker.damageBonus;
              1
+
 
             );
+
    if (isChecked(attacker.empireMalus)) {
          };
+
       empireMalus = 1;
          improvedByBonus = true;
+
    }
          improvedBySkillBonus = true;
+
  } else {
          break;
+
    if (isPC(victim)) {
         // Tourbillon du dragon
+
      if (isChecked(victim.isMarried)) {
         case 2:
+
         if (isChecked(victim.harmonyBracelet)) {
          skillFormula = function (atk) {
+
           monsterResistanceMarriage = getMarriageBonusValue(
            return floorMultiplication(
+
             victim,
              1.1 * atk +
+
            marriageTable,
                2 * lv +
+
            "harmonyBracelet"
                2 * int +
+
          );
                (1.5 * atk + str + 12 * int) * skillPower,
+
        }
              1
+
 
             );
+
        if (isChecked(victim.harmonyNecklace) && !skillType) {
          };
+
          defenseMarriage = getMarriageBonusValue(
          improvedByBonus = true;
+
            victim,
          break;
+
            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]);
 +
        }
 
       }
 
       }
    } else if (attackerClass === "black_magic") {
+
 
      switch (skillId) {
+
      if (!skillType) {
        // Attaque des ténèbres
+
        if (isMeleeAttacker(attacker)) {
        case 1:
+
          missPercentage = victim.meleeBlock;
           skillFormula = function (mav) {
+
          averageDamageResistance = victim.averageDamageResistance;
            return floorMultiplication(
+
          berserkBonus = calcBerserkBonus(skillPowerTable, victim);
              40 +
+
          blessingBonus = calcBlessingBonus(skillPowerTable, victim);
                5 * lv +
+
           fearBonus = calcFearBonus(skillPowerTable, victim);
                2 * int +
+
        } else if (isRangeAttacker(attacker)) {
                (13 * int + 6 * mav + 75) * attackFactor * skillPower,
+
          missPercentage = victim.arrowBlock;
              1
+
          weaponDefense = victim.arrowDefense;
            );
+
          averageDamageResistance = victim.averageDamageResistance;
          };
+
          berserkBonus = calcBerserkBonus(skillPowerTable, victim);
           improvedByBonus = true;
+
          blessingBonus = calcBlessingBonus(skillPowerTable, victim);
           improvedBySkillBonus = true;
+
          fearBonus = calcFearBonus(skillPowerTable, victim);
           break;
+
        } else if (isMagicAttacker(attacker)) {
         // Attaque de flammes
+
           missPercentage = victim.arrowBlock;
        // case 2:
+
           skillDamageResistance = victim.skillDamageResistance;
         //  skillFormula = function (mav) {
+
           magicResistance = victim.magicResistance;
        //    return floorMultiplication(
+
         }
        //      5 * lv + 2 * int + (7 * int + 8 * mav + 4 * str + 2 * vit + 190) * skillPower,
+
 
        //       1
+
         missPercentage +=
        //     );
+
          victim.meleeArrowBlock -
        //   };
+
          (missPercentage * victim.meleeArrowBlock) / 100;
        //   improvedByBonus = true;
+
       }
        //  break;
+
    }
        // Esprit de flammes
+
 
        case 3:
+
    typeBonus = 1;
          skillFormula = function (mav) {
+
     damageMultiplier = attacker.damageMultiplier;
            return floorMultiplication(
+
   }
              30 +
+
 
                2 * lv +
+
   if (skillType) {
                2 * int +
+
    criticalHitPercentage = calcCriticalSkillChance(criticalHitPercentage);
                (7 * int + 6 * mav + 350) * attackFactor * skillPower,
+
    piercingHitPercentage = calcPiercingSkillChance(piercingHitPercentage);
              1
+
  }
            );
+
 
          };
+
  if (isPC(victim)) {
          break;
+
    if (!skillType && isChecked(victim.biologist70)) {
        // Frappe de l'esprit
+
      defenseBoost = Math.floor((defenseBoost * 110) / 100);
        // case 5:
+
    }
        //  skillFormula = function (mav) {
+
 
        //     return floorMultiplication(
+
    criticalHitPercentage = Math.max(
        //       40 + 2 * lv + 2 * int + (2 * vit + 2 * dex + 13 * int + 6 * mav + 190) * attackFactor * skillPower,
+
      0,
        //      1
+
      criticalHitPercentage - victim.criticalHitResistance
        //     );
+
    );
        //  };
+
    piercingHitPercentage = Math.max(
        //   break;
+
      0,
        // Orbe des ténèbres
+
      piercingHitPercentage - victim.piercingHitResistance
        case 6:
+
    );
          skillFormula = function (mav) {
+
 
            return floorMultiplication(
+
    if (skillType) {
              120 +
+
      skillDamageResistance = victim.skillDamageResistance;
                6 * lv +
+
    }
                (5 * vit + 5 * dex + 29 * int + 9 * mav) *
+
 
                  attackFactor *
+
    if (
                  skillPower,
+
      victim.useDarkProtection &&
              1
+
      victim.class === "black_magic" &&
            );
+
      victim.skillDarkProtection
          };
+
    ) {
          improvedByBonus = true;
+
      useDarkProtection = true;
          break;
+
      darkProtectionPoint = calcDarkProtectionPoint(skillPowerTable, victim);
 +
    }
 +
 
 +
     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)) {
 +
      typeBonus = 0;
 +
      raceBonus = 0;
 +
      raceResistance = 0;
 +
      stoneBonus = 0;
 +
      monsterBonus = 0;
 +
      for (var index = 0; index < elementBonus.length; index++) {
 +
        elementBonus[index] = 0;
 
       }
 
       }
     } else if (attackerClass === "dragon") {
+
     } else {
      switch (skillId) {
+
      magicResistance = victim.magicResistance;
        // Talisman volant
+
    }
        case 1:
+
    weaponDefense = 0;
          skillFormula = function (mav) {
+
    calcAttackValues = calcMagicAttackValue;
            return floorMultiplication(
+
  } else {
              70 +
+
    calcAttackValues = calcSecondaryAttackValue;
                5 * lv +
+
  }
                (18 * int + 7 * str + 5 * mav + 50) * attackFactor * skillPower,
+
 
              1
+
  missPercentage = Math.min(100, missPercentage);
            );
+
 
          };
+
  var bonusValues = {
          skillInfo.weaponBonus = [4, 10];
+
    missPercentage: missPercentage,
          improvedByBonus = true;
+
    weaponBonusCoeff: 1,
          break;
+
    attackValueCoeff: 100 + attackValueMeleeMagic,
         // Dragon chassant
+
    attackValueMarriage: attackValueMarriage,
         case 2:
+
    monsterResistanceMarriageCoeff: 100 - monsterResistanceMarriage,
          skillFormula = function (mav) {
+
    monsterResistanceCoeff: 100 - monsterResistance,
            return floorMultiplication(
+
    typeBonusCoeff: typeBonus,
              60 +
+
    raceBonusCoeff: raceBonus,
                5 * lv +
+
    raceResistanceCoeff: raceResistance,
                (16 * int + 6 * dex + 6 * mav + 120) *
+
    stoneBonusCoeff: stoneBonus,
                  attackFactor *
+
    monsterBonusCoeff: monsterBonus,
                  skillPower,
+
    elementBonusCoeff: elementBonus,
              1
+
    damageMultiplier: damageMultiplier,
            );
+
    useDamage: useDamage,
          };
+
    defense: defense,
          skillInfo.weaponBonus = [4, 10];
+
    defenseBoost: defenseBoost,
          improvedByBonus = true;
+
    defenseMarriage: defenseMarriage,
          improvedBySkillBonus = true;
+
    tigerStrengthCoeff: 100 + tigerStrength,
          break;
+
    magicResistanceCoeff: magicResistanceToCoeff(magicResistance),
         // Rugissement du dragon
+
    weaponDefenseCoeff: 100 - weaponDefense,
         case 3:
+
    berserkBonusCoeff: 100 + berserkBonus,
          skillFormula = function (mav) {
+
    blessingBonusCoeff: 100 - blessingBonus,
            return floorMultiplication(
+
    fearBonusCoeff: 100 - fearBonus,
              70 +
+
    magicAttackValueCoeff: 100 + magicAttackValueMeleeMagic,
                3 * lv +
+
    isPlayerVsPlayer: isPlayerVsPlayer,
                (20 * int + 3 * str + 10 * mav + 100) *
+
    extraPiercingHitCoeff: 5 * extraPiercingHitPercentage,
                  attackFactor *
+
    averageDamageCoeff: 100 + averageDamage,
                  skillPower,
+
    averageDamageResistanceCoeff: 100 - Math.min(99, averageDamageResistance),
              1
+
    skillDamageCoeff: 100 + skillDamage,
            );
+
    skillDamageResistanceCoeff: 100 - Math.min(99, skillDamageResistance),
          };
+
    useDarkProtection: useDarkProtection,
          skillInfo.weaponBonus = [4, 10];
+
    darkProtectionPoint: darkProtectionPoint,
          improvedByBonus = true;
+
    darkProtectionSp: victim.darkProtectionSp,
          break;
+
    rankBonusCoeff: 100 + rankBonus,
      }
+
    defensePercent: Math.floor(defensePercent),
     } else if (attackerClass === "heal") {
+
    damageBonusCoeff: Math.min(20, damageBonus),
       switch (skillId) {
+
    empireMalusCoeff: 10 - empireMalus,
        // Jet de foudre
+
    sungMaStrBonusCoeff: 10000 + sungMaStrBonus,
        case 1:
+
    sungmaStrMalusCoeff: sungmaStrMalus,
          skillFormula = function (mav) {
+
    whiteDragonElixirCoeff: 100 + whiteDragonElixir,
            return floorMultiplication(
+
    steelDragonElixirCoeff: 100 - steelDragonElixir,
              60 +
+
  };
                5 * lv +
+
 
                (8 * int + 2 * dex + 8 * mav + 10 * int) *
+
  criticalHitPercentage = Math.min(criticalHitPercentage, 100);
                  attackFactor *
+
  piercingHitPercentage = Math.min(piercingHitPercentage, 100);
                  skillPower,
+
 
              1
+
  var damageTypeCombinaison = [
            );
+
    {
          };
+
      isCriticalHit: false,
          skillInfo.weaponBonus = [6, 10];
+
      isPiercingHit: false,
          improvedByBonus = true;
+
      weight:
          break;
+
         (100 - criticalHitPercentage) *
        // Invocation de foudre
+
         (100 - piercingHitPercentage) *
        case 2:
+
        (100 - missPercentage),
          skillFormula = function (mav) {
+
      name: "normalHit",
            return floorMultiplication(
+
    },
              40 +
+
    {
                4 * lv +
+
      isCriticalHit: true,
                (13 * int + 2 * str + 10 * mav + 10.5 * int) *
+
      isPiercingHit: false,
                  attackFactor *
+
      weight:
                  skillPower,
+
        criticalHitPercentage *
              1
+
        (100 - piercingHitPercentage) *
            );
+
        (100 - missPercentage),
          };
+
      name: "criticalHit",
          skillInfo.weaponBonus = [6, 10];
+
    },
          improvedByBonus = true;
+
    {
          improvedBySkillBonus = true;
+
      isCriticalHit: false,
          break;
+
      isPiercingHit: true,
        // Griffe de foudre
+
      weight:
        case 3:
+
        (100 - criticalHitPercentage) *
          skillFormula = function (mav) {
+
         piercingHitPercentage *
            return floorMultiplication(
+
         (100 - missPercentage),
              50 +
+
      name: "piercingHit",
                5 * lv +
+
    },
                (8 * int + 2 * str + 8 * mav + 400.5) *
+
    {
                  attackFactor *
+
      isCriticalHit: true,
                  skillPower,
+
      isPiercingHit: true,
              1
+
      weight:
            );
+
        criticalHitPercentage * piercingHitPercentage * (100 - missPercentage),
          };
+
      name: "criticalPiercingHit",
          improvedByBonus = true;
+
    },
          break;
+
  ];
      }
+
 
    } else if (attackerClass === "lycan") {
+
  return {
      switch (skillId) {
+
    attacker: attacker,
        // Déchiqueter
+
    victim: victim,
        // case 1:
+
    attackFactor: calcAttackFactor(attacker, victim),
        //   skillFormula = function (atk) {
+
    mainAttackValue: calcMainAttackValue(attacker),
        //     return floorMultiplication(
+
    attackValues: calcAttackValues(attacker, isPlayerVsPlayer),
        //      1.1 * atk + (0.3 * atk + 1.5 * str) * skillPower,
+
    bonusValues: bonusValues,
        //      1
+
    damageTypeCombinaison: damageTypeCombinaison,
        //     );
+
  };
        //   };
+
}
        //   skillInfo.weaponBonus = [5, 54];
+
 
        //   improvedByBonus = true;
+
function updateBattleValues(battleValues, skillFormula, skillInfo) {
        //   break;
+
  var weaponBonus = 0;
        // Souffle de loup
+
  var skillWard = 0;
        case 2:
+
  var skillBonus = 0;
          skillFormula = function (atk) {
+
  var skillBonusByBonus = 0;
            return floorMultiplication(
+
  var { attacker: attacker, bonusValues: bonusValues } = battleValues;
              2 * atk + (atk + 3 * dex + 5 * str + vit) * skillPower,
+
  var {
              1
+
    range: [minVariation, maxVariation],
            );
+
  } = skillInfo;
          };
+
  var variationLength = maxVariation - minVariation + 1;
          skillInfo.weaponBonus = [5, 35];
+
 
          improvedByBonus = true;
+
  if (skillInfo.hasOwnProperty("weaponBonus")) {
          improvedBySkillBonus = true;
+
    var [weaponType, weaponBonusValue] = skillInfo.weaponBonus;
          break;
+
 
         // Bond de loup
+
     if (weaponType === attacker.weapon.type) {
         case 3:
+
       weaponBonus = weaponBonusValue;
           skillFormula = function (atk) {
+
    }
             return floorMultiplication(
+
  }
               atk + (1.6 * atk + 200 + 7 * dex + 7 * str) * skillPower,
+
 
 +
  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.possibleDamageCount *= 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 calcBerserkBonus(skillPowerTable, victim) {
 +
  if (!isChecked(victim.useBerserk) || victim.class !== "body") {
 +
    return 0;
 +
  }
 +
 
 +
  var skillPower = getSkillPower(victim.skillBerserk, skillPowerTable);
 +
 
 +
  if (!skillPower) {
 +
    return 0;
 +
  }
 +
 
 +
  var berserkBonus = Math.floor(skillPower * 25);
 +
 
 +
  return berserkBonus;
 +
}
 +
 
 +
function calcBlessingBonus(skillPowerTable, victim) {
 +
  if (!isChecked(victim.isBlessed)) {
 +
    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(victim.blessingOnself)) {
 +
     blessingBonus = floorMultiplication(blessingBonus, 1.1);
 +
   }
 +
 
 +
  return blessingBonus;
 +
}
 +
 
 +
function calcFearBonus(skillPowerTable, victim) {
 +
   if (!isChecked(victim.useFear) || victim.class !== "weaponary") {
 +
    return 0;
 +
  }
 +
 
 +
  var skillPower = getSkillPower(victim.skillFear, skillPowerTable);
 +
 
 +
  if (!skillPower) {
 +
    return 0;
 +
   }
 +
 
 +
  var fearBonus = 5 + Math.floor(skillPower * 20);
 +
 
 +
   return fearBonus;
 +
}
 +
 
 +
function calcDarkProtectionPoint(skillPowerTable, victim) {
 +
  var skillPower = getSkillPower(victim.skillDarkProtection, skillPowerTable);
 +
 
 +
  return floorMultiplication(100 - victim.int * 0.84 * skillPower, 1);
 +
}
 +
 
 +
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
 
               1
 
             );
 
             );
 
           };
 
           };
          skillInfo.weaponBonus = [5, 35];
 
 
           improvedByBonus = true;
 
           improvedByBonus = true;
 
           break;
 
           break;
         // Griffe de loup
+
         // Moulinet à l'épée
         case 4:
+
         case 2:
 
           skillFormula = function (atk) {
 
           skillFormula = function (atk) {
 
             return floorMultiplication(
 
             return floorMultiplication(
               3 * atk + (0.8 * atk + 6 * str + 2 * dex + vit) * skillPower,
+
               3 * atk + (0.8 * atk + 5 * str + 3 * dex + vit) * skillPower,
 
               1
 
               1
 
             );
 
             );
 
           };
 
           };
 
           improvedByBonus = true;
 
           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;
 
           break;
 
       }
 
       }
     }
+
     } else if (attackerClass === "mental") {
    if (improvedBySkillBonus) {
+
       switch (skillId) {
       skillInfo.skillBonus =
+
        // Attaque de l'esprit
        16 * getSkillPower(attacker.skillBonus, skillPowerTable);
+
        case 1:
 
+
          skillFormula = function (atk) {
      var skillWardChoice = victim.skillWardChoice;
+
            return floorMultiplication(
 
+
              2.3 * atk + (4 * atk + 4 * str + vit) * skillPower,
      if (skillWardChoice && skillWardChoice === attackerClass) {
+
              1
        skillInfo.skillWard =
+
            );
          24 * getSkillPower(victim.skillWard, skillPowerTable);
+
          };
      }
+
          improvedByBonus = true;
    }
+
          improvedBySkillBonus = true;
 
+
          break;
    if (improvedByBonus) {
+
        // Attaque de la paume
      skillInfo.skillBonusByBonus = attacker["skillBonus" + skillId];
+
        case 2:
    }
+
          skillFormula = function (atk) {
  } else {
+
            return floorMultiplication(
    var skillPower = getSkillPower(
+
              2.3 * atk + (3 * atk + 4 * str + 3 * vit) * skillPower,
      attacker["horseSkill" + skillId],
+
              1
      skillPowerTable
+
            );
    );
+
          };
 
+
          improvedByBonus = true;
    switch (skillId) {
+
          break;
      case 137:
+
        // Charge
        skillFormula = function (atk) {
+
        case 3:
          return floorMultiplication(atk + 2 * atk * skillPower, 1);
+
          skillFormula = function (atk) {
        };
+
            return floorMultiplication(
        break;
+
              2 * atk + (2 * atk + 2 * dex + 2 * vit + 4 * str) * skillPower,
      case 138:
+
              1
        skillFormula = function (atk) {
+
            );
          return floorMultiplication(
+
          };
            2.4 * (200 + 1.5 * lv) + 600 * skillPower,
+
          break;
            1
+
        // Coup d'épée
          );
+
        case 5:
        };
+
          skillFormula = function (atk) {
        break;
+
            return floorMultiplication(
      case 139:
+
              2 * atk + (atk + 3 * dex + 5 * str + vit) * skillPower,
        skillFormula = function (atk) {
+
              1
          return floorMultiplication(
+
            );
            2 * (200 + 1.5 * lv) + 600 * skillPower,
+
          };
            1
+
          improvedByBonus = true;
          );
+
          break;
        };
+
        // Orbe de l'épée
        break;
+
        case 6:
    }
+
          skillFormula = function (atk) {
  }
+
            return floorMultiplication(
 
+
              (2 * atk + (2 * atk + 2 * dex + 2 * vit + 4 * str) * skillPower) *
  return [skillFormula, skillInfo];
+
                1.1,
}
+
              1
 
+
            );
function calcPhysicalSkillDamages(
+
          };
  attacker,
+
          break;
  attackerWeapon,
+
        // Tremblement de terre
  victim,
+
        case 9:
  tableResult,
+
          skillFormula = function (atk, variation) {
  mapping,
+
            return floorMultiplication(
  constants,
+
              3 * atk +
  skillId
+
                (0.9 * atk + variation + 5 * str + 3 * dex + lv) * skillPower,
) {
+
              1
  var battleValues = createSkillBattleValues(
+
            );
    attacker,
+
          };
    attackerWeapon,
+
          skillInfo.range = [1, 1000];
    victim,
+
          break;
    mapping,
+
      }
    constants.marriageTable
+
    } else if (attackerClass === "blade_fight") {
  );
+
      switch (skillId) {
 
+
        // Embuscade
  var sumDamages = 0;
+
        case 1:
  var minMaxDamages = { min: Infinity, max: 0 };
+
          skillFormula = function (atk, variation) {
  clearTableResult(tableResult);
+
            return floorMultiplication(
 
+
              atk + (1.2 * atk + variation + 4 * dex + 4 * str) * skillPower,
  var attackFactor = calcAttackFactor(attacker, victim);
+
              1
  var mainAttackValue = calcMainAttackValue(attacker, attackerWeapon);
+
            );
  var [
+
          };
    minAttackValue,
+
          skillInfo.weaponBonus = [1, 50];
    maxAttackValue,
+
          skillInfo.range = [500, 700];
    attackValueOther,
+
          improvedByBonus = true;
    minInterval,
+
          improvedBySkillBonus = true;
    totalCardinal,
+
          break;
  ] = calcSecondaryAttackValue(attacker, attackerWeapon);
+
         // Attaque rapide
 
+
         case 2:
  var lastWeightsLimit = maxAttackValue - minInterval + 1;
+
          skillFormula = function (atk, variation) {
  var firstWeightLimit = minAttackValue + minInterval - 1;
+
            return floorMultiplication(
 
+
              atk + (1.6 * atk + variation + 7 * dex + 7 * str) * skillPower,
  var [skillFormula, skillInfo] = getSkillFormula(
+
              1
    constants.skillPowerTable,
+
            );
    skillId,
+
          };
    attacker,
+
          skillInfo.weaponBonus = [1, 35];
    attackFactor,
+
          skillInfo.range = [200, 300];
    victim
+
          improvedByBonus = true;
  );
+
           break;
 
+
        // Dague filante
  updateBattleValues(battleValues, skillInfo, attackerWeapon);
+
        case 3:
 
+
           skillFormula = function (atk) {
  for (var damagesType of battleValues.damagesTypeCombinaison) {
+
            return floorMultiplication(
    if (!damagesType.weight) {
+
              2 * atk + (0.5 * atk + 9 * dex + 7 * str) * skillPower,
      continue;
+
              1
    }
+
             );
 
+
           };
    var damagesWeighted = {};
+
           improvedByBonus = true;
    addRowToTableResult(tableResult, damagesType.name);
+
           break;
 
+
        // Brume empoisonnée
    for (
+
        case 5:
      var attackValue = minAttackValue;
+
           skillFormula = function (atk) {
      attackValue <= maxAttackValue;
+
             return floorMultiplication(
      attackValue++
+
              2 * lv + (atk + 3 * str + 18 * dex) * skillPower,
    ) {
+
              1
      var weight;
+
            );
 
+
           };
      if (attackValue > lastWeightsLimit) {
+
           improvedByBonus = true;
        weight = maxAttackValue - attackValue + 1;
+
          break;
      } else if (attackValue < firstWeightLimit) {
+
         // Poison insidieux
        weight = attackValue - minAttackValue + 1;
+
         case 6:
      } else {
+
          skillFormula = function (atk) {
        weight = minInterval;
+
            return floorMultiplication(
      }
+
              (2 * lv + (atk + 3 * str + 18 * dex) * skillPower) * 1.1,
 
+
              1
      var secondaryAttackValue = 2 * attackValue + attackValueOther;
+
            );
      var rawDamages =
+
           };
         mainAttackValue +
+
           break;
         floorMultiplication(attackFactor, secondaryAttackValue);
+
         // Étoiles brillantes
 
+
         case 9:
      var damagesWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
+
           skillFormula = function (atk, variation) {
        rawDamages,
+
            return floorMultiplication(
        battleValues
+
              atk + (1.7 * atk + variation + 6 * dex + 5 * lv) * skillPower,
      );
+
              1
 
+
            );
      if (damagesWithPrimaryBonuses <= 2) {
+
          };
        for (var damages = 1; damages <= 5; damages++) {
+
          skillInfo.range = [1, 1000];
           damages *= battleValues.useDamages;
+
          break;
 
 
           var damagesWithFormula = skillFormula(damages);
 
 
 
          damagesWithFormula = floorMultiplication(
 
            damagesWithFormula,
 
             battleValues.weaponBonusCoeff
 
           );
 
 
 
           var finalDamages = calcSkillDamageWithSecondaryBonuses(
 
            damagesWithFormula,
 
            battleValues,
 
            damagesType,
 
            damagesWithPrimaryBonuses
 
           );
 
 
 
           addKeyValue(
 
             damagesWeighted,
 
            finalDamages,
 
            (weight * damagesType.weight) / (5 * totalCardinal)
 
           );
 
           sumDamages += (finalDamages * weight * damagesType.weight) / 5;
 
         }
 
      } else {
 
         damagesWithPrimaryBonuses *= battleValues.useDamages;
 
 
 
        var damagesWithFormula = skillFormula(damagesWithPrimaryBonuses);
 
 
 
        damagesWithFormula = floorMultiplication(
 
          damagesWithFormula,
 
          battleValues.weaponBonusCoeff
 
        );
 
 
 
        var finalDamages = calcSkillDamageWithSecondaryBonuses(
 
           damagesWithFormula,
 
           battleValues,
 
          damagesType,
 
          damagesWithPrimaryBonuses
 
         );
 
 
 
         addKeyValue(
 
           damagesWeighted,
 
          finalDamages,
 
          (weight * damagesType.weight) / totalCardinal
 
        );
 
        sumDamages += finalDamages * weight * damagesType.weight;
 
 
       }
 
       }
     }
+
     } else if (attackerClass === "archery") {
 
+
      switch (skillId) {
    addToTableResult(tableResult, damagesWeighted, minMaxDamages);
+
        // Tir à répétition
  }
+
        // case 1:
 
+
        //   skillFormula = function (atk) {
  if (minMaxDamages.min === Infinity) {
+
        //    return floorMultiplication(
    minMaxDamages.min = 0;
+
        //      atk + 0.2 * atk * Math.floor(2 + 6 * skillPower) + (0.8 * atk + 8 * dex * attackFactor + 2 * int) * skillPower,
  }
+
        //      1
 
+
        //     );
   return [sumDamages / totalCardinal, minMaxDamages];
+
        //   };
}
+
        //   improvedByBonus = true;
 
+
        //   break;
function calcMagicSkillDamages(
+
        // Pluie de flèches
  attacker,
+
        case 2:
  attackerWeapon,
+
          skillFormula = function (atk) {
  victim,
+
            return floorMultiplication(
  tableResult,
+
              atk + (1.7 * atk + 5 * dex + str) * skillPower,
  mapping,
+
              1
  constants,
+
            );
  skillId
+
          };
) {
+
          improvedByBonus = true;
  var battleValues = createSkillBattleValues(
+
          break;
    attacker,
+
        // Flèche de feu
    attackerWeapon,
+
        case 3:
     victim,
+
          skillFormula = function (atk, variation) {
    mapping,
+
            return floorMultiplication(
    constants.marriageTable,
+
              1.5 * atk + (2.6 * atk + 0.9 * int + variation) * skillPower,
    true
+
              1
  );
+
            );
 
+
          };
   var sumDamages = 0;
+
          skillInfo.range = [100, 300];
   var minMaxDamages = { min: Infinity, max: 0 };
+
          improvedByBonus = true;
   clearTableResult(tableResult);
+
          improvedBySkillBonus = true;
 
+
          break;
  var attackFactor = calcAttackFactor(attacker, victim);
+
        // Foulée de plume
  var [minMagicAttackValue, maxMagicAttackValue, minInterval, totalCardinal] =
+
        case 4:
    calcMagicAttackValue(attacker, attackerWeapon);
+
          skillFormula = function (atk) {
 
+
            return floorMultiplication(
  var lastWeightsLimit = maxMagicAttackValue - minInterval + 1;
+
              (3 * dex + 200 + 2 * str + 2 * int) * skillPower,
  var firstWeightLimit = minMagicAttackValue + minInterval - 1;
+
              1
 
+
            );
  var [skillFormula, skillInfo] = getSkillFormula(
+
          };
    constants.skillPowerTable,
+
          skillInfo.removeWeaponReduction = true;
    skillId,
+
          break;
    attacker,
+
         // Flèche empoisonnée
    attackFactor,
+
         case 5:
    victim
+
          skillFormula = function (atk, variation) {
  );
+
            return floorMultiplication(
 
+
              atk +
  updateBattleValues(battleValues, skillInfo, attackerWeapon);
+
                (1.4 * atk + variation + 7 * dex + 4 * str + 4 * int) *
 
+
                  skillPower,
  for (var damagesType of battleValues.damagesTypeCombinaison) {
+
              1
    if (!damagesType.weight) {
+
             );
      continue;
+
          };
    }
+
          skillInfo.range = [100, 200];
 
+
          improvedByBonus = true;
    var damagesWeighted = {};
+
           break;
    addRowToTableResult(tableResult, damagesType.name);
+
        // Coup étincelant
 
+
        case 6:
    for (
+
           skillFormula = function (atk, variation) {
      var magicAttackValue = minMagicAttackValue;
+
             return floorMultiplication(
      magicAttackValue <= maxMagicAttackValue;
+
              (atk +
      magicAttackValue++
+
                (1.2 * atk + variation + 6 * dex + 3 * str + 3 * int) *
    ) {
+
                  skillPower) *
      var weight;
+
                1.2,
 
+
              1
      if (magicAttackValue > lastWeightsLimit) {
+
            );
        weight = maxMagicAttackValue - magicAttackValue + 1;
+
           };
      } else if (magicAttackValue < firstWeightLimit) {
+
           skillInfo.range = [100, 200];
        weight = magicAttackValue - minMagicAttackValue + 1;
+
           improvedByBonus = true;
      } else {
+
           break;
        weight = minInterval;
+
         // Tir tempête
      }
+
         case 9:
 
+
           skillFormula = function (atk, variation) {
      var rawDamages = skillFormula(magicAttackValue);
+
            return floorMultiplication(
 
+
              1.9 * atk + (2.6 * atk + variation) * skillPower,
      rawDamages = floorMultiplication(
+
              1
         rawDamages,
+
            );
         battleValues.weaponBonusCoeff
+
          };
      );
+
          skillInfo.range = [1, 1000];
 
+
          break;
      var damagesWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
 
        rawDamages,
 
        battleValues
 
      );
 
 
 
      if (damagesWithPrimaryBonuses <= 2) {
 
        for (var damages = 1; damages <= 5; damages++) {
 
          var finalDamages = calcSkillDamageWithSecondaryBonuses(
 
            damages,
 
             battleValues,
 
            damagesType,
 
            damagesWithPrimaryBonuses,
 
            skillFormula
 
           );
 
 
 
           addKeyValue(
 
             damagesWeighted,
 
            finalDamages,
 
            (weight * damagesType.weight) / (5 * totalCardinal)
 
          );
 
          sumDamages += (finalDamages * weight * damagesType.weight) / 5;
 
        }
 
      } else {
 
        var finalDamages = calcSkillDamageWithSecondaryBonuses(
 
          damagesWithPrimaryBonuses,
 
           battleValues,
 
           damagesType,
 
           damagesWithPrimaryBonuses,
 
           skillFormula
 
         );
 
 
 
         addKeyValue(
 
           damagesWeighted,
 
          finalDamages,
 
          (weight * damagesType.weight) / totalCardinal
 
        );
 
        sumDamages += finalDamages * weight * damagesType.weight;
 
 
       }
 
       }
     }
+
     } else if (attackerClass === "weaponary") {
 
+
      switch (skillId) {
    addToTableResult(tableResult, damagesWeighted, minMaxDamages);
+
        // Toucher brûlant
  }
+
        case 1:
 
+
          skillFormula = function (atk) {
  if (minMaxDamages.min === Infinity) {
+
            return floorMultiplication(
    minMaxDamages.min = 0;
+
              atk +
  }
+
                2 * lv +
 
+
                2 * int +
  return [sumDamages / totalCardinal, minMaxDamages];
+
                (2 * atk + 4 * str + 14 * int) * skillPower,
}
+
              1
 
+
            );
function changeMonsterValues(monster, instance, attacker) {
+
          };
  switch (instance) {
+
          improvedByBonus = true;
    case "SungMahiTower":
+
          improvedBySkillBonus = true;
      var sungMahiFloor = 1;
+
          break;
      var sungMahiStep = 1;
+
        // Tourbillon du dragon
      var rawDefense = 120;
+
        case 2:
 
+
          skillFormula = function (atk) {
      if (isPC(attacker)) {
+
            return floorMultiplication(
        sungMahiFloor = attacker.sungMahiFloor;
+
              1.1 * atk +
        sungMahiStep = attacker.sungMahiStep;
+
                2 * lv +
      }
+
                2 * int +
 
+
                (1.5 * atk + str + 12 * int) * skillPower,
      if (monster.rank === 5) {
+
              1
        monster.level = 121;
+
            );
         monster.dex = 75;
+
          };
        rawDefense += 1;
+
          improvedByBonus = true;
      } else if (monster.rank === 6) {
+
          break;
        monster.level = 123;
+
         // Contre-sort
        monster.dex = 75;
+
        case 6:
        rawDefense += 1;
+
          skillFormula = function (mav, variation) {
      } else {
+
            return floorMultiplication(
        monster.level = 120;
+
              40 +
        monster.dex = 68;
+
                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;
 
       }
 
       }
      monster.vit = 100;
+
    } else if (attackerClass === "black_magic") {
       monster.rawDefense = rawDefense + (sungMahiStep - 1) * 6;
+
       switch (skillId) {
      monster.fistDefense = 0;
+
        // Attaque des ténèbres
      monster.swordDefense = 0;
+
        case 1:
      monster.twoHandedSwordDefense = 0;
+
          skillFormula = function (mav, variation) {
      monster.daggerDefense = 0;
+
            return floorMultiplication(
      monster.bellDefense = 0;
+
              40 +
      monster.fanDefense = 0;
+
                5 * lv +
      monster.arrowDefense = 0;
+
                2 * int +
      monster.clawDefense = 0;
+
                (13 * int + 6 * mav + variation) * attackFactor * skillPower,
      monster.magicResistance = 0;
+
              1
      monster.fireResistance = -20;
+
            );
   }
+
          };
}
+
          skillInfo.range = [50, 100];
 
+
          improvedByBonus = true;
function createMonster(monsterVnum, attacker) {
+
          improvedBySkillBonus = true;
  var monsterAttributes = monsterData[monsterVnum];
+
          break;
 
+
        // Attaque de flammes
  var monster = {
+
        // case 2:
     name: monsterAttributes[36],
+
        //   skillFormula = function (mav, variation) {
    rank: monsterAttributes[0],
+
        //     return floorMultiplication(
    race: monsterAttributes[1],
+
        //      5 * lv + 2 * int + (7 * int + 8 * mav + 4 * str + 2 * vit + variation) * skillPower,
    attack: monsterAttributes[2],
+
        //      1
    level: monsterAttributes[3],
+
        //     );
    type: monsterAttributes[4],
+
        //  };
    str: monsterAttributes[5],
+
        //  skillInfo.range = [180, 100];
    dex: monsterAttributes[6],
+
        //  improvedByBonus = true;
    vit: monsterAttributes[7],
+
        //  break;
    int: monsterAttributes[8],
+
        // Esprit de flammes
     minAttackValue: monsterAttributes[9],
+
        case 3:
    maxAttackValue: monsterAttributes[10],
+
          skillFormula = function (mav, variation) {
    rawDefense: monsterAttributes[11],
+
            return floorMultiplication(
    criticalHit: monsterAttributes[12],
+
              30 +
    piercingHit: monsterAttributes[13],
+
                2 * lv +
    fistDefense: monsterAttributes[14],
+
                2 * int +
    swordDefense: monsterAttributes[15],
+
                (7 * int + 6 * mav + variation) * attackFactor * skillPower,
    twoHandedSwordDefense: monsterAttributes[16],
+
              1
    daggerDefense: monsterAttributes[17],
+
            );
    bellDefense: monsterAttributes[18],
+
          };
    fanDefense: monsterAttributes[19],
+
          skillInfo.range = [200, 500];
    arrowDefense: monsterAttributes[20],
+
          break;
    clawDefense: monsterAttributes[21],
+
        // Frappe de l'esprit
    fireResistance: monsterAttributes[22],
+
        // case 5:
    lightningResistance: monsterAttributes[23],
+
        //  skillFormula = function (mav, variation) {
    magicResistance: monsterAttributes[24],
+
        //     return floorMultiplication(
    windResistance: monsterAttributes[25],
+
        //      40 + 2 * lv + 2 * int + (2 * vit + 2 * dex + 13 * int + 6 * mav + variation) * attackFactor * skillPower,
    lightningBonus: monsterAttributes[26],
+
        //      1
    fireBonus: monsterAttributes[27],
+
        //     );
    iceBonus: monsterAttributes[28],
+
        //   };
    windBonus: monsterAttributes[29],
+
        //   skillInfo.range = [180, 200];
    earthBonus: monsterAttributes[30],
+
        //  break;
    darknessBonus: monsterAttributes[31],
+
        // Orbe des ténèbres
     darknessResistance: monsterAttributes[32],
+
        case 6:
    iceResistance: monsterAttributes[33],
+
          skillFormula = function (mav) {
    earthResistance: monsterAttributes[34],
+
            return floorMultiplication(
     damageMultiplier: monsterAttributes[35],
+
              120 +
   };
+
                6 * lv +
 
+
                (5 * vit + 5 * dex + 29 * int + 9 * mav) *
  // monster.instance = 0;
+
                  attackFactor *
 
+
                  skillPower,
  // if (attacker && monster.instance === 0) {
+
              1
  //  changeMonsterValues(monster, "SungMahiTower", attacker);
+
            );
  // }
+
          };
 
+
          improvedByBonus = true;
  monster.defense = monster.rawDefense + monster.level + monster.vit;
+
          break;
 
+
        // Vague mortelle
  return monster;
+
        case 9:
}
+
          skillFormula = function (mav, variation) {
 
+
            return floorMultiplication(
function addPotentialErrorInformation(
+
              120 +
  errorInformation,
+
                6 * lv +
  attacker,
+
                (5 * vit + 5 * dex + 30 * int + variation + 9 * mav) *
  victim,
+
                  attackFactor *
  characters
+
                  skillPower,
) {
+
              1
  for (var error of Object.values(errorInformation)) {
+
            );
    hideElement(error);
+
          };
  }
+
          skillInfo.range = [1, 1000];
 
+
          break;
  if (isPC(attacker)) {
 
    if (isRiding(attacker)) {
 
      if (attacker.horsePoint === 0) {
 
        showElement(errorInformation["horse-level"]);
 
 
       }
 
       }
      showElement(errorInformation["horse-stat"]);
+
     } else if (attackerClass === "dragon") {
     } else if (isPolymorph(attacker)) {
+
       switch (skillId) {
       if (attacker.polymorphPoint === 0) {
+
         // Talisman volant
         showElement(errorInformation["polymorph-level"]);
+
        case 1:
      }
+
          skillFormula = function (mav) {
 
+
            return floorMultiplication(
      if (
+
              70 +
        (attacker.polymorphPoint <= 39 && attacker.attackValuePercent <= 199) ||
+
                5 * lv +
         (attacker.polymorphPoint === 40 && attacker.attackValuePercent <= 299)
+
                (18 * int + 7 * str + 5 * mav + 50) * attackFactor * skillPower,
      ) {
+
              1
        showElement(errorInformation["polymorph-bonus"]);
+
            );
      }
+
          };
    }
+
          skillInfo.weaponBonus = [4, 10];
  } else {
+
          improvedByBonus = true;
    showElement(errorInformation["monster-attacker"]);
+
          break;
  }
+
        // Dragon chassant
 
+
         case 2:
  if (isPC(victim)) {
+
          skillFormula = function (mav) {
    if (isRiding(victim)) {
+
            return floorMultiplication(
      showElement(errorInformation["horse-stat"]);
+
              60 +
    } else if (isPolymorph(victim)) {
+
                5 * lv +
      if (attacker.polymorphPoint === 0) {
+
                (16 * int + 6 * dex + 6 * mav + 120) *
        showElement(errorInformation["polymorph-level"]);
+
                  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;
 
       }
 
       }
      showElement(errorInformation["polymorph-defense"]);
+
     } else if (attackerClass === "heal") {
     }
+
      switch (skillId) {
  }
+
        // Jet de foudre
 
+
        case 1:
  if (characters.unsavedChanges) {
+
          skillFormula = function (mav, variation) {
    showElement(errorInformation["save"]);
+
            return floorMultiplication(
  }
+
              60 +
}
+
                5 * lv +
 
+
                (8 * int + 2 * dex + 8 * mav + variation) *
function createBattle(characters, battle) {
+
                  attackFactor *
  function isPseudoSaved(pseudo) {
+
                  skillPower,
    return characters.savedCharacters.hasOwnProperty(pseudo);
+
              1
  }
+
            );
 
+
          };
  battle.battleForm.addEventListener("submit", function (event) {
+
          skillInfo.weaponBonus = [6, 10];
    event.preventDefault();
+
          skillInfo.range = [5 * int, 15 * int];
 
+
          improvedByBonus = true;
    // auto save
+
          break;
    if (characters.unsavedChanges) {
+
        // Invocation de foudre
      characters.saveButton.click();
+
        case 2:
    }
+
          skillFormula = function (mav, variation) {
 
+
            return floorMultiplication(
    var battleInfo = new FormData(event.target);
+
              40 +
    var attackerName = battleInfo.get("attacker");
+
                4 * lv +
    var attackType = battleInfo.get("attackTypeSelection");
+
                (13 * int + 2 * str + 10 * mav + variation) *
    var victimName = battleInfo.get("victim");
+
                  attackFactor *
 
+
                  skillPower,
    if (!attackerName && !attackType && !victimName) {
+
              1
      return;
+
            );
    }
+
          };
 
+
          skillInfo.weaponBonus = [6, 10];
    var attackerWeapon = null;
+
          skillInfo.range = [5 * int, 16 * int];
 
+
          improvedByBonus = true;
    if (isPseudoSaved(attackerName)) {
+
          improvedBySkillBonus = true;
      var attacker = copyObject(characters.savedCharacters[attackerName]);
+
          break;
 
+
        // Griffe de foudre
      if (weaponData.hasOwnProperty(attacker.weapon)) {
+
        case 3:
        attackerWeapon = weaponData[attacker.weapon];
+
          skillFormula = function (mav, variation) {
      } else {
+
            return floorMultiplication(
        attackerWeapon = weaponData[0];
+
              50 +
 +
                5 * lv +
 +
                (8 * int + 2 * str + 8 * mav + variation) *
 +
                  attackFactor *
 +
                  skillPower,
 +
              1
 +
            );
 +
          };
 +
          skillInfo.range = [1, 800];
 +
          improvedByBonus = true;
 +
          break;
 
       }
 
       }
     } else {
+
     } else if (attackerClass === "lycan") {
       var attacker = createMonster(attackerName);
+
       switch (skillId) {
    }
+
        // Déchiqueter
 
+
        // case 1:
    if (isPseudoSaved(victimName)) {
+
        //  skillFormula = function (atk) {
       var victim = copyObject(characters.savedCharacters[victimName]);
+
        //    return floorMultiplication(
     } else {
+
        //       1.1 * atk + (0.3 * atk + 1.5 * str) * skillPower,
      var victim = createMonster(victimName, attacker);
+
        //      1
    }
+
        //     );
 
+
        //  };
    var meanDamages, minMaxDamages;
+
        //  skillInfo.weaponBonus = [5, 54];
    var calcDamages;
+
        //  improvedByBonus = true;
    var skillId = 0;
+
        //  break;
 
+
        // Souffle de loup
    if (attackType === "physical") {
+
        case 2:
      calcDamages = calcPhysicalDamages;
+
          skillFormula = function (atk) {
    } else if (attackType.startsWith("attackSkill")) {
+
            return floorMultiplication(
      skillId = Number(attackType.split("attackSkill")[1]);
+
              2 * atk + (atk + 3 * dex + 5 * str + vit) * skillPower,
 
+
              1
      if (isMagicClass(attacker)) {
+
            );
        calcDamages = calcMagicSkillDamages;
+
          };
      } else {
+
          skillInfo.weaponBonus = [5, 35];
         calcDamages = calcPhysicalSkillDamages;
+
          improvedByBonus = true;
      }
+
          improvedBySkillBonus = true;
    } else if (attackType.startsWith("horseSkill")) {
+
          break;
      skillId = Number(attackType.split("horseSkill")[1]);
+
         // Bond de loup
      calcDamages = calcPhysicalSkillDamages;
+
        case 3:
    }
+
          skillFormula = function (atk) {
 
+
            return floorMultiplication(
    [meanDamages, minMaxDamages] = calcDamages(
+
              atk + (1.6 * atk + 200 + 7 * dex + 7 * str) * skillPower,
      attacker,
+
              1
      attackerWeapon,
+
            );
      victim,
+
          };
      battle.tableResult,
+
          skillInfo.weaponBonus = [5, 35];
      battle.mapping,
+
          improvedByBonus = true;
      battle.constants,
+
          break;
      skillId
+
        // Griffe de loup
    );
+
        case 4:
 
+
          skillFormula = function (atk) {
    battle.damageResult.textContent =
+
            return floorMultiplication(
      attacker.name +
+
              3 * atk + (0.8 * atk + 6 * str + 2 * dex + vit) * skillPower,
      " inflige " +
+
              1
      numberDisplay(meanDamages, 1) +
+
            );
      " dégâts en moyenne à " +
+
          };
      victim.name +
+
          improvedByBonus = true;
      " (minimum : " +
+
          break;
      minMaxDamages.min +
+
        // Tempête cinglante
      ", maximum : " +
+
        case 9:
      minMaxDamages.max +
+
          skillFormula = function (atk, variation) {
      ").";
+
            return floorMultiplication(
 
+
              1.8 * atk +
    addPotentialErrorInformation(
+
                (atk + 6 * dex + variation + 3 * str + lv) * skillPower,
      battle.errorInformation,
+
              1
      attacker,
+
            );
      victim,
+
          };
      characters
+
          skillInfo.range = [1, 1000];
    );
+
          break;
    showElement(battle.tableContainer);
+
      }
  });
+
     }
 
+
     if (improvedBySkillBonus) {
  battle.attackerSelection.addEventListener("change", function (event) {
+
       skillInfo.skillBonus =
    var attackerName = event.target.value;
+
        16 * getSkillPower(attacker.skillBonus, skillPowerTable);
     var attackTypeSelection = battle.attackTypeSelection;
 
 
 
     if (isPseudoSaved(attackerName)) {
 
       var attacker = characters.savedCharacters[attackerName];
 
      filterAttackTypeSelection(attacker, attackTypeSelection);
 
    } else {
 
      filterAttackTypeSelectionMonster(attackTypeSelection);
 
    }
 
  });
 
}
 
  
function createMapping() {
+
      var skillWardChoice = victim.skillWardChoice;
  mapping = {
+
 
    typeFlag: [
+
      if (skillWardChoice && skillWardChoice === attackerClass) {
      "animalBonus", // 0
+
        skillInfo.skillWard =
       "humanBonus", // 1
+
          24 * getSkillPower(victim.skillWard, skillPowerTable);
      "orcBonus", // 2
+
       }
      "mysticBonus", // 3
+
    }
      "undeadBonus", // 4
+
 
       "insectBonus", // 5
+
    if (improvedByBonus) {
      "desertBonus", // 6
+
       skillInfo.skillBonusByBonus = attacker["skillBonus" + skillId];
      "devilBonus", // 7
+
    }
     ],
+
 
    raceBonus: {
+
     if (removeSkillVariation) {
       warrior: "warriorBonus",
+
       var averageVariation = (skillInfo.range[0] + skillInfo.range[0]) / 2;
       sura: "suraBonus",
+
       skillInfo.range = [averageVariation, averageVariation];
      ninja: "ninjaBonus",
+
     }
      shaman: "shamanBonus",
+
  } else {
      lycan: "lycanBonus",
+
    var skillPower = getSkillPower(
     },
+
       attacker["horseSkill" + skillId],
    raceResistance: {
+
       skillPowerTable
      warrior: "warriorResistance",
+
    );
       sura: "suraResistance",
+
 
       ninja: "ninjaResistance",
+
     switch (skillId) {
      shaman: "shamanResistance",
+
       // Combat équestre
      lycan: "lycanResistance",
+
       case 137:
     },
+
        skillFormula = function (atk) {
    defenseWeapon: [
+
          return floorMultiplication(atk + 2 * atk * skillPower, 1);
       "swordDefense", // 0
+
        };
       "daggerDefense", // 1
+
        break;
      "arrowDefense", // 2
+
       // Charge à cheval
      "twoHandedSwordDefense", // 3
+
       case 138:
      "bellDefense", // 4
+
        skillFormula = function (atk) {
      "clawDefense", // 5
+
          return floorMultiplication(
       "fanDefense", // 6
+
            2.4 * (200 + 1.5 * lv) + 600 * skillPower,
       "swordDefense", // 7
+
            1
      "fistDefense", // 8
+
          );
    ],
+
        };
    breakWeapon: [
+
        break;
      "breakSwordDefense", // 0
+
       // Vague de Pouvoir
      "breakDaggerDefense", // 1
+
       case 139:
      "breakArrowDefense", // 2
+
        skillFormula = function (atk) {
      "breakTwoHandedSwordDefense", // 3
+
          return floorMultiplication(
      "breakBellDefense", // 4
+
            2 * (200 + 1.5 * lv) + 600 * skillPower,
       "breakClawDefense", // 5
+
            1
       "breakFanDefense", // 6
+
          );
      "breakSwordDefense", // 7
+
        };
    ],
+
        break;
    elementBonus: [
+
       // Grêle de flèches
      "fireBonus", // 0
+
       case 140:
      "iceBonus", // 1
+
        skillFormula = function (atk) {
      "windBonus", // 2
+
          return floorMultiplication(atk + 2 * atk * skillPower, 1);
      "lightningBonus", // 3
+
        };
       "earthBonus", // 4
+
        break;
       "darknessBonus", // 5
+
     }
    ],
+
   }
    elementResistance: [
+
 
      "fireResistance", // 0
+
   updateBattleValues(battleValues, skillFormula, skillInfo);
      "iceResistance", // 1
 
      "windResistance", // 2
 
      "lightningResistance", // 3
 
      "earthResistance", // 4
 
      "darknessResistance", // 5
 
     ],
 
   };
 
   return mapping;
 
 
}
 
}
  
function createConstants() {
+
function calcMagicAttackValueAugmentation(
   var constants = {
+
   magicAttackValueWeapon,
    polymorphPowerTable: [
+
  magicAttackValueBonus
      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,
+
  if (magicAttackValueBonus) {
      84, 89, 94, 100, 0,
+
     return Math.max(
    ],
+
       1,
     skillPowerTable: [
+
       0.0025056 *
       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,
+
        magicAttackValueBonus ** 0.602338 *
       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,
+
        magicAttackValueWeapon ** 1.20476
      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,
+
   }
     ],
+
   return 0;
    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],
 
    },
 
   };
 
   return constants;
 
 
}
 
}
  
function createDamageCalculatorInformation() {
+
function getMagicAttackValueAugmentation(
   var characters = {
+
  minMagicAttackValue,
     unsavedChanges: false,
+
  maxMagicAttackValue,
     savedCharacters: {},
+
  magicAttackValueBonus
     currentCharacter: null,
+
) {
     characterCreation: document.getElementById("character-creation"),
+
  var magicAttackValueAugmentation = [];
     addNewCharacterButton: document.getElementById("add-new-character"),
+
 
     dropZone: document.getElementById("character-drop-zone"),
+
  for (
     characterInput: document.getElementById("character-input"),
+
    var magicAttackValue = minMagicAttackValue;
     newCharacterTemplate: document.getElementById("new-character-template")
+
    magicAttackValue <= maxMagicAttackValue;
       .children[0],
+
    magicAttackValue++
     charactersContainer: document.getElementById("characters-container"),
+
  ) {
     newMonsterTemplate: document.getElementById("new-monster-template")
+
    magicAttackValueAugmentation.push(
       .children[0],
+
      calcMagicAttackValueAugmentation(magicAttackValue, magicAttackValueBonus)
     monstersContainer: document.getElementById("monsters-container"),
+
    );
     monsterListForm: document.getElementById("monster-list-form"),
+
  }
     searchMonster: document.getElementById("search-monster"),
+
 
    monsterList: document.getElementById("monster-list"),
+
  return magicAttackValueAugmentation;
     saveButton: document.getElementById("save-character"),
+
}
     weaponCategory: document.getElementById("weapon-category"),
+
 
 +
function calcPhysicalDamage(battleValues) {
 +
  var {
 +
    attackFactor,
 +
    mainAttackValue,
 +
    attackValues: {
 +
      minAttackValue,
 +
      maxAttackValue,
 +
      attackValueOther,
 +
      weights,
 +
      weapon,
 +
    },
 +
    bonusValues,
 +
    damageTypeCombinaison,
 +
  } = battleValues;
 +
 
 +
  var damageWeightedByType = {};
 +
 
 +
  if (bonusValues.missPercentage) {
 +
    damageWeightedByType.miss = bonusValues.missPercentage / 100;
 +
  }
 +
 
 +
  for (var damageType of damageTypeCombinaison) {
 +
    if (!damageType.weight) {
 +
      continue;
 +
    }
 +
 
 +
    var damageWeighted = {};
 +
    damageWeightedByType[damageType.name] = damageWeighted;
 +
 
 +
    for (
 +
      var attackValue = minAttackValue;
 +
      attackValue <= maxAttackValue;
 +
      attackValue++
 +
    ) {
 +
      var weight = weights[attackValue - minAttackValue] * damageType.weight;
 +
 
 +
      var secondaryAttackValue = 2 * attackValue + attackValueOther;
 +
      var rawDamage =
 +
        mainAttackValue +
 +
        floorMultiplication(attackFactor, secondaryAttackValue);
 +
 
 +
      var damageWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
 +
        rawDamage,
 +
        bonusValues
 +
      );
 +
 
 +
      var minPiercingDamage =
 +
        damageWithPrimaryBonuses -
 +
        bonusValues.defenseBoost -
 +
        bonusValues.defenseMarriage;
 +
 
 +
      if (minPiercingDamage <= 2) {
 +
        for (var damage = 1; damage <= 5; damage++) {
 +
          saveFinalDamage(
 +
            damage,
 +
            bonusValues,
 +
            damageType,
 +
            weapon,
 +
            minPiercingDamage,
 +
            damageWithPrimaryBonuses,
 +
            damageWeighted,
 +
            weight / 5
 +
          );
 +
        }
 +
      } else {
 +
        saveFinalDamage(
 +
          minPiercingDamage,
 +
          bonusValues,
 +
          damageType,
 +
          weapon,
 +
          minPiercingDamage,
 +
          damageWithPrimaryBonuses,
 +
          damageWeighted,
 +
          weight
 +
        );
 +
      }
 +
    }
 +
  }
 +
 
 +
  return damageWeightedByType;
 +
}
 +
 
 +
function calcPhysicalSkillDamage(battleValues) {
 +
  var {
 +
    attackFactor,
 +
    mainAttackValue,
 +
    attackValues: {
 +
      minAttackValue,
 +
      maxAttackValue,
 +
      attackValueOther,
 +
      weights,
 +
      weapon,
 +
    },
 +
    bonusValues,
 +
    damageTypeCombinaison,
 +
    skillFormula,
 +
    skillRange: [minVariation, maxVariation],
 +
  } = battleValues;
 +
 
 +
  var damageWeightedByType = {};
 +
 
 +
  for (var damageType of damageTypeCombinaison) {
 +
    if (!damageType.weight) {
 +
      continue;
 +
    }
 +
 
 +
    var damageWeighted = {};
 +
    var savedDamage = {};
 +
    var savedCriticalDamage = {};
 +
 
 +
    damageWeightedByType[damageType.name] = damageWeighted;
 +
 
 +
    for (
 +
      var attackValue = minAttackValue;
 +
      attackValue <= maxAttackValue;
 +
      attackValue++
 +
    ) {
 +
      var weight = weights[attackValue - minAttackValue] * damageType.weight;
 +
 
 +
      var secondaryAttackValue = 2 * attackValue + attackValueOther;
 +
      var rawDamage =
 +
        mainAttackValue +
 +
        floorMultiplication(attackFactor, secondaryAttackValue);
 +
 
 +
      var damageWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
 +
        rawDamage,
 +
        bonusValues
 +
      );
 +
 
 +
      for (
 +
        var variation = minVariation;
 +
        variation <= maxVariation;
 +
        variation++
 +
      ) {
 +
        if (damageWithPrimaryBonuses <= 2) {
 +
          for (var damage = 1; damage <= 5; damage++) {
 +
            var damageWithFormula = skillFormula(
 +
              damage * bonusValues.useDamage,
 +
              variation
 +
            );
 +
 
 +
            damageWithFormula = Math.floor(
 +
              (damageWithFormula * bonusValues.weaponBonusCoeff) / 100
 +
            );
 +
 
 +
            saveFinalSkillDamage(
 +
              damageWithFormula,
 +
              bonusValues,
 +
              damageType,
 +
              weapon,
 +
              damageWithPrimaryBonuses,
 +
              damageWeighted,
 +
              weight
 +
            );
 +
          }
 +
        } else {
 +
          var damageWithFormula = skillFormula(
 +
            damageWithPrimaryBonuses * bonusValues.useDamage,
 +
            variation
 +
          );
 +
 
 +
          if (savedDamage.hasOwnProperty(damageWithFormula)) {
 +
            var finalDamage = savedDamage[damageWithFormula];
 +
            damageWeighted[finalDamage] += weight;
 +
            continue;
 +
          }
 +
 
 +
          var finalDamage = Math.floor(
 +
            (damageWithFormula * bonusValues.weaponBonusCoeff) / 100
 +
          );
 +
 
 +
          finalDamage = saveFinalSkillDamage(
 +
            finalDamage,
 +
            bonusValues,
 +
            damageType,
 +
            weapon,
 +
            damageWithPrimaryBonuses,
 +
            damageWeighted,
 +
            weight,
 +
            savedCriticalDamage
 +
          );
 +
 
 +
          if (finalDamage) {
 +
            savedDamage[damageWithFormula] = finalDamage;
 +
          }
 +
        }
 +
      }
 +
    }
 +
  }
 +
 
 +
  return damageWeightedByType;
 +
}
 +
 
 +
function calcMagicSkillDamage(battleValues) {
 +
  var {
 +
    attackValues: {
 +
      minMagicAttackValue,
 +
      maxMagicAttackValue,
 +
      magicAttackValueAugmentation,
 +
      weights,
 +
      weapon,
 +
    },
 +
    bonusValues,
 +
    damageTypeCombinaison,
 +
    skillFormula,
 +
    skillRange: [minVariation, maxVariation],
 +
  } = battleValues;
 +
 
 +
  var damageWeightedByType = {};
 +
 
 +
  for (var damageType of damageTypeCombinaison) {
 +
    if (!damageType.weight) {
 +
      continue;
 +
    }
 +
 
 +
    var damageWeighted = {};
 +
    var savedDamage = {};
 +
    var savedCriticalDamage = {};
 +
 
 +
    damageWeightedByType[damageType.name] = damageWeighted;
 +
 
 +
    for (
 +
      var magicAttackValue = minMagicAttackValue;
 +
      magicAttackValue <= maxMagicAttackValue;
 +
      magicAttackValue++
 +
    ) {
 +
      var index = magicAttackValue - minMagicAttackValue;
 +
      var weight = weights[index] * damageType.weight;
 +
 
 +
      for (
 +
        var variation = minVariation;
 +
        variation <= maxVariation;
 +
        variation++
 +
      ) {
 +
        var rawDamage = skillFormula(
 +
          magicAttackValue + magicAttackValueAugmentation[index],
 +
          variation
 +
        );
 +
 
 +
        if (savedDamage.hasOwnProperty(rawDamage)) {
 +
          var finalDamage = savedDamage[rawDamage];
 +
          damageWeighted[finalDamage] += weight;
 +
          continue;
 +
        }
 +
 
 +
        var damageWithPrimaryBonuses = Math.floor(
 +
          (rawDamage * bonusValues.weaponBonusCoeff) / 100
 +
        );
 +
 
 +
        damageWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
 +
          damageWithPrimaryBonuses,
 +
          bonusValues
 +
        );
 +
 
 +
        if (damageWithPrimaryBonuses <= 2) {
 +
          for (var damage = 1; damage <= 5; damage++) {
 +
            saveFinalSkillDamage(
 +
              damage,
 +
              bonusValues,
 +
              damageType,
 +
              weapon,
 +
              damageWithPrimaryBonuses,
 +
              damageWeighted,
 +
              weight
 +
            );
 +
          }
 +
        } else {
 +
          var finalDamage = saveFinalSkillDamage(
 +
            damageWithPrimaryBonuses,
 +
            bonusValues,
 +
            damageType,
 +
            weapon,
 +
            damageWithPrimaryBonuses,
 +
            damageWeighted,
 +
            weight,
 +
            savedCriticalDamage
 +
          );
 +
 
 +
          if (finalDamage) {
 +
            savedDamage[rawDamage] = finalDamage;
 +
          }
 +
        }
 +
      }
 +
    }
 +
  }
 +
 
 +
  return damageWeightedByType;
 +
}
 +
 
 +
function calcDamage(
 +
  attacker,
 +
  victim,
 +
  attackType,
 +
  battle,
 +
  removeSkillVariation
 +
) {
 +
  var damageCalculator, skillId, skillType;
 +
 
 +
  if (attackType === "physical") {
 +
    damageCalculator = calcPhysicalDamage;
 +
  } else if (attackType.startsWith("attackSkill")) {
 +
    skillId = Number(attackType.split("attackSkill")[1]);
 +
 
 +
    if (isMagicClass(attacker) || isDispell(attacker, skillId)) {
 +
      skillType = "magic";
 +
      damageCalculator = calcMagicSkillDamage;
 +
    } else {
 +
      skillType = "physical";
 +
      damageCalculator = calcPhysicalSkillDamage;
 +
    }
 +
  } else if (attackType.startsWith("horseSkill")) {
 +
    skillType = "physical";
 +
    skillId = Number(attackType.split("horseSkill")[1]);
 +
    damageCalculator = calcPhysicalSkillDamage;
 +
  }
 +
 
 +
  var battleValues = createBattleValues(attacker, victim, battle, skillType);
 +
 
 +
  if (skillId) {
 +
    getSkillFormula(battle, skillId, battleValues, removeSkillVariation);
 +
  }
 +
 
 +
  return {
 +
    damageWeightedByType: damageCalculator(battleValues),
 +
    attackValues: battleValues.attackValues,
 +
    skillType: skillType,
 +
  };
 +
}
 +
 
 +
function damageWithoutVariation(
 +
  attacker,
 +
  victim,
 +
  attackType,
 +
  battle,
 +
  characters
 +
) {
 +
  var startDamageTime = performance.now();
 +
 
 +
  var { damageWeightedByType, attackValues, skillType } = calcDamage(
 +
    attacker,
 +
    victim,
 +
    attackType,
 +
    battle
 +
  );
 +
 
 +
  var endDamageTime = performance.now();
 +
 
 +
  displayResults(
 +
    attackValues,
 +
    damageWeightedByType,
 +
    battle,
 +
    attacker.name,
 +
    victim.name
 +
  );
 +
 
 +
  var endDisplayTime = performance.now();
 +
 
 +
  displayFightInfo(
 +
    attackValues.possibleDamageCount,
 +
    endDamageTime - startDamageTime,
 +
    endDisplayTime - endDamageTime,
 +
    battle
 +
  );
 +
  addPotentialErrorInformation(
 +
    battle.errorInformation,
 +
    attacker,
 +
    victim,
 +
    skillType,
 +
    characters
 +
  );
 +
 
 +
  hideElement(battle.bonusVariationResultContainer);
 +
  showElement(battle.fightResultTitle);
 +
  showElement(battle.fightResultContainer);
 +
}
 +
 
 +
function damageWithVariation(
 +
  attacker,
 +
  victim,
 +
  attackType,
 +
  battle,
 +
  entity,
 +
  entityVariation
 +
) {
 +
  var startTime = performance.now();
 +
  var damageByBonus = [];
 +
  var augmentationByBonus = [];
 +
  var {
 +
    bonusVariationMinValue: minVariation,
 +
    bonusVariationMaxValue: maxVariation,
 +
  } = entity;
 +
  var step = Math.ceil((maxVariation - minVariation + 1) / 500);
 +
  var simulationCount = 0;
 +
  var simulationTime;
 +
  var firstDamage = 1;
 +
 
 +
  for (
 +
    var bonusValue = minVariation;
 +
    bonusValue <= maxVariation;
 +
    bonusValue += step
 +
  ) {
 +
    entity[entityVariation] = bonusValue;
 +
 
 +
    var { damageWeightedByType, totalCardinal } = calcDamage(
 +
      copyObject(attacker),
 +
      copyObject(victim),
 +
      attackType,
 +
      battle,
 +
      true
 +
    );
 +
 
 +
    var meanDamage = calcMeanDamage(damageWeightedByType, totalCardinal);
 +
 
 +
    if (bonusValue === minVariation) {
 +
      firstDamage = Math.max(meanDamage, 1e-3);
 +
    }
 +
 
 +
    damageByBonus.push({ x: bonusValue, y: meanDamage });
 +
    augmentationByBonus.push({
 +
      x: bonusValue,
 +
      y: meanDamage / firstDamage - 1,
 +
    });
 +
    simulationCount++;
 +
  }
 +
 
 +
  var endTime = performance.now();
 +
 
 +
  battle.damageByBonus = damageByBonus.concat(entityVariation);
 +
 
 +
  addToBonusVariationChart(
 +
    damageByBonus,
 +
    augmentationByBonus,
 +
    entity.bonusVariationName,
 +
    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.fightResultTitle);
 +
  showElement(battle.bonusVariationResultContainer);
 +
 
 +
  if (
 +
    isChecked(attacker.useBonusVariation) &&
 +
    isChecked(victim.useBonusVariation)
 +
  ) {
 +
    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;
 +
  }
 +
 
 +
  // Alastor
 +
  if (monster.vnum === 6790) {
 +
    monster.iceResistance = 0;
 +
    monster.iceBonus = 0;
 +
    monster.lightningResistance = -10;
 +
    monster.lightningBonus = 65;
 +
  }
 +
}
 +
 
 +
function createWeapon(vnum) {
 +
  var weapon = copyObject(weaponData[vnum]);
 +
  var serpentVnums = [360, 380, 1210, 2230, 3250, 5200, 6150, 7330];
 +
  var weaponValues = weapon[1];
 +
  var isSerpent = isValueInArray(Number(vnum), serpentVnums);
 +
  var isSpecial = Array.isArray(weaponValues[0]);
 +
  var isMagic;
 +
 
 +
  if (isSpecial) {
 +
    isMagic = weaponValues[0][1] > 0;
 +
  } else {
 +
    isMagic = weaponValues[1] > 0;
 +
  }
 +
 
 +
  return {
 +
    type: weapon[0],
 +
    maxUpgrade: weapon[2].length - 1,
 +
    isSerpent: isSerpent,
 +
    isMagic: isMagic,
 +
    getValues: function (upgrade) {
 +
      var currentWeaponValues = weaponValues;
 +
 
 +
      if (upgrade === undefined) {
 +
        // rare bug when weaponUpgrade is deleted
 +
        console.warn("WeaponUpgrade is missing.");
 +
        upgrade = this.maxUpgrade;
 +
      }
 +
      upgrade = Math.min(upgrade, this.maxUpgrade);
 +
 
 +
      if (isSpecial) {
 +
        currentWeaponValues = weaponValues[upgrade];
 +
      }
 +
 
 +
      this.minAttackValue = currentWeaponValues[2];
 +
      this.maxAttackValue = currentWeaponValues[3];
 +
      this.minMagicAttackValue = currentWeaponValues[0];
 +
      this.maxMagicAttackValue = currentWeaponValues[1];
 +
      this.growth = weapon[2][upgrade];
 +
    },
 +
  };
 +
}
 +
 
 +
function createMonster(monsterVnum, attacker, polymorphMonster) {
 +
  var monsterAttributes = monsterData[monsterVnum];
 +
 
 +
  var monster = {
 +
    vnum: Number(monsterVnum),
 +
    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);
 +
  // }
 +
  if (!polymorphMonster) {
 +
    changeMonsterValues(monster);
 +
  }
 +
 
 +
  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"]);
 +
    }
 +
 
 +
    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"]);
 +
    }
 +
    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 () {
 +
    reduceChartPoints.disabled = true;
 +
 
 +
    var startDisplayTime = performance.now();
 +
    var scatterDataByType = battle.scatterDataByType;
 +
    var {
 +
      chart,
 +
      maxPoints,
 +
      chart: {
 +
        data: { datasets },
 +
      },
 +
    } = battle.damageChart;
 +
    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 = aggregateDamage(scatterData, maxPoints);
 +
        addAnimations = true;
 +
      } else {
 +
        dataset.data = scatterData;
 +
      }
 +
    }
 +
 
 +
    handleChartAnimations(chart, addAnimations);
 +
    chart.update();
 +
 
 +
    displayTime.textContent = numberFormat.format(
 +
      (performance.now() - startDisplayTime) / 1000
 +
    );
 +
 
 +
    setTimeout(function () {
 +
      reduceChartPoints.disabled = false;
 +
    }, 1000);
 +
  });
 +
 
 +
  reduceChartPointsContainer.addEventListener("pointerup", function (event) {
 +
    if (event.pointerType === "mouse") {
 +
      if (event.target.closest("label")) {
 +
        return;
 +
      }
 +
      reduceChartPoints.click();
 +
    }
 +
  });
 +
}
 +
 
 +
function downloadRawDataListener(battle) {
 +
  var { downLoadRawData, downLoadRawDataVariation } = battle;
 +
  var fileType = "text/csv;charset=utf-8;";
 +
 
 +
  downLoadRawData.addEventListener("click", function () {
 +
    var damageWeightedByType = battle.damageWeightedByType;
 +
    var filename = "raw_damage.csv";
 +
    var csvContent = "damage,probabilities,damageType\n";
 +
 
 +
    for (var damageType in damageWeightedByType) {
 +
      var damageWeighted = damageWeightedByType[damageType];
 +
 
 +
      for (var damage in damageWeighted) {
 +
        csvContent +=
 +
          damage + "," + damageWeighted[damage] + "," + damageType + "\n";
 +
      }
 +
    }
 +
 
 +
    downloadData(csvContent, fileType, filename);
 +
  });
 +
 
 +
  downLoadRawDataVariation.addEventListener("click", function () {
 +
    var damageByBonus = battle.damageByBonus;
 +
    var damageByBonusLength = damageByBonus.length;
 +
    var filename = "damage_variation.csv";
 +
 
 +
    if (!damageByBonusLength) {
 +
      return;
 +
    }
 +
 
 +
    var csvContent =
 +
      damageByBonus[damageByBonusLength - 1] + ",averageDamage\n";
 +
 
 +
    for (var index = 0; index < damageByBonusLength - 1; index++) {
 +
      var row = damageByBonus[index];
 +
 
 +
      csvContent += row.x + "," + row.y + "\n";
 +
    }
 +
 
 +
    downloadData(csvContent, fileType, filename);
 +
  });
 +
}
 +
 
 +
function displayResults(
 +
  attackValues,
 +
  damageWeightedByType,
 +
  battle,
 +
  attackerName,
 +
  victimName
 +
) {
 +
  var [meanDamage, minDamage, maxDamage, scatterDataByType, uniqueDamageCount] =
 +
    prepareDamageData(damageWeightedByType, attackValues);
 +
 
 +
  addToDamageChart(
 +
    scatterDataByType,
 +
    battle.damageChart,
 +
    battle.reduceChartPoints.checked
 +
  );
 +
  updateDamageChartDescription(
 +
    battle.uniqueDamageCounters,
 +
    uniqueDamageCount,
 +
    battle.numberFormats.default
 +
  );
 +
  displayFightResults(
 +
    battle,
 +
    attackerName,
 +
    victimName,
 +
    meanDamage,
 +
    minDamage,
 +
    maxDamage
 +
  );
 +
  battle.damageWeightedByType = damageWeightedByType;
 +
  battle.scatterDataByType = scatterDataByType;
 +
}
 +
 
 +
function displayFightResults(
 +
  battle,
 +
  attackerName,
 +
  victimName,
 +
  meanDamage,
 +
  minDamage,
 +
  maxDamage
 +
) {
 +
  var {
 +
    tableResultFight,
 +
    tableResultHistory,
 +
    savedFights,
 +
    numberFormats: { default: numberFormat },
 +
    deleteFightTemplate,
 +
  } = battle;
 +
 
 +
  hideElement(tableResultHistory.rows[1]);
 +
 
 +
  var valuesToDisplay = [
 +
    attackerName,
 +
    victimName,
 +
    battle.battleChoice.attackType.selectedText,
 +
    meanDamage,
 +
    minDamage,
 +
    maxDamage,
 +
  ];
 +
 
 +
  savedFights.push(valuesToDisplay);
 +
  updateSavedFights(savedFights);
 +
 
 +
  editTableResultRow(tableResultFight.rows[1], valuesToDisplay, numberFormat);
 +
  addRowToTableResultHistory(
 +
    tableResultHistory,
 +
    valuesToDisplay,
 +
    deleteFightTemplate,
 +
    numberFormat
 +
  );
 +
}
 +
 
 +
function displayFightInfo(
 +
  possibleDamageCount,
 +
  damageTimeDuration,
 +
  displayTimeDuration,
 +
  battle
 +
) {
 +
  var container = battle.possibleDamageCounter.parentElement;
 +
 
 +
  if (possibleDamageCount <= 1) {
 +
    hideElement(container);
 +
    return;
 +
  } else {
 +
    showElement(container);
 +
  }
 +
 
 +
  var { numberFormats, possibleDamageCounter, damageTime, displayTime } =
 +
    battle;
 +
 
 +
  possibleDamageCount = numberFormats.default.format(possibleDamageCount);
 +
  damageTimeDuration = numberFormats.second.format(damageTimeDuration / 1000);
 +
  displayTimeDuration = numberFormats.second.format(displayTimeDuration / 1000);
 +
 
 +
  possibleDamageCounter.textContent = possibleDamageCount;
 +
  damageTime.textContent = damageTimeDuration;
 +
  displayTime.textContent = displayTimeDuration;
 +
}
 +
 
 +
function parseTypeAndName(data) {
 +
  var [type, nameOrVnum] = splitFirst(data, "-");
 +
 
 +
  return {
 +
    type: type,
 +
    nameOrVnum: nameOrVnum,
 +
    isCharacter: type === "character",
 +
  };
 +
}
 +
 
 +
function isCharacterSelected(characterName, selected) {
 +
  if (!selected) {
 +
    return false;
 +
  }
 +
 
 +
  var { nameOrVnum, isCharacter } = parseTypeAndName(selected);
 +
 
 +
  return nameOrVnum === characterName && isCharacter;
 +
}
 +
 
 +
function useBonusVariationMode(character, variation) {
 +
  return (
 +
    isChecked(character.useBonusVariation) &&
 +
    character.hasOwnProperty(variation) &&
 +
    character.bonusVariationMinValue < character.bonusVariationMaxValue
 +
  );
 +
}
 +
 
 +
function createBattle(characters, battle) {
 +
  var battleChoice = battle.battleChoice;
 +
  var battleForm = battleChoice.form;
 +
  var lastInvalidTime = 0;
 +
 
 +
  battleForm.addEventListener("change", handleBattleFormChange);
 +
  battleForm.addEventListener("invalid", handleBattleFormInvalid, true);
 +
  battleForm.addEventListener("submit", handleBattleFormSubmit);
 +
 
 +
  function handleBattleFormChange(event) {
 +
    var target = event.target;
 +
    var { name: targetName, value: targetValue, type: targetType } = target;
 +
 
 +
    if (targetType !== "radio") {
 +
      return;
 +
    }
 +
 
 +
    if (targetName === "attackType") {
 +
      battleChoice.attackType.selectedText =
 +
        target.previousElementSibling.dataset.o;
 +
    } else {
 +
      updateBattleChoiceButton(battleChoice, targetName, targetValue);
 +
 
 +
      if (targetName === "attacker") {
 +
        filterAttackTypeSelection(characters, battleChoice, targetValue);
 +
      }
 +
    }
 +
  }
 +
 
 +
  function handleBattleFormInvalid(event) {
 +
    var currentTime = Date.now();
 +
 
 +
    if (currentTime - lastInvalidTime < 100) {
 +
      return;
 +
    }
 +
 
 +
    lastInvalidTime = currentTime;
 +
 
 +
    var target = event.target;
 +
    var modal = target.closest(".modal");
 +
 
 +
    if (!modal) {
 +
      return;
 +
    }
 +
 
 +
    var dataModal = modal.dataset.modal;
 +
 
 +
    if (!dataModal) {
 +
      return;
 +
    }
 +
 
 +
    var category = dataModal.split("-")[0];
 +
 
 +
    if (isValueInArray(category, battleChoice.categories)) {
 +
      battleChoice[category].button.click();
 +
    }
 +
  }
 +
 
 +
  function handleBattleFormSubmit(event) {
 +
    event.preventDefault();
 +
 
 +
    // auto save
 +
    if (characters.unsavedChanges) {
 +
      characters.saveButton.click();
 +
    }
 +
 
 +
    var battleInfo = new FormData(event.target);
 +
    var attackerData = battleInfo.get("attacker");
 +
    var attackType = battleInfo.get("attackType");
 +
    var victimData = battleInfo.get("victim");
 +
    var attackerVariation;
 +
    var victimVariation;
 +
 
 +
    if (!attackerData && !attackType && !victimData) {
 +
      return;
 +
    }
 +
 
 +
    var { nameOrVnum: attackerNameOrVnum, isCharacter: attackerIsPlayer } =
 +
      parseTypeAndName(attackerData);
 +
    var { nameOrVnum: victimNameOrVnum, isCharacter: victimIsPlayer } =
 +
      parseTypeAndName(victimData);
 +
 
 +
    if (attackerIsPlayer) {
 +
      var attacker = copyObject(characters.savedCharacters[attackerNameOrVnum]);
 +
      attackerVariation = attacker.bonusVariation;
 +
    } else {
 +
      var attacker = createMonster(attackerNameOrVnum);
 +
    }
 +
 
 +
    if (victimIsPlayer) {
 +
      var victim = copyObject(characters.savedCharacters[victimNameOrVnum]);
 +
      victimVariation = victim.bonusVariation;
 +
    } else {
 +
      var victim = createMonster(victimNameOrVnum, attacker);
 +
    }
 +
 
 +
    if (useBonusVariationMode(attacker, attackerVariation)) {
 +
      damageWithVariation(
 +
        attacker,
 +
        victim,
 +
        attackType,
 +
        battle,
 +
        attacker,
 +
        attackerVariation
 +
      );
 +
    } else if (useBonusVariationMode(victim, victimVariation)) {
 +
      damageWithVariation(
 +
        attacker,
 +
        victim,
 +
        attackType,
 +
        battle,
 +
        victim,
 +
        victimVariation
 +
      );
 +
    } else {
 +
      damageWithoutVariation(attacker, victim, attackType, battle, characters);
 +
    }
 +
  }
 +
}
 +
 
 +
function createMapping() {
 +
  var 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: {
 +
        damage: "Dégâts",
 +
        percentage: "Pourcentage",
 +
        miss: "Miss",
 +
        normalHit: "Coup classique",
 +
        criticalHit: "Coup critique",
 +
        piercingHit: "Coup perçant",
 +
        criticalPiercingHit: "Coup critique perçant",
 +
        damageRepartition: "Distribution des dégâts",
 +
        averageDamage: "Dégâts moyens",
 +
        damageAugmentation: "Augmentation des dégâts",
 +
        bonusVariationTitle: [
 +
          "Évolution des dégâts moyens",
 +
          "par rapport à la valeur d'un bonus",
 +
        ],
 +
      },
 +
      en: {
 +
        damage: "Damage",
 +
        percentage: "Percentage",
 +
        miss: "Miss",
 +
        normalHit: "Normal Hit",
 +
        criticalHit: "Critical Hit",
 +
        piercingHit: "Piercing Hit",
 +
        criticalPiercingHit: "Critical Piercing Hit",
 +
        damageRepartition: "Damage Repartition",
 +
        averageDamage: "Average Damage",
 +
        damageAugmentation: "Damage Augmentation",
 +
        bonusVariationTitle: [
 +
          "Evolution of Average Damage",
 +
          "Relative to a Bonus Value",
 +
        ],
 +
      },
 +
      tr: {
 +
        damage: "Hasar",
 +
        percentage: "Yüzde",
 +
        miss: "Miss Vuruş",
 +
        normalHit: "Düz Vuruş",
 +
        criticalHit: "Kritik Vuruş",
 +
        piercingHit: "Delici Vuruş",
 +
        criticalPiercingHit: "Kritikli Delici Vuruş",
 +
        damageRepartition: "Hasar Dağılımı",
 +
        averageDamage: "Ortalama Hasar",
 +
        damageAugmentation: "Ortalama Hasar Artışı",
 +
        bonusVariationTitle: [
 +
          "Bir bonusun değerine kıyasla",
 +
          "Ortalama Hasar Çizelgesi",
 +
        ],
 +
      },
 +
      ro: {
 +
        damage: "Daune",
 +
        percentage: "Procent",
 +
        miss: "Miss",
 +
        normalHit: "Lovitura normala",
 +
        criticalHit: "Lovitura critica",
 +
        piercingHit: "Lovitura patrunzatoare",
 +
        criticalPiercingHit: "Lovitura critica si patrunzatoare",
 +
        damageRepartition: "Distribuția daunelor",
 +
        averageDamage: "Media damageului",
 +
        damageAugmentation: "Damage imbunatatit",
 +
        bonusVariationTitle: [
 +
          "Evolutia mediei damageului",
 +
          "relativ la o valoare bonus",
 +
        ],
 +
      },
 +
      de: {
 +
        damage: "Schäden",
 +
        percentage: "Prozentsatz",
 +
        miss: "Verfehlen",
 +
        normalHit: "Normaler Treffer",
 +
        criticalHit: "Kritischer Treffer",
 +
        piercingHit: "Durchdringender Treffer",
 +
        criticalPiercingHit: "Kritischer durchdringender Treffer",
 +
        damageRepartition: "Schadensverteilung",
 +
        averageDamage: "Durchschnittlicher Schaden",
 +
        damageAugmentation: "Schadenserhöhung",
 +
        bonusVariationTitle: [
 +
          "Entwicklung des durchschnittlichen Schadens",
 +
          "im Verhältnis zu einem Bonus",
 +
        ],
 +
      },
 +
      pt: {
 +
        damage: "Dano",
 +
        percentage: "Percentagem",
 +
        miss: "Miss",
 +
        normalHit: "Dano normal",
 +
        criticalHit: "Dano crítico",
 +
        piercingHit: "Dano perfurante",
 +
        criticalPiercingHit: "Dano crítico perfurante",
 +
        damageRepartition: "Repartição de dano",
 +
        averageDamage: "Dano médio",
 +
        damageAugmentation: "Aumento de dano",
 +
        bonusVariationTitle: ["Evolução do dano médio", "relativo a um bónus"],
 +
      },
 +
      // es: {
 +
      //  damage: "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",
 +
      //  damageRepartition: "Repartición de daños",
 +
      //  averageDamage: "Daño medio",
 +
      //  damageAugmentation: "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 initDamageChart(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.plotDamage.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.damageRepartition,
 +
          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
 +
              );
 +
              var 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.damage,
 +
            font: {
 +
              size: 16,
 +
            },
 +
          },
 +
          ticks: {
 +
            callback: function (value) {
 +
              return Number.isInteger(value) ? value : "";
 +
            },
 +
          },
 +
        },
 +
        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.damageChart = {
 +
    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.averageDamage,
 +
          backgroundColor: "rgba(75, 192, 192, 0.2)",
 +
          borderColor: "rgba(75, 192, 192, 1)",
 +
          fill: true,
 +
        },
 +
        {
 +
          label: translation.damageAugmentation,
 +
          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.averageDamage,
 +
            font: {
 +
              size: 16,
 +
            },
 +
          },
 +
        },
 +
      },
 +
      elements: {
 +
        point: {
 +
          borderWidth: 1,
 +
          radius: 3,
 +
          hitRadius: 3,
 +
          hoverRadius: 6,
 +
          hoverBorderWidth: 2,
 +
        },
 +
      },
 +
    },
 +
  });
 +
 
 +
  battle.bonusVariationChart = chart;
 +
}
 +
 
 +
function filterAttackTypeSelection(characters, battleChoice, targetValue) {
 +
  var { nameOrVnum: attackerNameOrVnum, isCharacter: isAttackerPlayer } =
 +
    parseTypeAndName(targetValue);
 +
 
 +
  if (isAttackerPlayer) {
 +
    var attacker = characters.savedCharacters[attackerNameOrVnum];
 +
    filterAttackTypeSelectionCharacter(attacker, battleChoice.attackType);
 +
  } else {
 +
    filterAttackTypeSelectionMonster(battleChoice.attackType);
 +
  }
 +
}
 +
 
 +
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 addBattleData(battle) {
 +
  var errorElements = document.querySelectorAll("[data-error]");
 +
  var { elements: attackTypeElements, container: attackTypeContainer } =
 +
    battle.battleChoice.attackType;
 +
  var attackTypeChilds = attackTypeContainer.children;
 +
 
 +
  for (var index = 0; index < errorElements.length; index++) {
 +
    var errorElement = errorElements[index];
 +
    battle.errorInformation[errorElement.dataset.error] = errorElement;
 +
  }
 +
 
 +
  for (var index = 1; index < attackTypeChilds.length - 1; index++) {
 +
    var attackTypeChild = attackTypeChilds[index];
 +
    var input = attackTypeChild.querySelector("input");
 +
 
 +
    attackTypeElements.push({
 +
      container: attackTypeChild,
 +
      input: input,
 +
      inputClass: input.dataset.class,
 +
      inputValue: input.value,
 +
    });
 +
  }
 +
}
 +
 
 +
function createDamageCalculatorInformation(chartSource) {
 +
   var characters = {
 +
     unsavedChanges: false,
 +
     savedCharacters: {},
 +
     currentCharacter: null,
 +
    savedMonsters: getSavedMonsters(),
 +
    monsterElements: {},
 +
     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"),
 +
     monsterButtonTemplates: document.getElementById("monster-button-templates"),
 +
    monsterTemplate: document.getElementById("new-monster-template")
 +
       .children[0],
 +
     monstersContainer: document.getElementById("monsters-container"),
 +
     monsteriFrame: document.getElementById("monster-iframe"),
 +
     stoneiFrame: document.getElementById("stone-iframe"),
 +
     saveButton: document.getElementById("save-character"),
 +
     weaponCategory: document.getElementById("weapon-category"),
 
     weaponDisplay: document.getElementById("weapon-display"),
 
     weaponDisplay: document.getElementById("weapon-display"),
     randomAttackValue: document.getElementById("random-attack-value"),
+
     randomAttackValue: document.getElementById("random-attack-value"),
     randomMagicAttackValue: document.getElementById(
+
     randomMagicAttackValue: document.getElementById(
       "random-magic-attack-value"
+
       "random-magic-attack-value"
     ),
+
     ),
     yoharaCreation: document.getElementById("yohara-creation"),
+
     toggleSiblings: {},
     blessingCreation: document.getElementById("blessing-creation"),
+
    polymorphDisplay: document.getElementById("polymorph-display"),
    marriageCreation: document.getElementById("marriage-creation"),
+
     bonusVariation: {
  };
+
      tabButton: document.getElementById("Variation"),
 
+
      checkbox: document.getElementById("use-bonus-variation"),
  delete characters.newCharacterTemplate.dataset.click;
+
      input: document.getElementById("bonus-variation"),
 
+
      inputName: document.getElementById("bonus-variation-name"),
  var savedCharacters = getSavedCharacters();
+
      container: document.getElementById("bonus-variation-creation"),
   var savedMonsters = getSavedMonsters();
+
      minValue: document.getElementById("bonus-variation-min-value"),
 
+
      maxValue: document.getElementById("bonus-variation-max-value"),
   for (var [pseudo, character] of Object.entries(savedCharacters)) {
+
      disabledText: document.getElementById("bonus-variation-disabled"),
     characters.savedCharacters[pseudo] = character;
+
      selectedText: document.getElementById("bonus-variation-selected"),
   }
+
      displaySpan: document.getElementById("bonus-variation-display"),
 
+
    },
   characters.savedMonsters = savedMonsters;
+
    skillElementsToFilter: document.querySelectorAll(
 
+
      "#skill-container [data-class]"
  var skillContainer = document.getElementById("skill-container");
+
    ),
  characters.skillElementsToFilter =
+
   };
    skillContainer.querySelectorAll("[data-class]");
+
 
 
+
   for (var [pseudo, character] of Object.entries(getSavedCharacters())) {
   var mapping = createMapping();
+
     characters.savedCharacters[pseudo] = character;
   var constants = createConstants();
+
   }
 
+
 
   var battle = {
+
   document.querySelectorAll(".toggle-sibling").forEach(function (element) {
     resetAttackType: false,
+
    var target = element.dataset.target;
    battleForm: document.getElementById("create-battle"),
+
    var sibling = document.getElementById(target);
    attackerSelection: document.getElementById("attacker-selection"),
+
    characters.toggleSiblings[element.name] = sibling;
    attackTypeSelection: document.getElementById("attack-type-selection"),
+
   });
    victimSelection: document.getElementById("victim-selection"),
+
 
    damageResult: document.getElementById("result-damage"),
+
   var constants = createConstants();
     errorInformation: {},
+
 
     tableContainer: document.getElementById("result-table-container"),
+
   var battle = {
     tableResult: document.getElementById("result-table").children[0],
+
     savedFights: getSavedFights(),
     mapping: mapping,
+
    battleChoice: {
     constants: constants,
+
      resetAttackType: false,
  };
+
      form: document.getElementById("create-battle"),
 
+
      template: document.getElementById("battle-selection-template")
  var errorElements = document
+
        .children[0],
     .getElementById("error-information")
+
      raceToImage: {
     .querySelectorAll("li[data-error]");
+
        warrior: "/images/0/0f/Bandeaurougehomme.png",
 
+
        ninja: "/images/0/0e/Queuedechevalclair.png",
   for (var index = 0; index < errorElements.length; index++) {
+
        sura: "/images/3/37/Couperespectablerouge.png",
     var errorElement = errorElements[index];
+
        shaman: "/images/6/6a/Coupeeleganteclairfemme.png",
     battle.errorInformation[errorElement.dataset.error] = errorElement;
+
        lycan: "/images/4/4e/Protectionfrontalerouge.png",
   }
+
      },
 +
      categories: ["attacker", "victim"],
 +
      attacker: {
 +
        character: {
 +
          count: 0,
 +
          container: document.getElementById("attacker-selection-characters"),
 +
          elements: {},
 +
        },
 +
        monster: {
 +
          count: 0,
 +
          container: document.getElementById("attacker-selection-monsters"),
 +
          elements: {},
 +
        },
 +
        button: document.getElementById("attacker-trigger"),
 +
        defaultButtonContent: document.getElementById(
 +
          "attacker-default-button-content"
 +
        ),
 +
        buttonContent: document.getElementById("attacker-button-content"),
 +
        container: document.getElementById("attacker-selection"),
 +
        selected: null,
 +
      },
 +
      victim: {
 +
        character: {
 +
          count: 0,
 +
          container: document.getElementById("victim-selection-characters"),
 +
          elements: {},
 +
        },
 +
        monster: {
 +
          count: 0,
 +
          container: document.getElementById("victim-selection-monsters"),
 +
          elements: {},
 +
        },
 +
        stone: {
 +
          count: 0,
 +
          container: document.getElementById("victim-selection-stones"),
 +
          elements: {},
 +
        },
 +
        button: document.getElementById("victim-trigger"),
 +
        defaultButtonContent: document.getElementById(
 +
          "victim-default-button-content"
 +
        ),
 +
        buttonContent: document.getElementById("victim-button-content"),
 +
        selected: null,
 +
      },
 +
      attackType: {
 +
        container: document.getElementById("attack-type-selection"),
 +
        elements: [],
 +
        defaultInput: document.getElementById("physical-attack"),
 +
        selectedText: "",
 +
      },
 +
     },
 +
    damageWeightedByType: {},
 +
    scatterDataByType: {},
 +
     damageByBonus: [],
 +
    tableResultFight: document.getElementById("result-table-fight"),
 +
     tableResultHistory: document.getElementById("result-table-history"),
 +
    deleteFightTemplate: document.getElementById("delete-fight-template")
 +
      .children[0],
 +
     errorInformation: {},
 +
    fightResultTitle: document.getElementById("fight-result-title"),
 +
    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"),
 +
    plotDamage: document.getElementById("plot-damage"),
 +
     plotBonusVariation: document.getElementById("plot-bonus-variation"),
 +
     uniqueDamageCounters: document.querySelectorAll(".unique-damage-counter"),
 +
    possibleDamageCounter: document.getElementById("possible-damage-counter"),
 +
    damageTime: document.getElementById("damage-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: createMapping(),
 +
    constants: constants,
 +
    translation: getTranslation(constants.translation),
 +
  };
 +
 
 +
   addBattleData(battle);
 +
  initResultTableHistory(battle);
 +
  addScript(chartSource, function () {
 +
     initDamageChart(battle);
 +
     initBonusVariationChart(battle);
 +
   });
 +
  reduceChartPointsListener(battle);
 +
  downloadRawDataListener(battle);
  
 
   return [characters, battle];
 
   return [characters, battle];
}
 
 
function loadScript(src, callback) {
 
  var script = document.createElement("script");
 
  script.src = src;
 
 
  function onComplete() {
 
    if (script.parentNode) {
 
      script.parentNode.removeChild(script);
 
    }
 
    callback();
 
  }
 
 
  document.head.appendChild(script);
 
 
  script.onload = onComplete;
 
  script.onerror = onComplete;
 
 
}
 
}
  
Ligne 3 838 : Ligne 5 799 :
  
 
   document.head.appendChild(link);
 
   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");
 
 
}
 
}
  
Ligne 3 853 : Ligne 5 806 :
 
   var cssSource =
 
   var cssSource =
 
     "/index.php?title=Utilisateur:Ankhseram/Style.css&action=raw&ctype=text/css";
 
     "/index.php?title=Utilisateur:Ankhseram/Style.css&action=raw&ctype=text/css";
 +
  var chartSource = "https://cdn.jsdelivr.net/npm/chart.js";
  
 
   loadStyle(cssSource);
 
   loadStyle(cssSource);
  
 
   function main() {
 
   function main() {
     var [characters, battle] = createDamageCalculatorInformation();
+
     var [characters, battle] = createDamageCalculatorInformation(chartSource);
  
 
     characterManagement(characters, battle);
 
     characterManagement(characters, battle);
 
     monsterManagement(characters, battle);
 
     monsterManagement(characters, battle);
  
     updateBattleChoice(characters, battle);
+
     updateBattleChoice(characters, battle.battleChoice);
 
     createBattle(characters, battle);
 
     createBattle(characters, battle);
 
    loading();
 
 
   }
 
   }
 
+
   addScript(javascriptSource, main);
   loadScript(javascriptSource, main);
 
 
})();
 
})();

Version actuelle datée du 17 février 2025 à 06:17

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

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

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

function splitFirst(value, delimiter) {
  var parts = value.split(delimiter);
  var first = parts[0];
  var rest = parts.slice(1).join(delimiter);
  return [first, rest];
}

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 newChangeEvent() {
  return new Event("change", { bubbles: true });
}

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

function openTargetTab(target) {
  var tabberContainer = target.closest(".tabber-container");

  if (!tabberContainer) {
    return;
  }

  var [buttonsContainer, tabsContainer] = tabberContainer.children;
  var buttons = buttonsContainer.children;
  var tabs = tabsContainer.children;

  for (var index = 0; index < tabs.length; index++) {
    var tab = tabs[index];
    if (tab.contains(target) && !tab.checkVisibility()) {
      buttons[index].click();
      break;
    }
  }
}

function openTargetCollapsible(target) {
  var collapsible = target.closest(".improved-collapsible");

  if (!collapsible) {
    return;
  }

  var span = collapsible.firstElementChild;

  if (!span.classList.contains("mw-collapsible-toggle-expanded")) {
    span.click();
  }
}

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 calcMeanDamage(damageWeightedByType, totalCardinal) {
  var sumDamage = 0;

  for (var damageTypeName in damageWeightedByType) {
    if (damageTypeName === "miss") {
      continue;
    }

    var damageWeighted = damageWeightedByType[damageTypeName];

    for (var damage in damageWeighted) {
      sumDamage += damage * damageWeighted[damage];
    }
  }

  return sumDamage / totalCardinal;
}

function prepareDamageData(damageWeightedByType, attackValues) {
  var totalCardinal = attackValues.totalCardinal;
  var minDamage = Infinity;
  var maxDamage = 0;
  var scatterDataByType = {};
  var sumDamage = 0;
  var possibleDamageCount = 0;
  var possibleDamageCountTemp = 0;
  var uniqueDamageCount = 0;

  for (var damageTypeName in damageWeightedByType) {
    if (damageTypeName === "miss") {
      scatterDataByType.miss = damageWeightedByType.miss;
      possibleDamageCount++;
      uniqueDamageCount++;
      continue;
    } else if (
      (damageTypeName === "criticalHit" ||
        damageTypeName === "criticalPiercingHit") &&
      attackValues.isPlayerVsPlayer
    ) {
      possibleDamageCountTemp =
        attackValues.possibleDamageCount * attackValues.weapon.interval;
    } else {
      possibleDamageCountTemp = attackValues.possibleDamageCount;
    }

    var firstIteration = true;
    var damageWeighted = damageWeightedByType[damageTypeName];
    var scatterData = [];
    scatterDataByType[damageTypeName] = scatterData;

    for (var damage in damageWeighted) {
      damage = +damage;

      if (firstIteration) {
        if (damage < minDamage) {
          minDamage = damage;
        }
        firstIteration = false;
      }

      var weight = damageWeighted[damage];
      var probability = weight / totalCardinal;

      sumDamage += damage * weight;
      damageWeighted[damage] = probability;
      scatterData.push({ x: damage, y: probability });
    }

    var scatterDataLength = scatterData.length;

    possibleDamageCount += possibleDamageCountTemp;
    uniqueDamageCount += scatterDataLength;

    if (damage > maxDamage) {
      maxDamage = damage;
    }
  }

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

  attackValues.possibleDamageCount = possibleDamageCount;

  return [
    sumDamage / totalCardinal,
    minDamage,
    maxDamage,
    scatterDataByType,
    uniqueDamageCount,
  ];
}

function aggregateDamage(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 aggregateDamage = 0;
    var aggregateProbability = 0;

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

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

    remainingData -= groupLength;
  }

  return aggregateScatterData;
}

function addToDamageChart(
  scatterDataByType,
  damageChart,
  isReducePointsChecked
) {
  var { chart, datasetsStyle, maxPoints, reduceChartPointsContainer } =
    damageChart;
  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 = aggregateDamage(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(
  damageByBonus,
  augmentationByBonus,
  xLabel,
  chart
) {
  chart.data.datasets[0].data = damageByBonus;
  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 updateDamageChartDescription(
  uniqueDamageCounters,
  uniqueDamageCount,
  formatNumber
) {
  uniqueDamageCounters.forEach(function (element) {
    if (uniqueDamageCount <= 1) {
      hideElement(element.parentElement);
    } else {
      showElement(element.parentElement);
      element.textContent = formatNumber.format(uniqueDamageCount);
    }
  });
}

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

function filterClass(selectedRace, classChoice, selectValueIsChanged = false) {
  for (var radioNode of classChoice) {
    var radioGrandParent = radioNode.parentElement.parentElement;

    if (radioNode.getAttribute("data-race") === selectedRace) {
      if (!selectValueIsChanged) {
        radioNode.checked = true;
        selectValueIsChanged = true;
      }
      showElement(radioGrandParent);
    } else {
      hideElement(radioGrandParent);
    }
  }
}

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

  if (!selectValueIsChanged) {
    var weaponType = createWeapon(weaponElement.value).type;

    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 changePolymorphValues(characterCreation, monsterVnum, monsterImage) {
  var { polymorphMonster, polymorphMonsterImage } = characterCreation;

  polymorphMonster.value = monsterVnum;
  polymorphMonsterImage.value = monsterImage;

  polymorphMonster.dispatchEvent(newChangeEvent());
}

function resetImageFromWiki(image) {
  image.removeAttribute("srcset");
  image.removeAttribute("data-file-width");
  image.removeAttribute("data-file-height");
}

function handleImageFromWiki(image, newSrc) {
  image.src = newSrc;
  image.alt = newSrc.split("/").pop();
}

function handlePolymorphDisplay(polymorphDisplay, monsterVnum, monsterSrc) {
  var oldImage = polymorphDisplay.firstChild;
  var oldLink = oldImage.nextElementSibling;
  var monsterName = getMonsterName(monsterVnum);
  var newLink = createWikiLink(monsterName);

  resetImageFromWiki(oldImage);
  handleImageFromWiki(oldImage, monsterSrc);

  polymorphDisplay.replaceChild(newLink, oldLink);
}

function createWikiLink(pageName) {
  var wikiLink = document.createElement("a");

  wikiLink.href = mw.util.getUrl(pageName);
  wikiLink.title = pageName;
  wikiLink.textContent = pageName;

  return wikiLink;
}

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

function handleWeaponDisplay(
  weaponCategory,
  weaponDisplay,
  weaponVnum,
  newWeapon
) {
  var newWeapon = newWeapon || getSelectedWeapon(weaponCategory);

  var newImage = newWeapon.nextElementSibling;
  var newText = document.createElement("span");
  var oldImage = weaponDisplay.firstChild;
  var oldText = oldImage.nextElementSibling;
  var weaponName = newImage.nextElementSibling.dataset.o;

  if (weaponVnum == 0) {
    newText.textContent = weaponName;
  } else {
    var weaponLink = createWikiLink(weaponName);
    newText.appendChild(weaponLink);
  }

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

function filterUpgrade(
  weaponUpgrade,
  weaponVnum,
  randomAttackValue,
  randomMagicAttackValue
) {
  var weapon = createWeapon(weaponVnum);
  var currentUpgrade = Number(weaponUpgrade.value);
  var weaponUpgradeChildren = weaponUpgrade.children;

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

    if (weapon.isMagic) {
      showElement(randomMagicAttackValue);
    }
  } else {
    hideElement(randomAttackValue);
    hideElement(randomMagicAttackValue);
  }

  if (weapon.maxUpgrade < 1) {
    hideElement(weaponUpgrade.parentElement);
  } else {
    showElement(weaponUpgrade.parentElement);
  }

  for (var upgrade = 0; upgrade <= weapon.maxUpgrade; upgrade++) {
    showElement(weaponUpgradeChildren[upgrade]);
  }

  for (
    var upgrade = weapon.maxUpgrade + 1;
    upgrade < weaponUpgradeChildren.length;
    upgrade++
  ) {
    hideElement(weaponUpgradeChildren[upgrade]);
  }

  if (currentUpgrade > weapon.maxUpgrade) {
    weaponUpgrade.value = weapon.maxUpgrade;
  }
}

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 filterForm(characters, battle) {
  var { saveButton, characterCreation, toggleSiblings } = characters;
  var allowedWeaponsPerRace = battle.constants.allowedWeaponsPerRace;
  var battleChoice = battle.battleChoice;

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

    saveButtonOrange(saveButton);
    characters.unsavedChanges = true;

    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
        );
        handleWeaponDisplay(
          characters.weaponCategory,
          characters.weaponDisplay,
          weaponElement.value
        );
        filterUpgrade(
          characterCreation.weaponUpgrade,
          weaponElement.value,
          characters.randomAttackValue,
          characters.randomMagicAttackValue
        );
        filterSkills(classChoice.value, characters.skillElementsToFilter);
        handleBonusVariationUpdate(
          characterCreation,
          characters.bonusVariation,
          true
        );

        battleChoice.resetAttackType = true;
        break;
      case "class":
        filterSkills(target.value, characters.skillElementsToFilter);
        handleBonusVariationUpdate(
          characterCreation,
          characters.bonusVariation,
          true
        );

        battleChoice.resetAttackType = true;
        break;
      case "weapon":
        var weaponElement = characterCreation.weapon;

        handleWeaponDisplay(
          characters.weaponCategory,
          characters.weaponDisplay,
          weaponElement.value,
          target
        );
        filterUpgrade(
          characterCreation.weaponUpgrade,
          target.value,
          characters.randomAttackValue,
          characters.randomMagicAttackValue
        );
        handleBonusVariationUpdate(
          characterCreation,
          characters.bonusVariation
        );
        break;
      case "isRiding":
        battleChoice.resetAttackType = true;
        break;
      case "isPolymorph":
        battleChoice.resetAttackType = true;
        break;
    }

    if (toggleSiblings.hasOwnProperty(targetName)) {
      filterCheckbox(target, toggleSiblings[targetName]);
    }

    if (
      targetName.startsWith("attackSkill") ||
      targetName.startsWith("horseSkill")
    ) {
      battleChoice.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() {
  var savedMonsters = getLocalStorageValue("savedMonstersCalculator", {});

  if (Array.isArray(savedMonsters)) {
    return {};
  }

  var filteredMonsters = {};

  for (var vnum in savedMonsters) {
    if (
      String(Number(vnum)) === vnum &&
      savedMonsters[vnum].hasOwnProperty("category")
    ) {
      filteredMonsters[vnum] = savedMonsters[vnum];
    }
  }

  updateSavedMonsters(filteredMonsters);

  return filteredMonsters;
}

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);
    });
  }

  var characterPseudo = characterDataObject.name;
  var { battleChoice } = battle;

  savedCharacters[characterPseudo] = characterDataObject;
  updateSavedCharacters(savedCharacters);

  if (newCharacter) {
    addBattleChoice(battleChoice, characterPseudo, characterDataObject);
  } else {
    updateBattleChoiceImage(
      battleChoice,
      characterPseudo,
      characterDataObject.race
    );
  }

  if (battleChoice.resetAttackType) {
    if (isCharacterSelected(characterPseudo, battleChoice.attacker.selected)) {
      filterAttackTypeSelectionCharacter(
        characterDataObject,
        battleChoice.attackType
      );
    }
    battleChoice.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", handleSubmitForm);
  characterCreation.addEventListener("invalid", handleInvalidInput, true);
  document.addEventListener("keydown", handleSaveShortcut);
  weaponCategory.addEventListener("mouseover", handleTooltipOverflow);

  function handleSubmitForm(event) {
    event.preventDefault();

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

  function handleInvalidInput(event) {
    var target = event.target;

    if (target.checkVisibility()) {
      return;
    }

    var autoCorrectInput = target.closest(".auto-correct-input");

    if (
      autoCorrectInput &&
      autoCorrectInput.classList.contains("tabber-noactive")
    ) {
      target.value = target.defaultValue;
      return;
    }

    openTargetTab(target);
    openTargetCollapsible(target);
  }

  function handleSaveShortcut(event) {
    if (event.ctrlKey && event.key === "s") {
      event.preventDefault();
      saveButton.click();
    }
  }

  function handleTooltipOverflow(event) {
    var 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) {
  delete characters.savedCharacters[pseudo];
  element.remove();

  updateSavedCharacters(characters.savedCharacters);
  removeBattleChoice(battle.battleChoice, pseudo, "character");

  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, battle, monsterVnum) {
  var monsterElements = characters.monsterElements;
  var monsterType = characters.savedMonsters[monsterVnum].category;

  if (monsterElements.hasOwnProperty(monsterVnum)) {
    monsterElements[monsterVnum].remove();
    delete monsterElements[monsterVnum];
  }

  delete characters.savedMonsters[monsterVnum];

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

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
  );
  handlePolymorphDisplay(
    characters.polymorphDisplay,
    characterCreation.polymorphMonster.value,
    characterCreation.polymorphMonsterImage.value
  );
  handleWeaponDisplay(
    characters.weaponCategory,
    characters.weaponDisplay,
    weaponElement.value
  );
  filterUpgrade(
    characterCreation.weaponUpgrade,
    weaponElement.value,
    characters.randomAttackValue,
    characters.randomMagicAttackValue
  );
  for (var [targetName, sibling] of Object.entries(characters.toggleSiblings)) {
    filterCheckbox(characterCreation[targetName], sibling);
  }
  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 resetBonusVariation(bonusVariation) {
  function resetInput(input) {
    input.removeAttribute("min");
    input.removeAttribute("max");
    input.defaultValue = 0;
    input.value = input.defaultValue;
  }
  var { minValue, maxValue, checkbox, container, disabledText, selectedText } =
    bonusVariation;

  resetInput(minValue);
  resetInput(maxValue);
  showElement(disabledText);
  hideElement(selectedText);
  hideElement(container);
  checkbox.checked = false;
}

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

  if (
    resetSkill &&
    (selectedBonus.startsWith("attackSkill") ||
      selectedBonus.startsWith("horseSkill") ||
      selectedBonus.startsWith("skillBonus"))
  ) {
    resetBonusVariation(bonusVariation);
    return;
  }

  if (
    characterCreation.hasOwnProperty(selectedBonus) &&
    selectedBonus != 0 &&
    displayName != 0
  ) {
    handleBonusVariation(
      characterCreation[selectedBonus],
      bonusVariation,
      displayName
    );
  } else {
    resetBonusVariation(bonusVariation);
  }
}

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 (targetName === "level") {
    targetContent = targetParent.textContent.replace("Lv", "").trim();
  } else if (isSkill) {
    var container = targetParent.children[1];

    for (var index = 0; 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.trim();
}

function handleBonusVariation(target, bonusVariation, displayName) {
  var {
    minValue,
    maxValue,
    checkbox,
    container,
    disabledText,
    selectedText,
    displaySpan,
  } = 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 (isSkill) {
    var options = target.options;

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

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

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

  hideElement(disabledText);
  showElement(selectedText);

  if (displayName) {
    minValue.value = Math.min(Math.max(minValue.value, targetMin), targetMax);
    maxValue.value = Math.max(Math.min(maxValue.value, targetMax), targetMin);

    displaySpan.textContent = displayName;
  } else {
    var { input, inputName, tabButton } = bonusVariation;
    var targetContent = getTargetContent(targetParent, targetName, isSkill);

    input.value = targetName;
    inputName.value = targetContent;
    displaySpan.textContent = targetContent;

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

    showElement(container);
    checkbox.checked = true;

    tabButton.click();
    tabButton.scrollIntoView(true);

    input.dispatchEvent(newChangeEvent());
  }
}

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
  );

  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") ||
      target.classList.contains("disabled-variation")
    ) {
      return;
    }

    handleBonusVariation(target, bonusVariation, null);
  }

  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 addMonsterElement(characters, battle, monsterVnum, iframeInfo) {
  var { monsterTemplate, monstersContainer } = characters;
  var monsterElement = monsterTemplate.cloneNode(true);
  var spanInput = monsterElement.querySelector("span.input");
  var deleteSvg = monsterElement.querySelector("svg");
  var monsterName = getMonsterName(monsterVnum);
  var link = createWikiLink(monsterName);

  monsterElement.setAttribute("tabindex", "0");
  spanInput.appendChild(link);
  monstersContainer.appendChild(monsterElement);
  characters.monsterElements[monsterVnum] = monsterElement;

  deleteSvg.addEventListener("click", function () {
    var monster = characters.savedMonsters[monsterVnum];
    iframeInfo[monster.category].shouldBeUpdated = true;

    deleteMonster(characters, battle, monsterVnum);
  });
}

function addNewMonster(
  characters,
  battle,
  monsterVnum,
  monsterImage,
  iframeInfo,
  category
) {
  if (isValueInArray(monsterVnum, Object.keys(characters.savedMonsters)))
    return;

  var newMonster = {
    image: monsterImage,
    category: category,
  };

  characters.savedMonsters[monsterVnum] = newMonster;

  addMonsterElement(characters, battle, monsterVnum, iframeInfo);
  updateSavedMonsters(characters.savedMonsters);
  addBattleChoice(battle.battleChoice, monsterVnum, newMonster, true);
}

function addButtonsToCards(characters, iframeDoc, iframeInfo, category) {
  var buttonTemplates = characters.monsterButtonTemplates.children[0];
  var cardToEdit = iframeDoc.getElementById("cards-container").children;
  var { nameToVnum } = iframeInfo;
  var vnumToButtons = iframeInfo[category].vnumToButtons;

  for (var cardIndex = 0; cardIndex < cardToEdit.length; cardIndex++) {
    var card = cardToEdit[cardIndex];
    var cardName = card.querySelector("[data-name]").firstChild.title;
    var buttonTemplatesClone = buttonTemplates.cloneNode(true);

    cardName = cardName.replace(/\s/g, " ");

    if (!nameToVnum.hasOwnProperty(cardName)) {
      continue;
    }

    var monsterVnum = nameToVnum[cardName];

    buttonTemplatesClone.dataset.monsterId = monsterVnum;
    card.lastElementChild.appendChild(buttonTemplatesClone);
    vnumToButtons[monsterVnum] = buttonTemplatesClone.children;
  }
}

function updateiFrameButtons(characters, iframeInfo, category) {
  var addedMonsters = Object.keys(characters.savedMonsters);
  var { currentiFrameIsMonster } = iframeInfo;
  var vnumToButtons = iframeInfo[category].vnumToButtons;
  var isPolymorphModal = category === "monster" && !currentiFrameIsMonster;

  for (var monsterVnum in vnumToButtons) {
    var [addButton, deleteButton, selectButton] = vnumToButtons[monsterVnum];

    if (isPolymorphModal) {
      hideElement(addButton);
      hideElement(deleteButton);
      showElement(selectButton);
      continue;
    }

    if (isValueInArray(monsterVnum, addedMonsters)) {
      hideElement(addButton);
      showElement(deleteButton);
      hideElement(selectButton);
    } else {
      showElement(addButton);
      hideElement(deleteButton);
      hideElement(selectButton);
    }
  }

  iframeInfo.lastiFrameIsMonster = currentiFrameIsMonster;
}

function getMonsterImage(buttonsContainer) {
  var elder = buttonsContainer.parentElement.firstElementChild;
  var image = elder.querySelector("img");

  if (image) {
    return image.getAttribute("src") || "";
  }

  return "";
}

function handleiFrame(iframeInfo, category) {
  var { characters, battle } = iframeInfo;
  var iframeInfoCategory = iframeInfo[category];
  var { iframe, pageName, vnumToButtons } = iframeInfoCategory;
  var loadingAnimation = iframeInfoCategory.iframe.previousElementSibling;

  iframe.src = mw.util.getUrl(pageName);

  iframe.addEventListener("load", function () {
    var iframeDoc = this.contentDocument || this.contentWindow.document;
    var iframeBody = iframeDoc.body;
    var content = iframeDoc.getElementById("show-after-loading");

    iframeBody.firstElementChild.replaceWith(content);

    iframeBody.style.background = "transparent";
    iframeBody.style.paddingRight = "10px";

    addButtonsToCards(characters, iframeDoc, iframeInfo, category);
    updateiFrameButtons(characters, iframeInfo, category);

    iframeInfoCategory.loadIsFinished = true;

    hideElement(loadingAnimation);
    showElement(iframe);

    iframeDoc.addEventListener("click", handleButtonClick);

    function handleButtonClick(event) {
      var target = event.target;
      var link = target.closest("a");
      var buttonsContainer = target.closest(".handle-monster");

      if (link) {
        event.preventDefault();
        window.open(link.href, "_blank");
      } else if (buttonsContainer) {
        var { monsterId: monsterVnum } = buttonsContainer.dataset;

        // polymorph iframe
        if (category === "monster" && !iframeInfo.currentiFrameIsMonster) {
          var monsterImage = getMonsterImage(buttonsContainer);
          changePolymorphValues(
            characters.characterCreation,
            monsterVnum,
            monsterImage
          );
          handlePolymorphDisplay(
            characters.polymorphDisplay,
            monsterVnum,
            monsterImage
          );
          iframe.dispatchEvent(newChangeEvent());
        } else {
          var addedMonsters = Object.keys(characters.savedMonsters);
          var [addButton, deleteButton] = vnumToButtons[monsterVnum];

          if (isValueInArray(monsterVnum, addedMonsters)) {
            deleteMonster(characters, battle, monsterVnum);
            showElement(addButton);
            hideElement(deleteButton);
          } else {
            var monsterImage = getMonsterImage(buttonsContainer);
            addNewMonster(
              characters,
              battle,
              monsterVnum,
              monsterImage,
              iframeInfo,
              category
            );
            hideElement(addButton);
            showElement(deleteButton);
          }
        }
      }
    }
  });
}

function getNameToVnumMapping() {
  var nameToVnum = {};

  for (var monsterVnum in monsterData) {
    nameToVnum[monsterData[monsterVnum][monsterData[monsterVnum].length - 1]] =
      monsterVnum;
  }

  return nameToVnum;
}

function monsterManagement(characters, battle) {
  var { monsteriFrame, stoneiFrame } = characters;
  var monsterVnums = Object.keys(monsterData);

  var iframeInfo = {
    lastiFrameIsMonster: false,
    currentiFrameIsMonster: false,
    characters: characters,
    battle: battle,
    nameToVnum: getNameToVnumMapping(),
    monster: {
      isLoaded: false,
      loadIsFinished: false,
      shouldBeUpdated: false,
      vnumToButtons: {},
      pageName: "Monstres",
      iframe: monsteriFrame,
    },
    stone: {
      isLoaded: false,
      loadIsFinished: false,
      shouldBeUpdated: false,
      vnumToButtons: {},
      pageName: "Pierres Metin",
      iframe: stoneiFrame,
    },
  };

  document.addEventListener("modalOpen", function (event) {
    var modalName = event.detail.name;

    if (modalName === "monster" || modalName === "stone") {
      handleModal(event.target, modalName);
    }
  });

  function handleModal(target, modalName) {
    var iframeInfoCategory = iframeInfo[modalName];

    if (modalName === "monster") {
      var isMonsteriFrame = target.id === "add-new-monster";
      iframeInfo.currentiFrameIsMonster = isMonsteriFrame;

      if (
        iframeInfoCategory.loadIsFinished &&
        ((isMonsteriFrame && !iframeInfo.lastiFrameIsMonster) ||
          (!isMonsteriFrame && iframeInfo.lastiFrameIsMonster))
      ) {
        iframeInfoCategory.shouldBeUpdated = true;
      }
    }

    if (!iframeInfoCategory.isLoaded) {
      handleiFrame(iframeInfo, modalName);
      iframeInfoCategory.isLoaded = true;
    }

    if (
      iframeInfoCategory.loadIsFinished &&
      iframeInfoCategory.shouldBeUpdated
    ) {
      updateiFrameButtons(characters, iframeInfo, modalName);
      iframeInfoCategory.shouldBeUpdated = false;
    }
  }

  Object.keys(characters.savedMonsters)
    .slice()
    .forEach(function (monsterVnum) {
      if (isValueInArray(monsterVnum, monsterVnums)) {
        addMonsterElement(characters, battle, monsterVnum, iframeInfo);
      } else {
        deleteMonster(characters, battle, monsterVnum);
      }
    });
}

function hideAttackType(container, input, attackType) {
  hideElement(container);

  if (input.checked) {
    var defaultInput = attackType.defaultInput;

    input.checked = false;
    defaultInput.checked = true;
    defaultInput.dispatchEvent(newChangeEvent());
  }
}

function filterAttackTypeSelectionCharacter(attacker, attackType) {
  var attackerClass = attacker.class;
  var attackerIsNotPolymorph = !isPolymorph(attacker);
  var { elements: attackTypeElements } = attackType;

  for (var index = 0; index < attackTypeElements.length; index++) {
    var { container, input, inputClass, inputValue } =
      attackTypeElements[index];

    if (
      attackerIsNotPolymorph &&
      attacker[inputValue] &&
      (attackerClass === inputClass ||
        (inputValue.startsWith("horseSkill") &&
          isRiding(attacker) &&
          (!inputClass || isValueInArray(attackerClass, inputClass))))
    ) {
      showElement(container);
    } else {
      hideAttackType(container, input, attackType);
    }
  }
}

function filterAttackTypeSelectionMonster(attackType) {
  var attackTypeElements = attackType.elements;

  for (var index = 0; index < attackTypeElements.length; index++) {
    var { container, input } = attackTypeElements[index];
    hideAttackType(container, input, attackType);
  }
}

function removeBattleElement(battleChoice, nameOrVnum, category, type) {
  var elements = battleChoice[category][type].elements;

  if (elements.hasOwnProperty(nameOrVnum)) {
    elements[nameOrVnum].container.remove();
    delete elements[nameOrVnum];
    battleChoice[category][type].count--;
  }
}

function removeBattleChoice(battleChoice, nameOrVnum, type) {
  battleChoice.categories.forEach(function (category) {
    if (category === "attacker" && type === "stone") {
      return;
    }
    var selected = battleChoice[category].selected;

    if (selected) {
      var { type: selectedType, nameOrVnum: selectedNameOrVnum } =
        parseTypeAndName(selected);

      if (nameOrVnum === selectedNameOrVnum && type === selectedType) {
        resetBattleChoiceButton(battleChoice, category);
        battleChoice[category].selected = null;
      }
    }

    removeBattleElement(battleChoice, nameOrVnum, category, type);
    updateBattleChoiceText(battleChoice, category, type);
  });
}

function addBattleElement(
  battleChoice,
  pseudoOrVnum,
  characterOrMonster,
  category,
  isMonster
) {
  var { template, raceToImage } = battleChoice;
  var battleChoiceCategory = battleChoice[category];
  var templateClone = template.cloneNode(true);
  var label = templateClone.firstElementChild;
  var [input, image, span] = label.children;
  var imageSrc;
  var name;
  var type;

  if (isMonster) {
    type = characterOrMonster.category;

    if (type === "stone" && category === "attacker") {
      return;
    }
    imageSrc = characterOrMonster.image;
    name = getMonsterName(pseudoOrVnum);
  } else {
    type = "character";
    imageSrc = raceToImage[characterOrMonster.race];
    name = pseudoOrVnum;
    label.classList.add("notranslate");
  }

  var value = type + "-" + pseudoOrVnum;
  var id = category + "-" + value;
  var currentBattleChoice = battleChoiceCategory[type];

  label.setAttribute("for", id);
  input.value = value;
  input.id = id;
  input.name = category;
  span.textContent = name;

  handleImageFromWiki(image, imageSrc);

  currentBattleChoice.container.appendChild(templateClone);
  currentBattleChoice.count++;
  currentBattleChoice.elements[pseudoOrVnum] = {
    container: templateClone,
    image: image,
    name: name,
  };

  updateBattleChoiceText(battleChoice, category, type);
}

function addBattleChoice(
  battleChoice,
  pseudoOrVnum,
  characterOrMonster,
  isMonster = false
) {
  addBattleElement(
    battleChoice,
    pseudoOrVnum,
    characterOrMonster,
    "attacker",
    isMonster
  );
  addBattleElement(
    battleChoice,
    pseudoOrVnum,
    characterOrMonster,
    "victim",
    isMonster
  );
}

function updateBattleChoiceImage(battleChoice, characterName, newRace) {
  var imageSrc = battleChoice.raceToImage[newRace];

  battleChoice.categories.forEach(function (category) {
    handleImageFromWiki(
      battleChoice[category].character.elements[characterName].image,
      imageSrc
    );

    var selected = battleChoice[category].selected;

    if (isCharacterSelected(characterName, selected)) {
      updateBattleChoiceButton(battleChoice, category, selected);
    }
  });
}

function updateBattleChoiceText(battleChoice, category, type) {
  var { count, container } = battleChoice[category][type];
  var parentContainer = container.parentElement;

  if (count === 0) {
    hideElement(parentContainer);
    showElement(parentContainer.nextElementSibling);
  } else if (count === 1) {
    showElement(parentContainer);
    hideElement(parentContainer.nextElementSibling);
  }
}

function updateBattleChoice(characters, battleChoice) {
  var templateImage = battleChoice.template.querySelector("img");
  var attackerImage = battleChoice.attacker.buttonContent.querySelector("img");
  var victimImage = battleChoice.victim.buttonContent.querySelector("img");

  resetImageFromWiki(templateImage);
  resetImageFromWiki(attackerImage);
  resetImageFromWiki(victimImage);

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

  for (var [vnum, monster] of Object.entries(characters.savedMonsters)) {
    addBattleChoice(battleChoice, vnum, monster, true);
  }
}

function updateBattleChoiceButton(battleChoice, category, data) {
  var battleChoiceCategory = battleChoice[category];
  var { defaultButtonContent, buttonContent } = battleChoiceCategory;
  var [buttonImage, buttonSpan] = buttonContent.firstElementChild.children;
  var { type, nameOrVnum } = parseTypeAndName(data);
  var { name, image } = battleChoiceCategory[type].elements[nameOrVnum];

  hideElement(defaultButtonContent);
  showElement(buttonContent);

  buttonSpan.textContent = name;
  buttonImage.src = image.src;
  battleChoiceCategory.selected = data;
}

function resetBattleChoiceButton(battleChoice, category) {
  var { defaultButtonContent, buttonContent } = battleChoice[category];

  showElement(defaultButtonContent);
  hideElement(buttonContent);

  if (category === "attacker") {
    filterAttackTypeSelectionMonster(battleChoice.attackType);
  }
}

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 isChecked(character.isPolymorph);
}

function isRiding(character) {
  return isChecked(character.isRiding);
}

function isBow(weaponType) {
  return weaponType === 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 weaponGrowth = 0;

  if (isPC(attacker)) {
    weaponGrowth = attacker.weapon.growth;
    leadership = attacker.leadership;
  }

  return 2 * (attacker.level + weaponGrowth) + 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, isPlayerVsPlayer) {
  var attackValueOther = 0;

  var minAttackValue = 0;
  var maxAttackValue = 0;

  var minWeaponAttackValue = 0;
  var maxWeaponAttackValue = 0;

  var minAttackValueSlash = 0;
  var maxAttackValueSlash = 0;

  if (isPC(attacker)) {
    var { type, isSerpent, minAttackValue, maxAttackValue, growth } =
      attacker.weapon;

    if (isSerpent) {
      minAttackValue = Math.max(0, attacker.minAttackValueRandom - growth);
      maxAttackValue = Math.max(
        minAttackValue,
        attacker.maxAttackValueRandom - growth
      );
    }

    minWeaponAttackValue = minAttackValue + growth;
    maxWeaponAttackValue = maxAttackValue + growth;

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

    attackValueOther += attacker.attackValue;

    if (isBow(type) && !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),
    possibleDamageCount: maxAttackValue - minAttackValue + 1,
    weapon: {
      minAttackValue: minWeaponAttackValue,
      maxAttackValue: maxWeaponAttackValue,
      interval: weaponInterval,
    },
    isPlayerVsPlayer: isPlayerVsPlayer,
  };
}

function calcMagicAttackValue(attacker, isPlayerVsPlayer) {
  var minMagicAttackValueSlash = 0;
  var maxMagicAttackValueSlash = 0;

  var minWeaponAttackValue = 0;
  var maxWeaponAttackValue = 0;

  var {
    isSerpent,
    minMagicAttackValue,
    maxMagicAttackValue,
    minAttackValue,
    maxAttackValue,
    growth,
  } = attacker.weapon;

  if (isSerpent) {
    minMagicAttackValue = Math.max(0, attacker.minMagicAttackValueRandom);
    maxMagicAttackValue = Math.max(
      minMagicAttackValue,
      attacker.maxMagicAttackValueRandom
    );
    minAttackValue = Math.max(0, attacker.minAttackValueRandom - growth);
    maxAttackValue = Math.max(
      minAttackValue,
      attacker.maxAttackValueRandom - growth
    );
  } else {
    minMagicAttackValue += growth;
    maxMagicAttackValue += growth;
  }

  minWeaponAttackValue = minAttackValue + growth;
  maxWeaponAttackValue = maxAttackValue + growth;

  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),
    possibleDamageCount: maxMagicAttackValue - minMagicAttackValue + 1,
    weapon: {
      minAttackValue: minWeaponAttackValue,
      maxAttackValue: maxWeaponAttackValue,
      interval: maxWeaponAttackValue - minWeaponAttackValue + 1,
    },
    isPlayerVsPlayer: isPlayerVsPlayer,
  };
}

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(damage, bonusValues) {
  damage = Math.floor((damage * bonusValues.attackValueCoeff) / 100);

  damage += bonusValues.attackValueMarriage;

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

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

  var elementBonusCoeff = bonusValues.elementBonusCoeff;

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

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

  return damage;
}

function calcFinalDamage(
  damage,
  bonusValues,
  damageType,
  minPiercingDamage,
  tempDamage
) {
  if (damageType.isPiercingHit) {
    damage += bonusValues.defenseBoost + Math.min(0, minPiercingDamage);
    damage += Math.floor(
      (tempDamage * bonusValues.extraPiercingHitCoeff) / 1000
    );
  }

  damage = Math.floor((damage * bonusValues.averageDamageCoeff) / 100);
  damage = Math.floor(
    (damage * bonusValues.averageDamageResistanceCoeff) / 100
  );
  damage = Math.floor((damage * bonusValues.skillDamageCoeff) / 100);
  damage = Math.floor((damage * bonusValues.skillDamageResistanceCoeff) / 100);
  damage = Math.floor((damage * bonusValues.rankBonusCoeff) / 100);

  if (bonusValues.useDarkProtection) {
    var { darkProtectionPoint, darkProtectionSp } = bonusValues;

    var damageReduction = Math.floor(damage / 3);
    var spAbsorption = Math.floor(
      (damageReduction * darkProtectionPoint) / 100
    );

    if (spAbsorption <= darkProtectionSp) {
      damage -= damageReduction;
    } else {
      damage -= Math.floor((darkProtectionSp * 100) / darkProtectionPoint);
    }
  }

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

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

  return damage;
}

function saveFinalDamage(
  damage,
  bonusValues,
  damageType,
  weapon,
  minPiercingDamage,
  damageWithPrimaryBonuses,
  damageWeighted,
  weight
) {
  damage = Math.floor(damage * bonusValues.magicResistanceCoeff);
  damage = Math.trunc((damage * bonusValues.weaponDefenseCoeff) / 100);
  damage = Math.floor((damage * bonusValues.tigerStrengthCoeff) / 100);
  damage = Math.floor((damage * bonusValues.berserkBonusCoeff) / 100);
  damage = Math.floor((damage * bonusValues.fearBonusCoeff) / 100);
  damage = Math.floor((damage * bonusValues.blessingBonusCoeff) / 100);

  const isCriticalHit = damageType.isCriticalHit;

  if (isCriticalHit && bonusValues.isPlayerVsPlayer) {
    for (
      let weaponAttackValue = weapon.minAttackValue;
      weaponAttackValue <= weapon.maxAttackValue;
      weaponAttackValue++
    ) {
      const criticalDamage = damage + 2 * weaponAttackValue;
      const finalDamage = calcFinalDamage(
        criticalDamage,
        bonusValues,
        damageType,
        minPiercingDamage,
        damageWithPrimaryBonuses
      );

      addKeyValue(damageWeighted, finalDamage, weight / weapon.interval);
    }
  } else {
    if (isCriticalHit) {
      damage *= 2;
    }

    damage = calcFinalDamage(
      damage,
      bonusValues,
      damageType,
      minPiercingDamage,
      damageWithPrimaryBonuses
    );

    addKeyValue(damageWeighted, damage, weight);
  }
}

function saveFinalSkillDamage(
  damage,
  bonusValues,
  damageType,
  weapon,
  minPiercingDamage,
  damageWeighted,
  weight,
  savedCriticalDamage
) {
  damage = Math.floor(damage * bonusValues.magicResistanceCoeff);
  damage = Math.trunc((damage * bonusValues.weaponDefenseCoeff) / 100);

  damage -= bonusValues.defense;

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

  const tempDamage = Math.floor(
    (damage * bonusValues.skillBonusByBonusCoeff) / 100
  );

  damage = Math.floor(
    (tempDamage * bonusValues.magicAttackValueCoeff) / 100 + 0.5
  );
  damage = Math.floor((damage * bonusValues.tigerStrengthCoeff) / 100);

  const isCriticalHit = damageType.isCriticalHit;

  if (isCriticalHit && bonusValues.isPlayerVsPlayer) {
    const { minAttackValue, maxAttackValue, interval } = weapon;
    const criticalWeight = weight / interval;

    for (
      let weaponAttackValue = minAttackValue;
      weaponAttackValue <= maxAttackValue;
      weaponAttackValue++
    ) {
      const criticalDamage = damage + 2 * weaponAttackValue;

      if (
        savedCriticalDamage &&
        savedCriticalDamage.hasOwnProperty(criticalDamage) &&
        minPiercingDamage >= 0
      ) {
        const savedDamage = savedCriticalDamage[criticalDamage];
        damageWeighted[savedDamage] += criticalWeight;
        continue;
      }

      const finalDamage = calcFinalDamage(
        criticalDamage,
        bonusValues,
        damageType,
        minPiercingDamage,
        tempDamage
      );

      addKeyValue(damageWeighted, finalDamage, criticalWeight);
      if (savedCriticalDamage) {
        savedCriticalDamage[criticalDamage] = finalDamage;
      }
    }
  } else {
    if (isCriticalHit) {
      damage *= 2;
    }

    damage = calcFinalDamage(
      damage,
      bonusValues,
      damageType,
      minPiercingDamage,
      tempDamage
    );

    addKeyValue(damageWeighted, damage, weight);

    return damage;
  }
}

function computePolymorphPoint(attacker, victim, polymorphPowerTable) {
  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, null, true);

    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);
    }
  }
}

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

  if (isPC(attacker) && isRiding(attacker) && !isPolymorph(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 (!isChecked(attacker.lowRank)) {
    return 0;
  }

  switch (attacker.rank) {
    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 calcCriticalHitChance(criticalHitPercentage) {
  if (criticalHitPercentage <= 9) {
    return Math.floor((criticalHitPercentage + 5) / 5);
  }
  return Math.floor((criticalHitPercentage + 5) / 6);
}

function calcCriticalSkillChance(criticalHitPercentage) {
  if (criticalHitPercentage === 0) {
    return 0;
  } else if (criticalHitPercentage <= 9) {
    return Math.floor((criticalHitPercentage + 7) / 3);
  }
  return Math.floor((criticalHitPercentage + 5) / 3);
}

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

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

function createBattleValues(attacker, victim, battle, skillType) {
  var {
    mapping,
    constants: { polymorphPowerTable, skillPowerTable, marriageTable },
  } = 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 useDamage = 1;
  var defense = victim.defense;
  var defenseBoost = defense;
  var magicResistance = 0;
  var weaponDefense = 0;
  var tigerStrength = 0;
  var berserkBonus = 0;
  var blessingBonus = 0;
  var fearBonus = 0;
  var magicAttackValueMeleeMagic = 0;
  var criticalHitPercentage = attacker.criticalHit;
  var isPlayerVsPlayer = false;
  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 useDarkProtection = false;
  var darkProtectionPoint = 0;
  var defensePercent = 0;
  var damageBonus = 0;
  var empireMalus = 0;
  var sungMaStrBonus = 0;
  var sungmaStrMalus = 0;
  var whiteDragonElixir = 0;
  var steelDragonElixir = 0;

  attacker.statAttackValue = calcStatAttackValue(attacker);

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

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

    attacker.weapon.getValues(attacker.weaponUpgrade);

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

    var weaponType = attacker.weapon.type;

    if (skillType && attacker.class === "archery") {
      if (weaponType !== 2) {
        useDamage = 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)) {
      isPlayerVsPlayer = true;

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

        criticalHitPercentage = calcCriticalHitChance(criticalHitPercentage);
        berserkBonus = calcBerserkBonus(skillPowerTable, victim);
        blessingBonus = calcBlessingBonus(skillPowerTable, victim);
        fearBonus = calcFearBonus(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];
      }
    } else {
      if (isChecked(attacker.isMarried)) {
        if (isChecked(attacker.loveNecklace)) {
          attackValueMarriage = getMarriageBonusValue(
            attacker,
            marriageTable,
            "loveNecklace"
          );
        }

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

        if (isChecked(attacker.harmonyEarrings)) {
          piercingHitPercentage += getMarriageBonusValue(
            attacker,
            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,
            marriageTable,
            "harmonyBracelet"
          );
        }

        if (isChecked(victim.harmonyNecklace) && !skillType) {
          defenseMarriage = getMarriageBonusValue(
            victim,
            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;
          berserkBonus = calcBerserkBonus(skillPowerTable, victim);
          blessingBonus = calcBlessingBonus(skillPowerTable, victim);
          fearBonus = calcFearBonus(skillPowerTable, victim);
        } else if (isRangeAttacker(attacker)) {
          missPercentage = victim.arrowBlock;
          weaponDefense = victim.arrowDefense;
          averageDamageResistance = victim.averageDamageResistance;
          berserkBonus = calcBerserkBonus(skillPowerTable, victim);
          blessingBonus = calcBlessingBonus(skillPowerTable, victim);
          fearBonus = calcFearBonus(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 = calcCriticalSkillChance(criticalHitPercentage);
    piercingHitPercentage = calcPiercingSkillChance(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 (
      victim.useDarkProtection &&
      victim.class === "black_magic" &&
      victim.skillDarkProtection
    ) {
      useDarkProtection = true;
      darkProtectionPoint = calcDarkProtectionPoint(skillPowerTable, victim);
    }

    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)) {
      typeBonus = 0;
      raceBonus = 0;
      raceResistance = 0;
      stoneBonus = 0;
      monsterBonus = 0;
      for (var index = 0; index < elementBonus.length; index++) {
        elementBonus[index] = 0;
      }
    } else {
      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,
    useDamage: useDamage,
    defense: defense,
    defenseBoost: defenseBoost,
    defenseMarriage: defenseMarriage,
    tigerStrengthCoeff: 100 + tigerStrength,
    magicResistanceCoeff: magicResistanceToCoeff(magicResistance),
    weaponDefenseCoeff: 100 - weaponDefense,
    berserkBonusCoeff: 100 + berserkBonus,
    blessingBonusCoeff: 100 - blessingBonus,
    fearBonusCoeff: 100 - fearBonus,
    magicAttackValueCoeff: 100 + magicAttackValueMeleeMagic,
    isPlayerVsPlayer: isPlayerVsPlayer,
    extraPiercingHitCoeff: 5 * extraPiercingHitPercentage,
    averageDamageCoeff: 100 + averageDamage,
    averageDamageResistanceCoeff: 100 - Math.min(99, averageDamageResistance),
    skillDamageCoeff: 100 + skillDamage,
    skillDamageResistanceCoeff: 100 - Math.min(99, skillDamageResistance),
    useDarkProtection: useDarkProtection,
    darkProtectionPoint: darkProtectionPoint,
    darkProtectionSp: victim.darkProtectionSp,
    rankBonusCoeff: 100 + rankBonus,
    defensePercent: Math.floor(defensePercent),
    damageBonusCoeff: Math.min(20, 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 damageTypeCombinaison = [
    {
      isCriticalHit: false,
      isPiercingHit: false,
      weight:
        (100 - criticalHitPercentage) *
        (100 - piercingHitPercentage) *
        (100 - missPercentage),
      name: "normalHit",
    },
    {
      isCriticalHit: true,
      isPiercingHit: false,
      weight:
        criticalHitPercentage *
        (100 - piercingHitPercentage) *
        (100 - missPercentage),
      name: "criticalHit",
    },
    {
      isCriticalHit: false,
      isPiercingHit: true,
      weight:
        (100 - criticalHitPercentage) *
        piercingHitPercentage *
        (100 - missPercentage),
      name: "piercingHit",
    },
    {
      isCriticalHit: true,
      isPiercingHit: true,
      weight:
        criticalHitPercentage * piercingHitPercentage * (100 - missPercentage),
      name: "criticalPiercingHit",
    },
  ];

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

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.possibleDamageCount *= 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 calcBerserkBonus(skillPowerTable, victim) {
  if (!isChecked(victim.useBerserk) || victim.class !== "body") {
    return 0;
  }

  var skillPower = getSkillPower(victim.skillBerserk, skillPowerTable);

  if (!skillPower) {
    return 0;
  }

  var berserkBonus = Math.floor(skillPower * 25);

  return berserkBonus;
}

function calcBlessingBonus(skillPowerTable, victim) {
  if (!isChecked(victim.isBlessed)) {
    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(victim.blessingOnself)) {
    blessingBonus = floorMultiplication(blessingBonus, 1.1);
  }

  return blessingBonus;
}

function calcFearBonus(skillPowerTable, victim) {
  if (!isChecked(victim.useFear) || victim.class !== "weaponary") {
    return 0;
  }

  var skillPower = getSkillPower(victim.skillFear, skillPowerTable);

  if (!skillPower) {
    return 0;
  }

  var fearBonus = 5 + Math.floor(skillPower * 20);

  return fearBonus;
}

function calcDarkProtectionPoint(skillPowerTable, victim) {
  var skillPower = getSkillPower(victim.skillDarkProtection, skillPowerTable);

  return floorMultiplication(100 - victim.int * 0.84 * skillPower, 1);
}

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 calcPhysicalDamage(battleValues) {
  var {
    attackFactor,
    mainAttackValue,
    attackValues: {
      minAttackValue,
      maxAttackValue,
      attackValueOther,
      weights,
      weapon,
    },
    bonusValues,
    damageTypeCombinaison,
  } = battleValues;

  var damageWeightedByType = {};

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

  for (var damageType of damageTypeCombinaison) {
    if (!damageType.weight) {
      continue;
    }

    var damageWeighted = {};
    damageWeightedByType[damageType.name] = damageWeighted;

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

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

      var damageWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
        rawDamage,
        bonusValues
      );

      var minPiercingDamage =
        damageWithPrimaryBonuses -
        bonusValues.defenseBoost -
        bonusValues.defenseMarriage;

      if (minPiercingDamage <= 2) {
        for (var damage = 1; damage <= 5; damage++) {
          saveFinalDamage(
            damage,
            bonusValues,
            damageType,
            weapon,
            minPiercingDamage,
            damageWithPrimaryBonuses,
            damageWeighted,
            weight / 5
          );
        }
      } else {
        saveFinalDamage(
          minPiercingDamage,
          bonusValues,
          damageType,
          weapon,
          minPiercingDamage,
          damageWithPrimaryBonuses,
          damageWeighted,
          weight
        );
      }
    }
  }

  return damageWeightedByType;
}

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

  var damageWeightedByType = {};

  for (var damageType of damageTypeCombinaison) {
    if (!damageType.weight) {
      continue;
    }

    var damageWeighted = {};
    var savedDamage = {};
    var savedCriticalDamage = {};

    damageWeightedByType[damageType.name] = damageWeighted;

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

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

      var damageWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
        rawDamage,
        bonusValues
      );

      for (
        var variation = minVariation;
        variation <= maxVariation;
        variation++
      ) {
        if (damageWithPrimaryBonuses <= 2) {
          for (var damage = 1; damage <= 5; damage++) {
            var damageWithFormula = skillFormula(
              damage * bonusValues.useDamage,
              variation
            );

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

            saveFinalSkillDamage(
              damageWithFormula,
              bonusValues,
              damageType,
              weapon,
              damageWithPrimaryBonuses,
              damageWeighted,
              weight
            );
          }
        } else {
          var damageWithFormula = skillFormula(
            damageWithPrimaryBonuses * bonusValues.useDamage,
            variation
          );

          if (savedDamage.hasOwnProperty(damageWithFormula)) {
            var finalDamage = savedDamage[damageWithFormula];
            damageWeighted[finalDamage] += weight;
            continue;
          }

          var finalDamage = Math.floor(
            (damageWithFormula * bonusValues.weaponBonusCoeff) / 100
          );

          finalDamage = saveFinalSkillDamage(
            finalDamage,
            bonusValues,
            damageType,
            weapon,
            damageWithPrimaryBonuses,
            damageWeighted,
            weight,
            savedCriticalDamage
          );

          if (finalDamage) {
            savedDamage[damageWithFormula] = finalDamage;
          }
        }
      }
    }
  }

  return damageWeightedByType;
}

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

  var damageWeightedByType = {};

  for (var damageType of damageTypeCombinaison) {
    if (!damageType.weight) {
      continue;
    }

    var damageWeighted = {};
    var savedDamage = {};
    var savedCriticalDamage = {};

    damageWeightedByType[damageType.name] = damageWeighted;

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

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

        if (savedDamage.hasOwnProperty(rawDamage)) {
          var finalDamage = savedDamage[rawDamage];
          damageWeighted[finalDamage] += weight;
          continue;
        }

        var damageWithPrimaryBonuses = Math.floor(
          (rawDamage * bonusValues.weaponBonusCoeff) / 100
        );

        damageWithPrimaryBonuses = calcDamageWithPrimaryBonuses(
          damageWithPrimaryBonuses,
          bonusValues
        );

        if (damageWithPrimaryBonuses <= 2) {
          for (var damage = 1; damage <= 5; damage++) {
            saveFinalSkillDamage(
              damage,
              bonusValues,
              damageType,
              weapon,
              damageWithPrimaryBonuses,
              damageWeighted,
              weight
            );
          }
        } else {
          var finalDamage = saveFinalSkillDamage(
            damageWithPrimaryBonuses,
            bonusValues,
            damageType,
            weapon,
            damageWithPrimaryBonuses,
            damageWeighted,
            weight,
            savedCriticalDamage
          );

          if (finalDamage) {
            savedDamage[rawDamage] = finalDamage;
          }
        }
      }
    }
  }

  return damageWeightedByType;
}

function calcDamage(
  attacker,
  victim,
  attackType,
  battle,
  removeSkillVariation
) {
  var damageCalculator, skillId, skillType;

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

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

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

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

  return {
    damageWeightedByType: damageCalculator(battleValues),
    attackValues: battleValues.attackValues,
    skillType: skillType,
  };
}

function damageWithoutVariation(
  attacker,
  victim,
  attackType,
  battle,
  characters
) {
  var startDamageTime = performance.now();

  var { damageWeightedByType, attackValues, skillType } = calcDamage(
    attacker,
    victim,
    attackType,
    battle
  );

  var endDamageTime = performance.now();

  displayResults(
    attackValues,
    damageWeightedByType,
    battle,
    attacker.name,
    victim.name
  );

  var endDisplayTime = performance.now();

  displayFightInfo(
    attackValues.possibleDamageCount,
    endDamageTime - startDamageTime,
    endDisplayTime - endDamageTime,
    battle
  );
  addPotentialErrorInformation(
    battle.errorInformation,
    attacker,
    victim,
    skillType,
    characters
  );

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

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

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

    var { damageWeightedByType, totalCardinal } = calcDamage(
      copyObject(attacker),
      copyObject(victim),
      attackType,
      battle,
      true
    );

    var meanDamage = calcMeanDamage(damageWeightedByType, totalCardinal);

    if (bonusValue === minVariation) {
      firstDamage = Math.max(meanDamage, 1e-3);
    }

    damageByBonus.push({ x: bonusValue, y: meanDamage });
    augmentationByBonus.push({
      x: bonusValue,
      y: meanDamage / firstDamage - 1,
    });
    simulationCount++;
  }

  var endTime = performance.now();

  battle.damageByBonus = damageByBonus.concat(entityVariation);

  addToBonusVariationChart(
    damageByBonus,
    augmentationByBonus,
    entity.bonusVariationName,
    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.fightResultTitle);
  showElement(battle.bonusVariationResultContainer);

  if (
    isChecked(attacker.useBonusVariation) &&
    isChecked(victim.useBonusVariation)
  ) {
    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;
  }

  // Alastor
  if (monster.vnum === 6790) {
    monster.iceResistance = 0;
    monster.iceBonus = 0;
    monster.lightningResistance = -10;
    monster.lightningBonus = 65;
  }
}

function createWeapon(vnum) {
  var weapon = copyObject(weaponData[vnum]);
  var serpentVnums = [360, 380, 1210, 2230, 3250, 5200, 6150, 7330];
  var weaponValues = weapon[1];
  var isSerpent = isValueInArray(Number(vnum), serpentVnums);
  var isSpecial = Array.isArray(weaponValues[0]);
  var isMagic;

  if (isSpecial) {
    isMagic = weaponValues[0][1] > 0;
  } else {
    isMagic = weaponValues[1] > 0;
  }

  return {
    type: weapon[0],
    maxUpgrade: weapon[2].length - 1,
    isSerpent: isSerpent,
    isMagic: isMagic,
    getValues: function (upgrade) {
      var currentWeaponValues = weaponValues;

      if (upgrade === undefined) {
        // rare bug when weaponUpgrade is deleted
        console.warn("WeaponUpgrade is missing.");
        upgrade = this.maxUpgrade;
      }
      upgrade = Math.min(upgrade, this.maxUpgrade);

      if (isSpecial) {
        currentWeaponValues = weaponValues[upgrade];
      }

      this.minAttackValue = currentWeaponValues[2];
      this.maxAttackValue = currentWeaponValues[3];
      this.minMagicAttackValue = currentWeaponValues[0];
      this.maxMagicAttackValue = currentWeaponValues[1];
      this.growth = weapon[2][upgrade];
    },
  };
}

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

  var monster = {
    vnum: Number(monsterVnum),
    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);
  // }
  if (!polymorphMonster) {
    changeMonsterValues(monster);
  }

  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"]);
    }

    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"]);
    }
    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 () {
    reduceChartPoints.disabled = true;

    var startDisplayTime = performance.now();
    var scatterDataByType = battle.scatterDataByType;
    var {
      chart,
      maxPoints,
      chart: {
        data: { datasets },
      },
    } = battle.damageChart;
    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 = aggregateDamage(scatterData, maxPoints);
        addAnimations = true;
      } else {
        dataset.data = scatterData;
      }
    }

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

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

    setTimeout(function () {
      reduceChartPoints.disabled = false;
    }, 1000);
  });

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

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

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

    for (var damageType in damageWeightedByType) {
      var damageWeighted = damageWeightedByType[damageType];

      for (var damage in damageWeighted) {
        csvContent +=
          damage + "," + damageWeighted[damage] + "," + damageType + "\n";
      }
    }

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

  downLoadRawDataVariation.addEventListener("click", function () {
    var damageByBonus = battle.damageByBonus;
    var damageByBonusLength = damageByBonus.length;
    var filename = "damage_variation.csv";

    if (!damageByBonusLength) {
      return;
    }

    var csvContent =
      damageByBonus[damageByBonusLength - 1] + ",averageDamage\n";

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

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

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

function displayResults(
  attackValues,
  damageWeightedByType,
  battle,
  attackerName,
  victimName
) {
  var [meanDamage, minDamage, maxDamage, scatterDataByType, uniqueDamageCount] =
    prepareDamageData(damageWeightedByType, attackValues);

  addToDamageChart(
    scatterDataByType,
    battle.damageChart,
    battle.reduceChartPoints.checked
  );
  updateDamageChartDescription(
    battle.uniqueDamageCounters,
    uniqueDamageCount,
    battle.numberFormats.default
  );
  displayFightResults(
    battle,
    attackerName,
    victimName,
    meanDamage,
    minDamage,
    maxDamage
  );
  battle.damageWeightedByType = damageWeightedByType;
  battle.scatterDataByType = scatterDataByType;
}

function displayFightResults(
  battle,
  attackerName,
  victimName,
  meanDamage,
  minDamage,
  maxDamage
) {
  var {
    tableResultFight,
    tableResultHistory,
    savedFights,
    numberFormats: { default: numberFormat },
    deleteFightTemplate,
  } = battle;

  hideElement(tableResultHistory.rows[1]);

  var valuesToDisplay = [
    attackerName,
    victimName,
    battle.battleChoice.attackType.selectedText,
    meanDamage,
    minDamage,
    maxDamage,
  ];

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

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

function displayFightInfo(
  possibleDamageCount,
  damageTimeDuration,
  displayTimeDuration,
  battle
) {
  var container = battle.possibleDamageCounter.parentElement;

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

  var { numberFormats, possibleDamageCounter, damageTime, displayTime } =
    battle;

  possibleDamageCount = numberFormats.default.format(possibleDamageCount);
  damageTimeDuration = numberFormats.second.format(damageTimeDuration / 1000);
  displayTimeDuration = numberFormats.second.format(displayTimeDuration / 1000);

  possibleDamageCounter.textContent = possibleDamageCount;
  damageTime.textContent = damageTimeDuration;
  displayTime.textContent = displayTimeDuration;
}

function parseTypeAndName(data) {
  var [type, nameOrVnum] = splitFirst(data, "-");

  return {
    type: type,
    nameOrVnum: nameOrVnum,
    isCharacter: type === "character",
  };
}

function isCharacterSelected(characterName, selected) {
  if (!selected) {
    return false;
  }

  var { nameOrVnum, isCharacter } = parseTypeAndName(selected);

  return nameOrVnum === characterName && isCharacter;
}

function useBonusVariationMode(character, variation) {
  return (
    isChecked(character.useBonusVariation) &&
    character.hasOwnProperty(variation) &&
    character.bonusVariationMinValue < character.bonusVariationMaxValue
  );
}

function createBattle(characters, battle) {
  var battleChoice = battle.battleChoice;
  var battleForm = battleChoice.form;
  var lastInvalidTime = 0;

  battleForm.addEventListener("change", handleBattleFormChange);
  battleForm.addEventListener("invalid", handleBattleFormInvalid, true);
  battleForm.addEventListener("submit", handleBattleFormSubmit);

  function handleBattleFormChange(event) {
    var target = event.target;
    var { name: targetName, value: targetValue, type: targetType } = target;

    if (targetType !== "radio") {
      return;
    }

    if (targetName === "attackType") {
      battleChoice.attackType.selectedText =
        target.previousElementSibling.dataset.o;
    } else {
      updateBattleChoiceButton(battleChoice, targetName, targetValue);

      if (targetName === "attacker") {
        filterAttackTypeSelection(characters, battleChoice, targetValue);
      }
    }
  }

  function handleBattleFormInvalid(event) {
    var currentTime = Date.now();

    if (currentTime - lastInvalidTime < 100) {
      return;
    }

    lastInvalidTime = currentTime;

    var target = event.target;
    var modal = target.closest(".modal");

    if (!modal) {
      return;
    }

    var dataModal = modal.dataset.modal;

    if (!dataModal) {
      return;
    }

    var category = dataModal.split("-")[0];

    if (isValueInArray(category, battleChoice.categories)) {
      battleChoice[category].button.click();
    }
  }

  function handleBattleFormSubmit(event) {
    event.preventDefault();

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

    var battleInfo = new FormData(event.target);
    var attackerData = battleInfo.get("attacker");
    var attackType = battleInfo.get("attackType");
    var victimData = battleInfo.get("victim");
    var attackerVariation;
    var victimVariation;

    if (!attackerData && !attackType && !victimData) {
      return;
    }

    var { nameOrVnum: attackerNameOrVnum, isCharacter: attackerIsPlayer } =
      parseTypeAndName(attackerData);
    var { nameOrVnum: victimNameOrVnum, isCharacter: victimIsPlayer } =
      parseTypeAndName(victimData);

    if (attackerIsPlayer) {
      var attacker = copyObject(characters.savedCharacters[attackerNameOrVnum]);
      attackerVariation = attacker.bonusVariation;
    } else {
      var attacker = createMonster(attackerNameOrVnum);
    }

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

    if (useBonusVariationMode(attacker, attackerVariation)) {
      damageWithVariation(
        attacker,
        victim,
        attackType,
        battle,
        attacker,
        attackerVariation
      );
    } else if (useBonusVariationMode(victim, victimVariation)) {
      damageWithVariation(
        attacker,
        victim,
        attackType,
        battle,
        victim,
        victimVariation
      );
    } else {
      damageWithoutVariation(attacker, victim, attackType, battle, characters);
    }
  }
}

function createMapping() {
  var 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: {
        damage: "Dégâts",
        percentage: "Pourcentage",
        miss: "Miss",
        normalHit: "Coup classique",
        criticalHit: "Coup critique",
        piercingHit: "Coup perçant",
        criticalPiercingHit: "Coup critique perçant",
        damageRepartition: "Distribution des dégâts",
        averageDamage: "Dégâts moyens",
        damageAugmentation: "Augmentation des dégâts",
        bonusVariationTitle: [
          "Évolution des dégâts moyens",
          "par rapport à la valeur d'un bonus",
        ],
      },
      en: {
        damage: "Damage",
        percentage: "Percentage",
        miss: "Miss",
        normalHit: "Normal Hit",
        criticalHit: "Critical Hit",
        piercingHit: "Piercing Hit",
        criticalPiercingHit: "Critical Piercing Hit",
        damageRepartition: "Damage Repartition",
        averageDamage: "Average Damage",
        damageAugmentation: "Damage Augmentation",
        bonusVariationTitle: [
          "Evolution of Average Damage",
          "Relative to a Bonus Value",
        ],
      },
      tr: {
        damage: "Hasar",
        percentage: "Yüzde",
        miss: "Miss Vuruş",
        normalHit: "Düz Vuruş",
        criticalHit: "Kritik Vuruş",
        piercingHit: "Delici Vuruş",
        criticalPiercingHit: "Kritikli Delici Vuruş",
        damageRepartition: "Hasar Dağılımı",
        averageDamage: "Ortalama Hasar",
        damageAugmentation: "Ortalama Hasar Artışı",
        bonusVariationTitle: [
          "Bir bonusun değerine kıyasla",
          "Ortalama Hasar Çizelgesi",
        ],
      },
      ro: {
        damage: "Daune",
        percentage: "Procent",
        miss: "Miss",
        normalHit: "Lovitura normala",
        criticalHit: "Lovitura critica",
        piercingHit: "Lovitura patrunzatoare",
        criticalPiercingHit: "Lovitura critica si patrunzatoare",
        damageRepartition: "Distribuția daunelor",
        averageDamage: "Media damageului",
        damageAugmentation: "Damage imbunatatit",
        bonusVariationTitle: [
          "Evolutia mediei damageului",
          "relativ la o valoare bonus",
        ],
      },
      de: {
        damage: "Schäden",
        percentage: "Prozentsatz",
        miss: "Verfehlen",
        normalHit: "Normaler Treffer",
        criticalHit: "Kritischer Treffer",
        piercingHit: "Durchdringender Treffer",
        criticalPiercingHit: "Kritischer durchdringender Treffer",
        damageRepartition: "Schadensverteilung",
        averageDamage: "Durchschnittlicher Schaden",
        damageAugmentation: "Schadenserhöhung",
        bonusVariationTitle: [
          "Entwicklung des durchschnittlichen Schadens",
          "im Verhältnis zu einem Bonus",
        ],
      },
      pt: {
        damage: "Dano",
        percentage: "Percentagem",
        miss: "Miss",
        normalHit: "Dano normal",
        criticalHit: "Dano crítico",
        piercingHit: "Dano perfurante",
        criticalPiercingHit: "Dano crítico perfurante",
        damageRepartition: "Repartição de dano",
        averageDamage: "Dano médio",
        damageAugmentation: "Aumento de dano",
        bonusVariationTitle: ["Evolução do dano médio", "relativo a um bónus"],
      },
      // es: {
      //   damage: "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",
      //   damageRepartition: "Repartición de daños",
      //   averageDamage: "Daño medio",
      //   damageAugmentation: "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 initDamageChart(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.plotDamage.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.damageRepartition,
          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
              );
              var 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.damage,
            font: {
              size: 16,
            },
          },
          ticks: {
            callback: function (value) {
              return Number.isInteger(value) ? value : "";
            },
          },
        },
        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.damageChart = {
    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.averageDamage,
          backgroundColor: "rgba(75, 192, 192, 0.2)",
          borderColor: "rgba(75, 192, 192, 1)",
          fill: true,
        },
        {
          label: translation.damageAugmentation,
          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.averageDamage,
            font: {
              size: 16,
            },
          },
        },
      },
      elements: {
        point: {
          borderWidth: 1,
          radius: 3,
          hitRadius: 3,
          hoverRadius: 6,
          hoverBorderWidth: 2,
        },
      },
    },
  });

  battle.bonusVariationChart = chart;
}

function filterAttackTypeSelection(characters, battleChoice, targetValue) {
  var { nameOrVnum: attackerNameOrVnum, isCharacter: isAttackerPlayer } =
    parseTypeAndName(targetValue);

  if (isAttackerPlayer) {
    var attacker = characters.savedCharacters[attackerNameOrVnum];
    filterAttackTypeSelectionCharacter(attacker, battleChoice.attackType);
  } else {
    filterAttackTypeSelectionMonster(battleChoice.attackType);
  }
}

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 addBattleData(battle) {
  var errorElements = document.querySelectorAll("[data-error]");
  var { elements: attackTypeElements, container: attackTypeContainer } =
    battle.battleChoice.attackType;
  var attackTypeChilds = attackTypeContainer.children;

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

  for (var index = 1; index < attackTypeChilds.length - 1; index++) {
    var attackTypeChild = attackTypeChilds[index];
    var input = attackTypeChild.querySelector("input");

    attackTypeElements.push({
      container: attackTypeChild,
      input: input,
      inputClass: input.dataset.class,
      inputValue: input.value,
    });
  }
}

function createDamageCalculatorInformation(chartSource) {
  var characters = {
    unsavedChanges: false,
    savedCharacters: {},
    currentCharacter: null,
    savedMonsters: getSavedMonsters(),
    monsterElements: {},
    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"),
    monsterButtonTemplates: document.getElementById("monster-button-templates"),
    monsterTemplate: document.getElementById("new-monster-template")
      .children[0],
    monstersContainer: document.getElementById("monsters-container"),
    monsteriFrame: document.getElementById("monster-iframe"),
    stoneiFrame: document.getElementById("stone-iframe"),
    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"
    ),
    toggleSiblings: {},
    polymorphDisplay: document.getElementById("polymorph-display"),
    bonusVariation: {
      tabButton: document.getElementById("Variation"),
      checkbox: document.getElementById("use-bonus-variation"),
      input: document.getElementById("bonus-variation"),
      inputName: document.getElementById("bonus-variation-name"),
      container: document.getElementById("bonus-variation-creation"),
      minValue: document.getElementById("bonus-variation-min-value"),
      maxValue: document.getElementById("bonus-variation-max-value"),
      disabledText: document.getElementById("bonus-variation-disabled"),
      selectedText: document.getElementById("bonus-variation-selected"),
      displaySpan: document.getElementById("bonus-variation-display"),
    },
    skillElementsToFilter: document.querySelectorAll(
      "#skill-container [data-class]"
    ),
  };

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

  document.querySelectorAll(".toggle-sibling").forEach(function (element) {
    var target = element.dataset.target;
    var sibling = document.getElementById(target);
    characters.toggleSiblings[element.name] = sibling;
  });

  var constants = createConstants();

  var battle = {
    savedFights: getSavedFights(),
    battleChoice: {
      resetAttackType: false,
      form: document.getElementById("create-battle"),
      template: document.getElementById("battle-selection-template")
        .children[0],
      raceToImage: {
        warrior: "/images/0/0f/Bandeaurougehomme.png",
        ninja: "/images/0/0e/Queuedechevalclair.png",
        sura: "/images/3/37/Couperespectablerouge.png",
        shaman: "/images/6/6a/Coupeeleganteclairfemme.png",
        lycan: "/images/4/4e/Protectionfrontalerouge.png",
      },
      categories: ["attacker", "victim"],
      attacker: {
        character: {
          count: 0,
          container: document.getElementById("attacker-selection-characters"),
          elements: {},
        },
        monster: {
          count: 0,
          container: document.getElementById("attacker-selection-monsters"),
          elements: {},
        },
        button: document.getElementById("attacker-trigger"),
        defaultButtonContent: document.getElementById(
          "attacker-default-button-content"
        ),
        buttonContent: document.getElementById("attacker-button-content"),
        container: document.getElementById("attacker-selection"),
        selected: null,
      },
      victim: {
        character: {
          count: 0,
          container: document.getElementById("victim-selection-characters"),
          elements: {},
        },
        monster: {
          count: 0,
          container: document.getElementById("victim-selection-monsters"),
          elements: {},
        },
        stone: {
          count: 0,
          container: document.getElementById("victim-selection-stones"),
          elements: {},
        },
        button: document.getElementById("victim-trigger"),
        defaultButtonContent: document.getElementById(
          "victim-default-button-content"
        ),
        buttonContent: document.getElementById("victim-button-content"),
        selected: null,
      },
      attackType: {
        container: document.getElementById("attack-type-selection"),
        elements: [],
        defaultInput: document.getElementById("physical-attack"),
        selectedText: "",
      },
    },
    damageWeightedByType: {},
    scatterDataByType: {},
    damageByBonus: [],
    tableResultFight: document.getElementById("result-table-fight"),
    tableResultHistory: document.getElementById("result-table-history"),
    deleteFightTemplate: document.getElementById("delete-fight-template")
      .children[0],
    errorInformation: {},
    fightResultTitle: document.getElementById("fight-result-title"),
    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"),
    plotDamage: document.getElementById("plot-damage"),
    plotBonusVariation: document.getElementById("plot-bonus-variation"),
    uniqueDamageCounters: document.querySelectorAll(".unique-damage-counter"),
    possibleDamageCounter: document.getElementById("possible-damage-counter"),
    damageTime: document.getElementById("damage-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: createMapping(),
    constants: constants,
    translation: getTranslation(constants.translation),
  };

  addBattleData(battle);
  initResultTableHistory(battle);
  addScript(chartSource, function () {
    initDamageChart(battle);
    initBonusVariationChart(battle);
  });
  reduceChartPointsListener(battle);
  downloadRawDataListener(battle);

  return [characters, battle];
}

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

  document.head.appendChild(link);
}

(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.battleChoice);
    createBattle(characters, battle);
  }
  addScript(javascriptSource, main);
})();