diff --git a/proto/common.proto b/proto/common.proto index 5420ba6333..ec3192af63 100644 --- a/proto/common.proto +++ b/proto/common.proto @@ -304,6 +304,7 @@ enum AgilityElixir { ElixirOfGreaterAgility = 2; ElixirOfLesserAgility = 3; ScrollOfAgility = 4; + ElixirOfAgility = 5; } enum ManaRegenElixir { diff --git a/proto/druid.proto b/proto/druid.proto index 61d9823305..b1440bd0a4 100644 --- a/proto/druid.proto +++ b/proto/druid.proto @@ -74,6 +74,7 @@ enum DruidRune { RuneLegsSavageRoar = 407988; RuneLegsLifebloom = 409824; RuneLegsSkullBash = 410176; + RuneBeltEclipse = 408248; } message BalanceDruid { diff --git a/sim/_hunter/explosive_trap.go b/sim/_hunter/explosive_trap.go deleted file mode 100644 index 81d32be5af..0000000000 --- a/sim/_hunter/explosive_trap.go +++ /dev/null @@ -1,131 +0,0 @@ -package hunter - -import ( - "time" - - "github.com/wowsims/sod/sim/core" -) - -func (hunter *Hunter) registerExplosiveTrapSpell(timer *core.Timer) { - bonusPeriodicDamageMultiplier := .10 * float64(hunter.Talents.TrapMastery) - - hunter.ExplosiveTrap = hunter.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 49067}, - SpellSchool: core.SpellSchoolFire, - ProcMask: core.ProcMaskSpellDamage, - Flags: core.SpellFlagAPL, - - ManaCost: core.ManaCostOptions{ - BaseCost: 0.19, - Multiplier: 1 - 0.2*float64(hunter.Talents.Resourcefulness), - }, - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: core.GCDDefault, - }, - CD: core.Cooldown{ - Timer: timer, - Duration: time.Second*30 - time.Second*2*time.Duration(hunter.Talents.Resourcefulness), - }, - }, - - DamageMultiplierAdditive: 1 + - .02*float64(hunter.Talents.TNT), - CritMultiplier: hunter.critMultiplier(false, false, false), - ThreatMultiplier: 1, - - Dot: core.DotConfig{ - IsAOE: true, - Aura: core.Aura{ - Label: "Explosive Trap", - }, - NumberOfTicks: 10, - TickLength: time.Second * 2, - - OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { - baseDamage := 90 + 0.1*dot.Spell.RangedAttackPower(target) - dot.Spell.DamageMultiplierAdditive += bonusPeriodicDamageMultiplier - for _, aoeTarget := range sim.Encounter.TargetUnits { - dot.Spell.CalcAndDealPeriodicDamage(sim, aoeTarget, baseDamage, dot.Spell.OutcomeRangedHit) - } - dot.Spell.DamageMultiplierAdditive -= bonusPeriodicDamageMultiplier - }, - }, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - if sim.CurrentTime < 0 { - // Traps only last 30s. - if sim.CurrentTime < -time.Second*30 { - return - } - - // If using this on prepull, the trap effect will go off when the fight starts - // instead of immediately. - core.StartDelayedAction(sim, core.DelayedActionOptions{ - DoAt: 0, - OnAction: func(sim *core.Simulation) { - for _, aoeTarget := range sim.Encounter.TargetUnits { - baseDamage := sim.Roll(523, 671) + 0.1*spell.RangedAttackPower(aoeTarget) - baseDamage *= sim.Encounter.AOECapMultiplier() - spell.CalcAndDealDamage(sim, aoeTarget, baseDamage, spell.OutcomeRangedHitAndCritNoBlock) - } - hunter.ExplosiveTrap.AOEDot().Apply(sim) - }, - }) - } else { - for _, aoeTarget := range sim.Encounter.TargetUnits { - baseDamage := sim.Roll(523, 671) + 0.1*spell.RangedAttackPower(aoeTarget) - baseDamage *= sim.Encounter.AOECapMultiplier() - spell.CalcAndDealDamage(sim, aoeTarget, baseDamage, spell.OutcomeRangedHitAndCritNoBlock) - } - hunter.ExplosiveTrap.AOEDot().Apply(sim) - } - }, - }) - - timeToTrapWeave := time.Millisecond * time.Duration(hunter.Options.TimeToTrapWeaveMs) - halfWeaveTime := timeToTrapWeave / 2 - hunter.TrapWeaveSpell = hunter.RegisterSpell(core.SpellConfig{ - ActionID: hunter.ExplosiveTrap.ActionID.WithTag(1), - Flags: core.SpellFlagNoOnCastComplete | core.SpellFlagNoMetrics | core.SpellFlagNoLogs | core.SpellFlagAPL, - - ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { - return hunter.ExplosiveTrap.CanCast(sim, target) - }, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - if sim.CurrentTime < 0 { - hunter.mayMoveAt = sim.CurrentTime - } - - // Assume we started running after the most recent ranged auto, so that time - // can be subtracted from the run in. - reachLocationAt := hunter.mayMoveAt + halfWeaveTime - layTrapAt := max(reachLocationAt, sim.CurrentTime) - doneAt := layTrapAt + halfWeaveTime - - hunter.AutoAttacks.DelayRangedUntil(sim, doneAt+time.Millisecond*500) - - if layTrapAt == sim.CurrentTime { - hunter.ExplosiveTrap.Cast(sim, target) - if doneAt > hunter.GCD.ReadyAt() { - hunter.GCD.Set(doneAt) - } - } else { - // Make sure the GCD doesn't get used while we're waiting. - hunter.WaitUntil(sim, doneAt) - - core.StartDelayedAction(sim, core.DelayedActionOptions{ - DoAt: layTrapAt, - OnAction: func(sim *core.Simulation) { - hunter.GCD.Reset() - hunter.ExplosiveTrap.Cast(sim, target) - if doneAt > hunter.GCD.ReadyAt() { - hunter.GCD.Set(doneAt) - } - }, - }) - } - }, - }) -} diff --git a/sim/_hunter/steady_shot.go b/sim/_hunter/steady_shot.go deleted file mode 100644 index 1e96559015..0000000000 --- a/sim/_hunter/steady_shot.go +++ /dev/null @@ -1,98 +0,0 @@ -package hunter - -import ( - "time" - - "github.com/wowsims/sod/sim/core" -) - -func (hunter *Hunter) registerSteadyShotSpell() { - impSSProcChance := 0.05 * float64(hunter.Talents.ImprovedSteadyShot) - if hunter.Talents.ImprovedSteadyShot > 0 { - hunter.ImprovedSteadyShotAura = hunter.RegisterAura(core.Aura{ - Label: "Improved Steady Shot", - ActionID: core.ActionID{SpellID: 53220}, - Duration: time.Second * 12, - OnGain: func(aura *core.Aura, sim *core.Simulation) { - hunter.ArcaneShot.DamageMultiplierAdditive += .15 - hunter.ArcaneShot.CostMultiplier -= 0.2 - if hunter.AimedShot != nil { - hunter.AimedShot.DamageMultiplierAdditive += .15 - hunter.AimedShot.CostMultiplier -= 0.2 - } - if hunter.ChimeraShot != nil { - hunter.ChimeraShot.DamageMultiplierAdditive += .15 - hunter.ChimeraShot.CostMultiplier -= 0.2 - } - }, - OnExpire: func(aura *core.Aura, sim *core.Simulation) { - hunter.ArcaneShot.DamageMultiplierAdditive -= .15 - hunter.ArcaneShot.CostMultiplier += 0.2 - if hunter.AimedShot != nil { - hunter.AimedShot.DamageMultiplierAdditive -= .15 - hunter.AimedShot.CostMultiplier += 0.2 - } - if hunter.ChimeraShot != nil { - hunter.ChimeraShot.DamageMultiplierAdditive -= .15 - hunter.ChimeraShot.CostMultiplier += 0.2 - } - }, - OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { - if spell == hunter.AimedShot || spell == hunter.ArcaneShot || spell == hunter.ChimeraShot { - aura.Deactivate(sim) - } - }, - }) - } - - hunter.SteadyShot = hunter.RegisterSpell(core.SpellConfig{ - ActionID: core.ActionID{SpellID: 49052}, - SpellSchool: core.SpellSchoolPhysical, - ProcMask: core.ProcMaskRangedSpecial, - Flags: core.SpellFlagMeleeMetrics | core.SpellFlagIncludeTargetBonusDamage | core.SpellFlagAPL, - - ManaCost: core.ManaCostOptions{ - BaseCost: 0.05, - Multiplier: 1 - - 0.03*float64(hunter.Talents.Efficiency) - - 0.05*float64(hunter.Talents.MasterMarksman), - }, - Cast: core.CastConfig{ - DefaultCast: core.Cast{ - GCD: core.GCDDefault, - CastTime: time.Second * 2, - }, - IgnoreHaste: true, // Hunter GCD is locked at 1.5s - ModifyCast: func(_ *core.Simulation, spell *core.Spell, cast *core.Cast) { - cast.CastTime = spell.CastTime() - }, - - CastTime: func(spell *core.Spell) time.Duration { - return time.Duration(float64(spell.DefaultCast.CastTime) / hunter.RangedSwingSpeed()) - }, - }, - - BonusCritRating: 0 + - 2*core.CritRatingPerCritChance*float64(hunter.Talents.SurvivalInstincts), - DamageMultiplierAdditive: 1 + - .03*float64(hunter.Talents.FerociousInspiration) + - core.TernaryFloat64(hunter.HasSetBonus(ItemSetGronnstalker, 4), .1, 0), - DamageMultiplier: 1 * - hunter.markedForDeathMultiplier(), - CritMultiplier: hunter.critMultiplier(true, true, false), - ThreatMultiplier: 1, - - ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := 0.1*spell.RangedAttackPower(target) + - hunter.AutoAttacks.Ranged().BaseDamage(sim)*2.8/hunter.AutoAttacks.Ranged().SwingSpeed + - hunter.NormalizedAmmoDamageBonus + - 252 - - result := spell.CalcDamage(sim, target, baseDamage, spell.OutcomeRangedHitAndCrit) - if result.Landed() && impSSProcChance > 0 && sim.RandomFloat("Imp Steady Shot") < impSSProcChance { - hunter.ImprovedSteadyShotAura.Activate(sim) - } - spell.DealDamage(sim, result) - }, - }) -} diff --git a/sim/common/sod/melee_items.go b/sim/common/sod/melee_items.go index 51488fbb22..fcabf46f50 100644 --- a/sim/common/sod/melee_items.go +++ b/sim/common/sod/melee_items.go @@ -114,10 +114,12 @@ func init() { Duration: time.Second * 10, OnGain: func(aura *core.Aura, sim *core.Simulation) { character.MultiplyAttackSpeed(sim, 1.1) + character.MultiplyRangedSpeed(sim, 1.1) character.PseudoStats.ThreatMultiplier *= 1.2 }, OnExpire: func(aura *core.Aura, sim *core.Simulation) { character.MultiplyAttackSpeed(sim, 1.0/1.1) + character.MultiplyRangedSpeed(sim, 1.0/1.1) character.PseudoStats.ThreatMultiplier /= 1.2 }, }) @@ -203,10 +205,12 @@ func init() { ActionID: core.ActionID{SpellID: 437349}, Duration: time.Second * 10, OnGain: func(aura *core.Aura, sim *core.Simulation) { - character.AddStatDynamic(sim, stats.MeleeHaste, 20) + character.MultiplyAttackSpeed(sim, 1.2) + character.MultiplyRangedSpeed(sim, 1.2) }, OnExpire: func(aura *core.Aura, sim *core.Simulation) { - character.AddStatDynamic(sim, stats.MeleeHaste, -20) + character.MultiplyAttackSpeed(sim, 1.0/1.2) + character.MultiplyRangedSpeed(sim, 1.0/1.2) }, }) diff --git a/sim/common/sod/melee_sets.go b/sim/common/sod/melee_sets.go index 8caa3068e0..0ac0cee5f9 100644 --- a/sim/common/sod/melee_sets.go +++ b/sim/common/sod/melee_sets.go @@ -75,6 +75,7 @@ var ItemSetStormshroud = core.NewItemSet(core.ItemSet{ }, 4: func(a core.Agent) { a.GetCharacter().AddStat(stats.AttackPower, 14) + a.GetCharacter().AddStat(stats.RangedAttackPower, 14) }, }, }) diff --git a/sim/core/buffs.go b/sim/core/buffs.go index 097af495d4..dd29433230 100644 --- a/sim/core/buffs.go +++ b/sim/core/buffs.go @@ -670,6 +670,7 @@ func applyBuffEffects(agent Agent, raidBuffs *proto.RaidBuffs, partyBuffs *proto // TODO: character.AddStat(stats.RangedCrit, 2 * CritRatingPerCritChance) character.AddStat(stats.SpellHit, 3*SpellHitRatingPerHitChance) character.AddStat(stats.AttackPower, 20) + character.AddStat(stats.RangedAttackPower, 20) character.AddStat(stats.SpellPower, 25) } diff --git a/sim/core/consumes.go b/sim/core/consumes.go index daf8b20e7a..0945fd9211 100644 --- a/sim/core/consumes.go +++ b/sim/core/consumes.go @@ -105,6 +105,10 @@ func applyConsumeEffects(agent Agent, partyBuffs *proto.PartyBuffs) { character.AddStats(stats.Stats{ stats.Agility: 25, }) + case proto.AgilityElixir_ElixirOfAgility: + character.AddStats(stats.Stats{ + stats.Agility: 15, + }) case proto.AgilityElixir_ElixirOfLesserAgility: character.AddStats(stats.Stats{ stats.Agility: 8, @@ -183,8 +187,9 @@ func applyConsumeEffects(agent Agent, partyBuffs *proto.PartyBuffs) { switch consumes.EnchantedSigil { case proto.EnchantedSigil_InnovationSigil: character.AddStats(stats.Stats{ - stats.AttackPower: 20, - stats.SpellPower: 20, + stats.AttackPower: 20, + stats.RangedAttackPower: 20, + stats.SpellPower: 20, }) } } diff --git a/sim/core/racials.go b/sim/core/racials.go index 45bf3e1198..62c6d543d7 100644 --- a/sim/core/racials.go +++ b/sim/core/racials.go @@ -116,10 +116,12 @@ func applyRaceEffects(agent Agent) { OnGain: func(aura *Aura, sim *Simulation) { character.MultiplyCastSpeed(1.2) character.MultiplyAttackSpeed(sim, 1.2) + character.MultiplyRangedSpeed(sim, 1.2) }, OnExpire: func(aura *Aura, sim *Simulation) { character.MultiplyCastSpeed(1 / 1.2) character.MultiplyAttackSpeed(sim, 1/1.2) + character.MultiplyRangedSpeed(sim, 1/1.2) }, }) diff --git a/sim/druid/druid.go b/sim/druid/druid.go index c43b3a48bd..16b735c89b 100644 --- a/sim/druid/druid.go +++ b/sim/druid/druid.go @@ -14,6 +14,12 @@ const ( var TalentTreeSizes = [3]int{16, 16, 15} +const ( + SpellCode_DruidWrath int32 = iota + SpellCode_DruidStarfire + SpellCode_DruidStarsurge +) + type Druid struct { core.Character SelfBuffs @@ -72,6 +78,7 @@ type Druid struct { ClearcastingAura *core.Aura DemoralizingRoarAuras core.AuraArray EnrageAura *core.Aura + EclipseAura *core.Aura FaerieFireAuras core.AuraArray FrenziedRegenerationAura *core.Aura FuryOfStormrageAura *core.Aura @@ -82,6 +89,8 @@ type Druid struct { SurvivalInstinctsAura *core.Aura TigersFuryAura *core.Aura SavageRoarAura *core.Aura + SolarEclipseProcAura *core.Aura + LunarEclipseProcAura *core.Aura WildStrikesBuffAura *core.Aura BleedCategories core.ExclusiveCategoryArray diff --git a/sim/druid/runes.go b/sim/druid/runes.go index fc68edb76f..16f3ffba08 100644 --- a/sim/druid/runes.go +++ b/sim/druid/runes.go @@ -8,6 +8,7 @@ import ( ) func (druid *Druid) ApplyRunes() { + druid.applyEclipse() druid.applyFuryOfStormRage() druid.applySunfire() druid.applyStarsurge() @@ -31,6 +32,118 @@ func (druid *Druid) applyFuryOfStormRage() { }) } +func (druid *Druid) applyEclipse() { + + if !druid.HasRune(proto.DruidRune_RuneBeltEclipse) { + return + } + + // Solar + solarProcMultiplier := 30.0 + var affectedWrathSpells []*DruidSpell + druid.SolarEclipseProcAura = druid.RegisterAura(core.Aura{ + Label: "Solar Eclipse proc", + Duration: time.Second * 15, + MaxStacks: 4, + ActionID: core.ActionID{SpellID: 408250}, + OnInit: func(aura *core.Aura, sim *core.Simulation) { + affectedWrathSpells = core.FilterSlice( + druid.Wrath, func(spell *DruidSpell) bool { return spell != nil }, + ) + }, + OnGain: func(aura *core.Aura, sim *core.Simulation) { + core.Each(affectedWrathSpells, func(spell *DruidSpell) { + spell.Spell.BonusCritRating += solarProcMultiplier + }) + }, + OnExpire: func(aura *core.Aura, sim *core.Simulation) { + core.Each(affectedWrathSpells, func(spell *DruidSpell) { + spell.Spell.BonusCritRating -= solarProcMultiplier + }) + }, + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + // Assert we are casting wrath + if spell.SpellCode != SpellCode_DruidWrath && + //Do we proc this starsurge + spell.SpellCode != SpellCode_DruidStarsurge { + return + } + + if !result.Landed() { + return + } + + aura.RemoveStack(sim) + }, + }) + + // Lunar + lunarBonusCrit := 30.0 + var affectedLunarSpells []*DruidSpell + druid.LunarEclipseProcAura = druid.RegisterAura(core.Aura{ + Label: "Lunar Eclipse proc", + Duration: time.Second * 15, + MaxStacks: 4, + ActionID: core.ActionID{SpellID: 408255}, + OnInit: func(aura *core.Aura, sim *core.Simulation) { + affectedLunarSpells = append( + druid.Starfire, + druid.Starsurge, // Does this proc eclipse? + ) + affectedLunarSpells = core.FilterSlice( + affectedLunarSpells, func(spell *DruidSpell) bool { return spell != nil }, + ) + }, + OnGain: func(aura *core.Aura, sim *core.Simulation) { + core.Each(affectedLunarSpells, func(spell *DruidSpell) { + spell.Spell.BonusCritRating += lunarBonusCrit + }) + }, + OnExpire: func(aura *core.Aura, sim *core.Simulation) { + core.Each(affectedLunarSpells, func(spell *DruidSpell) { + spell.Spell.BonusCritRating -= lunarBonusCrit + }) + }, + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + // Assert we are casting Starfire + if spell.SpellCode != SpellCode_DruidStarfire { + return + } + if !result.Landed() { + return + } + + aura.RemoveStack(sim) + }, + }) + + druid.EclipseAura = druid.RegisterAura(core.Aura{ + Label: "Eclipse", + Duration: core.NeverExpires, + ActionID: core.ActionID{SpellID: 408248}, + OnReset: func(aura *core.Aura, sim *core.Simulation) { + aura.Activate(sim) + }, + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + switch spell.SpellCode { + case SpellCode_DruidWrath: + // Proc Lunar + druid.LunarEclipseProcAura.Activate(sim) + druid.LunarEclipseProcAura.SetStacks(sim, 1) + + case SpellCode_DruidStarfire: + // Proc Solar + druid.SolarEclipseProcAura.Activate(sim) + druid.SolarEclipseProcAura.SetStacks(sim, 2) + + default: + return + } + }, + }) + +} + // https://www.wowhead.com/classic/news/patch-1-15-build-52124-ptr-datamining-season-of-discovery-runes-336044#news-post-336044 func (druid *Druid) applySunfire() { if !druid.HasRune(proto.DruidRune_RuneHandsSunfire) { @@ -51,7 +164,6 @@ func (druid *Druid) applySunfire() { SpellSchool: core.SpellSchoolNature, ProcMask: core.ProcMaskSpellDamage, Flags: core.SpellFlagAPL, - ManaCost: core.ManaCostOptions{ BaseCost: 0.21, }, @@ -109,7 +221,7 @@ func (druid *Druid) applyStarsurge() { SpellSchool: core.SpellSchoolArcane, ProcMask: core.ProcMaskSpellDamage, Flags: core.SpellFlagAPL, - + SpellCode: SpellCode_DruidStarsurge, // Please check if this is right - Starsurge affected by all Starfire talents/procs? ManaCost: core.ManaCostOptions{ BaseCost: 0.01 * (1 - 0.03*float64(druid.Talents.Moonglow)), }, diff --git a/sim/druid/starfire.go b/sim/druid/starfire.go index 77c12a38a2..b3c0d72ccb 100644 --- a/sim/druid/starfire.go +++ b/sim/druid/starfire.go @@ -41,7 +41,7 @@ func (druid *Druid) newStarfireSpellConfig(rank int) core.SpellConfig { Flags: core.SpellFlagAPL, RequiredLevel: level, Rank: rank, - + SpellCode: SpellCode_DruidStarfire, ManaCost: core.ManaCostOptions{ FlatCost: manaCost * (1 - 0.03*float64(druid.Talents.Moonglow)), }, diff --git a/sim/druid/wrath.go b/sim/druid/wrath.go index 0ed32c74bf..c69d54edae 100644 --- a/sim/druid/wrath.go +++ b/sim/druid/wrath.go @@ -44,7 +44,7 @@ func (druid *Druid) newWrathSpellConfig(rank int) core.SpellConfig { RequiredLevel: level, Rank: rank, MissileSpeed: 20, - + SpellCode: SpellCode_DruidWrath, ManaCost: core.ManaCostOptions{ FlatCost: core.TernaryFloat64(druid.FuryOfStormrageAura != nil, 0, manaCost), }, diff --git a/sim/hunter/aimed_shot.go b/sim/hunter/aimed_shot.go index 76951620e0..ee54b2558e 100644 --- a/sim/hunter/aimed_shot.go +++ b/sim/hunter/aimed_shot.go @@ -62,9 +62,8 @@ func (hunter *Hunter) getAimedShotConfig(rank int, timer *core.Timer) core.Spell ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := 0.2*spell.RangedAttackPower(target) + - hunter.AutoAttacks.Ranged().BaseDamage(sim) + - hunter.AmmoDamageBonus + + baseDamage := hunter.AutoAttacks.Ranged().CalculateNormalizedWeaponDamage(sim, spell.RangedAttackPower(target)) + + hunter.NormalizedAmmoDamageBonus + spell.BonusWeaponDamage() + baseDamage diff --git a/sim/hunter/aspects.go b/sim/hunter/aspects.go index 1d0488457f..6b5bcd880c 100644 --- a/sim/hunter/aspects.go +++ b/sim/hunter/aspects.go @@ -1,6 +1,7 @@ package hunter import ( + "strconv" "time" "github.com/wowsims/sod/sim/core" @@ -32,8 +33,8 @@ func (hunter *Hunter) getAspectOfTheHawkSpellConfig(rank int) core.SpellConfig { } actionID := core.ActionID{SpellID: spellId} - hunter.AspectOfTheHawkAura = hunter.NewTemporaryStatsAuraWrapped( - "Aspect of the Hawk", + aspectOfTheHawkAura := hunter.NewTemporaryStatsAuraWrapped( + "Aspect of the Hawk"+strconv.Itoa(rank), actionID, stats.Stats{ stats.RangedAttackPower: rap, @@ -57,15 +58,11 @@ func (hunter *Hunter) getAspectOfTheHawkSpellConfig(rank int) core.SpellConfig { Rank: rank, RequiredLevel: level, - // ManaCost: core.ManaCostOptions{ - // FlatCost: manaCost, - // }, - ApplyEffects: func(sim *core.Simulation, _ *core.Unit, _ *core.Spell) { - if hunter.AspectOfTheHawkAura.IsActive() { - hunter.AspectOfTheHawkAura.Deactivate(sim) + if aspectOfTheHawkAura.IsActive() { + aspectOfTheHawkAura.Deactivate(sim) } else { - hunter.AspectOfTheHawkAura.Activate(sim) + aspectOfTheHawkAura.Activate(sim) } }, } diff --git a/sim/hunter/chimera_shot.go b/sim/hunter/chimera_shot.go index 417cc8b584..16c66db27d 100644 --- a/sim/hunter/chimera_shot.go +++ b/sim/hunter/chimera_shot.go @@ -49,9 +49,8 @@ func (hunter *Hunter) registerChimeraShotSpell() { ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := 0.2*spell.RangedAttackPower(target) + - hunter.AutoAttacks.Ranged().BaseDamage(sim) + - hunter.AmmoDamageBonus + + baseDamage := hunter.AutoAttacks.Ranged().CalculateNormalizedWeaponDamage(sim, spell.RangedAttackPower(target)) + + hunter.NormalizedAmmoDamageBonus + spell.BonusWeaponDamage() result := spell.CalcDamage(sim, target, baseDamage, spell.OutcomeRangedHitAndCrit) diff --git a/sim/hunter/explosive_trap.go b/sim/hunter/explosive_trap.go new file mode 100644 index 0000000000..7f255ece70 --- /dev/null +++ b/sim/hunter/explosive_trap.go @@ -0,0 +1,96 @@ +package hunter + +import ( + "strconv" + "time" + + "github.com/wowsims/sod/sim/core" + "github.com/wowsims/sod/sim/core/proto" +) + +func (hunter *Hunter) getExplosiveTrapConfig(rank int, timer *core.Timer) core.SpellConfig { + spellId := [4]int32{0, 409532, 409534, 409535}[rank] + dotDamage := [4]float64{0, 15, 24, 33}[rank] + minDamage := [4]float64{0, 104, 145, 208}[rank] + maxDamage := [4]float64{0, 135, 193, 265}[rank] + manaCost := [4]float64{0, 275, 395, 520}[rank] + level := [4]int{0, 34, 44, 54}[rank] + + numHits := hunter.Env.GetNumTargets() + + return core.SpellConfig{ + ActionID: core.ActionID{SpellID: spellId}, + SpellSchool: core.SpellSchoolFire, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagAPL, + Rank: rank, + RequiredLevel: level, + MissileSpeed: 24, + + ManaCost: core.ManaCostOptions{ + FlatCost: manaCost, + }, + Cast: core.CastConfig{ + CD: core.Cooldown{ + Timer: timer, + Duration: time.Second * 15, + }, + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + IgnoreHaste: true, // Hunter GCD is locked at 1.5s + }, + + DamageMultiplierAdditive: 1 + 0.15*float64(hunter.Talents.CleverTraps), + CritMultiplier: hunter.critMultiplier(true, hunter.CurrentTarget), + ThreatMultiplier: 1, + + Dot: core.DotConfig{ + IsAOE: true, + Aura: core.Aura{ + Label: "ExplosiveTrap" + hunter.Label + strconv.Itoa(rank), + Tag: "ExplosiveTrap", + }, + NumberOfTicks: 10, + TickLength: time.Second * 2, + + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.SnapshotBaseDamage = dotDamage + dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + for _, aoeTarget := range sim.Encounter.TargetUnits { + dot.CalcAndDealPeriodicSnapshotDamage(sim, aoeTarget, dot.OutcomeTick) + } + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + spell.WaitTravelTime(sim, func(s *core.Simulation) { + curTarget := target + for hitIndex := int32(0); hitIndex < numHits; hitIndex++ { + baseDamage := sim.Roll(minDamage, maxDamage) + baseDamage *= sim.Encounter.AOECapMultiplier() + spell.CalcAndDealDamage(sim, curTarget, baseDamage, spell.OutcomeMagicHitAndCrit) + curTarget = sim.Environment.NextTargetUnit(curTarget) + } + spell.AOEDot().ApplyOrReset(sim) + }) + }, + } +} + +func (hunter *Hunter) registerExplosiveTrapSpell(timer *core.Timer) { + if !hunter.HasRune(proto.HunterRune_RuneBootsTrapLauncher) { + return + } + + maxRank := 3 + for i := 1; i <= maxRank; i++ { + config := hunter.getExplosiveTrapConfig(i, timer) + + if config.RequiredLevel <= int(hunter.Level) { + hunter.ExplosiveTrap = hunter.GetOrRegisterSpell(config) + } + } +} diff --git a/sim/hunter/hunter.go b/sim/hunter/hunter.go index b63ecdc4c2..6dfb64fe63 100644 --- a/sim/hunter/hunter.go +++ b/sim/hunter/hunter.go @@ -52,6 +52,7 @@ type Hunter struct { ChimeraShot *core.Spell ExplosiveShot *core.Spell ExplosiveTrap *core.Spell + ImmolationTrap *core.Spell KillCommand *core.Spell KillShot *core.Spell MultiShot *core.Spell @@ -72,8 +73,6 @@ type Hunter struct { SniperTrainingAura *core.Aura CobraStrikesAura *core.Aura - AspectOfTheHawkAura *core.Aura - AspectOfTheViperAura *core.Aura ImprovedSteadyShotAura *core.Aura LockAndLoadAura *core.Aura RapidFireAura *core.Aura @@ -120,14 +119,20 @@ func (hunter *Hunter) Initialize() { hunter.registerAimedShotSpell(arcaneShotTimer) hunter.registerMultiShotSpell(multiShotTimer) hunter.registerChimeraShotSpell() + hunter.registerSteadyShotSpell() hunter.registerRaptorStrikeSpell() hunter.registerFlankingStrikeSpell() hunter.registerCarveSpell() hunter.registerWingClipSpell() + fireTraps := hunter.NewTimer() + + hunter.registerExplosiveTrapSpell(fireTraps) + hunter.registerImmolationTrapSpell(fireTraps) + hunter.registerKillCommand() - //hunter.registerRapidFireCD() + hunter.registerRapidFire() } func (hunter *Hunter) Reset(sim *core.Simulation) { diff --git a/sim/hunter/immolation_trap.go b/sim/hunter/immolation_trap.go new file mode 100644 index 0000000000..3e128fa885 --- /dev/null +++ b/sim/hunter/immolation_trap.go @@ -0,0 +1,86 @@ +package hunter + +import ( + "strconv" + "time" + + "github.com/wowsims/sod/sim/core" + "github.com/wowsims/sod/sim/core/proto" +) + +func (hunter *Hunter) getImmolationTrapConfig(rank int, timer *core.Timer) core.SpellConfig { + spellId := [6]int32{0, 409521, 409524, 409526, 409528, 409530}[rank] + dotDamage := [6]float64{0, 105, 215, 340, 510, 690}[rank] + manaCost := [6]float64{0, 50, 90, 135, 190, 245}[rank] + level := [6]int{0, 16, 26, 36, 46, 56}[rank] + + return core.SpellConfig{ + ActionID: core.ActionID{SpellID: spellId}, + SpellSchool: core.SpellSchoolFire, + ProcMask: core.ProcMaskSpellDamage, + Flags: core.SpellFlagAPL, + Rank: rank, + RequiredLevel: level, + MissileSpeed: 24, + + ManaCost: core.ManaCostOptions{ + FlatCost: manaCost, + }, + Cast: core.CastConfig{ + CD: core.Cooldown{ + Timer: timer, + Duration: time.Second * 15, + }, + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + }, + IgnoreHaste: true, // Hunter GCD is locked at 1.5s + }, + + DamageMultiplierAdditive: 1 + 0.15*float64(hunter.Talents.CleverTraps), + CritMultiplier: hunter.critMultiplier(true, hunter.CurrentTarget), + ThreatMultiplier: 1, + + Dot: core.DotConfig{ + Aura: core.Aura{ + Label: "ImmolationTrap" + hunter.Label + strconv.Itoa(rank), + Tag: "ImmolationTrap", + }, + NumberOfTicks: 5, + TickLength: time.Second * 3, + + OnSnapshot: func(sim *core.Simulation, target *core.Unit, dot *core.Dot, isRollover bool) { + dot.SnapshotBaseDamage = dotDamage / 5 + dot.SnapshotAttackerMultiplier = dot.Spell.AttackerDamageMultiplier(dot.Spell.Unit.AttackTables[target.UnitIndex]) + }, + OnTick: func(sim *core.Simulation, target *core.Unit, dot *core.Dot) { + dot.CalcAndDealPeriodicSnapshotDamage(sim, target, dot.OutcomeTick) + }, + }, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + result := spell.CalcOutcome(sim, target, spell.OutcomeMagicHit) + spell.WaitTravelTime(sim, func(s *core.Simulation) { + spell.DealOutcome(sim, result) + if result.Landed() { + spell.Dot(target).Apply(sim) + } + }) + }, + } +} + +func (hunter *Hunter) registerImmolationTrapSpell(timer *core.Timer) { + if !hunter.HasRune(proto.HunterRune_RuneBootsTrapLauncher) { + return + } + + maxRank := 5 + for i := 1; i <= maxRank; i++ { + config := hunter.getImmolationTrapConfig(i, timer) + + if config.RequiredLevel <= int(hunter.Level) { + hunter.ImmolationTrap = hunter.GetOrRegisterSpell(config) + } + } +} diff --git a/sim/hunter/multi_shot.go b/sim/hunter/multi_shot.go index d183e308cb..49b7008c14 100644 --- a/sim/hunter/multi_shot.go +++ b/sim/hunter/multi_shot.go @@ -57,23 +57,21 @@ func (hunter *Hunter) getMultiShotConfig(rank int, timer *core.Timer) core.Spell return hunter.DistanceFromTarget >= 8 }, - BonusCritRating: 0, - DamageMultiplierAdditive: 1 + - .05*float64(hunter.Talents.Barrage), - DamageMultiplier: 1, - CritMultiplier: hunter.critMultiplier(true, hunter.CurrentTarget), - ThreatMultiplier: 1, + BonusCritRating: 0, + DamageMultiplierAdditive: 1 + .05*float64(hunter.Talents.Barrage), + DamageMultiplier: 1, + CritMultiplier: hunter.critMultiplier(true, hunter.CurrentTarget), + ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { curTarget := target - sharedDmg := hunter.AutoAttacks.Ranged().BaseDamage(sim) + - hunter.AmmoDamageBonus + - spell.BonusWeaponDamage() + - baseDamage + sharedDmg := spell.BonusWeaponDamage() + baseDamage for hitIndex := int32(0); hitIndex < numHits; hitIndex++ { - baseDamage := sharedDmg + 0.2*spell.RangedAttackPower(curTarget) + baseDamage := sharedDmg + + hunter.AutoAttacks.Ranged().CalculateNormalizedWeaponDamage(sim, spell.RangedAttackPower(target)) + + hunter.NormalizedAmmoDamageBonus results[hitIndex] = spell.CalcDamage(sim, curTarget, baseDamage, spell.OutcomeRangedHitAndCrit) diff --git a/sim/hunter/pet.go b/sim/hunter/pet.go index c1c62cdeca..79fd35e65e 100644 --- a/sim/hunter/pet.go +++ b/sim/hunter/pet.go @@ -15,13 +15,6 @@ type HunterPet struct { KillCommandAura *core.Aura - claw *core.Spell - bite *core.Spell - furiousHowl *core.Spell - screech *core.Spell - scorpidPoison *core.Spell - lightningBreath *core.Spell - specialAbility *core.Spell focusDump *core.Spell @@ -40,6 +33,77 @@ func (hunter *Hunter) NewHunterPet() *HunterPet { } petConfig := PetConfigs[hunter.Options.PetType] + hunterPetBaseStats := stats.Stats{} + + baseMinDamage := 0.0 + baseMaxDamage := 0.0 + attackSpeed := 2.0 + + switch hunter.Level { + case 25: + baseMinDamage = 15 + baseMaxDamage = 20 + hunterPetBaseStats = stats.Stats{ + stats.Strength: 53, + stats.Agility: 45, + stats.Stamina: 120, + stats.Intellect: 29, + stats.Spirit: 39, + + stats.AttackPower: -20, + + // Add 1.8% because pets aren't affected by that component of crit suppression. + stats.MeleeCrit: (3.2 + 1.8) * core.CritRatingPerCritChance, + } + case 40: + baseMinDamage = 25 + baseMaxDamage = 40 + hunterPetBaseStats = stats.Stats{ + stats.Strength: 78, + stats.Agility: 66, + stats.Stamina: 160, + stats.Intellect: 37, + stats.Spirit: 55, + + stats.AttackPower: -20, + + // Add 1.8% because pets aren't affected by that component of crit suppression. + stats.MeleeCrit: (3.2 + 1.8) * core.CritRatingPerCritChance, + } + case 50: + // TODO: + baseMinDamage = 25 + baseMaxDamage = 40 + hunterPetBaseStats = stats.Stats{ + stats.Strength: 78, + stats.Agility: 66, + stats.Stamina: 160, + stats.Intellect: 37, + stats.Spirit: 55, + + stats.AttackPower: -20, + + // Add 1.8% because pets aren't affected by that component of crit suppression. + stats.MeleeCrit: (3.2 + 1.8) * core.CritRatingPerCritChance, + } + case 60: + // TODO: + baseMinDamage = 25 + baseMaxDamage = 40 + hunterPetBaseStats = stats.Stats{ + stats.Strength: 78, + stats.Agility: 66, + stats.Stamina: 160, + stats.Intellect: 37, + stats.Spirit: 55, + + stats.AttackPower: -20, + + // Add 1.8% because pets aren't affected by that component of crit suppression. + stats.MeleeCrit: (3.2 + 1.8) * core.CritRatingPerCritChance, + } + } + hp := &HunterPet{ Pet: core.NewPet(petConfig.Name, &hunter.Character, hunterPetBaseStats, hunter.makeStatInheritance(), true, false), config: petConfig, @@ -50,9 +114,9 @@ func (hunter *Hunter) NewHunterPet() *HunterPet { hp.EnableAutoAttacks(hp, core.AutoAttackOptions{ MainHand: core.Weapon{ - BaseDamageMin: 27, - BaseDamageMax: 37, - SwingSpeed: 2.0, + BaseDamageMin: baseMinDamage * (attackSpeed / 2.0), + BaseDamageMax: baseMaxDamage * (attackSpeed / 2.0), + SwingSpeed: attackSpeed, CritMultiplier: hp.MeleeCritMultiplier(1, 0), }, AutoSwingMelee: true, @@ -158,15 +222,6 @@ func (hp *HunterPet) killCommandMult() float64 { return 1 + 0.2*float64(hp.KillCommandAura.GetStacks()) } -var hunterPetBaseStats = stats.Stats{ - stats.Agility: 45, - stats.Strength: 53, - stats.AttackPower: -20, // Apparently pets and warriors have a AP penalty. - - // Add 1.8% because pets aren't affected by that component of crit suppression. - stats.MeleeCrit: (3.2 + 1.8) * core.CritRatingPerCritChance, -} - const PetExpertiseScale = 3.25 func (hunter *Hunter) makeStatInheritance() core.PetStatInheritance { diff --git a/sim/_hunter/rapid_fire.go b/sim/hunter/rapid_fire.go similarity index 53% rename from sim/_hunter/rapid_fire.go rename to sim/hunter/rapid_fire.go index 4601d59b33..8c88280d9c 100644 --- a/sim/_hunter/rapid_fire.go +++ b/sim/hunter/rapid_fire.go @@ -6,33 +6,22 @@ import ( "github.com/wowsims/sod/sim/core" ) -func (hunter *Hunter) registerRapidFireCD() { - actionID := core.ActionID{SpellID: 3045} - - var manaMetrics *core.ResourceMetrics - if hunter.Talents.RapidRecuperation > 0 { - manaMetrics = hunter.NewManaMetrics(core.ActionID{SpellID: 53232}) +func (hunter *Hunter) registerRapidFire() { + if hunter.Level < 26 { + return } + actionID := core.ActionID{SpellID: 3045} + hasteMultiplier := 1.4 hunter.RapidFireAura = hunter.RegisterAura(core.Aura{ Label: "Rapid Fire", ActionID: actionID, Duration: time.Second * 15, + OnGain: func(aura *core.Aura, sim *core.Simulation) { aura.Unit.MultiplyRangedSpeed(sim, hasteMultiplier) - - if manaMetrics != nil { - manaPerTick := 0.02 * float64(hunter.Talents.RapidRecuperation) * hunter.MaxMana() - core.StartPeriodicAction(sim, core.PeriodicActionOptions{ - Period: time.Second * 3, - NumTicks: 5, - OnAction: func(sim *core.Simulation) { - hunter.AddMana(sim, manaPerTick, manaMetrics) - }, - }) - } }, OnExpire: func(aura *core.Aura, sim *core.Simulation) { aura.Unit.MultiplyRangedSpeed(sim, 1/hasteMultiplier) @@ -43,18 +32,14 @@ func (hunter *Hunter) registerRapidFireCD() { ActionID: actionID, ManaCost: core.ManaCostOptions{ - BaseCost: 0.03, + FlatCost: 100, }, Cast: core.CastConfig{ CD: core.Cooldown{ Timer: hunter.NewTimer(), - Duration: time.Minute*5 - time.Minute*time.Duration(hunter.Talents.RapidKilling), + Duration: time.Minute * 5, }, }, - ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { - // Make sure we don't reuse after a Readiness cast. - return !hunter.RapidFireAura.IsActive() - }, ApplyEffects: func(sim *core.Simulation, _ *core.Unit, _ *core.Spell) { hunter.RapidFireAura.Activate(sim) diff --git a/sim/hunter/runes.go b/sim/hunter/runes.go index 6110161f9e..c1b46cf4df 100644 --- a/sim/hunter/runes.go +++ b/sim/hunter/runes.go @@ -37,6 +37,78 @@ func (hunter *Hunter) ApplyRunes() { hunter.applySniperTraining() hunter.applyCobraStrikes() + hunter.applyExposeWeakness() + hunter.applyInvigoration() +} + +func (hunter *Hunter) applyInvigoration() { + if !hunter.HasRune(proto.HunterRune_RuneBootsInvigoration) || hunter.pet == nil { + return + } + + procSpellId := core.ActionID{SpellID: 437999} + metrics := hunter.NewManaMetrics(procSpellId) + procSpell := hunter.RegisterSpell(core.SpellConfig{ + ActionID: procSpellId, + SpellSchool: core.SpellSchoolNature, + ApplyEffects: func(sim *core.Simulation, u *core.Unit, spell *core.Spell) { + hunter.AddMana(sim, hunter.MaxMana()*0.05, metrics) + }, + }) + + core.MakePermanent(hunter.pet.GetOrRegisterAura(core.Aura{ + Label: "Invigoration", + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if !spell.ProcMask.Matches(core.ProcMaskMeleeSpecial) { + return + } + + if !result.DidCrit() { + return + } + + procSpell.Cast(sim, result.Target) + }, + })) +} + +func (hunter *Hunter) applyExposeWeakness() { + if !hunter.HasRune(proto.HunterRune_RuneBeltExposeWeakness) { + return + } + + apBonus := hunter.NewDynamicStatDependency(stats.Agility, stats.AttackPower, 0.4) + apRangedBonus := hunter.NewDynamicStatDependency(stats.Agility, stats.RangedAttackPower, 0.4) + + procAura := hunter.GetOrRegisterAura(core.Aura{ + Label: "Expose Weakness Proc", + ActionID: core.ActionID{SpellID: 409507}, + Duration: time.Second * 7, + + OnGain: func(aura *core.Aura, sim *core.Simulation) { + hunter.EnableDynamicStatDep(sim, apBonus) + hunter.EnableDynamicStatDep(sim, apRangedBonus) + }, + OnExpire: func(aura *core.Aura, sim *core.Simulation) { + hunter.DisableDynamicStatDep(sim, apBonus) + hunter.DisableDynamicStatDep(sim, apRangedBonus) + }, + }) + + core.MakePermanent(hunter.GetOrRegisterAura(core.Aura{ + Label: "Expose Weakness", + OnSpellHitDealt: func(aura *core.Aura, sim *core.Simulation, spell *core.Spell, result *core.SpellResult) { + if !spell.ProcMask.Matches(core.ProcMaskMeleeOrRanged) { + return + } + + if !result.DidCrit() { + return + } + + procAura.Activate(sim) + }, + })) } func (hunter *Hunter) applySniperTraining() { diff --git a/sim/hunter/serpent_sting.go b/sim/hunter/serpent_sting.go index 5e4c6444f4..3d0dae32fb 100644 --- a/sim/hunter/serpent_sting.go +++ b/sim/hunter/serpent_sting.go @@ -17,7 +17,7 @@ func (hunter *Hunter) getSerpentStingConfig(rank int) core.SpellConfig { return core.SpellConfig{ ActionID: core.ActionID{SpellID: spellId}, SpellSchool: core.SpellSchoolNature, - ProcMask: core.ProcMaskEmpty, + ProcMask: core.ProcMaskRangedSpecial, Flags: core.SpellFlagAPL | core.SpellFlagPureDot, Rank: rank, RequiredLevel: level, @@ -37,9 +37,6 @@ func (hunter *Hunter) getSerpentStingConfig(rank int) core.SpellConfig { return hunter.DistanceFromTarget >= 8 }, - // Need to specially apply LethalShots here, because this spell uses an empty proc mask - BonusCritRating: 1 * core.CritRatingPerCritChance * float64(hunter.Talents.LethalShots), - DamageMultiplierAdditive: 1 + 0.02*float64(hunter.Talents.ImprovedSerpentSting), CritMultiplier: hunter.critMultiplier(true, hunter.CurrentTarget), ThreatMultiplier: 1, @@ -81,7 +78,7 @@ func (hunter *Hunter) getSerpentStingConfig(rank int) core.SpellConfig { func (hunter *Hunter) chimeraShotSerpentStingSpell(rank int) *core.Spell { baseDamage := [10]float64{0, 20, 40, 80, 140, 210, 290, 385, 490, 555}[rank] - spellCoeff := [10]float64{0, .08, .125, .185, .2, .2, .2, .2, .2, .2}[rank] + spellCoeff := 0.4 return hunter.RegisterSpell(core.SpellConfig{ ActionID: core.ActionID{SpellID: 409493}, @@ -90,12 +87,12 @@ func (hunter *Hunter) chimeraShotSerpentStingSpell(rank int) *core.Spell { Flags: core.SpellFlagMeleeMetrics, DamageMultiplierAdditive: 1 + 0.02*float64(hunter.Talents.ImprovedSerpentSting), - DamageMultiplier: 0.4, + DamageMultiplier: 1, CritMultiplier: hunter.critMultiplier(true, hunter.CurrentTarget), ThreatMultiplier: 1, ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { - baseDamage := baseDamage + spellCoeff*spell.SpellPower() + baseDamage := baseDamage*0.4 + spellCoeff*spell.SpellPower() spell.CalcAndDealDamage(sim, target, baseDamage, spell.OutcomeRangedCritOnly) }, }) diff --git a/sim/hunter/steady_shot.go b/sim/hunter/steady_shot.go new file mode 100644 index 0000000000..48d5e6033f --- /dev/null +++ b/sim/hunter/steady_shot.go @@ -0,0 +1,72 @@ +package hunter + +import ( + "time" + + "github.com/wowsims/sod/sim/core" + "github.com/wowsims/sod/sim/core/proto" +) + +func (hunter *Hunter) registerSteadyShotSpell() { + if !hunter.HasRune(proto.HunterRune_RuneBeltSteadyShot) { + return + } + + hasCobraStrikes := hunter.pet != nil && hunter.HasRune(proto.HunterRune_RuneChestCobraStrikes) + + manaCostMultiplier := 1 - 0.02*float64(hunter.Talents.Efficiency) + if hunter.HasRune(proto.HunterRune_RuneChestMasterMarksman) { + manaCostMultiplier -= 0.25 + } + + hunter.GetOrRegisterSpell(core.SpellConfig{ + ActionID: core.ActionID{SpellID: 437123}, + SpellSchool: core.SpellSchoolPhysical, + ProcMask: core.ProcMaskRangedSpecial, + Flags: core.SpellFlagMeleeMetrics | core.SpellFlagIncludeTargetBonusDamage | core.SpellFlagAPL, + MissileSpeed: 24, + + ManaCost: core.ManaCostOptions{ + BaseCost: 0.05, + Multiplier: manaCostMultiplier, + }, + Cast: core.CastConfig{ + DefaultCast: core.Cast{ + GCD: core.GCDDefault, + CastTime: time.Millisecond * 1500, + }, + ModifyCast: func(_ *core.Simulation, spell *core.Spell, cast *core.Cast) { + cast.CastTime = spell.CastTime() + }, + IgnoreHaste: true, // Hunter GCD is locked at 1.5s + CastTime: func(spell *core.Spell) time.Duration { + return time.Duration(float64(spell.DefaultCast.CastTime) / hunter.RangedSwingSpeed()) + }, + }, + ExtraCastCondition: func(sim *core.Simulation, target *core.Unit) bool { + return hunter.DistanceFromTarget >= 8 + }, + + BonusCritRating: 0, + DamageMultiplierAdditive: 1, + DamageMultiplier: 0.6, + CritMultiplier: hunter.critMultiplier(true, hunter.CurrentTarget), + ThreatMultiplier: 1, + + ApplyEffects: func(sim *core.Simulation, target *core.Unit, spell *core.Spell) { + baseDamage := hunter.AutoAttacks.Ranged().CalculateWeaponDamage(sim, spell.RangedAttackPower(target)) + + hunter.AmmoDamageBonus + + spell.BonusWeaponDamage() + + result := spell.CalcDamage(sim, target, baseDamage, spell.OutcomeRangedHitAndCrit) + spell.WaitTravelTime(sim, func(s *core.Simulation) { + spell.DealDamage(sim, result) + + if hasCobraStrikes && result.DidCrit() { + hunter.CobraStrikesAura.Activate(sim) + hunter.CobraStrikesAura.SetStacks(sim, 2) + } + }) + }, + }) +} diff --git a/ui/balance_druid/apls/phase_2.apl.json b/ui/balance_druid/apls/phase_2.apl.json new file mode 100644 index 0000000000..b14423c19f --- /dev/null +++ b/ui/balance_druid/apls/phase_2.apl.json @@ -0,0 +1,108 @@ +{ + "type": "TypeAPL", + "prepullActions": [ + { + "action": { + "castSpell": { + "spellId": { + "spellId": 778, + "rank": 2 + } + } + }, + "doAtValue": { "const": { "val": "-1s" } } + } + ], + "priorityList": [ + { + "action": { + "condition": { "spellCanCast": { "spellId": { "spellId": 417157 } } }, + "castSpell": { "spellId": { "spellId": 417157 } } + } + }, + { + "action": { + "condition": { "not": { "val": { "dotIsActive": { "spellId": { "spellId": 414684 } } } } }, + "castSpell": { "spellId": { "spellId": 414684 } } + } + }, + { + "action": { + "condition": { + "not": { + "val": { + "dotIsActive": { + "spellId": { + "spellId": 8929, + "rank": 7 + } + } + } + } + }, + "castSpell": { + "spellId": { + "spellId": 8929, + "rank": 7 + } + } + } + }, + { + "action": { + "condition": { "currentMana": {} }, + "sequence": { + "name": "w", + "actions": [ + { + "castSpell": { + "spellId": { + "spellId": 6780, + "rank": 6 + } + } + }, + { + "castSpell": { + "spellId": { + "spellId": 6780, + "rank": 6 + } + } + }, + { "resetSequence": { "sequenceName": "s" } }, + { "sequence": { "name": "s" } } + ] + } + } + }, + { + "action": { + "condition": { "currentMana": {} }, + "sequence": { + "name": "s", + "actions": [ + { + "castSpell": { + "spellId": { + "spellId": 8950, + "rank": 3 + } + } + }, + { + "castSpell": { + "spellId": { + "spellId": 8950, + "rank": 3 + } + } + }, + { "resetSequence": { "sequenceName": "w" } }, + { "sequence": { "name": "w" } } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/ui/core/components/icon_enum_picker.tsx b/ui/core/components/icon_enum_picker.tsx index bb136da5f1..b19d18815f 100644 --- a/ui/core/components/icon_enum_picker.tsx +++ b/ui/core/components/icon_enum_picker.tsx @@ -63,8 +63,10 @@ export class IconEnumPicker extends Input { this.config.changedEvent(this.modObject).on(_ => { if (this.showWhen()) { + this.restoreValue(); this.rootElem.classList.remove('hide'); } else { + this.storeValue(); this.rootElem.classList.add('hide'); } }); @@ -123,6 +125,11 @@ export class IconEnumPicker extends Input { optionContainer.classList.remove('hide'); } else { optionContainer.classList.add('hide'); + // Zero out the picker if the selected option is hidden + if (this.currentValue == valueConfig.value) { + this.setInputValue(this.config.zeroValue) + this.inputChanged(TypedEvent.nextEventID()) + } } }; @@ -152,7 +159,9 @@ export class IconEnumPicker extends Input { * restoration. Useful for events which trigger the element * on and off. */ - public storeValue(){ + storeValue() { + if (typeof this.storedValue !== 'undefined') return; + this.storedValue = this.getInputValue(); this.setInputValue(this.config.zeroValue); this.inputChanged(TypedEvent.nextEventID()); @@ -161,8 +170,8 @@ export class IconEnumPicker extends Input { /** * Restores value of current input and shows the element. */ - public restoreValue(){ - if (!this.storedValue) return; + restoreValue() { + if (typeof this.storedValue === 'undefined') return; this.setInputValue(this.storedValue); this.inputChanged(TypedEvent.nextEventID()); diff --git a/ui/core/components/individual_sim_ui/settings_tab.ts b/ui/core/components/individual_sim_ui/settings_tab.ts index 8b94cdf89a..e890b36da7 100644 --- a/ui/core/components/individual_sim_ui/settings_tab.ts +++ b/ui/core/components/individual_sim_ui/settings_tab.ts @@ -128,9 +128,9 @@ export class SettingsTab extends SimTab { value: level, }; }), - changedEvent: sim => sim.levelChangeEmitter, - getValue: sim => sim.getLevel(), - setValue: (eventID, sim, newValue) => sim.setLevel(eventID, newValue), + changedEvent: player => player.levelChangeEmitter, + getValue: player => player.getLevel(), + setValue: (eventID, player, newValue) => player.setLevel(eventID, newValue), }); const races = specToEligibleRaces[this.simUI.player.spec]; @@ -142,9 +142,9 @@ export class SettingsTab extends SimTab { value: race, }; }), - changedEvent: sim => sim.raceChangeEmitter, - getValue: sim => sim.getRace(), - setValue: (eventID, sim, newValue) => sim.setRace(eventID, newValue), + changedEvent: player => player.raceChangeEmitter, + getValue: player => player.getRace(), + setValue: (eventID, player, newValue) => player.setRace(eventID, newValue), }); if (this.simUI.individualConfig.playerInputs?.inputs.length) { @@ -163,9 +163,9 @@ export class SettingsTab extends SimTab { value: p, }; }), - changedEvent: sim => sim.professionChangeEmitter, - getValue: sim => sim.getProfession1(), - setValue: (eventID, sim, newValue) => sim.setProfession1(eventID, newValue), + changedEvent: player => player.professionChangeEmitter, + getValue: player => player.getProfession1(), + setValue: (eventID, player, newValue) => player.setProfession1(eventID, newValue), }); new EnumPicker(professionGroup, this.simUI.player, { @@ -176,9 +176,9 @@ export class SettingsTab extends SimTab { value: p, }; }), - changedEvent: sim => sim.professionChangeEmitter, - getValue: sim => sim.getProfession2(), - setValue: (eventID, sim, newValue) => sim.setProfession2(eventID, newValue), + changedEvent: player => player.professionChangeEmitter, + getValue: player => player.getProfession2(), + setValue: (eventID, player, newValue) => player.setProfession2(eventID, newValue), }); } diff --git a/ui/core/components/inputs/consumables.ts b/ui/core/components/inputs/consumables.ts index 6a325f4f18..dbcc6bef6b 100644 --- a/ui/core/components/inputs/consumables.ts +++ b/ui/core/components/inputs/consumables.ts @@ -335,10 +335,17 @@ export const ElixirOfTheMongoose: ConsumableInputConfig = { }; export const ElixirOfGreaterAgility: ConsumableInputConfig = { actionId: (player: Player) => player.getMatchingItemActionId([ - { id: 9187, minLevel: 38 }, + // Requires skill 240 + { id: 9187, minLevel: 41 }, ]), value: AgilityElixir.ElixirOfGreaterAgility, }; +export const ElixirOfAgility: ConsumableInputConfig = { + actionId: (player: Player) => player.getMatchingItemActionId([ + { id: 8949, minLevel: 27 }, + ]), + value: AgilityElixir.ElixirOfAgility, +}; export const ElixirOfLesserAgility: ConsumableInputConfig = { actionId: (player: Player) => player.getMatchingItemActionId([ { id: 3390, minLevel: 18 }, @@ -358,6 +365,7 @@ export const ScrollOfAgility: ConsumableInputConfig = { export const AGILITY_CONSUMES_CONFIG: ConsumableStatOption[] = [ { config: ElixirOfTheMongoose, stats: [Stat.StatAgility] }, { config: ElixirOfGreaterAgility, stats: [Stat.StatAgility] }, + { config: ElixirOfAgility, stats: [Stat.StatAgility] }, { config: ElixirOfLesserAgility, stats: [Stat.StatAgility] }, { config: ScrollOfAgility, stats: [Stat.StatAgility] }, ]; diff --git a/ui/core/encounter.ts b/ui/core/encounter.ts index 23a8d179f0..d80b3f7e16 100644 --- a/ui/core/encounter.ts +++ b/ui/core/encounter.ts @@ -162,13 +162,15 @@ export class Encounter { } applyDefaults(eventID: EventID) { + const level = this.sim.raid.getPlayer(0)?.getLevel() ?? Mechanics.CURRENT_LEVEL_CAP; + const presetTarget = this.presetTargets.find(preset => (preset.target?.level ?? 0) >= level) ?? this.presetTargets[0]; this.fromProto(eventID, EncounterProto.create({ duration: 60, durationVariation: 5, executeProportion20: 0.2, executeProportion25: 0.25, executeProportion35: 0.35, - targets: [this.presetTargets[0].target!], + targets: [presetTarget.target!], })); } diff --git a/ui/core/launched_sims.ts b/ui/core/launched_sims.ts index ea6f06bdaa..d3d45c4633 100644 --- a/ui/core/launched_sims.ts +++ b/ui/core/launched_sims.ts @@ -52,7 +52,7 @@ export const simLaunchStatuses: Record = { status: LaunchStatus.Unlaunched, }, [Spec.SpecHunter]: { - phase: Phase.Phase1, + phase: Phase.Phase2, status: LaunchStatus.Alpha, }, [Spec.SpecMage]: { diff --git a/ui/hunter/apls/melee.p2.json b/ui/hunter/apls/melee.p2.json new file mode 100644 index 0000000000..890db3a9c3 --- /dev/null +++ b/ui/hunter/apls/melee.p2.json @@ -0,0 +1,14 @@ +{ + "type": "TypeAPL", + "prepullActions": [ + {"action":{"castSpell":{"spellId":{"spellId":14318,"rank":2}}},"doAtValue":{"const":{"val":"-1.5s"}}} + ], + "priorityList": [ + {"action":{"autocastOtherCooldowns":{}}}, + {"action":{"castSpell":{"spellId":{"spellId":415320}}}}, + {"action":{"castSpell":{"spellId":{"spellId":415341,"rank":6}}}}, + {"action":{"castSpell":{"spellId":{"spellId":425711}}}}, + {"action":{"castSpell":{"spellId":{"itemId":215168}}}}, + {"action":{"castSpell":{"spellId":{"spellId":14267,"rank":2}}}} + ] +} \ No newline at end of file diff --git a/ui/hunter/apls/melee.weave.25.json b/ui/hunter/apls/melee.weave.p1.json similarity index 100% rename from ui/hunter/apls/melee.weave.25.json rename to ui/hunter/apls/melee.weave.p1.json diff --git a/ui/hunter/gear_sets/phase2.json b/ui/hunter/gear_sets/phase2.json new file mode 100644 index 0000000000..076c73419e --- /dev/null +++ b/ui/hunter/gear_sets/phase2.json @@ -0,0 +1,19 @@ +{"items": [ + {"id":215166}, + {"id":19540}, + {"id":213304}, + {"id":213308,"enchant":247}, + {"id":213314,"enchant":866,"rune":415370}, + {"id":213317}, + {"id":213278,"enchant":904,"rune":425711}, + {"id":213325,"rune":415352}, + {"id":213333,"rune":415320}, + {"id":6423,"enchant":849,"rune":409687}, + {"id":2951}, + {"id":9533}, + {"id":213348}, + {"id":211449}, + {"id":213409,"enchant":943}, + {"id":213442,"enchant":943}, + {"id":216516} +]} \ No newline at end of file diff --git a/ui/hunter/presets.ts b/ui/hunter/presets.ts index 5e2511a808..b4f152895e 100644 --- a/ui/hunter/presets.ts +++ b/ui/hunter/presets.ts @@ -13,6 +13,7 @@ import { Hunter_Options as HunterOptions, Hunter_Options_Ammo as Ammo, Hunter_Options_PetType as PetType, + Hunter_Options_QuiverBonus, } from '../core/proto/hunter.js'; import * as PresetUtils from '../core/preset_utils.js'; @@ -26,38 +27,45 @@ import * as PresetUtils from '../core/preset_utils.js'; /////////////////////////////////////////////////////////////////////////// import Phase1Gear from './gear_sets/phase1.json'; +import Phase2Gear from './gear_sets/phase2.json'; export const GearBeastMasteryPhase1 = PresetUtils.makePresetGear('P1 Beast Mastery', Phase1Gear, { talentTree: 0 }) export const GearMarksmanPhase1 = PresetUtils.makePresetGear('P1 Marksmanship', Phase1Gear, { talentTree: 1 }) export const GearSurvivalPhase1 = PresetUtils.makePresetGear('P1 Survival', Phase1Gear, { talentTree: 2 }) +export const GearPhase2 = PresetUtils.makePresetGear('P2 Gear', Phase2Gear) + export const GearPresets = { - [Phase.Phase1]: [ - GearBeastMasteryPhase1, + [Phase.Phase1]: [ + GearBeastMasteryPhase1, GearMarksmanPhase1, GearSurvivalPhase1, - ], - [Phase.Phase2]: [ - ] + ], + [Phase.Phase2]: [ + GearPhase2 + ] }; // TODO: Add Phase 2 preset and pull from map -export const DefaultGear = GearPresets[Phase.Phase1][0]; +export const DefaultGear = GearPhase2; /////////////////////////////////////////////////////////////////////////// // APL Presets /////////////////////////////////////////////////////////////////////////// -import MeleeWeaveP1 from './apls/melee.weave.25.json'; +import MeleeWeaveP1 from './apls/melee.weave.p1.json'; +import MeleeP2 from './apls/melee.p2.json'; -export const APLMeleeWeavePhase1 = PresetUtils.makePresetAPLRotation('Melee Weave P1', MeleeWeaveP1, { talentTree: 0 }); +export const APLMeleeWeavePhase1 = PresetUtils.makePresetAPLRotation('Melee Weave P1', MeleeWeaveP1); +export const APLMeleePhase2 = PresetUtils.makePresetAPLRotation('Melee P2', MeleeP2); export const APLPresets = { - [Phase.Phase1]: [ - APLMeleeWeavePhase1, - ], - [Phase.Phase2]: [ - ] + [Phase.Phase1]: [ + APLMeleeWeavePhase1, + ], + [Phase.Phase2]: [ + APLMeleePhase2 + ] }; // TODO: Add Phase 2 preset and pull from map @@ -68,9 +76,9 @@ export const DefaultAPLs: Record { return Presets.DefaultAPLs[player.getLevel()][player.getTalentTree()].rotation.rotation!; }, - - simpleRotation: (player, simple, cooldowns) => { - let [prepullActions, actions] = AplUtils.standardCooldownDefaults(cooldowns); - - const serpentSting = APLAction.fromJsonString(`{"condition":{"cmp":{"op":"OpGt","lhs":{"remainingTime":{}},"rhs":{"const":{"val":"6s"}}}},"multidot":{"spellId":{"spellId":49001},"maxDots":${simple.multiDotSerpentSting ? 3 : 1},"maxOverlap":{"const":{"val":"0ms"}}}}`); - const scorpidSting = APLAction.fromJsonString(`{"condition":{"auraShouldRefresh":{"auraId":{"spellId":3043},"maxOverlap":{"const":{"val":"0ms"}}}},"castSpell":{"spellId":{"spellId":3043}}}`); - const _trapWeave = APLAction.fromJsonString(`{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":49067}}}}},"castSpell":{"spellId":{"tag":1,"spellId":49067}}}`); - const volley = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":58434}}}`); - const killShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":61006}}}`); - const aimedShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49050}}}`); - const multiShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49048}}}`); - const steadyShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49052}}}`); - const silencingShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":34490}}}`); - const chimeraShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":53209}}}`); - const blackArrow = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":63672}}}`); - const explosiveShot4 = APLAction.fromJsonString(`{"condition":{"not":{"val":{"dotIsActive":{"spellId":{"spellId":60053}}}}},"castSpell":{"spellId":{"spellId":60053}}}`); - const _explosiveShot3 = APLAction.fromJsonString(`{"condition":{"dotIsActive":{"spellId":{"spellId":60053}}},"castSpell":{"spellId":{"spellId":60052}}}`); - //const arcaneShot = APLAction.fromJsonString(`{"castSpell":{"spellId":{"spellId":49045}}}`); - - const talentTree = player.getTalentTree(); - if (simple.type == Hunter_Rotation_RotationType.Aoe) { - actions.push(...[ - simple.sting == StingType.ScorpidSting ? scorpidSting : null, - simple.sting == StingType.SerpentSting ? serpentSting : null, - volley, - ].filter(a => a) as Array) - } else if (talentTree == 0) { // BM - actions.push(...[ - killShot, - simple.sting == StingType.ScorpidSting ? scorpidSting : null, - simple.sting == StingType.SerpentSting ? serpentSting : null, - aimedShot, - multiShot, - steadyShot, - ].filter(a => a) as Array) - } else if (talentTree == 1) { // MM - actions.push(...[ - silencingShot, - killShot, - simple.sting == StingType.ScorpidSting ? scorpidSting : null, - simple.sting == StingType.SerpentSting ? serpentSting : null, - chimeraShot, - aimedShot, - multiShot, - steadyShot, - ].filter(a => a) as Array) - } else if (talentTree == 2) { // SV - actions.push(...[ - killShot, - explosiveShot4, - simple.sting == StingType.ScorpidSting ? scorpidSting : null, - simple.sting == StingType.SerpentSting ? serpentSting : null, - blackArrow, - aimedShot, - multiShot, - steadyShot, - ].filter(a => a) as Array) - } - - return APLRotation.create({ - prepullActions: prepullActions, - priorityList: actions.map(action => APLListItem.create({ - action: action, - })) - }); - }, raidSimPresets: [ {