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:
lord 2020-07-30 07:14:15 -07:00 committed by GitHub
parent 4dc0aa0f48
commit bfd3f1046d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 2329 additions and 216 deletions

View File

@ -270,6 +270,8 @@ func (a *App) loadDataDict() error {
entry.loader(data)
}
d2datadict.LoadItemEquivalencies() // depends on ItemCommon and ItemTypes
return nil
}

View File

@ -9,9 +9,11 @@ import (
)
// MagicPrefix stores all of the magic prefix records
var MagicPrefix []*ItemAffixCommonRecord //nolint:gochecknoglobals // Currently global by design
var MagicPrefix map[string]*ItemAffixCommonRecord //nolint:gochecknoglobals // Currently global by
// design
// MagicSuffix stores all of the magic suffix records
var MagicSuffix []*ItemAffixCommonRecord //nolint:gochecknoglobals // Currently global by design
var MagicSuffix map[string]*ItemAffixCommonRecord //nolint:gochecknoglobals // Currently global by
// design
// LoadMagicPrefix loads MagicPrefix.txt
func LoadMagicPrefix(file []byte) {
@ -48,7 +50,11 @@ func getAffixString(t1 d2enum.ItemAffixSuperType, t2 d2enum.ItemAffixSubType) st
return name
}
func loadDictionary(file []byte, superType d2enum.ItemAffixSuperType, subType d2enum.ItemAffixSubType) []*ItemAffixCommonRecord {
func loadDictionary(
file []byte,
superType d2enum.ItemAffixSuperType,
subType d2enum.ItemAffixSubType,
) map[string]*ItemAffixCommonRecord {
d := d2common.LoadDataDictionary(file)
records := createItemAffixRecords(d, superType, subType)
name := getAffixString(superType, subType)
@ -57,8 +63,12 @@ func loadDictionary(file []byte, superType d2enum.ItemAffixSuperType, subType d2
return records
}
func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAffixSuperType, subType d2enum.ItemAffixSubType) []*ItemAffixCommonRecord {
records := make([]*ItemAffixCommonRecord, 0)
func createItemAffixRecords(
d *d2common.DataDictionary,
superType d2enum.ItemAffixSuperType,
subType d2enum.ItemAffixSubType,
) map[string]*ItemAffixCommonRecord {
records := make(map[string]*ItemAffixCommonRecord)
for d.Next() {
affix := &ItemAffixCommonRecord{
@ -82,7 +92,7 @@ func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAff
PriceScale: d.Number("multiply"),
}
// modifiers (Property references with parameters to be eval'd)
// modifiers (Code references with parameters to be eval'd)
for i := 1; i <= 3; i++ {
codeKey := fmt.Sprintf("mod%dcode", i)
paramKey := fmt.Sprintf("mod%dparam", i)
@ -125,7 +135,7 @@ func createItemAffixRecords(d *d2common.DataDictionary, superType d2enum.ItemAff
group := ItemAffixGroups[affix.GroupID]
group.addMember(affix)
records = append(records, affix)
records[affix.Name] = affix
}
if d.Err != nil {
panic(d.Err)

View File

@ -177,6 +177,10 @@ func LoadCommonItems(file []byte, source d2enum.InventoryItemType) map[string]*I
}
rec := createCommonItemRecord(line, mapping, source)
if rec.Name == "Expansion" {
continue
}
items[rec.Code] = &rec
CommonItems[rec.Code] = &rec
}
@ -392,3 +396,4 @@ func createItemUsageStats(r *[]string, mapping map[string]int) [3]ItemUsageStat
return result
}

View File

@ -246,7 +246,7 @@ func LoadItemTypes(file []byte) {
StorePage: d.String("StorePage"),
}
ItemTypes[itemType.Name] = itemType
ItemTypes[itemType.Code] = itemType
}
if d.Err != nil {
@ -255,3 +255,101 @@ func LoadItemTypes(file []byte) {
log.Printf("Loaded %d ItemType records", len(ItemTypes))
}
// ItemEquivalenciesByTypeCode describes item equivalencies for ItemTypes
var ItemEquivalenciesByTypeCode map[string][]*ItemCommonRecord
// LoadItemEquivalencies loads a map of ItemType string codes to slices of ItemCommonRecord pointers
func LoadItemEquivalencies() {
ItemEquivalenciesByTypeCode = make(map[string][]*ItemCommonRecord)
makeEmptyEquivalencyMaps()
for icrCode := range CommonItems {
commonItem := CommonItems[icrCode]
updateEquivalencies(commonItem, ItemTypes[commonItem.Type], nil)
if commonItem.Type2 != "" { // some items (like gems) have a secondary type
updateEquivalencies(commonItem, ItemTypes[commonItem.Type2], nil)
}
}
}
func makeEmptyEquivalencyMaps() {
for typeCode := range ItemTypes {
code := []string{
typeCode,
ItemTypes[typeCode].Equiv1,
ItemTypes[typeCode].Equiv2,
}
for _, str := range code {
if str == "" {
continue
}
if ItemEquivalenciesByTypeCode[str] == nil {
ItemEquivalenciesByTypeCode[str] = make([]*ItemCommonRecord, 0)
}
}
}
}
func updateEquivalencies(icr *ItemCommonRecord, itemType *ItemTypeRecord, checked []string) {
if itemType.Code == "" {
return
}
if checked == nil {
checked = make([]string, 0)
}
checked = append(checked, itemType.Code)
if !itemEquivPresent(icr, ItemEquivalenciesByTypeCode[itemType.Code]) {
ItemEquivalenciesByTypeCode[itemType.Code] = append(ItemEquivalenciesByTypeCode[itemType.Code], icr)
}
if itemType.Equiv1 != "" {
updateEquivalencies(icr, ItemTypes[itemType.Equiv1], checked)
}
if itemType.Equiv2 != "" {
updateEquivalencies(icr, ItemTypes[itemType.Equiv2], checked)
}
}
func itemEquivPresent(icr *ItemCommonRecord, list []*ItemCommonRecord) bool {
for idx := range list {
if list[idx] == icr {
return true
}
}
return false
}
var itemCommonTypeLookup map[*ItemCommonRecord][]string
func FindEquivalentTypesByItemCommonRecord(icr *ItemCommonRecord) []string {
if itemCommonTypeLookup == nil {
itemCommonTypeLookup = make(map[*ItemCommonRecord][]string)
}
// the first lookup generates the lookup table entry, next time will just use the table
if itemCommonTypeLookup[icr] == nil {
itemCommonTypeLookup[icr] = make([]string, 0)
for code := range ItemEquivalenciesByTypeCode {
icrList := ItemEquivalenciesByTypeCode[code]
for idx := range icrList {
if icr == icrList[idx] {
itemCommonTypeLookup[icr] = append(itemCommonTypeLookup[icr], code)
break
}
}
}
}
return itemCommonTypeLookup[icr]
}

View File

@ -1,24 +1,23 @@
package d2datadict
import (
"log"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
)
type stat struct {
SetID int
Value int
type PropertyStatRecord struct {
SetID int
Value int
FunctionID int
StatCode string
StatCode string
}
// PropertyRecord is a representation of a single row of gems.txt
// it describes the properties of socketable items
// PropertyRecord is a representation of a single row of properties.txt
type PropertyRecord struct {
Code string
Code string
Active string
Stats [7]*stat
Stats [7]*PropertyStatRecord
}
// Properties stores all of the PropertyRecords
@ -32,50 +31,50 @@ func LoadProperties(file []byte) {
d := d2common.LoadDataDictionary(file)
for d.Next() {
prop := &PropertyRecord{
Code: d.String("code"),
Code: d.String("code"),
Active: d.String("*done"),
Stats: [7]*stat{
Stats: [7]*PropertyStatRecord{
{
SetID: d.Number("set1"),
Value: d.Number("val1"),
SetID: d.Number("set1"),
Value: d.Number("val1"),
FunctionID: d.Number("func1"),
StatCode: d.String("stat1"),
StatCode: d.String("stat1"),
},
{
SetID: d.Number("set2"),
Value: d.Number("val2"),
SetID: d.Number("set2"),
Value: d.Number("val2"),
FunctionID: d.Number("func2"),
StatCode: d.String("stat2"),
StatCode: d.String("stat2"),
},
{
SetID: d.Number("set3"),
Value: d.Number("val3"),
SetID: d.Number("set3"),
Value: d.Number("val3"),
FunctionID: d.Number("func3"),
StatCode: d.String("stat3"),
StatCode: d.String("stat3"),
},
{
SetID: d.Number("set4"),
Value: d.Number("val4"),
SetID: d.Number("set4"),
Value: d.Number("val4"),
FunctionID: d.Number("func4"),
StatCode: d.String("stat4"),
StatCode: d.String("stat4"),
},
{
SetID: d.Number("set5"),
Value: d.Number("val5"),
SetID: d.Number("set5"),
Value: d.Number("val5"),
FunctionID: d.Number("func5"),
StatCode: d.String("stat5"),
StatCode: d.String("stat5"),
},
{
SetID: d.Number("set6"),
Value: d.Number("val6"),
SetID: d.Number("set6"),
Value: d.Number("val6"),
FunctionID: d.Number("func6"),
StatCode: d.String("stat6"),
StatCode: d.String("stat6"),
},
{
SetID: d.Number("set7"),
Value: d.Number("val7"),
SetID: d.Number("set7"),
Value: d.Number("val7"),
FunctionID: d.Number("func7"),
StatCode: d.String("stat7"),
StatCode: d.String("stat7"),
},
},
}
@ -88,4 +87,3 @@ func LoadProperties(file []byte) {
log.Printf("Loaded %d Property records", len(Properties))
}

View File

@ -1,15 +1,31 @@
package d2datadict
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"log"
)
const (
numPropertiesOnSetItem = 9
numBonusPropertiesOnSetItem = 5
bonusToken1 = "a"
bonusToken2 = "b"
propCodeFmt = "prop%d"
propParamFmt = "par%d"
propMinFmt = "min%d"
propMaxFmt = "max%d"
bonusCodeFmt = "aprop%d%s"
bonusParamFmt = "apar%d%s"
bonusMinFmt = "amin%d%s"
bonusMaxFmt = "amax%d%s"
)
// SetItemRecord represents a set item
type SetItemRecord struct {
// StringTableKey (index)
// SetItemKey (index)
// string key to item's name in a .tbl file
StringTableKey string
SetItemKey string
// SetKey (set)
// string key to the index field in Sets.txt - the set the item is a part of.
@ -90,63 +106,37 @@ type SetItemRecord struct {
// on a set item. See the appendix for further details about this field's effects.
AddFn int
// Prop (prop1 to prop9)
// An ID pointer of a property from Properties.txt,
// these columns control each of the nine different fixed (
// blue) modifiers a set item can grant you at most.
Prop [9]string
// Properties are a propert code, parameter, min, max for generating an item propert
Properties [numPropertiesOnSetItem]*SetItemProperty
// Par (par1 to par9)
// The parameter passed on to the associated property, this is used to pass skill IDs, state IDs,
// monster IDs, montype IDs and the like on to the properties that require them,
// these fields support calculations.
Par [9]int
// SetPropertiesLevel1 is the first version of bonus properties for the set
SetPropertiesLevel1 [numBonusPropertiesOnSetItem]*SetItemProperty
// Min, Max (min1 to min9, max1 to max9)
// Minimum value to assign to the associated (blue) property.
// Certain properties have special interpretations based on stat encoding (e.g.
// chance-to-cast and charged skills). See the File Guide for Properties.txt and ItemStatCost.
// txt for further details.
Min [9]int
Max [9]int
// SetPropertiesLevel2 is the second version of bonus properties for the set
SetPropertiesLevel2 [numBonusPropertiesOnSetItem]*SetItemProperty
}
// APropA, APropB (aprop1a,aprop1b to aprop5a,aprop5b)
// An ID pointer of a property from Properties.txt,
// these columns control each of the five pairs of different variable (
// green) modifiers a set item can grant you at most.
APropA [5]string
APropB [5]string
// AParA, AParB (apar1a,apar1b to apar5a,apar5b)
// The parameter passed on to the associated property, this is used to pass skill IDs, state IDs,
// monster IDs, montype IDs and the like on to the properties that require them,
// these fields support calculations.
AParA [5]int
AParB [5]int
// AMinA, AMinB, AMaxA, AMaxB (amin1a,amin1b to amin5a,amin5b)
// Minimum value to assign to the associated property.
// Certain properties have special interpretations based on stat encoding (e.g.
// chance-to-cast and charged skills). See the File Guide for Properties.txt and ItemStatCost.
// txt for further details.
AMinA [5]int
AMinB [5]int
AMaxA [5]int
AMaxB [5]int
// SetItemProperty is describes a property of a set item
type SetItemProperty struct {
Code string
Parameter string // depending on the property, this may be an int (usually), or a string
Min int
Max int
}
// SetItems holds all of the SetItemRecords
var SetItems []*SetItemRecord //nolint:gochecknoglobals // Currently global by design, only written once
var SetItems map[string]*SetItemRecord //nolint:gochecknoglobals // Currently global by design,
// only written once
// LoadSetItems loads all of the SetItemRecords from SetItems.txt
func LoadSetItems(file []byte) {
SetItems = make([]*SetItemRecord, 0)
SetItems = make(map[string]*SetItemRecord)
d := d2common.LoadDataDictionary(file)
for d.Next() {
record := &SetItemRecord{
StringTableKey: d.String("index"),
SetItemKey: d.String("index"),
SetKey: d.String("set"),
ItemCode: d.String("item"),
Rarity: d.Number("rarity"),
@ -162,109 +152,47 @@ func LoadSetItems(file []byte) {
CostMult: d.Number("cost mult"),
CostAdd: d.Number("cost add"),
AddFn: d.Number("add func"),
Prop: [9]string{
d.String("prop1"),
d.String("prop2"),
d.String("prop3"),
d.String("prop4"),
d.String("prop5"),
d.String("prop6"),
d.String("prop7"),
d.String("prop8"),
d.String("prop9"),
},
Par: [9]int{
d.Number("par1"),
d.Number("par2"),
d.Number("par3"),
d.Number("par4"),
d.Number("par5"),
d.Number("par6"),
d.Number("par7"),
d.Number("par8"),
d.Number("par9"),
},
Min: [9]int{
d.Number("min1"),
d.Number("min2"),
d.Number("min3"),
d.Number("min4"),
d.Number("min5"),
d.Number("min6"),
d.Number("min7"),
d.Number("min8"),
d.Number("min9"),
},
Max: [9]int{
d.Number("max1"),
d.Number("max2"),
d.Number("max3"),
d.Number("max4"),
d.Number("max5"),
d.Number("max6"),
d.Number("max7"),
d.Number("max8"),
d.Number("max9"),
},
APropA: [5]string{
d.String("aprop1a"),
d.String("aprop2a"),
d.String("aprop3a"),
d.String("aprop4a"),
d.String("aprop5a"),
},
APropB: [5]string{
d.String("aprop1b"),
d.String("aprop2b"),
d.String("aprop3b"),
d.String("aprop4b"),
d.String("aprop5b"),
},
AParA: [5]int{
d.Number("apar1a"),
d.Number("apar2a"),
d.Number("apar3a"),
d.Number("apar4a"),
d.Number("apar5a"),
},
AParB: [5]int{
d.Number("apar1b"),
d.Number("apar2b"),
d.Number("apar3b"),
d.Number("apar4b"),
d.Number("apar5b"),
},
AMinA: [5]int{
d.Number("amin1a"),
d.Number("amin2a"),
d.Number("amin3a"),
d.Number("amin4a"),
d.Number("amin5a"),
},
AMinB: [5]int{
d.Number("amin1b"),
d.Number("amin2b"),
d.Number("amin3b"),
d.Number("amin4b"),
d.Number("amin5b"),
},
AMaxA: [5]int{
d.Number("amax1a"),
d.Number("amax2a"),
d.Number("amax3a"),
d.Number("amax4a"),
d.Number("amax5a"),
},
AMaxB: [5]int{
d.Number("amax1b"),
d.Number("amax2b"),
d.Number("amax3b"),
d.Number("amax4b"),
d.Number("amax5b"),
},
}
SetItems = append(SetItems, record)
// normal properties
props := [numPropertiesOnSetItem]*SetItemProperty{}
for idx := 0; idx < numPropertiesOnSetItem; idx++ {
num := idx + 1
props[idx] = &SetItemProperty{
d.String(fmt.Sprintf(propCodeFmt, num)),
d.String(fmt.Sprintf(propParamFmt, num)),
d.Number(fmt.Sprintf(propMinFmt, num)),
d.Number(fmt.Sprintf(propMaxFmt, num)),
}
}
// set bonus properties
bonus1 := [numBonusPropertiesOnSetItem]*SetItemProperty{}
bonus2 := [numBonusPropertiesOnSetItem]*SetItemProperty{}
for idx := 0; idx < numBonusPropertiesOnSetItem; idx++ {
num := idx + 1
bonus1[idx] = &SetItemProperty{
d.String(fmt.Sprintf(bonusCodeFmt, num, bonusToken1)),
d.String(fmt.Sprintf(bonusParamFmt, num, bonusToken1)),
d.Number(fmt.Sprintf(bonusMinFmt, num,bonusToken1)),
d.Number(fmt.Sprintf(bonusMaxFmt, num, bonusToken1)),
}
bonus2[idx] = &SetItemProperty{
d.String(fmt.Sprintf(bonusCodeFmt, num, bonusToken2)),
d.String(fmt.Sprintf(bonusParamFmt, num, bonusToken2)),
d.Number(fmt.Sprintf(bonusMinFmt, num,bonusToken2)),
d.Number(fmt.Sprintf(bonusMaxFmt, num, bonusToken2)),
}
}
record.Properties = props
SetItems[record.SetItemKey] = record
}
if d.Err != nil {

View File

@ -2,15 +2,45 @@ package d2datadict
import (
"fmt"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common"
"log"
)
const (
numTreasures = 10
treasureItemFmt = "Item%d"
treasureProbFmt = "Prob%d"
maxTreasuresPerRecord = 10
treasureItemFmt = "Item%d"
treasureProbFmt = "Prob%d"
)
// TreasureDropType indicates the drop type of the treasure
type TreasureDropType int
const (
// TreasureNone is default bad case, but nothing should have this
TreasureNone TreasureDropType = iota
// TreasureGold indicates that the treasure drop type is for gold
TreasureGold
// indicates that the drop type resolves directly to an ItemCommonRecord
TreasureWeapon
TreasureArmor
TreasureMisc
// indicates that the code is for a dynamic item record, because the treasure code has
// and item level appended to it. this is for things like `armo63` or `weap24` which does not
// explicitly have an item record that matches this code, but we need to resolve this
TreasureWeaponDynamic
TreasureArmorDynamic
TreasureMiscDynamic
)
const (
GoldMultDropCodeStr string = "gld,mul="
GoldDropCodeStr = "gld"
WeaponDropCodeStr = "weap"
ArmorDropCodeStr = "armo"
MiscDropCodeStr = "misc"
)
// TreasureClassRecord represents a rule for item drops in diablo 2
@ -28,12 +58,13 @@ type TreasureClassRecord struct {
}
// Treasure describes a treasure to drop
// the key is either a reference to an item, or to another treasure class
// the Name is either a reference to an item, or to another treasure class
type Treasure struct {
Name string
Code string
Probability int
}
// TreasureClass contains all of the TreasureClassRecords
var TreasureClass map[string]*TreasureClassRecord //nolint:gochecknoglobals // Currently global by design
// LoadTreasureClassRecords loads treasure class records from TreasureClassEx.txt
@ -56,7 +87,11 @@ func LoadTreasureClassRecords(file []byte) {
FreqNoDrop: d.Number("NoDrop"),
}
for treasureIdx := 0; treasureIdx < numTreasures; treasureIdx++ {
if record.Name == "" {
continue
}
for treasureIdx := 0; treasureIdx < maxTreasuresPerRecord; treasureIdx++ {
treasureColumnKey := fmt.Sprintf(treasureItemFmt, treasureIdx+1)
probColumnKey := fmt.Sprintf(treasureProbFmt, treasureIdx+1)
@ -68,16 +103,15 @@ func LoadTreasureClassRecords(file []byte) {
prob := d.Number(probColumnKey)
treasure := &Treasure{
Name: treasureName,
Code: treasureName,
Probability: prob,
}
if record.Treasures == nil {
record.Treasures = []*Treasure{treasure}
continue
} else {
record.Treasures = append(record.Treasures, treasure)
}
record.Treasures = append(record.Treasures, treasure)
}
TreasureClass[record.Name] = record

View File

@ -42,8 +42,8 @@ type UniqueItemRecord struct {
// UniqueItemProperty is describes a property of a unique item
type UniqueItemProperty struct {
Property string
Parameter d2common.CalcString // depending on the property, this may be an int (usually), or a string
Code string
Parameter string // depending on the property, this may be an int (usually), or a string
Min int
Max int
}
@ -105,8 +105,8 @@ func createUniqueItemRecord(r []string) UniqueItemRecord {
func createUniqueItemProperty(r *[]string, inc func() int) UniqueItemProperty {
result := UniqueItemProperty{
Property: (*r)[inc()],
Parameter: d2common.CalcString((*r)[inc()]),
Code: (*r)[inc()],
Parameter: (*r)[inc()],
Min: d2common.StringToInt(d2common.EmptyToZero((*r)[inc()])),
Max: d2common.StringToInt(d2common.EmptyToZero((*r)[inc()])),
}

View File

@ -5,7 +5,8 @@ type EquippedSlot int
// Equipped slot ID's
const (
EquippedSlotHead EquippedSlot = iota + 1
EquippedSlotNone EquippedSlot = iota
EquippedSlotHead
EquippedSlotTorso
EquippedSlotLegs
EquippedSlotRightArm

12
d2core/d2item/context.go Normal file
View 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
}

View 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()
}

View File

@ -0,0 +1,3 @@
// Package Item provides the Diablo 2 implementation of items for
// the OpenDiablo2 interfaces
package diablo2item

View 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
}

View 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
}

View 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
}

View 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
View File

@ -0,0 +1,3 @@
// Package d2item provides a generic interface for what the OpenDiablo2
// engine considers to be an item.
package d2item

View File

@ -0,0 +1,6 @@
package d2item
type Equipper interface {
EquippedItems() []Item
CarriedItems() []Item
}

11
d2core/d2item/item.go Normal file
View 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
}

View File

@ -50,13 +50,20 @@ type diablo2Stat struct {
// depending on the stat record, sets up the proper number of values,
// as well as set up the stat value number types, value combination types, and
// the value stringer functions used
func (s *diablo2Stat) init(numbers ...float64) {
func (s *diablo2Stat) init(numbers ...float64) {//nolint:funlen doesn't make sense to split
if s.record == nil {
return
}
//nolint:gomdn introducing a const for these would be worse
switch s.record.DescFnID {
case 0:
// special case for poisonlength, or other stats, which have a
// 0-value descfnID field but need to store values
s.values = make([]d2stats.StatValue, len(numbers))
for idx := range s.values {
s.values[idx] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
}
case 1:
// +31 to Strength
// Replenish Life +20 || Drain Life -8
@ -167,7 +174,7 @@ func (s *diablo2Stat) init(numbers ...float64) {
}
for idx := range numbers {
if idx > len(s.values) {
if idx > len(s.values)-1 {
break
}
@ -222,6 +229,10 @@ func (s *diablo2Stat) Clone() d2stats.Stat {
dstVal.SetFloat(srcVal.Float())
}
if len(clone.values) < len(s.values) {
clone.values = make([]d2stats.StatValue, len(s.values))
}
clone.values[idx] = dstVal
}
@ -561,6 +572,12 @@ func (s *diablo2Stat) descFn24() string {
}
func (s *diablo2Stat) descFn27() string {
// property "skill-rand" will try to make an instance with an invalid hero index
// in this case, we use descfn 28
if s.values[2].Int() == -1 {
return s.descFn28()
}
amount, skill, hero := s.values[0], s.values[1], s.values[2]
return fmt.Sprintf(fourComponentStr, amount, "to", skill, hero)

View File

@ -13,15 +13,15 @@ const (
monsterNotFound = "{Monster not found!}"
)
func getHeroMap() map[int]d2enum.Hero {
return map[int]d2enum.Hero{
int(d2enum.HeroAmazon): d2enum.HeroAmazon,
int(d2enum.HeroSorceress): d2enum.HeroSorceress,
int(d2enum.HeroNecromancer): d2enum.HeroNecromancer,
int(d2enum.HeroPaladin): d2enum.HeroPaladin,
int(d2enum.HeroBarbarian): d2enum.HeroBarbarian,
int(d2enum.HeroDruid): d2enum.HeroDruid,
int(d2enum.HeroAssassin): d2enum.HeroAssassin,
func getHeroMap() []d2enum.Hero {
return []d2enum.Hero{
d2enum.HeroAmazon,
d2enum.HeroSorceress,
d2enum.HeroNecromancer,
d2enum.HeroPaladin,
d2enum.HeroBarbarian,
d2enum.HeroDruid,
d2enum.HeroAssassin,
}
}