diff --git a/sim/core/apl.go b/sim/core/apl.go index aac8e9cc5d..a4a0d484eb 100644 --- a/sim/core/apl.go +++ b/sim/core/apl.go @@ -400,7 +400,7 @@ func (apl *APLRotation) DoNextAction(sim *Simulation) { return } - if apl.unit.IsChanneling() && !apl.unit.ChanneledDot.Spell.Flags.Matches(SpellFlagCastWhileChanneling) { + if apl.unit.IsChanneling() { return } diff --git a/sim/core/armor.go b/sim/core/armor.go index e46f3c5713..13f4819288 100644 --- a/sim/core/armor.go +++ b/sim/core/armor.go @@ -5,13 +5,13 @@ func (result *SpellResult) applyArmor(spell *Spell, isPeriodic bool, attackTable result.Damage *= armorMitigationMultiplier - result.ArmorMultiplier = armorMitigationMultiplier - result.PostArmorDamage = result.Damage + result.ArmorAndResistanceMultiplier = armorMitigationMultiplier + result.PostArmorAndResistanceMultiplier = result.Damage } // Returns Armor mitigation fraction for the spell func (spell *Spell) armorMultiplier(isPeriodic bool, attackTable *AttackTable) float64 { - if spell.Flags.Matches(SpellFlagIgnoreArmor) { + if spell.Flags.Matches(SpellFlagIgnoreResists) { return 1 } diff --git a/sim/core/attack.go b/sim/core/attack.go index 3c9baa3e39..190d4f366b 100644 --- a/sim/core/attack.go +++ b/sim/core/attack.go @@ -443,7 +443,7 @@ func (unit *Unit) EnableAutoAttacks(agent Agent, options AutoAttackOptions) { ActionID: ActionID{OtherID: proto.OtherAction_OtherActionAttack, Tag: 2}, SpellSchool: options.OffHand.GetSpellSchool(), ProcMask: Ternary(options.ProcMask == ProcMaskUnknown, ProcMaskMeleeOHAuto, options.ProcMask), - Flags: SpellFlagMeleeMetrics | SpellFlagNoOnCastComplete, + Flags: SpellFlagMeleeMetrics | SpellFlagIncludeTargetBonusDamage | SpellFlagNoOnCastComplete, DamageMultiplier: 1, DamageMultiplierAdditive: 1, @@ -463,7 +463,7 @@ func (unit *Unit) EnableAutoAttacks(agent Agent, options AutoAttackOptions) { ActionID: ActionID{OtherID: proto.OtherAction_OtherActionShoot}, SpellSchool: options.Ranged.GetSpellSchool(), ProcMask: Ternary(options.ProcMask == ProcMaskUnknown, ProcMaskRangedAuto, options.ProcMask), - Flags: SpellFlagMeleeMetrics | SpellFlagRanged, + Flags: SpellFlagMeleeMetrics | SpellFlagIncludeTargetBonusDamage, MissileSpeed: 40, DamageMultiplier: 1, diff --git a/sim/core/aura_helpers.go b/sim/core/aura_helpers.go index 6a6db5cf69..cf3ab858eb 100644 --- a/sim/core/aura_helpers.go +++ b/sim/core/aura_helpers.go @@ -659,7 +659,7 @@ func (unit *Unit) NewDamageAbsorptionAura(config AbsorptionAuraConfig) *DamageAb }) extraSpellCheck := func(sim *Simulation, spell *Spell, result *SpellResult, isPeriodic bool) bool { - return !spell.Flags.Matches(SpellFlagBypassAbsorbs) && ((config.ShouldApplyToResult == nil) || config.ShouldApplyToResult(sim, spell, result, isPeriodic)) + return ((config.ShouldApplyToResult == nil) || config.ShouldApplyToResult(sim, spell, result, isPeriodic)) } unit.AddDynamicDamageTakenModifier(func(sim *Simulation, spell *Spell, result *SpellResult, isPeriodic bool) { diff --git a/sim/core/cast.go b/sim/core/cast.go index 96b53fca17..27666accda 100644 --- a/sim/core/cast.go +++ b/sim/core/cast.go @@ -155,7 +155,7 @@ func (spell *Spell) makeCastFunc(config CastConfig) CastSuccessFunc { return spell.castFailureHelper(sim, "casting/channeling %v for %s, curTime = %s", hc.ActionID, hc.Expires-sim.CurrentTime, sim.CurrentTime) } - if spell.Unit.IsCastingDuringChannel() && !spell.CanCastDuringChannel(sim) { + if !spell.CanCastDuringChannel(sim) { return spell.castFailureHelper(sim, "cannot interrupt in-progress channel of %v with a cast of %v", spell.Unit.ChanneledDot.ActionID, spell.ActionID) } diff --git a/sim/core/consumes.go b/sim/core/consumes.go index ffa880e3b6..3ca7865393 100644 --- a/sim/core/consumes.go +++ b/sim/core/consumes.go @@ -323,86 +323,86 @@ func registerExplosivesCD(agent Agent, consumes *proto.ConsumesSpec) { return } switch consumes.ExplosiveId { - case 89637: - bomb := character.GetOrRegisterSpell(SpellConfig{ - ActionID: BigDaddyActionID, - SpellSchool: SpellSchoolFire, - ProcMask: ProcMaskEmpty, - Flags: SpellFlagAoE, - - Cast: CastConfig{ - CD: Cooldown{ - Timer: character.NewTimer(), - Duration: time.Minute, - }, - - DefaultCast: Cast{ - CastTime: time.Millisecond * 500, - }, - - ModifyCast: func(sim *Simulation, spell *Spell, cast *Cast) { - spell.Unit.AutoAttacks.StopMeleeUntil(sim, sim.CurrentTime) - spell.Unit.AutoAttacks.StopRangedUntil(sim, sim.CurrentTime) - }, - }, - - // Explosives always have 1% resist chance, so just give them hit cap. - BonusHitPercent: 100, - DamageMultiplier: 1, - CritMultiplier: 2, - ThreatMultiplier: 1, - - ApplyEffects: func(sim *Simulation, _ *Unit, spell *Spell) { - spell.CalcAndDealAoeDamage(sim, 5006, spell.OutcomeMagicHitAndCrit) - }, - }) - - character.AddMajorCooldown(MajorCooldown{ - Spell: bomb, - Type: CooldownTypeDPS | CooldownTypeExplosive, - Priority: CooldownPriorityLow + 10, - }) - case 40771: - boltGun := character.GetOrRegisterSpell(SpellConfig{ - ActionID: ActionID{SpellID: 82207}, - SpellSchool: SpellSchoolFire, - ProcMask: ProcMaskEmpty, - Flags: SpellFlagNoOnCastComplete | SpellFlagCanCastWhileMoving, - - Cast: CastConfig{ - DefaultCast: Cast{ - GCD: GCDDefault, - CastTime: time.Second, - }, - IgnoreHaste: true, - CD: Cooldown{ - Timer: character.NewTimer(), - Duration: time.Minute * 2, - }, - SharedCD: Cooldown{ - Timer: character.GetOffensiveTrinketCD(), - Duration: time.Second * 15, - }, - }, - - // Explosives always have 1% resist chance, so just give them hit cap. - BonusHitPercent: 100, - DamageMultiplier: 1, - CritMultiplier: 2, - ThreatMultiplier: 1, - - ApplyEffects: func(sim *Simulation, target *Unit, spell *Spell) { - spell.CalcAndDealDamage(sim, target, 8860, spell.OutcomeMagicHitAndCrit) - }, - }) - - character.AddMajorCooldown(MajorCooldown{ - Spell: boltGun, - Type: CooldownTypeDPS | CooldownTypeExplosive, - Priority: CooldownPriorityLow + 10, - ShouldActivate: func(s *Simulation, c *Character) bool { - return false // Intentionally not automatically used - }, - }) + // case 89637: + // bomb := character.GetOrRegisterSpell(SpellConfig{ + // ActionID: BigDaddyActionID, + // SpellSchool: SpellSchoolFire, + // ProcMask: ProcMaskEmpty, + // Flags: SpellFlagAoE, + + // Cast: CastConfig{ + // CD: Cooldown{ + // Timer: character.NewTimer(), + // Duration: time.Minute, + // }, + + // DefaultCast: Cast{ + // CastTime: time.Millisecond * 500, + // }, + + // ModifyCast: func(sim *Simulation, spell *Spell, cast *Cast) { + // spell.Unit.AutoAttacks.StopMeleeUntil(sim, sim.CurrentTime) + // spell.Unit.AutoAttacks.StopRangedUntil(sim, sim.CurrentTime) + // }, + // }, + + // // Explosives always have 1% resist chance, so just give them hit cap. + // BonusHitPercent: 100, + // DamageMultiplier: 1, + // CritMultiplier: 2, + // ThreatMultiplier: 1, + + // ApplyEffects: func(sim *Simulation, _ *Unit, spell *Spell) { + // spell.CalcAndDealAoeDamage(sim, 5006, spell.OutcomeMagicHitAndCrit) + // }, + // }) + + // character.AddMajorCooldown(MajorCooldown{ + // Spell: bomb, + // Type: CooldownTypeDPS | CooldownTypeExplosive, + // Priority: CooldownPriorityLow + 10, + // }) + // case 40771: + // boltGun := character.GetOrRegisterSpell(SpellConfig{ + // ActionID: ActionID{SpellID: 82207}, + // SpellSchool: SpellSchoolFire, + // ProcMask: ProcMaskEmpty, + // Flags: SpellFlagNoOnCastComplete | SpellFlagCanCastWhileMoving, + + // Cast: CastConfig{ + // DefaultCast: Cast{ + // GCD: GCDDefault, + // CastTime: time.Second, + // }, + // IgnoreHaste: true, + // CD: Cooldown{ + // Timer: character.NewTimer(), + // Duration: time.Minute * 2, + // }, + // SharedCD: Cooldown{ + // Timer: character.GetOffensiveTrinketCD(), + // Duration: time.Second * 15, + // }, + // }, + + // // Explosives always have 1% resist chance, so just give them hit cap. + // BonusHitPercent: 100, + // DamageMultiplier: 1, + // CritMultiplier: 2, + // ThreatMultiplier: 1, + + // ApplyEffects: func(sim *Simulation, target *Unit, spell *Spell) { + // spell.CalcAndDealDamage(sim, target, 8860, spell.OutcomeMagicHitAndCrit) + // }, + // }) + + // character.AddMajorCooldown(MajorCooldown{ + // Spell: boltGun, + // Type: CooldownTypeDPS | CooldownTypeExplosive, + // Priority: CooldownPriorityLow + 10, + // ShouldActivate: func(s *Simulation, c *Character) bool { + // return false // Intentionally not automatically used + // }, + // }) } } diff --git a/sim/core/dot_test.go b/sim/core/dot_test.go index 698736df43..cfbdc04f45 100644 --- a/sim/core/dot_test.go +++ b/sim/core/dot_test.go @@ -56,7 +56,7 @@ func NewFakeElementalShaman(char *Character, _ *proto.Player) Agent { ActionID: ActionID{SpellID: 42}, SpellSchool: SpellSchoolShadow, ProcMask: ProcMaskSpellDamage, - Flags: SpellFlagIgnoreArmor, + Flags: SpellFlagIgnoreResists, Cast: CastConfig{}, BonusCritPercent: 3, diff --git a/sim/core/flags.go b/sim/core/flags.go index e1f052c96e..f9d5f7c1e0 100644 --- a/sim/core/flags.go +++ b/sim/core/flags.go @@ -2,6 +2,7 @@ package core import ( "github.com/wowsims/tbc/sim/core/proto" + "github.com/wowsims/tbc/sim/core/stats" ) //go:generate stringer -type=ProcMask @@ -114,10 +115,16 @@ const ( // These bits are set by the crit and damage rolls. OutcomeCrit OutcomeCrush + + OutcomePartial1 + OutcomePartial2 + OutcomePartial4 + OutcomePartial8 ) const ( - OutcomeLanded = OutcomeHit | OutcomeCrit | OutcomeCrush | OutcomeGlance | OutcomeBlock + OutcomePartial = OutcomePartial1 | OutcomePartial2 | OutcomePartial4 | OutcomePartial8 + OutcomeLanded = OutcomeHit | OutcomeCrit | OutcomeCrush | OutcomeGlance | OutcomeBlock ) func (ho HitOutcome) String() string { @@ -148,6 +155,18 @@ func (ho HitOutcome) String() string { } } +func (ho HitOutcome) PartialResistString() string { + if ho.Matches(OutcomePartial1) { + return " (30% Resist)" + } else if ho.Matches(OutcomePartial2) { + return " (20% Resist)" + } else if ho.Matches(OutcomePartial4) { + return " (10% Resist)" + } else { + return "" + } +} + // Other flags type SpellFlag uint64 @@ -157,39 +176,36 @@ func (se SpellFlag) Matches(other SpellFlag) bool { } const ( - SpellFlagNone SpellFlag = 0 - SpellFlagIgnoreArmor SpellFlag = 1 << iota // skip armor - SpellFlagIgnoreTargetModifiers // skip target damage modifiers - SpellFlagIgnoreAttackerModifiers // skip attacker damage modifiers - SpellFlagApplyArmorReduction // Forces damage reduction from armor to apply, even if it otherwise wouldn't. - SpellFlagCannotBeDodged // Ignores dodge in physical hit rolls - SpellFlagBinary // Does not do partial resists and could need a different hit roll. - SpellFlagBypassAbsorbs // Prevents any active DamageAbsorptionAuras from applying their damage reduction effects. - SpellFlagChanneled // Spell is channeled - SpellFlagCastWhileChanneling // Spell can be cast while channeling. If SpellFlagChanneled and SpellFlagCastWhileChanneling are both set, it means that other spells with the SpellFlagCastWhileChanneling flag can be cast without interrupting the channeled spell. - SpellFlagDisease // Spell is categorized as disease - SpellFlagHelpful // For healing spells / buffs. - SpellFlagMeleeMetrics // Marks a spell as a melee ability for metrics. - SpellFlagNoOnCastComplete // Disables the OnCastComplete callback. - SpellFlagNoMetrics // Disables metrics for a spell. - SpellFlagNoLogs // Disables logs for a spell. - SpellFlagAPL // Indicates this spell can be used from an APL rotation. - SpellFlagMCD // Indicates this spell is a MajorCooldown. - SpellFlagReactive // Allows a spell flagged as an MCD to be cast off-GCD. Used for instant cast defensive CDs. - SpellFlagNoOnDamageDealt // Disables OnSpellHitDealt and OnPeriodicDamageDealt aura callbacks for this spell. - SpellFlagPrepullOnly // Indicates this spell should only be used during prepull. Not enforced, just a signal for the APL UI. - SpellFlagEncounterOnly // Indicates this spell should only be used during the encounter (not prepull). Not enforced, just a signal for the APL UI. - SpellFlagPotion // Indicates this spell is a potion spell. - SpellFlagPrepullPotion // Indicates this spell is the prepull potion. - SpellFlagCombatPotion // Indicates this spell is the combat potion. - SpellFlagNoSpellMods // Indicates that no spell mods should be applied to this spell - SpellFlagCanCastWhileMoving // Allows the cast to be casted while moving - SpellFlagPassiveSpell // Indicates this spell is applied/cast as a result of another spell - SpellFlagSupressDoTApply // If present this spell will not apply dots (Used for DTR dot supression) - SpellFlagSwapped // Indicates that this spell is not useable because it is from a currently swapped item - SpellFlagAoE // Indicates that this spell is an AoE spell. Spells flagged with this will use the AoE Cap multiplier when calculating damage. - SpellFlagRanged // Indicates that this spell is a ranged spell. Spells flagged with this will have increased damage when Hunters Mark is active. - SpellFlagReadinessTrinket // Indicates that this spell part of Readiness. Used by Siege of Orgrimmar CDR trinkets. + SpellFlagNone SpellFlag = 0 + SpellFlagIgnoreResists SpellFlag = 1 << iota // skip spell resist/armor + SpellFlagIgnoreTargetModifiers // skip target damage modifiers + SpellFlagIgnoreAttackerModifiers // skip attacker damage modifiers + SpellFlagApplyArmorReduction // Forces damage reduction from armor to apply, even if it otherwise wouldn't. + SpellFlagCannotBeDodged // Ignores dodge in physical hit rolls + SpellFlagIncludeTargetBonusDamage // Spell benefits from Gift of Arthas and Hemorrhage. + SpellFlagBinary // Does not do partial resists and could need a different hit roll. + SpellFlagChanneled // Spell is channeled + SpellFlagDisease // Spell is categorized as disease + SpellFlagHauntSE // Spell benefits from haunt/SE effects + SpellFlagHelpful // For healing spells / buffs. + SpellFlagMeleeMetrics // Marks a spell as a melee ability for metrics. + SpellFlagNoOnCastComplete // Disables the OnCastComplete callback. + SpellFlagNoMetrics // Disables metrics for a spell. + SpellFlagNoLogs // Disables logs for a spell. + SpellFlagAPL // Indicates this spell can be used from an APL rotation. + SpellFlagMCD // Indicates this spell is a MajorCooldown. + SpellFlagReactive // Allows a spell flagged as an MCD to be cast off-GCD. Used for instant cast defensive CDs. + SpellFlagNoOnDamageDealt // Disables OnSpellHitDealt and OnPeriodicDamageDealt aura callbacks for this spell. + SpellFlagPrepullOnly // Indicates this spell should only be used during prepull. Not enforced, just a signal for the APL UI. + SpellFlagEncounterOnly // Indicates this spell should only be used during the encounter (not prepull). Not enforced, just a signal for the APL UI. + SpellFlagPotion // Indicates this spell is a potion spell. + SpellFlagPrepullPotion // Indicates this spell is the prepull potion. + SpellFlagCombatPotion // Indicates this spell is the combat potion. + SpellFlagNoSpellMods // Indicates that no spell mods should be applied to this spell + SpellFlagCanCastWhileMoving // Allows the cast to be casted while moving + SpellFlagPassiveSpell // Indicates this spell is applied/cast as a result of another spell + SpellFlagSupressDoTApply // If present this spell will not apply dots (Used for DTR dot supression) + SpellFlagSwapped // Indicates that this spell is not useable because it is from a currently swapped item // Used to let agents categorize their spells. SpellFlagAgentReserved1 @@ -225,6 +241,27 @@ func (ss SpellSchool) Matches(other SpellSchool) bool { return (ss & other) != 0 } +func (ss SpellSchool) ResistanceStat() stats.Stat { + switch ss { + case SpellSchoolPhysical: + return stats.ArmorPenetration + case SpellSchoolArcane: + return stats.ArcaneResistance + case SpellSchoolFire: + return stats.FireResistance + case SpellSchoolFrost: + return stats.FrostResistance + case SpellSchoolHoly: + return 0 // Holy resistance doesn't exist. + case SpellSchoolNature: + return stats.NatureResistance + case SpellSchoolShadow: + return stats.ShadowResistance + default: + return 0 // This applies to spell school combinations, which supposedly use the "path of the least resistance", so 0 is a good fit. + } +} + func SpellSchoolFromProto(p proto.SpellSchool) SpellSchool { switch p { case proto.SpellSchool_SpellSchoolPhysical: diff --git a/sim/core/spell.go b/sim/core/spell.go index e38a4dee55..e7338b7731 100644 --- a/sim/core/spell.go +++ b/sim/core/spell.go @@ -593,7 +593,7 @@ func (spell *Spell) CanCast(sim *Simulation, target *Unit) bool { } // While casting or channeling, no other action is possible - if (spell.Unit.Hardcast.Expires > sim.CurrentTime) || (spell.Unit.IsCastingDuringChannel() && !spell.CanCastDuringChannel(sim)) { + if (spell.Unit.Hardcast.Expires > sim.CurrentTime) || !spell.CanCastDuringChannel(sim) { //if sim.Log != nil { // sim.Log("Cant cast because already casting/channeling") //} @@ -673,15 +673,6 @@ func (spell *Spell) CanCompleteCast(sim *Simulation, target *Unit, logCastFailur } func (spell *Spell) CanCastDuringChannel(sim *Simulation) bool { - // Don't allow bypassing of channel clip logic for re-casts of the same channel - if spell == spell.Unit.ChanneledDot.Spell { - return false - } - - if spell.Flags.Matches(SpellFlagCastWhileChanneling) { - return true - } - return false } diff --git a/sim/core/spell_mod.go b/sim/core/spell_mod.go index ac7b90b860..56cc25493d 100644 --- a/sim/core/spell_mod.go +++ b/sim/core/spell_mod.go @@ -442,11 +442,6 @@ var spellModMap = map[SpellModType]*SpellModFunctions{ Remove: removeAllowCastWhileMoving, }, - SpellMod_AllowCastWhileChanneling: { - Apply: applyAllowCastWhileChanneling, - Remove: removeAllowCastWhileChanneling, - }, - SpellMod_BonusSpellPower_Flat: { Apply: applyBonusSpellPowerFlat, Remove: removeBonusSpellPowerFlat, @@ -667,14 +662,6 @@ func removeAllowCastWhileMoving(mod *SpellMod, spell *Spell) { spell.Flags ^= SpellFlagCanCastWhileMoving } -func applyAllowCastWhileChanneling(mod *SpellMod, spell *Spell) { - spell.Flags |= SpellFlagCastWhileChanneling -} - -func removeAllowCastWhileChanneling(mod *SpellMod, spell *Spell) { - spell.Flags ^= SpellFlagCastWhileChanneling -} - func applyBonusSpellPowerFlat(mod *SpellMod, spell *Spell) { spell.BonusSpellPower += mod.floatValue } diff --git a/sim/core/spell_outcome.go b/sim/core/spell_outcome.go index a894edaa76..ff4a023316 100644 --- a/sim/core/spell_outcome.go +++ b/sim/core/spell_outcome.go @@ -288,13 +288,11 @@ func (spell *Spell) outcomeMeleeWhite(sim *Simulation, result *SpellResult, atta if unit.PseudoStats.InFrontOfTarget { if !result.applyAttackTableMiss(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && - !result.applyAttackTableParry(spell, attackTable, roll, &chance) { - if result.applyAttackTableGlance(spell, attackTable, roll, &chance) || - result.applyAttackTableCrit(spell, attackTable, roll, &chance, countHits) { - result.applyAttackTableBlock(sim, spell, attackTable) - } else if !result.applyAttackTableBlock(sim, spell, attackTable) { - result.applyAttackTableHit(spell, countHits) - } + !result.applyAttackTableParry(spell, attackTable, roll, &chance) && + !result.applyAttackTableGlance(spell, attackTable, roll, &chance) && + !result.applyAttackTableBlock(spell, attackTable, roll, &chance) && + !result.applyAttackTableCrit(spell, attackTable, roll, &chance, countHits) { + result.applyAttackTableHit(spell, countHits) } } else { if !result.applyAttackTableMiss(spell, attackTable, roll, &chance) && @@ -313,18 +311,16 @@ func (spell *Spell) OutcomeMeleeWhiteNoGlance(sim *Simulation, result *SpellResu if unit.PseudoStats.InFrontOfTarget { if !result.applyAttackTableMiss(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && - !result.applyAttackTableParry(spell, attackTable, roll, &chance) { - if result.applyAttackTableCrit(spell, attackTable, roll, &chance, true) { - result.applyAttackTableBlock(sim, spell, attackTable) - } else if !result.applyAttackTableBlock(sim, spell, attackTable) { - result.applyAttackTableHit(spell, true) - } - } - } else { - if !result.applyAttackTableMiss(spell, attackTable, roll, &chance) && - !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && + !result.applyAttackTableParry(spell, attackTable, roll, &chance) && + !result.applyAttackTableBlock(spell, attackTable, roll, &chance) && !result.applyAttackTableCrit(spell, attackTable, roll, &chance, true) { result.applyAttackTableHit(spell, true) + } else { + if !result.applyAttackTableMiss(spell, attackTable, roll, &chance) && + !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && + !result.applyAttackTableCrit(spell, attackTable, roll, &chance, true) { + result.applyAttackTableHit(spell, true) + } } } } @@ -343,8 +339,7 @@ func (spell *Spell) outcomeMeleeSpecialHit(sim *Simulation, result *SpellResult, if unit.PseudoStats.InFrontOfTarget { if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && - !result.applyAttackTableParry(spell, attackTable, roll, &chance) && - !result.applyAttackTableBlock(sim, spell, attackTable) { + !result.applyAttackTableParry(spell, attackTable, roll, &chance) { result.applyAttackTableHit(spell, countHits) } } else { @@ -371,9 +366,11 @@ func (spell *Spell) outcomeMeleeSpecialHitAndCrit(sim *Simulation, result *Spell !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyAttackTableParry(spell, attackTable, roll, &chance) { if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { - result.applyAttackTableBlock(sim, spell, attackTable) - } else if !result.applyAttackTableBlock(sim, spell, attackTable) { - result.applyAttackTableHit(spell, countHits) + result.applyAttackTableBlock(spell, attackTable, roll, &chance) + } else { + if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) { + result.applyAttackTableHit(spell, countHits) + } } } } else { @@ -399,7 +396,7 @@ func (spell *Spell) outcomeMeleeWeaponSpecialNoParry(sim *Simulation, result *Sp if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && - !result.applyAttackTableBlock(sim, spell, attackTable) && + !result.applyAttackTableBlock(spell, attackTable, roll, &chance) && !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { result.applyAttackTableHit(spell, countHits) } @@ -424,7 +421,7 @@ func (spell *Spell) outcomeMeleeWeaponSpecialHitAndCrit(sim *Simulation, result if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyAttackTableParry(spell, attackTable, roll, &chance) && - !result.applyAttackTableBlock(sim, spell, attackTable) && + !result.applyAttackTableBlock(spell, attackTable, roll, &chance) && !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { result.applyAttackTableHit(spell, countHits) } @@ -448,7 +445,7 @@ func (spell *Spell) outcomeMeleeWeaponSpecialNoCrit(sim *Simulation, result *Spe if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && !result.applyAttackTableParry(spell, attackTable, roll, &chance) && - !result.applyAttackTableBlock(sim, spell, attackTable) { + !result.applyAttackTableBlock(spell, attackTable, roll, &chance) { result.applyAttackTableHit(spell, countHits) } } else { @@ -511,9 +508,11 @@ func (spell *Spell) OutcomeMeleeSpecialBlockAndCritNoHitCounter(sim *Simulation, func (spell *Spell) outcomeMeleeSpecialBlockAndCrit(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { if spell.Unit.PseudoStats.InFrontOfTarget { - if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { - result.applyAttackTableBlock(sim, spell, attackTable) - } else if !result.applyAttackTableBlock(sim, spell, attackTable) { + roll := sim.RandomFloat("White Hit Table") + chance := 0.0 + + if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) && + !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { result.applyAttackTableHit(spell, countHits) } } else { @@ -533,8 +532,7 @@ func (spell *Spell) outcomeRangedHit(sim *Simulation, result *SpellResult, attac roll := sim.RandomFloat("White Hit Table") chance := 0.0 - if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && - !result.applyAttackTableDodge(spell, attackTable, roll, &chance) { + if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) { result.applyAttackTableHit(spell, countHits) } } @@ -549,12 +547,22 @@ func (spell *Spell) outcomeRangedHitAndCrit(sim *Simulation, result *SpellResult roll := sim.RandomFloat("White Hit Table") chance := 0.0 - if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && - !result.applyAttackTableDodge(spell, attackTable, roll, &chance) && - !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { - result.applyAttackTableHit(spell, countHits) + if spell.Unit.PseudoStats.InFrontOfTarget { + if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) { + if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableBlock(spell, attackTable, roll, &chance) + } else { + if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) { + result.applyAttackTableHit(spell, countHits) + } + } + } + } else { + if !result.applyAttackTableMissNoDWPenalty(spell, attackTable, roll, &chance) && + !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) + } } - } func (dot *Dot) OutcomeRangedHitAndCritSnapshot(sim *Simulation, result *SpellResult, attackTable *AttackTable) { @@ -567,10 +575,21 @@ func (dot *Dot) outcomeRangedHitAndCritSnapshot(sim *Simulation, result *SpellRe roll := sim.RandomFloat("White Hit Table") chance := 0.0 - if !result.applyAttackTableMissNoDWPenalty(dot.Spell, attackTable, roll, &chance) && - !result.applyAttackTableDodge(dot.Spell, attackTable, roll, &chance) && - !result.applyAttackTableCritSeparateRollSnapshot(sim, dot) { - result.applyAttackTableHit(dot.Spell, countHits) + if dot.Spell.Unit.PseudoStats.InFrontOfTarget { + if !result.applyAttackTableMissNoDWPenalty(dot.Spell, attackTable, roll, &chance) { + if result.applyAttackTableCritSeparateRollSnapshot(sim, dot) { + result.applyAttackTableBlock(dot.Spell, attackTable, roll, &chance) + } else { + if !result.applyAttackTableBlock(dot.Spell, attackTable, roll, &chance) { + result.applyAttackTableHit(dot.Spell, countHits) + } + } + } + } else { + if !result.applyAttackTableMissNoDWPenalty(dot.Spell, attackTable, roll, &chance) && + !result.applyAttackTableCritSeparateRollSnapshot(sim, dot) { + result.applyAttackTableHit(dot.Spell, countHits) + } } } @@ -597,9 +616,22 @@ func (spell *Spell) OutcomeRangedCritOnlyNoHitCounter(sim *Simulation, result *S spell.outcomeRangedCritOnly(sim, result, attackTable, false) } func (spell *Spell) outcomeRangedCritOnly(sim *Simulation, result *SpellResult, attackTable *AttackTable, countHits bool) { + // Block already checks for this, but we can skip the RNG roll which is expensive. + if spell.Unit.PseudoStats.InFrontOfTarget { + roll := sim.RandomFloat("White Hit Table") + chance := 0.0 - if !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { - result.applyAttackTableHit(spell, countHits) + if result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableBlock(spell, attackTable, roll, &chance) + } else { + if !result.applyAttackTableBlock(spell, attackTable, roll, &chance) { + result.applyAttackTableHit(spell, countHits) + } + } + } else { + if !result.applyAttackTableCritSeparateRoll(sim, spell, attackTable, countHits) { + result.applyAttackTableHit(spell, countHits) + } } } @@ -615,12 +647,10 @@ func (spell *Spell) outcomeEnemyMeleeWhite(sim *Simulation, result *SpellResult, if !result.applyEnemyAttackTableMiss(spell, attackTable, roll, &chance) && !result.applyEnemyAttackTableDodge(spell, attackTable, roll, &chance) && - !result.applyEnemyAttackTableParry(spell, attackTable, roll, &chance) { - if result.applyEnemyAttackTableCrit(spell, attackTable, roll, &chance, countHits) { - result.applyEnemyAttackTableBlock(sim, spell, attackTable) - } else if !result.applyEnemyAttackTableBlock(sim, spell, attackTable) { - result.applyAttackTableHit(spell, countHits) - } + !result.applyEnemyAttackTableParry(spell, attackTable, roll, &chance) && + !result.applyEnemyAttackTableBlock(sim, spell, attackTable, roll, &chance) && + !result.applyEnemyAttackTableCrit(spell, attackTable, roll, &chance, countHits) { + result.applyAttackTableHit(spell, countHits) } } @@ -663,26 +693,18 @@ func (result *SpellResult) applyAttackTableMissNoDWPenalty(spell *Spell, attackT return false } -func (result *SpellResult) applyAttackTableBlock(sim *Simulation, spell *Spell, attackTable *AttackTable) bool { - chance := attackTable.BaseBlockChance +func (result *SpellResult) applyAttackTableBlock(spell *Spell, attackTable *AttackTable, roll float64, chance *float64) bool { + *chance += attackTable.BaseBlockChance - if sim.RandomFloat("Block Roll") < chance { + if roll < *chance { result.Outcome |= OutcomeBlock if result.DidCrit() { - // Subtract Crits because they happen before Blocks - spell.SpellMetrics[result.Target.UnitIndex].Crits-- spell.SpellMetrics[result.Target.UnitIndex].CritBlocks++ - } else if result.DidGlance() { - // Subtract Glances because they happen before Blocks - spell.SpellMetrics[result.Target.UnitIndex].Glances-- - spell.SpellMetrics[result.Target.UnitIndex].GlanceBlocks++ - } else { spell.SpellMetrics[result.Target.UnitIndex].Blocks++ } damageReduced := result.Damage * (1 - result.Target.BlockDamageReduction()) result.Damage = max(0, damageReduced) - return true } return false @@ -693,7 +715,7 @@ func (result *SpellResult) applyAttackTableDodge(spell *Spell, attackTable *Atta return false } - *chance += max(0, attackTable.BaseDodgeChance-spell.DodgeSuppression()) + *chance += max(0, attackTable.BaseDodgeChance-spell.DodgeParrySuppression()-spell.Unit.PseudoStats.DodgeReduction) if roll < *chance { result.Outcome = OutcomeDodge @@ -705,7 +727,7 @@ func (result *SpellResult) applyAttackTableDodge(spell *Spell, attackTable *Atta } func (result *SpellResult) applyAttackTableParry(spell *Spell, attackTable *AttackTable, roll float64, chance *float64) bool { - *chance += max(0, attackTable.BaseParryChance-spell.ParrySuppression(attackTable)) + *chance += max(0, attackTable.BaseParryChance-spell.DodgeParrySuppression()) if roll < *chance { result.Outcome = OutcomeParry @@ -797,26 +819,16 @@ func (result *SpellResult) applyEnemyAttackTableMiss(spell *Spell, attackTable * return false } -func (result *SpellResult) applyEnemyAttackTableBlock(sim *Simulation, spell *Spell, attackTable *AttackTable) bool { +func (result *SpellResult) applyEnemyAttackTableBlock(sim *Simulation, spell *Spell, attackTable *AttackTable, roll float64, chance *float64) bool { if !result.Target.PseudoStats.CanBlock || result.Target.PseudoStats.Stunned { return false } - chance := result.Target.GetTotalBlockChanceAsDefender(attackTable) + *chance += result.Target.GetTotalBlockChanceAsDefender(attackTable) - if sim.RandomFloat("Player Block") < chance { + if roll < *chance { result.Outcome |= OutcomeBlock - if result.DidCrit() { - // Subtract Crits because they happen before Blocks - spell.SpellMetrics[result.Target.UnitIndex].Crits-- - spell.SpellMetrics[result.Target.UnitIndex].CritBlocks++ - } else if result.DidGlance() { - // Subtract Glances because they happen before Blocks - spell.SpellMetrics[result.Target.UnitIndex].Glances-- - spell.SpellMetrics[result.Target.UnitIndex].GlanceBlocks++ - } else { - spell.SpellMetrics[result.Target.UnitIndex].Blocks++ - } + spell.SpellMetrics[result.Target.UnitIndex].Blocks++ if result.Target.Blockhandler != nil { result.Target.Blockhandler(sim, spell, result) @@ -835,7 +847,7 @@ func (result *SpellResult) applyEnemyAttackTableDodge(spell *Spell, attackTable return false } - *chance += max(result.Target.GetTotalDodgeChanceAsDefender(attackTable), 0.0) + *chance += max(result.Target.GetTotalDodgeChanceAsDefender(attackTable)-spell.Unit.PseudoStats.DodgeReduction, 0.0) if roll < *chance { result.Outcome = OutcomeDodge @@ -929,24 +941,24 @@ func (spell *Spell) OutcomeExpectedPhysicalCrit(_ *Simulation, result *SpellResu func (spell *Spell) OutcomeExpectedMeleeWhite(_ *Simulation, result *SpellResult, attackTable *AttackTable) { missChance := spell.GetPhysicalMissChance(attackTable) - dodgeChance := TernaryFloat64(spell.Flags.Matches(SpellFlagCannotBeDodged), 0, max(0, attackTable.BaseDodgeChance-spell.DodgeSuppression())) - parryChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, max(0, attackTable.BaseParryChance-spell.ParrySuppression(attackTable)), 0) + dodgeChance := TernaryFloat64(spell.Flags.Matches(SpellFlagCannotBeDodged), 0, max(0, attackTable.BaseDodgeChance-spell.DodgeParrySuppression()-spell.Unit.PseudoStats.DodgeReduction)) + parryChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, max(0, attackTable.BaseParryChance-spell.DodgeParrySuppression()), 0) glanceChance := attackTable.BaseGlanceChance blockChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, attackTable.BaseBlockChance, 0) - whiteCritCap := 1.0 - missChance - dodgeChance - parryChance - glanceChance + whiteCritCap := 1.0 - missChance - dodgeChance - parryChance - glanceChance - blockChance critChance := min(spell.PhysicalCritChance(attackTable), whiteCritCap) - averageMultiplier := (1.0 - missChance - dodgeChance - parryChance + (spell.CritDamageMultiplier()-1)*critChance - glanceChance*(1.0-attackTable.GlanceMultiplier)) * (1.0 - blockChance*result.Target.BlockDamageReduction()) + averageMultiplier := 1.0 - missChance - dodgeChance - parryChance + (spell.CritDamageMultiplier()-1)*critChance - glanceChance*(1.0-attackTable.GlanceMultiplier) - blockChance*result.Target.BlockDamageReduction() result.Damage *= averageMultiplier } func (spell *Spell) OutcomeExpectedMeleeWeaponSpecialHitAndCrit(_ *Simulation, result *SpellResult, attackTable *AttackTable) { missChance := max(0, attackTable.BaseMissChance-spell.PhysicalHitChance(attackTable)) - dodgeChance := TernaryFloat64(spell.Flags.Matches(SpellFlagCannotBeDodged), 0, max(0, attackTable.BaseDodgeChance-spell.DodgeSuppression())) - parryChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, max(0, attackTable.BaseParryChance-spell.ParrySuppression(attackTable)), 0) + dodgeChance := TernaryFloat64(spell.Flags.Matches(SpellFlagCannotBeDodged), 0, max(0, attackTable.BaseDodgeChance-spell.DodgeParrySuppression()-spell.Unit.PseudoStats.DodgeReduction)) + parryChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, max(0, attackTable.BaseParryChance-spell.DodgeParrySuppression()), 0) blockChance := TernaryFloat64(spell.Unit.PseudoStats.InFrontOfTarget, attackTable.BaseBlockChance, 0) critChance := spell.PhysicalCritChance(attackTable) - critFactor := (spell.CritDamageMultiplier() - 1) * critChance - averageMultiplier := (1.0 - missChance - dodgeChance - parryChance) * (1.0 + critFactor - blockChance*(critFactor+result.Target.BlockDamageReduction())) + averageMultiplier := (1.0 - missChance - dodgeChance - parryChance) * (1.0 + (spell.CritDamageMultiplier()-1)*critChance) + averageMultiplier -= blockChance * ((spell.CritDamageMultiplier()-1)*critChance + result.Target.BlockDamageReduction()) result.Damage *= averageMultiplier } diff --git a/sim/core/spell_queueing.go b/sim/core/spell_queueing.go index 1d38b9ec3f..7f17195976 100644 --- a/sim/core/spell_queueing.go +++ b/sim/core/spell_queueing.go @@ -89,7 +89,7 @@ func (spell *Spell) CanQueue(sim *Simulation, target *Unit) bool { } // Apply SQW leniency to any pending hardcasts - if (spell.Unit.Hardcast.Expires > sim.CurrentTime+MaxSpellQueueWindow) || (spell.Unit.IsCastingDuringChannel() && !spell.CanCastDuringChannel(sim)) { + if (spell.Unit.Hardcast.Expires > sim.CurrentTime+MaxSpellQueueWindow) || !spell.CanCastDuringChannel(sim) { return false } diff --git a/sim/core/spell_resistances.go b/sim/core/spell_resistances.go new file mode 100644 index 0000000000..75f04de8af --- /dev/null +++ b/sim/core/spell_resistances.go @@ -0,0 +1,172 @@ +package core + +import ( + "fmt" + "math" + "strings" + + "github.com/wowsims/tbc/sim/core/stats" +) + +func (result *SpellResult) applyResistances(sim *Simulation, spell *Spell, isPeriodic bool, attackTable *AttackTable) { + resistanceMultiplier, outcome := spell.ResistanceMultiplier(sim, isPeriodic, attackTable) + + result.Damage *= resistanceMultiplier + result.Outcome |= outcome + + result.ArmorAndResistanceMultiplier = resistanceMultiplier + result.PostArmorAndResistanceMultiplier = result.Damage +} + +// Modifies damage based on Armor or Magic resistances, depending on the damage type. +func (spell *Spell) ResistanceMultiplier(sim *Simulation, isPeriodic bool, attackTable *AttackTable) (float64, HitOutcome) { + if spell.Flags.Matches(SpellFlagIgnoreResists) { + return 1, OutcomeEmpty + } + + if spell.SpellSchool.Matches(SpellSchoolPhysical) { + // All physical dots (Bleeds) ignore armor. + if isPeriodic && !spell.Flags.Matches(SpellFlagApplyArmorReduction) { + return 1, OutcomeEmpty + } + + // Physical resistance (armor). + return attackTable.GetArmorDamageModifier(spell), OutcomeEmpty + } + + // Magical resistance. + averageResist := attackTable.Defender.averageResist(spell.SpellSchool, attackTable.Attacker) + if averageResist == 0 { // for equal or lower level mobs + return 1, OutcomeEmpty + } + + if spell.Flags.Matches(SpellFlagBinary) { + if resistanceRoll := sim.RandomFloat("Binary Resist"); resistanceRoll < averageResist { + return 0, OutcomeEmpty + } + return 1, OutcomeEmpty + } + + thresholds := attackTable.Defender.partialResistRollThresholds(averageResist) + + switch resistanceRoll := sim.RandomFloat("Partial Resist"); { + case resistanceRoll < thresholds[0].cumulativeChance: + return thresholds[0].damageMultiplier(), OutcomePartial8 + case resistanceRoll < thresholds[1].cumulativeChance: + return thresholds[1].damageMultiplier(), OutcomePartial4 + case resistanceRoll < thresholds[2].cumulativeChance: + return thresholds[2].damageMultiplier(), OutcomePartial2 + default: + return thresholds[3].damageMultiplier(), OutcomePartial1 + } +} + +// https://web.archive.org/web/20130208043756/http://elitistjerks.com/f15/t29453-combat_ratings_level_85_cataclysm/ +// https://web.archive.org/web/20110309163709/http://elitistjerks.com/f78/t105429-cataclysm_mechanics_testing/ +func (at *AttackTable) GetArmorDamageModifier(spell *Spell) float64 { + if at.IgnoreArmor { + return 1.0 + } + + ignoreArmorFactor := Clamp(at.ArmorIgnoreFactor, 0.0, 1.0) + + // Assume target > 80 + armorConstant := float64(at.Attacker.Level)*467.5 - 22167.5 + defenderArmor := at.Defender.Armor() - (at.Defender.Armor() * ignoreArmorFactor) + // TBC ANNI: Apply flat ArP + defenderArmor = max(defenderArmor-math.Abs(at.Attacker.stats[stats.ArmorPenetration]), 0) + return 1 - defenderArmor/(defenderArmor+armorConstant) +} + +/* + The following calculations are based on + https://web.archive.org/web/20110207221537/http://elitistjerks.com/f15/t44675-resistance_mechanics_wotlk/ + This handles the mob vs. player case + - average resist is calculated as AR = R / (R + C), C is 400 for level 80 mobs, assumed 510 for level 83 mobs + - actual resist values come in multiples of 10%, with 3-4 values around the average resist + - probability for a given resist value is P(x) = 0.5 - 2.5*|x - AR| (transformed for AR < 0.1 or AR > 0.9) + - the resist cap is likely gone, since resists work like armor now + https://web.archive.org/web/20110209210726/http://elitistjerks.com/f75/t38540-general_mage_discussion_information/p11/#post1171056 + This handles the player vs. mob partial resists case + - it's modelled identical to the mob vs. player case + - the resulting numbers have been verified in game (55% for 0%, 30% for 10%, 15% for 20% resists) +*/ + +func (unit *Unit) averageResist(school SpellSchool, attacker *Unit) float64 { + resistance := unit.GetStat(school.ResistanceStat()) - attacker.stats[stats.SpellPenetration] + if resistance <= 0 { + // https://wowpedia.fandom.com/wiki/Resistance?oldid=6512353 + // With the release of cataclysm, level based resistances seem to have been removed + return 0 + } + + level := float64(unit.Level) + c := 150 + (level-60)*(level-67.5) + + return resistance / (c + resistance) +} + +type Threshold struct { + cumulativeChance float64 + bracket int +} + +func (x Threshold) damageMultiplier() float64 { + return 1 - 0.1*float64(x.bracket) +} + +type Thresholds [4]Threshold + +func (x Thresholds) String() string { + var sb strings.Builder + var chance float64 + for _, t := range x { + sb.WriteString(fmt.Sprintf("%.1f%% for %d%% ", (t.cumulativeChance-chance)*100, t.bracket*10)) + if t.cumulativeChance >= 1 { + break + } + chance = t.cumulativeChance + } + return sb.String() +} + +func (unit *Unit) partialResistRollThresholds(ar float64) Thresholds { + if ar <= 0.1 { // always 0%, 10%, or 20%; this covers all player vs. mob cases, in practice + return Thresholds{ + {cumulativeChance: 1 - 7.5*ar, bracket: 0}, + {cumulativeChance: 1 - 2.5*ar, bracket: 1}, + {cumulativeChance: 1, bracket: 2}, + } + } + + if ar >= 0.9 { // always 80%, 90%, or 100%; only relevant for tests ;) + return Thresholds{ + {cumulativeChance: 1 - 7.5*(1-ar), bracket: 10}, + {cumulativeChance: 1 - 2.5*(1-ar), bracket: 9}, + {cumulativeChance: 1, bracket: 8}, + } + } + + p := func(x float64) float64 { + return math.Max(0.5-2.5*math.Abs(x-ar), 0) + } + + const eps = 1e-9 // imprecision guard (25-50-25 might become almost0-25-50-25-almost0) + + var thresholds Thresholds + var cumulativeChance float64 + var index int + for bracket := 0; bracket <= 10; bracket++ { + if chance := p(float64(bracket) * 0.1); chance > eps { + cumulativeChance += chance + thresholds[index] = Threshold{cumulativeChance: cumulativeChance, bracket: bracket} + index++ + } + } + + if thresholds[index-1].cumulativeChance < 1 { // also guards against floating point imprecision + thresholds[index-1].cumulativeChance = 1 + } + + return thresholds +} diff --git a/sim/core/spell_result.go b/sim/core/spell_result.go index bdf85c7ac8..e871828348 100644 --- a/sim/core/spell_result.go +++ b/sim/core/spell_result.go @@ -17,9 +17,9 @@ type SpellResult struct { Damage float64 // Damage done by this cast. Threat float64 // The amount of threat generated by this cast. - ArmorMultiplier float64 // Armor multiplier - PostArmorDamage float64 // Damage done by this cast after Armor is applied - PostOutcomeDamage float64 // Damage done by this cast after Outcome is applied + ArmorAndResistanceMultiplier float64 // Armor multiplier + PostArmorAndResistanceMultiplier float64 // Damage done by this cast after Armor is applied + PostOutcomeDamage float64 // Damage done by this cast after Outcome is applied inUse bool } @@ -73,7 +73,7 @@ func (spell *Spell) NewResult(target *Unit) *SpellResult { result.Threat = 0 result.Outcome = OutcomeEmpty // for blocks result.inUse = true - result.PostArmorDamage = 0 + result.PostArmorAndResistanceMultiplier = 0 result.PostOutcomeDamage = 0 return result @@ -89,7 +89,7 @@ func (spell *Spell) CloneResult(result *SpellResult) *SpellResult { newResult.Damage = result.Damage newResult.Threat = result.Threat newResult.Outcome = result.Outcome - newResult.PostArmorDamage = result.PostArmorDamage + newResult.PostArmorAndResistanceMultiplier = result.PostArmorAndResistanceMultiplier newResult.PostOutcomeDamage = result.PostOutcomeDamage return newResult @@ -156,18 +156,12 @@ func (spell *Spell) RangedAttackPower() float64 { return spell.Unit.stats[stats.RangedAttackPower] } -func (spell *Spell) DodgeSuppression() float64 { +func (spell *Spell) DodgeParrySuppression() float64 { + // TBC ANNI: Verify if Expertise truncates expertiseRating := spell.Unit.stats[stats.ExpertiseRating] + spell.BonusExpertiseRating return expertiseRating / ExpertisePerQuarterPercentReduction / 400 } -// MoP reworked Parry. Rather than being innately ~2x Dodge chance, expertise now applies to Dodge first (down to 0), and then Parry. -// The base chance for Dodge/Parry are both 7.5%, assuming a +3 target. The 7.5% Dodge chance must be fully suppressed before Parry will go down. -// This makes the effect of each point of Expertise linear when attacking from the front -func (spell *Spell) ParrySuppression(attackTable *AttackTable) float64 { - return max(0, spell.DodgeSuppression()-attackTable.BaseDodgeChance) -} - func (spell *Spell) PhysicalHitChance(attackTable *AttackTable) float64 { hitPercent := spell.Unit.stats[stats.PhysicalHitPercent] + spell.BonusHitPercent return hitPercent / 100 @@ -237,9 +231,10 @@ func (spell *Spell) HealingCritCheck(sim *Simulation) bool { func (spell *Spell) ApplyPostOutcomeDamageModifiers(sim *Simulation, result *SpellResult, isPeriodic bool) { result.PostOutcomeDamage = result.Damage - if spell.Flags.Matches(SpellFlagAoE) { - result.Damage *= sim.Encounter.AOECapMultiplier() - } + // TBC ANNI: Look into this + // if spell.Flags.Matches(SpellFlagAoE) { + // result.Damage *= sim.Encounter.AOECapMultiplier() + // } for i := range result.Target.DynamicDamageTakenModifiers { result.Target.DynamicDamageTakenModifiers[i](sim, spell, result, isPeriodic) } @@ -272,28 +267,52 @@ func (spell *Spell) calcDamageInternal(sim *Simulation, target *Unit, baseDamage if sim.Log == nil { result.Damage *= attackerMultiplier - result.applyArmor(spell, isPeriodic, attackTable) + result.applyResistances(sim, spell, isPeriodic, attackTable) result.applyTargetModifiers(sim, spell, attackTable, isPeriodic) + // Save partial outcome which comes from applyResistances call + partialOutcome := OutcomeEmpty + if result.Outcome.Matches(OutcomePartial) { + partialOutcome = result.Outcome & OutcomePartial + } + outcomeApplier(sim, result, attackTable) + // Restore partial outcome + if partialOutcome != OutcomeEmpty { + result.Outcome |= partialOutcome + } + spell.ApplyPostOutcomeDamageModifiers(sim, result, isPeriodic) } else { result.Damage *= attackerMultiplier afterAttackMods := result.Damage - result.applyArmor(spell, isPeriodic, attackTable) + result.applyResistances(sim, spell, isPeriodic, attackTable) + afterResistances := result.Damage result.applyTargetModifiers(sim, spell, attackTable, isPeriodic) afterTargetMods := result.Damage + // Save partial outcome which comes from applyResistances call + partialOutcome := OutcomeEmpty + if result.Outcome.Matches(OutcomePartial) { + partialOutcome = result.Outcome & OutcomePartial + } + outcomeApplier(sim, result, attackTable) + afterOutcome := result.Damage + + // Restore partial outcome + if partialOutcome != OutcomeEmpty { + result.Outcome |= partialOutcome + } spell.ApplyPostOutcomeDamageModifiers(sim, result, isPeriodic) afterPostOutcome := result.Damage spell.Unit.Log( sim, - "%s %s [DEBUG] MAP: %0.01f, RAP: %0.01f, SP: %0.01f, BaseDamage:%0.01f, AfterAttackerMods:%0.01f, AfterArmor:%0.01f, AfterTargetMods:%0.01f, AfterOutcome:%0.01f, AfterPostOutcome:%0.01f", - target.LogLabel(), spell.ActionID, spell.Unit.GetStat(stats.AttackPower), spell.Unit.GetStat(stats.RangedAttackPower), spell.SpellPower(), baseDamage, afterAttackMods, result.PostArmorDamage, afterTargetMods, result.PostOutcomeDamage, afterPostOutcome) + "%s %s [DEBUG] MAP: %0.01f, RAP: %0.01f, SP: %0.01f, BaseDamage:%0.01f, AfterAttackerMods:%0.01f, AfterResistances:%0.01f, AfterTargetMods:%0.01f, AfterOutcome:%0.01f, AfterPostOutcome:%0.01f", + target.LogLabel(), spell.ActionID, spell.Unit.GetStat(stats.AttackPower), spell.Unit.GetStat(stats.RangedAttackPower), spell.SpellPower(), baseDamage, afterAttackMods, afterResistances, afterTargetMods, afterOutcome, afterPostOutcome) } result.Threat = spell.ThreatFromDamage(sim, result.Outcome, result.Damage, attackTable) @@ -696,10 +715,6 @@ func (spell *Spell) TargetDamageMultiplier(sim *Simulation, attackTable *AttackT multiplier *= attackTable.Defender.PseudoStats.PeriodicPhysicalDamageTakenMultiplier } - if spell.Flags.Matches(SpellFlagRanged) { - multiplier *= attackTable.RangedDamageTakenMultiplier - } - if attackTable.DamageDoneByCasterMultiplier != nil { multiplier *= attackTable.DamageDoneByCasterMultiplier(sim, spell, attackTable) } diff --git a/sim/core/stats/stats.go b/sim/core/stats/stats.go index 7a0e4ac4c2..0d14429227 100644 --- a/sim/core/stats/stats.go +++ b/sim/core/stats/stats.go @@ -397,8 +397,11 @@ func (stats Stats) ToProtoMap() map[int32]float64 { func FromProtoMap(m map[int32]float64) Stats { var stats Stats for k, v := range m { + if k == int32(proto.Stat_StatArmorPenetration) || k == int32(proto.Stat_StatSpellPenetration) { + stats[k] = -v + continue + } stats[k] = v - } return stats } @@ -435,6 +438,12 @@ type PseudoStats struct { DisableDWMissPenalty bool // Used by Heroic Strike and Cleave + IncreasedMissChance float64 // Insect Swarm and Scorpid Sting + DodgeReduction float64 // Used by Warrior talent 'Weapon Mastery' and SWP boss auras. + + MobTypeAttackPower float64 // Bonus AP against mobs of the current type. + MobTypeSpellPower float64 // Bonus SP against mobs of the current type. + ThreatMultiplier float64 // Modulates the threat generated. Affected by things like salv. DamageDealtMultiplier float64 // All damage @@ -468,7 +477,10 @@ type PseudoStats struct { ReducedCritTakenChance float64 // Reduces chance to be crit. - BonusHealingTaken float64 // Talisman of Troll Divinity + BonusHealingTaken float64 // Talisman of Troll Divinity + BonusRangedAttackPowerTaken float64 // Hunters mark + BonusSpellCritPercentTaken float64 // Imp Shadow Bolt / Imp Scorch / Winter's Chill debuff + BonusPhysicalDamageTaken float64 // Hemo, Gift of Arthas, etc DamageTakenMultiplier float64 // All damage SchoolDamageTakenMultiplier [SchoolLen]float64 // For specific spell schools (arcane, fire, shadow, etc.) diff --git a/sim/core/statweight.go b/sim/core/statweight.go index af0133be92..813a957df6 100644 --- a/sim/core/statweight.go +++ b/sim/core/statweight.go @@ -170,11 +170,12 @@ func buildStatWeightRequests(swr *proto.StatWeightsRequest) *proto.StatWeightReq for _, s := range statsToWeigh { stat := stats.UnitStatFromStat(s) statMod := defaultStatMod - // Primary stats have half the value of a secondary stat - if s <= stats.Intellect { - statMod /= 2 - } else if stat.EqualsStat(stats.Armor) || stat.EqualsStat(stats.BonusArmor) { + if stat.EqualsStat(stats.Armor) || stat.EqualsStat(stats.BonusArmor) { statMod = defaultStatMod * 10 + } else if stat.EqualsStat(stats.ArmorPenetration) || stat.EqualsStat(stats.SpellPenetration) { + // Pen stats are stored as negatives + statMod = -defaultStatMod + println("ASDF") } statModsHigh[stat] = statMod statModsLow[stat] = -statMod @@ -314,8 +315,8 @@ func computeStatWeights(swcr *proto.StatWeightsCalcRequest) *proto.StatWeightsRe } mean := weightResults.Weights.Get(stat) / weightResults.Weights.Stats[refStat] stdev := weightResults.WeightsStdev.Get(stat) / math.Abs(weightResults.Weights.Stats[refStat]) - weightResults.EpValues.AddStat(stat, mean) - weightResults.EpValuesStdev.AddStat(stat, stdev) + weightResults.EpValues.AddStat(stat, math.Abs(mean)) + weightResults.EpValuesStdev.AddStat(stat, math.Abs(stdev)) } calcEpResults(&result.Dps, referenceStat) diff --git a/sim/core/unit.go b/sim/core/unit.go index 6a4b92e9d2..1e961b1e8e 100644 --- a/sim/core/unit.go +++ b/sim/core/unit.go @@ -475,10 +475,6 @@ func (unit *Unit) IsChanneling() bool { return unit.ChanneledDot != nil } -func (unit *Unit) IsCastingDuringChannel() bool { - return unit.IsChanneling() && unit.ChanneledDot.Spell.Flags.Matches(SpellFlagCastWhileChanneling) -} - func (unit *Unit) SpellGCD() time.Duration { return max(GCDMin, unit.ApplyCastSpeed(GCDDefault)) } diff --git a/sim/druid/might_of_ursoc.go b/sim/druid/might_of_ursoc.go index 6f71c3d83b..deaabbde3c 100644 --- a/sim/druid/might_of_ursoc.go +++ b/sim/druid/might_of_ursoc.go @@ -30,7 +30,6 @@ func (druid *Druid) registerMightOfUrsocCD() { druid.MightOfUrsoc = druid.RegisterSpell(Any, core.SpellConfig{ ActionID: actionID, - Flags: core.SpellFlagReadinessTrinket, ClassSpellMask: DruidSpellMightOfUrsoc, Cast: core.CastConfig{ diff --git a/sim/druid/survival_instincts.go b/sim/druid/survival_instincts.go index 47799bea32..021457f669 100644 --- a/sim/druid/survival_instincts.go +++ b/sim/druid/survival_instincts.go @@ -24,7 +24,6 @@ func (druid *Druid) registerSurvivalInstinctsCD() { druid.SurvivalInstincts = druid.RegisterSpell(Cat|Bear, core.SpellConfig{ ActionID: actionID, - Flags: core.SpellFlagReadinessTrinket, Cast: core.CastConfig{ CD: core.Cooldown{ diff --git a/sim/druid/talents.go b/sim/druid/talents.go index 662d563f55..5d80e146c8 100644 --- a/sim/druid/talents.go +++ b/sim/druid/talents.go @@ -254,9 +254,9 @@ func (druid *Druid) registerNaturesVigil() { }, OnSpellHitDealt: func(_ *core.Aura, _ *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if !spell.Flags.Matches(core.SpellFlagAoE) { - smartHealStrength = max(smartHealStrength, result.Damage) - } + // if !spell.Flags.Matches(core.SpellFlagAoE) { + // smartHealStrength = max(smartHealStrength, result.Damage) + // } }, }) diff --git a/sim/druid/tranquility.go b/sim/druid/tranquility.go index b6e6f5377d..48a30ace1f 100644 --- a/sim/druid/tranquility.go +++ b/sim/druid/tranquility.go @@ -16,7 +16,7 @@ func (druid *Druid) registerTranquilityCD() { ActionID: core.ActionID{SpellID: 44203}, SpellSchool: core.SpellSchoolNature, ProcMask: core.ProcMaskSpellHealing, - Flags: core.SpellFlagHelpful | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell | core.SpellFlagReadinessTrinket, + Flags: core.SpellFlagHelpful | core.SpellFlagNoOnCastComplete | core.SpellFlagPassiveSpell, DamageMultiplier: 1, ThreatMultiplier: 1, CritMultiplier: druid.DefaultCritMultiplier(), diff --git a/sim/warlock/hellfire.go b/sim/warlock/hellfire.go index e39e5c49ea..73e3351049 100644 --- a/sim/warlock/hellfire.go +++ b/sim/warlock/hellfire.go @@ -17,7 +17,7 @@ func (warlock *Warlock) RegisterHellfire(callback WarlockSpellCastedCallback) *c warlock.Hellfire = warlock.RegisterSpell(core.SpellConfig{ ActionID: hellfireActionID, SpellSchool: core.SpellSchoolFire, - Flags: core.SpellFlagAoE | core.SpellFlagChanneled | core.SpellFlagAPL, + Flags: core.SpellFlagChanneled | core.SpellFlagAPL, ProcMask: core.ProcMaskSpellDamage, ClassSpellMask: WarlockSpellHellfire, ThreatMultiplier: 1, diff --git a/tools/database/dbc/item.go b/tools/database/dbc/item.go index 372ce3956f..467f9de722 100644 --- a/tools/database/dbc/item.go +++ b/tools/database/dbc/item.go @@ -129,6 +129,9 @@ func (item *Item) GetStats(itemLevel int) *stats.Stats { continue } stats[stat] = item.GetScaledStat(i, itemLevel) + if stat == proto.Stat_StatArmorPenetration { + stats[stat] = -stats[stat] + } } armor := item.GetArmorValue(itemLevel) diff --git a/tools/database/dbc/spell_effect.go b/tools/database/dbc/spell_effect.go index f37c3e521e..77def6af2b 100644 --- a/tools/database/dbc/spell_effect.go +++ b/tools/database/dbc/spell_effect.go @@ -294,6 +294,7 @@ func (effect *SpellEffect) ParseStatEffect(scalesWithIlvl bool, ilvl int) *stats effectStats[stat] = effect.CalcCoefficientStatValue(ilvl) break } + // Armor/Spell Pen is negative in the DB, so Subtract it effectStats[stat] += float64(effect.EffectBasePoints + effect.EffectDieSides) } }