1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-11-10 14:26:15 -05:00

Stats refactor (#617)

* 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

* d2stats + diablo2stats refactored again

- simplified `NewStat` provider function
- added initializer for stat values that sets the stringer functions, value types, and combination types for values when created
- removed redundant description functions
- added stat value combination types `sum` and `static`

`static` stat values which are not altered when stats are combined. this makes sense for stats like proc-on-hit or +skills to class

example:
	Stat A: `10% reanimate as: skeleton mage`
	Stat B: `8% reanimate as: skeleton archer`
	Stat C: `6% reanimate as: skeleton archer`

	A and B can not be combined
	B and C can be combined to `14% reanimate as: skeleton archer`
This commit is contained in:
lord 2020-07-23 19:12:48 -07:00 committed by GitHub
parent c114ab9eb7
commit 9e61079e93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 365 additions and 259 deletions

View File

@ -6,16 +6,19 @@ import (
) )
// NewStat creates a stat instance with the given record and values // NewStat creates a stat instance with the given record and values
func NewStat(record *d2datadict.ItemStatCostRecord, values ...d2stats.StatValue) d2stats.Stat { func NewStat(key string, values ...float64) d2stats.Stat {
record := d2datadict.ItemStatCosts[key]
if record == nil { if record == nil {
return nil return nil
} }
stat := &Diablo2Stat{ stat := &diablo2Stat{
record: record, record: record,
values: values,
} }
stat.init(values...) // init stat values, value types, and value combination rules
return stat return stat
} }
@ -24,22 +27,21 @@ func NewStatList(stats ...d2stats.Stat) d2stats.StatList {
return &Diablo2StatList{stats} return &Diablo2StatList{stats}
} }
// NewStatValue creates a stat value of the given type // NewValue creates a stat value of the given type
func NewStatValue(t d2stats.StatValueType) d2stats.StatValue { func NewValue(t d2stats.StatNumberType, c d2stats.ValueCombineType) d2stats.StatValue {
sv := &Diablo2StatValue{_type: t} sv := &Diablo2StatValue{
numberType: t,
combineType: c,
}
switch t { switch t {
case d2stats.StatValueFloat: case d2stats.StatValueFloat:
sv._stringer = stringerUnsignedFloat sv.stringerFn = stringerUnsignedFloat
case d2stats.StatValueInt: case d2stats.StatValueInt:
sv._stringer = stringerUnsignedInt sv.stringerFn = stringerUnsignedInt
default: default:
sv._stringer = stringerEmpty sv.stringerFn = stringerEmpty
} }
return sv return sv
} }
func intVal(i int) d2stats.StatValue {
return NewStatValue(d2stats.StatValueInt).SetInt(i)
}

View File

@ -2,7 +2,6 @@ package diablo2stats
import ( import (
"fmt" "fmt"
"reflect"
"strings" "strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common"
@ -10,8 +9,8 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
) )
// static check that Diablo2Stat implements Stat // static check that diablo2Stat implements Stat
var _ d2stats.Stat = &Diablo2Stat{} var _ d2stats.Stat = &diablo2Stat{}
type descValPosition int type descValPosition int
@ -23,6 +22,10 @@ const (
const ( const (
maxSkillTabIndex = 2 maxSkillTabIndex = 2
oneValue = 1
twoValue = 2
threeValue = 3
fourValue = 4
) )
const ( const (
@ -31,47 +34,188 @@ const (
fourComponentStr = "%s %s %s %s" fourComponentStr = "%s %s %s %s"
) )
// Diablo2Stat is an instance of a Diablo2Stat, with a set of values const (
type Diablo2Stat struct { intVal = d2stats.StatValueInt
sum = d2stats.StatValueCombineSum
static = d2stats.StatValueCombineStatic
)
// diablo2Stat is an implementation of an OpenDiablo2 Stat, with a set of values.
// It is pretty tightly coupled to the data files for d2
type diablo2Stat struct {
record *d2datadict.ItemStatCostRecord record *d2datadict.ItemStatCostRecord
values []d2stats.StatValue values []d2stats.StatValue
} }
// depending on the stat record, sets up the proper number of values,
// as well as set up the stat value number types, value combination types, and
// the value stringer functions used
func (s *diablo2Stat) init(numbers ...float64) {
if s.record == nil {
return
}
//nolint:gomdn introducing a const for these would be worse
switch s.record.DescFnID {
case 1:
// +31 to Strength
// Replenish Life +20 || Drain Life -8
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
case 2:
// +16% Increased Chance of Blocking
// Lightning Absorb +10%
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
case 3:
// Damage Reduced by 25
// Slain Monsters Rest in Peace
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum)
case 4:
// Poison Resist +25%
// +25% Faster Run/Walk
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
case 5:
// Hit Causes Monster to Flee 25%
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum)
s.values[0].SetStringer(stringerIntPercentageUnsigned)
case 6:
// +25 to Life (Based on Character Level)
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
case 7:
// Lightning Resist +25% (Based on Character Level)
// +25% Better Chance of Getting Magic Items (Based on Character Level)
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
case 8:
// +25% Enhanced Defense (Based on Character Level)
// Heal Stamina Plus +25% (Based on Character Level)
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
case 9:
// Attacker Takes Damage of 25 (Based on Character Level)
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum)
case 11:
// Repairs 2 durability per second
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum)
case 12:
// Hit Blinds Target +5
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
case 13:
// +5 to Paladin Skill Levels
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[1] = NewValue(intVal, sum).SetStringer(stringerClassAllSkills)
case 14:
// +5 to Combat Skills (Paladin Only)
s.values = make([]d2stats.StatValue, threeValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[1] = NewValue(intVal, sum).SetStringer(stringerClassOnly)
s.values[2] = NewValue(intVal, static)
case 15:
// 5% Chance to cast level 7 Frozen Orb on attack
s.values = make([]d2stats.StatValue, threeValue)
s.values[0] = NewValue(intVal, sum)
s.values[1] = NewValue(intVal, static)
s.values[2] = NewValue(intVal, static).SetStringer(stringerSkillName)
case 16:
// Level 3 Warmth Aura When Equipped
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum)
s.values[1] = NewValue(intVal, static).SetStringer(stringerSkillName)
case 20:
// -25% Target Defense
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
case 22:
// 25% to Attack Rating versus Specter
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageUnsigned)
s.values[1] = NewValue(intVal, static).SetStringer(stringerMonsterName)
case 23:
// 25% Reanimate as: Specter
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageUnsigned)
s.values[1] = NewValue(intVal, static).SetStringer(stringerMonsterName)
case 24:
// Level 25 Frozen Orb (19/20 Charges)
s.values = make([]d2stats.StatValue, fourValue)
s.values[0] = NewValue(intVal, static)
s.values[1] = NewValue(intVal, static).SetStringer(stringerSkillName)
s.values[2] = NewValue(intVal, static)
s.values[3] = NewValue(intVal, static)
case 27:
// +25 to Frozen Orb (Paladin Only)
s.values = make([]d2stats.StatValue, threeValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[1] = NewValue(intVal, static).SetStringer(stringerSkillName)
s.values[2] = NewValue(intVal, static).SetStringer(stringerClassOnly)
case 28:
// +25 to Frozen Orb
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[1] = NewValue(intVal, static).SetStringer(stringerSkillName)
default:
return
}
for idx := range numbers {
if idx > len(s.values) {
break
}
s.values[idx].SetFloat(numbers[idx])
}
}
// Name returns the name of the stat (the key in itemstatcosts) // Name returns the name of the stat (the key in itemstatcosts)
func (s *Diablo2Stat) Name() string { func (s *diablo2Stat) Name() string {
return s.record.Name return s.record.Name
} }
// Priority returns the description printing priority // Priority returns the description printing priority
func (s *Diablo2Stat) Priority() int { func (s *diablo2Stat) Priority() int {
return s.record.DescPriority return s.record.DescPriority
} }
// Values returns the stat values of the stat // Values returns the stat values of the stat
func (s *Diablo2Stat) Values() []d2stats.StatValue { func (s *diablo2Stat) Values() []d2stats.StatValue {
return s.values return s.values
} }
// SetValues sets the stat values // SetValues sets the stat values
func (s *Diablo2Stat) SetValues(values ...d2stats.StatValue) { func (s *diablo2Stat) SetValues(values ...d2stats.StatValue) {
s.values = make([]d2stats.StatValue, len(values)) s.values = make([]d2stats.StatValue, len(values))
for idx := range values { for idx := range values {
s.values[idx] = values[idx] s.values[idx] = values[idx]
} }
} }
// Clone returns a deep copy of the Diablo2Stat // Clone returns a deep copy of the diablo2Stat
func (s *Diablo2Stat) Clone() d2stats.Stat { func (s *diablo2Stat) Clone() d2stats.Stat {
clone := &Diablo2Stat{ clone := &diablo2Stat{
record: s.record, record: s.record,
values: make([]d2stats.StatValue, len(s.Values())),
} }
clone.init()
for idx := range s.values { for idx := range s.values {
srcVal := s.values[idx] srcVal := s.values[idx]
dstVal := reflect.New(reflect.ValueOf(srcVal).Elem().Type()).Interface().(d2stats.StatValue) dstVal := &Diablo2StatValue{
numberType: srcVal.NumberType(),
combineType: srcVal.CombineType(),
}
switch srcVal.Type() { dstVal.SetStringer(srcVal.Stringer())
switch srcVal.NumberType() {
case d2stats.StatValueInt: case d2stats.StatValueInt:
dstVal.SetInt(srcVal.Int()) dstVal.SetInt(srcVal.Int())
case d2stats.StatValueFloat: case d2stats.StatValueFloat:
@ -85,14 +229,14 @@ func (s *Diablo2Stat) Clone() d2stats.Stat {
} }
// Copy to this stat value the values of the given stat value // Copy to this stat value the values of the given stat value
func (s *Diablo2Stat) Copy(from d2stats.Stat) d2stats.Stat { func (s *diablo2Stat) Copy(from d2stats.Stat) d2stats.Stat {
srcValues := from.Values() srcValues := from.Values()
s.values = make([]d2stats.StatValue, len(srcValues)) s.values = make([]d2stats.StatValue, len(srcValues))
for idx := range srcValues { for idx := range srcValues {
src := srcValues[idx] src := srcValues[idx]
valType := src.Type() valType := src.NumberType()
dst := &Diablo2StatValue{_type: valType} dst := &Diablo2StatValue{numberType: valType}
dst.SetStringer(src.Stringer()) dst.SetStringer(src.Stringer())
switch valType { switch valType {
@ -109,7 +253,7 @@ func (s *Diablo2Stat) Copy(from d2stats.Stat) d2stats.Stat {
} }
// Combine sums the other stat with this one (does not alter this stat, returns altered clone!) // 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) { func (s *diablo2Stat) Combine(other d2stats.Stat) (result d2stats.Stat, err error) {
cantBeCombinedErr := fmt.Errorf("cannot combine %s with %s", s.Name(), other.Name()) cantBeCombinedErr := fmt.Errorf("cannot combine %s with %s", s.Name(), other.Name())
if !s.canBeCombinedWith(other) { if !s.canBeCombinedWith(other) {
@ -121,20 +265,31 @@ func (s *Diablo2Stat) Combine(other d2stats.Stat) (result d2stats.Stat, err erro
for idx := range result.Values() { for idx := range result.Values() {
v1, v2 := dstValues[idx], srcValues[idx] v1, v2 := dstValues[idx], srcValues[idx]
combinationRule := v1.CombineType()
valType := v1.Type() if combinationRule == d2stats.StatValueCombineStatic {
switch valType { // we do not add the values, they remain the same
case d2stats.StatValueInt: // for things like monster/class/skill index or on proc stats
v1.SetInt(v1.Int() + v2.Int()) // where the level of a skill isn't summed, but the
case d2stats.StatValueFloat: // chance to cast values are
v1.SetFloat(v1.Float() + v2.Float()) continue
}
if combinationRule == d2stats.StatValueCombineSum {
valType := v1.NumberType()
switch valType {
case d2stats.StatValueInt:
v1.SetInt(v1.Int() + v2.Int())
case d2stats.StatValueFloat:
v1.SetFloat(v1.Float() + v2.Float())
}
} }
} }
return result, nil return result, nil
} }
func (s *Diablo2Stat) canBeCombinedWith(other d2stats.Stat) bool { func (s *diablo2Stat) canBeCombinedWith(other d2stats.Stat) bool {
if s.Name() != other.Name() { if s.Name() != other.Name() {
return false return false
} }
@ -145,86 +300,65 @@ func (s *Diablo2Stat) canBeCombinedWith(other d2stats.Stat) bool {
} }
for idx := range values1 { for idx := range values1 {
if values1[idx].Type() != values2[idx].Type() { if values1[idx].NumberType() != values2[idx].NumberType() {
return false return false
} }
if values1[idx].CombineType() != values2[idx].CombineType() {
return false
}
// in the case that we are trying to combine stats like:
// +1 to Paladin Skills
// +1 to Sorceress Skills
// the numeric value (an index) that denotes the class skill type knows not to be summed
// with the other index, even though the format of the stat and stat value is pretty much
// the same.
if values1[idx].CombineType() == d2stats.StatValueCombineStatic {
if values1[idx].Float() != values2[idx].Float() {
return false
}
}
} }
return true return true
} }
// String returns the formatted description string // String returns the formatted description string
func (s *Diablo2Stat) String() string { //nolint:gocyclo switch statement is not so bad func (s *diablo2Stat) String() string { //nolint:gocyclo switch statement is not so bad
var result string var result string
for idx := range s.values {
if s.values[idx].Stringer() == nil {
s.values[idx].SetStringer(stringerUnsignedInt)
}
}
//nolint:gomdn introducing a const for these would be worse //nolint:gomdn introducing a const for these would be worse
switch s.record.DescFnID { switch s.record.DescFnID {
case 1: case 1, 2, 3, 4, 5, 12, 20:
s.values[0].SetStringer(stringerIntSigned)
result = s.descFn1() result = s.descFn1()
case 2: case 6, 7, 8:
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() 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: case 9:
result = s.descFn9() result = s.descFn9()
case 11: case 11:
result = s.descFn11() result = s.descFn11()
case 12:
s.values[0].SetStringer(stringerIntSigned)
result = s.descFn12()
case 13: case 13:
s.values[0].SetStringer(stringerIntSigned)
s.values[1].SetStringer(stringerClassAllSkills)
result = s.descFn13() result = s.descFn13()
case 14: case 14:
s.values[0].SetStringer(stringerIntSigned)
s.values[1].SetStringer(stringerClassOnly)
result = s.descFn14() result = s.descFn14()
case 15: case 15:
s.values[2].SetStringer(stringerSkillName)
result = s.descFn15() result = s.descFn15()
case 16: case 16:
s.values[1].SetStringer(stringerSkillName)
result = s.descFn16() result = s.descFn16()
case 20: case 22, 23:
s.values[0].SetStringer(stringerIntPercentageSigned)
result = s.descFn20()
case 22:
s.values[0].SetStringer(stringerIntPercentageUnsigned)
s.values[1].SetStringer(stringerMonsterName)
result = s.descFn22() result = s.descFn22()
case 23:
s.values[0].SetStringer(stringerIntPercentageUnsigned)
s.values[1].SetStringer(stringerMonsterName)
result = s.descFn23()
case 24: case 24:
s.values[1].SetStringer(stringerSkillName)
result = s.descFn24() result = s.descFn24()
case 27: case 27:
s.values[0].SetStringer(stringerIntSigned)
s.values[1].SetStringer(stringerSkillName)
s.values[2].SetStringer(stringerClassOnly)
result = s.descFn27() result = s.descFn27()
case 28: case 28:
s.values[0].SetStringer(stringerIntSigned)
s.values[1].SetStringer(stringerSkillName)
result = s.descFn28() result = s.descFn28()
default: default:
result = "" result = ""
@ -233,9 +367,7 @@ func (s *Diablo2Stat) String() string { //nolint:gocyclo switch statement is not
return result return result
} }
// +31 to Strength func (s *diablo2Stat) descFn1() string {
// Replenish Life +20 || Drain Life -8
func (s *Diablo2Stat) descFn1() string {
var stringTableKey, result string var stringTableKey, result string
value := s.values[0] value := s.values[0]
@ -264,35 +396,7 @@ func (s *Diablo2Stat) descFn1() string {
return result return result
} }
// +16% Increased Chance of Blocking func (s *diablo2Stat) descFn6() string {
// 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 var stringTableKey, result string
value := s.values[0] value := s.values[0]
@ -323,22 +427,7 @@ func (s *Diablo2Stat) descFn6() string {
return result return result
} }
// Lightning Resist +25% (Based on Character Level) func (s *diablo2Stat) descFn9() string {
// +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 var stringTableKey, result string
value := s.values[0] value := s.values[0]
@ -368,8 +457,7 @@ func (s *Diablo2Stat) descFn9() string {
return result return result
} }
// Repairs 2 durability per second func (s *diablo2Stat) descFn11() string {
func (s *Diablo2Stat) descFn11() string {
var stringTableKey string var stringTableKey string
value := s.values[0] value := s.values[0]
@ -387,14 +475,7 @@ func (s *Diablo2Stat) descFn11() string {
return fmt.Sprintf(formatString, value) return fmt.Sprintf(formatString, value)
} }
// Hit Blinds Target +5 func (s *diablo2Stat) descFn13() string {
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] value := s.values[0]
allSkills := s.values[1] allSkills := s.values[1]
@ -412,8 +493,7 @@ func (s *Diablo2Stat) descFn13() string {
} }
} }
// +5 to Combat Skills (Paladin Only) func (s *diablo2Stat) descFn14() string {
func (s *Diablo2Stat) descFn14() string {
// strings come out like `+5 to Combat Skills (Paladin Only)` // strings come out like `+5 to Combat Skills (Paladin Only)`
numSkills, hero, skillTab := s.values[0], s.values[1], s.values[2] numSkills, hero, skillTab := s.values[0], s.values[1], s.values[2]
heroMap := getHeroMap() heroMap := getHeroMap()
@ -440,8 +520,7 @@ func (s *Diablo2Stat) descFn14() string {
return fmt.Sprintf(threeComponentStr, numSkillsStr, skillTabStr, classOnlyStr) return fmt.Sprintf(threeComponentStr, numSkillsStr, skillTabStr, classOnlyStr)
} }
// 5% Chance to cast level 7 Frozen Orb on attack func (s *diablo2Stat) descFn15() string {
func (s *Diablo2Stat) descFn15() string {
chance, lvl, skill := s.values[0], s.values[1], s.values[2] chance, lvl, skill := s.values[0], s.values[1], s.values[2]
// Special case, `chance to cast` format is actually in the string table! // Special case, `chance to cast` format is actually in the string table!
@ -450,8 +529,7 @@ func (s *Diablo2Stat) descFn15() string {
return fmt.Sprintf(chanceToCastStr, chance.Int(), lvl.Int(), skill) return fmt.Sprintf(chanceToCastStr, chance.Int(), lvl.Int(), skill)
} }
// Level 3 Warmth Aura When Equipped func (s *diablo2Stat) descFn16() string {
func (s *Diablo2Stat) descFn16() string {
skillLevel, skillIndex := s.values[0], s.values[1] skillLevel, skillIndex := s.values[0], s.values[1]
// Special case, `Level # XYZ Aura When Equipped`, format is actually in the string table! // Special case, `Level # XYZ Aura When Equipped`, format is actually in the string table!
@ -460,28 +538,14 @@ func (s *Diablo2Stat) descFn16() string {
return fmt.Sprintf(format, skillLevel.Int(), skillIndex) return fmt.Sprintf(format, skillLevel.Int(), skillIndex)
} }
// -25% Target Defense func (s *diablo2Stat) descFn22() string {
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] arBonus, monsterIndex := s.values[0], s.values[1]
arVersus := d2common.TranslateString(s.record.DescStrPos) arVersus := d2common.TranslateString(s.record.DescStrPos)
return fmt.Sprintf(threeComponentStr, arBonus, arVersus, monsterIndex) return fmt.Sprintf(threeComponentStr, arBonus, arVersus, monsterIndex)
} }
// 25% Reanimate as: Specter func (s *diablo2Stat) descFn24() string {
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 // Special case formatting
format := "Level " + threeComponentStr format := "Level " + threeComponentStr
@ -496,22 +560,20 @@ func (s *Diablo2Stat) descFn24() string {
return fmt.Sprintf(format, lvl, skill, chargeStr) return fmt.Sprintf(format, lvl, skill, chargeStr)
} }
// +25 to Frozen Orb (Paladin Only) func (s *diablo2Stat) descFn27() string {
func (s *Diablo2Stat) descFn27() string {
amount, skill, hero := s.values[0], s.values[1], s.values[2] amount, skill, hero := s.values[0], s.values[1], s.values[2]
return fmt.Sprintf(fourComponentStr, amount, "to", skill, hero) return fmt.Sprintf(fourComponentStr, amount, "to", skill, hero)
} }
// +25 to Frozen Orb func (s *diablo2Stat) descFn28() string {
func (s *Diablo2Stat) descFn28() string {
amount, skill := s.values[0], s.values[1] amount, skill := s.values[0], s.values[1]
return fmt.Sprintf(threeComponentStr, amount, "to", skill) return fmt.Sprintf(threeComponentStr, amount, "to", skill)
} }
// DescGroupString return a string based on the DescGroupFuncID // DescGroupString return a string based on the DescGroupFuncID
func (s *Diablo2Stat) DescGroupString(a ...interface{}) string { func (s *diablo2Stat) DescGroupString(a ...interface{}) string {
if s.record.DescGroupFuncID < 0 { if s.record.DescGroupFuncID < 0 {
return "" return ""
} }

View File

@ -2,7 +2,6 @@ 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"
@ -261,8 +260,7 @@ func TestStat_InitMockData(t *testing.T) {
} }
func TestStat_Clone(t *testing.T) { func TestStat_Clone(t *testing.T) {
r := d2datadict.ItemStatCosts["strength"] s1 := NewStat("strength", 5)
s1 := NewStat(r, intVal(5))
s2 := s1.Clone() s2 := s1.Clone()
// make sure the stats are distinct // make sure the stats are distinct
@ -288,89 +286,94 @@ func TestStat_Clone(t *testing.T) {
func TestStat_Descriptions(t *testing.T) { func TestStat_Descriptions(t *testing.T) {
tests := []struct { tests := []struct {
recordKey string recordKey string
vals []d2stats.StatValue vals []float64
expect string expect string
}{ }{
// DescFn1 // DescFn1
{"strength", []d2stats.StatValue{intVal(31)}, "+31 to Strength"}, {"strength", []float64{31}, "+31 to Strength"},
{"hpregen", []d2stats.StatValue{intVal(20)}, "Replenish Life +20"}, {"hpregen", []float64{20}, "Replenish Life +20"},
{"hpregen", []d2stats.StatValue{intVal(-8)}, "Drain Life -8"}, {"hpregen", []float64{-8}, "Drain Life -8"},
// DescFn2 // DescFn2
{"toblock", []d2stats.StatValue{intVal(16)}, "+16% Increased Chance of Blocking"}, {"toblock", []float64{16}, "+16% Increased Chance of Blocking"},
{"item_absorblight_percent", []d2stats.StatValue{intVal(10)}, "Lightning Absorb +10%"}, {"item_absorblight_percent", []float64{10}, "Lightning Absorb +10%"},
// DescFn3 // DescFn3
{"normal_damage_reduction", []d2stats.StatValue{intVal(25)}, "Damage Reduced by 25"}, {"normal_damage_reduction", []float64{25}, "Damage Reduced by 25"},
{"item_restinpeace", []d2stats.StatValue{intVal(25)}, "Slain Monsters Rest in Peace"}, {"item_restinpeace", []float64{25}, "Slain Monsters Rest in Peace"},
// DescFn4 // DescFn4
{"poisonresist", []d2stats.StatValue{intVal(25)}, "Poison Resist +25%"}, {"poisonresist", []float64{25}, "Poison Resist +25%"},
{"item_fastermovevelocity", []d2stats.StatValue{intVal(25)}, "+25% Faster Run/Walk"}, {"item_fastermovevelocity", []float64{25}, "+25% Faster Run/Walk"},
// DescFn5 // DescFn5
{"item_howl", []d2stats.StatValue{intVal(25)}, "Hit Causes Monster to Flee 25%"}, {"item_howl", []float64{25}, "Hit Causes Monster to Flee 25%"},
// DescFn6 // DescFn6
{"item_hp_perlevel", []d2stats.StatValue{intVal(25)}, "+25 to Life (Based on Character Level)"}, {"item_hp_perlevel", []float64{25}, "+25 to Life (Based on Character Level)"},
// DescFn7 // DescFn7
{"item_resist_ltng_perlevel", []d2stats.StatValue{intVal(25)}, "Lightning Resist +25% (Based on Character Level)"}, {"item_resist_ltng_perlevel", []float64{25},
{"item_find_magic_perlevel", []d2stats.StatValue{intVal(25)}, "+25% Better Chance of Getting Magic Items (" + "Lightning Resist +25% (Based on Character Level)"},
{"item_find_magic_perlevel", []float64{25}, "+25% Better Chance of Getting Magic Items (" +
"Based on Character Level)"}, "Based on Character Level)"},
// DescFn8 // DescFn8
{"item_armorpercent_perlevel", []d2stats.StatValue{intVal(25)}, "+25% Enhanced Defense (Based on Character Level)"}, {"item_armorpercent_perlevel", []float64{25},
{"item_regenstamina_perlevel", []d2stats.StatValue{intVal(25)}, "+25% Enhanced Defense (Based on Character Level)"},
{"item_regenstamina_perlevel", []float64{25},
"Heal Stamina Plus +25% (Based on Character Level)"}, "Heal Stamina Plus +25% (Based on Character Level)"},
// DescFn9 // DescFn9
{"item_thorns_perlevel", []d2stats.StatValue{intVal(25)}, "Attacker Takes Damage of 25 (Based on Character Level)"}, {"item_thorns_perlevel", []float64{25}, "Attacker Takes Damage of 25 (" +
"Based on Character Level)"},
// DescFn11 // DescFn11
{"item_replenish_durability", []d2stats.StatValue{intVal(2)}, "Repairs 2 durability per second"}, {"item_replenish_durability", []float64{2}, "Repairs 2 durability per second"},
// DescFn12 // DescFn12
{"item_stupidity", []d2stats.StatValue{intVal(5)}, "Hit Blinds Target +5"}, {"item_stupidity", []float64{5}, "Hit Blinds Target +5"},
// DescFn13 // DescFn13
{"item_addclassskills", []d2stats.StatValue{intVal(5), intVal(3)}, "+5 to Paladin Skill Levels"}, {"item_addclassskills", []float64{5, 3}, "+5 to Paladin Skill Levels"},
// DescFn14 // DescFn14
{"item_addskill_tab", []d2stats.StatValue{intVal(5), intVal(3), intVal(0)}, "+5 to Combat Skills (Paladin Only)"}, {"item_addskill_tab", []float64{5, 3, 0}, "+5 to Combat Skills (Paladin Only)"},
{"item_addskill_tab", []d2stats.StatValue{intVal(5), intVal(3), intVal(1)}, "+5 to Offensive Auras (Paladin Only)"}, {"item_addskill_tab", []float64{5, 3, 1}, "+5 to Offensive Auras (Paladin Only)"},
{"item_addskill_tab", []d2stats.StatValue{intVal(5), intVal(3), intVal(2)}, "+5 to Defensive Auras (Paladin Only)"}, {"item_addskill_tab", []float64{5, 3, 2}, "+5 to Defensive Auras (Paladin Only)"},
// DescFn15 // DescFn15
{"item_skillonattack", []d2stats.StatValue{intVal(5), intVal(7), intVal(64)}, "5% Chance to cast level 7 Frozen Orb on attack"}, {"item_skillonattack", []float64{5, 7, 64},
"5% Chance to cast level 7 Frozen Orb on attack"},
// DescFn16 // DescFn16
{"item_aura", []d2stats.StatValue{intVal(3), intVal(37)}, "Level 3 Warmth Aura When Equipped"}, {"item_aura", []float64{3, 37}, "Level 3 Warmth Aura When Equipped"},
// DescFn20 // DescFn20
{"item_fractionaltargetac", []d2stats.StatValue{intVal(-25)}, "-25% Target Defense"}, {"item_fractionaltargetac", []float64{-25}, "-25% Target Defense"},
// DescFn22 // DescFn22
{"attack_vs_montype", []d2stats.StatValue{intVal(25), intVal(40)}, "25% to Attack Rating versus Specter"}, {"attack_vs_montype", []float64{25, 40}, "25% to Attack Rating versus Specter"},
// DescFn23 // DescFn23
{"item_reanimate", []d2stats.StatValue{intVal(25), intVal(40)}, "25% Reanimate as: Specter"}, {"item_reanimate", []float64{25, 40}, "25% Reanimate as: Specter"},
// DescFn24 // DescFn24
{"item_charged_skill", []d2stats.StatValue{intVal(25), intVal(64), intVal(20), intVal(19)}, "Level 25 Frozen Orb (19/20 Charges)"}, {"item_charged_skill", []float64{25, 64, 20, 19}, "Level 25 Frozen Orb (19/20 Charges)"},
// DescFn27 // DescFn27
{"item_singleskill", []d2stats.StatValue{intVal(25), intVal(64), intVal(3)}, "+25 to Frozen Orb (Paladin Only)"}, {"item_singleskill", []float64{25, 64, 3}, "+25 to Frozen Orb (Paladin Only)"},
// DescFn28 // DescFn28
{"item_nonclassskill", []d2stats.StatValue{intVal(25), intVal(64)}, "+25 to Frozen Orb"}, {"item_nonclassskill", []float64{25, 64}, "+25 to Frozen Orb"},
} }
for idx := range tests { for idx := range tests {
test := tests[idx] test := tests[idx]
record := d2datadict.ItemStatCosts[test.recordKey] key := test.recordKey
record := d2datadict.ItemStatCosts[key]
expect := test.expect expect := test.expect
stat := NewStat(record, test.vals...) stat := NewStat(key, test.vals...)
if got := stat.String(); got != expect { if got := stat.String(); got != expect {
t.Errorf(errFmt, errStr, record.DescFnID, test.recordKey, test.vals, expect, got) t.Errorf(errFmt, errStr, record.DescFnID, test.recordKey, test.vals, expect, got)
@ -381,3 +384,21 @@ func TestStat_Descriptions(t *testing.T) {
} }
} }
} }
func TestDiablo2Stat_Combine(t *testing.T) {
a := NewStat("item_nonclassskill", 25, 64) // "+25 to Frozen Orb"
b := NewStat("item_nonclassskill", 5, 64) // "+5 to Frozen Orb"
c, err := a.Combine(b)
if err != nil || c.String() != "+30 to Frozen Orb" {
t.Errorf("stats combination failed\r%s", err)
}
d := NewStat("item_nonclassskill", 5, 37) // "+5 to Warmth"
_, err = c.Combine(d)
if err == nil {
t.Error("stats were combined when they should not have been.")
}
}

View File

@ -9,28 +9,34 @@ var _ d2stats.StatValue = &Diablo2StatValue{}
// Diablo2StatValue is a diablo 2 implementation of a stat value // Diablo2StatValue is a diablo 2 implementation of a stat value
type Diablo2StatValue struct { type Diablo2StatValue struct {
number float64 number float64
_stringer func(d2stats.StatValue) string stringerFn func(d2stats.StatValue) string
_type d2stats.StatValueType numberType d2stats.StatNumberType
combineType d2stats.ValueCombineType
} }
// Type returns the stat value type // NumberType returns the stat value type
func (sv *Diablo2StatValue) Type() d2stats.StatValueType { func (sv *Diablo2StatValue) NumberType() d2stats.StatNumberType {
return sv._type return sv.numberType
}
// CombineType returns the stat value combination type
func (sv *Diablo2StatValue) CombineType() d2stats.ValueCombineType {
return sv.combineType
} }
// Clone returns a deep copy of the stat value // Clone returns a deep copy of the stat value
func (sv Diablo2StatValue) Clone() d2stats.StatValue { func (sv Diablo2StatValue) Clone() d2stats.StatValue {
clone := &Diablo2StatValue{} clone := &Diablo2StatValue{}
switch sv._type { switch sv.numberType {
case d2stats.StatValueInt: case d2stats.StatValueInt:
clone.SetInt(sv.Int()) clone.SetInt(sv.Int())
case d2stats.StatValueFloat: case d2stats.StatValueFloat:
clone.SetFloat(sv.Float()) clone.SetFloat(sv.Float())
} }
clone._stringer = sv._stringer clone.stringerFn = sv.stringerFn
return clone return clone
} }
@ -42,7 +48,7 @@ func (sv *Diablo2StatValue) Int() int {
// String returns a string version of the value // String returns a string version of the value
func (sv *Diablo2StatValue) String() string { func (sv *Diablo2StatValue) String() string {
return sv._stringer(sv) return sv.stringerFn(sv)
} }
// Float returns a float64 version of the value // Float returns a float64 version of the value
@ -66,11 +72,11 @@ func (sv *Diablo2StatValue) SetFloat(f float64) d2stats.StatValue {
// Stringer returns the string evaluation function // Stringer returns the string evaluation function
func (sv *Diablo2StatValue) Stringer() func(d2stats.StatValue) string { func (sv *Diablo2StatValue) Stringer() func(d2stats.StatValue) string {
return sv._stringer return sv.stringerFn
} }
// SetStringer sets the string evaluation function // SetStringer sets the string evaluation function
func (sv *Diablo2StatValue) SetStringer(f func(d2stats.StatValue) string) d2stats.StatValue { func (sv *Diablo2StatValue) SetStringer(f func(d2stats.StatValue) string) d2stats.StatValue {
sv._stringer = f sv.stringerFn = f
return sv return sv
} }

View File

@ -4,7 +4,7 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
) )
// static check that Diablo2Stat implements Stat // static check that diablo2Stat implements Stat
var _ d2stats.StatList = &Diablo2StatList{} var _ d2stats.StatList = &Diablo2StatList{}
// Diablo2StatList is a diablo 2 implementation of a stat list // Diablo2StatList is a diablo 2 implementation of a stat list

View File

@ -3,13 +3,11 @@ package diablo2stats
import ( import (
"testing" "testing"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
) )
func TestDiablo2StatList_Index(t *testing.T) { func TestDiablo2StatList_Index(t *testing.T) {
record := d2datadict.ItemStatCosts["strength"] strength := NewStat("strength", 10)
strength := NewStat(record, intVal(10))
list1 := &Diablo2StatList{stats: []d2stats.Stat{strength}} list1 := &Diablo2StatList{stats: []d2stats.Stat{strength}}
if list1.Index(0) != strength { if list1.Index(0) != strength {
@ -18,8 +16,7 @@ func TestDiablo2StatList_Index(t *testing.T) {
} }
func TestStatList_Clone(t *testing.T) { func TestStatList_Clone(t *testing.T) {
record := d2datadict.ItemStatCosts["strength"] strength := NewStat("strength", 10)
strength := NewStat(record, intVal(10))
list1 := &Diablo2StatList{} list1 := &Diablo2StatList{}
list1.Push(strength) list1.Push(strength)
@ -40,56 +37,42 @@ func TestStatList_Clone(t *testing.T) {
} }
func TestStatList_Reduce(t *testing.T) { func TestStatList_Reduce(t *testing.T) {
records := []*d2datadict.ItemStatCostRecord{
d2datadict.ItemStatCosts["strength"],
d2datadict.ItemStatCosts["energy"],
d2datadict.ItemStatCosts["dexterity"],
d2datadict.ItemStatCosts["vitality"],
}
stats := []d2stats.Stat{ stats := []d2stats.Stat{
NewStat(records[0], intVal(1)), NewStat("strength", 1),
NewStat(records[0], intVal(1)), NewStat("strength", 1),
NewStat(records[0], intVal(1)), NewStat("strength", 1),
NewStat(records[0], intVal(1)), NewStat("strength", 1),
} }
list := NewStatList(stats...) list := NewStatList(stats...)
reduction := list.ReduceStats() reduction := list.ReduceStats()
if len(reduction.Stats()) != 1 || reduction.Index(0).String() != "+4 to Strength" { if len(reduction.Stats()) != 1 || reduction.Index(0).String() != "+4 to Strength" {
t.Errorf("Diablo2Stat reduction failed") t.Errorf("diablo2Stat reduction failed")
} }
stats = []d2stats.Stat{ stats = []d2stats.Stat{
NewStat(records[0], intVal(1)), NewStat("strength", 1),
NewStat(records[1], intVal(1)), NewStat("energy", 1),
NewStat(records[2], intVal(1)), NewStat("dexterity", 1),
NewStat(records[3], intVal(1)), NewStat("vitality", 1),
} }
list = NewStatList(stats...) list = NewStatList(stats...)
reduction = list.ReduceStats() reduction = list.ReduceStats()
if len(reduction.Stats()) != 4 { if len(reduction.Stats()) != 4 {
t.Errorf("Diablo2Stat reduction failed") t.Errorf("diablo2Stat reduction failed")
} }
} }
func TestStatList_Append(t *testing.T) { func TestStatList_Append(t *testing.T) {
records := []*d2datadict.ItemStatCostRecord{
d2datadict.ItemStatCosts["strength"],
d2datadict.ItemStatCosts["energy"],
d2datadict.ItemStatCosts["dexterity"],
d2datadict.ItemStatCosts["vitality"],
}
list1 := &Diablo2StatList{ list1 := &Diablo2StatList{
[]d2stats.Stat{ []d2stats.Stat{
NewStat(records[0], intVal(1)), NewStat("strength", 1),
NewStat(records[1], intVal(1)), NewStat("energy", 1),
NewStat(records[2], intVal(1)), NewStat("dexterity", 1),
NewStat(records[3], intVal(1)), NewStat("vitality", 1),
}, },
} }
list2 := list1.Clone() list2 := list1.Clone()
@ -97,10 +80,10 @@ func TestStatList_Append(t *testing.T) {
list3 := list1.AppendStatList(list2) list3 := list1.AppendStatList(list2)
if len(list3.Stats()) != 8 { if len(list3.Stats()) != 8 {
t.Errorf("Diablo2Stat append failed") t.Errorf("diablo2Stat append failed")
} }
if len(list3.ReduceStats().Stats()) != 4 { if len(list3.ReduceStats().Stats()) != 4 {
t.Errorf("Diablo2Stat append failed") t.Errorf("diablo2Stat append failed")
} }
} }

View File

@ -1,19 +1,51 @@
package d2stats package d2stats
// StatValueType is a value type for a stat value // StatNumberType is a value type for a stat value
type StatValueType int type StatNumberType int
// Stat value types // Stat value types
const ( const (
StatValueInt StatValueType = iota StatValueInt StatNumberType = iota
StatValueFloat StatValueFloat
) )
// ValueCombineType is a rule for combining stat values
type ValueCombineType int
const (
// StatValueCombineSum means that the values are simply summed
StatValueCombineSum ValueCombineType = iota
// StatValueCombineStatic means that values can be combined only if they
// have the same number value, and that the combination does not alter
// the number value. This is typically for things like static skill level
// monster/skill index for on proc stats where it doesnt make sense to sum
// the values
// example 1:
// if
// Stat_A := `25% chance to cast level 2 Frozen Orb on attack`
// Stat_B := `25% chance to cast level 3 Frozen Orb on attack`
// then
// Stat_A can NOT be combined with Stat_B
// even though the skills are the same, the levels are different
//
// example 2:
// if
// Stat_A := `25% chance to cast level 20 Frost Nova on attack`
// Stat_B := `25% chance to cast level 20 Frost Nova on attack`
// then
// the skills and skill levels are the same, so it can be combined
// (Stat_A + Stat_B) == `50% chance to cast level 20 Frost Nova on attack`
StatValueCombineStatic
)
// StatValue is something that can have both integer and float // StatValue is something that can have both integer and float
// number components, as well as a means of retrieving a string for // number components, as well as a means of retrieving a string for
// its values. // its values.
type StatValue interface { type StatValue interface {
Type() StatValueType NumberType() StatNumberType
CombineType() ValueCombineType
Clone() StatValue Clone() StatValue
SetInt(int) StatValue SetInt(int) StatValue