1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-11-10 14:26:15 -05:00
OpenDiablo2/d2core/d2stats/stat.go
dk cf6029eb95
Stat descriptions + tests, Skilldesc.txt loader (#590)
* adding ranged number type, for use in stats

* Loaded Skills.txt

* asset manager only binds terminal commands if terminal != nil

* WIP stats

* cache getter and clear methods were not implemented

* asset manager handles a nil terminal pointer

* adding skilldesc.txt loader (needs work)

* ctc stat descriptions functions working

* moving description functionality out of itemstatcost loader and into stats

* stats seem like a central part of diablo, moving into d2core.

* stats seem like a central part of diablo, moving into d2core.

* delint

* adding statlist, statlist reduction, unit tests

* minor edits to stat.go

* lint error in statlist.go

* Remove dependency on actual data from mpq files

stats unit tests now use mock data

* fixing some lint errors, formatting

Co-authored-by: Maxime Lavigne (malavv) <duguigne@gmail.com>
2020-07-17 18:50:45 -04:00

725 lines
18 KiB
Go

package d2stats
import (
"fmt"
"regexp"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
type descValPosition int
const (
descValHide descValPosition = iota
descValPrefix
descValPostfix
)
// CreateStat creates a stat instance with the given ID and number of values
func CreateStat(record *d2datadict.ItemStatCostRecord, values ...int) *Stat {
if record == nil {
return nil
}
stat := &Stat{
Record: record,
Values: values,
}
return stat
}
// Stat is an instance of a Stat, with a set of Values
type Stat struct {
Record *d2datadict.ItemStatCostRecord
Values []int
}
// Clone returns a deep copy of the Stat
func (s Stat) Clone() *Stat {
clone := &Stat{
Record: s.Record,
Values: make([]int, len(s.Values)),
}
for idx := range s.Values {
clone.Values[idx] = s.Values[idx]
}
return clone
}
// Combine sums the other stat with this one, altering the
// values of this one.
func (s *Stat) combine(other *Stat) (success bool) {
if !s.canBeCombinedWith(other) {
return false
}
for idx := range s.Values {
// todo different combination logic per descfnid
s.Values[idx] += other.Values[idx]
}
return true
}
func (s *Stat) canBeCombinedWith(other *Stat) bool {
if s.Record != other.Record {
return false
}
if len(s.Values) != len(other.Values) {
return false
}
// todo `10% reanimate as: foo` is not the same as `10% reanimate as: bar`
return true
}
// Description returns the formatted description string
func (s *Stat) Description() string {
return s.DescString(s.Values...)
}
// StatDescriptionFormatStrings is an array of the base format strings used
// by the `descfn` methods for stats. The records in itemstatcost.txt have a
// number field which denotes which of these functions is used for formatting
// the stat description.
// These came from phrozen keep:
// https://d2mods.info/forum/kb/viewarticle?a=448
//nolint:gochecknoglobals // better for lookup
var baseFormatStrings = []string{
"",
"%v %s",
"%v%% %s",
"%v %s",
"%v%% %s",
"%v%% %s",
"%v %s %s",
"%v%% %v %s",
"%v%% %s %s",
"%v %s %s",
"%v %s %s",
"Repairs 1 Durability In %v Seconds",
"%v +%v",
"+%v %s",
"+%v to %s %s",
"%v%% %s",
"%v %s",
"%v %s (Increases near %v)",
"%v%% %s (Increases near %v)",
"",
"%v%% %s",
"%v %s",
"%v%% %s %s",
"%v%% %s %s",
"Level %v %s %s",
"",
"",
"+%v to %s %s",
"+%v to %s",
}
var statValueCountLookup map[int]int //nolint:gochecknoglobals // lookup
// DescString return a string based on the DescFnID
func (s *Stat) DescString(values ...int) string {// nolint:gocyclo switch statement is not so bad
if s.Record.DescFnID < 0 || s.Record.DescFnID > len(baseFormatStrings) {
return ""
}
var result string
//nolint:gomdn introducing a const for these would be worse
switch s.Record.DescFnID {
case 1:
result = s.descFn1(values...)
case 2:
result = s.descFn2(values...)
case 3:
result = s.descFn3(values...)
case 4:
result = s.descFn4(values...)
case 5:
result = s.descFn5(values...)
case 6:
result = s.descFn6(values...)
case 7:
result = s.descFn7(values...)
case 8:
result = s.descFn8(values...)
case 9:
result = s.descFn9(values...)
case 11:
result = s.descFn11(values...)
case 12:
result = s.descFn12(values...)
case 13:
result = s.descFn13(values...)
case 14:
result = s.descFn14(values...)
case 15:
result = s.descFn15(values...)
case 16:
result = s.descFn16(values...)
case 20:
result = s.descFn20(values...)
case 22:
result = s.descFn22(values...)
case 23:
result = s.descFn23(values...)
case 24:
result = s.descFn24(values...)
case 27:
result = s.descFn27(values...)
case 28:
result = s.descFn28(values...)
}
return result
}
func (s *Stat) descFn1(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
// we know there is only one value for this stat
value := values[0]
var stringTableKey string
if value < 0 {
stringTableKey = s.Record.DescStrNeg
} else {
format = strings.Join([]string{"+", format}, "")
stringTableKey = s.Record.DescStrPos
}
stringTableString := d2common.TranslateString(stringTableKey)
var result string
switch descValPosition(s.Record.DescVal) {
case descValHide:
result = fmt.Sprintf(format, stringTableString)
case descValPrefix:
result = fmt.Sprintf(format, value, stringTableString)
case descValPostfix:
formatSplit := strings.Split(format, " ")
format = strings.Join(reverseStringSlice(formatSplit), " ")
result = fmt.Sprintf(format, stringTableString, value)
default:
result = ""
}
result = strings.ReplaceAll(result, "+-", "-")
result = strings.ReplaceAll(result, " +%d", "")
return result
}
func (s *Stat) descFn2(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
// we know there is only one value for this stat
value := values[0]
var stringTableKey string
if value < 0 {
stringTableKey = s.Record.DescStrNeg
} else {
format = strings.Join([]string{"+", format}, "")
stringTableKey = s.Record.DescStrPos
}
stringTableString := d2common.TranslateString(stringTableKey)
var result string
switch descValPosition(s.Record.DescVal) {
case descValHide:
result = fmt.Sprintf(format, stringTableString)
case descValPrefix:
result = fmt.Sprintf(format, value, stringTableString)
case descValPostfix:
formatSplit := strings.Split(format, " ")
format = strings.Join(reverseStringSlice(formatSplit), " ")
result = fmt.Sprintf(format, stringTableString, value)
default:
result = ""
}
return fixString(result)
}
func (s *Stat) descFn3(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
// we know there is only one value for this stat
value := values[0]
var stringTableKey string
if value < 0 {
stringTableKey = s.Record.DescStrNeg
} else {
stringTableKey = s.Record.DescStrPos
}
stringTableString := d2common.TranslateString(stringTableKey)
var result string
switch descValPosition(s.Record.DescVal) {
case descValHide:
format = strings.Split(format, " ")[0]
result = fmt.Sprintf(format, stringTableString)
case descValPrefix:
result = fmt.Sprintf(format, value, stringTableString)
case descValPostfix:
formatSplit := strings.Split(format, " ")
format = strings.Join(reverseStringSlice(formatSplit), " ")
result = fmt.Sprintf(format, stringTableString, value)
default:
result = ""
}
return fixString(result)
}
func (s *Stat) descFn4(values ...int) string {
// for now, same as fn2
return s.descFn2(values...)
}
func (s *Stat) descFn5(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
// we know there is only one value for this stat
value := values[0]
var stringTableKey string
if value < 0 {
stringTableKey = s.Record.DescStrNeg
} else {
stringTableKey = s.Record.DescStrPos
}
stringTableString := d2common.TranslateString(stringTableKey)
var result string
switch descValPosition(s.Record.DescVal) {
case descValHide:
result = fmt.Sprintf(format, stringTableString)
case descValPrefix:
result = fmt.Sprintf(format, value, stringTableString)
case descValPostfix:
formatSplit := strings.Split(format, " ")
format = strings.Join(reverseStringSlice(formatSplit), " ")
result = fmt.Sprintf(format, stringTableString, value)
default:
result = ""
}
return fixString(result)
}
func (s *Stat) descFn6(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
// we know there is only one value for this stat
value := values[0]
var stringTableKey1 string
if value < 0 {
stringTableKey1 = s.Record.DescStrNeg
} else {
format = strings.Join([]string{"+", format}, "")
stringTableKey1 = s.Record.DescStrPos
}
stringTableStr1 := d2common.TranslateString(stringTableKey1)
// this stat has an additional string (Based on Character Level)
stringTableStr2 := d2common.TranslateString(s.Record.DescStr2)
var result string
switch descValPosition(s.Record.DescVal) {
case descValHide:
result = fmt.Sprintf(format, stringTableStr1, stringTableStr2)
case descValPrefix:
result = fmt.Sprintf(format, value, stringTableStr1, stringTableStr2)
case descValPostfix:
formatSplit := strings.Split(format, " ")
format = strings.Join(reverseStringSlice(formatSplit), " ")
result = fmt.Sprintf(format, stringTableStr1, value)
default:
result = ""
}
return fixString(result)
}
func (s *Stat) descFn7(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
// we know there is only one value for this stat
value := values[0]
var stringTableKey string
if value < 0 {
stringTableKey = s.Record.DescStrNeg
} else {
format = strings.Join([]string{"+", format}, "")
stringTableKey = s.Record.DescStrPos
}
stringTableStr1 := d2common.TranslateString(stringTableKey)
// this stat has an additional string (Based on Character Level)
stringTableStr2 := d2common.TranslateString(s.Record.DescStr2)
var result string
switch descValPosition(s.Record.DescVal) {
case descValHide:
result = fmt.Sprintf(format, stringTableStr1, stringTableStr2)
case descValPrefix:
result = fmt.Sprintf(format, value, stringTableStr1, stringTableStr2)
case descValPostfix:
formatSplit := strings.Split(format, " ")
formatSplit[0], formatSplit[1] = formatSplit[1], formatSplit[0]
format = strings.Join(formatSplit, " ")
result = fmt.Sprintf(format, stringTableStr1, value, stringTableStr2)
default:
result = ""
}
return result
}
func (s *Stat) descFn8(values ...int) string {
// for now, same as fn7
return s.descFn7(values...)
}
func (s *Stat) descFn9(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
// we know there is only one value for this stat
value := values[0]
var stringTableKey1 string
if value < 0 {
stringTableKey1 = s.Record.DescStrNeg
} else {
stringTableKey1 = s.Record.DescStrPos
}
stringTableStr1 := d2common.TranslateString(stringTableKey1)
// this stat has an additional string (Based on Character Level)
stringTableStr2 := d2common.TranslateString(s.Record.DescStr2)
var result string
switch descValPosition(s.Record.DescVal) {
case descValHide:
result = fmt.Sprintf(format, stringTableStr1, stringTableStr2)
case descValPrefix:
result = fmt.Sprintf(format, value, stringTableStr1, stringTableStr2)
case descValPostfix:
formatSplit := strings.Split(format, " ")
formatSplit[0], formatSplit[1] = formatSplit[1], formatSplit[0]
format = strings.Join(formatSplit, " ")
result = fmt.Sprintf(format, stringTableStr1, value, stringTableStr2)
default:
result = ""
}
return result
}
func (s *Stat) descFn11(values ...int) string {
// we know there is only one value for this stat
value := values[0]
// the only stat to use this fn is "Repairs durability in X seconds"
format := d2common.TranslateString(s.Record.DescStrPos)
return fmt.Sprintf(format, value)
}
func (s *Stat) descFn12(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
// we know there is only one value for this stat
value := values[0]
str1 := d2common.TranslateString(s.Record.DescStrPos)
return fmt.Sprintf(format, str1, value)
}
func (s *Stat) descFn13(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
numSkills, heroIndex := values[0], values[1]
heroMap := map[int]d2enum.Hero{
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
int(d2enum.HeroDruid): d2enum.HeroDruid,
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
}
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
descStr1 := d2common.TranslateString(classRecord.SkillStrAll)
result := fmt.Sprintf(format, numSkills, descStr1)
result = strings.ReplaceAll(result, "+-", "-")
return result
}
func (s *Stat) descFn14(values ...int) string {
numSkills, heroIndex, skillTabIndex := values[0], values[1], values[2]
if skillTabIndex > 2 || skillTabIndex < 0 {
skillTabIndex = 0
}
heroMap := map[int]d2enum.Hero{
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
int(d2enum.HeroDruid): d2enum.HeroDruid,
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
}
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
skillTabKey := classRecord.SkillStrTab[skillTabIndex]
classOnlyKey := classRecord.SkillStrClassOnly
skillTabStr := d2common.TranslateString(skillTabKey) + " %v"
skillTabStr = strings.ReplaceAll(skillTabStr, "%d", "%v")
classOnlyStr := d2common.TranslateString(classOnlyKey)
result := fmt.Sprintf(skillTabStr, numSkills, classOnlyStr)
return fixString(result)
}
func within(n, min, max int) int {
if n < min {
n = min
} else if n > max {
n = max
}
return n
}
func (s *Stat) descFn15(values ...int) string {
format := d2common.TranslateString(s.Record.DescStrPos)
chanceToCast, skillLevel, skillIndex := values[0], values[1], values[2]
chanceToCast = within(chanceToCast, 0, 100)
skillLevel = within(skillLevel, 0, 255)
skillLevel = within(skillLevel, 0, 255)
skillRecord := d2datadict.SkillDetails[skillIndex]
result := fmt.Sprintf(format, chanceToCast, skillLevel, skillRecord.Skill)
result = strings.ReplaceAll(result, "+-", "-")
return result
}
func (s *Stat) descFn16(values ...int) string {
skillLevel, skillIndex := values[0], values[1]
str1 := d2common.TranslateString(s.Record.DescStrPos)
skillRecord := d2datadict.SkillDetails[skillIndex]
result := fmt.Sprintf(str1, skillLevel, skillRecord.Skill)
return fixString(result)
}
/*
func (s *Stat) descFn17(values ...int) string {
// these were not implemented in original D2
// leaving them out for now as I don't know how to
// write a test for them, nor do I think vanilla content uses them
// but these are the stat keys which point to this func...
// item_armor_bytime
// item_hp_bytime
// item_mana_bytime
// item_maxdamage_bytime
// item_strength_bytime
// item_dexterity_bytime
// item_energy_bytime
// item_vitality_bytime
// item_tohit_bytime
// item_cold_damagemax_bytime
// item_fire_damagemax_bytime
// item_ltng_damagemax_bytime
// item_pois_damagemax_bytime
// item_stamina_bytime
// item_tohit_demon_bytime
// item_tohit_undead_bytime
// item_kick_damage_bytime
}
func (s *Stat) descFn18(values ...int) string {
// ... same with these ...
// item_armorpercent_bytime
// item_maxdamage_percent_bytime
// item_tohitpercent_bytime
// item_resist_cold_bytime
// item_resist_fire_bytime
// item_resist_ltng_bytime
// item_resist_pois_bytime
// item_absorb_cold_bytime
// item_absorb_fire_bytime
// item_absorb_ltng_bytime
// item_find_gold_bytime
// item_find_magic_bytime
// item_regenstamina_bytime
// item_damage_demon_bytime
// item_damage_undead_bytime
// item_crushingblow_bytime
// item_openwounds_bytime
// item_deadlystrike_bytime
}
*/
func (s *Stat) descFn20(values ...int) string {
// for now, same as fn2
return s.descFn2(values...)
}
func (s *Stat) descFn22(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
statAgainst, monsterIndex := values[0], values[1]
var monsterKey string
for key := range d2datadict.MonStats {
if d2datadict.MonStats[key].Id == monsterIndex {
monsterKey = key
break
}
}
str1 := d2common.TranslateString(s.Record.DescStrPos)
monsterName := d2datadict.MonStats[monsterKey].NameString
result := fmt.Sprintf(format, statAgainst, str1, monsterName)
result = strings.ReplaceAll(result, "+-", "-")
return result
}
func (s *Stat) descFn23(values ...int) string {
// for now, same as fn22
return s.descFn22(values...)
}
func (s *Stat) descFn24(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
lvl, skillID, chargeMax, chargeCurrent := values[0], values[1], values[2], values[3]
charges := d2common.TranslateString(s.Record.DescStrPos)
charges = fmt.Sprintf(charges, chargeCurrent, chargeMax)
skillName := d2datadict.SkillDetails[skillID].Skill
result := fmt.Sprintf(format, lvl, skillName, charges)
return result
}
func (s *Stat) descFn27(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
amount, skillID, heroIndex := values[0], values[1], values[2]
skillName := d2datadict.SkillDetails[skillID].Skill
heroMap := map[int]d2enum.Hero{
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
int(d2enum.HeroDruid): d2enum.HeroDruid,
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
}
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
classOnlyStr := d2common.TranslateString(classRecord.SkillStrClassOnly)
return fmt.Sprintf(format, amount, skillName, classOnlyStr)
}
func (s *Stat) descFn28(values ...int) string {
format := baseFormatStrings[s.Record.DescFnID]
amount, skillID := values[0], values[1]
skillName := d2datadict.SkillDetails[skillID].Skill
return fmt.Sprintf(format, amount, skillName)
}
func fixString(s string) string {
s = strings.ReplaceAll(s, "+-", "-")
s = strings.ReplaceAll(s, " +%d", "")
return s
}
// DescGroupString return a string based on the DescGroupFuncID
func (s *Stat) DescGroupString(a ...interface{}) string {
if s.Record.DescGroupFuncID < 0 || s.Record.DescGroupFuncID > len(
baseFormatStrings) {
return ""
}
format := baseFormatStrings[s.Record.DescGroupFuncID]
return fmt.Sprintf(format, a...)
}
// NumStatValues returns the number of values a stat instance for this
// record should have
func (s *Stat) NumStatValues() int {
if num, found := statValueCountLookup[s.Record.DescGroupFuncID]; found {
return num
}
if statValueCountLookup == nil {
statValueCountLookup = make(map[int]int)
}
format := baseFormatStrings[s.Record.DescGroupFuncID]
pattern := regexp.MustCompile("%v")
matches := pattern.FindAllStringIndex(format, -1)
num := len(matches)
statValueCountLookup[s.Record.DescGroupFuncID] = num
return num
}
func reverseStringSlice(s []string) []string {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return s
}