Jump to content
This is a wiki for a reason. Anyone can contribute. If you see something that is inaccurate or can be improved, don't ask that it be fixed--just improve it.
[ Disclaimer, Create new user --- Wiki markup help, Install P99 ]

MediaWiki:DamageCalculator.js

From p99 Backup
Revision as of 23:33, 13 April 2025 by imported>Loramin
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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