mirror of
https://github.com/araxiaonline/wow-item-generator.git
synced 2026-06-13 03:02:22 -04:00
466 lines
13 KiB
Go
466 lines
13 KiB
Go
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
|
|
}
|