mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-11-10 14:26:15 -05:00
bfd3f1046d
* 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
254 lines
6.4 KiB
Go
254 lines
6.4 KiB
Go
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
|
|
}
|