mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-09 18:17:07 -05:00
add interfaces for stats, added diablo 2 implementation (#614)
* add interface for stats, d2 is an implementation * fix incorrect comment, remove ennecessary int * simplified description functions, remove duplicates * moved default stringer functions * fixed incorrect stat combine method * change `Create` to `New` in method names
This commit is contained in:
parent
7a49f3637f
commit
c114ab9eb7
45
d2core/d2stats/diablo2stats/diablo2stats.go
Normal file
45
d2core/d2stats/diablo2stats/diablo2stats.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package diablo2stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewStat creates a stat instance with the given record and values
|
||||||
|
func NewStat(record *d2datadict.ItemStatCostRecord, values ...d2stats.StatValue) d2stats.Stat {
|
||||||
|
if record == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stat := &Diablo2Stat{
|
||||||
|
record: record,
|
||||||
|
values: values,
|
||||||
|
}
|
||||||
|
|
||||||
|
return stat
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatList creates a stat list
|
||||||
|
func NewStatList(stats ...d2stats.Stat) d2stats.StatList {
|
||||||
|
return &Diablo2StatList{stats}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStatValue creates a stat value of the given type
|
||||||
|
func NewStatValue(t d2stats.StatValueType) d2stats.StatValue {
|
||||||
|
sv := &Diablo2StatValue{_type: t}
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case d2stats.StatValueFloat:
|
||||||
|
sv._stringer = stringerUnsignedFloat
|
||||||
|
case d2stats.StatValueInt:
|
||||||
|
sv._stringer = stringerUnsignedInt
|
||||||
|
default:
|
||||||
|
sv._stringer = stringerEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
return sv
|
||||||
|
}
|
||||||
|
|
||||||
|
func intVal(i int) d2stats.StatValue {
|
||||||
|
return NewStatValue(d2stats.StatValueInt).SetInt(i)
|
||||||
|
}
|
2
d2core/d2stats/diablo2stats/doc.go
Normal file
2
d2core/d2stats/diablo2stats/doc.go
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
// Package diablo2stats is the Diablo 2 stats implementation
|
||||||
|
package diablo2stats
|
527
d2core/d2stats/diablo2stats/stat.go
Normal file
527
d2core/d2stats/diablo2stats/stat.go
Normal file
@ -0,0 +1,527 @@
|
|||||||
|
package diablo2stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
// static check that Diablo2Stat implements Stat
|
||||||
|
var _ d2stats.Stat = &Diablo2Stat{}
|
||||||
|
|
||||||
|
type descValPosition int
|
||||||
|
|
||||||
|
const (
|
||||||
|
descValHide descValPosition = iota
|
||||||
|
descValPrefix
|
||||||
|
descValPostfix
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxSkillTabIndex = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
twoComponentStr = "%s %s"
|
||||||
|
threeComponentStr = "%s %s %s"
|
||||||
|
fourComponentStr = "%s %s %s %s"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Diablo2Stat is an instance of a Diablo2Stat, with a set of values
|
||||||
|
type Diablo2Stat struct {
|
||||||
|
record *d2datadict.ItemStatCostRecord
|
||||||
|
values []d2stats.StatValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns the name of the stat (the key in itemstatcosts)
|
||||||
|
func (s *Diablo2Stat) Name() string {
|
||||||
|
return s.record.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority returns the description printing priority
|
||||||
|
func (s *Diablo2Stat) Priority() int {
|
||||||
|
return s.record.DescPriority
|
||||||
|
}
|
||||||
|
|
||||||
|
// Values returns the stat values of the stat
|
||||||
|
func (s *Diablo2Stat) Values() []d2stats.StatValue {
|
||||||
|
return s.values
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetValues sets the stat values
|
||||||
|
func (s *Diablo2Stat) SetValues(values ...d2stats.StatValue) {
|
||||||
|
s.values = make([]d2stats.StatValue, len(values))
|
||||||
|
for idx := range values {
|
||||||
|
s.values[idx] = values[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a deep copy of the Diablo2Stat
|
||||||
|
func (s *Diablo2Stat) Clone() d2stats.Stat {
|
||||||
|
clone := &Diablo2Stat{
|
||||||
|
record: s.record,
|
||||||
|
values: make([]d2stats.StatValue, len(s.Values())),
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range s.values {
|
||||||
|
srcVal := s.values[idx]
|
||||||
|
dstVal := reflect.New(reflect.ValueOf(srcVal).Elem().Type()).Interface().(d2stats.StatValue)
|
||||||
|
|
||||||
|
switch srcVal.Type() {
|
||||||
|
case d2stats.StatValueInt:
|
||||||
|
dstVal.SetInt(srcVal.Int())
|
||||||
|
case d2stats.StatValueFloat:
|
||||||
|
dstVal.SetFloat(srcVal.Float())
|
||||||
|
}
|
||||||
|
|
||||||
|
clone.values[idx] = dstVal
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to this stat value the values of the given stat value
|
||||||
|
func (s *Diablo2Stat) Copy(from d2stats.Stat) d2stats.Stat {
|
||||||
|
srcValues := from.Values()
|
||||||
|
s.values = make([]d2stats.StatValue, len(srcValues))
|
||||||
|
|
||||||
|
for idx := range srcValues {
|
||||||
|
src := srcValues[idx]
|
||||||
|
valType := src.Type()
|
||||||
|
dst := &Diablo2StatValue{_type: valType}
|
||||||
|
dst.SetStringer(src.Stringer())
|
||||||
|
|
||||||
|
switch valType {
|
||||||
|
case d2stats.StatValueInt:
|
||||||
|
dst.SetInt(src.Int())
|
||||||
|
case d2stats.StatValueFloat:
|
||||||
|
dst.SetFloat(src.Float())
|
||||||
|
}
|
||||||
|
|
||||||
|
s.values[idx] = dst
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine sums the other stat with this one (does not alter this stat, returns altered clone!)
|
||||||
|
func (s *Diablo2Stat) Combine(other d2stats.Stat) (result d2stats.Stat, err error) {
|
||||||
|
cantBeCombinedErr := fmt.Errorf("cannot combine %s with %s", s.Name(), other.Name())
|
||||||
|
|
||||||
|
if !s.canBeCombinedWith(other) {
|
||||||
|
return nil, cantBeCombinedErr
|
||||||
|
}
|
||||||
|
|
||||||
|
result = s.Clone()
|
||||||
|
srcValues, dstValues := other.Values(), result.Values()
|
||||||
|
|
||||||
|
for idx := range result.Values() {
|
||||||
|
v1, v2 := dstValues[idx], srcValues[idx]
|
||||||
|
|
||||||
|
valType := v1.Type()
|
||||||
|
switch valType {
|
||||||
|
case d2stats.StatValueInt:
|
||||||
|
v1.SetInt(v1.Int() + v2.Int())
|
||||||
|
case d2stats.StatValueFloat:
|
||||||
|
v1.SetFloat(v1.Float() + v2.Float())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Diablo2Stat) canBeCombinedWith(other d2stats.Stat) bool {
|
||||||
|
if s.Name() != other.Name() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
values1, values2 := s.Values(), other.Values()
|
||||||
|
if len(values1) != len(values2) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := range values1 {
|
||||||
|
if values1[idx].Type() != values2[idx].Type() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the formatted description string
|
||||||
|
func (s *Diablo2Stat) String() string { //nolint:gocyclo switch statement is not so bad
|
||||||
|
var result string
|
||||||
|
|
||||||
|
//nolint:gomdn introducing a const for these would be worse
|
||||||
|
switch s.record.DescFnID {
|
||||||
|
case 1:
|
||||||
|
s.values[0].SetStringer(stringerIntSigned)
|
||||||
|
result = s.descFn1()
|
||||||
|
case 2:
|
||||||
|
s.values[0].SetStringer(stringerIntPercentageSigned)
|
||||||
|
result = s.descFn2()
|
||||||
|
case 3:
|
||||||
|
result = s.descFn3()
|
||||||
|
case 4:
|
||||||
|
s.values[0].SetStringer(stringerIntPercentageSigned)
|
||||||
|
result = s.descFn4()
|
||||||
|
case 5:
|
||||||
|
s.values[0].SetStringer(stringerIntPercentageUnsigned)
|
||||||
|
result = s.descFn5()
|
||||||
|
case 6:
|
||||||
|
s.values[0].SetStringer(stringerIntSigned)
|
||||||
|
result = s.descFn6()
|
||||||
|
case 7:
|
||||||
|
s.values[0].SetStringer(stringerIntPercentageSigned)
|
||||||
|
result = s.descFn7()
|
||||||
|
case 8:
|
||||||
|
s.values[0].SetStringer(stringerIntPercentageSigned)
|
||||||
|
result = s.descFn8()
|
||||||
|
case 9:
|
||||||
|
result = s.descFn9()
|
||||||
|
case 11:
|
||||||
|
result = s.descFn11()
|
||||||
|
case 12:
|
||||||
|
s.values[0].SetStringer(stringerIntSigned)
|
||||||
|
result = s.descFn12()
|
||||||
|
case 13:
|
||||||
|
s.values[0].SetStringer(stringerIntSigned)
|
||||||
|
s.values[1].SetStringer(stringerClassAllSkills)
|
||||||
|
result = s.descFn13()
|
||||||
|
case 14:
|
||||||
|
s.values[0].SetStringer(stringerIntSigned)
|
||||||
|
s.values[1].SetStringer(stringerClassOnly)
|
||||||
|
result = s.descFn14()
|
||||||
|
case 15:
|
||||||
|
s.values[2].SetStringer(stringerSkillName)
|
||||||
|
result = s.descFn15()
|
||||||
|
case 16:
|
||||||
|
s.values[1].SetStringer(stringerSkillName)
|
||||||
|
result = s.descFn16()
|
||||||
|
case 20:
|
||||||
|
s.values[0].SetStringer(stringerIntPercentageSigned)
|
||||||
|
result = s.descFn20()
|
||||||
|
case 22:
|
||||||
|
s.values[0].SetStringer(stringerIntPercentageUnsigned)
|
||||||
|
s.values[1].SetStringer(stringerMonsterName)
|
||||||
|
result = s.descFn22()
|
||||||
|
case 23:
|
||||||
|
s.values[0].SetStringer(stringerIntPercentageUnsigned)
|
||||||
|
s.values[1].SetStringer(stringerMonsterName)
|
||||||
|
result = s.descFn23()
|
||||||
|
case 24:
|
||||||
|
s.values[1].SetStringer(stringerSkillName)
|
||||||
|
result = s.descFn24()
|
||||||
|
case 27:
|
||||||
|
s.values[0].SetStringer(stringerIntSigned)
|
||||||
|
s.values[1].SetStringer(stringerSkillName)
|
||||||
|
s.values[2].SetStringer(stringerClassOnly)
|
||||||
|
result = s.descFn27()
|
||||||
|
case 28:
|
||||||
|
s.values[0].SetStringer(stringerIntSigned)
|
||||||
|
s.values[1].SetStringer(stringerSkillName)
|
||||||
|
result = s.descFn28()
|
||||||
|
default:
|
||||||
|
result = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// +31 to Strength
|
||||||
|
// Replenish Life +20 || Drain Life -8
|
||||||
|
func (s *Diablo2Stat) descFn1() string {
|
||||||
|
var stringTableKey, result string
|
||||||
|
|
||||||
|
value := s.values[0]
|
||||||
|
|
||||||
|
formatString := twoComponentStr
|
||||||
|
|
||||||
|
if value.Int() < 0 {
|
||||||
|
stringTableKey = s.record.DescStrNeg
|
||||||
|
} else {
|
||||||
|
stringTableKey = s.record.DescStrPos
|
||||||
|
}
|
||||||
|
|
||||||
|
stringTableString := d2common.TranslateString(stringTableKey)
|
||||||
|
|
||||||
|
switch descValPosition(s.record.DescVal) {
|
||||||
|
case descValPrefix:
|
||||||
|
result = fmt.Sprintf(formatString, value, stringTableString)
|
||||||
|
case descValPostfix:
|
||||||
|
result = fmt.Sprintf(formatString, stringTableString, value)
|
||||||
|
case descValHide:
|
||||||
|
result = stringTableString
|
||||||
|
default:
|
||||||
|
result = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// +16% Increased Chance of Blocking
|
||||||
|
// Lightning Absorb +10%
|
||||||
|
func (s *Diablo2Stat) descFn2() string {
|
||||||
|
// for now, same as fn1
|
||||||
|
return s.descFn1()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Damage Reduced by 25
|
||||||
|
// Slain Monsters Rest in Peace
|
||||||
|
func (s *Diablo2Stat) descFn3() string {
|
||||||
|
// for now, same as fn1
|
||||||
|
return s.descFn1()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Poison Resist +25%
|
||||||
|
// +25% Faster Run/Walk
|
||||||
|
func (s *Diablo2Stat) descFn4() string {
|
||||||
|
// for now, same as fn1
|
||||||
|
return s.descFn1()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hit Causes Monster to Flee 25%
|
||||||
|
func (s *Diablo2Stat) descFn5() string {
|
||||||
|
// for now, same as fn1
|
||||||
|
return s.descFn1()
|
||||||
|
}
|
||||||
|
|
||||||
|
// +25 to Life (Based on Character Level)
|
||||||
|
func (s *Diablo2Stat) descFn6() string {
|
||||||
|
var stringTableKey, result string
|
||||||
|
|
||||||
|
value := s.values[0]
|
||||||
|
|
||||||
|
formatString := threeComponentStr
|
||||||
|
|
||||||
|
if value.Int() < 0 {
|
||||||
|
stringTableKey = s.record.DescStrNeg
|
||||||
|
} else {
|
||||||
|
stringTableKey = s.record.DescStrPos
|
||||||
|
}
|
||||||
|
|
||||||
|
str1 := d2common.TranslateString(stringTableKey)
|
||||||
|
str2 := d2common.TranslateString(s.record.DescStr2)
|
||||||
|
|
||||||
|
switch descValPosition(s.record.DescVal) {
|
||||||
|
case descValPrefix:
|
||||||
|
result = fmt.Sprintf(formatString, value, str1, str2)
|
||||||
|
case descValPostfix:
|
||||||
|
result = fmt.Sprintf(formatString, str1, value, str2)
|
||||||
|
case descValHide:
|
||||||
|
formatString = twoComponentStr
|
||||||
|
result = fmt.Sprintf(formatString, value, str2)
|
||||||
|
default:
|
||||||
|
result = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lightning Resist +25% (Based on Character Level)
|
||||||
|
// +25% Better Chance of Getting Magic Items (Based on Character Level)
|
||||||
|
func (s *Diablo2Stat) descFn7() string {
|
||||||
|
// for now, same as fn6
|
||||||
|
return s.descFn6()
|
||||||
|
}
|
||||||
|
|
||||||
|
// +25% Enhanced Defense (Based on Character Level)
|
||||||
|
// Heal Stamina Plus +25% (Based on Character Level)
|
||||||
|
func (s *Diablo2Stat) descFn8() string {
|
||||||
|
// for now, same as fn6
|
||||||
|
return s.descFn6()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attacker Takes Damage of 25 (Based on Character Level)
|
||||||
|
func (s *Diablo2Stat) descFn9() string {
|
||||||
|
var stringTableKey, result string
|
||||||
|
|
||||||
|
value := s.values[0]
|
||||||
|
|
||||||
|
formatString := threeComponentStr
|
||||||
|
|
||||||
|
if value.Int() < 0 {
|
||||||
|
stringTableKey = s.record.DescStrNeg
|
||||||
|
} else {
|
||||||
|
stringTableKey = s.record.DescStrPos
|
||||||
|
}
|
||||||
|
|
||||||
|
str1 := d2common.TranslateString(stringTableKey)
|
||||||
|
str2 := d2common.TranslateString(s.record.DescStr2)
|
||||||
|
|
||||||
|
switch descValPosition(s.record.DescVal) {
|
||||||
|
case descValPrefix:
|
||||||
|
result = fmt.Sprintf(formatString, value, str1, str2)
|
||||||
|
case descValPostfix:
|
||||||
|
result = fmt.Sprintf(formatString, str1, value, str2)
|
||||||
|
case descValHide:
|
||||||
|
result = fmt.Sprintf(twoComponentStr, value, str2)
|
||||||
|
default:
|
||||||
|
result = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repairs 2 durability per second
|
||||||
|
func (s *Diablo2Stat) descFn11() string {
|
||||||
|
var stringTableKey string
|
||||||
|
|
||||||
|
value := s.values[0]
|
||||||
|
|
||||||
|
if value.Int() < 0 {
|
||||||
|
stringTableKey = s.record.DescStrNeg
|
||||||
|
} else {
|
||||||
|
stringTableKey = s.record.DescStrPos
|
||||||
|
}
|
||||||
|
|
||||||
|
str1 := d2common.TranslateString(stringTableKey)
|
||||||
|
|
||||||
|
formatString := str1
|
||||||
|
|
||||||
|
return fmt.Sprintf(formatString, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hit Blinds Target +5
|
||||||
|
func (s *Diablo2Stat) descFn12() string {
|
||||||
|
// for now, same as fn1
|
||||||
|
return s.descFn1()
|
||||||
|
}
|
||||||
|
|
||||||
|
// +5 to Paladin Skill Levels
|
||||||
|
func (s *Diablo2Stat) descFn13() string {
|
||||||
|
value := s.values[0]
|
||||||
|
allSkills := s.values[1]
|
||||||
|
|
||||||
|
formatString := twoComponentStr
|
||||||
|
|
||||||
|
switch descValPosition(s.record.DescVal) {
|
||||||
|
case descValPrefix:
|
||||||
|
return fmt.Sprintf(formatString, value, allSkills)
|
||||||
|
case descValPostfix:
|
||||||
|
return fmt.Sprintf(formatString, allSkills, value)
|
||||||
|
case descValHide:
|
||||||
|
return allSkills.String()
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// +5 to Combat Skills (Paladin Only)
|
||||||
|
func (s *Diablo2Stat) descFn14() string {
|
||||||
|
// strings come out like `+5 to Combat Skills (Paladin Only)`
|
||||||
|
numSkills, hero, skillTab := s.values[0], s.values[1], s.values[2]
|
||||||
|
heroMap := getHeroMap()
|
||||||
|
heroIndex := hero.Int()
|
||||||
|
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
|
||||||
|
|
||||||
|
// diablo 2 is hardcoded to have only 3 skill tabs
|
||||||
|
skillTabIndex := skillTab.Int()
|
||||||
|
if skillTabIndex < 0 || skillTabIndex > maxSkillTabIndex {
|
||||||
|
skillTabIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// `+5`
|
||||||
|
numSkillsStr := numSkills.String()
|
||||||
|
|
||||||
|
// `to Combat Skills`
|
||||||
|
skillTabKey := classRecord.SkillStrTab[skillTabIndex]
|
||||||
|
skillTabStr := d2common.TranslateString(skillTabKey)
|
||||||
|
skillTabStr = strings.ReplaceAll(skillTabStr, "+%d ", "") // has a token we dont need
|
||||||
|
|
||||||
|
// `(Paladin Only)`
|
||||||
|
classOnlyStr := hero.String()
|
||||||
|
|
||||||
|
return fmt.Sprintf(threeComponentStr, numSkillsStr, skillTabStr, classOnlyStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5% Chance to cast level 7 Frozen Orb on attack
|
||||||
|
func (s *Diablo2Stat) descFn15() string {
|
||||||
|
chance, lvl, skill := s.values[0], s.values[1], s.values[2]
|
||||||
|
|
||||||
|
// Special case, `chance to cast` format is actually in the string table!
|
||||||
|
chanceToCastStr := d2common.TranslateString(s.record.DescStrPos)
|
||||||
|
|
||||||
|
return fmt.Sprintf(chanceToCastStr, chance.Int(), lvl.Int(), skill)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 3 Warmth Aura When Equipped
|
||||||
|
func (s *Diablo2Stat) descFn16() string {
|
||||||
|
skillLevel, skillIndex := s.values[0], s.values[1]
|
||||||
|
|
||||||
|
// Special case, `Level # XYZ Aura When Equipped`, format is actually in the string table!
|
||||||
|
format := d2common.TranslateString(s.record.DescStrPos)
|
||||||
|
|
||||||
|
return fmt.Sprintf(format, skillLevel.Int(), skillIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -25% Target Defense
|
||||||
|
func (s *Diablo2Stat) descFn20() string {
|
||||||
|
// for now, same as fn2
|
||||||
|
return s.descFn2()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 25% to Attack Rating versus Specter
|
||||||
|
func (s *Diablo2Stat) descFn22() string {
|
||||||
|
arBonus, monsterIndex := s.values[0], s.values[1]
|
||||||
|
arVersus := d2common.TranslateString(s.record.DescStrPos)
|
||||||
|
|
||||||
|
return fmt.Sprintf(threeComponentStr, arBonus, arVersus, monsterIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 25% Reanimate as: Specter
|
||||||
|
func (s *Diablo2Stat) descFn23() string {
|
||||||
|
// for now, same as fn22
|
||||||
|
return s.descFn22()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Level 25 Frozen Orb (19/20 Charges)
|
||||||
|
func (s *Diablo2Stat) descFn24() string {
|
||||||
|
// Special case formatting
|
||||||
|
format := "Level " + threeComponentStr
|
||||||
|
|
||||||
|
lvl, skill, chargeMax, chargeCurrent := s.values[0],
|
||||||
|
s.values[1],
|
||||||
|
s.values[2].Int(),
|
||||||
|
s.values[3].Int()
|
||||||
|
|
||||||
|
chargeStr := d2common.TranslateString(s.record.DescStrPos)
|
||||||
|
chargeStr = fmt.Sprintf(chargeStr, chargeCurrent, chargeMax)
|
||||||
|
|
||||||
|
return fmt.Sprintf(format, lvl, skill, chargeStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// +25 to Frozen Orb (Paladin Only)
|
||||||
|
func (s *Diablo2Stat) descFn27() string {
|
||||||
|
amount, skill, hero := s.values[0], s.values[1], s.values[2]
|
||||||
|
|
||||||
|
return fmt.Sprintf(fourComponentStr, amount, "to", skill, hero)
|
||||||
|
}
|
||||||
|
|
||||||
|
// +25 to Frozen Orb
|
||||||
|
func (s *Diablo2Stat) descFn28() string {
|
||||||
|
amount, skill := s.values[0], s.values[1]
|
||||||
|
|
||||||
|
return fmt.Sprintf(threeComponentStr, amount, "to", skill)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DescGroupString return a string based on the DescGroupFuncID
|
||||||
|
func (s *Diablo2Stat) DescGroupString(a ...interface{}) string {
|
||||||
|
if s.record.DescGroupFuncID < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
format := ""
|
||||||
|
for range a {
|
||||||
|
format += "%s "
|
||||||
|
}
|
||||||
|
|
||||||
|
format = strings.Trim(format, " ")
|
||||||
|
|
||||||
|
return fmt.Sprintf(format, a...)
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
package d2stats
|
package diablo2stats
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||||
@ -10,7 +11,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
errStr string = "incorrect description string format for stat"
|
errStr string = "incorrect description string format for stat"
|
||||||
errFmt string = "%v:\n\tKey: %v\n\tVal: %+v\n\texpected: %v\n\tgot: %v\n\n"
|
errFmt string = "%v:\n\tDescFnID: %v\n\tKey: %v\n\tVal: %+v\n\texpected: %v\n\tgot: %v\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
//nolint:funlen // this just gets mock data ready for the tests
|
//nolint:funlen // this just gets mock data ready for the tests
|
||||||
@ -261,7 +262,7 @@ func TestStat_InitMockData(t *testing.T) {
|
|||||||
|
|
||||||
func TestStat_Clone(t *testing.T) {
|
func TestStat_Clone(t *testing.T) {
|
||||||
r := d2datadict.ItemStatCosts["strength"]
|
r := d2datadict.ItemStatCosts["strength"]
|
||||||
s1 := CreateStat(r, 5)
|
s1 := NewStat(r, intVal(5))
|
||||||
s2 := s1.Clone()
|
s2 := s1.Clone()
|
||||||
|
|
||||||
// make sure the stats are distinct
|
// make sure the stats are distinct
|
||||||
@ -270,109 +271,109 @@ func TestStat_Clone(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make sure the stat values are unique
|
// make sure the stat values are unique
|
||||||
vs1, vs2 := s1.Values, s2.Values
|
vs1, vs2 := s1.Values(), s2.Values()
|
||||||
if &vs1 == &vs2 {
|
if &vs1 == &vs2 {
|
||||||
t.Errorf("stat values share the same pointer %d == %d", &s1, &s2)
|
t.Errorf("stat values share the same pointer %d == %d", &s1, &s2)
|
||||||
}
|
}
|
||||||
|
|
||||||
s2.Values[0] = 6
|
s2.Values()[0].SetInt(6)
|
||||||
v1, v2 := s1.Values[0], s2.Values[0]
|
v1, v2 := s1.Values()[0].Int(), s2.Values()[0].Int()
|
||||||
|
|
||||||
// make sure the value ranges are distinct
|
// make sure the value ranges are distinct
|
||||||
if v1 == v2 {
|
if v1 == v2 {
|
||||||
t.Errorf("stat value ranges should not be equal")
|
t.Errorf("clones should not share stat values")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStat_Descriptions(t *testing.T) {
|
func TestStat_Descriptions(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
recordKey string
|
recordKey string
|
||||||
vals []int
|
vals []d2stats.StatValue
|
||||||
expect string
|
expect string
|
||||||
}{
|
}{
|
||||||
// DescFn1
|
// DescFn1
|
||||||
{"strength", []int{31}, "+31 to Strength"},
|
{"strength", []d2stats.StatValue{intVal(31)}, "+31 to Strength"},
|
||||||
{"hpregen", []int{20}, "Replenish Life +20"},
|
{"hpregen", []d2stats.StatValue{intVal(20)}, "Replenish Life +20"},
|
||||||
{"hpregen", []int{-8}, "Drain Life -8"},
|
{"hpregen", []d2stats.StatValue{intVal(-8)}, "Drain Life -8"},
|
||||||
|
|
||||||
// DescFn2
|
// DescFn2
|
||||||
{"toblock", []int{16}, "+16% Increased Chance of Blocking"},
|
{"toblock", []d2stats.StatValue{intVal(16)}, "+16% Increased Chance of Blocking"},
|
||||||
{"item_absorblight_percent", []int{10}, "Lightning Absorb +10%"},
|
{"item_absorblight_percent", []d2stats.StatValue{intVal(10)}, "Lightning Absorb +10%"},
|
||||||
|
|
||||||
// DescFn3
|
// DescFn3
|
||||||
{"normal_damage_reduction", []int{25}, "Damage Reduced by 25"},
|
{"normal_damage_reduction", []d2stats.StatValue{intVal(25)}, "Damage Reduced by 25"},
|
||||||
{"item_restinpeace", []int{25}, "Slain Monsters Rest in Peace"},
|
{"item_restinpeace", []d2stats.StatValue{intVal(25)}, "Slain Monsters Rest in Peace"},
|
||||||
|
|
||||||
// DescFn4
|
// DescFn4
|
||||||
{"poisonresist", []int{25}, "Poison Resist +25%"},
|
{"poisonresist", []d2stats.StatValue{intVal(25)}, "Poison Resist +25%"},
|
||||||
{"item_fastermovevelocity", []int{25}, "+25% Faster Run/Walk"},
|
{"item_fastermovevelocity", []d2stats.StatValue{intVal(25)}, "+25% Faster Run/Walk"},
|
||||||
|
|
||||||
// DescFn5
|
// DescFn5
|
||||||
{"item_howl", []int{25}, "Hit Causes Monster to Flee 25%"},
|
{"item_howl", []d2stats.StatValue{intVal(25)}, "Hit Causes Monster to Flee 25%"},
|
||||||
|
|
||||||
// DescFn6
|
// DescFn6
|
||||||
{"item_hp_perlevel", []int{25}, "+25 to Life (Based on Character Level)"},
|
{"item_hp_perlevel", []d2stats.StatValue{intVal(25)}, "+25 to Life (Based on Character Level)"},
|
||||||
|
|
||||||
// DescFn7
|
// DescFn7
|
||||||
{"item_resist_ltng_perlevel", []int{25}, "Lightning Resist +25% (Based on Character Level)"},
|
{"item_resist_ltng_perlevel", []d2stats.StatValue{intVal(25)}, "Lightning Resist +25% (Based on Character Level)"},
|
||||||
{"item_find_magic_perlevel", []int{25}, "+25% Better Chance of Getting Magic Items (" +
|
{"item_find_magic_perlevel", []d2stats.StatValue{intVal(25)}, "+25% Better Chance of Getting Magic Items (" +
|
||||||
"Based on Character Level)"},
|
"Based on Character Level)"},
|
||||||
|
|
||||||
// DescFn8
|
// DescFn8
|
||||||
{"item_armorpercent_perlevel", []int{25}, "+25% Enhanced Defense (Based on Character Level)"},
|
{"item_armorpercent_perlevel", []d2stats.StatValue{intVal(25)}, "+25% Enhanced Defense (Based on Character Level)"},
|
||||||
{"item_regenstamina_perlevel", []int{25},
|
{"item_regenstamina_perlevel", []d2stats.StatValue{intVal(25)},
|
||||||
"Heal Stamina Plus +25% (Based on Character Level)"},
|
"Heal Stamina Plus +25% (Based on Character Level)"},
|
||||||
|
|
||||||
// DescFn9
|
// DescFn9
|
||||||
{"item_thorns_perlevel", []int{25}, "Attacker Takes Damage of 25 (Based on Character Level)"},
|
{"item_thorns_perlevel", []d2stats.StatValue{intVal(25)}, "Attacker Takes Damage of 25 (Based on Character Level)"},
|
||||||
|
|
||||||
// DescFn11
|
// DescFn11
|
||||||
{"item_replenish_durability", []int{2}, "Repairs 2 durability per second"},
|
{"item_replenish_durability", []d2stats.StatValue{intVal(2)}, "Repairs 2 durability per second"},
|
||||||
|
|
||||||
// DescFn12
|
// DescFn12
|
||||||
{"item_stupidity", []int{5}, "Hit Blinds Target +5"},
|
{"item_stupidity", []d2stats.StatValue{intVal(5)}, "Hit Blinds Target +5"},
|
||||||
|
|
||||||
// DescFn13
|
// DescFn13
|
||||||
{"item_addclassskills", []int{5, 3}, "+5 to Paladin Skill Levels"},
|
{"item_addclassskills", []d2stats.StatValue{intVal(5), intVal(3)}, "+5 to Paladin Skill Levels"},
|
||||||
|
|
||||||
// DescFn14
|
// DescFn14
|
||||||
{"item_addskill_tab", []int{5, 3, 0}, "+5 to Combat Skills (Paladin Only)"},
|
{"item_addskill_tab", []d2stats.StatValue{intVal(5), intVal(3), intVal(0)}, "+5 to Combat Skills (Paladin Only)"},
|
||||||
{"item_addskill_tab", []int{5, 3, 1}, "+5 to Offensive Auras (Paladin Only)"},
|
{"item_addskill_tab", []d2stats.StatValue{intVal(5), intVal(3), intVal(1)}, "+5 to Offensive Auras (Paladin Only)"},
|
||||||
{"item_addskill_tab", []int{5, 3, 2}, "+5 to Defensive Auras (Paladin Only)"},
|
{"item_addskill_tab", []d2stats.StatValue{intVal(5), intVal(3), intVal(2)}, "+5 to Defensive Auras (Paladin Only)"},
|
||||||
|
|
||||||
// DescFn15
|
// DescFn15
|
||||||
{"item_skillonattack", []int{5, 7, 64}, "5% Chance to cast level 7 Frozen Orb on attack"},
|
{"item_skillonattack", []d2stats.StatValue{intVal(5), intVal(7), intVal(64)}, "5% Chance to cast level 7 Frozen Orb on attack"},
|
||||||
|
|
||||||
// DescFn16
|
// DescFn16
|
||||||
{"item_aura", []int{3, 37}, "Level 3 Warmth Aura When Equipped"},
|
{"item_aura", []d2stats.StatValue{intVal(3), intVal(37)}, "Level 3 Warmth Aura When Equipped"},
|
||||||
|
|
||||||
// DescFn20
|
// DescFn20
|
||||||
{"item_fractionaltargetac", []int{-25}, "-25% Target Defense"},
|
{"item_fractionaltargetac", []d2stats.StatValue{intVal(-25)}, "-25% Target Defense"},
|
||||||
|
|
||||||
// DescFn22
|
// DescFn22
|
||||||
{"attack_vs_montype", []int{25, 40}, "25% to Attack Rating versus Specter"},
|
{"attack_vs_montype", []d2stats.StatValue{intVal(25), intVal(40)}, "25% to Attack Rating versus Specter"},
|
||||||
|
|
||||||
// DescFn23
|
// DescFn23
|
||||||
{"item_reanimate", []int{25, 40}, "25% Reanimate as: Specter"},
|
{"item_reanimate", []d2stats.StatValue{intVal(25), intVal(40)}, "25% Reanimate as: Specter"},
|
||||||
|
|
||||||
// DescFn24
|
// DescFn24
|
||||||
{"item_charged_skill", []int{25, 64, 20, 19}, "Level 25 Frozen Orb (19/20 Charges)"},
|
{"item_charged_skill", []d2stats.StatValue{intVal(25), intVal(64), intVal(20), intVal(19)}, "Level 25 Frozen Orb (19/20 Charges)"},
|
||||||
|
|
||||||
// DescFn27
|
// DescFn27
|
||||||
{"item_singleskill", []int{25, 64, 3}, "+25 to Frozen Orb (Paladin Only)"},
|
{"item_singleskill", []d2stats.StatValue{intVal(25), intVal(64), intVal(3)}, "+25 to Frozen Orb (Paladin Only)"},
|
||||||
|
|
||||||
// DescFn28
|
// DescFn28
|
||||||
{"item_nonclassskill", []int{25, 64}, "+25 to Frozen Orb"},
|
{"item_nonclassskill", []d2stats.StatValue{intVal(25), intVal(64)}, "+25 to Frozen Orb"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx := range tests {
|
for idx := range tests {
|
||||||
test := tests[idx]
|
test := tests[idx]
|
||||||
record := d2datadict.ItemStatCosts[test.recordKey]
|
record := d2datadict.ItemStatCosts[test.recordKey]
|
||||||
expect := test.expect
|
expect := test.expect
|
||||||
stat := CreateStat(record, test.vals...)
|
stat := NewStat(record, test.vals...)
|
||||||
|
|
||||||
if got := stat.Description(); got != expect {
|
if got := stat.String(); got != expect {
|
||||||
t.Errorf(errFmt, errStr, test.recordKey, test.vals, expect, got)
|
t.Errorf(errFmt, errStr, record.DescFnID, test.recordKey, test.vals, expect, got)
|
||||||
} else {
|
} else {
|
||||||
success := "[Desc Func %d][%s %+v] %s"
|
success := "[Desc Func %d][%s %+v] %s"
|
||||||
success = fmt.Sprintf(success, record.DescFnID, record.Name, test.vals, got)
|
success = fmt.Sprintf(success, record.DescFnID, record.Name, test.vals, got)
|
76
d2core/d2stats/diablo2stats/stat_value.go
Normal file
76
d2core/d2stats/diablo2stats/stat_value.go
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
package diablo2stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
// static check that Diablo2StatValue implements StatValue
|
||||||
|
var _ d2stats.StatValue = &Diablo2StatValue{}
|
||||||
|
|
||||||
|
// Diablo2StatValue is a diablo 2 implementation of a stat value
|
||||||
|
type Diablo2StatValue struct {
|
||||||
|
number float64
|
||||||
|
_stringer func(d2stats.StatValue) string
|
||||||
|
_type d2stats.StatValueType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the stat value type
|
||||||
|
func (sv *Diablo2StatValue) Type() d2stats.StatValueType {
|
||||||
|
return sv._type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a deep copy of the stat value
|
||||||
|
func (sv Diablo2StatValue) Clone() d2stats.StatValue {
|
||||||
|
clone := &Diablo2StatValue{}
|
||||||
|
|
||||||
|
switch sv._type {
|
||||||
|
case d2stats.StatValueInt:
|
||||||
|
clone.SetInt(sv.Int())
|
||||||
|
case d2stats.StatValueFloat:
|
||||||
|
clone.SetFloat(sv.Float())
|
||||||
|
}
|
||||||
|
|
||||||
|
clone._stringer = sv._stringer
|
||||||
|
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Int returns the integer version of the stat value
|
||||||
|
func (sv *Diablo2StatValue) Int() int {
|
||||||
|
return int(sv.number)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string version of the value
|
||||||
|
func (sv *Diablo2StatValue) String() string {
|
||||||
|
return sv._stringer(sv)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Float returns a float64 version of the value
|
||||||
|
func (sv *Diablo2StatValue) Float() float64 {
|
||||||
|
return sv.number
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInt sets the stat value using an int
|
||||||
|
func (sv *Diablo2StatValue) SetInt(i int) d2stats.StatValue {
|
||||||
|
sv.number = float64(i)
|
||||||
|
|
||||||
|
return sv
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFloat sets the stat value using a float64
|
||||||
|
func (sv *Diablo2StatValue) SetFloat(f float64) d2stats.StatValue {
|
||||||
|
sv.number = f
|
||||||
|
|
||||||
|
return sv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stringer returns the string evaluation function
|
||||||
|
func (sv *Diablo2StatValue) Stringer() func(d2stats.StatValue) string {
|
||||||
|
return sv._stringer
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStringer sets the string evaluation function
|
||||||
|
func (sv *Diablo2StatValue) SetStringer(f func(d2stats.StatValue) string) d2stats.StatValue {
|
||||||
|
sv._stringer = f
|
||||||
|
return sv
|
||||||
|
}
|
83
d2core/d2stats/diablo2stats/stat_value_stringers.go
Normal file
83
d2core/d2stats/diablo2stats/stat_value_stringers.go
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
package diablo2stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
monsterNotFound = "{Monster not found!}"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getHeroMap() map[int]d2enum.Hero {
|
||||||
|
return 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerUnsignedInt(sv d2stats.StatValue) string {
|
||||||
|
return fmt.Sprintf("%d", sv.Int())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerUnsignedFloat(sv d2stats.StatValue) string {
|
||||||
|
return fmt.Sprintf("%.2f", sv.Float())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerEmpty(_ d2stats.StatValue) string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerIntSigned(sv d2stats.StatValue) string {
|
||||||
|
return fmt.Sprintf("%+d", sv.Int())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerIntPercentageSigned(sv d2stats.StatValue) string {
|
||||||
|
return fmt.Sprintf("%+d%%", sv.Int())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerIntPercentageUnsigned(sv d2stats.StatValue) string {
|
||||||
|
return fmt.Sprintf("%d%%", sv.Int())
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerClassAllSkills(sv d2stats.StatValue) string {
|
||||||
|
heroIndex := sv.Int()
|
||||||
|
|
||||||
|
heroMap := getHeroMap()
|
||||||
|
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
|
||||||
|
|
||||||
|
return d2common.TranslateString(classRecord.SkillStrAll)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerClassOnly(sv d2stats.StatValue) string {
|
||||||
|
heroMap := getHeroMap()
|
||||||
|
heroIndex := sv.Int()
|
||||||
|
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
|
||||||
|
classOnlyKey := classRecord.SkillStrClassOnly
|
||||||
|
|
||||||
|
return d2common.TranslateString(classOnlyKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerSkillName(sv d2stats.StatValue) string {
|
||||||
|
skillRecord := d2datadict.SkillDetails[sv.Int()]
|
||||||
|
return skillRecord.Skill
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringerMonsterName(sv d2stats.StatValue) string {
|
||||||
|
for key := range d2datadict.MonStats {
|
||||||
|
if d2datadict.MonStats[key].Id == sv.Int() {
|
||||||
|
return d2datadict.MonStats[key].NameString
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return monsterNotFound
|
||||||
|
}
|
120
d2core/d2stats/diablo2stats/statlist.go
Normal file
120
d2core/d2stats/diablo2stats/statlist.go
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
package diablo2stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
// static check that Diablo2Stat implements Stat
|
||||||
|
var _ d2stats.StatList = &Diablo2StatList{}
|
||||||
|
|
||||||
|
// Diablo2StatList is a diablo 2 implementation of a stat list
|
||||||
|
type Diablo2StatList struct {
|
||||||
|
stats []d2stats.Stat
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index returns a stat given with the given index
|
||||||
|
func (sl *Diablo2StatList) Index(idx int) d2stats.Stat {
|
||||||
|
if idx < 0 || idx > len(sl.stats) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return sl.stats[idx]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stats returns a slice of stats
|
||||||
|
func (sl *Diablo2StatList) Stats() []d2stats.Stat {
|
||||||
|
return sl.stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStats sets the stats, given a slice of stats
|
||||||
|
func (sl *Diablo2StatList) SetStats(stats []d2stats.Stat) d2stats.StatList {
|
||||||
|
sl.stats = stats
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop removes the last stat from the stat list
|
||||||
|
func (sl *Diablo2StatList) Pop() d2stats.Stat {
|
||||||
|
num := len(sl.stats)
|
||||||
|
if num < 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := num - 1
|
||||||
|
last := sl.stats[idx]
|
||||||
|
sl.stats = sl.stats[:idx]
|
||||||
|
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push adds a stat at the end of the stat list
|
||||||
|
func (sl *Diablo2StatList) Push(stat d2stats.Stat) d2stats.StatList {
|
||||||
|
sl.stats = append(sl.stats, stat)
|
||||||
|
|
||||||
|
return sl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone returns a deep copy of the stat list
|
||||||
|
func (sl *Diablo2StatList) Clone() d2stats.StatList {
|
||||||
|
clone := &Diablo2StatList{}
|
||||||
|
clone.stats = make([]d2stats.Stat, len(sl.stats))
|
||||||
|
|
||||||
|
for idx := range sl.stats {
|
||||||
|
if stat := sl.Index(idx); stat != nil {
|
||||||
|
clone.stats[idx] = stat.Clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReduceStats combines like stats (does not alter this stat list, returns clone)
|
||||||
|
func (sl *Diablo2StatList) ReduceStats() d2stats.StatList {
|
||||||
|
clone := sl.Clone()
|
||||||
|
reduction := make([]d2stats.Stat, 0)
|
||||||
|
|
||||||
|
// for quick lookups
|
||||||
|
lookup := make(map[string]int)
|
||||||
|
|
||||||
|
for len(clone.Stats()) > 0 {
|
||||||
|
stat := clone.Pop()
|
||||||
|
|
||||||
|
// if we find it in the lookup, immediately try to combine
|
||||||
|
// if it doesn't combine, we append to the reduction
|
||||||
|
if idx, found := lookup[stat.Name()]; found {
|
||||||
|
if result, err := reduction[idx].Combine(stat); err == nil {
|
||||||
|
reduction[idx] = result
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// we didnt find it in the lookup, so we will try to combine with other stats
|
||||||
|
for idx := range reduction {
|
||||||
|
if _, err := reduction[idx].Combine(stat); err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup[stat.Name()] = len(lookup)
|
||||||
|
|
||||||
|
reduction = append(reduction, stat)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone.SetStats(reduction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveStatAtIndex removes the stat from the stat list, returns the stat
|
||||||
|
func (sl *Diablo2StatList) RemoveStatAtIndex(idx int) d2stats.Stat {
|
||||||
|
picked := sl.stats[idx]
|
||||||
|
sl.stats[idx] = sl.stats[len(sl.stats)-1]
|
||||||
|
sl.stats[len(sl.stats)-1] = nil
|
||||||
|
sl.stats = sl.stats[:len(sl.stats)-1]
|
||||||
|
|
||||||
|
return picked
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendStatList adds the stats from the other stat list to this stat list
|
||||||
|
func (sl *Diablo2StatList) AppendStatList(other d2stats.StatList) d2stats.StatList {
|
||||||
|
sl.stats = append(sl.stats, other.Stats()...)
|
||||||
|
|
||||||
|
return sl
|
||||||
|
}
|
106
d2core/d2stats/diablo2stats/statlist_test.go
Normal file
106
d2core/d2stats/diablo2stats/statlist_test.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package diablo2stats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDiablo2StatList_Index(t *testing.T) {
|
||||||
|
record := d2datadict.ItemStatCosts["strength"]
|
||||||
|
strength := NewStat(record, intVal(10))
|
||||||
|
|
||||||
|
list1 := &Diablo2StatList{stats: []d2stats.Stat{strength}}
|
||||||
|
if list1.Index(0) != strength {
|
||||||
|
t.Error("list should contain a stat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatList_Clone(t *testing.T) {
|
||||||
|
record := d2datadict.ItemStatCosts["strength"]
|
||||||
|
strength := NewStat(record, intVal(10))
|
||||||
|
|
||||||
|
list1 := &Diablo2StatList{}
|
||||||
|
list1.Push(strength)
|
||||||
|
|
||||||
|
list2 := list1.Clone()
|
||||||
|
str1 := list1.Index(0).String()
|
||||||
|
str2 := list2.Index(0).String()
|
||||||
|
|
||||||
|
if str1 != str2 {
|
||||||
|
t.Errorf("Stats of cloned stat list should be identitcal")
|
||||||
|
}
|
||||||
|
|
||||||
|
list2.Index(0).Values()[0].SetInt(0)
|
||||||
|
|
||||||
|
if list1.Index(0).String() == list2.Index(0).String() {
|
||||||
|
t.Errorf("Stats of cloned stat list should be different")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatList_Reduce(t *testing.T) {
|
||||||
|
records := []*d2datadict.ItemStatCostRecord{
|
||||||
|
d2datadict.ItemStatCosts["strength"],
|
||||||
|
d2datadict.ItemStatCosts["energy"],
|
||||||
|
d2datadict.ItemStatCosts["dexterity"],
|
||||||
|
d2datadict.ItemStatCosts["vitality"],
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := []d2stats.Stat{
|
||||||
|
NewStat(records[0], intVal(1)),
|
||||||
|
NewStat(records[0], intVal(1)),
|
||||||
|
NewStat(records[0], intVal(1)),
|
||||||
|
NewStat(records[0], intVal(1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
list := NewStatList(stats...)
|
||||||
|
reduction := list.ReduceStats()
|
||||||
|
|
||||||
|
if len(reduction.Stats()) != 1 || reduction.Index(0).String() != "+4 to Strength" {
|
||||||
|
t.Errorf("Diablo2Stat reduction failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
stats = []d2stats.Stat{
|
||||||
|
NewStat(records[0], intVal(1)),
|
||||||
|
NewStat(records[1], intVal(1)),
|
||||||
|
NewStat(records[2], intVal(1)),
|
||||||
|
NewStat(records[3], intVal(1)),
|
||||||
|
}
|
||||||
|
|
||||||
|
list = NewStatList(stats...)
|
||||||
|
reduction = list.ReduceStats()
|
||||||
|
|
||||||
|
if len(reduction.Stats()) != 4 {
|
||||||
|
t.Errorf("Diablo2Stat reduction failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatList_Append(t *testing.T) {
|
||||||
|
records := []*d2datadict.ItemStatCostRecord{
|
||||||
|
d2datadict.ItemStatCosts["strength"],
|
||||||
|
d2datadict.ItemStatCosts["energy"],
|
||||||
|
d2datadict.ItemStatCosts["dexterity"],
|
||||||
|
d2datadict.ItemStatCosts["vitality"],
|
||||||
|
}
|
||||||
|
|
||||||
|
list1 := &Diablo2StatList{
|
||||||
|
[]d2stats.Stat{
|
||||||
|
NewStat(records[0], intVal(1)),
|
||||||
|
NewStat(records[1], intVal(1)),
|
||||||
|
NewStat(records[2], intVal(1)),
|
||||||
|
NewStat(records[3], intVal(1)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
list2 := list1.Clone()
|
||||||
|
|
||||||
|
list3 := list1.AppendStatList(list2)
|
||||||
|
|
||||||
|
if len(list3.Stats()) != 8 {
|
||||||
|
t.Errorf("Diablo2Stat append failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(list3.ReduceStats().Stats()) != 4 {
|
||||||
|
t.Errorf("Diablo2Stat append failed")
|
||||||
|
}
|
||||||
|
}
|
@ -1,724 +1,14 @@
|
|||||||
package d2stats
|
package d2stats
|
||||||
|
|
||||||
import (
|
// Stat a generic interface for a stat. It is something which can be
|
||||||
"fmt"
|
// combined with other stats, holds one or more values, and handles the
|
||||||
"regexp"
|
// way that it is printed as a string
|
||||||
"strings"
|
type Stat interface {
|
||||||
|
Name() string
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
Clone() Stat
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
Copy(Stat) Stat
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
Combine(Stat) (combined Stat, err error)
|
||||||
)
|
String() string
|
||||||
|
Values() []StatValue
|
||||||
type descValPosition int
|
SetValues(...StatValue)
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
16
d2core/d2stats/stat_list.go
Normal file
16
d2core/d2stats/stat_list.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package d2stats
|
||||||
|
|
||||||
|
// StatList is useful for reducing stats.
|
||||||
|
// They provide a context for stats to alter other stats or infer values
|
||||||
|
// during stat assignment/calculation
|
||||||
|
type StatList interface {
|
||||||
|
Index(idx int) Stat
|
||||||
|
Stats() []Stat
|
||||||
|
SetStats([]Stat) StatList
|
||||||
|
Clone() StatList
|
||||||
|
ReduceStats() StatList
|
||||||
|
RemoveStatAtIndex(idx int) Stat
|
||||||
|
AppendStatList(other StatList) StatList
|
||||||
|
Pop() Stat
|
||||||
|
Push(Stat) StatList
|
||||||
|
}
|
27
d2core/d2stats/stat_value.go
Normal file
27
d2core/d2stats/stat_value.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package d2stats
|
||||||
|
|
||||||
|
// StatValueType is a value type for a stat value
|
||||||
|
type StatValueType int
|
||||||
|
|
||||||
|
// Stat value types
|
||||||
|
const (
|
||||||
|
StatValueInt StatValueType = iota
|
||||||
|
StatValueFloat
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatValue is something that can have both integer and float
|
||||||
|
// number components, as well as a means of retrieving a string for
|
||||||
|
// its values.
|
||||||
|
type StatValue interface {
|
||||||
|
Type() StatValueType
|
||||||
|
Clone() StatValue
|
||||||
|
|
||||||
|
SetInt(int) StatValue
|
||||||
|
SetFloat(float64) StatValue
|
||||||
|
SetStringer(func(StatValue) string) StatValue
|
||||||
|
|
||||||
|
Int() int
|
||||||
|
Float() float64
|
||||||
|
String() string
|
||||||
|
Stringer() func(StatValue) string
|
||||||
|
}
|
@ -1,82 +0,0 @@
|
|||||||
package d2stats
|
|
||||||
|
|
||||||
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
|
||||||
|
|
||||||
// CreateStatList creates a stat list
|
|
||||||
func CreateStatList(stats ...*Stat) *StatList {
|
|
||||||
return &StatList{stats}
|
|
||||||
}
|
|
||||||
|
|
||||||
// StatList is a list that contains stats
|
|
||||||
type StatList struct {
|
|
||||||
stats []*Stat
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone returns a deep copy of the stat list
|
|
||||||
func (sl *StatList) Clone() *StatList {
|
|
||||||
clone := &StatList{}
|
|
||||||
clone.stats = make([]*Stat, len(sl.stats))
|
|
||||||
|
|
||||||
for idx := range sl.stats {
|
|
||||||
clone.stats[idx] = sl.stats[idx].Clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reduce returns a new stat list, combining like stats
|
|
||||||
func (sl *StatList) Reduce() *StatList {
|
|
||||||
clone := sl.Clone()
|
|
||||||
reduction := make([]*Stat, 0)
|
|
||||||
|
|
||||||
// for quick lookups
|
|
||||||
lookup := make(map[*d2datadict.ItemStatCostRecord]int)
|
|
||||||
|
|
||||||
for len(clone.stats) > 0 {
|
|
||||||
applied := false
|
|
||||||
stat := clone.removeStat(0)
|
|
||||||
|
|
||||||
// use lookup, may have found it already
|
|
||||||
if idx, found := lookup[stat.Record]; found {
|
|
||||||
if success := reduction[idx].combine(stat); success {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
reduction = append(reduction, stat)
|
|
||||||
}
|
|
||||||
|
|
||||||
for idx := range reduction {
|
|
||||||
if reduction[idx].combine(stat) {
|
|
||||||
lookup[stat.Record] = idx
|
|
||||||
applied = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !applied {
|
|
||||||
reduction = append(reduction, stat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
clone.stats = reduction
|
|
||||||
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sl *StatList) removeStat(idx int) *Stat {
|
|
||||||
picked := sl.stats[idx]
|
|
||||||
sl.stats[idx] = sl.stats[len(sl.stats)-1]
|
|
||||||
sl.stats[len(sl.stats)-1] = nil
|
|
||||||
sl.stats = sl.stats[:len(sl.stats)-1]
|
|
||||||
|
|
||||||
return picked
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append returns a new stat list, combining like stats
|
|
||||||
func (sl *StatList) Append(other *StatList) *StatList {
|
|
||||||
clone := sl.Clone()
|
|
||||||
clone.stats = append(clone.stats, other.stats...)
|
|
||||||
|
|
||||||
return clone
|
|
||||||
}
|
|
@ -1,90 +0,0 @@
|
|||||||
package d2stats
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStatList_Clone(t *testing.T) {
|
|
||||||
record := d2datadict.ItemStatCosts["strength"]
|
|
||||||
strength := CreateStat(record, 10)
|
|
||||||
|
|
||||||
list1 := CreateStatList(strength)
|
|
||||||
list2 := list1.Clone()
|
|
||||||
|
|
||||||
if list1.stats[0].Description() != list2.stats[0].Description() {
|
|
||||||
t.Errorf("Stats of cloned stat list should be identitcal")
|
|
||||||
}
|
|
||||||
|
|
||||||
list2.stats[0].Values[0] = 0
|
|
||||||
if list1.stats[0].Description() == list2.stats[0].Description() {
|
|
||||||
t.Errorf("Stats of cloned stat list should be different")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatList_Reduce(t *testing.T) {
|
|
||||||
records := []*d2datadict.ItemStatCostRecord{
|
|
||||||
d2datadict.ItemStatCosts["strength"],
|
|
||||||
d2datadict.ItemStatCosts["energy"],
|
|
||||||
d2datadict.ItemStatCosts["dexterity"],
|
|
||||||
d2datadict.ItemStatCosts["vitality"],
|
|
||||||
}
|
|
||||||
|
|
||||||
stats := []*Stat{
|
|
||||||
CreateStat(records[0], 1),
|
|
||||||
CreateStat(records[0], 1),
|
|
||||||
CreateStat(records[0], 1),
|
|
||||||
CreateStat(records[0], 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
list := CreateStatList(stats...)
|
|
||||||
reduction := list.Reduce()
|
|
||||||
|
|
||||||
if len(reduction.stats) != 1 || reduction.stats[0].Description() != "+4 to Strength" {
|
|
||||||
t.Errorf("Stat reduction failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
stats = []*Stat{
|
|
||||||
CreateStat(records[0], 1),
|
|
||||||
CreateStat(records[1], 1),
|
|
||||||
CreateStat(records[2], 1),
|
|
||||||
CreateStat(records[3], 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
list = CreateStatList(stats...)
|
|
||||||
reduction = list.Reduce()
|
|
||||||
|
|
||||||
if len(reduction.stats) != 4 {
|
|
||||||
t.Errorf("Stat reduction failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStatList_Append(t *testing.T) {
|
|
||||||
records := []*d2datadict.ItemStatCostRecord{
|
|
||||||
d2datadict.ItemStatCosts["strength"],
|
|
||||||
d2datadict.ItemStatCosts["energy"],
|
|
||||||
d2datadict.ItemStatCosts["dexterity"],
|
|
||||||
d2datadict.ItemStatCosts["vitality"],
|
|
||||||
}
|
|
||||||
|
|
||||||
list1 := &StatList{
|
|
||||||
[]*Stat{
|
|
||||||
CreateStat(records[0], 1),
|
|
||||||
CreateStat(records[1], 1),
|
|
||||||
CreateStat(records[2], 1),
|
|
||||||
CreateStat(records[3], 1),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
list2 := list1.Clone()
|
|
||||||
|
|
||||||
list3 := list1.Append(list2)
|
|
||||||
|
|
||||||
if len(list3.stats) != 8 {
|
|
||||||
t.Errorf("Stat append failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(list3.Reduce().stats) != 4 {
|
|
||||||
t.Errorf("Stat append failed")
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user