diff --git a/sim/core/debuffs.go b/sim/core/debuffs.go index 3f977f609..0493e58eb 100644 --- a/sim/core/debuffs.go +++ b/sim/core/debuffs.go @@ -257,6 +257,7 @@ func MangleAura(target *Target) *Aura { return target.GetOrRegisterAura(Aura{ Label: "Mangle", ActionID: ActionID{SpellID: 33876}, + Duration: time.Second * 12, OnGain: func(aura *Aura, sim *Simulation) { aura.Unit.PseudoStats.PeriodicPhysicalDamageTakenMultiplier *= 1.3 }, diff --git a/sim/druid/druid.go b/sim/druid/druid.go index 507ceac73..658dd3273 100644 --- a/sim/druid/druid.go +++ b/sim/druid/druid.go @@ -15,6 +15,7 @@ type Druid struct { RebirthUsed bool CatForm bool + Wolfshead bool FaerieFire *core.Spell Hurricane *core.Spell @@ -24,6 +25,7 @@ type Druid struct { Starfire6 *core.Spell Starfire8 *core.Spell Wrath *core.Spell + Powershift *core.Spell InsectSwarmDot *core.Dot MoonfireDot *core.Dot @@ -51,6 +53,7 @@ func (druid *Druid) AddRaidBuffs(raidBuffs *proto.RaidBuffs) { } const ravenGoddessItemID = 32387 +const wolfsheadItemID = 8345 func (druid *Druid) AddPartyBuffs(partyBuffs *proto.PartyBuffs) { if druid.Talents.MoonkinForm { // assume if you have moonkin talent you are using it. @@ -74,14 +77,23 @@ func (druid *Druid) AddPartyBuffs(partyBuffs *proto.PartyBuffs) { } func (druid *Druid) Init(sim *core.Simulation) { - druid.registerFaerieFireSpell(sim) - druid.registerHurricaneSpell(sim) - druid.registerInsectSwarmSpell(sim) - druid.registerMoonfireSpell(sim) - druid.registerRebirthSpell(sim) - druid.Starfire8 = druid.newStarfireSpell(sim, 8) - druid.Starfire6 = druid.newStarfireSpell(sim, 6) - druid.registerWrathSpell(sim) + if druid.CatForm { + druid.registerPowershiftSpell(sim) + druid.registerRipSpell(sim) + } else { + druid.registerFaerieFireSpell(sim) + druid.registerHurricaneSpell(sim) + druid.registerInsectSwarmSpell(sim) + druid.registerMoonfireSpell(sim) + druid.registerRebirthSpell(sim) + druid.Starfire8 = druid.newStarfireSpell(sim, 8) + druid.Starfire6 = druid.newStarfireSpell(sim, 6) + druid.registerWrathSpell(sim) + } + + if druid.CatForm { + druid.DelayCooldownsForArmorDebuffs(sim) + } } func (druid *Druid) Reset(sim *core.Simulation) { @@ -99,6 +111,7 @@ func New(char core.Character, selfBuffs SelfBuffs, talents proto.DruidTalents) * Talents: talents, RebirthUsed: false, CatForm: false, + Wolfshead: false, } druid.EnableManaBar() @@ -126,6 +139,14 @@ func New(char core.Character, selfBuffs SelfBuffs, talents proto.DruidTalents) * }, }) + // Check if Wolfshead Helm is equipped + for _, e := range druid.Equip { + if e.ID == wolfsheadItemID { + druid.Wolfshead = true + break + } + } + return druid } diff --git a/sim/druid/feral/feral.go b/sim/druid/feral/feral.go index 19ec6c031..e65342f3b 100644 --- a/sim/druid/feral/feral.go +++ b/sim/druid/feral/feral.go @@ -54,14 +54,14 @@ func NewFeralDruid(character core.Character, options proto.Player) *FeralDruid { // Set up base paw weapon. Assume that Predatory Instincts is a primary rather than secondary modifier for now, but this needs to confirmed! primaryModifier := 1 + 0.02*float64(cat.Talents.PredatoryInstincts) - critMultiplier := cat.MeleeCritMultiplier(primaryModifier, 0) + cat.critMultiplier = cat.MeleeCritMultiplier(primaryModifier, 0) basePaw := core.Weapon{ BaseDamageMin: 43.5, BaseDamageMax: 66.5, SwingSpeed: 1.0, NormalizedSwingSpeed: 1.0, SwingDuration: time.Duration(1.0 * float64(time.Second)), - CritMultiplier: critMultiplier, + CritMultiplier: cat.critMultiplier, } cat.EnableAutoAttacks(cat, core.AutoAttackOptions{ MainHand: basePaw, @@ -93,9 +93,19 @@ type FeralDruid struct { *druid.Druid Rotation proto.FeralDruid_Rotation + + readyToShift bool + waitingForTick bool + critMultiplier float64 } // GetDruid is to implement druid.Agent (supports nordrassil set bonus) func (cat *FeralDruid) GetDruid() *druid.Druid { return cat.Druid } + +func (cat *FeralDruid) Reset(sim *core.Simulation) { + cat.Druid.Reset(sim) + cat.readyToShift = false + cat.waitingForTick = false +} diff --git a/sim/druid/feral/rotation.go b/sim/druid/feral/rotation.go index 363f834f7..53cb43e56 100644 --- a/sim/druid/feral/rotation.go +++ b/sim/druid/feral/rotation.go @@ -1,12 +1,49 @@ package feral - import ( - "github.com/wowsims/tbc/sim/core" - ) +import ( + "github.com/wowsims/tbc/sim/core" + "github.com/wowsims/tbc/sim/core/proto" +) - func (cat *FeralDruid) OnGCDReady(sim *core.Simulation) { - cat.doRotation(sim) - } +func (cat *FeralDruid) OnGCDReady(sim *core.Simulation) { + cat.doRotation(sim) +} - func (cat *FeralDruid) doRotation(sim *core.Simulation) { - } +func (cat *FeralDruid) doRotation(sim *core.Simulation) { + // If we're out of form because we just cast Innervate, always shift + if !cat.CatForm { + cat.Powershift.Cast(sim, nil) + return + } + + // If we previously decided to shift, then execute the shift now once the input delay is over. + if cat.readyToShift { + cat.innervateOrShift(sim) + return + } + + // Get current Energy and CP + energy := cat.CurrentEnergy() + comboPoints := cat.ComboPoints() + + // Decide whether to cast Rip as our next special + ripNow := cat.ShouldCastRip(sim, cat.Rotation) + + // Decide whether to cast Mangle as our next special + mangleNow := !ripNow && !cat.MangleAura.IsActive() +} + +func (cat *FeralDruid) innervateOrShift(sim *core.Simulation) { + cat.waitingForTick = false + + // If we have just now decided to shift, then we do not execute the shift immediately, but instead trigger an input delay for realism. + if !cat.readyToShift { + cat.readyToShift = true + return + } + + cat.readyToShift = false + + // Logic for Innervate and Haste Pot usage will go here. For now we just execute simple powershifts without bundling any caster form CDs. + cat.Powershift.Cast(sim, nil) +} diff --git a/sim/druid/ferocious_bite.go b/sim/druid/ferocious_bite.go new file mode 100644 index 000000000..cb83677d3 --- /dev/null +++ b/sim/druid/ferocious_bite.go @@ -0,0 +1,55 @@ +package druid + +import ( + "time" + + "github.com/wowsims/tbc/sim/core" + "github.com/wowsims/tbc/sim/core/stats" +) + +var FerociousBiteActionID = core.ActionID{SpellID: 24248} +var FerociousBiteEnergyCost = 35.0 + +func (druid *Druid) registerFerociousBiteSpell(sim *core.Simulation) { + druid.FerociousBite = druid.RegisterSpell(core.SpellConfig{ + ActionID: FerociousBiteActionID, + SpellSchool: core.SpellSchoolPhysical, + SpellExtras: core.SpellExtrasMeleeMetrics, + + ResourceType: stats.Energy, + BaseCost: FerociousBiteEnergyCost, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + Cost: FerociousBiteEnergyCost, + GCD: time.Second, + }, + IgnoreHaste: true, + }, + + ApplyEffects: core.ApplyEffectFuncDirectDamage(core.SpellEffect{ + ProcMask: core.ProcMaskMeleeMHSpecial, + DamageMultiplier: 1 + 0.03*float64(druid.Talents.FeralAggression), + ThreatMultiplier: 1, + BaseDamage: core.BaseDamageConfig{ + Calculator: func(sim *core.Simulation, hitEffect *core.SpellEffect, spell *core.Spell) float64 { + comboPoints := float64(druid.ComboPoints()) + excessEnergy := druid.CurrentEnergy() - FerociousBiteEnergyCost + base := 57.0 + 169.0*comboPoints + 4.1*excessEnergy + roll := sim.RandomFloat("Ferocious Bite") * 66.0 + return base + roll + hitEffect.MeleeAttackPower(spell.Character)*0.05*comboPoints + }, + TargetSpellCoefficient: 1, + }, + OutcomeApplier: core.OutcomeFuncMeleeSpecialHitAndCrit(druid.critMultiplier), + OnSpellHit: func(sim *core.Simulation, spell *core.Spell, spellEffect *core.SpellEffect) { + if spellEffect.Landed() { + druid.SpendComboPoints(sim, spell.ActionID) + } + }, + }), + }) +} + +func (druid *Druid) ShouldCastBite(sim *core.Simulation, rotation proto.FeralDruid_Rotation) bool { +} diff --git a/sim/druid/items.go b/sim/druid/items.go index cc2b2e94d..d3bec8630 100644 --- a/sim/druid/items.go +++ b/sim/druid/items.go @@ -15,6 +15,7 @@ func init() { core.AddItemSet(&ItemSetMalorne) core.AddItemSet(&ItemSetNordrassil) core.AddItemSet(&ItemSetThunderheart) + core.AddItemSet(&ItemSetThunderheartFeral) } var ItemSetMalorne = core.ItemSet{ @@ -69,6 +70,18 @@ var ItemSetThunderheart = core.ItemSet{ }, } +var ItemSetThunderheartFeral = core.ItemSet{ + Name: "Thunderheart Harness", + Bonuses: map[int32]core.ApplyEffect{ + 2: func(agent core.Agent) { + // handled in mangle.go in template construction + }, + 4: func(agent core.Agent) { + // handled in rip.go and bite.go in template construction + }, + }, +} + func ApplyLivingRootoftheWildheart(agent core.Agent) { druidAgent := agent.(Agent) druid := druidAgent.GetDruid() diff --git a/sim/druid/mangle.go b/sim/druid/mangle.go new file mode 100644 index 000000000..59cb4a783 --- /dev/null +++ b/sim/druid/mangle.go @@ -0,0 +1,53 @@ +package druid + +import ( + "github.com/wowsims/tbc/sim/core" + "github.com/wowsims/tbc/sim/core/proto" + "github.com/wowsims/tbc/sim/core/stats" +) + +var MangleActionID = core.ActionID{SpellID: 33983} + +func (druid *Druid) registerMangleSpell(sim *core.Simulation) { + druid.MangleAura = core.MangleAura(sim.GetPrimaryTarget()) + + if druid.Rotation.MangleBot { + druid.MangleAura = core.MakePermanent(druid.MangleAura) + } + + energyCost := 45.0 - float64(druid.Talents.Ferocity) - core.TernaryFloat64(ItemSetThunderheartFeral.CharacterHasSetBonus(&druid.Character, 2), 5.0, 0) + refundAmount := energyCost * 0.8 + + druid.Mangle = druid.RegisterSpell(core.SpellConfig{ + ActionID: MangleActionID, + SpellSchool: core.SpellSchoolPhysical, + SpellExtras: core.SpellExtrasMeleeMetrics, + + ResourceType: stats.Energy, + BaseCost: energyCost, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + Cost: energyCost, + GCD: time.Second, + }, + IgnoreHaste: true, + }, + + ApplyEffects: core.ApplyEffectFuncDirectDamage(core.SpellEffect{ + ProcMask: core.ProcMaskMeleeMHSpecial, + DamageMultiplier: 1 + 0.1*float64(druid.Talents.SavageFury), + ThreatMultiplier: 1, + BaseDamage: core.BaseDamageConfigMeleeWeapon(core.MainHand, false, 264.0, 1.6, true), + OutcomeApplier: core.OutcomeFuncMeleeSpecialHitAndCrit(druid.critMultiplier), + OnSpellHit: func(sim *core.Simulation, spell *core.Spell, spellEffect *core.SpellEffect) { + if spellEffect.Landed() { + druid.AddComboPoints(sim, 1, MangleActionID) + druid.MangleAura.Activate() + } else { + druid.AddEnergy(sim, refundAmount, core.ActionID{OtherID: proto.OtherAction_OtherActionRefund}) + } + }, + }), + }) +} diff --git a/sim/druid/powershift.go b/sim/druid/powershift.go new file mode 100644 index 000000000..777f69793 --- /dev/null +++ b/sim/druid/powershift.go @@ -0,0 +1,39 @@ +package druid + +import ( + "time" + + "github.com/wowsims/tbc/sim/core" + "github.com/wowsims/tbc/sim/core/stats" +) + +var PowershiftActionID = core.ActionID{SpellID: 768} + +func (druid *Druid) registerPowershiftSpell(sim *core.Simulation) { + baseCost := 830.0 + finalEnergy := 40.0 + + if druid.Wolfshead { + finalEnergy += 20.0 + } + + druid.Powershift = druid.RegisterSpell(core.SpellConfig{ + ActionID: PowershiftActionID, + + ResourceType: stats.Mana, + BaseCost: baseCost, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + Cost: baseCost * (1 - 0.1*float64(druid.Talents.NaturalShapeshifter)), + GCD: core.GCDDefault, + }, + }, + + ApplyEffects: func(sim *core.Simulation, _ *core.Target, spell *core.Spell) { + currentEnergy := druid.CurrentEnergy() + druid.AddEnergy(sim, finalEnergy - currentEnergy, PowershiftActionID) + druid.CatForm = true + }, + }) +} diff --git a/sim/druid/rip.go b/sim/druid/rip.go new file mode 100644 index 000000000..af538a612 --- /dev/null +++ b/sim/druid/rip.go @@ -0,0 +1,88 @@ +package druid + +import ( + "strconv" + "time" + + "github.com/wowsims/tbc/sim/core" + "github.com/wowsims/tbc/sim/core/proto" + "github.com/wowsims/tbc/sim/core/stats" +) + +var RipActionID = core.ActionID{SpellID: 27008} +var RipEnergyCost = 30.0 + +func (druid *Druid) registerRipSpell(sim *core.Simulation) { + druid.Rip = druid.RegisterSpell(core.SpellConfig{ + ActionID: RipActionID, + SpellSchool: core.SpellSchoolPhysical, + SpellExtras: core.SpellExtrasMeleeMetrics | core.SpellExtrasIgnoreResists, + + ResourceType: stats.Energy, + BaseCost: RipEnergyCost, + + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + Cost: RipEnergyCost, + GCD: time.Second, + }, + IgnoreHaste: true, + }, + + ApplyEffects: core.ApplyEffectFuncDirectDamage(core.SpellEffect{ + ProcMask: core.ProcMaskMeleeMHSpecial, + DamageMultiplier: 1, + ThreatMultiplier: 1, + OutcomeApplier: core.OutcomeFuncMeleeSpecialHit(), + OnSpellHit: func(sim *core.Simulation, spell *core.Spell, spellEffect *core.SpellEffect) { + if spellEffect.Landed() { + druid.RipDot.Apply(sim) + druid.SpendComboPoints(sim, spell.ActionID) + } + }, + }), + }) + + target := sim.GetPrimaryTarget() + druid.RipDot = core.NewDot(core.Dot{ + Spell: druid.Rip, + Aura: target.RegisterAura(core.Aura{ + Label: "Rip-" + strconv.Itoa(int(druid.Index)), + ActionID: RipActionID, + }), + NumberOfTicks: 6, + TickLength: time.Second * 2, + TickEffects: core.TickFuncSnapshot(target, core.SpellEffect{ + DamageMultiplier: 1 + core.TernaryFloat64(ItemSetThunderheartFeral.CharacterHasSetBonus(&druid.Character, 4), 0.15, 0), + ThreatMultiplier: 1, + IsPeriodic: true, + BaseDamage: core.BuildBaseDamageConfig(func(sim *core.Simulation, hitEffect *core.SpellEffect, spell *core.Spell) float64 { + comboPoints := druid.ComboPoints() + attackPower := hitEffect.MeleeAttackPower(spell.Character) + + if comboPoints < 3 { + panic("Only 3-5 CP Rips are supported at present.") + } + if comboPoints == 3 { + return (990 + 0.18*attackPower) / 6 + } + if comboPoints == 4 { + return (1272 + 0.24*attackPower) / 6 + } + if comboPoints == 5 { + return (1554 + 0.24*attackPower) / 6 + } + }, 0), + OutcomeApplier: core.OutcomeFuncTick(), + }), + }) +} + +func (druid *Druid) ShouldCastRip(sim *core.Simulation, rotation proto.FeralDruid_Rotation) bool { + energy:= druid.CurrentEnergy() + comboPoints := druid.ComboPoints() + canPrimaryRip := (rotation.FinishingMove == proto.FeralDruid_Rotation_Rip) + canWeaveRip := (rotation.FinishingMove == proto.FeralDruid_Rotation_Bite) && rotation.Ripweave && (energy >= 52) && !druid.PseudoStats.NoCost + nearFightEnd := (sim.GetRemainingDuration() < time.Duration(10.0 * float64(time.Second))) + return (canPrimaryRip || canWeaveRip) && (comboPoints >= rotation.RipCp) && !druid.RipDot.IsActive() && !nearFightEnd +}