1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-11-10 06:16:27 -05:00
OpenDiablo2/d2core/d2item/diablo2item/item_property_test.go
lord bfd3f1046d
D2items WIP (#646)
* wip d2items system and item properties

* added loader for TreasureClassEx.txt

* wip item spawn from treasure class records

* wip items

* add call to init item equivalencies, remove treasure class test from d2app

* made item affix records global var a map of affix codes to the records

* changed how item to item common record equivalency is determined

* changed set items records export to a map of their codes to the records, grouped property params into a struct

* changed property parameter field from calcstring to string

* fixed bug in stat value clone

* adding equipper interface as part of stat context, eventually to be used to resolve set bonus (among other things)

* made the item interface simpler, only needs name and description methods

* adding equipper interface, for anything that will equip or have active items

* handle case where min and max are swapped, removed commented code

* added property/stat resolution for magic, rare, set, and unique items

* adding item generator which can roll for items using treasure class records

* fixed item equivalency func being called in the wrong spot
2020-07-30 10:14:15 -04:00

609 lines
14 KiB
Go

package diablo2item
import (
"fmt"
"math/rand"
"regexp"
"testing"
"time"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
//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: 1,
DescStrPos: "to Strength",
DescStrNeg: "to Strength",
},
"dexterity": {
Name: "dexterity",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Dexterity",
DescStrNeg: "to Dexterity",
},
"vitality": {
Name: "vitality",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Vitality",
DescStrNeg: "to Vitality",
},
"energy": {
Name: "energy",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Energy",
DescStrNeg: "to Energy",
},
"hpregen": {
Name: "hpregen",
DescFnID: 1,
DescVal: 2,
DescStrPos: "Replenish Life",
DescStrNeg: "Drain Life",
},
"toblock": {
Name: "toblock",
DescFnID: 2,
DescVal: 1,
DescStrPos: "Increased Chance of Blocking",
DescStrNeg: "Increased Chance of Blocking",
},
"item_absorblight_percent": {
Name: "item_absorblight_percent",
DescFnID: 2,
DescVal: 2,
DescStrPos: "Lightning Absorb",
DescStrNeg: "Lightning Absorb",
},
"item_maxdurability_percent": {
Name: "item_maxdurability_percent",
DescFnID: 2,
DescVal: 2,
DescStrPos: "Increase Maximum Durability",
DescStrNeg: "Increase Maximum Durability",
},
"item_restinpeace": {
Name: "item_restinpeace",
DescFnID: 3,
DescVal: 0,
DescStrPos: "Slain Monsters Rest in Peace",
DescStrNeg: "Slain Monsters Rest in Peace",
},
"normal_damage_reduction": {
Name: "normal_damage_reduction",
DescFnID: 3,
DescVal: 2,
DescStrPos: "Damage Reduced by",
DescStrNeg: "Damage Reduced by",
},
"poisonresist": {
Name: "poisonresist",
DescFnID: 4,
DescVal: 2,
DescStrPos: "Poison Resist",
DescStrNeg: "Poison Resist",
},
"item_fastermovevelocity": {
Name: "item_fastermovevelocity",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Faster Run/Walk",
DescStrNeg: "Faster Run/Walk",
},
"item_howl": {
Name: "item_howl",
DescFnID: 5,
DescVal: 2,
DescStrPos: "Hit Causes Monster to Flee",
DescStrNeg: "Hit Causes Monster to Flee",
},
"item_hp_perlevel": {
Name: "item_hp_perlevel",
DescFnID: 6,
DescVal: 1,
DescStrPos: "to Life",
DescStrNeg: "to Life",
DescStr2: "(Based on Character Level)",
},
"item_resist_ltng_perlevel": {
Name: "item_resist_ltng_perlevel",
DescFnID: 7,
DescVal: 2,
DescStrPos: "Lightning Resist",
DescStrNeg: "Lightning Resist",
DescStr2: "(Based on Character Level)",
},
"item_find_magic_perlevel": {
Name: "item_find_magic_perlevel",
DescFnID: 7,
DescVal: 1,
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: 1,
DescStrPos: "Enhanced Defense",
DescStrNeg: "Enhanced Defense",
DescStr2: "(Based on Character Level)",
},
"item_regenstamina_perlevel": {
Name: "item_regenstamina_perlevel",
DescFnID: 8,
DescVal: 2,
DescStrPos: "Heal Stamina Plus",
DescStrNeg: "Heal Stamina Plus",
DescStr2: "(Based on Character Level)",
},
"item_thorns_perlevel": {
Name: "item_thorns_perlevel",
DescFnID: 9,
DescVal: 2,
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: 1,
DescStrPos: "Repairs %v durability per second",
DescStrNeg: "Repairs %v durability per second",
DescStr2: "",
},
"item_stupidity": {
Name: "item_stupidity",
DescFnID: 12,
DescVal: 2,
DescStrPos: "Hit Blinds Target",
DescStrNeg: "Hit Blinds Target",
},
"item_addclassskills": {
Name: "item_addclassskills",
DescFnID: 13,
DescVal: 1,
},
"item_addskill_tab": {
Name: "item_addskill_tab",
DescFnID: 14,
DescVal: 1,
},
"item_skillonattack": {
Name: "item_skillonattack",
DescFnID: 15,
DescVal: 1,
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: 1,
DescStrPos: "Level %d %s Aura When Equipped",
DescStrNeg: "Level %d %s Aura When Equipped",
},
"item_fractionaltargetac": {
Name: "item_fractionaltargetac",
DescFnID: 20,
DescVal: 1,
DescStrPos: "Target Defense",
DescStrNeg: "Target Defense",
},
"attack_vs_montype": {
Name: "item_fractionaltargetac",
DescFnID: 22,
DescVal: 1,
DescStrPos: "to Attack Rating versus",
DescStrNeg: "to Attack Rating versus",
},
"item_reanimate": {
Name: "item_reanimate",
DescFnID: 23,
DescVal: 2,
DescStrPos: "Reanimate as:",
DescStrNeg: "Reanimate as:",
},
"item_charged_skill": {
Name: "item_charged_skill",
DescFnID: 24,
DescVal: 2,
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_singleskill": {
Name: "item_singleskill",
DescFnID: 27,
DescVal: 0,
},
"item_nonclassskill": {
Name: "item_nonclassskill",
DescFnID: 28,
DescVal: 2,
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_armor_percent": {
Name: "item_armor_percent",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Enhanced Defense",
DescStrNeg: "Enhanced Defense",
},
"item_fastercastrate": {
Name: "item_fastercastrate",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Faster Cast Rate",
DescStrNeg: "Faster Cast Rate",
},
"item_skillonlevelup": {
Name: "item_skillonlevelup",
DescFnID: 15,
DescVal: 0,
DescStrPos: "%d%% Chance to cast level %d %s when you Level-Up",
DescStrNeg: "%d%% Chance to cast level %d %s when you Level-Up",
},
"item_numsockets": {
Name: "item_numsockets",
},
"poisonmindam": {
Name: "poisonmindam",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Minimum Poison Damage",
DescStrNeg: "to Minimum Poison Damage",
},
"poisonmaxdam": {
Name: "poisonmaxdam",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Maximum Poison Damage",
DescStrNeg: "to Maximum Poison Damage",
},
"poisonlength": {
Name: "poisonlength",
},
}
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},
}
properties := map[string]*d2datadict.PropertyRecord{
"allstats": {
Code: "allstats",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 1, StatCode: "strength"},
{FunctionID: 3, StatCode: "dexterity"},
{FunctionID: 3, StatCode: "vitality"},
{FunctionID: 3, StatCode: "energy"},
},
},
"ac%": {
Code: "ac%",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 2, StatCode: "item_armor_percent"},
},
},
// dmg-min, dmg-max, dmg%, indestruct, and ethereal do not yield stats.
// these properties are used specifically to compute a value.
"dmg-min": {
Code: "dmg-min",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 5},
},
},
"dmg-max": {
Code: "dmg-max",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 6},
},
},
"dmg%": {
Code: "dmg%",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 7},
},
},
"cast1": {
Code: "cast1",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 8, StatCode: "item_fastercastrate"},
},
},
"skilltab": {
Code: "skilltab",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 10, StatCode: "item_addskill_tab"},
},
},
"levelup-skill": {
Code: "levelup-skill",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 11, StatCode: "item_skillonlevelup"},
},
},
"skill-rand": {
Code: "skill-rand",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 12, StatCode: "item_singleskill"},
},
},
"dur%": {
Code: "dur%",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 13, StatCode: "item_maxdurability_percent"},
},
},
"sock": {
Code: "sock",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 14, StatCode: "item_numsockets"},
},
},
"dmg-pois": {
Code: "dmg-pois",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 15, StatCode: "poisonmindam"},
{FunctionID: 16, StatCode: "poisonmaxdam"},
{FunctionID: 17, StatCode: "poisonlength"},
},
},
"charged": {
Code: "charged",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 19, StatCode: "item_charged_skill"},
},
},
"indestruct": {
Code: "indestruct",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 20},
},
},
"pal": {
Code: "pal",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 21, StatCode: "item_addclassskills", Value: 3},
},
},
"oskill": {
Code: "oskill",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 22, StatCode: "item_nonclassskill"},
},
},
"ethereal": {
Code: "ethereal",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 23},
},
},
}
d2datadict.ItemStatCosts = itemStatCosts
d2datadict.CharStats = charStats
d2datadict.SkillDetails = skillDetails
d2datadict.MonStats = monStats
d2datadict.Properties = properties
}
func TestNewProperty(t *testing.T) { //nolint:funlen it's mostly test-case definitions
rand.Seed(time.Now().UTC().UnixNano())
tests := []struct {
propKey string
inputValues []int
expectNumStats int
expectStr []string
}{
{ // fnId 1 + 3
"allstats",
[]int{1, 10},
4,
[]string{
"+# to Strength",
"+# to Dexterity",
"+# to Vitality",
"+# to Energy",
},
},
{ // fnId 2
"ac%",
[]int{1, 10},
1,
[]string{"+#% Enhanced Defense"},
},
{ // fnId 5
// dmg-min, dmg-max, dmg%, indestructable, and ethereal dont have stats!
"dmg-min",
[]int{1, 10},
0,
[]string{""},
},
{ // fnId 6
// dmg-min, dmg-max, dmg%, indestructable, and ethereal dont have stats!
"dmg-max",
[]int{1, 10},
0,
[]string{""},
},
{ // fnId 7
// dmg-min, dmg-max, dmg%, indestructable, and ethereal dont have stats!
"dmg%",
[]int{1, 10},
0,
[]string{""},
},
{ // fnId 8
"cast1",
[]int{1, 10},
1,
[]string{"+#% Faster Cast Rate"},
},
{
"indestruct",
[]int{0, 1},
0,
[]string{""},
},
{
"ethereal",
[]int{0, 1},
0,
[]string{""},
},
{ // fnId 10
"skilltab",
[]int{10, 1, 3},
1,
[]string{"+# to Offensive Auras (Paladin Only)"},
},
{ // fnId 11
"levelup-skill",
[]int{64, 100, 3},
1,
[]string{"#% Chance to cast level # Frozen Orb when you Level-Up"},
},
{ // fnId 12
"skill-rand",
[]int{10, 64, 64},
1,
[]string{"+# to Frozen Orb"},
},
{ // fnId 13
"dur%",
[]int{1, 10},
1,
[]string{"Increase Maximum Durability +#%"},
},
{ // fnId 14
"sock",
[]int{0, 6},
1,
[]string{""},
},
{ // fnId 15, 16, 17
"dmg-pois",
[]int{100, 5, 10},
3,
[]string{
"+# to Minimum Poison Damage",
"+# to Maximum Poison Damage",
"", // length, non-printing
},
},
{ // fnId 19
"charged",
[]int{64, 20, 10},
1,
[]string{"Level # Frozen Orb (#/# Charges)"},
},
{ // fnId 21
"pal",
[]int{1, 5},
1,
[]string{"+# to Paladin Skill Levels"},
},
{ // fnId 22
"oskill",
[]int{64, 1, 5},
1,
[]string{"+# to Frozen Orb"},
},
}
numericToken := "#"
re := regexp.MustCompile(`\d+`)
for testIdx := range tests {
test := &tests[testIdx]
prop := NewProperty(test.propKey, test.inputValues...)
if prop == nil {
t.Error("property is nil")
continue
}
infoFmt := "\r\nProperty `%s`, arguments %v"
infoStr := fmt.Sprintf(infoFmt, prop.record.Code, test.inputValues)
fmt.Println(infoStr)
if len(prop.stats) != test.expectNumStats {
errFmt := "unexpected property stat count: want %v, have %v"
t.Errorf(errFmt, test.expectNumStats, len(prop.stats))
continue
}
switch prop.PropertyType {
case PropertyComputeBoolean:
fmtStr := "\tGot: [Non-printing boolean property] [Bool Value: %v]"
got := fmt.Sprintf(fmtStr, prop.computedBool)
fmt.Println(got)
case PropertyComputeInteger:
fmtStr := "\tGot: [Non-printing integer property] [Int Value: %v]"
got := fmt.Sprintf(fmtStr, prop.computedInt)
fmt.Println(got)
case PropertyComputeStats:
for statIdx := range prop.stats {
stat := prop.stats[statIdx]
expectStr := test.expectStr[statIdx]
statStr := stat.String()
stripped := string(re.ReplaceAll([]byte(statStr), []byte(numericToken)))
if expectStr == "" {
statFmt := "[Non-printing stat] Code: %v, inputValues: %+v"
vals := stat.Values()
valInts := make([]int, len(vals))
for idx := range vals {
valInts[idx] = vals[idx].Int()
}
statStr = fmt.Sprintf(statFmt, stat.Name(), valInts)
got := fmt.Sprintf("\tGot: %s", statStr)
fmt.Println(got)
} else {
got := fmt.Sprintf("\tGot: %s", statStr)
fmt.Println(got)
}
if stripped != expectStr {
expected := fmt.Sprintf("\tExpected: %s", test.expectStr)
t.Error(expected)
}
}
}
}
}