Skip to content

Commit

Permalink
Initial commit! First version of simulation and client
Browse files Browse the repository at this point in the history
  • Loading branch information
lologarithm committed Mar 31, 2021
0 parents commit f9bfdbd
Show file tree
Hide file tree
Showing 24 changed files with 3,519 additions and 0 deletions.
2 changes: 2 additions & 0 deletions elesim/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Partially implemented classic simulator.
Probably never going to finish because TBC is coming.
109 changes: 109 additions & 0 deletions elesim/auras.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package elesim

type Aura struct {
ID string
Duration int // ticks aura will apply
OnCast func(c *Cast)
OnSpellHit func(c *Cast)
OnStruck func(c *Cast)
}

type Cast struct {
Spell *Spell
// Caster ... // Needed for onstruck effects?

// Mutatable State
ManaCost float64
Hit float64
Crit float64
Spellpower float64
}

type Spell struct {
ID string
CastTime float64
Cooldown int
Mana float64
MinDmg float64
MaxDmg float64
DamageType DamageType
Coeff float64
}

type DamageType int

const (
DamageTypeUnknown DamageType = iota
DamageTypeFire
DamageTypeNature
DamageTypeFrost

// who cares
DamageTypeShadow
DamageTypeHoly
DamageTypeArcane
)

func clearcasting() Aura {
return Aura{
Duration: 15 * tickPerSecond,
OnCast: func(c *Cast) {
debug("clearcasting...")
c.ManaCost = 0
},
}
}

func zhc() Aura {
dmgbonus := 204.0

return Aura{
Duration: 20 * tickPerSecond,
OnCast: func(c *Cast) {
debug("zhc(%.0f)...", dmgbonus)
c.Spellpower += dmgbonus
dmgbonus -= 17
},
}
}

func elemastery() Aura {
return Aura{
Duration: 99999999999999999,
OnCast: func(c *Cast) {
debug("ele mastery...")
c.Crit = 1.01 // 101% chance of crit
c.ManaCost = 0
},
}
}

func stormcaller() Aura {
return Aura{
Duration: 8 * tickPerSecond,
OnCast: func(c *Cast) {
debug("stormcaller...")
c.Spellpower += 50
},
}
}

// spells
var spells = []Spell{
{ID: "LB4", CastTime: 2.0, MinDmg: 88, MaxDmg: 100, Mana: 50, DamageType: DamageTypeNature},
{ID: "LB10", CastTime: 3.0, MinDmg: 428, MaxDmg: 477, Mana: 265, DamageType: DamageTypeNature},
{ID: "CL4", CastTime: 2.5, Cooldown: 6, MinDmg: 505, MaxDmg: 564, Mana: 605, DamageType: DamageTypeNature},
}

var spellmap = map[string]*Spell{}

func init() {
for _, sp := range spells {
sp2 := sp //wtf go?
spp := &sp2
if spp.Coeff == 0 {
spp.Coeff = spp.CastTime / 3.5
}
spellmap[sp.ID] = spp
}
}
1 change: 1 addition & 0 deletions elesim/buffs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package elesim
204 changes: 204 additions & 0 deletions elesim/sim.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
package elesim

import (
"fmt"
"math/rand"
"strings"
)

var IsDebug = false

func debug(s string, vals ...interface{}) {
if IsDebug {
fmt.Printf(s, vals...)
}
}

func Sim(seconds int, stats Stats, spellOrder []string) (float64, int) {
ticks := seconds * tickPerSecond

// ticks until cast is complete
currentMana := stats[StatMana]
casting := 0
// timeToRegen := 0
castingSpell := ""

spellIdx := 0
cds := map[string]int{}
auras := map[string]Aura{}

totalDmg := 0.0
castStats := map[string]struct {
Num int
Total float64
}{}
oomAt := 0

for i := 0; i < ticks; i++ {
casting--

// MP5 regen
currentMana += (stats[StatMP5] / 5.0) / float64(tickPerSecond)
if currentMana > stats[StatMana] {
currentMana = stats[StatMana]
}

if stats[StatMana]-currentMana >= 2250 && cds["potion"] < 1 {
// Restores 1350 to 2250 mana. (2 Min Cooldown)
currentMana += float64(1350 + rand.Intn(2250-1350))
cds["potion"] = 120 * tickPerSecond
debug("[%d] Used Mana Potion\n", i/tickPerSecond)
}
if stats[StatMana]-currentMana >= 1500 && cds["darkrune"] < 1 {
// Restores 900 to 1500 mana. (2 Min Cooldown)
currentMana += float64(900 + rand.Intn(1500-900))
cds["darkrune"] = 120 * tickPerSecond
debug("[%d] Used Mana Potion\n", i/tickPerSecond)
}

if castingSpell == "" && oomAt == 0 {
// debug("(%0.0f/%0.0f mana)\n", currentMana, stats[StatMana])
}

if casting <= 0 {
if castingSpell != "" {
sp := spellmap[castingSpell]
cast := &Cast{
Spell: sp,
ManaCost: float64(sp.Mana),
Spellpower: stats[StatSpellDmg], // TODO: type specific bonuses...
}
if strings.HasPrefix(sp.ID, "LB") || strings.HasPrefix(sp.ID, "CL") {
// totem of the storm
cast.Spellpower += 33
// Talent Convection
cast.ManaCost *= 0.9
}

cast.Hit = 0.83 + stats[StatSpellHit]
cast.Crit = stats[StatSpellCrit]

for _, aur := range auras {
if aur.OnCast != nil {
aur.OnCast(cast)
}
}

// TODO: generalize aura removal.
if _, ok := auras["elemastery"]; ok {
delete(auras, "elemastery")
} else if _, ok := auras["cc"]; ok {
delete(auras, "cc")
}

if rand.Float64() < cast.Hit {
dmg := (float64(rand.Intn(int(sp.MaxDmg-sp.MinDmg))) + sp.MinDmg) + (stats[StatSpellDmg] * sp.Coeff)

if rand.Float64() < cast.Crit {
dmg *= 2
debug("crit")
} else {
debug("hit")
}

if strings.HasPrefix(sp.ID, "LB") || strings.HasPrefix(sp.ID, "CL") {
// Talent Concussion
dmg *= 1.05
}

// Average Resistance = (Target's Resistance / (Caster's Level * 5)) * 0.75 "AR"
// P(x) = 50% - 250%*|x - AR| <- where X is chance of resist
// For now hardcode the 25% chance resist at 2.5% (this assumes bosses have 0 nature resist)
if rand.Float64() < 0.025 { // chance of 25% resist
dmg *= .75
debug("(partial resist)")
}
debug(": %0.0f\n", dmg)

totalDmg += dmg
stat := castStats[sp.ID]
stat.Num += 1
stat.Total += dmg
castStats[sp.ID] = stat
} else {
debug("miss.\n")
}

currentMana -= cast.ManaCost

castingSpell = ""
if sp.Cooldown > 0 {
cds[sp.ID] = sp.Cooldown * tickPerSecond
}
if rand.Float64() < 0.1 {
// TODO: make Elemental Focus an aura that applies an aura.
// talent for clearcast chance on cast
auras["cc"] = clearcasting()
debug("\tGained Clearcasting.\n")
}
if rand.Float64() < 0.2 {
auras["stc"] = stormcaller()
debug("\tGained Stormcaller.\n")
}
continue
} else {
// Choose next spell
so := spellOrder[spellIdx]

isclearcasting := auras["cc"].Duration > 0 || auras["elemastery"].Duration > 0

// anytime ZHC AND Ele Mastery are up, pop!
if cds["zhc"] <= 0 && cds["elemastery"] <= 0 {
// Apply auras
auras["zhc"] = zhc()
auras["elemastery"] = elemastery()

cds["zhc"] = 120 * tickPerSecond
cds["elemastery"] = 180 * tickPerSecond
}

sp := spellmap[so]
cost := sp.Mana
if strings.HasPrefix(sp.ID, "LB") || strings.HasPrefix(sp.ID, "CL") {
// Talent to reduce mana cost by 10%
cost *= 0.9
}
if cds[so] == 0 && (currentMana >= sp.Mana || isclearcasting) {
castTime := spellmap[so].CastTime
if strings.HasPrefix(sp.ID, "LB") || strings.HasPrefix(sp.ID, "CL") {
// Talent to reduce cast time.
castTime -= 1
}
casting = int(castTime * float64(tickPerSecond))
castingSpell = sp.ID
spellIdx++
if spellIdx == len(spellOrder) {
spellIdx = 0
}
debug("[%d] Casting %s ...", i/tickPerSecond, sp.ID)
} else if !isclearcasting && currentMana < 200 && oomAt == 0 {
oomAt = i / tickPerSecond
}

}
}
// CDS
for k := range cds {
cds[k]--
if cds[k] <= 0 {
delete(cds, k)
}
}
for k, v := range auras {
nv := v
nv.Duration--
if nv.Duration > 0 {
auras[k] = nv
} else {
delete(auras, k)
}
}
}

return totalDmg, oomAt
}
53 changes: 53 additions & 0 deletions elesim/stats.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package elesim

import "fmt"

var tickPerSecond = 30

type Stats []float64

type Stat int

const (
StatInt Stat = iota
StatSpellCrit
StatSpellHit
StatSpellDmg
StatMP5
StatMana
StatSpellPen
)

func (s Stat) StatName() string {
switch s {
case StatInt:
return "StatInt"
case StatSpellCrit:
return "StatSpellCrit"
case StatSpellHit:
return "StatSpellHit"
case StatSpellDmg:
return "StatSpellDmg"
case StatMP5:
return "StatMP5"
case StatMana:
return "StatMana"
case StatSpellPen:
return "StatSpellPen"
}

return "none"
}

func (st Stats) Print() {
fmt.Printf("Stats:\n")

for k, v := range st {
if v < 50 {
fmt.Printf("\t%s: %0.3f\n", Stat(k).StatName(), v)
} else {
fmt.Printf("\t%s: %0.0f\n", Stat(k).StatName(), v)
}

}
}
Loading

0 comments on commit f9bfdbd

Please sign in to comment.