Please upgrade your browser for the best possible experience.

Chrome Firefox Internet Explorer

Kinetic/Dark Bulwark Calculations

STAR WARS: The Old Republic > English > Classes
Kinetic/Dark Bulwark Calculations

dipstik's Avatar

02.19.2014 , 03:57 PM | #1
I am writing this to disclose and produce discussion about the methodology of determining the absorb contribution due to kinetic/dark bulwark. I had originally used a simulation that used a random number generator against the probability of shielding an attack to fill a table of states that weighted each stack by its uptime. Keyboardninja had found some errors in the simulation, which inspired a new methodology.

When I fist encountered this calculation, I thought using a binomial probability would be the most appropriate way to finding the absorb contribution. Unfortunately, if you have X swings in 20 seconds, you have 2^x number of ways to populate the "truth table," each of which needed to be weighted for its uptime and the probability of that set of X states.

To give you an example of what I'm talking about: If we have a swing timer of 1 hit every second, then we have 20 states. There are 20 different ways to get one success. If the first trial is a success, then we have an effective absorb contribution of 1*(0-20)/20=1%. If the last hit was shielded, the contribution would be 1*(19-20)/20=0.05%. If there are a total of 2 successes in the 20 hits, there are 20 choose 2 ways (20 choose 2 = 20!/(2!*(20-2)!) for that to happen, each with their own specific absorb weights, depending on when the shielded hits occurred. needless to say i did want to compute 2^20 different states.

Some background on binomial probability is due. If you roll a Y sided die, there is a p=1/Y chance (p for probability) of rolling a 1, lets call this a success. The probability of getting i successes from n trials is given by:
[n!/(i!*(n-i)!] tells you how many ways there are to roll a 1 i times out of n (this is just n choose i). the p^i part tells you the chance of getting i rolls, while the (1-p)^(n-i) part tells you the probability of not rolling 1 n-i times. For our purposes, p will be the chance of being able to shield a hit, n will be 20 seconds divided by the swing timer, and we will be finding this probability for every possible number of successes (i).

After some back and forth, Keyboardninja came up with a way of reducing the computational load required by separating the problem into each swing, with an effective absorb contribution for each swing, by taking all possible states after each trial/hit, using the binomial approach I wanted to start with. Each effective absorb contribution for each swing is then weighted by the amount of time for each swing.

Specifically, for each swing we take the probability of having i successes and multiply by the number of stacks associated with that number of successful shield events. We define the number of swings in 20 seconds by floor(20/T), where T is the period in seconds between swings. What we ended up with is:

E(n,p):=piecewise(n<=8, Summation((n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i)*i,i=1..n), Summation((n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i)*i,i=1..8)+Summation((n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i)*8,i=9..n)):

The function is piecewise because more than 8 successful shields will still only contribute 8 stacks of bulwark. We are summing from 1 success to n successes due to the fact that you cannot have more successes than you have had swings. So we have the binomial probability (n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i) multiplied by the number of stacks for the number of successes in question for that component of the summation, i for n<=8 and 8 for 9 to floor(20/T). We then have to sum each of these terms in order to find the total contribution over the entire 20 seconds, like so:


so each component contributes equally, since the expected value for each swing has an effective uptime of 1/floor(20/T).

As a sanity check, I also made a function that only allowed 15 or less successes to contribute, but have found no cases where this is reflected by the absorb amount.
E1(n,p):=piecewise(n<=8, Summation((n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i)*i,i=1..n), n>8, Summation((n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i)*i,i=1..8)+Summation((n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i)*8,i=9..n),n>15, Summation((n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i)*i,i=1..8)+Summation((n!)/(i!*(n-i)!)*p^(i)*(1-p)^(n-i)*8,i=9..15)):
This function gives the same values as E(n,p) listed above, which does not stop the sum at 15. part of this is because the probability of getting more than 15 successes when you have 18 to 25 trials is very small, and those very small weights were not evident in the absorb (no difference up to 4 decimal places for the percent).

The probability p will depend on the damage weights (kw is fraction damage melee/ranged kinetic/energy, kf is fraction damage melee/ranged kinetic/energy, since we cannot shield against internal/elemental attacks we do not include this, but we have to make sure we only look at the swing period, T, of kinetic/energy attacks), defense chance (d), shield chance (s), resist chance (r), as well as the fraction of melee/ranged attacks that are at 90% accuracy versus 100% accuracy (fb for fraction bland and fn for fraction named). Putting all these things together we get:


For each swing timer i can get an equation for effective absorb contribution in terms of p. For example, for a swing timer of T=1 i get


For a swing timer of 1.5 seconds i get a much more elegant function:

Once we have a swing timer agreed upon, i can put it into my optimization spreadsheet to find the best values of defense, shield and absorb. For the time being however, I took it upon myself to find a regression in terms of p and T, up to 3 orders in p. Using Minitab with data for p=0 to 1 in 0.05 increments and T=0.5,1 and 1.5 I found:

A(T,p) = 2.03129 + 22.4408 p - 4.24114 T - 20.377 p*p - 4.96008 p*T + 1.865 T*T + 2.08018 p*p*p + 10.2966 p*p*T - 2.62597 p*T*T

Summary of Model
S = 0.166931 R-Sq = 99.50% R-Sq(adj) = 99.43%
PRESS = 2.39703 R-Sq(pred) = 99.20%

This is what i currently have in my spreadsheet, until we choose a swing timer, but this is a great equation with a pretty good correlation coefficient (R-squared value).

The swing timers are not in yet, but here is an example using a budget of 2721, a swing timer of 1/0.966=1.035 (swing timer for dread council hm). Some discussion went on between Keyboardninja and Thok-Zeus about the 50% 100% accuracy melee/ranged attack and 50% 90% accuracy assumptions i have been making since i first started reporting optimization numbers. It turns out that much more of the damage is 90% accuracy than 100%. With that in mind, I will begin to use the assumption that fraction bland damage = 75% (fb, or fraction melee/ranged damage at 90% accuracy), and faction named, fn, equal to 25%. I am assuming df/dp average damage weights.
p(.2812, .5849, 0.2e-1, .7327, .2274, .75, .25)= 0.4231. that is a probability of shielding a kinetic/energy hit
A(1.035, .4231) = 4.089. That is an absorb contribution of 4.089%

In the spreadsheet i leave p as a variable, so it changes with steps of the optimization. I ended up with:
Defense 528
Shield 980
Absorb 1213
with 4.189% contribution from bulwark (the difference is due to using the regression instead of an equation).

KeyboardNinja's Avatar

02.19.2014 , 04:20 PM | #2
As an addendum to this, for anyone doing math checking (please do!), here is a screenshot of a more readable rendering of these formulae:

Another addendum is that dipstik's example stats for a budget of 2721 at the tail end of the post do not consider the effects of the Fortunate Redoubt relic. When those effects are taken into account, optimal defense falls to about 340, while shield and absorb rise commensurately. Just to give you an idea of what the new stat distributions will look like, here is a shadow graph for the average of df/dp (defense is purple, absorb is gold, shield is green):
Computer Programmer. Theory Crafter. Dilettante on The Ebon Hawk.
Tam (shadow tank) Tov-ren (commando healer) Aveo (retired sentinel) Nimri (ruffian scoundrel)
Averith (marksman sniper) Alish (lightning sorcerer) Aresham (vengeance jugg) Effek (pyro pt)

December 13, 2011 to January 30, 2017


02.20.2014 , 08:44 PM | #3
If you're still seeking an absorb estimation from a non-simulation, I rewrote my MATLAB parser. I'm like 90% sure that both of you have MATLAB, but if you want me to process a combat log but don't have access to a MATLAB computer, get the file to me somehow.

Example of how to run for a combatlog.txt file in the same folder as the .m file:
EDU>> cd C:\Users\John\Desktop\Storage\SWTOR\matlab
EDU>> filelist = dir(pwd)

filelist = 

12x1 struct array with fields:

EDU>> filelist(3).name

ans =


EDU>> tic; [dict, final_abs] = parser(filelist(3).name); toc; % run with this line
I define {shielded attacks} as anything that has the phrases 'ApplyEffect Damage' AND '-shield' in the line, and define {unmitigated attacks} as anything that has phrase ''ApplyEffect Damage' AND (no '-' character in the line); that removes events like dodges and misses. I then define ith effective absorb as mean(shielded attack i)/mean(unmitigated attack i).

Then I can naively take the mean of all absorbs by taking the average of i attack types
EDU>> final_abs(isnan(final_abs)) = [];
EDU>> size(final_abs)

ans =

     1    83

EDU>> mean(final_abs)

ans =

And find that 5 months ago, I was getting ~0.4980-0.405 (from character sheet) = 0.093 absorb bonus over time. Note that this bonus includes click relic effects, Reflexive Shield, and absorb adrenal.

I think given enough samples, it must do a decent job of marginalizing out your effects that change DTPS like Reflexive Shield (Smoke Grenade is already not counted because it's not a part of the shielded or unmitigated sets), absorb adrenal, click relics, and damage reduction debuffs on the boss. It's both convenient and sensible to leave these things unregressed in the calculation, since it gives me a value that already incorporates all those effects.

Edit: this part was written really badly. What I mean is that if I want to estimate DTPS, I could either regress out all effects like Reflexive Shield, absorb adrenal, and click relics, to find my premitigation DTPS. Then I could calculate new DTPS for different stat values. But since I'm likely going to have similar Reflexive Shield/adrenal/relic usage in the future, I'd have no way of incorporating those effects into my new projected DTPS. If I leave those effects unregressed, I don't need to account for them when projecting new DTPS.

In your case since it's going to **** things up since you guys do relic calculations, 5% damage reduction, and adrenals separately.

Instead of taking a naive average you could edit the code to give you a mean of absorbs weighted by samples. That would approximate your absorb bonus over time easily enough. You could also rewrite a parser to instead track just absorb bonuses over time.

Full MATLAB code:

(quote my post to get a copy of the code that has better indentation)

Sample output (after cutting out the sections which contain attacks that originate from myself; since no boss can absorb, those were just a ton of NaN values. Also hit the SWTOR forum character limit of 50,000 characters)

Edit: parsed a recent Shadow's log for some DF/DP content. His torparse is here:
I found that he had 0.51 effective absorb, and his character sheet absorb is 47.54% / 1126 Absorb rating. That gives 0.51-0.4754 = 0.0346 effective absorb bonus by weight. Note, again, that's not absorb averaged over time; it's slightly different from that computation. It's the average over all attacks of {shielded damage} / {unmitigated damage} when both shielded damage and unmitigated damage occurred.

dipstik's Avatar

02.21.2014 , 10:50 AM | #4
this codes looks like it could get a pretty good number for accuracy of each attack too. thanks!


02.21.2014 , 02:54 PM | #5
I discovered a bug. abs% should be 1 - mean(shield dam)/mean(raw dam), not shield dam/ raw dam. I am going to fix the bug and extend the script to calculate premitigation DTPS. Maybe. This post should be updated in 2 hours.


Made that change to absorb calculation. I now get a mean effective abs of 0.5020 for my Vanguard tank, or 0.5020 - 0.405 = 0.0970 absorb bonus from Energy Blast and Power Screen. For that Shadow log I linked, I get a mean effective abs of 0.49 and a bonus from Kinetic Bulwark of 0.49 - 0.4754 = 0.0146 from that Shadow log that I posted.

Not sure why that bulwark value is so low. If premitigation_damage*(1-abs) = shield_damage and mean(premitigation_damage) and mean(shield_damage) are known, then
premitigation_damage*(1-abs) = shield_damage
1-abs = shield_damage/premitigation_damage
abs-1 = -shield_damage/premitigation_damage
abs = 1 - shield_damage/premitigation_damage

Edit #4 or so: manually confirmed for the attack Sporeling->Alara:Rake that the values picked up by the parser were the values being reported by torparse. Also manually confirmed for several attacks that mean(unmitigated)*(1-effective absorb reduction) = ~mean(shielded). So the values I'm reporting seem to be accurate and unbuggy now.

Also now producing output of this style:
Attack PalaceInterrogator->Alara:Shocked had 3 total samples: 1 unmitigated, 1 shielded, 1 othermitigated.
	Effective shield chance: 0.33
	Effective othermit chance: 0.33
	Effective absorb reduction: 0.49
	Average damage per hit: 601.67
	Effective postarmor mitigation coefficient: 0.50
Attack PalaceWatchman->Alara:Double Strike had 20 total samples: 11 unmitigated, 7 shielded, 2 othermitigated.
	Effective shield chance: 0.35
	Effective othermit chance: 0.10
	Effective absorb reduction: 0.53
	Average damage per hit: 3070.40
	Effective postarmor mitigation coefficient: 0.72
Cut and cleaned some of the code

Can KBN modify his Perl script to compute 1 - mean(shielded)/mean(unmitigated)? It would be good to corroborate.

dipstik's Avatar

02.28.2014 , 05:05 PM | #6
Took me way to long to figure this out, but i think i got an expression that takes the more than 14 success cases into account.
A(T,p):=piecewise(m(T)<15, 1/(m(T))*Summation(E(n,p),n=1..m(T)),
Summation((Summation(E(n,p),n=1..15+i)* P(15+i,p))/(15+i),i=0..m(T)-15)+((1- Summation(P(n,p),n=15..m(T)))*Summation(E(n,p),n=1 ..m(T)))/(m(T))):

P computes the probability of having 15 successes given m trials. we then use this weight with a denominator that will change the weight of the expected value per trial (if we have to refresh after 15 trials, we divide by 15, if after 16 trialswe divide by 16 etc.). for the cases where less than 15 successes occur, we use the compliment of P(cumulitive for 15 to m) with the floor(20/T) weight.

using this i generated tables for p=0.35 to 0.65 in 15 steps for T=0.75,1,1.25,1.5 and got this regression:

Regression Equation

A = 3.94583 + 18.9477 P - 8.19726 T - 45.3335 P*P + 21.4219 P*T + 9.53125
P*P*P + 19.4587 P*P*T - 15.8852 P*T*T + 2.07302 T*T*T

Summary of Model

S = 0.0278044 R-Sq = 99.90% R-Sq(adj) = 99.89%
PRESS = 0.0659578 R-Sq(pred) = 99.85%

This regression is off by 0.07 in some places however.

Using a timer of 1.035 seconds I get a r squared value of 1 for the following equation:


03.08.2014 , 07:40 PM | #7
All right, I spoke to Dipstik about what he might find useful and incorporated most of that into this script. KBN never responded concerning his Perl script so I'm not gonna worry about his workflow. I had considered making a completely new thread to post these scripts, but then I'd be inviting people to post their own combat logs for me to parse and that's more trouble than I want to invite on myself.

If KBN doesn't post his own breakdown, when NIM comes I will post 16man NIM DTPS parses built off of this script. 8man NIM DTPS parses may follow within a week or two. PVP parses may follow a while after 2.7.

(Quote this post to get .m code with some semblance of tabbing preserved)

Example of how to invoke (note you must have MATLAB to run this code. It's ~$100 on Amazon):
EDU>> format bank
EDU>> cd C:\Users\John\Desktop\Storage\SWTOR\matlab
EDU>> filelist = dir(pwd);
EDU>> parser_handler(filelist(4).name, [0.4445 0.19 0.2375], 'Alara')
To run the parser_handler.m script, call
parser_handler('Name of file to parse, as string', [kinetic_DR internal_DR procced_defense_chance], 'Player name as string if you wish to ignore player's attacks')
Note that the parser pauses after it reports the results of each combat. Hit the <Enter> key to permit it to progress to parsing the next combat.

Code for parser.m was updated a bit, the full code is now:

Code for a parser_handler.m script has been written:

And it now gives output of this style:

Beginning parsing at 1327 with event [19:47:09.797] [@Alar'a] [@Alar'a] [] [Event {836045448945472}: EnterCombat {836045448945489}] ()
Ending parsing at 2497 with event [19:50:06.015] [@Alar'a] [@Alar'a] [] [Event {836045448945472}: ExitCombat {836045448945490}] ()
	NefraWhoBarstheWay->Alara:Twin Attack
		141 samples, 444496 total damage, 3152.45 average damage including avoidance events
		0.32 raw shield occurrence, 0.54 shield chance as proportion of nondefended attacks, 0.54 effective absorb as estimated by (1-mean(shielded))/mean(unmitigated)
		0.41 total avoid chance, estimated predefense miss chance 0.17 = count(avoided) / count(attacks) - (given defense chance)
		Damage type melee kinetic, premitigation 1064809.74 = count(attacks)*mean(unmitigated damage), estimated total prearmor damage: 1916849.21 = (premitigation damage) / (1 - type damage reduction)
		Mitigation coefficient 0.23, estimated by (observed total damage)/(prearmor estimated damage)
	NefraWhoBarstheWay->Alara:Voice of the Masters (Any)
		13 samples, 23228 total damage, 1786.77 average damage including avoidance events
		0.00 raw shield occurrence, 0.00 shield chance as proportion of nondefended attacks, NaN effective absorb as estimated by (1-mean(shielded))/mean(unmitigated)
		0.08 total avoid chance, estimated predefense miss chance 0.00 = count(avoided) / count(attacks) - (given defense chance)
		Damage type force internal, premitigation 25163.67 = count(attacks)*mean(unmitigated damage), estimated total prearmor damage: 31066.26 = (premitigation damage) / (1 - type damage reduction)
		Mitigation coefficient 0.75, estimated by (observed total damage)/(prearmor estimated damage)
Total duration 177 seconds
Damage profile:
	1916849.21 total damage/10829.66 PMDTPS of type {ranged kinetic} at 0.17 base miss chance
	0.17 mean dodge rate for melee kinetic damage when weighted by premitigation damage magnitude
	NaN mean dodge rate for melee internal damage when weighted by premitigation damage magnitude
	0.00 total damage/0.00 PMDTPS of type {force kinetic}
	31066.26 total damage/175.52 PMDTPS of type {force internal}
	0.24 mean mitigation coefficient when weighted by damage weights, counting base miss chance as mitigation
	11005.17 total PMDTPS
Finished displaying data for fight #5
  • sometimes NaN values originate when very few samples of an attack are available and they were all shielded, e.g. 1 hit from Strike in the log and it was shielded. I tried to prevent NaN values from spreading downstream and it seems okay, but if you see 1 attack with a NaN value that's probably what's happening
  • reporting ranged internal damage seemed silly but I threw it in just to be consistent
  • doesn't handle player cooldowns or debuffs and never will. Actual DTPS values and mean mitigation coefficient values will almost always be higher than reported, for this reason.
  • As reported in issue 1, the parser is agnostic toward player's absorb%. This causes trouble when all instances of an attack are shielded; since abs ~ (1-mean(shielded))/mean(unmitigated) some values will appear as NaN if there are no unmitigated values to pull. I could have extended the script to take player abs but frankly it's not common enough to be a serious issue
  • melee attacks must have a raw dodge/miss rate exceeding a threshold, default 0.15. This means 1) if you have n melee attack whose dodge rate is less than 0.15, it'll be misclassified as Force 2) if you have a force attack which you consistently resist, it'll be misclassified as melee. I'm not interested in parsing through all your dodges/misses/parries/resists/deflects and determining what is really going on there
  • does a poor job of understanding things like in combat stealth. Combat is defined as the first instance of 'EnterCombat' event to any of the following events: <'ExitCombat', 'EnterCombat', 'Revive', 'Death'>
  • doesn't support innate boss fight recognition
  • doesn't yet support multilog parsing. The easiest way to get that set up would be to write another script that defines EnterCombat events as being the first instance in time that an EnterCombat event is encountered, EndCombat as the last instance of its occurrence, and then concatenates together the logs over time

KareBarey's Avatar

03.09.2014 , 06:13 AM | #8
Is there an English translation to this?
When you look at the dark side, careful you must be ... for the dark side looks back.

--YODA, Dark Rendezvous

dipstik's Avatar

03.09.2014 , 11:42 AM | #9
this tool looks amazing! I'm sure once i start playing with it ill have some questions. I would think even one instance of the parse sating resist would be able to ensure the attack was force, likewise with other types of attacks. i think you can onlyparry a melee attack, deflect a ranged attack and so on. but im not sure.

Omophorus's Avatar

03.09.2014 , 05:48 PM | #10
Quote: Originally Posted by dipstik View Post
this tool looks amazing! I'm sure once i start playing with it ill have some questions. I would think even one instance of the parse sating resist would be able to ensure the attack was force, likewise with other types of attacks. i think you can onlyparry a melee attack, deflect a ranged attack and so on. but im not sure.
I'm fairly certain this is not, in fact, the case.

Or at least, not consistently.

For instance, from Hateful Entity last night:
01:19:20.143 Srs'bsns parries Hateful Entity's Shock, causing 1 threat.
Shock is very definitely a ranged attack, but is always parried in the log, rather than deflected.

I'll see about looking through logs from other fights when I get a chance to see if there's any kind of consistency.
Srs'bsns, GM of <Proper Villains> of The Ebon Hawk
5/5 Nightmare Power DF & DP
"This is why we don't bring Assassin tanks"