Melee DPS calculation (for animals): how to calculate with chanceFactor?

Started by Syrus, October 16, 2020, 10:49:10 AM

Previous topic - Next topic

Syrus

I've been trying to figure out how the melee DPS calculation (for animals) works. Of course, I'm not sure whether the calculation even represents the actual DPS ingame, but I'd still like to know the magic behind it.

I've figure out:
- The highest DPS attack makes up 75% of the total DPS. If there are several of the same, highest DPS, they will split the 75% chance among them.
- All attacks with more than 25% of the highest DPS divide the remaining 25% of the total DPS among them.
- All attacks with 25% or less of the highest DPS are ignored.

This, I'm quite certain of, so far.

The problem arises when the "chanceFactor" property is used, as I cannot figure out how exactly it comes into the calculation.


All tests were done using three attacks ("tools"). All chanceFactors for the various cases were found by testing.
All uninjured animals have a melee skill of 4, resulting in a 62% multiplier for the average DPS.
Damage values are just for testing, obviously they are quite ridiculous for ingame stuff.

Never did a 5th case, but there should be one where the third attack will be ignored when the chanceFactor is very low.

----- Test 1, damage is 10, 10, 40, cooldown is always 1. -----
Case 1, baseline:

DamageCooldownchanceFactor%_of_Total
10110%
10110%
4011100%
Results in "average" damage of 40, dps of 24.8.

Case 2, chanceFactor < 1.0 & >= 0.19:

DamageCooldownchanceFactor%_of_Total
101112.5%
101112.5%
4010.1975%
Results in "average" damage of 32.5, dps of 20.15.

Case 3, chanceFactor = 0.18:

DamageCooldownchanceFactor%_of_Total
101133.3...%
101133.3...%
4010.1833.3...%
Results in "average" damage of 20, dps of 12.4.

Case 4, chanceFactor <= 0.17:

DamageCooldownchanceFactor%_of_Total
101137.5%
101137.5%
4010.1725%
Results in "average" damage of 17.5, dps of 10.85.


----- Test 2, damage is 20, 20, 80, cooldown is always 1. -----
Case 1, baseline:

DamageCooldownchanceFactor%_of_Total
20110%
20110%
8011100%
Results in "average" damage of 80, dps of 49.6.

Case 2, chanceFactor < 1.0 & >= 0.16:

DamageCooldownchanceFactor%_of_Total
201112.5%
201112.5%
8010.1675%
Results in "average" damage of 65, dps of 40.3.

Case 3, chanceFactor = 0.15:

DamageCooldownchanceFactor%_of_Total
201133.3...%
201133.3...%
8010.1533.3...%
Results in "average" damage of 40, dps of 24.8.

Case 4, chanceFactor <= 0.14:

DamageCooldownchanceFactor%_of_Total
201137.5%
201137.5%
8010.1425%
Results in "average" damage of 35, dps of 21.7.


----- Test 3, damage is 1, 1, 4, cooldown is always 1. -----
Case 1, baseline:

DamageCooldownchanceFactor%_of_Total
1110%
1110%
411100%
Results in "average" damage of 4, dps of 2.48.

Case 2, chanceFactor < 1.0 & >= 0.26:

DamageCooldownchanceFactor%_of_Total
11112.5%
11112.5%
410.2675%
Results in "average" damage of 3.25, dps of 2.02.

Case 3, chanceFactor = 0.25:

DamageCooldownchanceFactor%_of_Total
11133.3...%
11133.3...%
410.2533.3...%
Results in "average" damage of 2, dps of 1.24.

Case 4, chanceFactor <= 0.22:

DamageCooldownchanceFactor%_of_Total
11137.5%
11137.5%
410.2225%
Results in "average" damage of 1.75, dps of 1.09.


----- Test 4, damage is always 1, cooldown is 1, 1, 0.25. -----
Case 1, baseline:

DamageCooldownchanceFactor%_of_Total
1110%
1110%
10.251100%
Results in "average" cooldown of 0.25, dps of 2.48.

Case 2, chanceFactor < 1.0 & >= 0.27:

DamageCooldownchanceFactor%_of_Total
11112.5%
11112.5%
10.250.2775%
Results in "average" cooldown of 0.44, dps of 1.42.

Case 3, chanceFactor = 0.26:

DamageCooldownchanceFactor%_of_Total
11133.3...%
11133.3...%
10.250.2633.3...%
Results in "average" cooldown of 0.75, dps of 0.83.

Case 4, chanceFactor <= 0.23:

DamageCooldownchanceFactor%_of_Total
11137.5%
11137.5%
10.250.2325%
Results in "average" cooldown of 0.81, dps of 0.76.


I just cannot figure out why the chanceFactor is different for these tests.
My assumption was, that if the damage is doubled for all attacks, the chanceFactor required to get a different calculation case would be the same.
But it's not. And the same behaviour appears when chanceFactor is set in a way that one attack "out-DPS-weights" the others enough to be the only attack picked. (Using a high factor of more than 1. The 4x-the-damage limit mentioned at the start, chanceFactor seems to modify that somehow.)

RawCode

download dnspy
check how game calculates DPS for info card

if you want custom DPS calculation you can patch same method and display other value

actual DPS and predicted DPS obviously won't match


Syrus

Sorry for the late response, but I finally got around to working on this again.
I've looked at the dnSpy tool, it looks very powerful, though I've not been able to figure out how to make it work. Or rather, it gave me a headache.

I have been coding together a mod that prints out a lot of data in detail, things like apparel, weapons, plants and now animals. Does not do anything fancy, but for someone like me, who loves to have spreadsheets for all kinds of stuff, this has been very lovely.
Anyway, after quite some searching - and even more headache - I thought I had found what I was looking for with this:
thingDef.GetStatValueAbstract(StatDefOf.MeleeWeapon_AverageDPS);
(With "thingDef" being the Animal's thingDef.) But sadly it does not seem to be correct either.

I tried "StatDefOf.MeleeDPS" before, but that just throws warnings:
"Getting MeleeDPS stat for [ANIMAL] without concrete pawn. This always returns 0."
... refuses to return a melee DPS without actually having a pawn. Great when you want to have the base value.

Why does this have to be so brain-twistingly difficult?
All I want is to know the freaking DPS that's displayed ingame, without any modifiers.


EDIT:
Alright, ILSpy has been helpful instead, and digging through code I think I figured out how the melee DPS for the info cards is calculated.
From that I derived the calculation for the raw values.

No guarantee that this is correct, but from some quick testing the values did appear to be correct.
One exception is that some pawns may not have body parts for manipulation, always incurring a -12 modifier on the post-process curve.
Also, all animals have a default melee skill of 4, as far as I can tell, so their DPS post-process curve is modified by +4. This results in a final value of 62% where no further modification is applied. If, as mentioned before, the animal has no manipulation body parts, the post-process curve value is -8, resulting in a 18% final value with which the calculated DPS is multiplied.

To calculate the theoretical raw DPS, as appears to be done for the infocards (which in no way has to represent the actual DPS, mind you):

Step 1:
Find the HighestInitialWeight of all thingDef.tools with:
   initialWeight = tool.damage * (1 + (tool.armorPenetration < 0 ? tool.damage * 0.015f : tool.armorPenetration)) / tool.cooldownTime * tool.chanceFactor

Step 2:
Via the InitialWeight all Tools are divided into three Categories: Best, Mid, Worst.
   Best gets a SelectionWeight-total of 0.75; condition: InitialWeight >= HighestInitialWeight * 0.95
   Mid gets a SelectionWeight-total of 0.25; condition: InitialWeight >= HighestInitialWeight * 0.25
   Worst gets no SelectionWeight, as in, these attacks are ignored, apparently.
We have to iterate over the thingDef.tools list again to find which category each tool falls into.
For later we also need to remember how many tools are in the Best and Mid category each; these two values will be BestCount and MidCount.

Step 3:
Calculate the BestCategoryFactor and MidCategoryFactor:
   BestCategoryFactor = 1 / BestCount * 0.75
   MidCategoryFactor = 1 / MidCount * 0.25
Iterate over the list once again to save the factor for each tool as well as to add up all factors so we know the TotalCategoryFactor.

Step 4:
Iterate over the list one last time to sum up the TotalDamage and TotalCooldown via:
   TotalDamage = CategoryFactor / TotalCategoryFactor * Damage
   TotalCooldown = CategoryFactor / TotalCategoryFactor * Cooldown
Using these two values, which are the ones displayed in the breakdown for DPS on the infocard, the raw TotalDPS can be calculated.
   TotalDPS = TotalDamage / TotalCooldown
Remember, this is the raw value. For the final DPS the pawn's skill - for animals apparently always 4 - as well as other factors have to be kept in mind and the proper post-process curve be applied. The default for animals would be TotalDPS * 62% = "Infocard-DPS".


My code:
private static float CalculateDPS(List<Tool> tools)
{
float highestInitialWeight = 0f;
int catMidCount = 0, catBestCount = 0;
List<DamageCalc> allDPSList = new List<DamageCalc>();

// find highest initial weight
foreach (var tool in tools)
{
float damage = tool.power;
float cooldown = tool.cooldownTime;
float armorPenetration = tool.armorPenetration;
float chanceFactor = tool.chanceFactor;
float initialWeight = damage * (1f + (armorPenetration < 0f ? damage * 0.015f : armorPenetration)) / cooldown * chanceFactor;
highestInitialWeight = Math.Max(initialWeight, highestInitialWeight);
}

// save each tools details
foreach (var tool in tools)
{
float damage = tool.power;
float cooldown = tool.cooldownTime;
float armorPenetration = tool.armorPenetration;
float chanceFactor = tool.chanceFactor;
float initialWeight = damage * (1f + (armorPenetration < 0f ? damage * 0.015f : armorPenetration)) / cooldown * chanceFactor;

int cat;
// worst
if (initialWeight < highestInitialWeight * 0.25f)
continue;
// mid
else if (initialWeight < highestInitialWeight * 0.95f)
{
cat = 1;
catMidCount++;
}
// best
else
{
cat = 2;
catBestCount++;
}

allDPSList.Add(new DamageCalc
{
Damage = damage,
Cooldown = cooldown,
ArmorPenetration = armorPenetration,
ChanceFactor = chanceFactor,
InitialWeight = initialWeight,
Cat = cat,
});
}

// calculate weighting factor
float factorCatMid = 1f / catMidCount * 0.25f;
float factorCatBest = 1f / catBestCount * 0.75f;
float factorCatTotal = 0f;
foreach (var dps in allDPSList)
{
switch (dps.Cat)
{
case 1:
dps.FactorCat = factorCatMid;
break;
case 2:
dps.FactorCat = factorCatBest;
break;
default:
continue;
}
factorCatTotal += dps.FactorCat;
}

float totalDamage = 0, totalCooldown = 0;
foreach (var dps in allDPSList)
{
totalDamage += dps.FactorCat / factorCatTotal * dps.Damage;
totalCooldown += dps.FactorCat / factorCatTotal * dps.Cooldown;
}
return totalDamage / totalCooldown;
}

private class DamageCalc
{
public float Damage;
public float Cooldown;
public float ArmorPenetration;
public float ChanceFactor;
public float InitialWeight;
public int Cat;
public float FactorCat;
public float DPS => Damage / Cooldown;
}




Oh, and obviously, getting the DPS for already existing pawns is much, much easier.
This was all for getting the theoretical raw DPS from a ThingDef.

If you now tell me there's an easier way, I'll name a colonist after you and do Rimworld-things to them.