diff --git a/README.md b/README.md index 58d8a4a7f..6bf77b9bd 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ Not yet implemented: To calculate EPs for a single character definition, use the following command: -`./tbcsim --calc-ep-single ` +`./tbcsim --calc-ep ` This uses the sim defaults of a step interval of 10ms and an iteration count of 10,000 - both can be adjusted to your preference. See the CLI usage below, or just run `./tbcsim`. diff --git a/src/commonMain/kotlin/character/Mutex.kt b/src/commonMain/kotlin/character/Mutex.kt index 848734f13..2b488b916 100644 --- a/src/commonMain/kotlin/character/Mutex.kt +++ b/src/commonMain/kotlin/character/Mutex.kt @@ -18,6 +18,7 @@ enum class Mutex { BUFF_EXPOSE_WEAKNESS, BUFF_FEROCIOUS_INSPIRATION, BUFF_FAERIE_FIRE, + BUFF_SPIRIT, // Hunter BUFF_HUNTER_ASPECT, diff --git a/src/commonMain/kotlin/character/Spec.kt b/src/commonMain/kotlin/character/Spec.kt index 03236eb99..84d2e1214 100644 --- a/src/commonMain/kotlin/character/Spec.kt +++ b/src/commonMain/kotlin/character/Spec.kt @@ -12,9 +12,9 @@ abstract class Spec { Triple("strength", Stats(strength = 50), 50.0), Triple("agility", Stats(agility = 50), 50.0), Triple("meleeCritRating", Stats(meleeCritRating = 5.0 * Rating.critPerPct), 5.0 * Rating.critPerPct), - Triple("physicalHitRating", Stats(physicalHitRating = 2.0 * Rating.physicalHitPerPct), 2.0 * Rating.physicalHitPerPct), + Triple("physicalHitRating", Stats(physicalHitRating = -5.0 * Rating.physicalHitPerPct), -5.0 * Rating.physicalHitPerPct), Triple("physicalHasteRating", Stats(physicalHasteRating = 5.0 * Rating.hastePerPct), 5.0 * Rating.hastePerPct), - Triple("expertiseRating", Stats(expertiseRating = 2.0 * Rating.expertisePerPct), 2.0 * Rating.expertisePerPct), + Triple("expertiseRating", Stats(expertiseRating = -2.0 * Rating.expertisePerPct), -2.0 * Rating.expertisePerPct), Triple("armorPen", Stats(armorPen = 100), 100.0), ) @@ -23,7 +23,7 @@ abstract class Spec { val defaultRangedDeltas: List = listOf( Triple("agility", Stats(agility = 50), 50.0), Triple("rangedCritRating", Stats(rangedCritRating = 5.0 * Rating.critPerPct), 5.0 * Rating.critPerPct), - Triple("physicalHitRating", Stats(physicalHitRating = 2.0 * Rating.physicalHitPerPct), 2.0 * Rating.physicalHitPerPct), + Triple("physicalHitRating", Stats(physicalHitRating = -2.0 * Rating.physicalHitPerPct), -2.0 * Rating.physicalHitPerPct), Triple("physicalHasteRating", Stats(physicalHasteRating = 5.0 * Rating.hastePerPct), 5.0 * Rating.hastePerPct), Triple("armorPen", Stats(armorPen = 100), 100.0) ) @@ -33,11 +33,11 @@ abstract class Spec { // AKA Enhancement Shaman val casterHybridDeltas = listOf( Triple("spellCritRating", Stats(spellCritRating = 5.0 * Rating.critPerPct), 5.0 * Rating.critPerPct), - Triple("spellHitRating", Stats(spellHitRating = 5.0 * Rating.spellHitPerPct), 5.0 * Rating.spellHitPerPct) + Triple("spellHitRating", Stats(spellHitRating = -5.0 * Rating.spellHitPerPct), -5.0 * Rating.spellHitPerPct) ) val defaultCasterDeltas: List = listOf( Triple("intellect", Stats(intellect = 50), 50.0), - Triple("spellHasteRating", Stats(spellHasteRating = 10.0 * Rating.hastePerPct), 10.0 * Rating.hastePerPct), + Triple("spellHasteRating", Stats(spellHasteRating = 5.0 * Rating.hastePerPct), 5.0 * Rating.hastePerPct), ) + casterHybridDeltas } abstract val name: String diff --git a/src/commonMain/kotlin/character/classes/mage/Mage.kt b/src/commonMain/kotlin/character/classes/mage/Mage.kt index 8152e8a32..d99d2dbbf 100644 --- a/src/commonMain/kotlin/character/classes/mage/Mage.kt +++ b/src/commonMain/kotlin/character/classes/mage/Mage.kt @@ -36,6 +36,7 @@ class Mage(talents: Map, spec: Spec) : Class(talents, spec) { IcyVeins.name -> IcyVeins() ManaEmerald.name -> ManaEmerald() MoltenArmor.name -> MoltenArmor() + MageArmor.name -> MageArmor() PresenceOfMind.name -> PresenceOfMind() Scorch.name -> Scorch() SummonWaterElemental.name -> SummonWaterElemental() diff --git a/src/commonMain/kotlin/character/classes/mage/abilities/Frostbolt.kt b/src/commonMain/kotlin/character/classes/mage/abilities/Frostbolt.kt index 512a0cf6e..62977d93c 100644 --- a/src/commonMain/kotlin/character/classes/mage/abilities/Frostbolt.kt +++ b/src/commonMain/kotlin/character/classes/mage/abilities/Frostbolt.kt @@ -49,7 +49,7 @@ class Frostbolt : Ability() { val spellPowerCoeff = Spell.spellPowerCoeff(baseCastTimeMs) override fun cast(sp: SimParticipant) { val elementalPrecision: ElementalPrecision? = sp.character.klass.talentInstance(ElementalPrecision.name) - val emHit = elementalPrecision?.bonusFireFrostHitPct() ?: 0.0 + val emHit = 2 * (elementalPrecision?.bonusFireFrostHitPct() ?: 0.0) val empFb: EmpoweredFrostbolt? = sp.character.klass.talentInstance(EmpoweredFrostbolt.name) val bonusFbCrit = empFb?.frostboltAddlCritPct() ?: 0.0 diff --git a/src/commonMain/kotlin/character/classes/mage/abilities/MageArmor.kt b/src/commonMain/kotlin/character/classes/mage/abilities/MageArmor.kt new file mode 100644 index 000000000..e188e0eae --- /dev/null +++ b/src/commonMain/kotlin/character/classes/mage/abilities/MageArmor.kt @@ -0,0 +1,32 @@ +package character.classes.mage.abilities + +import character.Ability +import character.Buff +import character.Stats +import mechanics.General +import sim.SimParticipant + +class MageArmor : Ability() { + companion object { + const val name = "Mage Armor" + } + override val id: Int = 22783 + override val name: String = Companion.name + override val icon: String = "spell_magearmor.jpg" + override fun gcdMs(sp: SimParticipant): Int = sp.spellGcd().toInt() + override fun resourceCost(sp: SimParticipant): Double = 630.0 + + val buff = object : Buff() { + override val name: String = Companion.name + override val icon: String = "spell_magearmor.jpg" + override val durationMs: Int = 30 * 60 * 1000 + + override fun modifyStats(sp: SimParticipant): Stats { + return Stats(manaPer5Seconds = (General.mp5FromSpiritNotCasting(sp) * .3).toInt()) + } + } + + override fun cast(sp: SimParticipant) { + sp.addBuff(buff) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/character/classes/mage/specs/Arcane.kt b/src/commonMain/kotlin/character/classes/mage/specs/Arcane.kt index 4a424a681..6018910e6 100644 --- a/src/commonMain/kotlin/character/classes/mage/specs/Arcane.kt +++ b/src/commonMain/kotlin/character/classes/mage/specs/Arcane.kt @@ -2,11 +2,15 @@ package character.classes.mage.specs import character.Spec import character.SpecEpDelta +import character.Stats +import kotlin.math.max class Arcane : Spec() { override val name: String = "Arcane" override val epBaseStat: SpecEpDelta = spellPowerBase - override val epStatDeltas: List = defaultCasterDeltas + override val epStatDeltas: List = listOf(Triple("spirit", Stats(spirit = 50), 50.0)) + + defaultCasterDeltas + override fun redSocketEp(deltas: Map): Double { // 12 spell dmg @@ -14,12 +18,12 @@ class Arcane : Spec() { } override fun yellowSocketEp(deltas: Map): Double { - // 5 spell haste rating / 6 spell damage - return ((deltas["spellHasteRating"] ?: 0.0) * 5.0) + 6.0 + // 10 int + return ((deltas["intellect"] ?: 0.0) * 10.0) } override fun blueSocketEp(deltas: Map): Double { - // 6 spell dmg - return 6.0 + // 5 int (+mp5, worth nearly nothing) or 10 spirit, whichever turns out to be better. + return max((deltas["intellect"] ?: 0.0) * 5.0, (deltas["spirit"] ?: 0.0) * 10.0) } } diff --git a/src/commonMain/kotlin/data/abilities/generic/AdeptsElixir.kt b/src/commonMain/kotlin/data/abilities/generic/AdeptsElixir.kt new file mode 100644 index 000000000..8fef490d3 --- /dev/null +++ b/src/commonMain/kotlin/data/abilities/generic/AdeptsElixir.kt @@ -0,0 +1,30 @@ +package data.abilities.generic + +import character.* +import sim.SimParticipant + +class AdeptsElixir : Ability() { + companion object { + const val name = "Adept's Elixir" + } + + override val id: Int = 28103 + override val name: String = Companion.name + override val icon: String = "inv_potion_96.jpg" + override fun gcdMs(sp: SimParticipant): Int = 0 + + val buff = object : Buff() { + override val name: String = "Adept's Elixir" + override val icon: String = "inv_potion_96.jpg" + override val durationMs: Int = 60 * 60 * 1000 + override val mutex: List = listOf(Mutex.GUARDIAN_ELIXIR) + + override fun modifyStats(sp: SimParticipant): Stats { + return Stats(spellDamage = 24, spellCritRating = 24.0) + } + } + + override fun cast(sp: SimParticipant) { + sp.addBuff(buff) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/data/abilities/generic/ElixirOfDraenicWisdom.kt b/src/commonMain/kotlin/data/abilities/generic/ElixirOfDraenicWisdom.kt new file mode 100644 index 000000000..02b0ce347 --- /dev/null +++ b/src/commonMain/kotlin/data/abilities/generic/ElixirOfDraenicWisdom.kt @@ -0,0 +1,30 @@ +package data.abilities.generic + +import character.* +import sim.SimParticipant + +class ElixirOfDraenicWisdom : Ability() { + companion object { + const val name = "Elixir of Draenic Wisdom" + } + + override val id: Int = 32067 + override val name: String = Companion.name + override val icon: String = "inv_potion_155.jpg" + override fun gcdMs(sp: SimParticipant): Int = 0 + + val buff = object : Buff() { + override val name: String = "Elixir of Draenic Wisdom" + override val icon: String = "inv_potion_155.jpg" + override val durationMs: Int = 60 * 60 * 1000 + override val mutex: List = listOf(Mutex.GUARDIAN_ELIXIR) + + override fun modifyStats(sp: SimParticipant): Stats { + return Stats(intellect = 30, spirit = 30) + } + } + + override fun cast(sp: SimParticipant) { + sp.addBuff(buff) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/data/abilities/generic/FlameCap.kt b/src/commonMain/kotlin/data/abilities/generic/FlameCap.kt new file mode 100644 index 000000000..e034c37b5 --- /dev/null +++ b/src/commonMain/kotlin/data/abilities/generic/FlameCap.kt @@ -0,0 +1,33 @@ +package data.abilities.generic + +import character.* +import sim.SimParticipant + +class FlameCap : Ability() { + companion object { + const val name = "Flame Cap" + const val icon: String = "inv_misc_herb_flamecap.jpg" + } + + override val id: Int = 22788 + override val name: String = Companion.name + override val icon: String = Companion.icon + override fun gcdMs(sp: SimParticipant): Int = 0 + override val castableOnGcd = true + override val sharedCooldown: SharedCooldown = SharedCooldown.RUNE_OR_MANA_GEM + override fun cooldownMs(sp: SimParticipant): Int = 180000 + + val buff = object : Buff() { + override val name: String = Companion.name + override val icon: String = Companion.icon + override val durationMs: Int = 60000 + + override fun modifyStats(sp: SimParticipant): Stats { + return Stats(fireDamage = 80) + } + } + + override fun cast(sp: SimParticipant) { + sp.addBuff(buff) + } +} diff --git a/src/commonMain/kotlin/data/abilities/generic/GenericAbilities.kt b/src/commonMain/kotlin/data/abilities/generic/GenericAbilities.kt index f98c30703..ca2ca9b1e 100644 --- a/src/commonMain/kotlin/data/abilities/generic/GenericAbilities.kt +++ b/src/commonMain/kotlin/data/abilities/generic/GenericAbilities.kt @@ -6,19 +6,24 @@ import character.Ability object GenericAbilities { fun byName(name: String): Ability? { return when(name) { + AdeptsElixir.name -> AdeptsElixir() BlackenedBasilisk.name -> BlackenedBasilisk() CrunchySerpent.name -> CrunchySerpent() DarkRune.name -> DarkRune() DemonicRune.name -> DemonicRune() DestructionPotion.name -> DestructionPotion() + ElixirOfDraenicWisdom.name -> ElixirOfDraenicWisdom() ElixirOfMajorAgility.name -> ElixirOfMajorAgility() ElixirOfMajorStrength.name -> ElixirOfMajorStrength() + FlameCap.name -> FlameCap() FlaskOfBlindingLight.name -> FlaskOfBlindingLight() FlaskOfPureDeath.name -> FlaskOfPureDeath() FlaskOfRelentlessAssault.name -> FlaskOfRelentlessAssault() HastePotion.name -> HastePotion() + Innervate.name -> Innervate() InsaneStrengthPotion.name -> InsaneStrengthPotion() RoastedClefthoof.name -> RoastedClefthoof() + ScrollOfSpiritV.name -> ScrollOfSpiritV() SpicyHotTalbuk.name -> SpicyHotTalbuk() SuperManaPotion.name -> SuperManaPotion() UseActiveTrinket.name -> UseActiveTrinket() diff --git a/src/commonMain/kotlin/data/abilities/generic/Innervate.kt b/src/commonMain/kotlin/data/abilities/generic/Innervate.kt new file mode 100644 index 000000000..228966b16 --- /dev/null +++ b/src/commonMain/kotlin/data/abilities/generic/Innervate.kt @@ -0,0 +1,35 @@ +package data.abilities.generic + +import character.Ability +import character.Buff +import character.Stats +import mechanics.General +import sim.SimParticipant + +class Innervate : Ability() { + companion object { + const val name = "Innervate" + } + + override val id: Int = 29166 + override val name: String = Companion.name + override val icon: String = "spell_nature_lightning.jpg" + override fun gcdMs(sp: SimParticipant): Int = 0 + override val castableOnGcd = true + override fun cooldownMs(sp: SimParticipant): Int = 720000 + + val buff = object : Buff() { + override val name: String = Companion.name + override val icon: String = "spell_nature_lightning.jpg" + override val durationMs: Int = 20000 + + //NOTE: This assumes arcane meditation and mage armor. Unsure how to detect actual value. + override fun modifyStats(sp: SimParticipant): Stats { + return Stats(manaPer5Seconds = (General.mp5FromSpiritNotCasting(sp) * 4.4).toInt()) + } + } + + override fun cast(sp: SimParticipant) { + sp.addBuff(buff) + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/data/abilities/generic/ScrollOfSpiritV.kt b/src/commonMain/kotlin/data/abilities/generic/ScrollOfSpiritV.kt new file mode 100644 index 000000000..14d1edc58 --- /dev/null +++ b/src/commonMain/kotlin/data/abilities/generic/ScrollOfSpiritV.kt @@ -0,0 +1,30 @@ +package data.abilities.generic + +import character.* +import sim.SimParticipant + +class ScrollOfSpiritV : Ability() { + companion object { + const val name = "Scroll of Spirit V" + } + + override val id: Int = 27501 + override val name: String = Companion.name + override val icon: String = "inv_scroll_01.jpg" + override fun gcdMs(sp: SimParticipant): Int = 0 + + val buff = object : Buff() { + override val name: String = "Scroll of Spirit V" + override val icon: String = "inv_scroll_01.jpg" + override val durationMs: Int = 30 * 60 * 1000 + override val mutex: List = listOf(Mutex.BUFF_SPIRIT) + + override fun modifyStats(sp: SimParticipant): Stats { + return Stats(spirit = 30) + } + } + + override fun cast(sp: SimParticipant) { + sp.addBuff(buff) + } +} diff --git a/src/commonMain/kotlin/data/abilities/raid/DivineSpirit.kt b/src/commonMain/kotlin/data/abilities/raid/DivineSpirit.kt index 9499dd93c..a0ca69bdc 100644 --- a/src/commonMain/kotlin/data/abilities/raid/DivineSpirit.kt +++ b/src/commonMain/kotlin/data/abilities/raid/DivineSpirit.kt @@ -2,6 +2,7 @@ package data.abilities.raid import character.Ability import character.Buff +import character.Mutex import character.Stats import mechanics.Rating import sim.SimParticipant @@ -20,6 +21,7 @@ class DivineSpirit : Ability() { override val name: String = Companion.name override val icon: String = "spell_holy_prayerofspirit.jpg" override val durationMs: Int = -1 + override val mutex: List = listOf(Mutex.BUFF_SPIRIT) override fun modifyStats(sp: SimParticipant): Stats { return Stats(spirit = 50) diff --git a/src/commonMain/kotlin/data/abilities/raid/ImprovedDivineSpirit.kt b/src/commonMain/kotlin/data/abilities/raid/ImprovedDivineSpirit.kt index 09b42d1cb..62df67ddf 100644 --- a/src/commonMain/kotlin/data/abilities/raid/ImprovedDivineSpirit.kt +++ b/src/commonMain/kotlin/data/abilities/raid/ImprovedDivineSpirit.kt @@ -2,6 +2,7 @@ package data.abilities.raid import character.Ability import character.Buff +import character.Mutex import character.Stats import mechanics.Rating import sim.SimParticipant diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt index cb4d85478..0e8b4c952 100644 --- a/src/jvmMain/kotlin/Main.kt +++ b/src/jvmMain/kotlin/Main.kt @@ -153,15 +153,8 @@ class TBCSim : CliktCommand() { ) fun singleEpSim(config: Config, opts: SimOptions, epDelta: SpecEpDelta? = null) : Pair { - // Most presets are hit capped, so apply a universal -2% hit buff so the hit has something to sim against - val hitReduction = Stats( - physicalHitRating = -2.0 * Rating.physicalHitPerPct, - expertiseRating = -2.0 * Rating.expertisePerPct, - spellHitRating = -5.0 * Rating.spellHitPerPct, - ) - val epStatMod = epDelta?.second ?: Stats() - val totalStatMod = Stats().add(epStatMod).add(hitReduction) + val totalStatMod = Stats().add(epStatMod)//.add(hitReduction) val iterations = runBlocking { Sim(config, opts, totalStatMod) {}.sim() } return Pair(epDelta, SimStats.dps(iterations).entries.sumByDouble { it.value?.mean ?: 0.0 }) @@ -265,7 +258,11 @@ class TBCSim : CliktCommand() { val specFilter = specFilterStr?.split(",") val categoryFilter = categoryFilterStr?.split(",") - if (calcEP) { + if (calcEP && configFile?.exists() == true) { + val config = ConfigMaker.fromYml(configFile!!.readText()) + println("Starting EP run") + val deltas = computeEpDeltas(config, opts) + } else if (calcEP) { val epTypeRef = object : TypeReference(){} val existing = mapper.readValue(File(epOutputPath).readText(), epTypeRef) // EP calculation sim diff --git a/ui/src/presets/samples/mage_arcane_phase2.yml b/ui/src/presets/samples/mage_arcane_phase2.yml index b174bb103..e1fb0acad 100644 --- a/ui/src/presets/samples/mage_arcane_phase2.yml +++ b/ui/src/presets/samples/mage_arcane_phase2.yml @@ -53,7 +53,7 @@ gear: mainHand: name: The Nexus Key enchant: Major Spellpower (Weapon) - tempEnchant: Superior Wizard Oil + tempEnchant: Brilliant Wizard Oil rangedTotemLibram: name: Eredar Wand of Obliteration head: @@ -112,14 +112,28 @@ gear: rotation: autoAttack: false precombat: - - name: Flask of Blinding Light + - name: Elixir of Draenic Wisdom + - name: Adept's Elixir - name: Crunchy Serpent - name: Arcane Intellect - - name: Molten Armor + - name: Mage Armor + - name: Scroll of Spirit V combat: + - name: Evocation + criteria: + - type: RESOURCE_PCT_LTE + pct: 30 + resourceType: MANA + - type: FIGHT_TIME_REMAINING_GTE + seconds: 30 - name: Blood Fury - name: Berserking + - name: Bloodlust + criteria: + - type: FIGHT_DURATION_GTE + seconds: 15 + - name: Drums of Battle - name: Mana Emerald criteria: - type: RESOURCE_MISSING_GTE @@ -131,31 +145,40 @@ rotation: - type: RESOURCE_MISSING_GTE amount: 3000 resourceType: MANA - - name: Evocation + - type: ABILITY_COOLDOWN_GTE + ability: Mana Emerald + seconds: 1 + - name: Innervate criteria: - type: RESOURCE_PCT_LTE - pct: 20 + pct: 35 resourceType: MANA - - type: FIGHT_TIME_REMAINING_GTE - seconds: 30 - name: Cold Snap criteria: + - type: ABILITY_COOLDOWN_LTE + ability: Mana Emerald + seconds: 5 - type: ABILITY_COOLDOWN_GTE - ability: Icy Veins + ability: Mana Emerald seconds: 1 - name: Icy Veins criteria: - - type: FIGHT_TIME_ELAPSED_GTE - seconds: 5 + - type: RESOURCE_MISSING_GTE + # Account for Serpent-Coil Braid bonus potential over the regular mana gem amount + amount: 3125 + resourceType: MANA - name: Arcane Power criteria: - - type: FIGHT_TIME_ELAPSED_GTE - seconds: 5 + - type: RESOURCE_MISSING_GTE + # Account for Serpent-Coil Braid bonus potential over the regular mana gem amount + amount: 3125 + resourceType: MANA - name: Presence of Mind - criteria: - - type: FIGHT_TIME_ELAPSED_GTE - seconds: 5 - name: Use Active Trinket + criteria: + - type: RESOURCE_MISSING_GTE + amount: 3125 + resourceType: MANA # Cast AB if we're using cooldowns, have high mana, or have low mana and low stacks - name: Arcane Blast criteria: @@ -170,16 +193,18 @@ rotation: - name: Arcane Blast criteria: - type: RESOURCE_PCT_GTE - pct: 20 + pct: 25 resourceType: MANA - name: Arcane Blast criteria: - type: BUFF_STACKS_LTE buff: Arcane Blast stacks: 2 - - type: RESOURCE_PCT_LTE - pct: 20 - resourceType: MANA + - name: Arcane Blast + criteria: + - type: BUFF_DURATION_LTE + buff: Arcane Blast + seconds: 1 - name: Frostbolt raidBuffs: