mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-02-04 15:46:51 -05:00
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
This commit is contained in:
parent
4dc0aa0f48
commit
bfd3f1046d
@ -270,6 +270,8 @@ func (a *App) loadDataDict() error {
|
||||
entry.loader(data)
|
||||
}
|
||||
|
||||
d2datadict.LoadItemEquivalencies() // depends on ItemCommon and ItemTypes
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -9,9 +9,11 @@ import (
|
||||
)
|
||||
|
||||
// MagicPrefix stores all of the magic prefix records
|
||||
var MagicPrefix []*ItemAffixCommonRecord //nolint:gochecknoglobals // Currently global by design
|
||||
var MagicPrefix map[string]*ItemAffixCommonRecord //nolint:gochecknoglobals // Currently global by
|
||||
// design
|
||||
// MagicSuffix stores all of the magic suffix records
|
||||
var MagicSuffix []*ItemAffixCommonRecord //nolint:gochecknoglobals // Currently global by design
|
||||
var MagicSuffix map[string]*ItemAffixCommonRecord //nolint:gochecknoglobals // Currently global by
|
||||
// design
|
||||
|
||||
// LoadMagicPrefix loads MagicPrefix.txt
|
||||
func LoadMagicPrefix(file []byte) {
|
||||
@ -48,7 +50,11 @@ func getAffixString(t1 d2enum.ItemAffixSuperType, t2 d2enum.ItemAffixSubType) st
|
||||
return name
|
||||
}
|
||||
|
||||
func loadDictionary(file []byte, superType d2enum.ItemAffixSuperType, subType d2enum.ItemAffixSubType) []*ItemAffixCommonRecord {
|
||||
func loadDictionary(
|
||||
file []byte,
|
||||
superType d2enum.ItemAffixSuperType,
|
||||
subType d2enum.ItemAffixSubType,
|
||||
) map[string]*ItemAffixCommonRecord {
|
||||
d := d2common.LoadDataDictionary(file)
|
||||
records := createItemAffixRecords(d, superType, subType)
|
||||
name := getAffixString(superType, subType)
|
||||
@ -57,8 +63,12 @@ func loadDictionary(file []byte, superType d2enum.ItemAffixSuperType, subType d2
|
||||
return records
|
||||
}
|
||||
|
||||
func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAffixSuperType, subType d2enum.ItemAffixSubType) []*ItemAffixCommonRecord {
|
||||
records := make([]*ItemAffixCommonRecord, 0)
|
||||
func createItemAffixRecords(
|
||||
d *d2common.DataDictionary,
|
||||
superType d2enum.ItemAffixSuperType,
|
||||
subType d2enum.ItemAffixSubType,
|
||||
) map[string]*ItemAffixCommonRecord {
|
||||
records := make(map[string]*ItemAffixCommonRecord)
|
||||
|
||||
for d.Next() {
|
||||
affix := &ItemAffixCommonRecord{
|
||||
@ -82,7 +92,7 @@ func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAff
|
||||
PriceScale: d.Number("multiply"),
|
||||
}
|
||||
|
||||
// modifiers (Property references with parameters to be eval'd)
|
||||
// modifiers (Code references with parameters to be eval'd)
|
||||
for i := 1; i <= 3; i++ {
|
||||
codeKey := fmt.Sprintf("mod%dcode", i)
|
||||
paramKey := fmt.Sprintf("mod%dparam", i)
|
||||
@ -125,7 +135,7 @@ func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAff
|
||||
group := ItemAffixGroups[affix.GroupID]
|
||||
group.addMember(affix)
|
||||
|
||||
records = append(records, affix)
|
||||
records[affix.Name] = affix
|
||||
}
|
||||
if d.Err != nil {
|
||||
panic(d.Err)
|
||||
|
@ -177,6 +177,10 @@ func LoadCommonItems(file []byte, source d2enum.InventoryItemType) map[string]*I
|
||||
}
|
||||
|
||||
rec := createCommonItemRecord(line, mapping, source)
|
||||
if rec.Name == "Expansion" {
|
||||
continue
|
||||
}
|
||||
|
||||
items[rec.Code] = &rec
|
||||
CommonItems[rec.Code] = &rec
|
||||
}
|
||||
@ -392,3 +396,4 @@ func createItemUsageStats(r *[]string, mapping map[string]int) [3]ItemUsageStat
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
@ -246,7 +246,7 @@ func LoadItemTypes(file []byte) {
|
||||
StorePage: d.String("StorePage"),
|
||||
}
|
||||
|
||||
ItemTypes[itemType.Name] = itemType
|
||||
ItemTypes[itemType.Code] = itemType
|
||||
}
|
||||
|
||||
if d.Err != nil {
|
||||
@ -255,3 +255,101 @@ func LoadItemTypes(file []byte) {
|
||||
|
||||
log.Printf("Loaded %d ItemType records", len(ItemTypes))
|
||||
}
|
||||
|
||||
// ItemEquivalenciesByTypeCode describes item equivalencies for ItemTypes
|
||||
var ItemEquivalenciesByTypeCode map[string][]*ItemCommonRecord
|
||||
|
||||
// LoadItemEquivalencies loads a map of ItemType string codes to slices of ItemCommonRecord pointers
|
||||
func LoadItemEquivalencies() {
|
||||
ItemEquivalenciesByTypeCode = make(map[string][]*ItemCommonRecord)
|
||||
|
||||
makeEmptyEquivalencyMaps()
|
||||
|
||||
for icrCode := range CommonItems {
|
||||
commonItem := CommonItems[icrCode]
|
||||
updateEquivalencies(commonItem, ItemTypes[commonItem.Type], nil)
|
||||
|
||||
if commonItem.Type2 != "" { // some items (like gems) have a secondary type
|
||||
updateEquivalencies(commonItem, ItemTypes[commonItem.Type2], nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func makeEmptyEquivalencyMaps() {
|
||||
for typeCode := range ItemTypes {
|
||||
code := []string{
|
||||
typeCode,
|
||||
ItemTypes[typeCode].Equiv1,
|
||||
ItemTypes[typeCode].Equiv2,
|
||||
}
|
||||
|
||||
for _, str := range code {
|
||||
if str == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if ItemEquivalenciesByTypeCode[str] == nil {
|
||||
ItemEquivalenciesByTypeCode[str] = make([]*ItemCommonRecord, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateEquivalencies(icr *ItemCommonRecord, itemType *ItemTypeRecord, checked []string) {
|
||||
if itemType.Code == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if checked == nil {
|
||||
checked = make([]string, 0)
|
||||
}
|
||||
|
||||
checked = append(checked, itemType.Code)
|
||||
|
||||
if !itemEquivPresent(icr, ItemEquivalenciesByTypeCode[itemType.Code]) {
|
||||
ItemEquivalenciesByTypeCode[itemType.Code] = append(ItemEquivalenciesByTypeCode[itemType.Code], icr)
|
||||
}
|
||||
|
||||
if itemType.Equiv1 != "" {
|
||||
updateEquivalencies(icr, ItemTypes[itemType.Equiv1], checked)
|
||||
}
|
||||
|
||||
if itemType.Equiv2 != "" {
|
||||
updateEquivalencies(icr, ItemTypes[itemType.Equiv2], checked)
|
||||
}
|
||||
}
|
||||
|
||||
func itemEquivPresent(icr *ItemCommonRecord, list []*ItemCommonRecord) bool {
|
||||
for idx := range list {
|
||||
if list[idx] == icr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var itemCommonTypeLookup map[*ItemCommonRecord][]string
|
||||
|
||||
func FindEquivalentTypesByItemCommonRecord(icr *ItemCommonRecord) []string {
|
||||
if itemCommonTypeLookup == nil {
|
||||
itemCommonTypeLookup = make(map[*ItemCommonRecord][]string)
|
||||
}
|
||||
|
||||
// the first lookup generates the lookup table entry, next time will just use the table
|
||||
if itemCommonTypeLookup[icr] == nil {
|
||||
itemCommonTypeLookup[icr] = make([]string, 0)
|
||||
|
||||
for code := range ItemEquivalenciesByTypeCode {
|
||||
icrList := ItemEquivalenciesByTypeCode[code]
|
||||
for idx := range icrList {
|
||||
if icr == icrList[idx] {
|
||||
itemCommonTypeLookup[icr] = append(itemCommonTypeLookup[icr], code)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return itemCommonTypeLookup[icr]
|
||||
}
|
||||
|
@ -1,24 +1,23 @@
|
||||
package d2datadict
|
||||
|
||||
import (
|
||||
"log"
|
||||
"log"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
)
|
||||
|
||||
type stat struct {
|
||||
SetID int
|
||||
Value int
|
||||
type PropertyStatRecord struct {
|
||||
SetID int
|
||||
Value int
|
||||
FunctionID int
|
||||
StatCode string
|
||||
StatCode string
|
||||
}
|
||||
|
||||
// PropertyRecord is a representation of a single row of gems.txt
|
||||
// it describes the properties of socketable items
|
||||
// PropertyRecord is a representation of a single row of properties.txt
|
||||
type PropertyRecord struct {
|
||||
Code string
|
||||
Code string
|
||||
Active string
|
||||
Stats [7]*stat
|
||||
Stats [7]*PropertyStatRecord
|
||||
}
|
||||
|
||||
// Properties stores all of the PropertyRecords
|
||||
@ -32,50 +31,50 @@ func LoadProperties(file []byte) {
|
||||
d := d2common.LoadDataDictionary(file)
|
||||
for d.Next() {
|
||||
prop := &PropertyRecord{
|
||||
Code: d.String("code"),
|
||||
Code: d.String("code"),
|
||||
Active: d.String("*done"),
|
||||
Stats: [7]*stat{
|
||||
Stats: [7]*PropertyStatRecord{
|
||||
{
|
||||
SetID: d.Number("set1"),
|
||||
Value: d.Number("val1"),
|
||||
SetID: d.Number("set1"),
|
||||
Value: d.Number("val1"),
|
||||
FunctionID: d.Number("func1"),
|
||||
StatCode: d.String("stat1"),
|
||||
StatCode: d.String("stat1"),
|
||||
},
|
||||
{
|
||||
SetID: d.Number("set2"),
|
||||
Value: d.Number("val2"),
|
||||
SetID: d.Number("set2"),
|
||||
Value: d.Number("val2"),
|
||||
FunctionID: d.Number("func2"),
|
||||
StatCode: d.String("stat2"),
|
||||
StatCode: d.String("stat2"),
|
||||
},
|
||||
{
|
||||
SetID: d.Number("set3"),
|
||||
Value: d.Number("val3"),
|
||||
SetID: d.Number("set3"),
|
||||
Value: d.Number("val3"),
|
||||
FunctionID: d.Number("func3"),
|
||||
StatCode: d.String("stat3"),
|
||||
StatCode: d.String("stat3"),
|
||||
},
|
||||
{
|
||||
SetID: d.Number("set4"),
|
||||
Value: d.Number("val4"),
|
||||
SetID: d.Number("set4"),
|
||||
Value: d.Number("val4"),
|
||||
FunctionID: d.Number("func4"),
|
||||
StatCode: d.String("stat4"),
|
||||
StatCode: d.String("stat4"),
|
||||
},
|
||||
{
|
||||
SetID: d.Number("set5"),
|
||||
Value: d.Number("val5"),
|
||||
SetID: d.Number("set5"),
|
||||
Value: d.Number("val5"),
|
||||
FunctionID: d.Number("func5"),
|
||||
StatCode: d.String("stat5"),
|
||||
StatCode: d.String("stat5"),
|
||||
},
|
||||
{
|
||||
SetID: d.Number("set6"),
|
||||
Value: d.Number("val6"),
|
||||
SetID: d.Number("set6"),
|
||||
Value: d.Number("val6"),
|
||||
FunctionID: d.Number("func6"),
|
||||
StatCode: d.String("stat6"),
|
||||
StatCode: d.String("stat6"),
|
||||
},
|
||||
{
|
||||
SetID: d.Number("set7"),
|
||||
Value: d.Number("val7"),
|
||||
SetID: d.Number("set7"),
|
||||
Value: d.Number("val7"),
|
||||
FunctionID: d.Number("func7"),
|
||||
StatCode: d.String("stat7"),
|
||||
StatCode: d.String("stat7"),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -88,4 +87,3 @@ func LoadProperties(file []byte) {
|
||||
|
||||
log.Printf("Loaded %d Property records", len(Properties))
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,31 @@
|
||||
package d2datadict
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"log"
|
||||
)
|
||||
|
||||
const (
|
||||
numPropertiesOnSetItem = 9
|
||||
numBonusPropertiesOnSetItem = 5
|
||||
bonusToken1 = "a"
|
||||
bonusToken2 = "b"
|
||||
propCodeFmt = "prop%d"
|
||||
propParamFmt = "par%d"
|
||||
propMinFmt = "min%d"
|
||||
propMaxFmt = "max%d"
|
||||
bonusCodeFmt = "aprop%d%s"
|
||||
bonusParamFmt = "apar%d%s"
|
||||
bonusMinFmt = "amin%d%s"
|
||||
bonusMaxFmt = "amax%d%s"
|
||||
)
|
||||
|
||||
// SetItemRecord represents a set item
|
||||
type SetItemRecord struct {
|
||||
// StringTableKey (index)
|
||||
// SetItemKey (index)
|
||||
// string key to item's name in a .tbl file
|
||||
StringTableKey string
|
||||
SetItemKey string
|
||||
|
||||
// SetKey (set)
|
||||
// string key to the index field in Sets.txt - the set the item is a part of.
|
||||
@ -90,63 +106,37 @@ type SetItemRecord struct {
|
||||
// on a set item. See the appendix for further details about this field's effects.
|
||||
AddFn int
|
||||
|
||||
// Prop (prop1 to prop9)
|
||||
// An ID pointer of a property from Properties.txt,
|
||||
// these columns control each of the nine different fixed (
|
||||
// blue) modifiers a set item can grant you at most.
|
||||
Prop [9]string
|
||||
// Properties are a propert code, parameter, min, max for generating an item propert
|
||||
Properties [numPropertiesOnSetItem]*SetItemProperty
|
||||
|
||||
// Par (par1 to par9)
|
||||
// The parameter passed on to the associated property, this is used to pass skill IDs, state IDs,
|
||||
// monster IDs, montype IDs and the like on to the properties that require them,
|
||||
// these fields support calculations.
|
||||
Par [9]int
|
||||
// SetPropertiesLevel1 is the first version of bonus properties for the set
|
||||
SetPropertiesLevel1 [numBonusPropertiesOnSetItem]*SetItemProperty
|
||||
|
||||
// Min, Max (min1 to min9, max1 to max9)
|
||||
// Minimum value to assign to the associated (blue) property.
|
||||
// Certain properties have special interpretations based on stat encoding (e.g.
|
||||
// chance-to-cast and charged skills). See the File Guide for Properties.txt and ItemStatCost.
|
||||
// txt for further details.
|
||||
Min [9]int
|
||||
Max [9]int
|
||||
// SetPropertiesLevel2 is the second version of bonus properties for the set
|
||||
SetPropertiesLevel2 [numBonusPropertiesOnSetItem]*SetItemProperty
|
||||
}
|
||||
|
||||
// APropA, APropB (aprop1a,aprop1b to aprop5a,aprop5b)
|
||||
// An ID pointer of a property from Properties.txt,
|
||||
// these columns control each of the five pairs of different variable (
|
||||
// green) modifiers a set item can grant you at most.
|
||||
APropA [5]string
|
||||
APropB [5]string
|
||||
|
||||
// AParA, AParB (apar1a,apar1b to apar5a,apar5b)
|
||||
// The parameter passed on to the associated property, this is used to pass skill IDs, state IDs,
|
||||
// monster IDs, montype IDs and the like on to the properties that require them,
|
||||
// these fields support calculations.
|
||||
AParA [5]int
|
||||
AParB [5]int
|
||||
|
||||
// AMinA, AMinB, AMaxA, AMaxB (amin1a,amin1b to amin5a,amin5b)
|
||||
// Minimum value to assign to the associated property.
|
||||
// Certain properties have special interpretations based on stat encoding (e.g.
|
||||
// chance-to-cast and charged skills). See the File Guide for Properties.txt and ItemStatCost.
|
||||
// txt for further details.
|
||||
AMinA [5]int
|
||||
AMinB [5]int
|
||||
AMaxA [5]int
|
||||
AMaxB [5]int
|
||||
// SetItemProperty is describes a property of a set item
|
||||
type SetItemProperty struct {
|
||||
Code string
|
||||
Parameter string // depending on the property, this may be an int (usually), or a string
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
|
||||
// SetItems holds all of the SetItemRecords
|
||||
var SetItems []*SetItemRecord //nolint:gochecknoglobals // Currently global by design, only written once
|
||||
var SetItems map[string]*SetItemRecord //nolint:gochecknoglobals // Currently global by design,
|
||||
// only written once
|
||||
|
||||
// LoadSetItems loads all of the SetItemRecords from SetItems.txt
|
||||
func LoadSetItems(file []byte) {
|
||||
SetItems = make([]*SetItemRecord, 0)
|
||||
SetItems = make(map[string]*SetItemRecord)
|
||||
|
||||
d := d2common.LoadDataDictionary(file)
|
||||
|
||||
for d.Next() {
|
||||
record := &SetItemRecord{
|
||||
StringTableKey: d.String("index"),
|
||||
SetItemKey: d.String("index"),
|
||||
SetKey: d.String("set"),
|
||||
ItemCode: d.String("item"),
|
||||
Rarity: d.Number("rarity"),
|
||||
@ -162,109 +152,47 @@ func LoadSetItems(file []byte) {
|
||||
CostMult: d.Number("cost mult"),
|
||||
CostAdd: d.Number("cost add"),
|
||||
AddFn: d.Number("add func"),
|
||||
Prop: [9]string{
|
||||
d.String("prop1"),
|
||||
d.String("prop2"),
|
||||
d.String("prop3"),
|
||||
d.String("prop4"),
|
||||
d.String("prop5"),
|
||||
d.String("prop6"),
|
||||
d.String("prop7"),
|
||||
d.String("prop8"),
|
||||
d.String("prop9"),
|
||||
},
|
||||
Par: [9]int{
|
||||
d.Number("par1"),
|
||||
d.Number("par2"),
|
||||
d.Number("par3"),
|
||||
d.Number("par4"),
|
||||
d.Number("par5"),
|
||||
d.Number("par6"),
|
||||
d.Number("par7"),
|
||||
d.Number("par8"),
|
||||
d.Number("par9"),
|
||||
},
|
||||
Min: [9]int{
|
||||
d.Number("min1"),
|
||||
d.Number("min2"),
|
||||
d.Number("min3"),
|
||||
d.Number("min4"),
|
||||
d.Number("min5"),
|
||||
d.Number("min6"),
|
||||
d.Number("min7"),
|
||||
d.Number("min8"),
|
||||
d.Number("min9"),
|
||||
},
|
||||
Max: [9]int{
|
||||
d.Number("max1"),
|
||||
d.Number("max2"),
|
||||
d.Number("max3"),
|
||||
d.Number("max4"),
|
||||
d.Number("max5"),
|
||||
d.Number("max6"),
|
||||
d.Number("max7"),
|
||||
d.Number("max8"),
|
||||
d.Number("max9"),
|
||||
},
|
||||
APropA: [5]string{
|
||||
d.String("aprop1a"),
|
||||
d.String("aprop2a"),
|
||||
d.String("aprop3a"),
|
||||
d.String("aprop4a"),
|
||||
d.String("aprop5a"),
|
||||
},
|
||||
APropB: [5]string{
|
||||
d.String("aprop1b"),
|
||||
d.String("aprop2b"),
|
||||
d.String("aprop3b"),
|
||||
d.String("aprop4b"),
|
||||
d.String("aprop5b"),
|
||||
},
|
||||
AParA: [5]int{
|
||||
d.Number("apar1a"),
|
||||
d.Number("apar2a"),
|
||||
d.Number("apar3a"),
|
||||
d.Number("apar4a"),
|
||||
d.Number("apar5a"),
|
||||
},
|
||||
AParB: [5]int{
|
||||
d.Number("apar1b"),
|
||||
d.Number("apar2b"),
|
||||
d.Number("apar3b"),
|
||||
d.Number("apar4b"),
|
||||
d.Number("apar5b"),
|
||||
},
|
||||
AMinA: [5]int{
|
||||
d.Number("amin1a"),
|
||||
d.Number("amin2a"),
|
||||
d.Number("amin3a"),
|
||||
d.Number("amin4a"),
|
||||
d.Number("amin5a"),
|
||||
},
|
||||
AMinB: [5]int{
|
||||
d.Number("amin1b"),
|
||||
d.Number("amin2b"),
|
||||
d.Number("amin3b"),
|
||||
d.Number("amin4b"),
|
||||
d.Number("amin5b"),
|
||||
},
|
||||
AMaxA: [5]int{
|
||||
d.Number("amax1a"),
|
||||
d.Number("amax2a"),
|
||||
d.Number("amax3a"),
|
||||
d.Number("amax4a"),
|
||||
d.Number("amax5a"),
|
||||
},
|
||||
AMaxB: [5]int{
|
||||
d.Number("amax1b"),
|
||||
d.Number("amax2b"),
|
||||
d.Number("amax3b"),
|
||||
d.Number("amax4b"),
|
||||
d.Number("amax5b"),
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
SetItems = append(SetItems, record)
|
||||
// normal properties
|
||||
props := [numPropertiesOnSetItem]*SetItemProperty{}
|
||||
|
||||
for idx := 0; idx < numPropertiesOnSetItem; idx++ {
|
||||
num := idx + 1
|
||||
props[idx] = &SetItemProperty{
|
||||
d.String(fmt.Sprintf(propCodeFmt, num)),
|
||||
d.String(fmt.Sprintf(propParamFmt, num)),
|
||||
d.Number(fmt.Sprintf(propMinFmt, num)),
|
||||
d.Number(fmt.Sprintf(propMaxFmt, num)),
|
||||
}
|
||||
}
|
||||
|
||||
// set bonus properties
|
||||
bonus1 := [numBonusPropertiesOnSetItem]*SetItemProperty{}
|
||||
bonus2 := [numBonusPropertiesOnSetItem]*SetItemProperty{}
|
||||
|
||||
for idx := 0; idx < numBonusPropertiesOnSetItem; idx++ {
|
||||
num := idx + 1
|
||||
|
||||
bonus1[idx] = &SetItemProperty{
|
||||
d.String(fmt.Sprintf(bonusCodeFmt, num, bonusToken1)),
|
||||
d.String(fmt.Sprintf(bonusParamFmt, num, bonusToken1)),
|
||||
d.Number(fmt.Sprintf(bonusMinFmt, num,bonusToken1)),
|
||||
d.Number(fmt.Sprintf(bonusMaxFmt, num, bonusToken1)),
|
||||
}
|
||||
|
||||
bonus2[idx] = &SetItemProperty{
|
||||
d.String(fmt.Sprintf(bonusCodeFmt, num, bonusToken2)),
|
||||
d.String(fmt.Sprintf(bonusParamFmt, num, bonusToken2)),
|
||||
d.Number(fmt.Sprintf(bonusMinFmt, num,bonusToken2)),
|
||||
d.Number(fmt.Sprintf(bonusMaxFmt, num, bonusToken2)),
|
||||
}
|
||||
}
|
||||
|
||||
record.Properties = props
|
||||
|
||||
SetItems[record.SetItemKey] = record
|
||||
}
|
||||
|
||||
if d.Err != nil {
|
||||
|
@ -2,15 +2,45 @@ package d2datadict
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"log"
|
||||
)
|
||||
|
||||
const (
|
||||
numTreasures = 10
|
||||
treasureItemFmt = "Item%d"
|
||||
treasureProbFmt = "Prob%d"
|
||||
maxTreasuresPerRecord = 10
|
||||
treasureItemFmt = "Item%d"
|
||||
treasureProbFmt = "Prob%d"
|
||||
)
|
||||
|
||||
// TreasureDropType indicates the drop type of the treasure
|
||||
type TreasureDropType int
|
||||
|
||||
const (
|
||||
// TreasureNone is default bad case, but nothing should have this
|
||||
TreasureNone TreasureDropType = iota
|
||||
|
||||
// TreasureGold indicates that the treasure drop type is for gold
|
||||
TreasureGold
|
||||
|
||||
// indicates that the drop type resolves directly to an ItemCommonRecord
|
||||
TreasureWeapon
|
||||
TreasureArmor
|
||||
TreasureMisc
|
||||
|
||||
// indicates that the code is for a dynamic item record, because the treasure code has
|
||||
// and item level appended to it. this is for things like `armo63` or `weap24` which does not
|
||||
// explicitly have an item record that matches this code, but we need to resolve this
|
||||
TreasureWeaponDynamic
|
||||
TreasureArmorDynamic
|
||||
TreasureMiscDynamic
|
||||
)
|
||||
|
||||
const (
|
||||
GoldMultDropCodeStr string = "gld,mul="
|
||||
GoldDropCodeStr = "gld"
|
||||
WeaponDropCodeStr = "weap"
|
||||
ArmorDropCodeStr = "armo"
|
||||
MiscDropCodeStr = "misc"
|
||||
)
|
||||
|
||||
// TreasureClassRecord represents a rule for item drops in diablo 2
|
||||
@ -28,12 +58,13 @@ type TreasureClassRecord struct {
|
||||
}
|
||||
|
||||
// Treasure describes a treasure to drop
|
||||
// the key is either a reference to an item, or to another treasure class
|
||||
// the Name is either a reference to an item, or to another treasure class
|
||||
type Treasure struct {
|
||||
Name string
|
||||
Code string
|
||||
Probability int
|
||||
}
|
||||
|
||||
// TreasureClass contains all of the TreasureClassRecords
|
||||
var TreasureClass map[string]*TreasureClassRecord //nolint:gochecknoglobals // Currently global by design
|
||||
|
||||
// LoadTreasureClassRecords loads treasure class records from TreasureClassEx.txt
|
||||
@ -56,7 +87,11 @@ func LoadTreasureClassRecords(file []byte) {
|
||||
FreqNoDrop: d.Number("NoDrop"),
|
||||
}
|
||||
|
||||
for treasureIdx := 0; treasureIdx < numTreasures; treasureIdx++ {
|
||||
if record.Name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
for treasureIdx := 0; treasureIdx < maxTreasuresPerRecord; treasureIdx++ {
|
||||
treasureColumnKey := fmt.Sprintf(treasureItemFmt, treasureIdx+1)
|
||||
probColumnKey := fmt.Sprintf(treasureProbFmt, treasureIdx+1)
|
||||
|
||||
@ -68,16 +103,15 @@ func LoadTreasureClassRecords(file []byte) {
|
||||
prob := d.Number(probColumnKey)
|
||||
|
||||
treasure := &Treasure{
|
||||
Name: treasureName,
|
||||
Code: treasureName,
|
||||
Probability: prob,
|
||||
}
|
||||
|
||||
if record.Treasures == nil {
|
||||
record.Treasures = []*Treasure{treasure}
|
||||
continue
|
||||
} else {
|
||||
record.Treasures = append(record.Treasures, treasure)
|
||||
}
|
||||
|
||||
record.Treasures = append(record.Treasures, treasure)
|
||||
}
|
||||
|
||||
TreasureClass[record.Name] = record
|
||||
|
@ -42,8 +42,8 @@ type UniqueItemRecord struct {
|
||||
|
||||
// UniqueItemProperty is describes a property of a unique item
|
||||
type UniqueItemProperty struct {
|
||||
Property string
|
||||
Parameter d2common.CalcString // depending on the property, this may be an int (usually), or a string
|
||||
Code string
|
||||
Parameter string // depending on the property, this may be an int (usually), or a string
|
||||
Min int
|
||||
Max int
|
||||
}
|
||||
@ -105,8 +105,8 @@ func createUniqueItemRecord(r []string) UniqueItemRecord {
|
||||
|
||||
func createUniqueItemProperty(r *[]string, inc func() int) UniqueItemProperty {
|
||||
result := UniqueItemProperty{
|
||||
Property: (*r)[inc()],
|
||||
Parameter: d2common.CalcString((*r)[inc()]),
|
||||
Code: (*r)[inc()],
|
||||
Parameter: (*r)[inc()],
|
||||
Min: d2common.StringToInt(d2common.EmptyToZero((*r)[inc()])),
|
||||
Max: d2common.StringToInt(d2common.EmptyToZero((*r)[inc()])),
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ type EquippedSlot int
|
||||
|
||||
// Equipped slot ID's
|
||||
const (
|
||||
EquippedSlotHead EquippedSlot = iota + 1
|
||||
EquippedSlotNone EquippedSlot = iota
|
||||
EquippedSlotHead
|
||||
EquippedSlotTorso
|
||||
EquippedSlotLegs
|
||||
EquippedSlotRightArm
|
||||
|
12
d2core/d2item/context.go
Normal file
12
d2core/d2item/context.go
Normal file
@ -0,0 +1,12 @@
|
||||
package d2item
|
||||
|
||||
import "github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||
|
||||
// StatContext is anything which has a `StatList` method which yields a StatList.
|
||||
// This is used for resolving stat dependencies for showing actual values, like
|
||||
// stats that are based off of the current character level
|
||||
type StatContext interface {
|
||||
Equipper
|
||||
BaseStatList() d2stats.StatList
|
||||
StatList() d2stats.StatList
|
||||
}
|
19
d2core/d2item/diablo2item/diablo2item.go
Normal file
19
d2core/d2item/diablo2item/diablo2item.go
Normal file
@ -0,0 +1,19 @@
|
||||
package diablo2item
|
||||
|
||||
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||
|
||||
// NewProperty creates a property
|
||||
func NewProperty(code string, values ...int) *Property {
|
||||
record := d2datadict.Properties[code]
|
||||
|
||||
if record == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := &Property{
|
||||
record: record,
|
||||
inputParams: values,
|
||||
}
|
||||
|
||||
return result.init()
|
||||
}
|
3
d2core/d2item/diablo2item/doc.go
Normal file
3
d2core/d2item/diablo2item/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package Item provides the Diablo 2 implementation of items for
|
||||
// the OpenDiablo2 interfaces
|
||||
package diablo2item
|
735
d2core/d2item/diablo2item/item.go
Normal file
735
d2core/d2item/diablo2item/item.go
Normal file
@ -0,0 +1,735 @@
|
||||
package diablo2item
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2item"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats/diablo2stats"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// PropertyPool is used for separating properties by their source
|
||||
type PropertyPool int
|
||||
|
||||
// Property pools
|
||||
const (
|
||||
PropertyPoolPrefix PropertyPool = iota
|
||||
PropertyPoolSuffix
|
||||
PropertyPoolUnique
|
||||
PropertyPoolSetItem
|
||||
PropertyPoolSet
|
||||
)
|
||||
|
||||
// for handling special cases
|
||||
const (
|
||||
jewelItemCode = "jew"
|
||||
propertyEthereal = "ethereal"
|
||||
propertyIndestructable = "indestruct"
|
||||
)
|
||||
|
||||
|
||||
const (
|
||||
magicItemPrefixMax = 1
|
||||
magicItemSuffixMax = 1
|
||||
rareItemPrefixMax = 3
|
||||
rareItemSuffixMax = 3
|
||||
rareJewelPrefixMax = 3
|
||||
rareJewelSuffixMax = 3
|
||||
rareJewelAffixMax = 4
|
||||
)
|
||||
|
||||
// static check to ensure Item implements Item
|
||||
var _ d2item.Item = &Item{}
|
||||
|
||||
type Item struct {
|
||||
name string
|
||||
Seed int64
|
||||
rand *rand.Rand // non-global rand instance for re-generating the item
|
||||
|
||||
slotType d2enum.EquippedSlot
|
||||
|
||||
TypeCode string
|
||||
CommonCode string
|
||||
UniqueCode string
|
||||
SetCode string
|
||||
SetItemCode string
|
||||
PrefixCodes []string
|
||||
SuffixCodes []string
|
||||
|
||||
properties map[PropertyPool][]*Property
|
||||
statContext d2item.StatContext
|
||||
statList d2stats.StatList
|
||||
uniqueStatList d2stats.StatList
|
||||
setItemStatList d2stats.StatList
|
||||
|
||||
attributes *itemAttributes
|
||||
|
||||
sockets []*d2item.Item // there will be checks for handling the craziness this might entail
|
||||
}
|
||||
|
||||
type itemAttributes struct {
|
||||
worldSprite *d2ui.Sprite
|
||||
inventorySprite *d2ui.Sprite
|
||||
|
||||
damageOneHand minMaxEnhanceable
|
||||
damageTwoHand minMaxEnhanceable
|
||||
damageMissile minMaxEnhanceable
|
||||
stackSize minMaxEnhanceable
|
||||
durability minMaxEnhanceable
|
||||
|
||||
personalization string
|
||||
|
||||
quality int
|
||||
defense int
|
||||
currentStackSize int
|
||||
currentDurability int
|
||||
baseItemLevel int
|
||||
requiredLevel int
|
||||
numSockets int
|
||||
requirementsEnhancement int
|
||||
requiredStrength int
|
||||
requiredDexterity int
|
||||
classSpecific d2enum.Hero
|
||||
|
||||
durable bool // some items specify that they have no durability
|
||||
indestructable bool
|
||||
ethereal bool
|
||||
throwable bool
|
||||
}
|
||||
|
||||
type minMaxEnhanceable struct {
|
||||
min int
|
||||
max int
|
||||
enhance int
|
||||
}
|
||||
|
||||
// Name returns the item name
|
||||
func (i *Item) Name() string {
|
||||
return i.name
|
||||
}
|
||||
|
||||
// Context returns the statContext that is being used to evaluate stats. for example,
|
||||
// stats which are based on character level will be evaluated with the player
|
||||
// as the statContext, as the player stat list will contain stats that describe the
|
||||
// character level
|
||||
func (i *Item) Context() d2item.StatContext {
|
||||
return i.statContext
|
||||
}
|
||||
|
||||
// SetContext sets the statContext for evaluating item stats
|
||||
func (i *Item) SetContext(ctx d2item.StatContext) {
|
||||
i.statContext = ctx
|
||||
}
|
||||
|
||||
// ItemType returns the type of item
|
||||
func (i *Item) ItemType() string {
|
||||
return i.TypeCode
|
||||
}
|
||||
|
||||
// ItemLevel returns the level of item
|
||||
func (i *Item) ItemLevel() int {
|
||||
return i.attributes.baseItemLevel
|
||||
}
|
||||
|
||||
// TypeRecord returns the ItemTypeRecord of the item
|
||||
func (i *Item) TypeRecord() *d2datadict.ItemTypeRecord {
|
||||
return d2datadict.ItemTypes[i.TypeCode]
|
||||
}
|
||||
|
||||
// CommonRecord returns the ItemCommonRecord of the item
|
||||
func (i *Item) CommonRecord() *d2datadict.ItemCommonRecord {
|
||||
return d2datadict.CommonItems[i.CommonCode]
|
||||
}
|
||||
|
||||
// UniqueRecord returns the UniqueItemRecord of the item
|
||||
func (i *Item) UniqueRecord() *d2datadict.UniqueItemRecord {
|
||||
return d2datadict.UniqueItems[i.UniqueCode]
|
||||
}
|
||||
|
||||
// SetRecord returns the SetRecord of the item
|
||||
func (i *Item) SetRecord() *d2datadict.SetRecord {
|
||||
return d2datadict.SetRecords[i.SetCode]
|
||||
}
|
||||
|
||||
// SetItemRecord returns the SetRecord of the item
|
||||
func (i *Item) SetItemRecord() *d2datadict.SetItemRecord {
|
||||
return d2datadict.SetItems[i.SetItemCode]
|
||||
}
|
||||
|
||||
// PrefixRecords returns the ItemAffixCommonRecords of the prefixes of the item
|
||||
func (i *Item) PrefixRecords() []*d2datadict.ItemAffixCommonRecord {
|
||||
return affixRecords(i.PrefixCodes, d2datadict.MagicPrefix)
|
||||
}
|
||||
|
||||
// PrefixRecords returns the ItemAffixCommonRecords of the prefixes of the item
|
||||
func (i *Item) SuffixRecords() []*d2datadict.ItemAffixCommonRecord {
|
||||
return affixRecords(i.SuffixCodes, d2datadict.MagicSuffix)
|
||||
}
|
||||
|
||||
func affixRecords(
|
||||
fromCodes []string,
|
||||
affixes map[string]*d2datadict.ItemAffixCommonRecord,
|
||||
) []*d2datadict.ItemAffixCommonRecord {
|
||||
if len(fromCodes) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]*d2datadict.ItemAffixCommonRecord, len(fromCodes))
|
||||
|
||||
for idx, code := range fromCodes {
|
||||
rec := affixes[code]
|
||||
result[idx] = rec
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
// SlotType returns the slot type (where it can be equipped)
|
||||
func (i *Item) SlotType() d2enum.EquippedSlot {
|
||||
return i.slotType
|
||||
}
|
||||
|
||||
// StatList returns the evaluated stat list
|
||||
func (i *Item) StatList() d2stats.StatList {
|
||||
return i.statList
|
||||
}
|
||||
|
||||
// Description returns the full description string for the item
|
||||
func (i *Item) Description() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// applyDropModifier attempts to find the necessary set, unique, or
|
||||
// affix records, depending on the drop modifier given. If an unsupported
|
||||
// drop modifier is supplied, it will attempt to reconcile by picked
|
||||
// magic affixes as if it were a rare.
|
||||
func (i *Item) applyDropModifier(modifier DropModifier) {
|
||||
|
||||
modifier = i.sanitizeDropModifier(modifier)
|
||||
|
||||
switch modifier {
|
||||
case DropModifierUnique:
|
||||
i.pickUniqueRecord()
|
||||
|
||||
if i.UniqueRecord() == nil {
|
||||
i.applyDropModifier(DropModifierRare)
|
||||
return
|
||||
}
|
||||
case DropModifierSet:
|
||||
i.pickSetRecords()
|
||||
|
||||
if i.SetRecord() == nil || i.SetItemRecord() == nil {
|
||||
i.applyDropModifier(DropModifierRare)
|
||||
return
|
||||
}
|
||||
case DropModifierRare, DropModifierMagic:
|
||||
// the method of picking stays the same for magic/rare
|
||||
// but magic gets to pick more, and jewels have a special
|
||||
// way of picking affixes
|
||||
i.pickMagicAffixes(modifier)
|
||||
case DropModifierNone:
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Item) sanitizeDropModifier(modifier DropModifier) DropModifier {
|
||||
if i.TypeRecord() == nil {
|
||||
i.TypeCode = i.CommonRecord().Type
|
||||
}
|
||||
|
||||
// should this item always be normal?
|
||||
if i.TypeRecord().Normal {
|
||||
modifier = DropModifierNone
|
||||
}
|
||||
|
||||
// should this item always be magic?
|
||||
if i.TypeRecord().Magic {
|
||||
modifier = DropModifierMagic
|
||||
}
|
||||
|
||||
// if it isn't allowed to be rare, force it to be magic
|
||||
if modifier == DropModifierRare && !i.TypeRecord().Rare {
|
||||
modifier = DropModifierMagic
|
||||
}
|
||||
|
||||
return modifier
|
||||
}
|
||||
|
||||
func (i *Item) pickUniqueRecord() {
|
||||
matches := findMatchingUniqueRecords(i.CommonRecord())
|
||||
if len(matches) > 0 {
|
||||
match := matches[i.rand.Intn(len(matches))]
|
||||
i.UniqueCode = match.Code
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Item) pickSetRecords() {
|
||||
if matches := findMatchingSetItemRecords(i.CommonRecord()); len(matches) > 0 {
|
||||
picked := matches[i.rand.Intn(len(matches))]
|
||||
i.SetItemCode = picked.SetItemKey
|
||||
|
||||
if rec := i.SetItemRecord(); rec != nil {
|
||||
i.SetCode = rec.SetKey
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Item) pickMagicAffixes(mod DropModifier) {
|
||||
if i.PrefixCodes == nil {
|
||||
i.PrefixCodes = make([]string, 0)
|
||||
}
|
||||
|
||||
if i.SuffixCodes == nil {
|
||||
i.SuffixCodes = make([]string, 0)
|
||||
}
|
||||
|
||||
totalAffixes, numSuffixes, numPrefixes := 0, 0, 0
|
||||
|
||||
switch mod {
|
||||
case DropModifierRare:
|
||||
if i.CommonRecord().Type == jewelItemCode {
|
||||
numPrefixes, numSuffixes = rareJewelPrefixMax, rareJewelSuffixMax
|
||||
totalAffixes = rareJewelAffixMax
|
||||
} else {
|
||||
numPrefixes, numSuffixes = rareItemPrefixMax, rareItemSuffixMax
|
||||
totalAffixes = numPrefixes + numSuffixes
|
||||
}
|
||||
case DropModifierMagic:
|
||||
numPrefixes, numSuffixes = magicItemPrefixMax, magicItemSuffixMax
|
||||
totalAffixes = numPrefixes + numSuffixes
|
||||
}
|
||||
|
||||
i.pickMagicPrefixes(numPrefixes, totalAffixes)
|
||||
i.pickMagicSuffixes(numSuffixes, totalAffixes)
|
||||
}
|
||||
|
||||
func (i *Item) pickMagicPrefixes(max, totalMax int) {
|
||||
for numPicks := 0; numPicks < max; numPicks++ {
|
||||
matches := findMatchingAffixes(i.CommonRecord(), d2datadict.MagicPrefix)
|
||||
|
||||
if rollPrefix := i.rand.Intn(2); rollPrefix > 0 {
|
||||
affixCount := len(i.PrefixRecords()) + len(i.SuffixRecords())
|
||||
if len(i.PrefixRecords()) > max || affixCount > totalMax {
|
||||
break
|
||||
}
|
||||
|
||||
if len(matches) > 0 {
|
||||
picked := matches[i.rand.Intn(len(matches))]
|
||||
i.PrefixCodes = append(i.PrefixCodes, picked.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Item) pickMagicSuffixes(max, totalMax int) {
|
||||
for numPicks := 0; numPicks < max; numPicks++ {
|
||||
matches := findMatchingAffixes(i.CommonRecord(), d2datadict.MagicSuffix)
|
||||
|
||||
if rollSuffix := i.rand.Intn(2); rollSuffix > 0 {
|
||||
affixCount := len(i.PrefixRecords()) + len(i.SuffixRecords())
|
||||
if len(i.PrefixRecords()) > max || affixCount > totalMax {
|
||||
break
|
||||
}
|
||||
|
||||
if len(matches) > 0 {
|
||||
picked := matches[i.rand.Intn(len(matches))]
|
||||
i.SuffixCodes = append(i.SuffixCodes, picked.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Item) generateAllProperties() {
|
||||
if i.attributes == nil {
|
||||
i.attributes = &itemAttributes{}
|
||||
}
|
||||
|
||||
// these will get updated by any generated properties
|
||||
i.attributes.ethereal = false
|
||||
i.attributes.indestructable = false
|
||||
|
||||
pools := []PropertyPool{
|
||||
PropertyPoolPrefix,
|
||||
PropertyPoolSuffix,
|
||||
PropertyPoolUnique,
|
||||
PropertyPoolSetItem,
|
||||
PropertyPoolSet,
|
||||
}
|
||||
|
||||
for _, pool := range pools {
|
||||
i.generateProperties(pool)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Item) generateProperties(pool PropertyPool) {
|
||||
var props []*Property
|
||||
|
||||
switch pool {
|
||||
case PropertyPoolPrefix:
|
||||
if generated := i.generatePrefixProperties(); generated != nil {
|
||||
props = generated
|
||||
}
|
||||
case PropertyPoolSuffix:
|
||||
if generated := i.generateSuffixProperties(); generated != nil {
|
||||
props = generated
|
||||
}
|
||||
case PropertyPoolUnique:
|
||||
if generated := i.generateUniqueProperties(); generated != nil {
|
||||
props = generated
|
||||
}
|
||||
case PropertyPoolSetItem:
|
||||
if generated := i.generateSetItemProperties(); generated != nil {
|
||||
props = generated
|
||||
}
|
||||
case PropertyPoolSet:
|
||||
// todo set bonus handling, needs player/equipment context
|
||||
}
|
||||
|
||||
if props == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if i.properties == nil {
|
||||
i.properties = make(map[PropertyPool][]*Property)
|
||||
}
|
||||
|
||||
i.properties[pool] = props
|
||||
|
||||
// in the case one of the properties is a stat-less prop for indestructable/ethereal
|
||||
// we need to set the item attributes to the rolled values. we use `||` here just in
|
||||
// case another property has already set the flag
|
||||
for propIdx := range props {
|
||||
prop := props[propIdx]
|
||||
switch prop.record.Code {
|
||||
case propertyEthereal:
|
||||
i.attributes.ethereal = i.attributes.ethereal || prop.computedBool
|
||||
case propertyIndestructable:
|
||||
i.attributes.indestructable = i.attributes.ethereal || prop.computedBool
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Item) updateItemAttributes() {
|
||||
i.generateName()
|
||||
|
||||
r := i.CommonRecord()
|
||||
i.attributes = &itemAttributes{
|
||||
damageOneHand: minMaxEnhanceable{
|
||||
min: r.MinDamage,
|
||||
max: r.MaxDamage,
|
||||
},
|
||||
|
||||
damageTwoHand: minMaxEnhanceable{
|
||||
min: r.Min2HandDamage,
|
||||
max: r.Max2HandDamage,
|
||||
},
|
||||
|
||||
damageMissile: minMaxEnhanceable{
|
||||
min: r.MinMissileDamage,
|
||||
max: r.MaxMissileDamage,
|
||||
},
|
||||
stackSize: minMaxEnhanceable{
|
||||
min: r.MinStack,
|
||||
max: r.MaxStack,
|
||||
},
|
||||
durability: minMaxEnhanceable{
|
||||
min: r.Durability,
|
||||
max: r.Durability,
|
||||
},
|
||||
|
||||
baseItemLevel: r.Level,
|
||||
requiredLevel: r.RequiredLevel,
|
||||
requiredStrength: r.RequiredStrength,
|
||||
requiredDexterity: r.RequiredDexterity,
|
||||
durable: !r.NoDurability,
|
||||
throwable: r.Throwable,
|
||||
}
|
||||
|
||||
def, minDef, maxDef := 0, r.MinAC, r.MaxAC
|
||||
|
||||
if minDef < 1 && maxDef < 1 {
|
||||
if maxDef < minDef {
|
||||
minDef, maxDef = maxDef, minDef
|
||||
}
|
||||
|
||||
def = i.rand.Intn(maxDef-minDef+1) + minDef
|
||||
}
|
||||
|
||||
i.attributes.defense = def
|
||||
}
|
||||
|
||||
func (i *Item) generatePrefixProperties() []*Property {
|
||||
if i.PrefixRecords() == nil || len(i.PrefixRecords()) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]*Property, 0)
|
||||
|
||||
// for each prefix
|
||||
for recIdx := range i.PrefixRecords() {
|
||||
prefix := i.PrefixRecords()[recIdx]
|
||||
// for each modifier
|
||||
for modIdx := range prefix.Modifiers {
|
||||
mod := prefix.Modifiers[modIdx]
|
||||
|
||||
prop := NewProperty(mod.Code, mod.Parameter, mod.Min, mod.Max)
|
||||
if prop == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, prop)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *Item) generateSuffixProperties() []*Property {
|
||||
if i.SuffixRecords() == nil || len(i.SuffixRecords()) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]*Property, 0)
|
||||
|
||||
// for each prefix
|
||||
for recIdx := range i.SuffixRecords() {
|
||||
prefix := i.SuffixRecords()[recIdx]
|
||||
// for each modifier
|
||||
for modIdx := range prefix.Modifiers {
|
||||
mod := prefix.Modifiers[modIdx]
|
||||
|
||||
prop := NewProperty(mod.Code, mod.Parameter, mod.Min, mod.Max)
|
||||
if prop == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, prop)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *Item) generateUniqueProperties() []*Property {
|
||||
if i.UniqueRecord() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]*Property, 0)
|
||||
|
||||
for propIdx := range i.UniqueRecord().Properties {
|
||||
propInfo := i.UniqueRecord().Properties[propIdx]
|
||||
|
||||
// sketchy ass unique records, the param should be an int but sometimes it's the name
|
||||
// of a skill, which needs to be converted to the skill index
|
||||
paramStr := getStringComponent(propInfo.Parameter)
|
||||
paramInt := getNumericComponent(propInfo.Parameter)
|
||||
|
||||
if paramStr != "" {
|
||||
for skillID := range d2datadict.SkillDetails {
|
||||
if d2datadict.SkillDetails[skillID].Skill == paramStr {
|
||||
paramInt = skillID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prop := NewProperty(propInfo.Code, paramInt, propInfo.Min, propInfo.Max)
|
||||
if prop == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, prop)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *Item) generateSetItemProperties() []*Property {
|
||||
if i.SetItemRecord() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result := make([]*Property, 0)
|
||||
|
||||
for propIdx := range i.SetItemRecord().Properties {
|
||||
setProp := i.SetItemRecord().Properties[propIdx]
|
||||
|
||||
// like with unique records, the property param is sometimes a skill name
|
||||
// as a string, not an integer index
|
||||
paramStr := getStringComponent(setProp.Parameter)
|
||||
paramInt := getNumericComponent(setProp.Parameter)
|
||||
|
||||
if paramStr != "" {
|
||||
for skillID := range d2datadict.SkillDetails {
|
||||
if d2datadict.SkillDetails[skillID].Skill == paramStr {
|
||||
paramInt = skillID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prop := NewProperty(setProp.Code, paramInt, setProp.Min, setProp.Max)
|
||||
if prop == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, prop)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (i *Item) generateName() {
|
||||
if i.SetItemRecord() != nil {
|
||||
i.name = d2common.TranslateString(i.SetItemRecord().SetItemKey)
|
||||
return
|
||||
}
|
||||
|
||||
if i.UniqueRecord() != nil {
|
||||
i.name = d2common.TranslateString(i.UniqueRecord().Name)
|
||||
return
|
||||
}
|
||||
|
||||
name := d2common.TranslateString(i.CommonRecord().NameString)
|
||||
|
||||
if i.PrefixRecords() != nil {
|
||||
if len(i.PrefixRecords()) > 0 {
|
||||
affix := i.PrefixRecords()[i.rand.Intn(len(i.PrefixRecords()))]
|
||||
name = fmt.Sprintf("%s %s", affix.Name, name)
|
||||
}
|
||||
}
|
||||
|
||||
if i.SuffixRecords() != nil {
|
||||
if len(i.SuffixRecords()) > 0 {
|
||||
affix := i.SuffixRecords()[i.rand.Intn(len(i.SuffixRecords()))]
|
||||
name = fmt.Sprintf("%s %s", name, affix.Name)
|
||||
}
|
||||
}
|
||||
|
||||
i.name = name
|
||||
}
|
||||
|
||||
// GetStatStrings is a test function for getting all stat strings
|
||||
func (i *Item) GetStatStrings() []string {
|
||||
result := make([]string, 0)
|
||||
stats := make([]d2stats.Stat, 0)
|
||||
|
||||
for pool := range i.properties {
|
||||
propPool := i.properties[pool]
|
||||
if propPool == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for propIdx := range propPool {
|
||||
if propPool[propIdx] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
prop := propPool[propIdx]
|
||||
|
||||
for statIdx := range prop.stats {
|
||||
stats = append(stats, prop.stats[statIdx])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(stats) > 0 {
|
||||
stats = diablo2stats.NewStatList(stats...).ReduceStats().Stats()
|
||||
}
|
||||
|
||||
for statIdx := range stats {
|
||||
statStr := stats[statIdx].String()
|
||||
if statStr != "" {
|
||||
result = append(result, statStr)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func findMatchingUniqueRecords(icr *d2datadict.ItemCommonRecord) []*d2datadict.UniqueItemRecord {
|
||||
result := make([]*d2datadict.UniqueItemRecord, 0)
|
||||
|
||||
c1, c2, c3, c4 := icr.Code, icr.NormalCode, icr.UberCode, icr.UltraCode
|
||||
|
||||
for uCode := range d2datadict.UniqueItems {
|
||||
uRec := d2datadict.UniqueItems[uCode]
|
||||
|
||||
switch uCode {
|
||||
case c1, c2, c3, c4:
|
||||
result = append(result, uRec)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// find possible SetItemRecords that the given ItemCommonRecord can have
|
||||
func findMatchingSetItemRecords(icr *d2datadict.ItemCommonRecord) []*d2datadict.SetItemRecord {
|
||||
result := make([]*d2datadict.SetItemRecord, 0)
|
||||
|
||||
c1, c2, c3, c4 := icr.Code, icr.NormalCode, icr.UberCode, icr.UltraCode
|
||||
|
||||
for setItemIdx := range d2datadict.SetItems {
|
||||
switch d2datadict.SetItems[setItemIdx].ItemCode {
|
||||
case c1, c2, c3, c4:
|
||||
result = append(result, d2datadict.SetItems[setItemIdx])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// for a given ItemCommonRecord, find all possible affixes that can spawn
|
||||
func findMatchingAffixes(
|
||||
icr *d2datadict.ItemCommonRecord,
|
||||
fromAffixes map[string]*d2datadict.ItemAffixCommonRecord,
|
||||
) []*d2datadict.ItemAffixCommonRecord {
|
||||
result := make([]*d2datadict.ItemAffixCommonRecord, 0)
|
||||
|
||||
equivItemTypes := d2datadict.FindEquivalentTypesByItemCommonRecord(icr)
|
||||
|
||||
for prefixIdx := range fromAffixes {
|
||||
include, exclude := false, false
|
||||
affix := fromAffixes[prefixIdx]
|
||||
|
||||
for itemTypeIdx := range equivItemTypes {
|
||||
itemType := equivItemTypes[itemTypeIdx]
|
||||
|
||||
for _, excludedType := range affix.ItemExclude {
|
||||
if itemType == excludedType {
|
||||
exclude = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if exclude {
|
||||
break
|
||||
}
|
||||
|
||||
for _, includedType := range affix.ItemInclude {
|
||||
if itemType == includedType {
|
||||
include = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !include {
|
||||
continue
|
||||
}
|
||||
|
||||
if icr.Level < affix.Level {
|
||||
continue
|
||||
}
|
||||
|
||||
result = append(result, affix)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
253
d2core/d2item/diablo2item/item_generator.go
Normal file
253
d2core/d2item/diablo2item/item_generator.go
Normal file
@ -0,0 +1,253 @@
|
||||
package diablo2item
|
||||
|
||||
import (
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
DropModifierBaseProbability = 1024 // base DropModifier probability total
|
||||
)
|
||||
|
||||
type DropModifier int
|
||||
|
||||
const (
|
||||
DropModifierNone DropModifier = iota
|
||||
DropModifierUnique
|
||||
DropModifierSet
|
||||
DropModifierRare
|
||||
DropModifierMagic
|
||||
)
|
||||
|
||||
const (
|
||||
// DynamicItemLevelRange for treasure codes like `armo33`, this code is used to
|
||||
// select all equivalent items (matching `armo` in this case) with item levels 33,34,35
|
||||
DynamicItemLevelRange = 3
|
||||
)
|
||||
|
||||
const (
|
||||
goldItemCodeWithMult = "gld,mul="
|
||||
goldItemCode = "gld"
|
||||
)
|
||||
|
||||
// ItemGenerator is a diablo 2 implementation of an item generator
|
||||
type ItemGenerator struct {
|
||||
rand *rand.Rand
|
||||
source rand.Source
|
||||
Seed int64
|
||||
}
|
||||
|
||||
// SetSeed sets the item generator seed
|
||||
func (ig *ItemGenerator) SetSeed(seed int64) {
|
||||
if ig.rand == nil || ig.source == nil {
|
||||
ig.source = rand.NewSource(seed)
|
||||
ig.rand = rand.New(ig.source)
|
||||
}
|
||||
ig.Seed = seed
|
||||
}
|
||||
|
||||
func (ig *ItemGenerator) rollDropModifier(tcr *d2datadict.TreasureClassRecord) DropModifier {
|
||||
modMap := map[int]DropModifier{
|
||||
0: DropModifierNone,
|
||||
1: DropModifierUnique,
|
||||
2: DropModifierSet,
|
||||
3: DropModifierRare,
|
||||
4: DropModifierMagic,
|
||||
}
|
||||
|
||||
dropModifiers := []int{
|
||||
DropModifierBaseProbability,
|
||||
tcr.FreqUnique,
|
||||
tcr.FreqSet,
|
||||
tcr.FreqRare,
|
||||
tcr.FreqMagic,
|
||||
}
|
||||
|
||||
for idx := range dropModifiers {
|
||||
if idx == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
dropModifiers[idx] += dropModifiers[idx-1]
|
||||
}
|
||||
|
||||
roll := ig.rand.Intn(dropModifiers[len(dropModifiers)-1])
|
||||
|
||||
for idx := range dropModifiers {
|
||||
if roll < dropModifiers[idx] {
|
||||
return modMap[idx]
|
||||
}
|
||||
}
|
||||
|
||||
return DropModifierNone
|
||||
}
|
||||
|
||||
func (ig *ItemGenerator) rollTreasurePick(tcr *d2datadict.TreasureClassRecord) *d2datadict.Treasure {
|
||||
// treasure probabilities
|
||||
tprob := make([]int, len(tcr.Treasures)+1)
|
||||
total := tcr.FreqNoDrop
|
||||
tprob[0] = total
|
||||
|
||||
for idx := range tcr.Treasures {
|
||||
total += tcr.Treasures[idx].Probability
|
||||
tprob[idx+1] = total
|
||||
}
|
||||
|
||||
roll := ig.rand.Intn(total)
|
||||
|
||||
for idx := range tprob {
|
||||
if roll < tprob[idx] {
|
||||
if idx == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
return tcr.Treasures[idx-1]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ItemsFromTreasureClass rolls for and creates items using a treasure class record
|
||||
func (ig *ItemGenerator) ItemsFromTreasureClass(tcr *d2datadict.TreasureClassRecord) []*Item {
|
||||
result := make([]*Item, 0)
|
||||
|
||||
treasurePicks := make([]*d2datadict.Treasure, 0)
|
||||
|
||||
// if tcr.NumPicks is negative, each item probability is instead a count for how many
|
||||
// of that treasure to drop
|
||||
if tcr.NumPicks < 0 {
|
||||
picksLeft := tcr.NumPicks
|
||||
|
||||
// for each of the treasures, we pick it N times, where N is the count for the item
|
||||
// we do this until we run out of picks
|
||||
for idx := range tcr.Treasures {
|
||||
howMany := tcr.Treasures[idx].Probability
|
||||
for count := 0; count < howMany && picksLeft < 0; count++ {
|
||||
treasurePicks = append(treasurePicks, tcr.Treasures[idx])
|
||||
picksLeft++
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// for N picks, we roll for a treasure and append to our treasures if it isn't a NoDrop
|
||||
for picksLeft := tcr.NumPicks; picksLeft > 0; picksLeft-- {
|
||||
rolledTreasure := ig.rollTreasurePick(tcr)
|
||||
|
||||
if rolledTreasure == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
treasurePicks = append(treasurePicks, rolledTreasure)
|
||||
}
|
||||
}
|
||||
|
||||
// for each of our picked/rolled treasures, we will attempt to generate an item.
|
||||
// The treasure may actually be a reference to another treasure class, in which
|
||||
// case we will roll that treasure class, eventually getting a slice of items
|
||||
for idx := range treasurePicks {
|
||||
picked := treasurePicks[idx]
|
||||
if record, found := d2datadict.TreasureClass[picked.Code]; found {
|
||||
// the code is for a treasure class, we roll again using that TC
|
||||
itemSlice := ig.ItemsFromTreasureClass(record)
|
||||
for itemIdx := range itemSlice {
|
||||
itemSlice[itemIdx].applyDropModifier(ig.rollDropModifier(tcr))
|
||||
itemSlice[itemIdx].generateAllProperties()
|
||||
itemSlice[itemIdx].updateItemAttributes()
|
||||
result = append(result, itemSlice[itemIdx])
|
||||
}
|
||||
} else {
|
||||
// the code is not for a treasure class, but for an item
|
||||
item := ig.ItemFromTreasure(picked)
|
||||
if item != nil {
|
||||
item.applyDropModifier(ig.rollDropModifier(tcr))
|
||||
item.generateAllProperties()
|
||||
item.updateItemAttributes()
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ItemFromTreasure rolls for a ig.rand.m item using the Treasure struct (from d2datadict)
|
||||
func (ig *ItemGenerator) ItemFromTreasure(treasure *d2datadict.Treasure) *Item {
|
||||
result := &Item{
|
||||
rand: rand.New(rand.NewSource(ig.Seed)),
|
||||
}
|
||||
|
||||
// in this case, the treasure code is a code used by an ItemCommonRecord
|
||||
commonRecord := d2datadict.CommonItems[treasure.Code]
|
||||
if commonRecord != nil {
|
||||
result.CommonCode = commonRecord.Code
|
||||
return result
|
||||
}
|
||||
|
||||
// next, we check if the treasure code is a generic type like `armo`
|
||||
equivList := d2datadict.ItemEquivalenciesByTypeCode[treasure.Code]
|
||||
if equivList != nil {
|
||||
result.CommonCode = equivList[ig.rand.Intn(len(equivList))].Code
|
||||
return result
|
||||
}
|
||||
|
||||
// in this case, the treasure code is something like `armo23` and needs to
|
||||
// be resolved to ItemCommonRecords for armors with levels 23,24,25
|
||||
matches := resolveDynamicTreasureCode(treasure.Code)
|
||||
if matches != nil {
|
||||
numItems := len(matches)
|
||||
if numItems < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
result.CommonCode = matches[ig.rand.Intn(numItems)].Code
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveDynamicTreasureCode(code string) []*d2datadict.ItemCommonRecord {
|
||||
numericComponent := getNumericComponent(code)
|
||||
stringComponent := getStringComponent(code)
|
||||
|
||||
if stringComponent == goldItemCodeWithMult {
|
||||
// todo need to do something with the numeric component (the gold multiplier)
|
||||
stringComponent = goldItemCode
|
||||
}
|
||||
|
||||
result := make([]*d2datadict.ItemCommonRecord, 0)
|
||||
equivList := d2datadict.ItemEquivalenciesByTypeCode[stringComponent]
|
||||
|
||||
for idx := range equivList {
|
||||
record := equivList[idx]
|
||||
minLevel := numericComponent
|
||||
maxLevel := minLevel + DynamicItemLevelRange
|
||||
|
||||
if record.Level >= minLevel && record.Level < maxLevel {
|
||||
result = append(result, record)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getStringComponent(code string) string {
|
||||
re := regexp.MustCompile(`\d+`)
|
||||
return string(re.ReplaceAll([]byte(code), []byte("")))
|
||||
}
|
||||
|
||||
func getNumericComponent(code string) int {
|
||||
result := 0
|
||||
|
||||
re := regexp.MustCompile(`[^\d]`)
|
||||
numStr := string(re.ReplaceAll([]byte(code), []byte("")))
|
||||
|
||||
if number, err := strconv.ParseInt(numStr, 10, 32); err == nil {
|
||||
result = int(number)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
370
d2core/d2item/diablo2item/item_property.go
Normal file
370
d2core/d2item/diablo2item/item_property.go
Normal file
@ -0,0 +1,370 @@
|
||||
package diablo2item
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats/diablo2stats"
|
||||
)
|
||||
|
||||
const (
|
||||
noValue = iota
|
||||
oneValue
|
||||
twoValue
|
||||
threeValue
|
||||
)
|
||||
|
||||
const (
|
||||
skillTabsPerClass = 3
|
||||
)
|
||||
|
||||
// these come from properties.txt, the types of functions that properties can use to evaluate args
|
||||
const (
|
||||
fnNone = iota
|
||||
fnValuesToStat
|
||||
fnArmorPercent
|
||||
fnRepeatPreviousWithMinMax // repeat only with min and max
|
||||
fnUnused
|
||||
fnDamageMin
|
||||
fnDamageMax
|
||||
fnDamagePercent
|
||||
fnSpeedRelated
|
||||
fnRepeatPreviousWithParamMinMax // repeat with param, man, and max
|
||||
fnClassSkillTab
|
||||
fnProcs
|
||||
fnRandomSkill
|
||||
fnMaxDurability
|
||||
fnNumSockets
|
||||
fnStatMin
|
||||
fnStatMax
|
||||
fnStatParam
|
||||
fnTimeRelated
|
||||
fnChargeRelated
|
||||
fnIndestructable
|
||||
fnClassSkills
|
||||
fnSingleSkill
|
||||
fnEthereal
|
||||
fnStateApplyToTarget
|
||||
)
|
||||
|
||||
// PropertyType describes what kind of property this is
|
||||
type PropertyType int
|
||||
|
||||
// Property types
|
||||
// Not all properties contain stats, some are just used to compute a value
|
||||
// examples are:
|
||||
// min/max
|
||||
// % damage
|
||||
// indestructable and etheral flags
|
||||
const (
|
||||
PropertyComputeStats = iota // for properties that do compute stats
|
||||
PropertyComputeInteger // for properties that compute an integer value
|
||||
PropertyComputeBoolean // for properties that compute a boolean
|
||||
)
|
||||
|
||||
const (
|
||||
fnRandClassSkill = 36
|
||||
)
|
||||
|
||||
// Property is an item property. Properties act as stat initializers, as well as
|
||||
// item attribute initializers. A good example of this is for the `Ethereal` property,
|
||||
// which DOES have a stat, but the stat is actually non-printable as far as the record
|
||||
// in itemstatcosts.txt is concerned. The behavior of displaying `Ethereal` on an item
|
||||
// in diablo 2 is hardcoded into whatever handled displaying item descriptions, not
|
||||
// what was generating stat descriptions (this is a guess, though).
|
||||
// Another example in min/max damage properties, which do NOT have stats!
|
||||
type Property struct {
|
||||
record *d2datadict.PropertyRecord
|
||||
stats []d2stats.Stat
|
||||
PropertyType PropertyType
|
||||
|
||||
// the inputValues that were passed initially when calling `NewProperty`
|
||||
inputParams []int
|
||||
|
||||
// some properties are statless and used only for computing a value
|
||||
computedInt int
|
||||
computedBool bool
|
||||
}
|
||||
|
||||
func (p *Property) init() *Property {
|
||||
p.stats = make([]d2stats.Stat, 0)
|
||||
|
||||
// some property functions need to be able to repeat last function
|
||||
// this is for properties with multiple stats that want to repeat the same
|
||||
// initialization step with the same min/max params
|
||||
var lastFnCalled int
|
||||
|
||||
var stat d2stats.Stat
|
||||
|
||||
for idx := range p.record.Stats {
|
||||
if p.record.Stats[idx] == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
stat, lastFnCalled = p.eval(idx, lastFnCalled)
|
||||
|
||||
// some property stats don't actually have a stat
|
||||
// but they have functions on the first stat entry
|
||||
if stat != nil {
|
||||
p.stats = append(p.stats, stat)
|
||||
}
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
// eval will attempt to create a stat, and will return the function id that was last run.
|
||||
// this is because some of the properties have a func index which indicates that it should
|
||||
// repeat the previous fn with the same parameters, but for a different stat.
|
||||
func (p *Property) eval(propStatIdx, previousFnID int) (stat d2stats.Stat, funcID int) {
|
||||
pStatRecord := p.record.Stats[propStatIdx]
|
||||
iscRecord := d2datadict.ItemStatCosts[pStatRecord.StatCode]
|
||||
|
||||
funcID = pStatRecord.FunctionID
|
||||
|
||||
switch funcID {
|
||||
case fnRepeatPreviousWithMinMax, fnRepeatPreviousWithParamMinMax:
|
||||
funcID = previousFnID
|
||||
fallthrough
|
||||
case fnValuesToStat, fnSpeedRelated, fnMaxDurability, fnNumSockets,
|
||||
fnStatMin, fnStatMax, fnSingleSkill, fnArmorPercent:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnValuesToStat(iscRecord)
|
||||
case fnDamageMin, fnDamageMax, fnDamagePercent:
|
||||
p.PropertyType = PropertyComputeInteger
|
||||
p.computedInt = p.fnComputeInteger()
|
||||
case fnClassSkillTab:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnClassSkillTab(iscRecord)
|
||||
case fnProcs:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnProcs(iscRecord)
|
||||
case fnRandomSkill:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnRandomSkill(iscRecord)
|
||||
case fnStatParam:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnStatParam(iscRecord)
|
||||
case fnChargeRelated:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnChargeRelated(iscRecord)
|
||||
case fnIndestructable, fnEthereal:
|
||||
p.PropertyType = PropertyComputeBoolean
|
||||
p.computedBool = p.fnBoolean()
|
||||
case fnClassSkills:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnClassSkills(pStatRecord, iscRecord)
|
||||
case fnStateApplyToTarget:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnStateApplyToTarget(iscRecord)
|
||||
case fnRandClassSkill:
|
||||
p.PropertyType = PropertyComputeStats
|
||||
stat = p.fnRandClassSkill(iscRecord)
|
||||
case fnNone, fnUnused, fnTimeRelated:
|
||||
default:
|
||||
}
|
||||
|
||||
return stat, funcID
|
||||
}
|
||||
|
||||
// fnValuesToStat Applies a value to a stat, can use SetX parameter.
|
||||
func (p *Property) fnValuesToStat(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
|
||||
// the only special case to handle for this function is for
|
||||
// property "color", which corresponds to ISC record "item_lightcolor"
|
||||
// I'm not yet sure how to handle this special case... it is likely
|
||||
// and index into one of the colors in colors.txt
|
||||
var min, max int
|
||||
|
||||
var propParam, statValue float64
|
||||
|
||||
switch len(p.inputParams) {
|
||||
case noValue, oneValue:
|
||||
return nil
|
||||
case twoValue:
|
||||
min, max = p.inputParams[0], p.inputParams[1]
|
||||
case threeValue:
|
||||
propParam = float64(p.inputParams[0])
|
||||
min, max = p.inputParams[1], p.inputParams[2]
|
||||
default:
|
||||
min, max = p.inputParams[0], p.inputParams[1]
|
||||
}
|
||||
|
||||
if max < min {
|
||||
min, max = max, min
|
||||
}
|
||||
|
||||
statValue = float64(rand.Intn(max-min+1) + min)
|
||||
|
||||
return diablo2stats.NewStat(iscRecord.Name, statValue, propParam)
|
||||
}
|
||||
|
||||
// fnComputeInteger Dmg-min related ???
|
||||
func (p *Property) fnComputeInteger() int {
|
||||
var min, max int
|
||||
|
||||
switch len(p.inputParams) {
|
||||
case noValue, oneValue:
|
||||
return 0
|
||||
default:
|
||||
min, max = p.inputParams[0], p.inputParams[1]
|
||||
}
|
||||
|
||||
statValue := rand.Intn(max-min+1) + min
|
||||
|
||||
return statValue
|
||||
}
|
||||
|
||||
// fnClassSkillTab skilltab skill group ???
|
||||
func (p *Property) fnClassSkillTab(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
|
||||
// from here: https://d2mods.info/forum/kb/viewarticle?a=45
|
||||
// Amazon
|
||||
// 0 - Bow & Crossbow
|
||||
// 1 - Passive & Magic
|
||||
// 2 - Spear & Javelin
|
||||
// Sorceress
|
||||
// 3 - Fire
|
||||
// 4 - Lightning
|
||||
// 5 - Cold
|
||||
// Necromancer
|
||||
// 6 - Curses
|
||||
// 7 - Poison & Bone
|
||||
// 8 - Summoning
|
||||
// Paladin
|
||||
// 9 - Offensive Auras
|
||||
// 10 - Combat Skills
|
||||
// 11 - Defensive Auras
|
||||
// Barbarian
|
||||
// 12 - Masteries
|
||||
// 13 - Combat Skills
|
||||
// 14 - Warcries
|
||||
// Druid
|
||||
// 15 - Summoning
|
||||
// 16 - Shapeshifting
|
||||
// 17 - Elemental
|
||||
// Assassin
|
||||
// 18 - Traps
|
||||
// 19 - Shadow Disciplines
|
||||
// 20 - Martial Arts
|
||||
param, min, max := p.inputParams[0], p.inputParams[1], p.inputParams[2]
|
||||
skillTabIdx := float64(param % skillTabsPerClass)
|
||||
classIdx := float64(param / skillTabsPerClass)
|
||||
level := float64(rand.Intn(max-min+1) + min)
|
||||
|
||||
return diablo2stats.NewStat(iscRecord.Name, level, classIdx, skillTabIdx)
|
||||
}
|
||||
|
||||
// fnProcs event-based skills ???
|
||||
func (p *Property) fnProcs(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
|
||||
var skillID, chance, skillLevel float64
|
||||
|
||||
switch len(p.inputParams) {
|
||||
case noValue, oneValue, twoValue:
|
||||
return nil
|
||||
default:
|
||||
skillID = float64(p.inputParams[0])
|
||||
chance = float64(p.inputParams[1])
|
||||
skillLevel = float64(p.inputParams[2])
|
||||
}
|
||||
|
||||
return diablo2stats.NewStat(iscRecord.Name, chance, skillLevel, skillID)
|
||||
}
|
||||
|
||||
// fnRandomSkill random selection of parameters for parameter-based stat ???
|
||||
func (p *Property) fnRandomSkill(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
|
||||
var skillLevel, skillID float64
|
||||
|
||||
invalidHeroIndex := -1.0
|
||||
|
||||
switch len(p.inputParams) {
|
||||
case noValue, oneValue, twoValue:
|
||||
return nil
|
||||
default:
|
||||
skillLevel = float64(p.inputParams[0])
|
||||
min, max := p.inputParams[1], p.inputParams[2]
|
||||
skillID = float64(rand.Intn(max-min+1) + min)
|
||||
}
|
||||
|
||||
return diablo2stats.NewStat(iscRecord.Name, skillLevel, skillID, invalidHeroIndex)
|
||||
}
|
||||
|
||||
// fnStatParam use param field only
|
||||
func (p *Property) fnStatParam(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
|
||||
switch len(p.inputParams) {
|
||||
case noValue:
|
||||
return nil
|
||||
default:
|
||||
val := float64(p.inputParams[0])
|
||||
return diablo2stats.NewStat(iscRecord.Name, val)
|
||||
}
|
||||
}
|
||||
|
||||
// fnChargeRelated Related to charged item.
|
||||
func (p *Property) fnChargeRelated(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
|
||||
var lvl, skill, charges float64
|
||||
|
||||
switch len(p.inputParams) {
|
||||
case noValue, oneValue, twoValue:
|
||||
return nil
|
||||
default:
|
||||
lvl = float64(p.inputParams[2])
|
||||
skill = float64(p.inputParams[0])
|
||||
charges = float64(p.inputParams[1])
|
||||
|
||||
return diablo2stats.NewStat(iscRecord.Name, lvl, skill, charges, charges)
|
||||
}
|
||||
}
|
||||
|
||||
// fnIndestructable Simple boolean stuff. Use by indestruct.
|
||||
func (p *Property) fnBoolean() bool {
|
||||
var min, max int
|
||||
|
||||
switch len(p.inputParams) {
|
||||
case noValue, oneValue:
|
||||
return false
|
||||
default:
|
||||
min, max = p.inputParams[0], p.inputParams[1]
|
||||
}
|
||||
|
||||
statValue := rand.Intn(max-min+1) + min
|
||||
|
||||
return statValue > 0
|
||||
}
|
||||
|
||||
// fnClassSkills Add to group of skills, group determined by stat ID, uses ValX parameter.
|
||||
func (p *Property) fnClassSkills(
|
||||
propStatRecord *d2datadict.PropertyStatRecord,
|
||||
iscRecord *d2datadict.ItemStatCostRecord,
|
||||
) d2stats.Stat {
|
||||
// in order 0..6
|
||||
// Amazon
|
||||
// Sorceress
|
||||
// Necromancer
|
||||
// Paladin
|
||||
// Druid
|
||||
// Assassin
|
||||
var min, max, classIdx int
|
||||
|
||||
switch len(p.inputParams) {
|
||||
case noValue, oneValue:
|
||||
return nil
|
||||
default:
|
||||
min, max = p.inputParams[0], p.inputParams[1]
|
||||
}
|
||||
|
||||
statValue := rand.Intn(max-min+1) + min
|
||||
classIdx = propStatRecord.Value
|
||||
|
||||
return diablo2stats.NewStat(iscRecord.Name, float64(statValue), float64(classIdx))
|
||||
}
|
||||
|
||||
// fnStateApplyToTarget property applied to character or target monster ???
|
||||
func (p *Property) fnStateApplyToTarget(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
|
||||
// todo need to implement states
|
||||
return nil
|
||||
}
|
||||
|
||||
// fnRandClassSkill property applied to character or target monster ???
|
||||
func (p *Property) fnRandClassSkill(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
|
||||
return nil
|
||||
}
|
608
d2core/d2item/diablo2item/item_property_test.go
Normal file
608
d2core/d2item/diablo2item/item_property_test.go
Normal file
@ -0,0 +1,608 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
d2core/d2item/doc.go
Normal file
3
d2core/d2item/doc.go
Normal file
@ -0,0 +1,3 @@
|
||||
// Package d2item provides a generic interface for what the OpenDiablo2
|
||||
// engine considers to be an item.
|
||||
package d2item
|
6
d2core/d2item/equipper.go
Normal file
6
d2core/d2item/equipper.go
Normal file
@ -0,0 +1,6 @@
|
||||
package d2item
|
||||
|
||||
type Equipper interface {
|
||||
EquippedItems() []Item
|
||||
CarriedItems() []Item
|
||||
}
|
11
d2core/d2item/item.go
Normal file
11
d2core/d2item/item.go
Normal file
@ -0,0 +1,11 @@
|
||||
package d2item
|
||||
|
||||
// Item describes all types of item that can be placed in the
|
||||
// player inventory grid (not just things that can be equipped!)
|
||||
type Item interface {
|
||||
Context() StatContext
|
||||
SetContext(StatContext)
|
||||
|
||||
Name() string
|
||||
Description() string
|
||||
}
|
@ -50,13 +50,20 @@ type diablo2Stat struct {
|
||||
// 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) {
|
||||
func (s *diablo2Stat) init(numbers ...float64) {//nolint:funlen doesn't make sense to split
|
||||
if s.record == nil {
|
||||
return
|
||||
}
|
||||
|
||||
//nolint:gomdn introducing a const for these would be worse
|
||||
switch s.record.DescFnID {
|
||||
case 0:
|
||||
// special case for poisonlength, or other stats, which have a
|
||||
// 0-value descfnID field but need to store values
|
||||
s.values = make([]d2stats.StatValue, len(numbers))
|
||||
for idx := range s.values {
|
||||
s.values[idx] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
|
||||
}
|
||||
case 1:
|
||||
// +31 to Strength
|
||||
// Replenish Life +20 || Drain Life -8
|
||||
@ -167,7 +174,7 @@ func (s *diablo2Stat) init(numbers ...float64) {
|
||||
}
|
||||
|
||||
for idx := range numbers {
|
||||
if idx > len(s.values) {
|
||||
if idx > len(s.values)-1 {
|
||||
break
|
||||
}
|
||||
|
||||
@ -222,6 +229,10 @@ func (s *diablo2Stat) Clone() d2stats.Stat {
|
||||
dstVal.SetFloat(srcVal.Float())
|
||||
}
|
||||
|
||||
if len(clone.values) < len(s.values) {
|
||||
clone.values = make([]d2stats.StatValue, len(s.values))
|
||||
}
|
||||
|
||||
clone.values[idx] = dstVal
|
||||
}
|
||||
|
||||
@ -561,6 +572,12 @@ func (s *diablo2Stat) descFn24() string {
|
||||
}
|
||||
|
||||
func (s *diablo2Stat) descFn27() string {
|
||||
// property "skill-rand" will try to make an instance with an invalid hero index
|
||||
// in this case, we use descfn 28
|
||||
if s.values[2].Int() == -1 {
|
||||
return s.descFn28()
|
||||
}
|
||||
|
||||
amount, skill, hero := s.values[0], s.values[1], s.values[2]
|
||||
|
||||
return fmt.Sprintf(fourComponentStr, amount, "to", skill, hero)
|
||||
|
@ -13,15 +13,15 @@ const (
|
||||
monsterNotFound = "{Monster not found!}"
|
||||
)
|
||||
|
||||
func getHeroMap() map[int]d2enum.Hero {
|
||||
return map[int]d2enum.Hero{
|
||||
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
|
||||
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
|
||||
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
|
||||
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
|
||||
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
|
||||
int(d2enum.HeroDruid): d2enum.HeroDruid,
|
||||
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
|
||||
func getHeroMap() []d2enum.Hero {
|
||||
return []d2enum.Hero{
|
||||
d2enum.HeroAmazon,
|
||||
d2enum.HeroSorceress,
|
||||
d2enum.HeroNecromancer,
|
||||
d2enum.HeroPaladin,
|
||||
d2enum.HeroBarbarian,
|
||||
d2enum.HeroDruid,
|
||||
d2enum.HeroAssassin,
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user