package spells import ( "fmt" "log" "math" "strconv" "strings" "github.com/thoas/go-funk" ) // list of spell effects that require scaling var SpellEffects = [...]int{ 2, // School Damage 6, // AppyAura 9, // HealthLeech 10, // Heal 30, // Restores Mana 35, // Apply Area Aura } // list of spell aura effects that require scaling var SpellAuraEffects = [...]int{ 3, // DOT 8, // HOT 13, // Modifies Spell Damage Done 15, // Modifies Damage Shield 22, // Modifies Resistance 34, // Modifies HEalth 85, // Modifies Mana Regen 99, // Modifies Attack Power 124, // Modifies Range Attack Power 135, // Modifies Healing Done 189, // Modifies Critical Strike } var AuraEffectsStatMap = map[int]int{ 8: 46, 13: 45, 85: 43, 99: 38, 124: 38, 135: 45, } // Usually in EffectMiscValueA to describe what the Aura modifies var SpellModifiers = [...]int{ 0, // damage 1, // duration 2, // threat 3, // effect1 4, // charges 5, // range 6, // radius 7, // crit chance 8, // all effects 9, // No pushback 10, // Cast Time 11, // CD 12, // effect2 13, // ignore armor 14, // cost 15, // crit damage bonus 16, // resist miss chance 17, // jump targets 18, // Chance of success 19, // Amplitude 20, // Dmg multiplier 21, // GCD 22, // DoT 23, // effect3 24, // bonus multiplier 26, // PPM 27, // value multiplier 28, // resist dispel chance 29, // crit damage bonus 2 } // result of a stat conversion from spell to raw stats on item type ConvItemStat struct { StatType int StatValue int Budget int } // Spell Effect with max value for effect storage type SpellEffect struct { Effect int BasePoints int DieSides int CalculatedMax int } // DB Mapping from spell_dbc type Spell struct { ID int `db:"ID"` Name string `db:"Name_Lang_enUS"` Description string `db:"Description_Lang_enUS"` AuraDescription string `db:"AuraDescription_Lang_enUS"` ProcChance int `db:"ProcChance"` SpellLevel int `db:"SpellLevel"` Effect1 int `db:"Effect_1"` Effect2 int `db:"Effect_2"` Effect3 int `db:"Effect_3"` EffectDieSides1 int `db:"EffectDieSides_1"` EffectDieSides2 int `db:"EffectDieSides_2"` EffectDieSides3 int `db:"EffectDieSides_3"` EffectRealPointsPerLevel1 int `db:"EffectRealPointsPerLevel_1"` EffectRealPointsPerLevel2 int `db:"EffectRealPointsPerLevel_2"` EffectRealPointsPerLevel3 int `db:"EffectRealPointsPerLevel_3"` EffectBasePoints1 int `db:"EffectBasePoints_1"` EffectBasePoints2 int `db:"EffectBasePoints_2"` EffectBasePoints3 int `db:"EffectBasePoints_3"` EffectAura1 int `db:"EffectAura_1"` EffectAura2 int `db:"EffectAura_2"` EffectAura3 int `db:"EffectAura_3"` EffectBonusMultiplier1 int `db:"EffectBonusMultiplier_1"` EffectBonusMultiplier2 int `db:"EffectBonusMultiplier_2"` EffectBonusMultiplier3 int `db:"EffectBonusMultiplier_3"` ItemSpellSlot int Scaled bool } func (db Database) GetSpell(id int) (Spell, error) { if id == 0 { return Spell{}, fmt.Errorf("id cannot be 0") } spell := Spell{} sql := "SELECT " + GetSpellFields() + " FROM `spell_dbc` WHERE ID = ? -- " + strconv.Itoa(id) err := db.client.Get(&spell, sql, id) if err != nil { return Spell{}, fmt.Errorf("failed to get spell: %v", err) } return spell, nil } func calcMaxValue(base int, sides int) int { if base < 0 { return base - sides } return base + sides } // get a List of the spell effects (not auras) that need to be scaled func (s Spell) GetSpellEffects() []SpellEffect { effects := make([]SpellEffect, 0) effects = append(effects, SpellEffect{ Effect: s.Effect1, BasePoints: s.EffectBasePoints1, DieSides: s.EffectDieSides1, CalculatedMax: calcMaxValue(s.EffectBasePoints1, s.EffectDieSides1), }) effects = append(effects, SpellEffect{ Effect: s.Effect2, BasePoints: s.EffectBasePoints2, DieSides: s.EffectDieSides2, CalculatedMax: calcMaxValue(s.EffectBasePoints2, s.EffectDieSides2), }) effects = append(effects, SpellEffect{ Effect: s.Effect3, BasePoints: s.EffectBasePoints3, DieSides: s.EffectDieSides3, CalculatedMax: calcMaxValue(s.EffectBasePoints3, s.EffectDieSides3), }) return effects } // Get he effects and calculate the max value for the a func (s Spell) GetAuraEffects() []SpellEffect { effects := make([]SpellEffect, 0) effects = append(effects, SpellEffect{ Effect: s.EffectAura1, BasePoints: s.EffectBasePoints1, DieSides: s.EffectDieSides1, CalculatedMax: calcMaxValue(s.EffectBasePoints1, s.EffectDieSides1), }) effects = append(effects, SpellEffect{ Effect: s.EffectAura2, BasePoints: s.EffectBasePoints2, DieSides: s.EffectDieSides2, CalculatedMax: calcMaxValue(s.EffectBasePoints2, s.EffectDieSides2), }) effects = append(effects, SpellEffect{ Effect: s.EffectAura3, BasePoints: s.EffectBasePoints3, DieSides: s.EffectDieSides3, CalculatedMax: calcMaxValue(s.EffectBasePoints3, s.EffectDieSides3), }) return effects } // this spell effect has stats or effects that need to be scaled func (s Spell) SpellEffectsNeedsScaled() bool { if s.Effect1 == 0 { return false } needsScaled := false effects := s.GetSpellEffects() for _, e := range effects { if !funk.Contains(SpellEffects, e.Effect) || e.Effect == 6 { continue } needsScaled = true } return needsScaled } // this aura effect has stats or effects that need to be scaled func (s Spell) AuraEffectNeedsScaled() bool { if s.EffectAura1 == 0 { return false } for _, effect := range SpellAuraEffects { if s.EffectAura1 == effect || s.EffectAura2 == effect || s.EffectAura3 == effect { return true } } return false } func (s Spell) HasAuraEffect() bool { return s.EffectAura1 != 0 || s.EffectAura2 != 0 || s.EffectAura3 != 0 } func AuraEffectCanBeConv(effect int) bool { statMods := [...]int{8, 13, 22, 34, 85, 99, 124, 135, 189} return funk.Contains(statMods, effect) } // Lookup details about the effect and return the stat type -1 indicates not found func convertAuraEffect(effect int) int { if !funk.Contains(AuraEffectsStatMap, effect) { log.Printf("effect %v not found in SpellEffectStatMap skipping", effect) return -1 } return AuraEffectsStatMap[effect] } // Converts spell buffs to item stats making it easier to convert and normalize func (s Spell) ConvertToStats() ([]ConvItemStat, error) { stats := []ConvItemStat{} if s.Effect1 == 0 && s.EffectAura1 == 0 { return stats, fmt.Errorf("spell does not have an effect1 or auraEffect1") } effects := s.GetAuraEffects() if s.ID == 9397 { log.Printf("Spell: %v AuraEffect1: %v AuraEffect2: %v AuraEffect3: %v", s.Name, s.EffectAura1, s.EffectAura2, s.EffectAura3) } var seen []int for _, e := range effects { if !AuraEffectCanBeConv(e.Effect) { continue } statId := convertAuraEffect(e.Effect) if statId == -1 { continue } if funk.Contains(seen, statId) { continue } // keep track if we have already seen this stat so we do not duplicate // Wotlk changed everything to spell power so might as well do the same in // scaling process. seen = append(seen, statId) statMod := float64(StatModifiers[statId]) stats = append(stats, ConvItemStat{ StatType: statId, StatValue: e.CalculatedMax, Budget: int(math.Abs(math.Ceil(float64(e.CalculatedMax) * statMod))), }) } // Handle special stat case where 189 is catch all for crit, dodge, parry, hit, haste, expertise if s.Effect1 != 0 && s.Effect1 == 6 && (s.EffectAura1 == 189 || s.EffectAura1 == 123) { log.Printf("Special case for spell aura effect: %v", s.Description) statId := parseStatDesc(s.Description) if statId == 0 { log.Printf("Could not determine stat for spell aura effect description: %v", s.Name) } calced := calcMaxValue(s.EffectBasePoints1, s.EffectDieSides1) log.Printf("StatId: %v Calced: %v", statId, calced) stats = append(stats, ConvItemStat{ StatType: statId, StatValue: calced, Budget: int(math.Abs(math.Ceil(float64(calced) * float64(StatModifiers[statId])))), }) } return stats, nil } // This spell can be converted fully into a stat and not needed on the item func (s Spell) CanBeConverted() bool { // if there are any spell effects that are not aura effects, then it can be converted effects := s.GetSpellEffects() for _, e := range effects { if e.Effect != 0 && e.Effect != 6 { return false } } // Unfortunately if there are any mixed effects for auras, it is too difficult to split those so just // bail out auras := s.GetAuraEffects() auraFlag := false for _, a := range auras { if a.Effect == 0 { continue } if AuraEffectCanBeConv(a.Effect) { return true } } return auraFlag } // based on the description determine the stat to func parseStatDesc(desc string) int { if strings.Contains(desc, "critical strike") { return 32 } if strings.Contains(desc, "dodge") { return 13 } if strings.Contains(desc, "parry") { return 14 } if strings.Contains(desc, "hit rating") { return 31 } if strings.Contains(desc, "haste rating") { return 36 } if strings.Contains(desc, "expertise rating") { return 37 } if strings.Contains(desc, "defense rating") { return 12 } if strings.Contains(desc, "block rating") { return 15 } if strings.Contains(desc, "armor penetration") { return 44 } if strings.Contains(desc, "spell penetration") { return 47 } return 0 } // Scales a spell effect, means creating a new spell with the same effect but scaled to a new item level, then passing // back the new spellId, In order to be predictable I will use 30000000 for rare, 31000000 for epic, 32000000 for legendary // An example of this might on hit do $s1 nature damage over $d seconds. We would just scale the $s1 value // based on the formula below. This assumes that Blizzard has already balanced the spell bonus against the // stats on the item level and quality. This is a big assumption as the stats are not penalized // from having the extra damage. This could really create some unique sought after weapons that exploit this. // modified ratio ((s1 / existing iLevel) * newIlevel) * (0.20 Rare or 0.30 Epic or 0.4 for Legendary). func (s *Spell) ScaleSpell(fromItemLevel int, itemLevel int, itemQuality int) (int, error) { s.Scaled = false qualModifier := map[int]float64{ 3: 1.20, 4: 1.30, 5: 1.40, } idBump := 30000000 if itemQuality == 4 { idBump = 31000000 } if itemQuality == 5 { idBump = 32000000 } // direct damage types dd := [...]int{2, 9, 10} didScale := false // Causes direct damage if s.Effect1 != 0 && funk.Contains(dd, s.Effect1) { s.EffectBasePoints1 = int(float64(s.EffectBasePoints1) / float64(fromItemLevel) * float64(itemLevel) * qualModifier[itemQuality] * 2.5) didScale = true } if s.Effect2 != 0 && funk.Contains(dd, s.Effect1) { s.EffectBasePoints2 = int(float64(s.EffectBasePoints2) / float64(fromItemLevel) * float64(itemLevel) * qualModifier[itemQuality] * 2.5) didScale = true } // Restores a Power / Mana if s.Effect1 != 0 && s.Effect1 == 30 { // skip anyhing else that is not mana as they are flat values if strings.Contains(s.Description, "Mana") || strings.Contains(s.Description, "mana") { s.EffectBasePoints1 = int(float64(s.EffectBasePoints1) / float64(fromItemLevel) * float64(itemLevel) * qualModifier[itemQuality]) didScale = true } } // Scales a stat buff if s.Effect1 != 0 && s.Effect1 == 35 { s.EffectBasePoints1 = int(float64(s.EffectBasePoints1) / float64(fromItemLevel) * float64(itemLevel) * qualModifier[itemQuality]) didScale = true } if s.Effect1 != 0 && s.Effect2 == 35 { s.EffectBasePoints2 = int(float64(s.EffectBasePoints2) / float64(fromItemLevel) * float64(itemLevel) * qualModifier[itemQuality]) didScale = true } // Handle special aura effects if s.EffectAura1 != 0 && s.EffectAura1 == 3 && s.Effect1 == 6 { s.EffectBasePoints1 = int(float64(s.EffectBasePoints1) / float64(fromItemLevel) * float64(itemLevel) * qualModifier[itemQuality] * 2) didScale = true } // Damage Shield Increase Scale due to HP curve if s.EffectAura1 != 0 && s.EffectAura1 == 15 && s.Effect1 == 6 { s.EffectBasePoints1 = int(float64(s.EffectBasePoints1) / float64(fromItemLevel) * float64(itemLevel) * qualModifier[itemQuality] * 1.50) didScale = true } if !didScale { return 0, fmt.Errorf("did not qualify to be scaled in ScaleSpell %v (%v)", s.Name, s.ID) } s.Scaled = true return idBump + s.ID, nil }