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)
|
entry.loader(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d2datadict.LoadItemEquivalencies() // depends on ItemCommon and ItemTypes
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,9 +9,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// MagicPrefix stores all of the magic prefix records
|
// 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
|
// 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
|
// LoadMagicPrefix loads MagicPrefix.txt
|
||||||
func LoadMagicPrefix(file []byte) {
|
func LoadMagicPrefix(file []byte) {
|
||||||
@ -48,7 +50,11 @@ func getAffixString(t1 d2enum.ItemAffixSuperType, t2 d2enum.ItemAffixSubType) st
|
|||||||
return name
|
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)
|
d := d2common.LoadDataDictionary(file)
|
||||||
records := createItemAffixRecords(d, superType, subType)
|
records := createItemAffixRecords(d, superType, subType)
|
||||||
name := getAffixString(superType, subType)
|
name := getAffixString(superType, subType)
|
||||||
@ -57,8 +63,12 @@ func loadDictionary(file []byte, superType d2enum.ItemAffixSuperType, subType d2
|
|||||||
return records
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAffixSuperType, subType d2enum.ItemAffixSubType) []*ItemAffixCommonRecord {
|
func createItemAffixRecords(
|
||||||
records := make([]*ItemAffixCommonRecord, 0)
|
d *d2common.DataDictionary,
|
||||||
|
superType d2enum.ItemAffixSuperType,
|
||||||
|
subType d2enum.ItemAffixSubType,
|
||||||
|
) map[string]*ItemAffixCommonRecord {
|
||||||
|
records := make(map[string]*ItemAffixCommonRecord)
|
||||||
|
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
affix := &ItemAffixCommonRecord{
|
affix := &ItemAffixCommonRecord{
|
||||||
@ -82,7 +92,7 @@ func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAff
|
|||||||
PriceScale: d.Number("multiply"),
|
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++ {
|
for i := 1; i <= 3; i++ {
|
||||||
codeKey := fmt.Sprintf("mod%dcode", i)
|
codeKey := fmt.Sprintf("mod%dcode", i)
|
||||||
paramKey := fmt.Sprintf("mod%dparam", i)
|
paramKey := fmt.Sprintf("mod%dparam", i)
|
||||||
@ -125,7 +135,7 @@ func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAff
|
|||||||
group := ItemAffixGroups[affix.GroupID]
|
group := ItemAffixGroups[affix.GroupID]
|
||||||
group.addMember(affix)
|
group.addMember(affix)
|
||||||
|
|
||||||
records = append(records, affix)
|
records[affix.Name] = affix
|
||||||
}
|
}
|
||||||
if d.Err != nil {
|
if d.Err != nil {
|
||||||
panic(d.Err)
|
panic(d.Err)
|
||||||
|
@ -177,6 +177,10 @@ func LoadCommonItems(file []byte, source d2enum.InventoryItemType) map[string]*I
|
|||||||
}
|
}
|
||||||
|
|
||||||
rec := createCommonItemRecord(line, mapping, source)
|
rec := createCommonItemRecord(line, mapping, source)
|
||||||
|
if rec.Name == "Expansion" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
items[rec.Code] = &rec
|
items[rec.Code] = &rec
|
||||||
CommonItems[rec.Code] = &rec
|
CommonItems[rec.Code] = &rec
|
||||||
}
|
}
|
||||||
@ -392,3 +396,4 @@ func createItemUsageStats(r *[]string, mapping map[string]int) [3]ItemUsageStat
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,7 +246,7 @@ func LoadItemTypes(file []byte) {
|
|||||||
StorePage: d.String("StorePage"),
|
StorePage: d.String("StorePage"),
|
||||||
}
|
}
|
||||||
|
|
||||||
ItemTypes[itemType.Name] = itemType
|
ItemTypes[itemType.Code] = itemType
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.Err != nil {
|
if d.Err != nil {
|
||||||
@ -255,3 +255,101 @@ func LoadItemTypes(file []byte) {
|
|||||||
|
|
||||||
log.Printf("Loaded %d ItemType records", len(ItemTypes))
|
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
|
package d2datadict
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type stat struct {
|
type PropertyStatRecord struct {
|
||||||
SetID int
|
SetID int
|
||||||
Value int
|
Value int
|
||||||
FunctionID int
|
FunctionID int
|
||||||
StatCode string
|
StatCode string
|
||||||
}
|
}
|
||||||
|
|
||||||
// PropertyRecord is a representation of a single row of gems.txt
|
// PropertyRecord is a representation of a single row of properties.txt
|
||||||
// it describes the properties of socketable items
|
|
||||||
type PropertyRecord struct {
|
type PropertyRecord struct {
|
||||||
Code string
|
Code string
|
||||||
Active string
|
Active string
|
||||||
Stats [7]*stat
|
Stats [7]*PropertyStatRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
// Properties stores all of the PropertyRecords
|
// Properties stores all of the PropertyRecords
|
||||||
@ -34,7 +33,7 @@ func LoadProperties(file []byte) {
|
|||||||
prop := &PropertyRecord{
|
prop := &PropertyRecord{
|
||||||
Code: d.String("code"),
|
Code: d.String("code"),
|
||||||
Active: d.String("*done"),
|
Active: d.String("*done"),
|
||||||
Stats: [7]*stat{
|
Stats: [7]*PropertyStatRecord{
|
||||||
{
|
{
|
||||||
SetID: d.Number("set1"),
|
SetID: d.Number("set1"),
|
||||||
Value: d.Number("val1"),
|
Value: d.Number("val1"),
|
||||||
@ -88,4 +87,3 @@ func LoadProperties(file []byte) {
|
|||||||
|
|
||||||
log.Printf("Loaded %d Property records", len(Properties))
|
log.Printf("Loaded %d Property records", len(Properties))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,31 @@
|
|||||||
package d2datadict
|
package d2datadict
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||||
"log"
|
"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
|
// SetItemRecord represents a set item
|
||||||
type SetItemRecord struct {
|
type SetItemRecord struct {
|
||||||
// StringTableKey (index)
|
// SetItemKey (index)
|
||||||
// string key to item's name in a .tbl file
|
// string key to item's name in a .tbl file
|
||||||
StringTableKey string
|
SetItemKey string
|
||||||
|
|
||||||
// SetKey (set)
|
// SetKey (set)
|
||||||
// string key to the index field in Sets.txt - the set the item is a part of.
|
// 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.
|
// on a set item. See the appendix for further details about this field's effects.
|
||||||
AddFn int
|
AddFn int
|
||||||
|
|
||||||
// Prop (prop1 to prop9)
|
// Properties are a propert code, parameter, min, max for generating an item propert
|
||||||
// An ID pointer of a property from Properties.txt,
|
Properties [numPropertiesOnSetItem]*SetItemProperty
|
||||||
// these columns control each of the nine different fixed (
|
|
||||||
// blue) modifiers a set item can grant you at most.
|
|
||||||
Prop [9]string
|
|
||||||
|
|
||||||
// Par (par1 to par9)
|
// SetPropertiesLevel1 is the first version of bonus properties for the set
|
||||||
// The parameter passed on to the associated property, this is used to pass skill IDs, state IDs,
|
SetPropertiesLevel1 [numBonusPropertiesOnSetItem]*SetItemProperty
|
||||||
// monster IDs, montype IDs and the like on to the properties that require them,
|
|
||||||
// these fields support calculations.
|
|
||||||
Par [9]int
|
|
||||||
|
|
||||||
// Min, Max (min1 to min9, max1 to max9)
|
// SetPropertiesLevel2 is the second version of bonus properties for the set
|
||||||
// Minimum value to assign to the associated (blue) property.
|
SetPropertiesLevel2 [numBonusPropertiesOnSetItem]*SetItemProperty
|
||||||
// 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
|
|
||||||
|
|
||||||
// APropA, APropB (aprop1a,aprop1b to aprop5a,aprop5b)
|
// SetItemProperty is describes a property of a set item
|
||||||
// An ID pointer of a property from Properties.txt,
|
type SetItemProperty struct {
|
||||||
// these columns control each of the five pairs of different variable (
|
Code string
|
||||||
// green) modifiers a set item can grant you at most.
|
Parameter string // depending on the property, this may be an int (usually), or a string
|
||||||
APropA [5]string
|
Min int
|
||||||
APropB [5]string
|
Max int
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetItems holds all of the SetItemRecords
|
// 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
|
// LoadSetItems loads all of the SetItemRecords from SetItems.txt
|
||||||
func LoadSetItems(file []byte) {
|
func LoadSetItems(file []byte) {
|
||||||
SetItems = make([]*SetItemRecord, 0)
|
SetItems = make(map[string]*SetItemRecord)
|
||||||
|
|
||||||
d := d2common.LoadDataDictionary(file)
|
d := d2common.LoadDataDictionary(file)
|
||||||
|
|
||||||
for d.Next() {
|
for d.Next() {
|
||||||
record := &SetItemRecord{
|
record := &SetItemRecord{
|
||||||
StringTableKey: d.String("index"),
|
SetItemKey: d.String("index"),
|
||||||
SetKey: d.String("set"),
|
SetKey: d.String("set"),
|
||||||
ItemCode: d.String("item"),
|
ItemCode: d.String("item"),
|
||||||
Rarity: d.Number("rarity"),
|
Rarity: d.Number("rarity"),
|
||||||
@ -162,109 +152,47 @@ func LoadSetItems(file []byte) {
|
|||||||
CostMult: d.Number("cost mult"),
|
CostMult: d.Number("cost mult"),
|
||||||
CostAdd: d.Number("cost add"),
|
CostAdd: d.Number("cost add"),
|
||||||
AddFn: d.Number("add func"),
|
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 {
|
if d.Err != nil {
|
||||||
|
@ -2,17 +2,47 @@ package d2datadict
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common"
|
||||||
|
"log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
numTreasures = 10
|
maxTreasuresPerRecord = 10
|
||||||
treasureItemFmt = "Item%d"
|
treasureItemFmt = "Item%d"
|
||||||
treasureProbFmt = "Prob%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
|
// TreasureClassRecord represents a rule for item drops in diablo 2
|
||||||
type TreasureClassRecord struct {
|
type TreasureClassRecord struct {
|
||||||
Name string
|
Name string
|
||||||
@ -28,12 +58,13 @@ type TreasureClassRecord struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Treasure describes a treasure to drop
|
// 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 {
|
type Treasure struct {
|
||||||
Name string
|
Code string
|
||||||
Probability int
|
Probability int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TreasureClass contains all of the TreasureClassRecords
|
||||||
var TreasureClass map[string]*TreasureClassRecord //nolint:gochecknoglobals // Currently global by design
|
var TreasureClass map[string]*TreasureClassRecord //nolint:gochecknoglobals // Currently global by design
|
||||||
|
|
||||||
// LoadTreasureClassRecords loads treasure class records from TreasureClassEx.txt
|
// LoadTreasureClassRecords loads treasure class records from TreasureClassEx.txt
|
||||||
@ -56,7 +87,11 @@ func LoadTreasureClassRecords(file []byte) {
|
|||||||
FreqNoDrop: d.Number("NoDrop"),
|
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)
|
treasureColumnKey := fmt.Sprintf(treasureItemFmt, treasureIdx+1)
|
||||||
probColumnKey := fmt.Sprintf(treasureProbFmt, treasureIdx+1)
|
probColumnKey := fmt.Sprintf(treasureProbFmt, treasureIdx+1)
|
||||||
|
|
||||||
@ -68,17 +103,16 @@ func LoadTreasureClassRecords(file []byte) {
|
|||||||
prob := d.Number(probColumnKey)
|
prob := d.Number(probColumnKey)
|
||||||
|
|
||||||
treasure := &Treasure{
|
treasure := &Treasure{
|
||||||
Name: treasureName,
|
Code: treasureName,
|
||||||
Probability: prob,
|
Probability: prob,
|
||||||
}
|
}
|
||||||
|
|
||||||
if record.Treasures == nil {
|
if record.Treasures == nil {
|
||||||
record.Treasures = []*Treasure{treasure}
|
record.Treasures = []*Treasure{treasure}
|
||||||
continue
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
record.Treasures = append(record.Treasures, treasure)
|
record.Treasures = append(record.Treasures, treasure)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TreasureClass[record.Name] = record
|
TreasureClass[record.Name] = record
|
||||||
}
|
}
|
||||||
|
@ -42,8 +42,8 @@ type UniqueItemRecord struct {
|
|||||||
|
|
||||||
// UniqueItemProperty is describes a property of a unique item
|
// UniqueItemProperty is describes a property of a unique item
|
||||||
type UniqueItemProperty struct {
|
type UniqueItemProperty struct {
|
||||||
Property string
|
Code string
|
||||||
Parameter d2common.CalcString // depending on the property, this may be an int (usually), or a string
|
Parameter string // depending on the property, this may be an int (usually), or a string
|
||||||
Min int
|
Min int
|
||||||
Max int
|
Max int
|
||||||
}
|
}
|
||||||
@ -105,8 +105,8 @@ func createUniqueItemRecord(r []string) UniqueItemRecord {
|
|||||||
|
|
||||||
func createUniqueItemProperty(r *[]string, inc func() int) UniqueItemProperty {
|
func createUniqueItemProperty(r *[]string, inc func() int) UniqueItemProperty {
|
||||||
result := UniqueItemProperty{
|
result := UniqueItemProperty{
|
||||||
Property: (*r)[inc()],
|
Code: (*r)[inc()],
|
||||||
Parameter: d2common.CalcString((*r)[inc()]),
|
Parameter: (*r)[inc()],
|
||||||
Min: d2common.StringToInt(d2common.EmptyToZero((*r)[inc()])),
|
Min: d2common.StringToInt(d2common.EmptyToZero((*r)[inc()])),
|
||||||
Max: d2common.StringToInt(d2common.EmptyToZero((*r)[inc()])),
|
Max: d2common.StringToInt(d2common.EmptyToZero((*r)[inc()])),
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,8 @@ type EquippedSlot int
|
|||||||
|
|
||||||
// Equipped slot ID's
|
// Equipped slot ID's
|
||||||
const (
|
const (
|
||||||
EquippedSlotHead EquippedSlot = iota + 1
|
EquippedSlotNone EquippedSlot = iota
|
||||||
|
EquippedSlotHead
|
||||||
EquippedSlotTorso
|
EquippedSlotTorso
|
||||||
EquippedSlotLegs
|
EquippedSlotLegs
|
||||||
EquippedSlotRightArm
|
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,
|
// 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
|
// as well as set up the stat value number types, value combination types, and
|
||||||
// the value stringer functions used
|
// 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 {
|
if s.record == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//nolint:gomdn introducing a const for these would be worse
|
//nolint:gomdn introducing a const for these would be worse
|
||||||
switch s.record.DescFnID {
|
switch s.record.DescFnID {
|
||||||
|
case 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:
|
case 1:
|
||||||
// +31 to Strength
|
// +31 to Strength
|
||||||
// Replenish Life +20 || Drain Life -8
|
// Replenish Life +20 || Drain Life -8
|
||||||
@ -167,7 +174,7 @@ func (s *diablo2Stat) init(numbers ...float64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for idx := range numbers {
|
for idx := range numbers {
|
||||||
if idx > len(s.values) {
|
if idx > len(s.values)-1 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,6 +229,10 @@ func (s *diablo2Stat) Clone() d2stats.Stat {
|
|||||||
dstVal.SetFloat(srcVal.Float())
|
dstVal.SetFloat(srcVal.Float())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(clone.values) < len(s.values) {
|
||||||
|
clone.values = make([]d2stats.StatValue, len(s.values))
|
||||||
|
}
|
||||||
|
|
||||||
clone.values[idx] = dstVal
|
clone.values[idx] = dstVal
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -561,6 +572,12 @@ func (s *diablo2Stat) descFn24() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *diablo2Stat) descFn27() 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]
|
amount, skill, hero := s.values[0], s.values[1], s.values[2]
|
||||||
|
|
||||||
return fmt.Sprintf(fourComponentStr, amount, "to", skill, hero)
|
return fmt.Sprintf(fourComponentStr, amount, "to", skill, hero)
|
||||||
|
@ -13,15 +13,15 @@ const (
|
|||||||
monsterNotFound = "{Monster not found!}"
|
monsterNotFound = "{Monster not found!}"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getHeroMap() map[int]d2enum.Hero {
|
func getHeroMap() []d2enum.Hero {
|
||||||
return map[int]d2enum.Hero{
|
return []d2enum.Hero{
|
||||||
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
|
d2enum.HeroAmazon,
|
||||||
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
|
d2enum.HeroSorceress,
|
||||||
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
|
d2enum.HeroNecromancer,
|
||||||
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
|
d2enum.HeroPaladin,
|
||||||
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
|
d2enum.HeroBarbarian,
|
||||||
int(d2enum.HeroDruid): d2enum.HeroDruid,
|
d2enum.HeroDruid,
|
||||||
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
|
d2enum.HeroAssassin,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user