January 12, 201511 yr After the discussion on the forums about critical hit damage being too high, and after seeing some questionable results from various things in the game I decided to ask Josh Sawyer about how total damage was calculated in Pillars of Eternity.His reply was Now much of the time, I'm not seeing results in this range on weapons. So I decided to look at the source code to see how damage is actually calculated in the game. The method which I think is most relevant is below.This is the AdjustDamageDealt method from the class CharacterStats, which is called in the AttackBase.calcDamage() method public void AdjustDamageDealt(GameObject enemy, ref DamageInfo damage) { damage.DamageAmount *= this.StatDamageHealMultiplier; if (this.OnPreDamageDealt != null) this.OnPreDamageDealt(((Component) this).get_gameObject(), new CombatEventArgs(damage, ((Component) this).get_gameObject(), enemy)); if (this.OnAddDamage != null) this.OnAddDamage(((Component) this).get_gameObject(), new CombatEventArgs(damage, ((Component) this).get_gameObject(), enemy)); int roll = Random.Range(1, 101); CharacterStats enemyStats = (CharacterStats) enemy.GetComponent<CharacterStats>(); if (Object.op_Equality((Object) enemyStats, (Object) null)) return; int toHitRollOverride = enemyStats.GetAttackerToHitRollOverride(roll); if (this.OnAttackRollCalculated != null) this.OnAttackRollCalculated(((Component) this).get_gameObject(), new CombatEventArgs(damage, ((Component) this).get_gameObject(), enemy)); int num1 = this.CalculateAccuracy(damage.Attack, enemy); int num2 = enemyStats.CalculateDefense(damage.DefendedBy, damage.Attack, ((Component) this).get_gameObject()); if (damage.DefendedBy != CharacterStats.DefenseType.None) { this.ComputeHitAdjustment(toHitRollOverride + num1 - num2, enemyStats, ref damage); if (damage.IsCriticalHit) damage.DamageAmount *= this.CriticalHitMultiplier; else if (damage.IsGraze) damage.DamageAmount *= CharacterStats.GrazeMultiplier; else if (damage.IsMiss) damage.DamageAmount = 0.0f; } damage.AccuracyRating = num1; damage.DefenseRating = num2; damage.RawRoll = toHitRollOverride; if (this.OnAdjustCritGrazeMiss != null) this.OnAdjustCritGrazeMiss(((Component) this).get_gameObject(), new CombatEventArgs(damage, ((Component) this).get_gameObject(), enemy)); if (!damage.IsMiss) { if (damage.Attack.IsDisengagementAttack) damage.DamageAmount += this.DisengagementDamageBonus; if (damage.Attack is AttackMelee) { damage.DamageAmount += this.BonusMeleeDamage; if ((damage.Attack as AttackMelee).Unarmed) damage.DamageAmount += this.BonusUnarmedDamage; } for (int index = 0; index < this.BonusDamage.Length; ++index) { if ((double) this.BonusDamage[index] != 0.0) { DamagePacket.DamageProcType damageProcType = new DamagePacket.DamageProcType((DamagePacket.DamageType) index, this.BonusDamage[index]); damage.Damage.DamageProc.Add(damageProcType); } } this.AddBonusDamagePerType(damage); this.AddBonusDamagePerRace(damage, enemyStats); if (Object.op_Inequality((Object) damage.Attack, (Object) null)) { Equippable equippable = (Equippable) ((Component) damage.Attack).GetComponent<Equippable>(); if (Object.op_Inequality((Object) equippable, (Object) null)) { if (equippable is Weapon) { if (damage.Attack is AttackMelee) { damage.DamageAmount *= this.BonusMeleeWeaponDamageMult; } else { damage.DamageAmount *= this.BonusRangedWeaponDamageMult; if (Object.op_Inequality((Object) enemy, (Object) null) && !this.IsEnemyDistant(enemy)) damage.DamageAmount *= this.BonusRangedWeaponCloseEnemyDamageMult; } } equippable.ApplyItemModDamageProcs(ref damage); } } } this.ComputeInterrupt(enemyStats, ref damage); if (this.m_isPartyMember) { if (Object.op_Implicit((Object) enemyStats)) { enemyStats.RevealDefense(damage.DefendedBy); enemyStats.RevealDT(damage.Damage.Type); using (List<DamagePacket.DamageProcType>.Enumerator enumerator = damage.Damage.DamageProc.GetEnumerator()) { while (enumerator.MoveNext()) { DamagePacket.DamageProcType current = enumerator.Current; enemyStats.RevealDT(current.Type); } } } if (damage.DefenseRating >= damage.AccuracyRating + 50) { GameState.AutoPause(AutoPauseOptions.PauseEvent.ExtraordinaryDefence, ((Component) this).get_gameObject(), enemy); TutorialManager.STriggerTutorialsOfTypeFast(TutorialManager.ExclusiveTriggerType.PARTYMEM_GETS_DEFENSE_TOO_HIGH); } if ((double) damage.MaxDamage - (double) damage.DTRating < (double) damage.MinDamage && Object.op_Inequality((Object) ((Component) damage.Attack).GetComponent<Weapon>(), (Object) null)) GameState.AutoPause(AutoPauseOptions.PauseEvent.WeaponIneffective, ((Component) this).get_gameObject(), enemy); } if (this.OnPostDamageDealt == null) return; this.OnPostDamageDealt(((Component) this).get_gameObject(), new CombatEventArgs(damage, ((Component) this).get_gameObject(), enemy)); } private void AddBonusDamagePerRace(DamageInfo damage, CharacterStats enemyStats) { if (enemyStats.CharacterRace >= (CharacterStats.Race) this.BonusDamagePerRace.Length || (double) this.BonusDamagePerRace[(int) enemyStats.CharacterRace] == 0.0) return; damage.DamageAmount += this.GetBonusDamagePerRace(enemyStats.CharacterRace, damage.DamageAmount); } Now I do not have perfect understanding of the code, and there could be connected methods that I don't know about, but what I think I'm seeing here is that damage multipliers are individually multiplying the base damage, instead of being added together in a formula like Josh presented on tumblr. I have shown this code to four other people and they agree that this looks like how it's working.To test damage multiplier stacking, I am using The Rogue Class Human or Aumaua to achieve High Might Living Lands Culture 18-20 Might score Blinding Strike ability Weapon Focus talent (whichever weapon I'm testing) Dirty Fighting and Vicious Fighting talents to score easier critical hits Have mostly been testing with 1H Normal style weapons, usually dual wielding Here are some screenshots of the strange results I've been getting. I know for a fact that there is something definitely off about spears. They seem to be doing the most damage of any of the weapons currently, and it could be a separate issue. You can create a character from scratch, equip two fine spears and use Blinding Strike on a party member or any other unit and regularly score 70 damage critical hits (before DR). Here is a Fine Sword crit for 70.7 damage (20 Might Aumaua Rogue). There is something strange about this one because it says Medreth only has 21 Deflection :/ I'm not sure why, it should be 40-something, unless the Bliding affliction was applied before the damage hit or something. Fine Spear crit hit for 70.8 damage (max should be 38.24) 45.3 damage with a Fine Sabre crit with Crippling Strike (18 Might Orlan Rogue) 47.9 damage sword HIT with Blinding Strike (Aumaua Rogue 20 Might) 44.8 damage sword HIT on the BB Fighter with Blinding Strike 41.9 damage War Hammer crit on Crippling Strike (Human 18 Might, max should be 38.24) Another instance - same character of 42.4 Fine Sabre crit of 49.3 on a Crippling Strike (18 Might Human, max should be 38.24) You get the picture.I am not 100% sure what the problem is, I suspect some of it might be related to the way in which damage is calculated in the source code, but it's possible that some of these are separate bugs and not the same bug.For instance, the spear screenshot is from a freshly launched game with a fresh character, no save/load or anything. Some of the others are from saved games.As I learn new information, I will post it. Edited January 12, 201511 yr by Sensuki
January 12, 201511 yr Thanks for digging this up. Looking at the code, I agree that the multipliers are not applied in the right order (as I expected all along as well). What you can see from the code is that the formula is something like [(BaseDamage * (1+Might_Mult) *(1+ Crit/Miss_Mult) + Disengagement_Bonus + Melee/Unarmed/Ranged_Bonus )*(1+Melee/Unarmed/Ranged_Mult) - DT] * (1+Racial_Mult) It's unclear whether the damage bonus from enhancements or support spells goes into the base damage or anywhere else. Edited January 12, 201511 yr by Doppelschwert
January 12, 201511 yr Doppleshwert formula is accurate with the code from what I can see. I'm wondering why the attack roll is using a 1-101 range instead of 1-100 though (the function is inclusive in Unity). And that explain how a rogue can get more than x3.0 instead of x2.39, heheheh. Edited January 12, 201511 yr by morhilane Azarhal, Chanter and Keeper of Truth of the Obsidian Order of Eternity.
January 12, 201511 yr Author That's just how unity handles their .random function for integers. For floats it's the starting and ending number
January 12, 201511 yr I know how the function works, that's why I'm wondering why 101 is a possible value for an attack roll. I though it was supposed to be 1-100. Not that it change anything to the maths. Azarhal, Chanter and Keeper of Truth of the Obsidian Order of Eternity.
January 12, 201511 yr I know how the function works, that's why I'm wondering why 101 is a possible value for an attack roll. I though it was supposed to be 1-100. Not that it change anything to the maths. Come on, that's an extra 1% ish bonus to Crit Chance that you're trying to quash...
January 12, 201511 yr Yep, they need to sum the some of the mult mods before *= them. Needs an extra local variable. And since they're going to do that.... Why not subtract DT before applying Crit Mods (and sneak attack? ! , it'd just be a cut and paste Actually, if they do sum modifiers instead of doing a cumulative product, it'd probably be easier to balance things. I'm just trying to get some differentiation between Might and Per as stats.
January 12, 201511 yr Author I know how the function works, that's why I'm wondering why 101 is a possible value for an attack roll. I though it was supposed to be 1-100. Not that it change anything to the maths. It isn't. The engine for some reason requires you to state one higher than the maximum integer as the range . 100 is the maximum in the game.
January 12, 201511 yr I know how the function works, that's why I'm wondering why 101 is a possible value for an attack roll. I though it was supposed to be 1-100. Not that it change anything to the maths. It isn't. The engine for some reason requires you to state one higher than the maximum integer as the range . 100 is the maximum in the game. OMG, you are right. What kind of morons doesn't keep his function behavior the same regardless of types? There is no point for it to work different between floats and integers. Azarhal, Chanter and Keeper of Truth of the Obsidian Order of Eternity.