Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Arcane Mage implementation #128

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Not yet implemented:

To calculate EPs for a single character definition, use the following command:

`./tbcsim --calc-ep-single <path_to_character_definition_file>`
`./tbcsim --calc-ep <path_to_character_definition_file>`

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`.

Expand Down
1 change: 1 addition & 0 deletions src/commonMain/kotlin/character/Mutex.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum class Mutex {
BUFF_EXPOSE_WEAKNESS,
BUFF_FEROCIOUS_INSPIRATION,
BUFF_FAERIE_FIRE,
BUFF_SPIRIT,

// Hunter
BUFF_HUNTER_ASPECT,
Expand Down
10 changes: 5 additions & 5 deletions src/commonMain/kotlin/character/Spec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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),
secretbis marked this conversation as resolved.
Show resolved Hide resolved
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),
)

Expand All @@ -23,7 +23,7 @@ abstract class Spec {
val defaultRangedDeltas: List<SpecEpDelta> = 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)
)
Expand All @@ -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<SpecEpDelta> = 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
Expand Down
1 change: 1 addition & 0 deletions src/commonMain/kotlin/character/classes/mage/Mage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class Mage(talents: Map<String, Talent>, 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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
secretbis marked this conversation as resolved.
Show resolved Hide resolved

val empFb: EmpoweredFrostbolt? = sp.character.klass.talentInstance(EmpoweredFrostbolt.name)
val bonusFbCrit = empFb?.frostboltAddlCritPct() ?: 0.0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package character.classes.mage.abilities

import character.Ability
import character.Buff
import character.Stats
import mechanics.General
import sim.SimParticipant

secretbis marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
14 changes: 9 additions & 5 deletions src/commonMain/kotlin/character/classes/mage/specs/Arcane.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,28 @@ 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<SpecEpDelta> = defaultCasterDeltas
override val epStatDeltas: List<SpecEpDelta> = listOf(Triple("spirit", Stats(spirit = 50), 50.0)) +
defaultCasterDeltas


override fun redSocketEp(deltas: Map<String, Double>): Double {
// 12 spell dmg
return 12.0
}

override fun yellowSocketEp(deltas: Map<String, Double>): 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)
secretbis marked this conversation as resolved.
Show resolved Hide resolved
}

override fun blueSocketEp(deltas: Map<String, Double>): 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)
}
}
30 changes: 30 additions & 0 deletions src/commonMain/kotlin/data/abilities/generic/AdeptsElixir.kt
Original file line number Diff line number Diff line change
@@ -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<Mutex> = listOf(Mutex.GUARDIAN_ELIXIR)

override fun modifyStats(sp: SimParticipant): Stats {
return Stats(spellDamage = 24, spellCritRating = 24.0)
}
}

secretbis marked this conversation as resolved.
Show resolved Hide resolved
override fun cast(sp: SimParticipant) {
sp.addBuff(buff)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package data.abilities.generic

secretbis marked this conversation as resolved.
Show resolved Hide resolved
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<Mutex> = listOf(Mutex.GUARDIAN_ELIXIR)

override fun modifyStats(sp: SimParticipant): Stats {
return Stats(intellect = 30, spirit = 30)
}
}

override fun cast(sp: SimParticipant) {
sp.addBuff(buff)
}
}
33 changes: 33 additions & 0 deletions src/commonMain/kotlin/data/abilities/generic/FlameCap.kt
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the melee proc implementation, but I'm happy to add that later - just noting so I don't forget.

}
}

override fun cast(sp: SimParticipant) {
sp.addBuff(buff)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
35 changes: 35 additions & 0 deletions src/commonMain/kotlin/data/abilities/generic/Innervate.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package data.abilities.generic

secretbis marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
30 changes: 30 additions & 0 deletions src/commonMain/kotlin/data/abilities/generic/ScrollOfSpiritV.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package data.abilities.generic

secretbis marked this conversation as resolved.
Show resolved Hide resolved
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<Mutex> = listOf(Mutex.BUFF_SPIRIT)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be useful to add an override for mutexPriority here, since that will resolve based on the potency of the buff. The default priority is whichever is newer, which isn't correct for most stat-based buffs.

The value there would be mapOf(Mutex.BUFF_SPIRIT to 30), and the corresponding 50 weight in Divine Spirit.


override fun modifyStats(sp: SimParticipant): Stats {
return Stats(spirit = 30)
}
}

override fun cast(sp: SimParticipant) {
sp.addBuff(buff)
}
}
2 changes: 2 additions & 0 deletions src/commonMain/kotlin/data/abilities/raid/DivineSpirit.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Mutex> = listOf(Mutex.BUFF_SPIRIT)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As with the spirit scroll implementation, this would benefit from a mutexPriority override to accurately express the potency.


override fun modifyStats(sp: SimParticipant): Stats {
return Stats(spirit = 50)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package data.abilities.raid

import character.Ability
import character.Buff
import character.Mutex
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Leftover import

import character.Stats
import mechanics.Rating
import sim.SimParticipant
Expand Down
15 changes: 6 additions & 9 deletions src/jvmMain/kotlin/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,8 @@ class TBCSim : CliktCommand() {
)

fun singleEpSim(config: Config, opts: SimOptions, epDelta: SpecEpDelta? = null) : Pair<SpecEpDelta?, Double> {
// 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 })
Expand Down Expand Up @@ -265,7 +258,11 @@ class TBCSim : CliktCommand() {
val specFilter = specFilterStr?.split(",")
val categoryFilter = categoryFilterStr?.split(",")

if (calcEP) {
if (calcEP && configFile?.exists() == true) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about this, looks like I broke that option a long time ago and never fixed the docs... The EP rankings on the site mostly replaced the intent of that feature.

Would the most useful output for a single-character sim be just a table of results to the console? Or, would something else be better?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found creating the list of my personal EPs to be the most useful thing. Arcane mages have a few different points where haste, in particular, changes very dramatically in value. Understanding where I am relative to those points of inflection is what I wanted to get, and the current output with my local changes does that.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! Whenever you're ready, I'd love to see the implementation!

val config = ConfigMaker.fromYml(configFile!!.readText())
println("Starting EP run")
val deltas = computeEpDeltas(config, opts)
} else if (calcEP) {
val epTypeRef = object : TypeReference<EpOutput>(){}
val existing = mapper.readValue(File(epOutputPath).readText(), epTypeRef)
// EP calculation sim
Expand Down
Loading