MediaWiki:DamageCalculator.js
Appearance
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
// Wiki form
const fieldLabels = {
'level': 'Level',
'strength': 'Strength',
'offenseSkill': 'Offense Skill',
'doubleAttackSkill': 'Double Attack Skill',
'dualWieldSkill': 'Dual Wield Skill',
'hastePercentage': 'Haste Percentage',
'itemAndSpellATKTotal': 'Item and Spell ATK Total',
'mainHandWeaponSkill': 'Main Hand Weapon Skill',
'mainHandWeaponDamage': 'Main Hand Weapon Damage',
'mainHandWeaponDelay': 'Main Hand Weapon Delay',
'mainHandDamageBonus': 'Main Hand Damage Bonus',
'offHandWeaponSkill': 'Off Hand Weapon Skill',
'offHandWeaponDamage': 'Off Hand Weapon Damage',
'offHandWeaponDelay': 'Off Hand Weapon Delay',
}
const fieldDefaults = {
'level': 52,
'strength': 144,
'offenseSkill': 236,
'doubleAttackSkill': 200,
'dualWieldSkill': 252,
'hastePercentage': 34,
'itemAndSpellATKTotal': 0,
'mainHandWeaponSkill': 236,
'mainHandWeaponDamage': 9,
'mainHandWeaponDelay': 16,
'mainHandDamageBonus': 9,
'offHandWeaponSkill': 236,
'offHandWeaponDamage': 17,
'offHandWeaponDelay': 28
}
const fields = Object.keys(fieldLabels);
const fieldsHtml = fields.map(field =>
`<label>${fieldLabels[field]}: <input name="${field}" placeholder="${fieldDefaults[field]}"/><label>`
).join('<br/>')
const mobFieldLabels = {
agility: 'Mob Agility',
defenseSkill: 'Mob Defense Skill',
totalAC: 'Mob Total AC',
riposteSkill: 'Mob Riposte Skill',
parrySkill: 'Mob Parry Skill',
dodgeSkill: 'Mob Dodge Skill'
};
const mobFieldDefaults = {
agility: 150,
defenseSkill: 200,
totalAC: 115,
riposteSkill: 25,
parrySkill: 25,
dodgeSkill: 25
};
const mobFields = Object.keys(mobFieldLabels);
const mobFieldsHtml = mobFields.map(field =>
`<label>${mobFieldLabels[field]}: <input name="${field}" placeholder="${mobFieldDefaults[field]}"/><label>`
).join('<br/>')
var playerStats = createStatStruct();
//playerStats.strength = 144;
//playerStats.offenseSkill = 236;
//playerStats.doubleAttackSkill = 200;
//playerStats.dualWieldSkill = 252;
//playerStats.hastePercentage = 34;
//playerStats.itemAndSpellATKTotal = 0;
//playerStats.mainHandWeaponSkill = 236;
//playerStats.mainHandWeaponDamage = 9;
//playerStats.mainHandWeaponDelay = 16;
//playerStats.mainHandDamageBonus = 9;
//playerStats.offHandWeaponSkill = 236;
//playerStats.offHandWeaponDamage = 17;
//playerStats.offHandWeaponDelay = 28;
//================================
// Edit these variables
//================================
playerStats.strength = 144;
playerStats.offenseSkill = 236;
playerStats.doubleAttackSkill = 200;
playerStats.dualWieldSkill = 252;
playerStats.hastePercentage = 34;
playerStats.itemAndSpellATKTotal = 0;
playerStats.mainHandWeaponSkill = 236;
playerStats.mainHandWeaponDamage = 9;
playerStats.mainHandWeaponDelay = 16;
playerStats.mainHandDamageBonus = 9;
playerStats.offHandWeaponSkill = 236;
playerStats.offHandWeaponDamage = 17;
playerStats.offHandWeaponDelay = 28;
var mobStats = createStatStruct();
mobStats.agility = 150; // AGI doesn't have a large effect, except at low levels. Setting it somewhere above 75 is fine.
mobStats.defenseSkill = 200; // Rule of thumb is probably 5 + (5 * level), just like player melee classes.
mobStats.totalAC = 115; // Level 50 mobs supposedly have around 115 AC according to EQEMU comments. Assume lower levels have lower AC. Raid targets probably have a decent amount more.
mobStats.riposteSkill = 25; // For now rule of thumb is Level / 2. Based on P99 parsing riposte amount doesn't match skill level of 5 + (5 * level), or magic numbers are off in EQEMU code.
mobStats.parrySkill = 25; // For now rule of thumb is Level / 2. Based on P99 parsing parry amount doesn't match skill level of 5 + (5 * level), or magic numbers are off in EQEMU code.
mobStats.dodgeSkill = 25; // For now rule of thumb is Level / 2. Based on P99 parsing dodge amount doesn't match skill level of 5 + (5 * level), or magic numbers are off in EQEMU code.
//===============================
// Do not edit, this is the code
//===============================
//--------------------
// GENERAL CODE NOTES:
//--------------------
// varuables with prefix mn_ are magic numbers ripped directly from EQEMU code.
// Math.random() generates a value between 0.0 and 1.0.
//console.log(GetDamageTable(playerStats.level));
//console.log("ComputeToHit() = " + ComputeToHit(playerStats, false));
//console.log("ComputeDefense() = " + ComputeDefense(mobStats));
//console.log("ComputeMitigation() = " + ComputeMitigation(mobStats));
//console.log("ComputeWrath() = " + ComputeWrath(playerStats, false));
//console.log("RollD20() = " + RollD20(ComputeWrath(playerStats, false), ComputeMitigation(mobStats)));
//console.log("ApplyDamageTable() with Damage Bonus = " + (ApplyDamageTable(playerStats.mainHandWeaponDamage * RollD20(ComputeWrath(playerStats, false), ComputeMitigation(mobStats)), playerStats, { bForceMaxDamageInput: false, bUseOffhand: false })));
const makeTable = results =>
`<table>`+
`<thead><tr>${results.map((_, i) => `<th style="border: 1px solid black; padding: 0.2em">${i ? i : 'Misses'}</td>`).join('')}</th></thead>`+
`<tbody><tr>${results.map(result => `<td style="border: 1px solid black; padding: 0.2em">${result}</td>`).join('')}</tr></tbody>`+
`</table>`;
const displayResults = () => {
for (const field of fields) {
playerStats[field] = Number($(`[name="${field}"]`).val() || $(`[name="${field}"]`).attr('placeholder'));
if (!playerStats[field] && playerStats[field] !== 0) {
alert(`You must supply a ${fieldLabels[field]}!`);
return false;
}
};
for (const field of mobFields) {
mobStats[field] = Number($(`[name="${field}"]`).val() || $(`[name="${field}"]`).attr('placeholder'));
if (!mobStats[field] && mobStats[field] !== 0) {
alert(`You must supply a ${fieldLabels[field]}!`);
return false;
}
};
const results = RunDPSTest(playerStats, mobStats, 500);
$('#results').html(
Object.keys(results)
.map(label => {
const value = Array.isArray(results[label]) ? makeTable(results[label]): results[label];
return `<div>${label}: ${value}</div>`;
})
.join('')
);
return false;
}
$('#form').html(`
<form>
<fieldset>${fieldsHtml}</fieldset>
<fieldset>${mobFieldsHtml}</fieldset>
<button type="submit">Submit</button>
</form>
<div id="results"></div>
`);
$('#form form').submit(displayResults);
function createStatStruct() {
var newStatStruct = {
level: 0,
// Player Only for now (don't have mob DPS set up)
strength: 0,
offenseSkill: 0,
doubleAttackSkill: 0,
dualWieldSkill: 0,
hastePercentage: 0,
itemAndSpellATKTotal: 0,
mainHandWeaponSkill: 0,
mainHandWeaponDamage: 0,
mainHandWeaponDelay: 0,
mainHandDamageBonus: 0,
offHandWeaponSkill: 0,
offHandWeaponDamage: 0,
offHandWeaponDelay: 0,
// Mob Only for now (don't have mob DPS set up)
agility: 0, // AGI doesn't have a large effect. Setting it somewhere above 75 is fine.
defenseSkill: 0, // Rule of thumb is probably 5 + (5 * level), just like player melee classes.
totalAC: 0, // Level 50 mobs supposedly have around 115 AC according to EQEMU comments. Assume lower levels have lower AC. Raid targets probably have a decent amount more.
riposteSkill: 0, // Rule of thumb is probably 5 + (5 * level), just like player melee classes.
parrySkill: 0, // Rule of thumb is probably 5 + (5 * level), just like player melee classes.
dodgeSkill: 0, // Rule of thumb is probably 5 + (5 * level), just like player melee classes.
};
return newStatStruct;
}
function GetDamageTable(levelInput) {
const mn_DamageTableArray = [
{ maxExtra: 210, chance: 49, minusFactor: 105 }, // level 1-50
{ maxExtra: 245, chance: 35, minusFactor: 80 }, // level 51-54
{ maxExtra: 265, chance: 28, minusFactor: 70 }, // level 55-59
{ maxExtra: 285, chance: 23, minusFactor: 65 } // level 60
];
if (levelInput > 50 && levelInput <= 54) {
return mn_DamageTableArray[1];
}
else if (levelInput >= 55 && levelInput < 60) {
return mn_DamageTableArray[2];
}
else if (levelInput >= 60) {
return mn_DamageTableArray[3];
}
return mn_DamageTableArray[0];
}
function ComputeToHit(attackerStatStruct, bUseOffhand) {
var attackerWeaponSkill = attackerStatStruct.mainHandWeaponSkill;
if (bUseOffhand) {
attackerWeaponSkill = attackerStatStruct.offHandWeaponSkill;
}
const mn_SkillBonus = 7 + 10; // There are two different magic numbers in the code for this, which is why I keep them separate for reference.
const mn_SkillMultiplier = 121;
const mn_SkillDenominator = 100;
return ((attackerStatStruct.offenseSkill + attackerWeaponSkill + mn_SkillBonus) * mn_SkillMultiplier) / mn_SkillDenominator;
}
function ComputeDefense(defenderStatStruct) {
// These two variables are designed to adjust AGI effectivenes based on era.
const mn_AgilityEraMultiplier = 1000; // May need to adjust if P99 specifically uses a different era offset.
const mn_AgilityEraDenominator = 1000; // Don't change this variable. The EraMultiplier needs this to stay at 1000.
const mn_DefenseSkillBonus = 10;
const mn_DefenseSkillMultiplier = 400;
const mn_DefenseSkillDenomimator = 225;
const mn_AgilitySubtraction = 40;
const mn_AgilityMultiplier = 800;
const mn_AgilityDenominator = 3600;
const finalDefense = ((defenderStatStruct.defenseSkill * mn_DefenseSkillMultiplier) / mn_DefenseSkillDenomimator) + mn_DefenseSkillBonus;
const finalAgility = (mn_AgilityEraMultiplier * (mn_AgilityMultiplier * (defenderStatStruct.agility - mn_AgilitySubtraction)) / mn_AgilityDenominator) / mn_AgilityEraDenominator;
return finalDefense + finalAgility;
}
function ComputeMitigation(defenderStatStruct) {
const mn_DefenseSkillDenominator = 5;
const mn_AgilityDenominator = 20;
return defenderStatStruct.totalAC + (defenderStatStruct.defenseSkill / mn_DefenseSkillDenominator) + (defenderStatStruct.agility / mn_AgilityDenominator);
}
// This is normally called "offense" in the EQEMU code (not the same as the Offense Skill). Renaming it to "wrath" (I made this up) to reduce confusion.
function ComputeWrath(attackerStatStruct, bUseOffhand) {
var attackerWeaponSkill = attackerStatStruct.mainHandWeaponSkill;
if (bUseOffhand) {
attackerWeaponSkill = attackerStatStruct.offHandWeaponSkill;
}
// These two variables are designed to adjust ATK effectivenes based on era.
const mn_ATKEraMultiplier = 100; // May need to adjust if P99 specifically uses a different era offset.
const mn_ATKEraDenominator = 100; // Don't change this variable. The EraMultiplier needs this to stay at 100.
const mn_ATKDenominator = 2;
const mn_minStrength = 75;
const mn_strengthMultiplier = 2;
const mn_strengthSubtraction = 150;
const mn_strengthDenominator = 3;
var StrengthModifier = 0;
if (attackerStatStruct.strength >= mn_minStrength) {
StrengthModifier = ((mn_strengthMultiplier * attackerStatStruct.strength) - mn_strengthSubtraction) / mn_strengthDenominator;
}
return attackerWeaponSkill + StrengthModifier + ((attackerStatStruct.itemAndSpellATKTotal / mn_ATKDenominator) * mn_ATKEraMultiplier) / mn_ATKEraDenominator;
}
function RollD20(wrathInput, mitigationInput) {
const mn_wrathBonus = 5;
const mn_mitigationBonus = 5;
const mn_averageDenominator = 2;
const mn_diceNumberOfSides = 20;
const mn_d20RollsArray = [
0.1, 0.2, 0.3, 0.4, 0.5,
0.6, 0.7, 0.8, 0.9, 1.0,
1.1, 1.2, 1.3, 1.4, 1.5,
1.6, 1.7, 1.8, 1.9, 2.0
];
const wrathRoll = Math.random() * (wrathInput + mn_wrathBonus);
const mitigationRoll = Math.random() * (mitigationInput + mn_mitigationBonus);
var wrathMitRollAvg = (wrathInput + mitigationInput + mn_wrathBonus + mn_mitigationBonus) / mn_averageDenominator;
if (wrathMitRollAvg < 1) {
wrathMitRollAvg = 1;
}
var index = ((wrathRoll - mitigationRoll) + (wrathMitRollAvg / mn_averageDenominator));
if (index < 0) {
index = 0;
}
var rollIndex = Math.floor((index * mn_diceNumberOfSides) / wrathMitRollAvg);
if (rollIndex > mn_diceNumberOfSides - 1) {
rollIndex = mn_diceNumberOfSides - 1;
}
if (rollIndex < 0) {
rollIndex = 0;
}
return mn_d20RollsArray[rollIndex];
}
function ApplyDamageTable(damageDoneInput, attackerStatStruct, booleansInput) {
const mn_attackerDamageRounding = 0.5;
const attackerDamageDone = damageDoneInput + mn_attackerDamageRounding;
const attackerWrath = ComputeWrath(attackerStatStruct, booleansInput.bUseOffhand);
const damageTable = GetDamageTable(attackerStatStruct.level);
if (!booleansInput.bForceMaxDamageInput && (Math.random() * 100) <= damageTable.chance) {
if (booleansInput.bUseOffhand) {
return attackerDamageDone;
}
return attackerDamageDone + attackerStatStruct.mainHandDamageBonus;
}
const mn_baseBonusDenominator = 2;
const mn_percentBonus = 100;
const mn_attackDamageDoneDenominator = 100;
const mn_baseBonusMinimum = 10;
var baseBonus = (attackerWrath - damageTable.minusFactor) / mn_baseBonusDenominator;
if (baseBonus < mn_baseBonusMinimum) {
baseBonus = mn_baseBonusMinimum;
}
var extraPercent = Math.random() * baseBonus;
if (booleansInput.bForceMaxDamageInput) {
extraPercent = baseBonus;
}
var percent = mn_percentBonus + extraPercent;
if (percent > damageTable.maxExtra) {
percent = damageTable.maxExtra;
}
if (booleansInput.bUseOffhand) {
return ((attackerDamageDone * percent) / mn_attackDamageDoneDenominator);
}
return ((attackerDamageDone * percent) / mn_attackDamageDoneDenominator) + attackerStatStruct.mainHandDamageBonus;
}
function computeDoubleAttack(attackerStatStruct) {
const mn_DoubleAttackDenominator = 500;
return (attackerStatStruct.doubleAttackSkill + attackerStatStruct.level) / mn_DoubleAttackDenominator;
}
function computeWeaponDelayWithHaste(attackerStatStruct, bUseOffhand) {
const mn_WeaponDelayDenominator = 100;
var attackerWeaponDelay = attackerStatStruct.mainHandWeaponDelay;
if (bUseOffhand) {
attackerWeaponDelay = attackerStatStruct.offHandWeaponDelay;
}
return attackerWeaponDelay / (1 + (attackerStatStruct.hastePercentage / mn_WeaponDelayDenominator));
}
function computeDualWield(attackerStatStruct) {
const mn_DualWieldDenominator = 400;
return (attackerStatStruct.dualWieldSkill + attackerStatStruct.level) / mn_DualWieldDenominator;
}
function computeRiposte(defenderStatStruct) {
if (defenderStatStruct.riposteSkill <= 0) {
return false;
}
var mn_riposteBonus = 100;
var mn_riposteDenominator = 50;
return (Math.random() * 100) < (defenderStatStruct.riposteSkill + mn_riposteBonus) / mn_riposteDenominator;
}
function computeParry(defenderStatStruct) {
if (defenderStatStruct.parrySkill <= 0) {
return false;
}
var mn_parryBonus = 100;
var mn_parryDenominator = 45;
return (Math.random() * 100) < (defenderStatStruct.parrySkill + mn_parryBonus) / mn_parryDenominator;
}
function computeDodge(defenderStatStruct) {
if (defenderStatStruct.dodgeSkill <= 0) {
return false;
}
var mn_dodgeBonus = 100;
var mn_dodgeDenominator = 45;
return (Math.random() * 100) < (defenderStatStruct.dodgeSkill + mn_dodgeBonus) / mn_dodgeDenominator;
}
function rollHitAndDamageWithTable(attackerStatStruct, defenderStatStruct, bUseOffhand) {
var returnStruct = {
bHitDefender: false,
diceRoll: 0,
rolledDamage: 0,
finalDamage: 0,
riposted: false,
parryed: false,
dodged: false
};
// Order in EQEMU is Riposte > Parry > Dodge.
// Parsing on P99 suggests Parry > Dodge > Riposte. Will use that instead for now.
if (computeParry(defenderStatStruct)) {
returnStruct.parryed = true;
return returnStruct;
}
if (computeDodge(defenderStatStruct)) {
returnStruct.dodged = true;
return returnStruct;
}
if (computeRiposte(defenderStatStruct)) {
returnStruct.riposted = true;
return returnStruct;
}
if ((Math.random() * ComputeToHit(attackerStatStruct, bUseOffhand)) > (Math.random() * ComputeDefense(defenderStatStruct))) {
returnStruct.bHitDefender = true;
returnStruct.diceRoll = RollD20(ComputeWrath(attackerStatStruct, bUseOffhand), ComputeMitigation(defenderStatStruct));
returnStruct.rolledDamage = attackerStatStruct.mainHandWeaponDamage * returnStruct.diceRoll;
if (bUseOffhand) {
returnStruct.rolledDamage = attackerStatStruct.offHandWeaponDamage * returnStruct.diceRoll;
}
returnStruct.finalDamage = Math.floor(ApplyDamageTable(returnStruct.rolledDamage, attackerStatStruct, { bForceMaxDamageInput: false, bUseOffhand: bUseOffhand }));
}
return returnStruct;
}
function RunDPSTest(attackerStatStruct, defenderStatStruct, numHitsToTestInput) {
const mainHandDelayHasteAdjusted = computeWeaponDelayWithHaste(attackerStatStruct, false);
const mainHandMaxDamage = Math.floor(ApplyDamageTable(attackerStatStruct.mainHandWeaponDamage * 2, attackerStatStruct, { bForceMaxDamageInput: true, bUseOffhand: false }));
var uniqueMainHandDamageValueCountArray = new Array(mainHandMaxDamage + 1).fill(0);
var uniquediceRollCountArray = new Array(21).fill(0);
var mainHandDamageAggregate = 0;
var dualWieldAttempts = 0;
var mainHandDoubleAttackAttempts = 0;
var offHandDoubleAttackAttempts = 0;
var ripostedCount = 0;
var parriedCount = 0;
var dodgedCount = 0;
var i = 0;
for (i = 0; i < numHitsToTestInput; i++) {
const mainHandRollResult = rollHitAndDamageWithTable(attackerStatStruct, defenderStatStruct, false);
if (mainHandRollResult.bHitDefender) {
uniquediceRollCountArray[Math.floor(mainHandRollResult.diceRoll * 10)] += 1;
uniqueMainHandDamageValueCountArray[mainHandRollResult.finalDamage] += 1;
mainHandDamageAggregate += mainHandRollResult.finalDamage;
}
else {
uniquediceRollCountArray[0] += 1;
uniqueMainHandDamageValueCountArray[0] += 1; // misses.
if (mainHandRollResult.riposted) {
ripostedCount++;
}
else if (mainHandRollResult.parryed) {
parriedCount++;
}
else if (mainHandRollResult.dodged) {
dodgedCount++;
}
}
if (attackerStatStruct.offHandWeaponDamage > 0 && Math.random() <= computeDualWield(attackerStatStruct)) {
dualWieldAttempts++;
if (attackerStatStruct.doubleAttackSkill > 0 && Math.random() <= computeDoubleAttack(attackerStatStruct)) {
offHandDoubleAttackAttempts++;
}
}
if (attackerStatStruct.doubleAttackSkill > 0 && Math.random() <= computeDoubleAttack(attackerStatStruct)) {
mainHandDoubleAttackAttempts++;
const mainHandRollResult = rollHitAndDamageWithTable(attackerStatStruct, defenderStatStruct, false);
if (mainHandRollResult.bHitDefender) {
uniquediceRollCountArray[Math.floor(mainHandRollResult.diceRoll * 10)] += 1;
uniqueMainHandDamageValueCountArray[mainHandRollResult.finalDamage] += 1;
mainHandDamageAggregate += mainHandRollResult.finalDamage;
}
else {
uniquediceRollCountArray[0] += 1;
uniqueMainHandDamageValueCountArray[0] += 1; // misses.
if (mainHandRollResult.riposted) {
ripostedCount++;
}
else if (mainHandRollResult.parryed) {
parriedCount++;
}
else if (mainHandRollResult.dodged) {
dodgedCount++;
}
}
}
}
const offHandDelayHasteAdjusted = computeWeaponDelayWithHaste(attackerStatStruct, true);
const offHandMaxDamage = Math.floor(ApplyDamageTable(attackerStatStruct.offHandWeaponDamage * 2, attackerStatStruct, { bForceMaxDamageInput: true, bUseOffhand: true }));
var offHandDamageAggregate = 0;
dualWieldAttempts = dualWieldAttempts / (offHandDelayHasteAdjusted / 10);
offHandDoubleAttackAttempts = offHandDoubleAttackAttempts / (offHandDelayHasteAdjusted / 10);
const uniqueOffHandDamageValueCountArray = new Array(Math.floor(offHandMaxDamage) + 1).fill(0);
for (i = 0; i < dualWieldAttempts + offHandDoubleAttackAttempts; i++) {
const offHandRollResult = rollHitAndDamageWithTable(attackerStatStruct, defenderStatStruct, true);
if (offHandRollResult.bHitDefender) {
uniquediceRollCountArray[Math.floor(offHandRollResult.diceRoll * 10)] += 1;
uniqueOffHandDamageValueCountArray[offHandRollResult.finalDamage] += 1;
offHandDamageAggregate += offHandRollResult.finalDamage;
}
else {
uniquediceRollCountArray[0] += 1;
uniqueOffHandDamageValueCountArray[0] += 1; // misses.
if (offHandRollResult.riposted) {
ripostedCount++;
}
else if (offHandRollResult.parryed) {
parriedCount++;
}
else if (offHandRollResult.dodged) {
dodgedCount++;
}
}
}
var diceRollAverage = 0;
var diceRollCount = 0;
for (i = 1; i < 21; i++) {
diceRollAverage += uniquediceRollCountArray[i] * i;
diceRollCount += uniquediceRollCountArray[i];
}
diceRollAverage = diceRollAverage / diceRollCount;
return {
"ApplyDamageTable() Main Hand Max Damge": mainHandMaxDamage,
"ApplyDamageTable() Off Hand Max Damage": offHandMaxDamage,
"Main Hand Swings": numHitsToTestInput,
"Main Hand Double Attack Swings": Math.floor(mainHandDoubleAttackAttempts),
"Dual Wield Swings": Math.floor(dualWieldAttempts),
"Dual Wield Double Attack Swings": Math.floor(offHandDoubleAttackAttempts),
"Dice Roll Average without zeros": diceRollAverage,
"Defender Riposte Count": ripostedCount,
"Defender Parry Count": parriedCount,
"Defender Dodged Count": dodgedCount,
"Unique Dice Roll Count": uniquediceRollCountArray,
"Main Hand Damage Aggregate": mainHandDamageAggregate,
"Uniquue Main Hand Damage Value Count": uniqueMainHandDamageValueCountArray,
"Off Hand Damage Aggregate": offHandDamageAggregate,
"Unique Off-Hand Damage Value Count": uniqueOffHandDamageValueCountArray,
"Predicted Total DPS": (((mainHandDamageAggregate / numHitsToTestInput) / (mainHandDelayHasteAdjusted / 10)) + (offHandDamageAggregate / numHitsToTestInput))
}
}