mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-01-27 03:37:53 -05:00
Stat descriptions + tests, Skilldesc.txt loader (#590)
* adding ranged number type, for use in stats * Loaded Skills.txt * asset manager only binds terminal commands if terminal != nil * WIP stats * cache getter and clear methods were not implemented * asset manager handles a nil terminal pointer * adding skilldesc.txt loader (needs work) * ctc stat descriptions functions working * moving description functionality out of itemstatcost loader and into stats * stats seem like a central part of diablo, moving into d2core. * stats seem like a central part of diablo, moving into d2core. * delint * adding statlist, statlist reduction, unit tests * minor edits to stat.go * lint error in statlist.go * Remove dependency on actual data from mpq files stats unit tests now use mock data * fixing some lint errors, formatting Co-authored-by: Maxime Lavigne (malavv) <duguigne@gmail.com>
This commit is contained in:
parent
54ff33c552
commit
cf6029eb95
@ -241,6 +241,7 @@ func (p *App) loadDataDict() error {
|
||||
{d2resource.Inventory, d2datadict.LoadInventory},
|
||||
{d2resource.Skills, d2datadict.LoadSkills},
|
||||
{d2resource.Properties, d2datadict.LoadProperties},
|
||||
{d2resource.SkillDesc, d2datadict.LoadSkillDescriptions},
|
||||
}
|
||||
|
||||
d2datadict.InitObjectRecords()
|
||||
|
@ -1,11 +1,9 @@
|
||||
package d2datadict
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"log"
|
||||
)
|
||||
|
||||
// ItemStatCostRecord represents a row from itemstatcost.txt
|
||||
@ -101,61 +99,6 @@ type ItemStatCostRecord struct {
|
||||
DamageRelated bool // prevents stacking of stats while dual wielding
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals // better for lookup
|
||||
var itemStatCostRecordLookup = []string{
|
||||
"+%f %s",
|
||||
"%f%% %s",
|
||||
"%f %s",
|
||||
"+%f%% %s",
|
||||
"%f%% %s",
|
||||
"+%f %s %s",
|
||||
"%f%% %s %s",
|
||||
"+%f%% %s %s",
|
||||
"%f %s %s",
|
||||
"%f %s %s",
|
||||
"Repairs 1 Durability In %.0f Seconds",
|
||||
"+%f %s",
|
||||
"+%.0f to %s Skill Levels",
|
||||
"+%.0f to %s Skill Levels (%s Only)",
|
||||
"%.0f%% chance to cast %d %s on %s",
|
||||
"Level %d %s Aura When Equipped",
|
||||
"%f %s (Increases near %d)",
|
||||
"%f%% %s (Increases near %d)",
|
||||
"",
|
||||
"%f%% %s",
|
||||
"%f %s",
|
||||
"%f%% %s %s",
|
||||
"",
|
||||
"%f%% %s %s",
|
||||
"Level %.0f %s (%d/%d Charges)",
|
||||
"",
|
||||
"",
|
||||
"+%f to %s (%s Only)",
|
||||
"+%.0f to %s",
|
||||
}
|
||||
|
||||
// DescString return a string based on the DescFnID
|
||||
func (r *ItemStatCostRecord) DescString(a ...interface{}) string {
|
||||
if r.DescFnID < 0 || r.DescFnID > len(itemStatCostRecordLookup) {
|
||||
return ""
|
||||
}
|
||||
|
||||
format := itemStatCostRecordLookup[r.DescFnID]
|
||||
|
||||
return fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
// DescGroupString return a string based on the DescGroupFuncID
|
||||
func (r *ItemStatCostRecord) DescGroupString(a ...interface{}) string {
|
||||
if r.DescGroupFuncID < 0 || r.DescGroupFuncID > len(itemStatCostRecordLookup) {
|
||||
return ""
|
||||
}
|
||||
|
||||
format := itemStatCostRecordLookup[r.DescGroupFuncID]
|
||||
|
||||
return fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
// ItemStatCosts stores all of the ItemStatCostRecords
|
||||
//nolint:gochecknoglobals // Currently global by design
|
||||
var ItemStatCosts map[string]*ItemStatCostRecord
|
||||
@ -217,10 +160,10 @@ func LoadItemStatCosts(file []byte) {
|
||||
|
||||
DescPriority: d.Number("descpriority"),
|
||||
DescFnID: d.Number("descfunc"),
|
||||
DescVal: d.Number("descval"),
|
||||
DescStrPos: d.String("descstrpos"),
|
||||
DescStrNeg: d.String("descstrneg"),
|
||||
DescStr2: d.String("descstr2"),
|
||||
// DescVal: d.Number("descval"), // needs special handling
|
||||
DescStrPos: d.String("descstrpos"),
|
||||
DescStrNeg: d.String("descstrneg"),
|
||||
DescStr2: d.String("descstr2"),
|
||||
|
||||
DescGroup: d.Number("dgrp"),
|
||||
DescGroupFuncID: d.Number("dgrpfunc"),
|
||||
@ -232,6 +175,18 @@ func LoadItemStatCosts(file []byte) {
|
||||
|
||||
Stuff: d.String("stuff"),
|
||||
}
|
||||
|
||||
descValStr := d.String("descval")
|
||||
switch descValStr {
|
||||
case "2":
|
||||
record.DescVal = 2
|
||||
case "0":
|
||||
record.DescVal = 0
|
||||
default:
|
||||
// handle empty fields, seems like they should have been 1
|
||||
record.DescVal = 1
|
||||
}
|
||||
|
||||
ItemStatCosts[record.Name] = record
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ type (
|
||||
// column also links other hardcoded effects to the units, such as the
|
||||
// transparency on necro summons and the name-color change on unique boss
|
||||
// units (thanks to Kingpin for the info)
|
||||
Id string //nolint:golint,stylecheck // called `hcIdx` in monstats.txt
|
||||
Id int //nolint:golint,stylecheck // called `hcIdx` in monstats.txt
|
||||
|
||||
// BaseKey is an ID pointer of the “base” unit for this specific
|
||||
// monster type (ex. There are five types of “Fallen”; all of them have
|
||||
@ -43,7 +43,7 @@ type (
|
||||
// NameStringTableKey the string-key used in the TBL (string.tbl,
|
||||
// expansionstring.tbl and patchstring.tbl) files to make this monsters
|
||||
// name appear when you highlight it.
|
||||
NameStringTableKey string // called `NameStr` in monstats.txt
|
||||
NameString string // called `NameStr` in monstats.txt
|
||||
|
||||
// ExtraDataKey the ID pointer to an entry in MonStats2.txt.
|
||||
ExtraDataKey string // called `MonStatsEx` in monstats.txt
|
||||
@ -691,11 +691,11 @@ func LoadMonStats(file []byte) { // nolint:funlen // Makes no sense to split
|
||||
for d.Next() {
|
||||
record := &MonStatsRecord{
|
||||
Key: d.String("Id"),
|
||||
Id: d.String("hcIdx"),
|
||||
Id: d.Number("hcIdx"),
|
||||
BaseKey: d.String("BaseId"),
|
||||
NextKey: d.String("NextInClass"),
|
||||
PaletteId: d.Number("TransLvl"),
|
||||
NameStringTableKey: d.String("NameStr"),
|
||||
NameString: d.String("NameStr"),
|
||||
ExtraDataKey: d.String("MonStatsEx"),
|
||||
PropertiesKey: d.String("MonProp"),
|
||||
MonsterGroup: d.String("MonType"),
|
||||
@ -943,6 +943,7 @@ func LoadMonStats(file []byte) { // nolint:funlen // Makes no sense to split
|
||||
SpecialEndGeneric: d.Number("SplEndGeneric") > 0,
|
||||
SpecialClientEnd: d.Number("SplClientEnd") > 0,
|
||||
}
|
||||
|
||||
MonStats[record.Key] = record
|
||||
}
|
||||
|
||||
|
258
d2common/d2data/d2datadict/skilldesc.go
Normal file
258
d2common/d2data/d2datadict/skilldesc.go
Normal file
@ -0,0 +1,258 @@
|
||||
package d2datadict
|
||||
|
||||
import (
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"log"
|
||||
)
|
||||
|
||||
type SkillDescriptionRecord struct {
|
||||
Name string // skilldesc
|
||||
SkillPage string // SkillPage
|
||||
SkillRow string // SkillRow
|
||||
SkillColumn string // SkillColumn
|
||||
ListRow string // ListRow
|
||||
ListPool string // ListPool
|
||||
IconCel string // IconCel
|
||||
NameKey string // str name
|
||||
ShortKey string // str short
|
||||
LongKey string // str long
|
||||
AltKey string // str alt
|
||||
ManaKey string // str mana
|
||||
Descdam string // descdam
|
||||
DdamCalc1 string // ddam calc1
|
||||
DdamCalc2 string // ddam calc2
|
||||
P1dmelem string // p1dmelem
|
||||
P1dmmin string // p1dmmin
|
||||
P1dmmax string // p1dmmax
|
||||
P2dmelem string // p2dmelem
|
||||
P2dmmin string // p2dmmin
|
||||
P2dmmax string // p2dmmax
|
||||
P3dmelem string // p3dmelem
|
||||
P3dmmin string // p3dmmin
|
||||
P3dmmax string // p3dmmax
|
||||
Descatt string // descatt
|
||||
Descmissile1 string // descmissile1
|
||||
Descmissile2 string // descmissile2
|
||||
Descmissile3 string // descmissile3
|
||||
Descline1 string // descline1
|
||||
Desctexta1 string // desctexta1
|
||||
Desctextb1 string // desctextb1
|
||||
Desccalca1 string // desccalca1
|
||||
Desccalcb1 string // desccalcb1
|
||||
Descline2 string // descline2
|
||||
Desctexta2 string // desctexta2
|
||||
Desctextb2 string // desctextb2
|
||||
Desccalca2 string // desccalca2
|
||||
Desccalcb2 string // desccalcb2
|
||||
Descline3 string // descline3
|
||||
Desctexta3 string // desctexta3
|
||||
Desctextb3 string // desctextb3
|
||||
Desccalca3 string // desccalca3
|
||||
Desccalcb3 string // desccalcb3
|
||||
Descline4 string // descline4
|
||||
Desctexta4 string // desctexta4
|
||||
Desctextb4 string // desctextb4
|
||||
Desccalca4 string // desccalca4
|
||||
Desccalcb4 string // desccalcb4
|
||||
Descline5 string // descline5
|
||||
Desctexta5 string // desctexta5
|
||||
Desctextb5 string // desctextb5
|
||||
Desccalca5 string // desccalca5
|
||||
Desccalcb5 string // desccalcb5
|
||||
Descline6 string // descline6
|
||||
Desctexta6 string // desctexta6
|
||||
Desctextb6 string // desctextb6
|
||||
Desccalca6 string // desccalca6
|
||||
Desccalcb6 string // desccalcb6
|
||||
Dsc2line1 string // dsc2line1
|
||||
Dsc2texta1 string // dsc2texta1
|
||||
Dsc2textb1 string // dsc2textb1
|
||||
Dsc2calca1 string // dsc2calca1
|
||||
Dsc2calcb1 string // dsc2calcb1
|
||||
Dsc2line2 string // dsc2line2
|
||||
Dsc2texta2 string // dsc2texta2
|
||||
Dsc2textb2 string // dsc2textb2
|
||||
Dsc2calca2 string // dsc2calca2
|
||||
Dsc2calcb2 string // dsc2calcb2
|
||||
Dsc2line3 string // dsc2line3
|
||||
Dsc2texta3 string // dsc2texta3
|
||||
Dsc2textb3 string // dsc2textb3
|
||||
Dsc2calca3 string // dsc2calca3
|
||||
Dsc2calcb3 string // dsc2calcb3
|
||||
Dsc2line4 string // dsc2line4
|
||||
Dsc2texta4 string // dsc2texta4
|
||||
Dsc2textb4 string // dsc2textb4
|
||||
Dsc2calca4 string // dsc2calca4
|
||||
Dsc2calcb4 string // dsc2calcb4
|
||||
Dsc3line1 string // dsc3line1
|
||||
Dsc3texta1 string // dsc3texta1
|
||||
Dsc3textb1 string // dsc3textb1
|
||||
Dsc3calca1 string // dsc3calca1
|
||||
Dsc3calcb1 string // dsc3calcb1
|
||||
Dsc3line2 string // dsc3line2
|
||||
Dsc3texta2 string // dsc3texta2
|
||||
Dsc3textb2 string // dsc3textb2
|
||||
Dsc3calca2 string // dsc3calca2
|
||||
Dsc3calcb2 string // dsc3calcb2
|
||||
Dsc3line3 string // dsc3line3
|
||||
Dsc3texta3 string // dsc3texta3
|
||||
Dsc3textb3 string // dsc3textb3
|
||||
Dsc3calca3 string // dsc3calca3
|
||||
Dsc3calcb3 string // dsc3calcb3
|
||||
Dsc3line4 string // dsc3line4
|
||||
Dsc3texta4 string // dsc3texta4
|
||||
Dsc3textb4 string // dsc3textb4
|
||||
Dsc3calca4 string // dsc3calca4
|
||||
Dsc3calcb4 string // dsc3calcb4
|
||||
Dsc3line5 string // dsc3line5
|
||||
Dsc3texta5 string // dsc3texta5
|
||||
Dsc3textb5 string // dsc3textb5
|
||||
Dsc3calca5 string // dsc3calca5
|
||||
Dsc3calcb5 string // dsc3calcb5
|
||||
Dsc3line6 string // dsc3line6
|
||||
Dsc3texta6 string // dsc3texta6
|
||||
Dsc3textb6 string // dsc3textb6
|
||||
Dsc3calca6 string // dsc3calca6
|
||||
Dsc3calcb6 string // dsc3calcb6
|
||||
Dsc3line7 string // dsc3line7
|
||||
Dsc3texta7 string // dsc3texta7
|
||||
Dsc3textb7 string // dsc3textb7
|
||||
Dsc3calca7 string // dsc3calca7
|
||||
Dsc3calcb7 string // dsc3calcb7
|
||||
}
|
||||
|
||||
// ItemStatCosts stores all of the ItemStatCostRecords
|
||||
//nolint:gochecknoglobals // Currently global by design
|
||||
var SkillDescriptions map[string]*SkillDescriptionRecord
|
||||
|
||||
// LoadItemStatCosts loads ItemStatCostRecord's from text
|
||||
func LoadSkillDescriptions(file []byte) {
|
||||
SkillDescriptions = make(map[string]*SkillDescriptionRecord)
|
||||
|
||||
d := d2common.LoadDataDictionary(file)
|
||||
for d.Next() {
|
||||
record := &SkillDescriptionRecord{
|
||||
d.String("skilldesc"),
|
||||
d.String("SkillPage"),
|
||||
d.String("SkillRow"),
|
||||
d.String("SkillColumn"),
|
||||
d.String("ListRow"),
|
||||
d.String("ListPool"),
|
||||
d.String("IconCel"),
|
||||
d.String("str name"),
|
||||
d.String("str short"),
|
||||
d.String("str long"),
|
||||
d.String("str alt"),
|
||||
d.String("str mana"),
|
||||
d.String("descdam"),
|
||||
d.String("ddam calc1"),
|
||||
d.String("ddam calc2"),
|
||||
d.String("p1dmelem"),
|
||||
d.String("p1dmmin"),
|
||||
d.String("p1dmmax"),
|
||||
d.String("p2dmelem"),
|
||||
d.String("p2dmmin"),
|
||||
d.String("p2dmmax"),
|
||||
d.String("p3dmelem"),
|
||||
d.String("p3dmmin"),
|
||||
d.String("p3dmmax"),
|
||||
d.String("descatt"),
|
||||
d.String("descmissile1"),
|
||||
d.String("descmissile2"),
|
||||
d.String("descmissile3"),
|
||||
d.String("descline1"),
|
||||
d.String("desctexta1"),
|
||||
d.String("desctextb1"),
|
||||
d.String("desccalca1"),
|
||||
d.String("desccalcb1"),
|
||||
d.String("descline2"),
|
||||
d.String("desctexta2"),
|
||||
d.String("desctextb2"),
|
||||
d.String("desccalca2"),
|
||||
d.String("desccalcb2"),
|
||||
d.String("descline3"),
|
||||
d.String("desctexta3"),
|
||||
d.String("desctextb3"),
|
||||
d.String("desccalca3"),
|
||||
d.String("desccalcb3"),
|
||||
d.String("descline4"),
|
||||
d.String("desctexta4"),
|
||||
d.String("desctextb4"),
|
||||
d.String("desccalca4"),
|
||||
d.String("desccalcb4"),
|
||||
d.String("descline5"),
|
||||
d.String("desctexta5"),
|
||||
d.String("desctextb5"),
|
||||
d.String("desccalca5"),
|
||||
d.String("desccalcb5"),
|
||||
d.String("descline6"),
|
||||
d.String("desctexta6"),
|
||||
d.String("desctextb6"),
|
||||
d.String("desccalca6"),
|
||||
d.String("desccalcb6"),
|
||||
d.String("dsc2line1"),
|
||||
d.String("dsc2texta1"),
|
||||
d.String("dsc2textb1"),
|
||||
d.String("dsc2calca1"),
|
||||
d.String("dsc2calcb1"),
|
||||
d.String("dsc2line2"),
|
||||
d.String("dsc2texta2"),
|
||||
d.String("dsc2textb2"),
|
||||
d.String("dsc2calca2"),
|
||||
d.String("dsc2calcb2"),
|
||||
d.String("dsc2line3"),
|
||||
d.String("dsc2texta3"),
|
||||
d.String("dsc2textb3"),
|
||||
d.String("dsc2calca3"),
|
||||
d.String("dsc2calcb3"),
|
||||
d.String("dsc2line4"),
|
||||
d.String("dsc2texta4"),
|
||||
d.String("dsc2textb4"),
|
||||
d.String("dsc2calca4"),
|
||||
d.String("dsc2calcb4"),
|
||||
d.String("dsc3line1"),
|
||||
d.String("dsc3texta1"),
|
||||
d.String("dsc3textb1"),
|
||||
d.String("dsc3calca1"),
|
||||
d.String("dsc3calcb1"),
|
||||
d.String("dsc3line2"),
|
||||
d.String("dsc3texta2"),
|
||||
d.String("dsc3textb2"),
|
||||
d.String("dsc3calca2"),
|
||||
d.String("dsc3calcb2"),
|
||||
d.String("dsc3line3"),
|
||||
d.String("dsc3texta3"),
|
||||
d.String("dsc3textb3"),
|
||||
d.String("dsc3calca3"),
|
||||
d.String("dsc3calcb3"),
|
||||
d.String("dsc3line4"),
|
||||
d.String("dsc3texta4"),
|
||||
d.String("dsc3textb4"),
|
||||
d.String("dsc3calca4"),
|
||||
d.String("dsc3calcb4"),
|
||||
d.String("dsc3line5"),
|
||||
d.String("dsc3texta5"),
|
||||
d.String("dsc3textb5"),
|
||||
d.String("dsc3calca5"),
|
||||
d.String("dsc3calcb5"),
|
||||
d.String("dsc3line6"),
|
||||
d.String("dsc3texta6"),
|
||||
d.String("dsc3textb6"),
|
||||
d.String("dsc3calca6"),
|
||||
d.String("dsc3calcb6"),
|
||||
d.String("dsc3line7"),
|
||||
d.String("dsc3texta7"),
|
||||
d.String("dsc3textb7"),
|
||||
d.String("dsc3calca7"),
|
||||
d.String("dsc3calcb7"),
|
||||
}
|
||||
|
||||
SkillDescriptions[record.Name] = record
|
||||
}
|
||||
|
||||
if d.Err != nil {
|
||||
panic(d.Err)
|
||||
}
|
||||
|
||||
log.Printf("Loaded %d Skill Description records", len(SkillDescriptions))
|
||||
}
|
@ -188,6 +188,7 @@ const (
|
||||
AutoMap = "/data/global/excel/AutoMap.txt"
|
||||
CubeRecipes = "/data/global/excel/cubemain.txt"
|
||||
Skills = "/data/global/excel/skills.txt"
|
||||
SkillDesc = "/data/global/excel/skilldesc.txt"
|
||||
|
||||
// --- Animations ---
|
||||
|
||||
|
@ -56,7 +56,7 @@ func CreateNPC(x, y int, monstat *d2datadict.MonStatsRecord, direction int) *NPC
|
||||
result.composite.SetDirection(direction)
|
||||
|
||||
if result.monstatRecord != nil && result.monstatRecord.IsInteractable {
|
||||
result.name = d2common.TranslateString(result.monstatRecord.NameStringTableKey)
|
||||
result.name = d2common.TranslateString(result.monstatRecord.NameString)
|
||||
}
|
||||
|
||||
return result
|
||||
|
2
d2core/d2stats/doc.go
Normal file
2
d2core/d2stats/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package d2stats provides item/skill/character stats functionality
|
||||
package d2stats
|
724
d2core/d2stats/stat.go
Normal file
724
d2core/d2stats/stat.go
Normal file
@ -0,0 +1,724 @@
|
||||
package d2stats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
)
|
||||
|
||||
type descValPosition int
|
||||
|
||||
const (
|
||||
descValHide descValPosition = iota
|
||||
descValPrefix
|
||||
descValPostfix
|
||||
)
|
||||
|
||||
// CreateStat creates a stat instance with the given ID and number of values
|
||||
func CreateStat(record *d2datadict.ItemStatCostRecord, values ...int) *Stat {
|
||||
if record == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
stat := &Stat{
|
||||
Record: record,
|
||||
Values: values,
|
||||
}
|
||||
|
||||
return stat
|
||||
}
|
||||
|
||||
// Stat is an instance of a Stat, with a set of Values
|
||||
type Stat struct {
|
||||
Record *d2datadict.ItemStatCostRecord
|
||||
Values []int
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of the Stat
|
||||
func (s Stat) Clone() *Stat {
|
||||
clone := &Stat{
|
||||
Record: s.Record,
|
||||
Values: make([]int, len(s.Values)),
|
||||
}
|
||||
|
||||
for idx := range s.Values {
|
||||
clone.Values[idx] = s.Values[idx]
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
// Combine sums the other stat with this one, altering the
|
||||
// values of this one.
|
||||
func (s *Stat) combine(other *Stat) (success bool) {
|
||||
if !s.canBeCombinedWith(other) {
|
||||
return false
|
||||
}
|
||||
|
||||
for idx := range s.Values {
|
||||
// todo different combination logic per descfnid
|
||||
s.Values[idx] += other.Values[idx]
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Stat) canBeCombinedWith(other *Stat) bool {
|
||||
if s.Record != other.Record {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(s.Values) != len(other.Values) {
|
||||
return false
|
||||
}
|
||||
// todo `10% reanimate as: foo` is not the same as `10% reanimate as: bar`
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Description returns the formatted description string
|
||||
func (s *Stat) Description() string {
|
||||
return s.DescString(s.Values...)
|
||||
}
|
||||
|
||||
// StatDescriptionFormatStrings is an array of the base format strings used
|
||||
// by the `descfn` methods for stats. The records in itemstatcost.txt have a
|
||||
// number field which denotes which of these functions is used for formatting
|
||||
// the stat description.
|
||||
// These came from phrozen keep:
|
||||
// https://d2mods.info/forum/kb/viewarticle?a=448
|
||||
//nolint:gochecknoglobals // better for lookup
|
||||
var baseFormatStrings = []string{
|
||||
"",
|
||||
"%v %s",
|
||||
"%v%% %s",
|
||||
"%v %s",
|
||||
"%v%% %s",
|
||||
"%v%% %s",
|
||||
"%v %s %s",
|
||||
"%v%% %v %s",
|
||||
"%v%% %s %s",
|
||||
"%v %s %s",
|
||||
"%v %s %s",
|
||||
"Repairs 1 Durability In %v Seconds",
|
||||
"%v +%v",
|
||||
"+%v %s",
|
||||
"+%v to %s %s",
|
||||
"%v%% %s",
|
||||
"%v %s",
|
||||
"%v %s (Increases near %v)",
|
||||
"%v%% %s (Increases near %v)",
|
||||
"",
|
||||
"%v%% %s",
|
||||
"%v %s",
|
||||
"%v%% %s %s",
|
||||
"%v%% %s %s",
|
||||
"Level %v %s %s",
|
||||
"",
|
||||
"",
|
||||
"+%v to %s %s",
|
||||
"+%v to %s",
|
||||
}
|
||||
|
||||
var statValueCountLookup map[int]int //nolint:gochecknoglobals // lookup
|
||||
|
||||
// DescString return a string based on the DescFnID
|
||||
func (s *Stat) DescString(values ...int) string {// nolint:gocyclo switch statement is not so bad
|
||||
if s.Record.DescFnID < 0 || s.Record.DescFnID > len(baseFormatStrings) {
|
||||
return ""
|
||||
}
|
||||
|
||||
var result string
|
||||
|
||||
//nolint:gomdn introducing a const for these would be worse
|
||||
switch s.Record.DescFnID {
|
||||
case 1:
|
||||
result = s.descFn1(values...)
|
||||
case 2:
|
||||
result = s.descFn2(values...)
|
||||
case 3:
|
||||
result = s.descFn3(values...)
|
||||
case 4:
|
||||
result = s.descFn4(values...)
|
||||
case 5:
|
||||
result = s.descFn5(values...)
|
||||
case 6:
|
||||
result = s.descFn6(values...)
|
||||
case 7:
|
||||
result = s.descFn7(values...)
|
||||
case 8:
|
||||
result = s.descFn8(values...)
|
||||
case 9:
|
||||
result = s.descFn9(values...)
|
||||
case 11:
|
||||
result = s.descFn11(values...)
|
||||
case 12:
|
||||
result = s.descFn12(values...)
|
||||
case 13:
|
||||
result = s.descFn13(values...)
|
||||
case 14:
|
||||
result = s.descFn14(values...)
|
||||
case 15:
|
||||
result = s.descFn15(values...)
|
||||
case 16:
|
||||
result = s.descFn16(values...)
|
||||
case 20:
|
||||
result = s.descFn20(values...)
|
||||
case 22:
|
||||
result = s.descFn22(values...)
|
||||
case 23:
|
||||
result = s.descFn23(values...)
|
||||
case 24:
|
||||
result = s.descFn24(values...)
|
||||
case 27:
|
||||
result = s.descFn27(values...)
|
||||
case 28:
|
||||
result = s.descFn28(values...)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Stat) descFn1(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
var stringTableKey string
|
||||
if value < 0 {
|
||||
stringTableKey = s.Record.DescStrNeg
|
||||
} else {
|
||||
format = strings.Join([]string{"+", format}, "")
|
||||
stringTableKey = s.Record.DescStrPos
|
||||
}
|
||||
|
||||
stringTableString := d2common.TranslateString(stringTableKey)
|
||||
|
||||
var result string
|
||||
|
||||
switch descValPosition(s.Record.DescVal) {
|
||||
case descValHide:
|
||||
result = fmt.Sprintf(format, stringTableString)
|
||||
case descValPrefix:
|
||||
result = fmt.Sprintf(format, value, stringTableString)
|
||||
case descValPostfix:
|
||||
formatSplit := strings.Split(format, " ")
|
||||
format = strings.Join(reverseStringSlice(formatSplit), " ")
|
||||
result = fmt.Sprintf(format, stringTableString, value)
|
||||
default:
|
||||
result = ""
|
||||
}
|
||||
|
||||
result = strings.ReplaceAll(result, "+-", "-")
|
||||
result = strings.ReplaceAll(result, " +%d", "")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Stat) descFn2(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
var stringTableKey string
|
||||
if value < 0 {
|
||||
stringTableKey = s.Record.DescStrNeg
|
||||
} else {
|
||||
format = strings.Join([]string{"+", format}, "")
|
||||
stringTableKey = s.Record.DescStrPos
|
||||
}
|
||||
|
||||
stringTableString := d2common.TranslateString(stringTableKey)
|
||||
|
||||
var result string
|
||||
|
||||
switch descValPosition(s.Record.DescVal) {
|
||||
case descValHide:
|
||||
result = fmt.Sprintf(format, stringTableString)
|
||||
case descValPrefix:
|
||||
result = fmt.Sprintf(format, value, stringTableString)
|
||||
case descValPostfix:
|
||||
formatSplit := strings.Split(format, " ")
|
||||
format = strings.Join(reverseStringSlice(formatSplit), " ")
|
||||
result = fmt.Sprintf(format, stringTableString, value)
|
||||
default:
|
||||
result = ""
|
||||
}
|
||||
|
||||
return fixString(result)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn3(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
var stringTableKey string
|
||||
if value < 0 {
|
||||
stringTableKey = s.Record.DescStrNeg
|
||||
} else {
|
||||
stringTableKey = s.Record.DescStrPos
|
||||
}
|
||||
|
||||
stringTableString := d2common.TranslateString(stringTableKey)
|
||||
|
||||
var result string
|
||||
|
||||
switch descValPosition(s.Record.DescVal) {
|
||||
case descValHide:
|
||||
format = strings.Split(format, " ")[0]
|
||||
result = fmt.Sprintf(format, stringTableString)
|
||||
case descValPrefix:
|
||||
result = fmt.Sprintf(format, value, stringTableString)
|
||||
case descValPostfix:
|
||||
formatSplit := strings.Split(format, " ")
|
||||
format = strings.Join(reverseStringSlice(formatSplit), " ")
|
||||
result = fmt.Sprintf(format, stringTableString, value)
|
||||
default:
|
||||
result = ""
|
||||
}
|
||||
|
||||
return fixString(result)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn4(values ...int) string {
|
||||
// for now, same as fn2
|
||||
return s.descFn2(values...)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn5(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
var stringTableKey string
|
||||
if value < 0 {
|
||||
stringTableKey = s.Record.DescStrNeg
|
||||
} else {
|
||||
stringTableKey = s.Record.DescStrPos
|
||||
}
|
||||
|
||||
stringTableString := d2common.TranslateString(stringTableKey)
|
||||
|
||||
var result string
|
||||
|
||||
switch descValPosition(s.Record.DescVal) {
|
||||
case descValHide:
|
||||
result = fmt.Sprintf(format, stringTableString)
|
||||
case descValPrefix:
|
||||
result = fmt.Sprintf(format, value, stringTableString)
|
||||
case descValPostfix:
|
||||
formatSplit := strings.Split(format, " ")
|
||||
format = strings.Join(reverseStringSlice(formatSplit), " ")
|
||||
result = fmt.Sprintf(format, stringTableString, value)
|
||||
default:
|
||||
result = ""
|
||||
}
|
||||
|
||||
return fixString(result)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn6(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
var stringTableKey1 string
|
||||
if value < 0 {
|
||||
stringTableKey1 = s.Record.DescStrNeg
|
||||
} else {
|
||||
format = strings.Join([]string{"+", format}, "")
|
||||
stringTableKey1 = s.Record.DescStrPos
|
||||
}
|
||||
|
||||
stringTableStr1 := d2common.TranslateString(stringTableKey1)
|
||||
|
||||
// this stat has an additional string (Based on Character Level)
|
||||
stringTableStr2 := d2common.TranslateString(s.Record.DescStr2)
|
||||
|
||||
var result string
|
||||
|
||||
switch descValPosition(s.Record.DescVal) {
|
||||
case descValHide:
|
||||
result = fmt.Sprintf(format, stringTableStr1, stringTableStr2)
|
||||
case descValPrefix:
|
||||
result = fmt.Sprintf(format, value, stringTableStr1, stringTableStr2)
|
||||
case descValPostfix:
|
||||
formatSplit := strings.Split(format, " ")
|
||||
format = strings.Join(reverseStringSlice(formatSplit), " ")
|
||||
result = fmt.Sprintf(format, stringTableStr1, value)
|
||||
default:
|
||||
result = ""
|
||||
}
|
||||
|
||||
return fixString(result)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn7(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
var stringTableKey string
|
||||
if value < 0 {
|
||||
stringTableKey = s.Record.DescStrNeg
|
||||
} else {
|
||||
format = strings.Join([]string{"+", format}, "")
|
||||
stringTableKey = s.Record.DescStrPos
|
||||
}
|
||||
|
||||
stringTableStr1 := d2common.TranslateString(stringTableKey)
|
||||
|
||||
// this stat has an additional string (Based on Character Level)
|
||||
stringTableStr2 := d2common.TranslateString(s.Record.DescStr2)
|
||||
|
||||
var result string
|
||||
|
||||
switch descValPosition(s.Record.DescVal) {
|
||||
case descValHide:
|
||||
result = fmt.Sprintf(format, stringTableStr1, stringTableStr2)
|
||||
case descValPrefix:
|
||||
result = fmt.Sprintf(format, value, stringTableStr1, stringTableStr2)
|
||||
case descValPostfix:
|
||||
formatSplit := strings.Split(format, " ")
|
||||
formatSplit[0], formatSplit[1] = formatSplit[1], formatSplit[0]
|
||||
format = strings.Join(formatSplit, " ")
|
||||
result = fmt.Sprintf(format, stringTableStr1, value, stringTableStr2)
|
||||
default:
|
||||
result = ""
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Stat) descFn8(values ...int) string {
|
||||
// for now, same as fn7
|
||||
return s.descFn7(values...)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn9(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
var stringTableKey1 string
|
||||
if value < 0 {
|
||||
stringTableKey1 = s.Record.DescStrNeg
|
||||
} else {
|
||||
stringTableKey1 = s.Record.DescStrPos
|
||||
}
|
||||
|
||||
stringTableStr1 := d2common.TranslateString(stringTableKey1)
|
||||
|
||||
// this stat has an additional string (Based on Character Level)
|
||||
stringTableStr2 := d2common.TranslateString(s.Record.DescStr2)
|
||||
|
||||
var result string
|
||||
|
||||
switch descValPosition(s.Record.DescVal) {
|
||||
case descValHide:
|
||||
result = fmt.Sprintf(format, stringTableStr1, stringTableStr2)
|
||||
case descValPrefix:
|
||||
result = fmt.Sprintf(format, value, stringTableStr1, stringTableStr2)
|
||||
case descValPostfix:
|
||||
formatSplit := strings.Split(format, " ")
|
||||
formatSplit[0], formatSplit[1] = formatSplit[1], formatSplit[0]
|
||||
format = strings.Join(formatSplit, " ")
|
||||
result = fmt.Sprintf(format, stringTableStr1, value, stringTableStr2)
|
||||
default:
|
||||
result = ""
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Stat) descFn11(values ...int) string {
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
// the only stat to use this fn is "Repairs durability in X seconds"
|
||||
format := d2common.TranslateString(s.Record.DescStrPos)
|
||||
|
||||
return fmt.Sprintf(format, value)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn12(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
|
||||
// we know there is only one value for this stat
|
||||
value := values[0]
|
||||
|
||||
str1 := d2common.TranslateString(s.Record.DescStrPos)
|
||||
|
||||
return fmt.Sprintf(format, str1, value)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn13(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
numSkills, heroIndex := values[0], values[1]
|
||||
|
||||
heroMap := map[int]d2enum.Hero{
|
||||
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
|
||||
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
|
||||
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
|
||||
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
|
||||
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
|
||||
int(d2enum.HeroDruid): d2enum.HeroDruid,
|
||||
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
|
||||
}
|
||||
|
||||
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
|
||||
descStr1 := d2common.TranslateString(classRecord.SkillStrAll)
|
||||
result := fmt.Sprintf(format, numSkills, descStr1)
|
||||
|
||||
result = strings.ReplaceAll(result, "+-", "-")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Stat) descFn14(values ...int) string {
|
||||
numSkills, heroIndex, skillTabIndex := values[0], values[1], values[2]
|
||||
|
||||
if skillTabIndex > 2 || skillTabIndex < 0 {
|
||||
skillTabIndex = 0
|
||||
}
|
||||
|
||||
heroMap := map[int]d2enum.Hero{
|
||||
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
|
||||
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
|
||||
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
|
||||
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
|
||||
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
|
||||
int(d2enum.HeroDruid): d2enum.HeroDruid,
|
||||
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
|
||||
}
|
||||
|
||||
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
|
||||
skillTabKey := classRecord.SkillStrTab[skillTabIndex]
|
||||
classOnlyKey := classRecord.SkillStrClassOnly
|
||||
|
||||
skillTabStr := d2common.TranslateString(skillTabKey) + " %v"
|
||||
skillTabStr = strings.ReplaceAll(skillTabStr, "%d", "%v")
|
||||
classOnlyStr := d2common.TranslateString(classOnlyKey)
|
||||
result := fmt.Sprintf(skillTabStr, numSkills, classOnlyStr)
|
||||
|
||||
return fixString(result)
|
||||
}
|
||||
|
||||
func within(n, min, max int) int {
|
||||
if n < min {
|
||||
n = min
|
||||
} else if n > max {
|
||||
n = max
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
|
||||
func (s *Stat) descFn15(values ...int) string {
|
||||
format := d2common.TranslateString(s.Record.DescStrPos)
|
||||
chanceToCast, skillLevel, skillIndex := values[0], values[1], values[2]
|
||||
|
||||
chanceToCast = within(chanceToCast, 0, 100)
|
||||
skillLevel = within(skillLevel, 0, 255)
|
||||
skillLevel = within(skillLevel, 0, 255)
|
||||
|
||||
skillRecord := d2datadict.SkillDetails[skillIndex]
|
||||
result := fmt.Sprintf(format, chanceToCast, skillLevel, skillRecord.Skill)
|
||||
result = strings.ReplaceAll(result, "+-", "-")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Stat) descFn16(values ...int) string {
|
||||
skillLevel, skillIndex := values[0], values[1]
|
||||
str1 := d2common.TranslateString(s.Record.DescStrPos)
|
||||
skillRecord := d2datadict.SkillDetails[skillIndex]
|
||||
result := fmt.Sprintf(str1, skillLevel, skillRecord.Skill)
|
||||
|
||||
return fixString(result)
|
||||
}
|
||||
|
||||
/*
|
||||
func (s *Stat) descFn17(values ...int) string {
|
||||
// these were not implemented in original D2
|
||||
// leaving them out for now as I don't know how to
|
||||
// write a test for them, nor do I think vanilla content uses them
|
||||
// but these are the stat keys which point to this func...
|
||||
// item_armor_bytime
|
||||
// item_hp_bytime
|
||||
// item_mana_bytime
|
||||
// item_maxdamage_bytime
|
||||
// item_strength_bytime
|
||||
// item_dexterity_bytime
|
||||
// item_energy_bytime
|
||||
// item_vitality_bytime
|
||||
// item_tohit_bytime
|
||||
// item_cold_damagemax_bytime
|
||||
// item_fire_damagemax_bytime
|
||||
// item_ltng_damagemax_bytime
|
||||
// item_pois_damagemax_bytime
|
||||
// item_stamina_bytime
|
||||
// item_tohit_demon_bytime
|
||||
// item_tohit_undead_bytime
|
||||
// item_kick_damage_bytime
|
||||
}
|
||||
|
||||
func (s *Stat) descFn18(values ...int) string {
|
||||
// ... same with these ...
|
||||
// item_armorpercent_bytime
|
||||
// item_maxdamage_percent_bytime
|
||||
// item_tohitpercent_bytime
|
||||
// item_resist_cold_bytime
|
||||
// item_resist_fire_bytime
|
||||
// item_resist_ltng_bytime
|
||||
// item_resist_pois_bytime
|
||||
// item_absorb_cold_bytime
|
||||
// item_absorb_fire_bytime
|
||||
// item_absorb_ltng_bytime
|
||||
// item_find_gold_bytime
|
||||
// item_find_magic_bytime
|
||||
// item_regenstamina_bytime
|
||||
// item_damage_demon_bytime
|
||||
// item_damage_undead_bytime
|
||||
// item_crushingblow_bytime
|
||||
// item_openwounds_bytime
|
||||
// item_deadlystrike_bytime
|
||||
}
|
||||
*/
|
||||
|
||||
func (s *Stat) descFn20(values ...int) string {
|
||||
// for now, same as fn2
|
||||
return s.descFn2(values...)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn22(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
statAgainst, monsterIndex := values[0], values[1]
|
||||
|
||||
var monsterKey string
|
||||
|
||||
for key := range d2datadict.MonStats {
|
||||
if d2datadict.MonStats[key].Id == monsterIndex {
|
||||
monsterKey = key
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
str1 := d2common.TranslateString(s.Record.DescStrPos)
|
||||
monsterName := d2datadict.MonStats[monsterKey].NameString
|
||||
|
||||
result := fmt.Sprintf(format, statAgainst, str1, monsterName)
|
||||
|
||||
result = strings.ReplaceAll(result, "+-", "-")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Stat) descFn23(values ...int) string {
|
||||
// for now, same as fn22
|
||||
return s.descFn22(values...)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn24(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
lvl, skillID, chargeMax, chargeCurrent := values[0], values[1], values[2], values[3]
|
||||
|
||||
charges := d2common.TranslateString(s.Record.DescStrPos)
|
||||
charges = fmt.Sprintf(charges, chargeCurrent, chargeMax)
|
||||
|
||||
skillName := d2datadict.SkillDetails[skillID].Skill
|
||||
|
||||
result := fmt.Sprintf(format, lvl, skillName, charges)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *Stat) descFn27(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
amount, skillID, heroIndex := values[0], values[1], values[2]
|
||||
|
||||
skillName := d2datadict.SkillDetails[skillID].Skill
|
||||
|
||||
heroMap := map[int]d2enum.Hero{
|
||||
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
|
||||
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
|
||||
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
|
||||
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
|
||||
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
|
||||
int(d2enum.HeroDruid): d2enum.HeroDruid,
|
||||
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
|
||||
}
|
||||
|
||||
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
|
||||
classOnlyStr := d2common.TranslateString(classRecord.SkillStrClassOnly)
|
||||
|
||||
return fmt.Sprintf(format, amount, skillName, classOnlyStr)
|
||||
}
|
||||
|
||||
func (s *Stat) descFn28(values ...int) string {
|
||||
format := baseFormatStrings[s.Record.DescFnID]
|
||||
amount, skillID := values[0], values[1]
|
||||
|
||||
skillName := d2datadict.SkillDetails[skillID].Skill
|
||||
|
||||
return fmt.Sprintf(format, amount, skillName)
|
||||
}
|
||||
|
||||
func fixString(s string) string {
|
||||
s = strings.ReplaceAll(s, "+-", "-")
|
||||
s = strings.ReplaceAll(s, " +%d", "")
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// DescGroupString return a string based on the DescGroupFuncID
|
||||
func (s *Stat) DescGroupString(a ...interface{}) string {
|
||||
if s.Record.DescGroupFuncID < 0 || s.Record.DescGroupFuncID > len(
|
||||
baseFormatStrings) {
|
||||
return ""
|
||||
}
|
||||
|
||||
format := baseFormatStrings[s.Record.DescGroupFuncID]
|
||||
|
||||
return fmt.Sprintf(format, a...)
|
||||
}
|
||||
|
||||
// NumStatValues returns the number of values a stat instance for this
|
||||
// record should have
|
||||
func (s *Stat) NumStatValues() int {
|
||||
if num, found := statValueCountLookup[s.Record.DescGroupFuncID]; found {
|
||||
return num
|
||||
}
|
||||
|
||||
if statValueCountLookup == nil {
|
||||
statValueCountLookup = make(map[int]int)
|
||||
}
|
||||
|
||||
format := baseFormatStrings[s.Record.DescGroupFuncID]
|
||||
pattern := regexp.MustCompile("%v")
|
||||
matches := pattern.FindAllStringIndex(format, -1)
|
||||
num := len(matches)
|
||||
statValueCountLookup[s.Record.DescGroupFuncID] = num
|
||||
|
||||
return num
|
||||
}
|
||||
|
||||
func reverseStringSlice(s []string) []string {
|
||||
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
|
||||
s[i], s[j] = s[j], s[i]
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
382
d2core/d2stats/stat_test.go
Normal file
382
d2core/d2stats/stat_test.go
Normal file
@ -0,0 +1,382 @@
|
||||
package d2stats
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
)
|
||||
|
||||
const (
|
||||
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"
|
||||
)
|
||||
|
||||
//nolint:funlen // this just gets mock data ready for the tests
|
||||
func TestStat_InitMockData(t *testing.T) {
|
||||
var itemStatCosts = map[string]*d2datadict.ItemStatCostRecord{
|
||||
"strength": {
|
||||
Name: "strength",
|
||||
DescFnID: 1,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "to Strength",
|
||||
DescStrNeg: "to Strength",
|
||||
},
|
||||
"dexterity": {
|
||||
Name: "dexterity",
|
||||
DescFnID: 1,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "to Dexterity",
|
||||
DescStrNeg: "to Dexterity",
|
||||
},
|
||||
"vitality": {
|
||||
Name: "vitality",
|
||||
DescFnID: 1,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "to Vitality",
|
||||
DescStrNeg: "to Vitality",
|
||||
},
|
||||
"energy": {
|
||||
Name: "energy",
|
||||
DescFnID: 1,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "to Energy",
|
||||
DescStrNeg: "to Energy",
|
||||
},
|
||||
"hpregen": {
|
||||
Name: "hpregen",
|
||||
DescFnID: 1,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Replenish Life",
|
||||
DescStrNeg: "Drain Life",
|
||||
},
|
||||
"toblock": {
|
||||
Name: "toblock",
|
||||
DescFnID: 2,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "Increased Chance of Blocking",
|
||||
DescStrNeg: "Increased Chance of Blocking",
|
||||
},
|
||||
"item_absorblight_percent": {
|
||||
Name: "item_absorblight_percent",
|
||||
DescFnID: 2,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Lightning Absorb",
|
||||
DescStrNeg: "Lightning Absorb",
|
||||
},
|
||||
"item_restinpeace": {
|
||||
Name: "item_restinpeace",
|
||||
DescFnID: 3,
|
||||
DescVal: int(descValHide),
|
||||
DescStrPos: "Slain Monsters Rest in Peace",
|
||||
DescStrNeg: "Slain Monsters Rest in Peace",
|
||||
},
|
||||
"normal_damage_reduction": {
|
||||
Name: "normal_damage_reduction",
|
||||
DescFnID: 3,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Damage Reduced by",
|
||||
DescStrNeg: "Damage Reduced by",
|
||||
},
|
||||
"poisonresist": {
|
||||
Name: "poisonresist",
|
||||
DescFnID: 4,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Poison Resist",
|
||||
DescStrNeg: "Poison Resist",
|
||||
},
|
||||
"item_fastermovevelocity": {
|
||||
Name: "item_fastermovevelocity",
|
||||
DescFnID: 4,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "Faster Run/Walk",
|
||||
DescStrNeg: "Faster Run/Walk",
|
||||
},
|
||||
"item_howl": {
|
||||
Name: "item_howl",
|
||||
DescFnID: 5,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Hit Causes Monster to Flee",
|
||||
DescStrNeg: "Hit Causes Monster to Flee",
|
||||
},
|
||||
"item_hp_perlevel": {
|
||||
Name: "item_hp_perlevel",
|
||||
DescFnID: 6,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "to Life",
|
||||
DescStrNeg: "to Life",
|
||||
DescStr2: "(Based on Character Level)",
|
||||
},
|
||||
"item_resist_ltng_perlevel": {
|
||||
Name: "item_resist_ltng_perlevel",
|
||||
DescFnID: 7,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Lightning Resist",
|
||||
DescStrNeg: "Lightning Resist",
|
||||
DescStr2: "(Based on Character Level)",
|
||||
},
|
||||
"item_find_magic_perlevel": {
|
||||
Name: "item_find_magic_perlevel",
|
||||
DescFnID: 7,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "Better Chance of Getting Magic Items",
|
||||
DescStrNeg: "Better Chance of Getting Magic Items",
|
||||
DescStr2: "(Based on Character Level)",
|
||||
},
|
||||
"item_armorpercent_perlevel": {
|
||||
Name: "item_armorpercent_perlevel",
|
||||
DescFnID: 8,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "Enhanced Defense",
|
||||
DescStrNeg: "Enhanced Defense",
|
||||
DescStr2: "(Based on Character Level)",
|
||||
},
|
||||
"item_regenstamina_perlevel": {
|
||||
Name: "item_regenstamina_perlevel",
|
||||
DescFnID: 8,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Heal Stamina Plus",
|
||||
DescStrNeg: "Heal Stamina Plus",
|
||||
DescStr2: "(Based on Character Level)",
|
||||
},
|
||||
"item_thorns_perlevel": {
|
||||
Name: "item_thorns_perlevel",
|
||||
DescFnID: 9,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Attacker Takes Damage of",
|
||||
DescStrNeg: "Attacker Takes Damage of",
|
||||
DescStr2: "(Based on Character Level)",
|
||||
},
|
||||
"item_replenish_durability": {
|
||||
Name: "item_replenish_durability",
|
||||
DescFnID: 11,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "Repairs %v durability per second",
|
||||
DescStrNeg: "Repairs %v durability per second",
|
||||
DescStr2: "",
|
||||
},
|
||||
"item_stupidity": {
|
||||
Name: "item_stupidity",
|
||||
DescFnID: 12,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Hit Blinds Target",
|
||||
DescStrNeg: "Hit Blinds Target",
|
||||
},
|
||||
"item_addclassskills": {
|
||||
Name: "item_addclassskills",
|
||||
DescFnID: 13,
|
||||
DescVal: int(descValPrefix),
|
||||
},
|
||||
"item_addskill_tab": {
|
||||
Name: "item_addskill_tab",
|
||||
DescFnID: 14,
|
||||
DescVal: int(descValPrefix),
|
||||
},
|
||||
"item_skillonattack": {
|
||||
Name: "item_skillonattack",
|
||||
DescFnID: 15,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "%d%% Chance to cast level %d %s on attack",
|
||||
DescStrNeg: "%d%% Chance to cast level %d %s on attack",
|
||||
},
|
||||
"item_aura": {
|
||||
Name: "item_aura",
|
||||
DescFnID: 16,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "Level %d %s Aura When Equipped",
|
||||
DescStrNeg: "Level %d %s Aura When Equipped",
|
||||
},
|
||||
"item_fractionaltargetac": {
|
||||
Name: "item_fractionaltargetac",
|
||||
DescFnID: 20,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "Target Defense",
|
||||
DescStrNeg: "Target Defense",
|
||||
},
|
||||
"attack_vs_montype": {
|
||||
Name: "item_fractionaltargetac",
|
||||
DescFnID: 22,
|
||||
DescVal: int(descValPrefix),
|
||||
DescStrPos: "to Attack Rating versus",
|
||||
DescStrNeg: "to Attack Rating versus",
|
||||
},
|
||||
"item_reanimate": {
|
||||
Name: "item_reanimate",
|
||||
DescFnID: 23,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "Reanimate as:",
|
||||
DescStrNeg: "Reanimate as:",
|
||||
},
|
||||
"item_charged_skill": {
|
||||
Name: "item_charged_skill",
|
||||
DescFnID: 24,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "(%d/%d Charges)",
|
||||
DescStrNeg: "(%d/%d Charges)",
|
||||
},
|
||||
"item_singleskill": {
|
||||
Name: "item_singleskill",
|
||||
DescFnID: 27,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "(%d/%d Charges)",
|
||||
DescStrNeg: "(%d/%d Charges)",
|
||||
},
|
||||
"item_nonclassskill": {
|
||||
Name: "item_nonclassskill",
|
||||
DescFnID: 28,
|
||||
DescVal: int(descValPostfix),
|
||||
DescStrPos: "(%d/%d Charges)",
|
||||
DescStrNeg: "(%d/%d Charges)",
|
||||
},
|
||||
}
|
||||
|
||||
var charStats = map[d2enum.Hero]*d2datadict.CharStatsRecord{
|
||||
d2enum.HeroPaladin: {
|
||||
Class: d2enum.HeroPaladin,
|
||||
SkillStrAll: "to Paladin Skill Levels",
|
||||
SkillStrClassOnly: "(Paladin Only)",
|
||||
SkillStrTab: [3]string{
|
||||
"+%d to Combat Skills",
|
||||
"+%d to Offensive Auras",
|
||||
"+%d to Defensive Auras",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var skillDetails = map[int]*d2datadict.SkillRecord{
|
||||
37: {Skill: "Warmth"},
|
||||
64: {Skill: "Frozen Orb"},
|
||||
}
|
||||
|
||||
var monStats = map[string]*d2datadict.MonStatsRecord{
|
||||
"Specter": {NameString: "Specter", Id: 40},
|
||||
}
|
||||
|
||||
d2datadict.ItemStatCosts = itemStatCosts
|
||||
d2datadict.CharStats = charStats
|
||||
d2datadict.SkillDetails = skillDetails
|
||||
d2datadict.MonStats = monStats
|
||||
}
|
||||
|
||||
func TestStat_Clone(t *testing.T) {
|
||||
r := d2datadict.ItemStatCosts["strength"]
|
||||
s1 := CreateStat(r, 5)
|
||||
s2 := s1.Clone()
|
||||
|
||||
// make sure the stats are distinct
|
||||
if &s1 == &s2 {
|
||||
t.Errorf("stats share the same pointer %d == %d", &s1, &s2)
|
||||
}
|
||||
|
||||
// make sure the stat values are unique
|
||||
vs1, vs2 := s1.Values, s2.Values
|
||||
if &vs1 == &vs2 {
|
||||
t.Errorf("stat values share the same pointer %d == %d", &s1, &s2)
|
||||
}
|
||||
|
||||
s2.Values[0] = 6
|
||||
v1, v2 := s1.Values[0], s2.Values[0]
|
||||
|
||||
// make sure the value ranges are distinct
|
||||
if v1 == v2 {
|
||||
t.Errorf("stat value ranges should not be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStat_Descriptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
recordKey string
|
||||
vals []int
|
||||
expect string
|
||||
}{
|
||||
// DescFn1
|
||||
{"strength", []int{31}, "+31 to Strength"},
|
||||
{"hpregen", []int{20}, "Replenish Life +20"},
|
||||
{"hpregen", []int{-8}, "Drain Life -8"},
|
||||
|
||||
// DescFn2
|
||||
{"toblock", []int{16}, "+16% Increased Chance of Blocking"},
|
||||
{"item_absorblight_percent", []int{10}, "Lightning Absorb +10%"},
|
||||
|
||||
// DescFn3
|
||||
{"normal_damage_reduction", []int{25}, "Damage Reduced by 25"},
|
||||
{"item_restinpeace", []int{25}, "Slain Monsters Rest in Peace"},
|
||||
|
||||
// DescFn4
|
||||
{"poisonresist", []int{25}, "Poison Resist +25%"},
|
||||
{"item_fastermovevelocity", []int{25}, "+25% Faster Run/Walk"},
|
||||
|
||||
// DescFn5
|
||||
{"item_howl", []int{25}, "Hit Causes Monster to Flee 25%"},
|
||||
|
||||
// DescFn6
|
||||
{"item_hp_perlevel", []int{25}, "+25 to Life (Based on Character Level)"},
|
||||
|
||||
// DescFn7
|
||||
{"item_resist_ltng_perlevel", []int{25}, "Lightning Resist +25% (Based on Character Level)"},
|
||||
{"item_find_magic_perlevel", []int{25}, "+25% Better Chance of Getting Magic Items (" +
|
||||
"Based on Character Level)"},
|
||||
|
||||
// DescFn8
|
||||
{"item_armorpercent_perlevel", []int{25}, "+25% Enhanced Defense (Based on Character Level)"},
|
||||
{"item_regenstamina_perlevel", []int{25},
|
||||
"Heal Stamina Plus +25% (Based on Character Level)"},
|
||||
|
||||
// DescFn9
|
||||
{"item_thorns_perlevel", []int{25}, "Attacker Takes Damage of 25 (Based on Character Level)"},
|
||||
|
||||
// DescFn11
|
||||
{"item_replenish_durability", []int{2}, "Repairs 2 durability per second"},
|
||||
|
||||
// DescFn12
|
||||
{"item_stupidity", []int{5}, "Hit Blinds Target +5"},
|
||||
|
||||
// DescFn13
|
||||
{"item_addclassskills", []int{5, 3}, "+5 to Paladin Skill Levels"},
|
||||
|
||||
// DescFn14
|
||||
{"item_addskill_tab", []int{5, 3, 0}, "+5 to Combat Skills (Paladin Only)"},
|
||||
{"item_addskill_tab", []int{5, 3, 1}, "+5 to Offensive Auras (Paladin Only)"},
|
||||
{"item_addskill_tab", []int{5, 3, 2}, "+5 to Defensive Auras (Paladin Only)"},
|
||||
|
||||
// DescFn15
|
||||
{"item_skillonattack", []int{5, 7, 64}, "5% Chance to cast level 7 Frozen Orb on attack"},
|
||||
|
||||
// DescFn16
|
||||
{"item_aura", []int{3, 37}, "Level 3 Warmth Aura When Equipped"},
|
||||
|
||||
// DescFn20
|
||||
{"item_fractionaltargetac", []int{-25}, "-25% Target Defense"},
|
||||
|
||||
// DescFn22
|
||||
{"attack_vs_montype", []int{25, 40}, "25% to Attack Rating versus Specter"},
|
||||
|
||||
// DescFn23
|
||||
{"item_reanimate", []int{25, 40}, "25% Reanimate as: Specter"},
|
||||
|
||||
// DescFn24
|
||||
{"item_charged_skill", []int{25, 64, 20, 19}, "Level 25 Frozen Orb (19/20 Charges)"},
|
||||
|
||||
// DescFn27
|
||||
{"item_singleskill", []int{25, 64, 3}, "+25 to Frozen Orb (Paladin Only)"},
|
||||
|
||||
// DescFn28
|
||||
{"item_nonclassskill", []int{25, 64}, "+25 to Frozen Orb"},
|
||||
}
|
||||
|
||||
for idx := range tests {
|
||||
test := tests[idx]
|
||||
record := d2datadict.ItemStatCosts[test.recordKey]
|
||||
expect := test.expect
|
||||
stat := CreateStat(record, test.vals...)
|
||||
|
||||
if got := stat.Description(); got != expect {
|
||||
t.Errorf(errFmt, errStr, test.recordKey, test.vals, expect, got)
|
||||
} else {
|
||||
success := "[Desc Func %d][%s %+v] %s"
|
||||
success = fmt.Sprintf(success, record.DescFnID, record.Name, test.vals, got)
|
||||
fmt.Println(success)
|
||||
}
|
||||
}
|
||||
}
|
82
d2core/d2stats/statlist.go
Normal file
82
d2core/d2stats/statlist.go
Normal file
@ -0,0 +1,82 @@
|
||||
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
|
||||
}
|
90
d2core/d2stats/statlist_test.go
Normal file
90
d2core/d2stats/statlist_test.go
Normal file
@ -0,0 +1,90 @@
|
||||
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